@metaobjectsdev/sdk 0.12.3 → 0.12.4
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
|
-
##
|
|
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
|
+
"version": "0.12.4",
|
|
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.
|
|
59
|
+
"@metaobjectsdev/metadata": "0.12.4",
|
|
60
60
|
"zod": "^3.23.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|