Skip to content

Latest commit

 

History

History
151 lines (105 loc) · 6.65 KB

File metadata and controls

151 lines (105 loc) · 6.65 KB

Record Hooks

Record hooks let compiled app code extend dygo Record lifecycle behavior.

Hooks are Go code. dygo does not load Go files dynamically at runtime. A project runner must import app hook packages and pass their registrars to the public runtime entrypoint.

Entity Bundle Convention

Hook source files live inside Entity bundles as hooks.go.

apps/crm/
  app.yml
  entities/
    lead/
      lead.entity.yml
      hooks.go

Collections do not get standalone hook scaffolds by default. Parent Entity hooks own collection row behavior.

Generate Hooks

Create the hook file and project runner wiring with:

dygo generate hook crm/lead

The command expects <app>/<entity>, where <app> is the app manifest name and <entity> is an Entity in that same app. It creates entities/<entity>/hooks.go when missing and creates or updates the project-local cmd/dygo/main.go runner.

After generation, edit the Entity hook file and run dygo through the project runner:

dygo dev
dygo fixture apply

If cmd/dygo/main.go already exists and was not generated by dygo, the command refuses to overwrite it and prints the manual wiring needed.

Registration Shape

An Entity hook file exposes one Register function and registers hooks for its matching Entity:

Hooks register by app-scoped Entity identity, not by route slug. That keeps hooks stable even when an Entity uses an explicit route.slug.

package hooks

import (
	"context"

	"github.com/hapyco/dygo/pkg/dygo"
)

func Register(registry dygo.RecordHookRegistry) error {
	return registry.RegisterEntity("sales", "lead", dygo.RecordBeforeCreate, "normalize-lead", normalizeLead)
}

func normalizeLead(ctx context.Context, hook dygo.RecordHook) error {
	return nil
}

The generated project runner imports each Entity hook package and calls dygo through pkg/dygo/runtime:

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"

	crmleadhooks "example.com/my-project/apps/crm/entities/lead"
	"github.com/hapyco/dygo/pkg/dygo"
	dygoruntime "github.com/hapyco/dygo/pkg/dygo/runtime"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	err := dygoruntime.Run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr, dygoruntime.Options{
		RecordHooks: []dygo.RecordHookRegistrar{crmleadhooks.Register},
	})
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Lifecycle Contract

All Record hooks run synchronously inside the current Record transaction. If a hook returns an error, dygo aborts the current operation and rolls back the transaction.

Hooks can read and write app data through hook.Records. Those calls use dygo's metadata-backed Record API and participate in the same transaction as the hook. hook.Records create, update, and delete calls do not re-enter app Record hooks for the target Entity; this keeps hook behavior bounded and avoids accidental recursive hook loops. Framework-owned hooks such as Activity history can still run.

hook.Records addresses Records by stable app-scoped Entity identity:

record, err := hook.Records.Get(ctx, "sales", "lead", 42)
created, err := hook.Records.Create(ctx, "sales", "activity", input)
updated, err := hook.Records.Update(ctx, hook.AppName, hook.Entity, hook.RecordID, input)

The app name is the app manifest name, and the Entity is the file-derived Entity key. Do not pass the route slug to hook.Records. hook.RouteSlug is exposed for URLs, metadata links, and display behavior when an Entity uses an explicit route.slug.

hook.Records is trusted server-side app code. It does not run the HTTP route permission checks used by /api/v1/records/{entity}. Use it for app-owned business behavior that must run with the hook transaction.

Examples name the hook runtime value hook. It is a local function parameter, not a global package variable. Keeping it local makes transaction, actor, permission, and hook state explicit.

Hook context fields are ordinary Go values, but only some context mutations change the current target operation:

Event Target operation effect Typical use
before-validate Mutating hook.Input changes the input dygo validates. Normalize raw input, fill required values, reject early.
validate Mutating context fields does not change the target write. Returning an error rejects the operation. Cross-field validation, permission/business checks, transactional lookups.
before-create Mutating hook.Input changes the row dygo inserts. dygo validates the final input before SQL is built. Compute derived fields, set defaults, create related data.
after-create Mutating context fields does not rewrite the created record. hook.Records can still write related data in the same transaction. Create follow-up records, update aggregates, observe the created snapshot.
before-update Mutating hook.Input changes the row dygo updates. dygo validates the final input before SQL is built. Compute final patch values, enforce state transitions, write related data.
after-update Mutating context fields does not rewrite the updated record or diff. hook.Records can still write related data in the same transaction. React to changes, update aggregates, write follow-up records.
before-delete There is no target input to mutate. Returning an error prevents the delete; hook.Records can write related data in the same transaction. Enforce delete rules, clean up related data, archive before delete.
after-delete Mutating context fields does not rewrite the deleted snapshot. hook.Records can still write related data in the same transaction. Record follow-up data or update aggregates after the delete succeeds.

Use before-validate, before-create, and before-update when you intend to mutate the current target input. Use hook.Records when you intend to mutate any other Record.

Avoid external side effects in v1 hooks unless they are safe to repeat or compensate manually. dygo does not have after-commit hooks yet, so a database rollback can still happen after an email, webhook, or external API call has already been sent.

Runtime Behavior

App hooks run after framework global hooks for the same Record event.

dygo dev, dygo serve, and dygo fixture apply use the compiled hook registry when run through a project binary built with pkg/dygo/runtime.

The stock cmd/dygo binary only includes framework hooks.

V1 app hooks are synchronous and transactional.

Coming soon:

  • actor-scoped and permission-scoped SDK access modes
  • dynamic loading
  • hook priority
  • framework hook override
  • scripting hooks
  • after-commit hooks
  • rollback hooks