@metaobjectsdev/sdk 0.12.3 → 0.12.4-rc.1

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.
@@ -15,36 +15,51 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a
15
15
  `package` on its root node. Files in the same `package` with the same object
16
16
  `name` are merged by the loader.
17
17
 
18
- ## Find the construct first — model it, don't hand-write it
18
+ ## The operating principle: model-first, generate-first
19
+
20
+ You are not hand-writing an application — you are **declaring the model it is
21
+ generated from.** Persistence, data access, validation, APIs, and UI scaffolding are
22
+ **derived from metadata, never authored by hand.** Model-first is the default for
23
+ *every* capability; hand-writing one of these layers is an exception you must
24
+ **justify**, not a convenience you reach for.
25
+
26
+ **This requires thinking differently.** Imperative code asks *"how do I implement
27
+ this endpoint?"* Model-first asks *"what is this resource, and what is true about
28
+ it?"* — and lets codegen own the *how*. **Describe WHAT, not HOW.** The metadata is
29
+ the source of truth; generated code is a disposable, regenerable artifact — delete
30
+ it and `meta gen` restores it identically.
31
+
32
+ **Why model-first wins even when hand-writing is cheaper this once — and it often
33
+ is, this once:**
34
+ - **Hand-writing a layer the metadata could own creates a second source of truth for
35
+ one fact.** A field's type, validation, column, route, and form then change in N
36
+ places and must stay consistent forever — not drift *risk*, but two sources of
37
+ truth for one fact, broken by construction.
38
+ - **The hand-roll saving is paid once; the consistency tax is paid on every future
39
+ change.** Assume the system will grow — it always does. The metadata amortizes
40
+ toward zero as the model is reused across layers and time; the hand-rolled
41
+ liability compounds with every field, refactor, and language port.
42
+ - **One metadata change regenerates persistence + DAO + API + UI consistently** —
43
+ and inherits every future generator improvement. Hand-writing opts out of all of
44
+ it, permanently.
45
+
46
+ **Before you hand-write anything data-shaped, STOP and find the model.** The moment
47
+ you reach for a hand-written query, route, validator, form, relationship, or
48
+ aggregate — that is almost always **metadata you have not declared yet.** In order:
49
+ 1. **Search the vocabulary** — `meta types <term>`, or `meta types --all
50
+ <what-it-does>` to search by behavior. There are field subtypes, relationships,
51
+ projections, origins, identities, sources, and attributes you may not know exist.
52
+ Find the construct that models it.
53
+ 2. **Declare it and generate** — then *consume* the generated query/type/route;
54
+ never reimplement it alongside.
55
+ 3. **Only if no construct can express it** — and you have actually looked —
56
+ hand-write it, wired to generated types. Business algorithms, external
57
+ integrations, and bespoke interactions are legitimately hand-written; CRUD,
58
+ validation, finders, relationships, and derived/aggregate data are not.
59
+
60
+ Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
61
+ call** — even when a one-off hand-write would be faster today.
19
62
 
20
- You do **not** know the full vocabulary from memory, and it is larger and more
21
- powerful than the basics below. Before authoring anything non-trivial, and the
22
- moment you reach for hand-written data logic, **search the live metamodel**:
23
-
24
- ```
25
- meta types relationship # find by name
26
- meta types --all aggregate # find by WHAT IT DOES (searches descriptions)
27
- meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs
28
- ```
29
-
30
- **The rule:** before you hand-write any data logic — a join, a foreign key, a
31
- derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field
32
- rule, a relationship between entities, a derived read model — run `meta types` and
33
- check for a construct that **declares** it. If one exists, declare it instead. That
34
- is the entire point of MetaObjects: declared metadata is generated, typed, and
35
- regenerates on change; hand-written logic drifts and is the thing this tool exists
36
- to eliminate. When you catch yourself writing a query, a validator, or an FK by
37
- hand, stop and search the types first.
38
-
39
- Two on-disk formats, one shape:
40
-
41
- - **Canonical JSON** — the on-disk interchange. Every node is a single-key map
42
- whose key fuses the type and subtype.
43
- - **YAML** — the sigil-free authoring front-end. Lowered to canonical JSON at load
44
- time, so it shares the entire downstream pipeline.
45
-
46
- Author in whichever fits the project. Prefer YAML for new hand-authored metadata
47
- (it's less noisy); JSON is the format conformance fixtures and tooling pin.
48
63
 
49
64
  ## The fused-key encoding (non-negotiable)
50
65
 
@@ -86,15 +86,63 @@ the data access too.
86
86
  `meta migrate` its DB view), and you **call that generated query from your
87
87
  route**. Declaring the projection is only half the win — *consuming* its
88
88
  generated query is the other half.
89
- - **Codegen is yours to extend.** A generated file carries the `@generated` header
90
- and is a normal source file: copy it and customize the copy (three-way merge
91
- preserves your edits on regen), or write your own `Generator` (the plugin
92
- interface) and add it to the `generators` array for an artifact the built-ins
93
- don't cover.
94
89
 
