@metaobjectsdev/sdk 0.24.4 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -7
- package/agent-context/servers/csharp.meta.json +3 -1
- package/agent-context/servers/java.meta.json +3 -1
- package/agent-context/servers/kotlin.meta.json +3 -1
- package/agent-context/servers/python.meta.json +3 -1
- package/agent-context/servers/typescript.meta.json +3 -1
- package/agent-context/skills/metaobjects-audit/SKILL.md +15 -8
- package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +6 -1
- package/agent-context/skills/metaobjects-audit/references/csharp.md +10 -1
- package/agent-context/skills/metaobjects-audit/references/java.md +10 -0
- package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -0
- package/agent-context/skills/metaobjects-audit/references/python.md +12 -2
- package/agent-context/skills/metaobjects-audit/references/typescript.md +7 -2
- package/agent-context/skills/metaobjects-authoring/SKILL.md +35 -6
- package/agent-context/skills/metaobjects-codegen/SKILL.md +249 -10
- package/agent-context/skills/metaobjects-codegen/references/csharp.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/java.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/kotlin.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/python.md +1 -0
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +190 -6
- package/agent-context/skills/metaobjects-prompts/references/typescript.md +10 -1
- package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +22 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +15 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/java.md +33 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +23 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/python.md +25 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +34 -7
- package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +20 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +28 -0
- package/agent-context/templates/always-on.md.mustache +45 -4
- package/dist/agent-context/assemble.d.ts.map +1 -1
- package/dist/agent-context/assemble.js +52 -4
- package/dist/agent-context/assemble.js.map +1 -1
- package/dist/agent-context/scaffold.d.ts +16 -1
- package/dist/agent-context/scaffold.d.ts.map +1 -1
- package/dist/agent-context/scaffold.js +81 -4
- package/dist/agent-context/scaffold.js.map +1 -1
- package/dist/forge-types.d.ts.map +1 -1
- package/dist/forge-types.js +19 -1
- package/dist/forge-types.js.map +1 -1
- package/package.json +4 -9
- package/src/agent-context/assemble.ts +86 -6
- package/src/agent-context/scaffold.ts +91 -5
- package/src/forge-types.ts +21 -0
- package/dist/agent-docs/body.d.ts +0 -6
- package/dist/agent-docs/body.d.ts.map +0 -1
- package/dist/agent-docs/body.js +0 -593
- package/dist/agent-docs/body.js.map +0 -1
- package/dist/agent-docs/content-hash.d.ts +0 -8
- package/dist/agent-docs/content-hash.d.ts.map +0 -1
- package/dist/agent-docs/content-hash.js +0 -23
- package/dist/agent-docs/content-hash.js.map +0 -1
- package/dist/agent-docs/index.d.ts +0 -3
- package/dist/agent-docs/index.d.ts.map +0 -1
- package/dist/agent-docs/index.js +0 -4
- package/dist/agent-docs/index.js.map +0 -1
- package/src/agent-docs/body.ts +0 -592
- package/src/agent-docs/content-hash.ts +0 -25
- package/src/agent-docs/index.ts +0 -8
|
@@ -24,8 +24,28 @@ You run a `gen` step. The runner:
|
|
|
24
24
|
2. Resolves output targets and precomputes shared render state.
|
|
25
25
|
3. Runs each configured **generator** — most emit one file per entity; some emit a
|
|
26
26
|
single shared file (a barrel, a DB-context, an app-config).
|
|
27
|
-
4.
|
|
28
|
-
|
|
27
|
+
4. Decides whether it may overwrite a file — and **the rule differs by port**:
|
|
28
|
+
- **TypeScript, C#, Python** — by a committed **hash manifest**
|
|
29
|
+
(`.metaobjects/.gen-state/.hashes.json`). If a file still hashes to what the
|
|
30
|
+
generator recorded writing, it is safe to overwrite; if it was edited, or there is
|
|
31
|
+
no record of it, the write is **refused by name**. TypeScript additionally
|
|
32
|
+
three-way-merges against a snapshot when one is present locally.
|
|
33
|
+
- **Java, Kotlin** — by a bare **`GENERATED`** token in the file's header comment
|
|
34
|
+
(`GeneratedFileWriter.GENERATED_MARKER`). The token is `GENERATED`, **not**
|
|
35
|
+
`@generated`: the matcher allows only whitespace between the comment punctuation
|
|
36
|
+
and the token, so an `@`-prefixed tag does not match it. Remove that token and
|
|
37
|
+
regeneration never touches the file again.
|
|
38
|
+
|
|
39
|
+
**The two rules protect a hand edit in opposite ways, so do not carry a habit across
|
|
40
|
+
ports.** On TypeScript, C# and Python, *editing the content* is what takes ownership —
|
|
41
|
+
that is what breaks the hash — and deleting the header changes nothing except your
|
|
42
|
+
ability to tell what generated the file. On Java and Kotlin the reverse holds: editing
|
|
43
|
+
a file protects it not at all, because the `GENERATED` token is still there and the
|
|
44
|
+
next run overwrites the edit; only removing that token does.
|
|
45
|
+
|
|
46
|
+
(The header text differs per port and none of it is the write decision on the
|
|
47
|
+
hash-manifest ports: TypeScript and Python emit `@generated by …`, C# emits
|
|
48
|
+
`<auto-generated/>`, Java and Kotlin emit the `GENERATED` token above.)
|
|
29
49
|
|
|
30
50
|
The output is normal idiomatic code in your language — you import it and use it
|
|
31
51
|
like any hand-written module.
|
|
@@ -35,8 +55,11 @@ like any hand-written module.
|
|
|
35
55
|
Every emitted file carries a `@generated` header. This is load-bearing:
|
|
36
56
|
|
|
37
57
|
- **Never hand-edit a file with a `@generated` header for a change you want to
|
|
38
|
-
keep.** The next `gen` run overwrites it. If you need different output, change
|
|
39
|
-
|
|
58
|
+
keep.** The next `gen` run overwrites it. If you need different output, change the
|
|
59
|
+
metadata, or change the generator that emits it.
|
|
60
|
+
- **This rule is about emitted output — it is not a rule about your generators.** A
|
|
61
|
+
generator you own carries no `@generated` header and is edited like any other source
|
|
62
|
+
file in your repo. See the next section before you conclude a shape is unreachable.
|
|
40
63
|
- **Hand-written regions are preserved by three-way merge.** Where the codegen
|
|
41
64
|
supports designated hand-editable regions, regeneration runs a three-way merge
|
|
42
65
|
(base → yours → newly-generated) so your edits survive a regen. Code review is
|
|
@@ -54,6 +77,43 @@ for a shape origins can express is drift the drift gate can't even see. A genuin
|
|
|
54
77
|
loose either — it goes in the `source.rdb` **`@sql`** escape (#208, ADR-0043) so the
|
|
55
78
|
tool still registers, fingerprints, and drift-checks it (see the projection bullet).
|
|
56
79
|
|
|
80
|
+
## Your generators are yours — editing one needs no permission
|
|
81
|
+
|
|
82
|
+
A generator in your repo is your code, not a vendor artifact. ADR-0034 is
|
|
83
|
+
**scaffold-and-own**: the generators the scaffolded config wires are copied into your
|
|
84
|
+
repo at init, and every reference template's header says so in its own first line
|
|
85
|
+
("copy this into your repo … and own it", "now YOURS to change"). None carries a
|
|
86
|
+
`@generated` header. Editing one is ordinary work.
|
|
87
|
+
|
|
88
|
+
**A standing rule not to change the MetaObjects repo is not a rule about your
|
|
89
|
+
generators.** They are different repositories, and you own yours outright. That
|
|
90
|
+
generalisation is the observed failure mode, not a hypothetical: an agent told not to
|
|
91
|
+
touch upstream quietly drops "edit the generator" from the moves available to it and
|
|
92
|
+
hand-writes the layer instead — the single outcome the rest of this skill exists to
|
|
93
|
+
prevent. If you are about to hand-write something data-shaped because the generated
|
|
94
|
+
shape is wrong, the generator is the file to open, and you do not need to ask first.
|
|
95
|
+
|
|
96
|
+
**The order when generated output does not fit:**
|
|
97
|
+
|
|
98
|
+
1. **The metadata**, if the model is wrong — wrong column type, missing relationship,
|
|
99
|
+
a join that should be a projection. Fix the spine first; it fixes every port at once.
|
|
100
|
+
2. **Your own generator**, if the model is right and the *emit* is wrong — naming, file
|
|
101
|
+
layout, imports, framework, signatures.
|
|
102
|
+
3. **Hand-write**, only for what metadata genuinely cannot express — and wire it to the
|
|
103
|
+
generated types.
|
|
104
|
+
|
|
105
|
+
Hand-writing something the metadata already describes is step 3 used as step 1.
|
|
106
|
+
|
|
107
|
+
**The converse, so ownership does not become sprawl:** wire a generator only for output
|
|
108
|
+
you will actually consume. Decide per generator, narrow one with its own `filter`, and
|
|
109
|
+
own the ones you keep — an emitted file nobody imports still reads as an invitation to
|
|
110
|
+
adopt the surface you decided against.
|
|
111
|
+
|
|
112
|
+
How you get a generator's source differs per port — a copy command on TypeScript,
|
|
113
|
+
implementing the port's generator interface elsewhere. Your language reference has the
|
|
114
|
+
mechanism; see also "The commands and config keys that implement the steps above differ
|
|
115
|
+
per port" below.
|
|
116
|
+
|
|
57
117
|
## Selecting generators by stable name
|
|
58
118
|
|
|
59
119
|
Codegen is a set of named generators you opt into. Each generator has a **stable
|
|
@@ -150,12 +210,13 @@ don't silently churn the existing code.
|
|
|
150
210
|
|
|
151
211
|
## Write your own generators — the built-ins rarely fit an app exactly
|
|
152
212
|
|
|
153
|
-
The built-in generators (entity, queries, routes,
|
|
154
|
-
common shape, but **real apps routinely need output the
|
|
155
|
-
— a bespoke REST contract, custom DTO/response shapes,
|
|
156
|
-
repository layer, a UI the defaults don't produce. When
|
|
157
|
-
move is **not** to abandon metadata and hand-write the
|
|
158
|
-
generator** that reads the same metadata and emits *your*
|
|
213
|
+
The built-in generators (entity, queries, routes, routes-hono, barrel, form, hooks,
|
|
214
|
+
grid, grid-hook) cover the common shape, but **real apps routinely need output the
|
|
215
|
+
built-ins don't emit as-is** — a bespoke REST contract, custom DTO/response shapes,
|
|
216
|
+
an app-specific service or repository layer, a UI the defaults don't produce. When
|
|
217
|
+
that happens the model-first move is **not** to abandon metadata and hand-write the
|
|
218
|
+
layer. Write a **custom generator** that reads the same metadata and emits *your*
|
|
219
|
+
app's shape.
|
|
159
220
|
|
|
160
221
|
Treat this as a first-class, expected activity — not an escape hatch. A custom
|
|
161
222
|
generator is still model-first: it derives from the metadata spine, so it
|
|
@@ -163,6 +224,12 @@ regenerates on change and stays consistent across every entity — the leverage
|
|
|
163
224
|
forfeit by hand-writing. Hand-rolling *away from* metadata is the anti-pattern;
|
|
164
225
|
generating *your own shape from* metadata is the point.
|
|
165
226
|
|
|
227
|
+
This is for when the *shape* itself needs to change. If a built-in's shape is
|
|
228
|
+
already right and only the *target* is wrong — a different framework than the
|
|
229
|
+
shipped reference emits for — take ownership of that generator instead of writing
|
|
230
|
+
one from scratch; see "Your framework isn't the default" below, and your language
|
|
231
|
+
reference for the command that does it.
|
|
232
|
+
|
|
166
233
|
The plugin interface is small (`@metaobjectsdev/codegen-ts`): a `Generator` is
|
|
167
234
|
`{ name, filter?, generate }`, where `generate(ctx)` returns `EmittedFile[]`
|
|
168
235
|
(`{ path, content }`). `perEntity` / `oncePerRun` wrap the common cases:
|
|
@@ -190,6 +257,31 @@ the `generators` array in `metaobjects.config.ts` next to the built-ins — it r
|
|
|
190
257
|
the same pass, writes under the same target rules, and carries the `@generated`
|
|
191
258
|
header so it round-trips like any other.
|
|
192
259
|
|
|
260
|
+
## Your framework isn't the default — the retargeting procedure
|
|
261
|
+
|
|
262
|
+
If the shipped templates do not emit for your stack, retargeting is the **normal first
|
|
263
|
+
move** — not a workaround and not a sign of a bug. Owning a generator is the supported
|
|
264
|
+
path to any framework; MetaObjects does not ship a codegen package per framework and is
|
|
265
|
+
not waiting to.
|
|
266
|
+
|
|
267
|
+
The doctrine, in order of what to try:
|
|
268
|
+
|
|
269
|
+
1. **Check config first.** Several apparent codegen failures are one config value
|
|
270
|
+
(module-specifier style, output directory, dialect, API prefix). Change it and retest
|
|
271
|
+
before writing any code.
|
|
272
|
+
2. **Own the generator, not the renderer.** Take a copy of the reference template for the
|
|
273
|
+
artifact that is wrong and edit the one step your framework disagrees about. Each
|
|
274
|
+
template's header names what its emit is coupled to and which call to swap.
|
|
275
|
+
3. **Compose, do not fork.** Call the exported render function and wrap its result where
|
|
276
|
+
you can, so you keep receiving upstream fixes. Forking a whole renderer is the thing
|
|
277
|
+
to avoid — not owning the generator.
|
|
278
|
+
4. **Server-tier output is usually already portable.** The entity module and the query
|
|
279
|
+
helpers carry no HTTP-framework coupling; retargeting is usually only needed at the
|
|
280
|
+
routes and UI tiers.
|
|
281
|
+
|
|
282
|
+
Hand-rolling *away from* metadata is the anti-pattern. Generating *your own shape from*
|
|
283
|
+
metadata is the point.
|
|
284
|
+
|
|
193
285
|
### Never read metadata through an `own*()` accessor (ADR-0039) — top bug source
|
|
194
286
|
|
|
195
287
|
When writing OR reviewing a generator, **read every field/node property and iterate
|
|
@@ -239,6 +331,153 @@ output/template · doesn't fit → write a generator that emits your shape *from
|
|
|
239
331
|
metadata* · only the genuinely un-modelable (business algorithms, external calls) is
|
|
240
332
|
hand-written outside codegen — and it still imports the generated types.
|
|
241
333
|
|
|
334
|
+
## Two ways to author a generator — pick deliberately
|
|
335
|
+
|
|
336
|
+
A generator can be **programmatic** (code that builds the output) or **declarative** (a
|
|
337
|
+
Mustache template plus a scope). Both are first-class, both ship in every port, and they
|
|
338
|
+
are good at different things.
|
|
339
|
+
|
|
340
|
+
| | Programmatic | Declarative template |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| What you write | a `Generator` in the port's language, using its AST builder (ts-poet, KotlinPoet, …) | a `.mustache` file + `{ template, scope, outputPattern, format? }` |
|
|
343
|
+
| Output shape | expressed in code | **is the file you are editing** |
|
|
344
|
+
| Cross-language | per-port by construction | one template emits for any language — it renders against the neutral, byte-gated data dict |
|
|
345
|
+
| Logic | any | what a template can express: sections, iteration, presence flags |
|
|
346
|
+
|
|
347
|
+
**The rule:** reach for **programmatic** when the logic is gnarly or the run is hot; reach
|
|
348
|
+
for a **template** when the *shape* is what you are iterating on, or when you want the same
|
|
349
|
+
output across languages. `scope` is `perEntity` / `perPackage` / `perModel` — the walk you
|
|
350
|
+
would otherwise hand-write — and `outputPattern` is the output path per item, with
|
|
351
|
+
`{name}` / `{Name}` / `{package}` placeholders (e.g. `"{package}/{Name}Service.java"`).
|
|
352
|
+
Full tradeoff table and the data dict: `docs/features/codegen-concepts.md` §3 and §10.
|
|
353
|
+
**Asking whether a base/extension split or a write-if-absent file exists? That's §5-§7, not
|
|
354
|
+
here.** §5 (*Preserving hand edits*) states MetaObjects ships exactly one hand-edit strategy —
|
|
355
|
+
no shipped generator on any port emits a generated-base + hand-owned-concrete pair, or a
|
|
356
|
+
write-if-absent file; §6 names the `skip-existing` merge strategy `runGen` accepts for
|
|
357
|
+
building that pair yourself, reachable only from a programmatic caller (no CLI flag selects
|
|
358
|
+
it); §7 (*Safety*) is the per-port write-decision mechanism behind "What codegen does" step 4
|
|
359
|
+
above.
|
|
360
|
+
|
|
361
|
+
**A template is not limited to documents.** It emits source as readily as docs — that is
|
|
362
|
+
what the neutral data dict is for.
|
|
363
|
+
|
|
364
|
+
### Which is available to you depends on the port — check before you plan
|
|
365
|
+
|
|
366
|
+
**TypeScript** has both, and the whole programmatic procedure is documented: `meta eject`,
|
|
367
|
+
the `metaobjects.config.ts` keys, the exported `render*` functions — see this skill's
|
|
368
|
+
`references/typescript.md`. The declarative path is declared in the SAME config: call
|
|
369
|
+
`templateGenerator()` in `generators`, or spread a parsed JSON spec with
|
|
370
|
+
`templateSpecToGenerators(parseTemplateSpec(...))` to reuse one written for C#/Python.
|
|
371
|
+
**There is no `--template-spec` flag on `meta gen` and its absence is not a gap** — the
|
|
372
|
+
config takes generator values, and keeping the declaration there is what keeps
|
|
373
|
+
`meta verify --codegen` regenerating with it.
|
|
374
|
+
|
|
375
|
+
**Java / Kotlin** have both. **No eject command** — a programmatic generator means
|
|
376
|
+
implementing `com.metaobjects.generator.Generator` and naming your class in the Maven
|
|
377
|
+
`<generator>` element, which the plugin loads from the project classpath. The declarative
|
|
378
|
+
path is `TemplateScopeGenerator`, wired the same way with `<template>` / `<scope>` /
|
|
379
|
+
`<outputPattern>` / `<format>` / `<templatesDir>` (plus the standard `<outputDir>`), and
|
|
380
|
+
covers Java and Kotlin alike. No `--template-spec` flag here either, for the same reason:
|
|
381
|
+
`<generator>` already loads a consumer class from the project classpath.
|
|
382
|
+
|
|
383
|
+
**C# and Python: the declarative path is your only option, and it is a real one.** Their
|
|
384
|
+
generator sets are **closed built-in registries** — `--generators` *selects* from what
|
|
385
|
+
ships, and there is no seam to register a `Generator` of your own. (Python's
|
|
386
|
+
`--provider module:symbol` registers **metamodel vocabulary**, not a generator; do not
|
|
387
|
+
reach for it here.) Use `--template-spec <json>` — plus `--templates <dir>` on Python or
|
|
388
|
+
`--template-root <dir>` on C# — and your entries are appended to the default suite. Worked
|
|
389
|
+
examples with the full JSON: `docs/ports/python.md` and `docs/ports/csharp.md`.
|
|
390
|
+
|
|
391
|
+
**The spec is auto-discovered, and that is load-bearing.** With no `--template-spec`, both
|
|
392
|
+
ports read `<projectRoot>/template-spec.json` — projectRoot being the metadata dir's parent.
|
|
393
|
+
Keep it there: `verify --codegen` accepts no `--template-spec` flag, so the conventional path
|
|
394
|
+
is how the drift gate learns your template generators exist. Put the spec somewhere else and
|
|
395
|
+
reach it only by flag, and `verify` regenerates without it and reports its output as stale.
|
|
396
|
+
|
|
397
|
+
So on C#/Python, "I need a shape the built-ins do not emit" is answered by a template, not
|
|
398
|
+
by writing generator code. Do not conclude the port cannot be customized.
|
|
399
|
+
|
|
400
|
+
Each port's `references/` fragment documents what its built-ins emit, which is what you
|
|
401
|
+
compare your own emit against; they do not carry a step-by-step retargeting procedure.
|
|
402
|
+
|
|
403
|
+
## Never hand-write a physical name — the generator emits them
|
|
404
|
+
|
|
405
|
+
A table name, a column name, a schema — these are declared in metadata and derived by the
|
|
406
|
+
same resolver the migration and the runtime use. A string literal for one is a magic string
|
|
407
|
+
that no compiler checks and no gate catches, and it goes wrong silently: `@column` is
|
|
408
|
+
free-form, so a field named `callPurpose` may map to a column named `purpose_code`, which is
|
|
409
|
+
neither the field name nor any transformation of it. A consumer deriving the column as
|
|
410
|
+
`to_snake_case(<the field's name>)` gets that case wrong and never finds out.
|
|
411
|
+
|
|
412
|
+
Every port emits a per-object names artifact. Reference it:
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { ProgramNames } from "./generated/Program.names.js";
|
|
416
|
+
|
|
417
|
+
ProgramNames.name // "Program" — the OBJECT's name
|
|
418
|
+
ProgramNames.sources.primary.table // "programs" — physical table
|
|
419
|
+
ProgramNames.sources.primary.kind // "table" — table | view | proc | …
|
|
420
|
+
ProgramNames.fields.createdAt.name // "createdAt" — logical / wire name
|
|
421
|
+
ProgramNames.fields.createdAt.column // "created_at" — physical column
|
|
422
|
+
ProgramNames.indexes.ix_prog_owner.index // "ix_prog_owner" — database index name
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The artifact mirrors the metadata tree: every node carries its own `type`, `subType` and
|
|
426
|
+
`name`, and a physical name sits under the key that says **what kind of database object it
|
|
427
|
+
is**. A view is `sources.primary.view`, a stored proc `sources.primary.proc`, and a
|
|
428
|
+
write-through entity's read view `sources.replica.view` — so `sources.replica.table` is a
|
|
429
|
+
compile error rather than a wrong answer. There is no `readOnly`: it was derived from
|
|
430
|
+
`kind`, never declared, so ask `kind`.
|
|
431
|
+
|
|
432
|
+
The artifact is per-object and the shape is per-language; the guarantee is the same
|
|
433
|
+
everywhere — **each physical name is spelled once, and generated code references it**:
|
|
434
|
+
|
|
435
|
+
| Port | Artifact | Reads as |
|
|
436
|
+
|---|---|---|
|
|
437
|
+
| TypeScript | `<Entity>.names.ts` | `ProgramNames.fields.createdAt.column` |
|
|
438
|
+
| C# | `<Entity>Names.g.cs` | `ProgramNames.CreatedAtColumn` |
|
|
439
|
+
| Java | `<Entity>Names.java` | `ProgramNames.CREATED_AT_COLUMN` |
|
|
440
|
+
| Kotlin | `<Entity>Names.kt` | `ProgramNames.CREATED_AT_COLUMN` |
|
|
441
|
+
| Python | `<entity_snake>_names.py` | `PROGRAM_CREATED_AT_COLUMN` |
|
|
442
|
+
|
|
443
|
+
On TypeScript (`meta init`), C# and Python the names generator is in the default suite; on
|
|
444
|
+
the JVM it is opt-in — add `SpringNamesGenerator` / `KotlinNamesGenerator` to the pom's
|
|
445
|
+
`<generators>`, and the Exposed table binding switches to the constants once it is present.
|
|
446
|
+
|
|
447
|
+
**It follows `extends`.** An object that extends another does not restate what it
|
|
448
|
+
inherits: C# and Java use real class inheritance (`class CopayAuthNames extends
|
|
449
|
+
AuthNames`), TypeScript spreads (`...AuthNames.fields`), and Kotlin and Python re-export
|
|
450
|
+
the parent's constants by reference. An abstract base a persisted object extends gets an
|
|
451
|
+
artifact of its own — columns only, no table name, because it has none. So if you are
|
|
452
|
+
reading a subtype's artifact and its table name is not there, it is on the base, which is
|
|
453
|
+
where it belongs.
|
|
454
|
+
|
|
455
|
+
**Where generated code consumes it, and where it does not.** TypeScript, C# and Kotlin
|
|
456
|
+
bind an ORM (Drizzle, EF Core, Exposed) and so must spell physical names — their generated
|
|
457
|
+
code references these constants, and a cross-port gate proves no generated file spells one
|
|
458
|
+
literally. **Java and Python generate no SQL at all**: their DTOs and models carry logical
|
|
459
|
+
names, and persistence is the repository interface/`Protocol` you implement. There the
|
|
460
|
+
artifact exists *for your code*, which is the only place a physical name appears.
|
|
461
|
+
|
|
462
|
+
Two categories stay literal, deliberately, and the gate pins them as such rather than
|
|
463
|
+
exempting them:
|
|
464
|
+
|
|
465
|
+
- a **flattened value-object column** (`@storage: flattened`) is a composite —
|
|
466
|
+
`<owner field column>_<member column>` — belonging to no single field of either object,
|
|
467
|
+
so there is no one constant to reference;
|
|
468
|
+
- a **write-through entity's replica view name**: the artifact holds the object's PRIMARY
|
|
469
|
+
source's name (its table), and a write-through entity has two physical names.
|
|
470
|
+
|
|
471
|
+
**But prefer a typed handle where one exists — this rule has a real limit.** If the ORM
|
|
472
|
+
gives you a type-checked object for the same thing, use that. Replacing a Drizzle column
|
|
473
|
+
object (`programs.createdAt`, checked against the schema at compile time) with a string
|
|
474
|
+
constant makes the code **worse**: it trades an error the compiler catches for one the
|
|
475
|
+
database raises at runtime. The constants are for the places with no typed handle — raw
|
|
476
|
+
SQL, migration scripts, log lines, an external system's column mapping, a port whose
|
|
477
|
+
generated model carries no persistence binding at all.
|
|
478
|
+
|
|
479
|
+
The rule is *don't invent the string*, not *replace every name with a constant*.
|
|
480
|
+
|
|
242
481
|
## Dialects
|
|
243
482
|
|
|
244
483
|
Generated DB schema/DDL targets a SQL **dialect**:
|
|
@@ -47,6 +47,7 @@ or run the default set. Output lands under `--namespace` in `--output-dir`.
|
|
|
47
47
|
| `callable` | `<Entity>.callable.g.cs` — an FR-015 calling method for a `source.rdb @kind="storedProc"|"tableFunction"`, via EF `FromSqlInterpolated` (args from the `@parameterRef` value object in declaration order). |
|
|
48
48
|
| `payload` | `<Payload>.payload.cs` — the strict typed payload `record` (+ any nested element records) per `template.prompt` `@payloadRef` / `@responseRef` (each an `object.value`): `@payloadRef` types the REQUEST, `@responseRef` the REPLY the parser/extractor bind to. |
|
|
49
49
|
| `output-parser` / `extractor` / `output-prompt` / `render-helper` | the prompt-pillar artifacts for a **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: these tiers are INBOUND; `template.output` is outbound only and emits no parser). The strict parser, the tolerant `extract`, the **output-format prompt fragment** (`output-prompt`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), and the typed render helper. See the **prompts** reference. |
|
|
50
|
+
| `names` | `<Entity>Names.g.cs` — `public abstract class <Entity>Names` of `const` physical database names, MIRRORING THE METADATA TREE: the object's own `Type`/`SubType`/`Name` (`Name` is the OBJECT's name, never a physical one), then per `source.rdb` keyed by `@role` — `SourcePrimaryType`/`SubType`/`Kind`, `SourcePrimarySchema` when declared, and the physical name under the alias for its `@kind` (`SourcePrimaryTable` / `SourceReplicaView` / `SourcePrimaryProc` / `…Function` / `…MaterializedView`) — then a `<Field>Field`/`<Field>Column` pair per field, then each identity and index (`IdentityPkName`, and `IdentityUqCustEmailIndex` / `IndexIxCustStatusIndex` — the DATABASE name, carried for `identity.secondary` and `index.lookup` only), then a complete `ColumnsByField`. There is no `ReadOnly`: it is a derivation over `@kind`, so ask `SourcePrimaryKind`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (its own identity + columns, and NO source members — it has no table). A class whose object extends another one **inherits** it (`class CopayAuthNames : AuthNames`) rather than restating its constants — a C# `const` is inherited, so `CopayAuthNames.IdColumn` and `CopayAuthNames.SourcePrimaryTable` resolve through the base (`Type`/`SubType`/`Name` are redeclared with `new`, since every artifact carries its own). `abstract`, not `static`, precisely so it can be inherited; it still cannot be instantiated. In the run, `entity` and `db-context` reference the constants (`[Table(OrderNames.SourcePrimaryTable)]`, `[Column(OrderNames.StatusColumn)]`, `.ToView(OrderNames.SourceReplicaView)`); a `--generators` selection omitting `names` falls back to literals. |
|
|
50
51
|
| `template` | the generic Mustache `templateGenerator()` primitive. |
|
|
51
52
|
|
|
52
53
|
Metadata lives under `metaobjects/` (or wherever you point `--metadata-dir`) in the
|
|
@@ -106,6 +106,7 @@ separate `metaobjects-codegen-base` module instead.)
|
|
|
106
106
|
| `SpringRenderHelperGenerator` | the typed render helper for a `template.prompt` payload |
|
|
107
107
|
| `LlmTraceHelperGenerator` | `<Entity>TraceHelper.java` per concrete entity — the LLM-trace helper |
|
|
108
108
|
| `SpringFilterAllowlistGenerator` | per-entity filter allowlist |
|
|
109
|
+
| `SpringNamesGenerator` | `<Entity>Names.java` — `public abstract class` of `static final String` names that MIRRORS THE METADATA TREE. Every node carries its own `TYPE`/`SUB_TYPE`/`NAME`, so `AuthorNames.NAME` is the OBJECT's name (`"Author"`), **not** a table name, and a physical name sits under the member that says what it is: `SOURCE_<ROLE>_TABLE` / `_VIEW` / `_MATERIALIZED_VIEW` / `_PROC` / `_FUNCTION`, from the metamodel's own `@kind`-to-alias map, with `<ROLE>` being `PRIMARY` or `REPLICA` (a write-through entity has a member for each). Plus a `<FIELD>_FIELD`/`<FIELD>_COLUMN` pair per field and a complete `COLUMNS_BY_FIELD`; an `identity.secondary` or `index.lookup` also carries `IDENTITY_<NAME>_INDEX` / `INDEX_<NAME>_INDEX`, while `identity.primary` deliberately carries no index member (migrate names a PK by a dialect-conditional formula this artifact must not restate). **There is no `READ_ONLY`** — it was a derivation over `@kind`, never metadata, so ask `SOURCE_<ROLE>_KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns only, no source). A class whose object extends another one **extends** it (`class CopayAuthNames extends AuthNames`) rather than restating its constants — Java inherits static members, so `CopayAuthNames.SOURCE_PRIMARY_TABLE` and `CopayAuthNames.ID_COLUMN` both resolve through the base. **This port generates no SQL** (the repository is an interface you implement), so nothing generated consumes these — they exist for your implementation. |
|
|
109
110
|
| `JavaObjectCodeGenerator` | module `metaobjects-codegen-base` (`com.metaobjects.generator.direct.object.javacode`), a separate module from the Spring generators above. Flavor-selected via the `flavor` generator arg. `flavor=pojoAware` → `class <Name> extends PojoObject` (a concrete `MetaObjectAware` class with a `(MetaObject)` constructor) — its inherited `getMetaData()` back-reference is what breaks a default Jackson/Gson mapper, see "Serializing generated objects" below. `flavor=valueObject` → `class <Name> extends ValueObject` (map-backed; less hostile to a default mapper, but still not the sanctioned serialization path). Either concrete flavor also emits a `<Name>Extractor` plus a self-registering `ObjectClassBindingProvider`. For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. |
|
|
110
111
|
|
|
111
112
|
**Projections (read-only views).** An `object.projection` (read-only `source.rdb`
|
|
@@ -120,6 +120,7 @@ All live in `metaobjects-codegen-kotlin` under
|
|
|
120
120
|
| `KotlinRenderHelperGenerator` | the typed render helper for a `template.prompt` payload |
|
|
121
121
|
| `KotlinValidatorGenerator` | `MetadataStartupValidator.kt` + `ExposedTableValidator.kt` (once per project) |
|
|
122
122
|
| `KotlinSpringConfigGenerator` | `MetadataExposedConfig.kt` — `@Configuration` wiring `Database.connect()` + the startup validator (once per project) |
|
|
123
|
+
| `KotlinNamesGenerator` | `<Entity>Names.kt` — an `object` of `const val` names that MIRRORS THE METADATA TREE. Every node carries its own `TYPE`/`SUB_TYPE`/`NAME`, so `AuthorNames.NAME` is the OBJECT's name (`"Author"`), **not** a table name, and a physical name sits under the member that says what it is: `SOURCE_<ROLE>_TABLE` / `_VIEW` / `_MATERIALIZED_VIEW` / `_PROC` / `_FUNCTION`, from the metamodel's own `@kind`-to-alias map, with `<ROLE>` being `PRIMARY` or `REPLICA` (a write-through entity has a member for each). Plus a `<FIELD>_FIELD`/`<FIELD>_COLUMN` pair per field and a complete `COLUMNS_BY_FIELD`; an `identity.secondary` or `index.lookup` also carries `IDENTITY_<NAME>_INDEX` / `INDEX_<NAME>_INDEX`, while `identity.primary` deliberately carries no index member (migrate names a PK by a dialect-conditional formula this artifact must not restate). **There is no `READ_ONLY`** — it was a derivation over `@kind`, never metadata, so ask `SOURCE_<ROLE>_KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns only, no source). Kotlin has no static inheritance — an `object` cannot extend another — so an artifact whose object extends another **re-exports** the parent's constants by reference (`const val SOURCE_PRIMARY_TABLE: String = AuthNames.SOURCE_PRIMARY_TABLE`) instead of restating the literal. Wire it alongside `KotlinExposedTableGenerator`: the Maven plugin then turns the table binding's constant substitution ON automatically (`AuthorTable : Table(AuthorNames.SOURCE_PRIMARY_TABLE)`); a run without it keeps the literals, so the output still compiles. |
|
|
123
124
|
| `KotlinStoredProcGenerator` | stored-procedure call wrappers for `source.rdb` `@kind="storedProc"` |
|
|
124
125
|
| `KotlinFilterAllowlistGenerator` | per-entity filter allowlist |
|
|
125
126
|
|
|
@@ -61,6 +61,7 @@ a renamed physical column).
|
|
|
61
61
|
| `routes` | a **FastAPI `APIRouter`** per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). The router declares a repository **`Protocol`** you implement and inject. A TPH `@discriminator` base emits ONE polymorphic router: `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. Its repository `Protocol` is subtype-keyed (`subtype=None` for the polymorphic base) so your implementation applies the single-table discriminator scope. |
|
|
62
62
|
| `filter-allowlist` | per-entity filter allowlist (FR-009 — the server-side field+operator allowlist the routes validate against). |
|
|
63
63
|
| `payload` / `output-parser` / `output-prompt` / `extractor` / `render-helper` / `trace-helper` | the prompt-pillar artifacts for a **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: these tiers are INBOUND; `template.output` is outbound only and emits no parser). The payload VO, the strict parser (`<template>_response_parser`), the **output-format prompt fragment** (`<template>_response_format`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), the tolerant `extract`, the typed render helper, and the LLM-trace helper. See the **prompts** reference. |
|
|
64
|
+
| `names` | `<entity_snake>_names.py` — module-level `Final` constants mirroring the object's metadata tree. Every node carries its own `_TYPE`/`_SUB_TYPE`/`_NAME`; `<ENTITY>_NAME` is the OBJECT's name, and a physical name sits under the member naming what it is: `<ENTITY>_SOURCE_<ROLE>_{TABLE,VIEW,MATERIALIZED_VIEW,PROC,FUNCTION}` (`<ROLE>` is `PRIMARY` or `REPLICA`, so a write-through entity's read view has a slot), plus `<ENTITY>_SOURCE_<ROLE>_{KIND,SCHEMA}`, a `<ENTITY>_<FIELD>_FIELD`/`_COLUMN` pair each, `<ENTITY>_IDENTITY_<NAME>_*` / `<ENTITY>_INDEX_<NAME>_*` carrying `_INDEX` (the database index name) for `identity.secondary` and `index.lookup`, and a complete `<ENTITY>_COLUMNS_BY_FIELD`. No `_READ_ONLY` — it was derived from `@kind`, never declared; ask `_SOURCE_<ROLE>_KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns and keys only, no `_SOURCE_*` — it has no table and must never acquire one). Python has no static inheritance, so a module whose object extends another **imports and re-exports** the parent's constants (`AUTHOR_CREATED_AT_COLUMN: Final[str] = BASEENTITY_CREATED_AT_COLUMN`) instead of restating the literal; a TPH subtype re-exports `_SOURCE_PRIMARY_*` too, since it shares its base's table. **This port generates no SQL**, so nothing generated consumes these — they exist for the repository `Protocol` implementation you write. |
|
|
64
65
|
| `template` | the generic Mustache `template` primitive. |
|
|
65
66
|
|
|
66
67
|
**Projections + entity read-views.** An `object.projection` (read-only `source.rdb`
|
|
@@ -8,9 +8,11 @@ packages. Codegen runs through the Node `meta` CLI (`@metaobjectsdev/cli`, binar
|
|
|
8
8
|
- Install
|
|
9
9
|
- `metaobjects.config.ts`
|
|
10
10
|
- The generators
|
|
11
|
+
- Declarative template-codegen (Mustache)
|
|
11
12
|
- Run
|
|
12
13
|
- Multiple output targets
|
|
13
14
|
- Field subtype → column mapping
|
|
15
|
+
- Retargeting to another framework — the TypeScript procedure
|
|
14
16
|
|
|
15
17
|
## Install
|
|
16
18
|
|
|
@@ -43,6 +45,7 @@ import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack
|
|
|
43
45
|
export default defineConfig({
|
|
44
46
|
outDir: "src/generated",
|
|
45
47
|
dialect: "postgres", // "postgres" | "sqlite" | "d1" (D1 is TS-only)
|
|
48
|
+
extStyle: "js", // "js" (default) for Node ESM / plain tsc; "none" for a bundler-resolution toolchain — see SKILL.md "Your framework isn't the default"
|
|
46
49
|
apiPrefix: "/api", // flows to routes AND client fetch URLs
|
|
47
50
|
columnNamingStrategy: "snake_case", // "snake_case" (default) | "literal" | "kebab-case"
|
|
48
51
|
timestampMode: "string", // "string" (default, ISO-8601 wire contract) | "date" (Drizzle native Date)
|
|
@@ -80,7 +83,10 @@ PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself.
|
|
|
80
83
|
|
|
81
84
|
## The generators
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
Server-side, framework-neutral. The first four are **scaffolded into your repo** by
|
|
87
|
+
`meta init` and imported from `./codegen/generators/*` (ADR-0034); the rest come from the
|
|
88
|
+
package main entry, `@metaobjectsdev/codegen-ts`. Do **not** import any of them from
|
|
89
|
+
`@metaobjectsdev/codegen-ts/generators` — that subpath is deprecated and removed at 1.0.
|
|
84
90
|
|
|
85
91
|
| Generator | Emits per entity |
|
|
86
92
|
|---|---|
|
|
@@ -91,6 +97,7 @@ From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):
|
|
|
91
97
|
| `promptRender()` | `render<Name>()` per `template.prompt` |
|
|
92
98
|
| `outputParser()` | `<Name>.response.ts` (`parse*` / `safeParse*`) per **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: this tier is INBOUND; `template.output` is outbound only and emits nothing here). Siblings: `outputPrompt()` → `<Name>.responseFormat.ts` (the FR-010 output-format fragment, presentation via `@promptStyle`), `extractor()` → `<Name>.extractor.ts` (the tolerant `extract` mapper). |
|
|
93
99
|
| `callableFile()` | `<Entity>.callable.ts` — an FR-015 `call<Entity>` wrapper for a `source.rdb` `@kind: storedProc`/`tableFunction` (args from the `@parameterRef` value object, in declaration order) |
|
|
100
|
+
| `namesFile()` | `<Entity>.names.ts` — `export const <Entity>Names`, mirroring the object's metadata tree. Every node carries its own `type`/`subType`/`name`; `name` is the OBJECT's name, and a physical name sits under the key naming what it is: `sources.<role>.{table,view,materializedView,proc,function}` (`<role>` is `primary` or `replica`, so a write-through entity's read view has a slot), plus `sources.<role>.{kind,schema}`, `fields.<field>.{name,column}`, and `identities.<name>` / `indexes.<name>` carrying `.index` (the database index name) for `identity.secondary` and `index.lookup`. No `readOnly` — it was derived from `kind`, never declared. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns only, `sources: {}` — it has no table and must never acquire one). An artifact whose object extends another spreads the parent's collections rather than restating them, and a TPH subtype spreads `...AuthNames.sources` too, since it shares its base's table. With this generator in the run, **no generated TypeScript spells a physical name at all** — table, view, proc, column, schema and index name all travel as references; drop it from the suite and they fall back to literals. |
|
|
94
101
|
|
|
95
102
|
**Projections (read-only views).** For an `object.projection` (a read-only `source.rdb`
|
|
96
103
|
`@kind: view` child), `entityFile()` emits a `pgView(...)` + read-only Zod + a read-only
|
|
@@ -178,8 +185,73 @@ From `@metaobjectsdev/codegen-ts-tanstack`: `tanstackQuery()` → `<Entity>.hook
|
|
|
178
185
|
`tanstackGridHook()` → `<Entity>.grid.tsx`.
|
|
179
186
|
|
|
180
187
|
`entityFile({ allowlists: false })` drops the `runtime-ts/drizzle-fastify` import
|
|
181
|
-
for edge/worker consumers that don't mount server routes.
|
|
182
|
-
|
|
188
|
+
for edge/worker consumers that don't mount server routes.
|
|
189
|
+
|
|
190
|
+
**Wire a generator only for output you consume, and narrow it with its `filter`.**
|
|
191
|
+
Every generator factory takes `{ filter?: (entity) => boolean }`, ANDed with the
|
|
192
|
+
generator's built-in gates — so it can only NARROW what emits, never widen it:
|
|
193
|
+
`tanstackQuery({ filter: (e) => e.name !== "InternalAudit" })` emits no hooks for
|
|
194
|
+
that entity. There is no `@emit*` metadata attribute to do this — `@emitTanstack`,
|
|
195
|
+
`@emitRoutes`, `@emitForm`, `@emitGrid` and `@emitAngular` were never registered
|
|
196
|
+
vocabulary, so they passed `meta gen` and failed `meta verify`. If a project carries
|
|
197
|
+
one, `meta upgrade --apply` removes it.
|
|
198
|
+
|
|
199
|
+
The one thing a `filter` can't express is opting a TPH subtype IN to its own
|
|
200
|
+
per-subtype grid (that WIDENS): `tanstackGrid({ tphSubtypeGrids: (e) => … })`,
|
|
201
|
+
default `() => false`. Pass the same predicate to `tanstackGridHook()` or you get
|
|
202
|
+
a `<Sub>.grid.ts` whose `<Sub>.columns.tsx` is never emitted.
|
|
203
|
+
|
|
204
|
+
## Declarative template-codegen (Mustache)
|
|
205
|
+
|
|
206
|
+
Everything above is the **programmatic** path. A generator can also be **declarative** —
|
|
207
|
+
a Mustache template plus a scope, no generator code — and on TypeScript you have both.
|
|
208
|
+
Pick a template when the output SHAPE is what you are iterating on, or when you want the
|
|
209
|
+
same output across languages; pick programmatic when the logic is gnarly or the run is
|
|
210
|
+
hot.
|
|
211
|
+
|
|
212
|
+
**There is no `--template-spec` flag on `meta gen`.** Do not look for one and do not
|
|
213
|
+
report its absence as a gap. `metaobjects.config.ts` takes generator VALUES, so a
|
|
214
|
+
template generator is declared there like any other — which is also what keeps it
|
|
215
|
+
visible to `meta verify --codegen`, a gate that re-runs the config's generator list.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { templateGenerator } from "@metaobjectsdev/codegen-ts";
|
|
219
|
+
|
|
220
|
+
export default defineConfig({
|
|
221
|
+
generators: [
|
|
222
|
+
entityFile(),
|
|
223
|
+
templateGenerator({
|
|
224
|
+
name: "entity-service",
|
|
225
|
+
template: "service/entity-service", // → templates/service/entity-service.mustache
|
|
226
|
+
scope: "perEntity", // "perEntity" | "perPackage" | "perModel"
|
|
227
|
+
outputPattern: "{package}/{Name}Service.ts",
|
|
228
|
+
}),
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `template` resolves under the project's `templates/` dir first, then framework defaults.
|
|
234
|
+
- `outputPattern` placeholders: `{name}`, `{Name}`, `{package}` (its `::` segments become
|
|
235
|
+
nested directories). An unknown placeholder throws.
|
|
236
|
+
- `scope` and `walk` are mutually exclusive — supply exactly one. `walk` is the escape
|
|
237
|
+
hatch for a walk none of the three scopes expresses.
|
|
238
|
+
- Abstract objects are excluded from every scope.
|
|
239
|
+
|
|
240
|
+
**Reusing a C#/Python spec.** Those ports declare the same generators as a JSON
|
|
241
|
+
template-spec because their registries are closed and the flag is their only seam. Parse
|
|
242
|
+
it and spread it:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { parseTemplateSpec, templateSpecToGenerators } from "@metaobjectsdev/codegen-ts";
|
|
246
|
+
|
|
247
|
+
const spec = parseTemplateSpec(JSON.parse(readFileSync("./template-spec.json", "utf8")));
|
|
248
|
+
// generators: [entityFile(), ...templateSpecToGenerators(spec)]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Portability runs ONE way: TS also accepts a `target` field that the CLI ports reject, so
|
|
252
|
+
a spec written there always runs here, but not the reverse. Keep `target` out of a shared
|
|
253
|
+
spec. The data dict a template renders against is the cross-port byte-gated contract —
|
|
254
|
+
`docs/features/codegen-data-shapes.md`.
|
|
183
255
|
|
|
184
256
|
## Run
|
|
185
257
|
|
|
@@ -189,9 +261,18 @@ npx meta gen --dry-run # preview without writing
|
|
|
189
261
|
npx meta gen Author Post # scope to named entities
|
|
190
262
|
```
|
|
191
263
|
|
|
192
|
-
Generated files carry an `@generated by @metaobjectsdev/codegen-ts` header
|
|
193
|
-
|
|
194
|
-
|
|
264
|
+
Generated files carry an `@generated by @metaobjectsdev/codegen-ts` header. It is
|
|
265
|
+
**informational** — the write decision never reads it. `.metaobjects/.gen-state/`
|
|
266
|
+
decides: the snapshot body if this machine has one (three-way merge), otherwise the
|
|
267
|
+
committed `.hashes.json` (byte-for-byte what it wrote ⇒ overwrite; anything else ⇒
|
|
268
|
+
refused, path named, exit 1). So the merge is machine-local: a file you edited and
|
|
269
|
+
pushed is REFUSED on a fresh clone or in CI, not merged. Recovery is in
|
|
270
|
+
`docs/features/own-your-codegen.md`.
|
|
271
|
+
|
|
272
|
+
Hand-customizations that metadata can't express go in a sibling module you create and
|
|
273
|
+
import yourself — `<Entity>.extra.ts` by convention. The name carries no tool behaviour:
|
|
274
|
+
the file is safe because codegen writes only the paths it records, and the generated
|
|
275
|
+
barrel (built from the model, not a directory listing) does **not** re-export it.
|
|
195
276
|
|
|
196
277
|
**Output format:** `meta gen` (and the CLI generally) is TTY-aware — human-readable
|
|
197
278
|
text on a terminal, TOON on a pipe or agent. Override with `--format toon|json|text`.
|
|
@@ -231,3 +312,106 @@ The VO type, its Zod `InsertSchema`, and this `.$type<>()` all import the VO fro
|
|
|
231
312
|
the same module (layout/package/`extStyle`-aware resolution). An opaque jsonb column
|
|
232
313
|
(`field.string @dbColumnType: jsonb`) gets no `.$type<>()` — it stays `unknown`,
|
|
233
314
|
which is the correct shape for freeform payloads with no fixed VO.
|
|
315
|
+
|
|
316
|
+
## Retargeting to another framework — the TypeScript procedure
|
|
317
|
+
|
|
318
|
+
This is the TypeScript implementation of the retargeting doctrine in SKILL.md
|
|
319
|
+
("Your framework isn't the default"). Read that first for the order of moves;
|
|
320
|
+
everything below — `meta eject`, `metaobjects.config.ts` keys, the exported
|
|
321
|
+
`render*` functions — is Node-CLI-specific and exists only on this port.
|
|
322
|
+
|
|
323
|
+
The shipped reference templates emit for **Fastify on Node** (plus a Hono variant) with
|
|
324
|
+
Drizzle and Zod. If that is not your stack, retargeting is the **normal first move** — not
|
|
325
|
+
a workaround and not a sign of a bug. Each template's header carries a `targets:` line
|
|
326
|
+
naming exactly what its emit is coupled to and which call to swap.
|
|
327
|
+
|
|
328
|
+
Work the list in order; the first two cost nothing.
|
|
329
|
+
|
|
330
|
+
**1. Check the target-shaped config first.** Several apparent codegen failures are one
|
|
331
|
+
config value in `metaobjects.config.ts`:
|
|
332
|
+
|
|
333
|
+
- **`extStyle`** — `"js"` emits `./Entity.js` specifiers, correct for Node ESM and a plain
|
|
334
|
+
`tsc` with `nodenext`. Bundlers disagree on whether they perform the TypeScript
|
|
335
|
+
`.js`→`.ts` rewrite: it fails outright under **Turbopack** — including between two
|
|
336
|
+
generated files, which makes the whole generated tree unresolvable — while Vite and
|
|
337
|
+
esbuild are documented to accept it and webpack needs `resolve.extensionAlias` to do the
|
|
338
|
+
same. **If a generated import fails to resolve, set `extStyle: "none"` and retest** for
|
|
339
|
+
your toolchain rather than assuming either setting from this list.
|
|
340
|
+
- **`clientDirective`** — `true` prepends `"use client";` to the generated form, hooks,
|
|
341
|
+
columns and grid-hook modules. Defaults to `false`. **Set it if your framework compiles
|
|
342
|
+
server and client from one tree** (React Server Components — Next.js App Router and
|
|
343
|
+
friends); leave it off otherwise, where the directive is inert and some bundlers warn
|
|
344
|
+
about it.
|
|
345
|
+
- **`outDir`** / **`targets`** — where output lands, per generator.
|
|
346
|
+
- **`apiPrefix`**, **`dialect`** — route mounting and column mapping.
|
|
347
|
+
|
|
348
|
+
**2. Ask whether your framework splits the module graph.** Some frameworks compile server
|
|
349
|
+
and client from one source tree and resolve each half under *different export conditions*
|
|
350
|
+
(React Server Components, Angular universal, Qwik). Where they do:
|
|
351
|
+
|
|
352
|
+
- a generated artifact using client-only APIs may need a **marker directive** or a distinct
|
|
353
|
+
import path, and
|
|
354
|
+
- the resulting error frequently **names a package that is installed and present** — because
|
|
355
|
+
resolution failed under the server condition, not because the dependency is missing.
|
|
356
|
+
|
|
357
|
+
Read that error as a *boundary* problem, not a dependency problem. The fix belongs in the
|
|
358
|
+
generator that emits the artifact, which you own.
|
|
359
|
+
|
|
360
|
+
**3. If the emit is wrong for your framework, own the generator.**
|
|
361
|
+
|
|
362
|
+
meta eject --list # every template you can take ownership of
|
|
363
|
+
meta eject form # copies it to codegen/generators/form.ts
|
|
364
|
+
|
|
365
|
+
Then compose the engine and replace only the step that differs. Every generator's renderer
|
|
366
|
+
is exported, so wrapping is available — but **how much that buys you differs by tier, and
|
|
367
|
+
it is worth knowing which one you are in before you start**:
|
|
368
|
+
|
|
369
|
+
- **Entity module (`entity`)** — genuinely composable. `renderDrizzleSchema`,
|
|
370
|
+
`renderZodValidators`, `renderInferredTypes`, `renderFilterAllowlist` and friends are
|
|
371
|
+
separate exported sections the template assembles into a `Code[]`. Swap or drop one and
|
|
372
|
+
keep the rest.
|
|
373
|
+
- **Routes and UI (`routes`, `routes-hono`, `form`, `hooks`, `grid`, `grid-hook`)** — one
|
|
374
|
+
whole-file renderer each, so "replace a step" really means wrap the whole output. That
|
|
375
|
+
is enough for a marker directive, a header, or a post-process, and it is what the RSC
|
|
376
|
+
case below needs. It is **not** enough to retarget the emitted framework: if you need
|
|
377
|
+
Svelte or Angular instead of React, you are writing a renderer, and the honest move is
|
|
378
|
+
to keep the generator's metadata walk and replace the render call entirely.
|
|
379
|
+
|
|
380
|
+
**`"use client"` needs no ejecting at all — it is a config knob.** The generated form,
|
|
381
|
+
hooks, columns and grid-hook modules are client components; React Server Components
|
|
382
|
+
frameworks (Next.js App Router and friends) require the directive saying so. Set it once:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
export default defineConfig({
|
|
386
|
+
clientDirective: true, // prepend `"use client";` to generated client artifacts
|
|
387
|
+
// ...
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Defaults to `false`, because the directive is only *required* under RSC and is inert
|
|
392
|
+
(and warned about by some bundlers) everywhere else. It is applied ahead of the
|
|
393
|
+
`@generated` header, exactly once, and only to the four client artifacts — the entity
|
|
394
|
+
module, the query helpers and `<Entity>.meta.ts` are untouched, since `.meta.ts` is plain
|
|
395
|
+
data and in RSC the boundary is the importing component, not everything it reaches.
|
|
396
|
+
|
|
397
|
+
For the general wrap-the-output case — a directive or header MetaObjects does not model:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
// codegen/generators/form.ts — OWNED
|
|
401
|
+
import { renderFormFile } from "@metaobjectsdev/codegen-ts-react";
|
|
402
|
+
|
|
403
|
+
// ...inside generate():
|
|
404
|
+
if (!ctx.renderContext) throw new Error("renderContext is required (provided by runGen)");
|
|
405
|
+
const body = renderFormFile(entity, ctx.renderContext);
|
|
406
|
+
return { path, content: `// @my-framework:client\n` + body };
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
You keep receiving upstream fixes to `renderFormFile` while owning the one line your
|
|
410
|
+
framework cares about. **Forking the whole renderer is the thing to avoid**, not owning the
|
|
411
|
+
generator.
|
|
412
|
+
|
|
413
|
+
**4. Server-tier output is usually already portable.** The entity module (a table plus
|
|
414
|
+
validation schemas) and the query helpers (which take `db` as a parameter rather than
|
|
415
|
+
importing a singleton) carry no HTTP-framework coupling — a server-rendered component can
|
|
416
|
+
call a generated query directly. Retargeting is usually only needed at the routes and UI
|
|
417
|
+
tiers.
|
|
@@ -22,7 +22,16 @@ provider/LLM-call layer — you compose the call yourself.
|
|
|
22
22
|
```ts
|
|
23
23
|
// metaobjects.config.ts
|
|
24
24
|
import { defineConfig } from "@metaobjectsdev/cli";
|
|
25
|
-
|
|
25
|
+
// The entity trio + barrel come from the OWNED copies `meta init` scaffolded into
|
|
26
|
+
// ./codegen/generators/ (ADR-0034). Importing them from the package instead is the
|
|
27
|
+
// deprecated path, and quietly hands their shape back to the package — so keep these
|
|
28
|
+
// lines as `meta init` wrote them and add only the prompt pair below.
|
|
29
|
+
import { entityFile } from "./codegen/generators/entity.js";
|
|
30
|
+
import { queriesFile } from "./codegen/generators/queries.js";
|
|
31
|
+
import { barrel } from "./codegen/generators/barrel.js";
|
|
32
|
+
// promptRender / outputParser are NOT in the ownable set — the render and parse engines
|
|
33
|
+
// are upstream-owned, so importing them from the package is the supported pattern.
|
|
34
|
+
import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators";
|
|
26
35
|
|
|
27
36
|
export default defineConfig({
|
|
28
37
|
outDir: "src/generated",
|