diff --git a/agent-context/skills/metaobjects-authoring/SKILL.md b/agent-context/skills/metaobjects-authoring/SKILL.md index 5bf36d705..917fc78cd 100644 --- a/agent-context/skills/metaobjects-authoring/SKILL.md +++ b/agent-context/skills/metaobjects-authoring/SKILL.md @@ -351,7 +351,10 @@ child traversal, package only on the root segment) and/or carry `origin.*` children (`passthrough` / `aggregate` / `collection`) declaring assembly; its identity passes through via `extends` (`identity.primary: { name: id, extends: "Author.id" }`); it is read-only by construction and the declared field set IS -the exposure (fail-closed). +the exposure (fail-closed). Give it a read-only `source.rdb` `@kind: view` +child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projection +detection + view DDL off that read-only source, so without it `meta gen` emits +nothing for the projection. ## Abstracts + `extends` (deferred resolution) + `overlay` diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index 0e6c2c5c1..7cc889316 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -61,6 +61,40 @@ entity never emits instance/write artifacts regardless. Per-entity opt-outs exist (e.g. skipping client-side artifacts for a given entity) and are set as attributes on the entity in metadata, not in code. +## You don't have to generate everything — pick your layers + +Codegen is **granular and à la carte, not all-or-nothing.** The most powerful +pattern when an app's API doesn't match generated CRUD: **generate the data layer, +hand-write only the API layer** — never abandon codegen wholesale and hand-write +the data access too. + +- **Generate the data layer, skip the routes.** Omit `routesFile()` from the + `generators` array (keep `entityFile()` + `queriesFile()` + `barrel()`): you get + the typed entity/table, schemas, and query/finder helpers, then write your own + routes by hand — *calling the generated queries*. Do this whenever the API shape + (custom paths, HTML responses, nested payloads) doesn't fit generated REST CRUD. +- **Mix generated and hand-written routes.** Even with custom paths, mount the + standard verbs with the runtime helpers and hand-write only the custom ones (see + the runtime skill's `mountCrudRoutes` / `mountRoute` / `expose`). You are + never forced into all-generated or all-hand-written. +- **Derived/aggregate data → declare a projection, then USE its generated query.** + Don't hand-write a join or an `AVG()`/`COUNT()`. Declare an `object.projection` + with `origin.aggregate` / `origin.passthrough` / `origin.collection` children + **and a read-only `source.rdb` `@kind: view` child** (codegen detects a + projection by that read-only source, not by the subtype alone — omit it and + nothing is generated). `meta gen` emits a read-only query for it (and + `meta migrate` its DB view), and you **call that generated query from your + route**. Declaring the projection is only half the win — *consuming* its + generated query is the other half. +- **Codegen is yours to extend.** A generated file carries the `@generated` header + and is a normal source file: copy it and customize the copy (three-way merge + preserves your edits on regen), or write your own `Generator` (the plugin + interface) and add it to the `generators` array for an artifact the built-ins + don't cover. + +`meta gen --list` prints every generator by stable name; the `generators` array in +`metaobjects.config.ts` is where you opt each one in or out. + ## Dialects Generated DB schema/DDL targets a SQL **dialect**: diff --git a/agent-context/skills/metaobjects-runtime-ui/references/typescript.md b/agent-context/skills/metaobjects-runtime-ui/references/typescript.md index 495307aa9..bed3e8f39 100644 --- a/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +++ b/agent-context/skills/metaobjects-runtime-ui/references/typescript.md @@ -90,3 +90,33 @@ The routes call `parseFilterParams` (from `@metaobjectsdev/runtime-ts/drizzle-fa to validate `?filter[..][..]=..&sort=..&limit=&offset=` against the generated `FilterAllowlist` / `SortAllowlist`, returning HTTP 400 on an unknown field or disallowed operator. + +### Granular routes — mount some, hand-write the rest (don't read `node_modules`) + +When the API doesn't match generated CRUD, you don't have to choose all-generated +or all-hand-written, and you never need to reverse-engineer the runtime package. +`@metaobjectsdev/runtime-ts/drizzle-fastify` exports the mount helpers the generated +routes are built from — call them directly: + +```ts +import { + mountCrudRoutes, mountGetRoute, mountListRoute, mountReadOnlyCrudRoutes, +} from "@metaobjectsdev/runtime-ts/drizzle-fastify"; +import { RecipeInsertSchema, RecipeUpdateSchema } from "./generated/Recipe.js"; + +// all five verbs: +mountCrudRoutes({ fastify: app, path: "/recipes", db, table: recipes, + insertSchema: RecipeInsertSchema, updateSchema: RecipeUpdateSchema }); + +mountCrudRoutes({ ...opts, expose: ["list", "get"] }); // only some verbs +mountReadOnlyCrudRoutes({ ...opts }); // list + get only +mountGetRoute({ ...opts }); // a single verb +``` + +`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }` +plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g. +`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default +/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the +custom routes** (HTML pages, nested resources, computed fields) — calling the +generated query helpers, and a projection's generated query for derived/aggregate +data. Generate the data layer; hand-write only what's genuinely custom. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index 86220b8b0..917fc78cd 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -15,6 +15,27 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a `package` on its root node. Files in the same `package` with the same object `name` are merged by the loader. +## Find the construct first — model it, don't hand-write it + +You do **not** know the full vocabulary from memory, and it is larger and more +powerful than the basics below. Before authoring anything non-trivial, and the +moment you reach for hand-written data logic, **search the live metamodel**: + +``` +meta types relationship # find by name +meta types --all aggregate # find by WHAT IT DOES (searches descriptions) +meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs +``` + +**The rule:** before you hand-write any data logic — a join, a foreign key, a +derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field +rule, a relationship between entities, a derived read model — run `meta types` and +check for a construct that **declares** it. If one exists, declare it instead. That +is the entire point of MetaObjects: declared metadata is generated, typed, and +regenerates on change; hand-written logic drifts and is the thing this tool exists +to eliminate. When you catch yourself writing a query, a validator, or an FK by +hand, stop and search the types first. + Two on-disk formats, one shape: - **Canonical JSON** — the on-disk interchange. Every node is a single-key map @@ -330,7 +351,10 @@ child traversal, package only on the root segment) and/or carry `origin.*` children (`passthrough` / `aggregate` / `collection`) declaring assembly; its identity passes through via `extends` (`identity.primary: { name: id, extends: "Author.id" }`); it is read-only by construction and the declared field set IS -the exposure (fail-closed). +the exposure (fail-closed). Give it a read-only `source.rdb` `@kind: view` +child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projection +detection + view DDL off that read-only source, so without it `meta gen` emits +nothing for the projection. ## Abstracts + `extends` (deferred resolution) + `overlay` diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0e6c2c5c1..7cc889316 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -61,6 +61,40 @@ entity never emits instance/write artifacts regardless. Per-entity opt-outs exist (e.g. skipping client-side artifacts for a given entity) and are set as attributes on the entity in metadata, not in code. +## You don't have to generate everything — pick your layers + +Codegen is **granular and à la carte, not all-or-nothing.** The most powerful +pattern when an app's API doesn't match generated CRUD: **generate the data layer, +hand-write only the API layer** — never abandon codegen wholesale and hand-write +the data access too. + +- **Generate the data layer, skip the routes.** Omit `routesFile()` from the + `generators` array (keep `entityFile()` + `queriesFile()` + `barrel()`): you get + the typed entity/table, schemas, and query/finder helpers, then write your own + routes by hand — *calling the generated queries*. Do this whenever the API shape + (custom paths, HTML responses, nested payloads) doesn't fit generated REST CRUD. +- **Mix generated and hand-written routes.** Even with custom paths, mount the + standard verbs with the runtime helpers and hand-write only the custom ones (see + the runtime skill's `mountCrudRoutes` / `mountRoute` / `expose`). You are + never forced into all-generated or all-hand-written. +- **Derived/aggregate data → declare a projection, then USE its generated query.** + Don't hand-write a join or an `AVG()`/`COUNT()`. Declare an `object.projection` + with `origin.aggregate` / `origin.passthrough` / `origin.collection` children + **and a read-only `source.rdb` `@kind: view` child** (codegen detects a + projection by that read-only source, not by the subtype alone — omit it and + nothing is generated). `meta gen` emits a read-only query for it (and + `meta migrate` its DB view), and you **call that generated query from your + route**. Declaring the projection is only half the win — *consuming* its + generated query is the other half. +- **Codegen is yours to extend.** A generated file carries the `@generated` header + and is a normal source file: copy it and customize the copy (three-way merge + preserves your edits on regen), or write your own `Generator` (the plugin + interface) and add it to the `generators` array for an artifact the built-ins + don't cover. + +`meta gen --list` prints every generator by stable name; the `generators` array in +`metaobjects.config.ts` is where you opt each one in or out. + ## Dialects Generated DB schema/DDL targets a SQL **dialect**: diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md index 495307aa9..bed3e8f39 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md @@ -90,3 +90,33 @@ The routes call `parseFilterParams` (from `@metaobjectsdev/runtime-ts/drizzle-fa to validate `?filter[..][..]=..&sort=..&limit=&offset=` against the generated `FilterAllowlist` / `SortAllowlist`, returning HTTP 400 on an unknown field or disallowed operator. + +### Granular routes — mount some, hand-write the rest (don't read `node_modules`) + +When the API doesn't match generated CRUD, you don't have to choose all-generated +or all-hand-written, and you never need to reverse-engineer the runtime package. +`@metaobjectsdev/runtime-ts/drizzle-fastify` exports the mount helpers the generated +routes are built from — call them directly: + +```ts +import { + mountCrudRoutes, mountGetRoute, mountListRoute, mountReadOnlyCrudRoutes, +} from "@metaobjectsdev/runtime-ts/drizzle-fastify"; +import { RecipeInsertSchema, RecipeUpdateSchema } from "./generated/Recipe.js"; + +// all five verbs: +mountCrudRoutes({ fastify: app, path: "/recipes", db, table: recipes, + insertSchema: RecipeInsertSchema, updateSchema: RecipeUpdateSchema }); + +mountCrudRoutes({ ...opts, expose: ["list", "get"] }); // only some verbs +mountReadOnlyCrudRoutes({ ...opts }); // list + get only +mountGetRoute({ ...opts }); // a single verb +``` + +`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }` +plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g. +`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default +/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the +custom routes** (HTML pages, nested resources, computed fields) — calling the +generated query helpers, and a projection's generated query for derived/aggregate +data. Generate the data layer; hand-write only what's genuinely custom. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md index 86220b8b0..917fc78cd 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -15,6 +15,27 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a `package` on its root node. Files in the same `package` with the same object `name` are merged by the loader. +## Find the construct first — model it, don't hand-write it + +You do **not** know the full vocabulary from memory, and it is larger and more +powerful than the basics below. Before authoring anything non-trivial, and the +moment you reach for hand-written data logic, **search the live metamodel**: + +``` +meta types relationship # find by name +meta types --all aggregate # find by WHAT IT DOES (searches descriptions) +meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs +``` + +**The rule:** before you hand-write any data logic — a join, a foreign key, a +derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field +rule, a relationship between entities, a derived read model — run `meta types` and +check for a construct that **declares** it. If one exists, declare it instead. That +is the entire point of MetaObjects: declared metadata is generated, typed, and +regenerates on change; hand-written logic drifts and is the thing this tool exists +to eliminate. When you catch yourself writing a query, a validator, or an FK by +hand, stop and search the types first. + Two on-disk formats, one shape: - **Canonical JSON** — the on-disk interchange. Every node is a single-key map @@ -330,7 +351,10 @@ child traversal, package only on the root segment) and/or carry `origin.*` children (`passthrough` / `aggregate` / `collection`) declaring assembly; its identity passes through via `extends` (`identity.primary: { name: id, extends: "Author.id" }`); it is read-only by construction and the declared field set IS -the exposure (fail-closed). +the exposure (fail-closed). Give it a read-only `source.rdb` `@kind: view` +child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projection +detection + view DDL off that read-only source, so without it `meta gen` emits +nothing for the projection. ## Abstracts + `extends` (deferred resolution) + `overlay` diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0e6c2c5c1..7cc889316 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -61,6 +61,40 @@ entity never emits instance/write artifacts regardless. Per-entity opt-outs exist (e.g. skipping client-side artifacts for a given entity) and are set as attributes on the entity in metadata, not in code. +## You don't have to generate everything — pick your layers + +Codegen is **granular and à la carte, not all-or-nothing.** The most powerful +pattern when an app's API doesn't match generated CRUD: **generate the data layer, +hand-write only the API layer** — never abandon codegen wholesale and hand-write +the data access too. + +- **Generate the data layer, skip the routes.** Omit `routesFile()` from the + `generators` array (keep `entityFile()` + `queriesFile()` + `barrel()`): you get + the typed entity/table, schemas, and query/finder helpers, then write your own + routes by hand — *calling the generated queries*. Do this whenever the API shape + (custom paths, HTML responses, nested payloads) doesn't fit generated REST CRUD. +- **Mix generated and hand-written routes.** Even with custom paths, mount the + standard verbs with the runtime helpers and hand-write only the custom ones (see + the runtime skill's `mountCrudRoutes` / `mountRoute` / `expose`). You are + never forced into all-generated or all-hand-written. +- **Derived/aggregate data → declare a projection, then USE its generated query.** + Don't hand-write a join or an `AVG()`/`COUNT()`. Declare an `object.projection` + with `origin.aggregate` / `origin.passthrough` / `origin.collection` children + **and a read-only `source.rdb` `@kind: view` child** (codegen detects a + projection by that read-only source, not by the subtype alone — omit it and + nothing is generated). `meta gen` emits a read-only query for it (and + `meta migrate` its DB view), and you **call that generated query from your + route**. Declaring the projection is only half the win — *consuming* its + generated query is the other half. +- **Codegen is yours to extend.** A generated file carries the `@generated` header + and is a normal source file: copy it and customize the copy (three-way merge + preserves your edits on regen), or write your own `Generator` (the plugin + interface) and add it to the `generators` array for an artifact the built-ins + don't cover. + +`meta gen --list` prints every generator by stable name; the `generators` array in +`metaobjects.config.ts` is where you opt each one in or out. + ## Dialects Generated DB schema/DDL targets a SQL **dialect**: diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md index 495307aa9..bed3e8f39 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md @@ -90,3 +90,33 @@ The routes call `parseFilterParams` (from `@metaobjectsdev/runtime-ts/drizzle-fa to validate `?filter[..][..]=..&sort=..&limit=&offset=` against the generated `FilterAllowlist` / `SortAllowlist`, returning HTTP 400 on an unknown field or disallowed operator. + +### Granular routes — mount some, hand-write the rest (don't read `node_modules`) + +When the API doesn't match generated CRUD, you don't have to choose all-generated +or all-hand-written, and you never need to reverse-engineer the runtime package. +`@metaobjectsdev/runtime-ts/drizzle-fastify` exports the mount helpers the generated +routes are built from — call them directly: + +```ts +import { + mountCrudRoutes, mountGetRoute, mountListRoute, mountReadOnlyCrudRoutes, +} from "@metaobjectsdev/runtime-ts/drizzle-fastify"; +import { RecipeInsertSchema, RecipeUpdateSchema } from "./generated/Recipe.js"; + +// all five verbs: +mountCrudRoutes({ fastify: app, path: "/recipes", db, table: recipes, + insertSchema: RecipeInsertSchema, updateSchema: RecipeUpdateSchema }); + +mountCrudRoutes({ ...opts, expose: ["list", "get"] }); // only some verbs +mountReadOnlyCrudRoutes({ ...opts }); // list + get only +mountGetRoute({ ...opts }); // a single verb +``` + +`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }` +plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g. +`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default +/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the +custom routes** (HTML pages, nested resources, computed fields) — calling the +generated query helpers, and a projection's generated query for derived/aggregate +data. Generate the data layer; hand-write only what's genuinely custom. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md index 86220b8b0..917fc78cd 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -15,6 +15,27 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a `package` on its root node. Files in the same `package` with the same object `name` are merged by the loader. +## Find the construct first — model it, don't hand-write it + +You do **not** know the full vocabulary from memory, and it is larger and more +powerful than the basics below. Before authoring anything non-trivial, and the +moment you reach for hand-written data logic, **search the live metamodel**: + +``` +meta types relationship # find by name +meta types --all aggregate # find by WHAT IT DOES (searches descriptions) +meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs +``` + +**The rule:** before you hand-write any data logic — a join, a foreign key, a +derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field +rule, a relationship between entities, a derived read model — run `meta types` and +check for a construct that **declares** it. If one exists, declare it instead. That +is the entire point of MetaObjects: declared metadata is generated, typed, and +regenerates on change; hand-written logic drifts and is the thing this tool exists +to eliminate. When you catch yourself writing a query, a validator, or an FK by +hand, stop and search the types first. + Two on-disk formats, one shape: - **Canonical JSON** — the on-disk interchange. Every node is a single-key map @@ -330,7 +351,10 @@ child traversal, package only on the root segment) and/or carry `origin.*` children (`passthrough` / `aggregate` / `collection`) declaring assembly; its identity passes through via `extends` (`identity.primary: { name: id, extends: "Author.id" }`); it is read-only by construction and the declared field set IS -the exposure (fail-closed). +the exposure (fail-closed). Give it a read-only `source.rdb` `@kind: view` +child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projection +detection + view DDL off that read-only source, so without it `meta gen` emits +nothing for the projection. ## Abstracts + `extends` (deferred resolution) + `overlay` diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0e6c2c5c1..7cc889316 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -61,6 +61,40 @@ entity never emits instance/write artifacts regardless. Per-entity opt-outs exist (e.g. skipping client-side artifacts for a given entity) and are set as attributes on the entity in metadata, not in code. +## You don't have to generate everything — pick your layers + +Codegen is **granular and à la carte, not all-or-nothing.** The most powerful +pattern when an app's API doesn't match generated CRUD: **generate the data layer, +hand-write only the API layer** — never abandon codegen wholesale and hand-write +the data access too. + +- **Generate the data layer, skip the routes.** Omit `routesFile()` from the + `generators` array (keep `entityFile()` + `queriesFile()` + `barrel()`): you get + the typed entity/table, schemas, and query/finder helpers, then write your own + routes by hand — *calling the generated queries*. Do this whenever the API shape + (custom paths, HTML responses, nested payloads) doesn't fit generated REST CRUD. +- **Mix generated and hand-written routes.** Even with custom paths, mount the + standard verbs with the runtime helpers and hand-write only the custom ones (see + the runtime skill's `mountCrudRoutes` / `mountRoute` / `expose`). You are + never forced into all-generated or all-hand-written. +- **Derived/aggregate data → declare a projection, then USE its generated query.** + Don't hand-write a join or an `AVG()`/`COUNT()`. Declare an `object.projection` + with `origin.aggregate` / `origin.passthrough` / `origin.collection` children + **and a read-only `source.rdb` `@kind: view` child** (codegen detects a + projection by that read-only source, not by the subtype alone — omit it and + nothing is generated). `meta gen` emits a read-only query for it (and + `meta migrate` its DB view), and you **call that generated query from your + route**. Declaring the projection is only half the win — *consuming* its + generated query is the other half. +- **Codegen is yours to extend.** A generated file carries the `@generated` header + and is a normal source file: copy it and customize the copy (three-way merge + preserves your edits on regen), or write your own `Generator` (the plugin + interface) and add it to the `generators` array for an artifact the built-ins + don't cover. + +`meta gen --list` prints every generator by stable name; the `generators` array in +`metaobjects.config.ts` is where you opt each one in or out. + ## Dialects Generated DB schema/DDL targets a SQL **dialect**: diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md index 495307aa9..bed3e8f39 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md @@ -90,3 +90,33 @@ The routes call `parseFilterParams` (from `@metaobjectsdev/runtime-ts/drizzle-fa to validate `?filter[..][..]=..&sort=..&limit=&offset=` against the generated `FilterAllowlist` / `SortAllowlist`, returning HTTP 400 on an unknown field or disallowed operator. + +### Granular routes — mount some, hand-write the rest (don't read `node_modules`) + +When the API doesn't match generated CRUD, you don't have to choose all-generated +or all-hand-written, and you never need to reverse-engineer the runtime package. +`@metaobjectsdev/runtime-ts/drizzle-fastify` exports the mount helpers the generated +routes are built from — call them directly: + +```ts +import { + mountCrudRoutes, mountGetRoute, mountListRoute, mountReadOnlyCrudRoutes, +} from "@metaobjectsdev/runtime-ts/drizzle-fastify"; +import { RecipeInsertSchema, RecipeUpdateSchema } from "./generated/Recipe.js"; + +// all five verbs: +mountCrudRoutes({ fastify: app, path: "/recipes", db, table: recipes, + insertSchema: RecipeInsertSchema, updateSchema: RecipeUpdateSchema }); + +mountCrudRoutes({ ...opts, expose: ["list", "get"] }); // only some verbs +mountReadOnlyCrudRoutes({ ...opts }); // list + get only +mountGetRoute({ ...opts }); // a single verb +``` + +`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }` +plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g. +`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default +/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the +custom routes** (HTML pages, nested resources, computed fields) — calling the +generated query helpers, and a projection's generated query for derived/aggregate +data. Generate the data layer; hand-write only what's genuinely custom. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index 86220b8b0..917fc78cd 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -15,6 +15,27 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a `package` on its root node. Files in the same `package` with the same object `name` are merged by the loader. +## Find the construct first — model it, don't hand-write it + +You do **not** know the full vocabulary from memory, and it is larger and more +powerful than the basics below. Before authoring anything non-trivial, and the +moment you reach for hand-written data logic, **search the live metamodel**: + +``` +meta types relationship # find by name +meta types --all aggregate # find by WHAT IT DOES (searches descriptions) +meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs +``` + +**The rule:** before you hand-write any data logic — a join, a foreign key, a +derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field +rule, a relationship between entities, a derived read model — run `meta types` and +check for a construct that **declares** it. If one exists, declare it instead. That +is the entire point of MetaObjects: declared metadata is generated, typed, and +regenerates on change; hand-written logic drifts and is the thing this tool exists +to eliminate. When you catch yourself writing a query, a validator, or an FK by +hand, stop and search the types first. + Two on-disk formats, one shape: - **Canonical JSON** — the on-disk interchange. Every node is a single-key map @@ -330,7 +351,10 @@ child traversal, package only on the root segment) and/or carry `origin.*` children (`passthrough` / `aggregate` / `collection`) declaring assembly; its identity passes through via `extends` (`identity.primary: { name: id, extends: "Author.id" }`); it is read-only by construction and the declared field set IS -the exposure (fail-closed). +the exposure (fail-closed). Give it a read-only `source.rdb` `@kind: view` +child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projection +detection + view DDL off that read-only source, so without it `meta gen` emits +nothing for the projection. ## Abstracts + `extends` (deferred resolution) + `overlay` diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 0e6c2c5c1..7cc889316 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -61,6 +61,40 @@ entity never emits instance/write artifacts regardless. Per-entity opt-outs exist (e.g. skipping client-side artifacts for a given entity) and are set as attributes on the entity in metadata, not in code. +## You don't have to generate everything — pick your layers + +Codegen is **granular and à la carte, not all-or-nothing.** The most powerful +pattern when an app's API doesn't match generated CRUD: **generate the data layer, +hand-write only the API layer** — never abandon codegen wholesale and hand-write +the data access too. + +- **Generate the data layer, skip the routes.** Omit `routesFile()` from the + `generators` array (keep `entityFile()` + `queriesFile()` + `barrel()`): you get + the typed entity/table, schemas, and query/finder helpers, then write your own + routes by hand — *calling the generated queries*. Do this whenever the API shape + (custom paths, HTML responses, nested payloads) doesn't fit generated REST CRUD. +- **Mix generated and hand-written routes.** Even with custom paths, mount the + standard verbs with the runtime helpers and hand-write only the custom ones (see + the runtime skill's `mountCrudRoutes` / `mountRoute` / `expose`). You are + never forced into all-generated or all-hand-written. +- **Derived/aggregate data → declare a projection, then USE its generated query.** + Don't hand-write a join or an `AVG()`/`COUNT()`. Declare an `object.projection` + with `origin.aggregate` / `origin.passthrough` / `origin.collection` children + **and a read-only `source.rdb` `@kind: view` child** (codegen detects a + projection by that read-only source, not by the subtype alone — omit it and + nothing is generated). `meta gen` emits a read-only query for it (and + `meta migrate` its DB view), and you **call that generated query from your + route**. Declaring the projection is only half the win — *consuming* its + generated query is the other half. +- **Codegen is yours to extend.** A generated file carries the `@generated` header + and is a normal source file: copy it and customize the copy (three-way merge + preserves your edits on regen), or write your own `Generator` (the plugin + interface) and add it to the `generators` array for an artifact the built-ins + don't cover. + +`meta gen --list` prints every generator by stable name; the `generators` array in +`metaobjects.config.ts` is where you opt each one in or out. + ## Dialects Generated DB schema/DDL targets a SQL **dialect**: diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md index 495307aa9..bed3e8f39 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md @@ -90,3 +90,33 @@ The routes call `parseFilterParams` (from `@metaobjectsdev/runtime-ts/drizzle-fa to validate `?filter[..][..]=..&sort=..&limit=&offset=` against the generated `FilterAllowlist` / `SortAllowlist`, returning HTTP 400 on an unknown field or disallowed operator. + +### Granular routes — mount some, hand-write the rest (don't read `node_modules`) + +When the API doesn't match generated CRUD, you don't have to choose all-generated +or all-hand-written, and you never need to reverse-engineer the runtime package. +`@metaobjectsdev/runtime-ts/drizzle-fastify` exports the mount helpers the generated +routes are built from — call them directly: + +```ts +import { + mountCrudRoutes, mountGetRoute, mountListRoute, mountReadOnlyCrudRoutes, +} from "@metaobjectsdev/runtime-ts/drizzle-fastify"; +import { RecipeInsertSchema, RecipeUpdateSchema } from "./generated/Recipe.js"; + +// all five verbs: +mountCrudRoutes({ fastify: app, path: "/recipes", db, table: recipes, + insertSchema: RecipeInsertSchema, updateSchema: RecipeUpdateSchema }); + +mountCrudRoutes({ ...opts, expose: ["list", "get"] }); // only some verbs +mountReadOnlyCrudRoutes({ ...opts }); // list + get only +mountGetRoute({ ...opts }); // a single verb +``` + +`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }` +plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g. +`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default +/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the +custom routes** (HTML pages, nested resources, computed fields) — calling the +generated query helpers, and a projection's generated query for derived/aggregate +data. Generate the data layer; hand-write only what's genuinely custom.