95
90
  `meta gen --list` prints every generator by stable name; the `generators` array in
96
91
  `metaobjects.config.ts` is where you opt each one in or out.
97
92
 
93
+ ## Write your own generators — the built-ins rarely fit an app exactly
94
+
95
+ The built-in generators (entity, queries, routes, form, grid, barrel) cover the
96
+ common shape, but **real apps routinely need output the built-ins don't emit as-is**
97
+ — a bespoke REST contract, custom DTO/response shapes, an app-specific service or
98
+ repository layer, a UI the defaults don't produce. When that happens the model-first
99
+ move is **not** to abandon metadata and hand-write the layer. Write a **custom
100
+ generator** that reads the same metadata and emits *your* app's shape.
101
+
102
+ Treat this as a first-class, expected activity — not an escape hatch. A custom
103
+ generator is still model-first: it derives from the metadata spine, so it
104
+ regenerates on change and stays consistent across every entity — the leverage you'd
105
+ forfeit by hand-writing. Hand-rolling *away from* metadata is the anti-pattern;
106
+ generating *your own shape from* metadata is the point.
107
+
108
+ The plugin interface is small (`@metaobjectsdev/codegen-ts`): a `Generator` is
109
+ `{ name, filter?, generate }`, where `generate(ctx)` returns `EmittedFile[]`
110
+ (`{ path, content }`). `perEntity` / `oncePerRun` wrap the common cases:
111
+
112
+ ```ts
113
+ import { perEntity } from "@metaobjectsdev/codegen-ts";
114
+ import type { Generator } from "@metaobjectsdev/codegen-ts";
115
+
116
+ // One file per entity, in YOUR shape — reads the loaded metadata, emits your code.
117
+ export function serviceFile(): Generator {
118
+ return {
119
+ name: "service-file", // kebab-case; shows in `meta gen --list`
120
+ filter: (e) => e.isEntity, // which nodes it applies to
121
+ generate: perEntity((entity, ctx) => ({
122
+ path: `${entity.name}.service.ts`,
123
+ content: renderYourService(entity.fields(), ctx), // walk the typed metadata
124
+ })),
125
+ };
126
+ }
127
+ ```
128
+
129
+ `ctx` gives you `entities`, the `loadedRoot`, and `config`; `oncePerRun((entities,
130
+ ctx) => …)` is the one-shot variant (a barrel, an app-config). Add your generator to
131
+ the `generators` array in `metaobjects.config.ts` next to the built-ins — it runs in
132
+ the same pass, writes under the same target rules, and carries the `@generated`
133
+ header so it round-trips like any other.
134
+
135
+ **Close but not exact?** You don't always need a new generator — a generated file is
136
+ a normal source file. Copy it and customize the copy (three-way merge preserves your
137
+ edits on regen), or customize the template a built-in renders from. Reach for a
138
+ custom generator when you want the change applied **consistently across every
139
+ entity** (the scale win); a one-off edit when it's genuinely one file.
140
+
141
+ **The decision ladder:** a built-in fits → use it · close → customize the
142
+ output/template · doesn't fit → write a generator that emits your shape *from the
143
+ metadata* · only the genuinely un-modelable (business algorithms, external calls) is
144
+ hand-written outside codegen — and it still imports the generated types.
145
+
98
146
  ## Dialects
99
147
 
100
148
  Generated DB schema/DDL targets a SQL **dialect**:
@@ -30,6 +30,20 @@ Two more are caught structurally rather than by a command: **generated-edited**
30
30
  **migration-vs-metadata** (migrations are emitted *from* metadata diffs, so they
31
31
  can't drift by construction).
32
32
 
33
+ ## Run `meta verify` before you call a build done
34
+
35
+ Make a bare `meta verify` the last step before you consider any MetaObjects work
36
+ finished — not only in CI. Besides the drift checks below, a bare `verify` (and
37
+ every `meta gen`) runs an **advisory anti-pattern pass**: it scans your authored
38
+ source and flags where you hand-rolled something the metadata could model, naming
39
+ the construct that replaces it — a hand-written aggregate (`AVG`/`reduce`-sum →
40
+ `origin.aggregate` on an `object.projection`), money as a float (`* 100`/`toFixed`
41
+ → `field.currency`), a `CHECK (... IN ...)` value set (→ `field.enum`). It is
42
+ advisory (never fails the build), but each line is the fix: when you see one, model
43
+ it and call the generated query/field instead of keeping the hand-rolled version.
44
+ This is the most common way a build ends up *declaring* a projection yet still
45
+ hand-aggregating in a route — verify catches exactly that.
46
+
33
47
  ## The `verify` subverbs
34
48
 
35
49
  `verify` has three drift checks. Run them in CI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/sdk",
3
- "version": "0.12.3",
3
+ "version": "0.12.4-rc.1",
4
4
  "description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@metaobjectsdev/metadata": "0.12.3",
59
+ "@metaobjectsdev/metadata": "0.12.4-rc.1",
60
60
  "zod": "^3.23.0"
61
61
  },
62
62
  "devDependencies": {