Skip to content

Declared actions: generate the envelope of a domain verb, keep Do as plain Go #18

Description

@jryannel

From §13.4 of the adoption evaluation (docs/review-adoption-existing-app.md, appendix lands with #15). Rank 3 of 6 — the biggest single win, and the one with a trap.

The gap

The evaluated application has 26 distinct POST /{id}/<verb> routes plus ~20 collection-level verbs; in its task handlers the verbs total 780 LOC against 464 for all of CRUD — the part sqlb does not generate is larger than the part it does. Every verb opens the same way: ~30 lines of parse id → org-scoped fetch → 404 → decode optional body before any domain logic. Across ~46 endpoints, roughly 1,400 lines of pure envelope — hand-written four times over (handler, OpenAPI, TS client, CLI).

The proposal

schema.Action("complete", schema.POST, "/{id}/complete").
    Body[CompleteTaskInput]().
    Writes("status", "closed_at", "custom_field_values").
    Do(completeTask)     // func(ctx, *Task, CompleteTaskInput) error

Generate the envelope only: route, body type, id parse, scoped fetch, transaction, response, OpenAPI operation, TS client function + invalidation key, CLI subcommand. The Do stays hand-written Go.

The trap

docs/vision.md names the failure mode itself: "if generated handlers get copied out and edited by hand, the seams are in the wrong place." Domain verbs are where logic is most idiosyncratic, so an action DSL that tries to express the transition will be fought and ejected. The design above does not — it generates the envelope and calls a plain Go func, the seam BeforeCreate already uses. That distinction is the difference between this working and it being the thing people leave.

Design note: actions inherit ADR-0030 for free

The envelope's scoped fetch goes through BeforeQuery, so a generated action on a Scoped model inherits the mount-time obligation machinery from ADR-0030. Today, hand-written verb handlers are exactly the places where the org-scoped fetch is remembered by hand — the failure class ADR-0030 just closed for CRUD. An Action whose fetch is generated extends that closure to the ~60% of routes where the risk actually lives, which the evaluation's drift measurement (§12.4) identifies as where the cost is.

Effort / effect

High / route coverage ~40% → ~90% — and, more importantly, the verbs enter the client emitters, which is where the measured drift lives.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions