@metaobjectsdev/codegen-ts 0.20.9 → 0.20.10-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.
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +9 -5
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/barrel.js +1 -1
- package/dist/generators/barrel.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +7 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +6 -1
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts.map +1 -1
- package/dist/generators/output-parser-file.js +6 -1
- package/dist/generators/output-parser-file.js.map +1 -1
- package/dist/generators/queries-file.d.ts +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +11 -6
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +4 -0
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +6 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +4 -0
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +6 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/naming/collision-names.d.ts +19 -0
- package/dist/naming/collision-names.d.ts.map +1 -0
- package/dist/naming/collision-names.js +79 -0
- package/dist/naming/collision-names.js.map +1 -0
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +5 -71
- package/dist/payload-codegen.js.map +1 -1
- package/dist/render-context.d.ts +35 -3
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +23 -0
- package/dist/render-context.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +36 -2
- package/dist/runner.js.map +1 -1
- package/dist/source-detect.d.ts +10 -0
- package/dist/source-detect.d.ts.map +1 -1
- package/dist/source-detect.js +22 -0
- package/dist/source-detect.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +14 -3
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-file.d.ts +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +13 -3
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/extract-delegate-emitter.d.ts +9 -4
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +46 -30
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts +2 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +48 -33
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +1 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +39 -16
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/output-parser.d.ts +2 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +37 -12
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/projection-decl.d.ts +1 -1
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +20 -9
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +5 -1
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +12 -2
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +21 -15
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +1 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +35 -22
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/generators/api-model.ts +9 -5
- package/src/generators/barrel.ts +1 -1
- package/src/generators/entity-file.ts +7 -1
- package/src/generators/extractor-file.ts +6 -1
- package/src/generators/output-parser-file.ts +6 -1
- package/src/generators/queries-file.ts +12 -7
- package/src/generators/routes-file-hono.ts +7 -1
- package/src/generators/routes-file.ts +6 -1
- package/src/index.ts +1 -1
- package/src/naming/collision-names.ts +87 -0
- package/src/payload-codegen.ts +4 -79
- package/src/reference/barrel.ts +1 -1
- package/src/reference/queries.ts +14 -6
- package/src/reference/routes.ts +8 -1
- package/src/render-context.ts +51 -3
- package/src/runner.ts +36 -4
- package/src/source-detect.ts +20 -0
- package/src/templates/drizzle-schema.ts +14 -3
- package/src/templates/entity-file.ts +14 -6
- package/src/templates/extract-delegate-emitter.ts +52 -27
- package/src/templates/extractor.ts +49 -32
- package/src/templates/inferred-types.ts +39 -17
- package/src/templates/output-parser.ts +40 -12
- package/src/templates/projection-decl.ts +20 -10
- package/src/templates/value-object-file.ts +5 -1
- package/src/templates/view-decl.ts +30 -18
- package/src/templates/zod-validators.ts +35 -23
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ADR-0044 collision-scoped naming — shared across every codegen tier that emits
|
|
2
|
+
// declarations from a reference closure (payload records today; the entity +
|
|
3
|
+
// extract/output-parser tiers per issue #228).
|
|
4
|
+
//
|
|
5
|
+
// `assignEmittedNames` is a PURE function of the closure: a bare short name
|
|
6
|
+
// unique in the closure emits bare; a collision emits EVERY member under its
|
|
7
|
+
// package-qualified derived name (PascalCase each package segment + short
|
|
8
|
+
// name). A still-colliding derived name fails loud (ERR_PAYLOAD_NAME_COLLISION).
|
|
9
|
+
|
|
10
|
+
import { type ErrorCode, type MetaData, PACKAGE_SEPARATOR } from "@metaobjectsdev/metadata";
|
|
11
|
+
|
|
12
|
+
// ADR-0044 backstop error code — a codegen-time (not loader) error, peer of
|
|
13
|
+
// @metaobjectsdev/render's ERR_VAR_NOT_ON_PAYLOAD. Already promoted to the shared
|
|
14
|
+
// cross-language error-code ledger in 0.19.3 (packages/metadata/src/errors.ts's
|
|
15
|
+
// ERROR_CODES, fixtures/conformance/ERROR-CODES.json, server/python/src/metaobjects/errors.py).
|
|
16
|
+
// That ledger has no per-code named export (only the ERROR_CODES array + the ErrorCode
|
|
17
|
+
// union type), so this stays a local literal — but `satisfies ErrorCode` binds it to the
|
|
18
|
+
// shared ledger's type: a future rename/removal there fails this file's typecheck instead
|
|
19
|
+
// of silently drifting.
|
|
20
|
+
export const ERR_PAYLOAD_NAME_COLLISION = "ERR_PAYLOAD_NAME_COLLISION" satisfies ErrorCode;
|
|
21
|
+
|
|
22
|
+
function pascalSegment(s: string): string {
|
|
23
|
+
return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** ADR-0044 — package-qualified derived name for a collision member: PascalCase
|
|
27
|
+
* each `::`-segment of the node's effective package, concatenated, then the
|
|
28
|
+
* bare short name (`acme::alpha::Note` -> `AcmeAlphaNote`). A root-level
|
|
29
|
+
* (no-package) node has nothing to qualify with and keeps its bare name — two
|
|
30
|
+
* root-level VOs can never share a name (the loader's own-package uniqueness
|
|
31
|
+
* already rejects that), so this can't silently under-qualify. */
|
|
32
|
+
export function packageQualifiedName(pkg: string, shortName: string): string {
|
|
33
|
+
if (pkg === "") return shortName;
|
|
34
|
+
return (
|
|
35
|
+
pkg
|
|
36
|
+
.split(PACKAGE_SEPARATOR)
|
|
37
|
+
.map(pascalSegment)
|
|
38
|
+
.join("") + shortName
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* ADR-0044 pass 2 — assign the emitted TS name for every VO in the closure. A
|
|
44
|
+
* PURE function of the closure's (fqn, bareName, package) triples — never of
|
|
45
|
+
* traversal order: bare short name unique in the closure -> bare name; a
|
|
46
|
+
* collision -> EVERY member gets its package-qualified derived name. If two
|
|
47
|
+
* DISTINCT fqns still derive the same name after qualification, throws
|
|
48
|
+
* (ERR_PAYLOAD_NAME_COLLISION) — never silently wrong.
|
|
49
|
+
*/
|
|
50
|
+
export function assignEmittedNames(closure: ReadonlyMap<string, MetaData>): Map<string, string> {
|
|
51
|
+
const byShortName = new Map<string, string[]>();
|
|
52
|
+
for (const [fqn, node] of closure) {
|
|
53
|
+
const bucket = byShortName.get(node.name);
|
|
54
|
+
if (bucket) bucket.push(fqn);
|
|
55
|
+
else byShortName.set(node.name, [fqn]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const nameMap = new Map<string, string>();
|
|
59
|
+
for (const [shortName, fqns] of byShortName) {
|
|
60
|
+
if (fqns.length === 1) {
|
|
61
|
+
nameMap.set(fqns[0]!, shortName);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const fqn of fqns) {
|
|
65
|
+
const node = closure.get(fqn)!;
|
|
66
|
+
const pkg = node.package ?? node.fileDefaultPackage ?? "";
|
|
67
|
+
nameMap.set(fqn, packageQualifiedName(pkg, shortName));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Backstop — sorted by fqn so which pair the message names (and whether the
|
|
72
|
+
// set of colliding names is non-empty) is a pure function of the closure, not
|
|
73
|
+
// of Map insertion/traversal order.
|
|
74
|
+
const ownerOf = new Map<string, string>();
|
|
75
|
+
for (const fqn of [...nameMap.keys()].sort()) {
|
|
76
|
+
const emitted = nameMap.get(fqn)!;
|
|
77
|
+
const existing = ownerOf.get(emitted);
|
|
78
|
+
if (existing !== undefined && existing !== fqn) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${ERR_PAYLOAD_NAME_COLLISION}: payload record name collision: "${emitted}" derives from both "${existing}" and "${fqn}" — rename one value-object or move it to a package that derives a distinct name`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
ownerOf.set(emitted, fqn);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return nameMap;
|
|
87
|
+
}
|
package/src/payload-codegen.ts
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
// unique in the closure emits bare; a collision emits
|
|
20
20
|
// EVERY member under its package-qualified derived
|
|
21
21
|
// name (PascalCase each package segment + short name).
|
|
22
|
-
// A still-colliding derived name fails loud.
|
|
22
|
+
// A still-colliding derived name fails loud. Lives in
|
|
23
|
+
// ./naming/collision-names.js — shared with the
|
|
24
|
+
// entity + extract/output-parser tiers (#228).
|
|
23
25
|
// 3. emitClosureDeclarations — emit each declaration + every reference through
|
|
24
26
|
// the name map.
|
|
25
27
|
|
|
@@ -35,12 +37,12 @@ import {
|
|
|
35
37
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
36
38
|
TEMPLATE_ATTR_TEXT_REF,
|
|
37
39
|
TEMPLATE_ATTR_FORMAT,
|
|
38
|
-
PACKAGE_SEPARATOR,
|
|
39
40
|
resolveObjectRef,
|
|
40
41
|
stripPackage,
|
|
41
42
|
} from "@metaobjectsdev/metadata";
|
|
42
43
|
import { enumValues } from "./enum-meta.js";
|
|
43
44
|
import { enumUnionAliasName, enumUnionString } from "./templates/inferred-types.js";
|
|
45
|
+
import { assignEmittedNames } from "./naming/collision-names.js";
|
|
44
46
|
|
|
45
47
|
const SCALAR_TS: Record<string, string> = {
|
|
46
48
|
string: "string",
|
|
@@ -59,16 +61,6 @@ const SCALAR_TS: Record<string, string> = {
|
|
|
59
61
|
timestamp: "string",
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
// ADR-0044 backstop error code — a codegen-time (not loader) error, peer of
|
|
63
|
-
// @metaobjectsdev/render's ERR_VAR_NOT_ON_PAYLOAD. Declared LOCALLY rather than
|
|
64
|
-
// added to packages/metadata/src/errors.ts's ERROR_CODES ledger: that ledger is
|
|
65
|
-
// checked for FULL cross-port agreement against fixtures/conformance/ERROR-CODES.json
|
|
66
|
-
// (packages/metadata/test/errors.test.ts) and, on the Python side, for corpus-code
|
|
67
|
-
// coverage — registering it there before every port implements the ADR-0044 fix
|
|
68
|
-
// would turn those OTHER ports' tests red. It moves into the shared ledger once the
|
|
69
|
-
// Java/Kotlin/Python follow-up (ADR-0044 §4, items 3-4) lands alongside this code.
|
|
70
|
-
const ERR_PAYLOAD_NAME_COLLISION = "ERR_PAYLOAD_NAME_COLLISION";
|
|
71
|
-
|
|
72
64
|
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
73
65
|
// ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
|
|
74
66
|
function findObject(root: MetaData, name: string, referrerPkg = ""): MetaData | undefined {
|
|
@@ -108,73 +100,6 @@ function collectClosure(
|
|
|
108
100
|
}
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
function pascalSegment(s: string): string {
|
|
112
|
-
return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** ADR-0044 — package-qualified derived name for a collision member: PascalCase
|
|
116
|
-
* each `::`-segment of the node's effective package, concatenated, then the
|
|
117
|
-
* bare short name (`acme::alpha::Note` -> `AcmeAlphaNote`). A root-level
|
|
118
|
-
* (no-package) node has nothing to qualify with and keeps its bare name — two
|
|
119
|
-
* root-level VOs can never share a name (the loader's own-package uniqueness
|
|
120
|
-
* already rejects that), so this can't silently under-qualify. */
|
|
121
|
-
function packageQualifiedName(pkg: string, shortName: string): string {
|
|
122
|
-
if (pkg === "") return shortName;
|
|
123
|
-
return (
|
|
124
|
-
pkg
|
|
125
|
-
.split(PACKAGE_SEPARATOR)
|
|
126
|
-
.map(pascalSegment)
|
|
127
|
-
.join("") + shortName
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* ADR-0044 pass 2 — assign the emitted TS name for every VO in the closure. A
|
|
133
|
-
* PURE function of the closure's (fqn, bareName, package) triples — never of
|
|
134
|
-
* traversal order: bare short name unique in the closure -> bare name; a
|
|
135
|
-
* collision -> EVERY member gets its package-qualified derived name. If two
|
|
136
|
-
* DISTINCT fqns still derive the same name after qualification, throws
|
|
137
|
-
* (ERR_PAYLOAD_NAME_COLLISION) — never silently wrong.
|
|
138
|
-
*/
|
|
139
|
-
function assignEmittedNames(closure: ReadonlyMap<string, MetaData>): Map<string, string> {
|
|
140
|
-
const byShortName = new Map<string, string[]>();
|
|
141
|
-
for (const [fqn, node] of closure) {
|
|
142
|
-
const bucket = byShortName.get(node.name);
|
|
143
|
-
if (bucket) bucket.push(fqn);
|
|
144
|
-
else byShortName.set(node.name, [fqn]);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const nameMap = new Map<string, string>();
|
|
148
|
-
for (const [shortName, fqns] of byShortName) {
|
|
149
|
-
if (fqns.length === 1) {
|
|
150
|
-
nameMap.set(fqns[0]!, shortName);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
for (const fqn of fqns) {
|
|
154
|
-
const node = closure.get(fqn)!;
|
|
155
|
-
const pkg = node.package ?? node.fileDefaultPackage ?? "";
|
|
156
|
-
nameMap.set(fqn, packageQualifiedName(pkg, shortName));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Backstop — sorted by fqn so which pair the message names (and whether the
|
|
161
|
-
// set of colliding names is non-empty) is a pure function of the closure, not
|
|
162
|
-
// of Map insertion/traversal order.
|
|
163
|
-
const ownerOf = new Map<string, string>();
|
|
164
|
-
for (const fqn of [...nameMap.keys()].sort()) {
|
|
165
|
-
const emitted = nameMap.get(fqn)!;
|
|
166
|
-
const existing = ownerOf.get(emitted);
|
|
167
|
-
if (existing !== undefined && existing !== fqn) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`${ERR_PAYLOAD_NAME_COLLISION}: payload record name collision: "${emitted}" derives from both "${existing}" and "${fqn}" — rename one value-object or move it to a package that derives a distinct name`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
ownerOf.set(emitted, fqn);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return nameMap;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
103
|
/** Resolve `ref`'s emitted TS interface name under the ADR-0044 naming rule,
|
|
179
104
|
* scoped to `ref`'s OWN reference closure (the same closure
|
|
180
105
|
* `generatePayloadInterfaces(root, ref, referrerPkg)` would emit). Returns
|
package/src/reference/barrel.ts
CHANGED
|
@@ -51,7 +51,7 @@ export const barrel = function barrel(opts?: BarrelOpts): Generator {
|
|
|
51
51
|
path: "index.ts",
|
|
52
52
|
content: await formatTs(
|
|
53
53
|
renderBarrel(
|
|
54
|
-
entities.map((e) => ({ name: e
|
|
54
|
+
entities.map((e) => ({ name: ctx.renderContext!.valueObjectEmittedName(e), package: e.package })),
|
|
55
55
|
ctx.renderContext!.extStyle,
|
|
56
56
|
ctx.renderContext!.selfTarget,
|
|
57
57
|
ctx.renderContext!.entityModuleTarget,
|
package/src/reference/queries.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// use-when: you want generated typed CRUD finders (find<E>ById, list<E>s, create/update/delete)
|
|
6
6
|
// over Drizzle. Drop it if you hand-write your data access.
|
|
7
|
-
// emits: <target>/<Entity>.queries.ts per
|
|
7
|
+
// emits: <target>/<Entity>.queries.ts per source-backed object (any source.rdb kind,
|
|
8
|
+
// incl. read-only projections) — skipped for sourceless objects (incl. every
|
|
9
|
+
// object.value, source-less by value purity) and TPH subtypes (#248 R2).
|
|
8
10
|
// customize: the vanilla CRUD assembly below is OWNED — reorder, drop verbs (e.g. no delete),
|
|
9
11
|
// change the Db type alias, add your own finders. The render<Verb>Fn primitives emit
|
|
10
12
|
// each block; call your own instead to change a verb's body.
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
// out of the package source. The vanilla path here is byte-identical to the built-in.
|
|
16
18
|
|
|
17
19
|
import { code, joinCode, type Code } from "ts-poet";
|
|
18
|
-
import {
|
|
20
|
+
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
19
21
|
import {
|
|
20
22
|
perEntity,
|
|
21
23
|
type Generator,
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
isProjection,
|
|
34
36
|
isWriteThrough,
|
|
35
37
|
isTphSubtype,
|
|
38
|
+
hasAnyRdbSource,
|
|
36
39
|
renderQueriesFile, // engine composer — used for the delegated variants
|
|
37
40
|
formatTs,
|
|
38
41
|
entityOutputPath,
|
|
@@ -105,10 +108,15 @@ export interface QueriesFileOpts {
|
|
|
105
108
|
target?: string;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
// #248 R2: persistability derives from declared/inherited source, never subtype.
|
|
112
|
+
// An object with no source.rdb (of ANY kind) isn't backed by any store — the
|
|
113
|
+
// rendered queries module would emit findById/updateById/deleteById against
|
|
114
|
+
// Drizzle table/schema exports the entity file never emits for it (value
|
|
115
|
+
// objects are subsumed here too: value purity bans sources on them, so no
|
|
116
|
+
// loadable value ever has hasAnyRdbSource === true). TPH subtypes emit no
|
|
117
|
+
// standalone queries file either — their per-subtype CRUD helpers live in the
|
|
118
|
+
// discriminator base's queries file (which targets the single shared table).
|
|
119
|
+
const skipNonQueryable = (e: MetaObject): boolean => hasAnyRdbSource(e) && !isTphSubtype(e);
|
|
112
120
|
|
|
113
121
|
export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generator {
|
|
114
122
|
const userFilter = opts?.filter;
|
package/src/reference/routes.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// if you need bespoke endpoints — or keep it and add handlers via <Entity>.extra.ts.
|
|
7
7
|
// emits: <target>/<Entity>.routes.ts — full CRUD for write-through entities, read-only
|
|
8
8
|
// (GET list + GET :id) for projections, polymorphic + per-subtype for TPH bases.
|
|
9
|
+
// Skipped for any sourceless object (incl. every object.value, source-less by
|
|
10
|
+
// value purity) and for TPH subtypes — no source.rdb means no table/allowlist
|
|
11
|
+
// for a routes file to import (#248 R2).
|
|
9
12
|
// customize: this generator (filter, output path, per-entity @emitRoutes opt-out, target) is
|
|
10
13
|
// YOURS — edit it freely. The route *composition* itself is richer than the others
|
|
11
14
|
// (M:N junction traversal, TPH per-subtype route sets), so it stays in the engine via
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
type GeneratorFactory,
|
|
24
27
|
renderRoutesFile,
|
|
25
28
|
isTphSubtype,
|
|
29
|
+
hasAnyRdbSource,
|
|
26
30
|
formatTs,
|
|
27
31
|
entityOutputPath,
|
|
28
32
|
CODEGEN_ATTR_EMIT_ROUTES,
|
|
@@ -39,9 +43,12 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
|
|
|
39
43
|
name: "routes-file",
|
|
40
44
|
// per-entity opt-out via `@emitRoutes: false`; TPH subtypes get no standalone routes
|
|
41
45
|
// file (their routes live in the discriminator base's); AND-composed with your filter.
|
|
46
|
+
// #248 R2: an object with no declared/inherited source.rdb (of ANY kind) isn't
|
|
47
|
+
// backed by any store — routes against it would import Drizzle table/allowlist
|
|
48
|
+
// exports the entity file never emits. Gated by hasAnyRdbSource.
|
|
42
49
|
filter: (e: MetaObject) =>
|
|
43
50
|
// ADR-0039: resolving — a concrete entity may inherit its @emit* opt-out flag via extends.
|
|
44
|
-
e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && !isTphSubtype(e) && userFilter(e),
|
|
51
|
+
e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
|
|
45
52
|
generate: perEntity(async (entity, ctx) => {
|
|
46
53
|
if (!ctx.renderContext) {
|
|
47
54
|
throw new Error("routes-file: renderContext is required (provided by runGen)");
|
package/src/render-context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// RenderContext — cross-cutting state passed to every template.
|
|
2
2
|
|
|
3
|
-
import type { MetaRoot } from "@metaobjectsdev/metadata";
|
|
3
|
+
import type { MetaRoot, MetaData, MetaField } from "@metaobjectsdev/metadata";
|
|
4
|
+
import { resolveObjectRef, stripPackage } from "@metaobjectsdev/metadata";
|
|
4
5
|
import type { Dialect } from "./column-mapper.js";
|
|
5
6
|
import type { PkInfo } from "./pk-resolver.js";
|
|
6
7
|
import type { RelationMap } from "./relation-resolver.js";
|
|
@@ -67,8 +68,28 @@ export interface RenderContext {
|
|
|
67
68
|
pkMap: Map<string, PkInfo>;
|
|
68
69
|
/** Pre-pass relation map for FK + relations() block emission. */
|
|
69
70
|
relationMap: RelationMap;
|
|
70
|
-
/**
|
|
71
|
+
/** Object name → its metadata package (undefined if the object has no package).
|
|
72
|
+
* Built once per run. Value objects are keyed by their ADR-0044 EMITTED name
|
|
73
|
+
* (bare when unique, package-qualified on a cross-package short-name collision;
|
|
74
|
+
* #228) so `valueObjectModuleSpecifier` resolves the right module; entities and
|
|
75
|
+
* other objects are keyed by their bare name. */
|
|
71
76
|
packageOf: Map<string, string | undefined>;
|
|
77
|
+
/** ADR-0044/#228 — `resolutionKey()` → emitted TS name for every emitted
|
|
78
|
+
* `object.value` in the run. A PURE function of the run's value-object set
|
|
79
|
+
* (collision-scoped): a bare short name unique in the set stays bare; a
|
|
80
|
+
* cross-package short-name collision qualifies EVERY member. Empty by default
|
|
81
|
+
* (bare names — byte-identical to pre-#228 output). */
|
|
82
|
+
valueObjectNames: ReadonlyMap<string, string>;
|
|
83
|
+
/** The ADR-0044 emitted name for a value object being DECLARED (its interface,
|
|
84
|
+
* Zod schema, and module filename). Non-value objects (entities) are never in
|
|
85
|
+
* the collision set, so this returns their bare `name`. */
|
|
86
|
+
valueObjectEmittedName: (obj: MetaData) => string;
|
|
87
|
+
/** The ADR-0044 emitted name for a REFERENCE to a value object (`@objectRef`,
|
|
88
|
+
* bare or FQN), resolved package-locally (ADR-0042) from `referrerPkg`. Falls
|
|
89
|
+
* back to the bare (package-stripped) ref when it resolves to no emitted value
|
|
90
|
+
* object — which is also the byte-identical result whenever there is no
|
|
91
|
+
* collision. */
|
|
92
|
+
resolveValueObjectName: (ref: string, referrerPkg: string | undefined) => string;
|
|
72
93
|
/** FR-019: module specifier to import externally-PROVIDED shared enums from
|
|
73
94
|
* (`@provided: true` declarations). Undefined when unset — referencing a
|
|
74
95
|
* provided enum without it is a codegen-time error. */
|
|
@@ -76,7 +97,7 @@ export interface RenderContext {
|
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
/** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). `collectionName` is built from `pluralizeCollections` + `collectionNameOverrides` (both default to always-pluralize). */
|
|
79
|
-
export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
|
|
100
|
+
export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "valueObjectNames" | "valueObjectEmittedName" | "resolveValueObjectName" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
|
|
80
101
|
extStyle?: ExtStyle;
|
|
81
102
|
omImport?: string;
|
|
82
103
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
@@ -85,6 +106,10 @@ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "
|
|
|
85
106
|
emitAbstractShapes?: boolean;
|
|
86
107
|
outputLayout?: OutputLayout;
|
|
87
108
|
packageOf?: Map<string, string | undefined>;
|
|
109
|
+
/** ADR-0044/#228 value-object emitted-name map (resolutionKey → emitted name).
|
|
110
|
+
* Defaults to an empty Map — bare names, byte-identical to pre-#228 output.
|
|
111
|
+
* `runGen` always provides the real map. */
|
|
112
|
+
valueObjectNames?: ReadonlyMap<string, string>;
|
|
88
113
|
selfTarget?: ResolvedTarget;
|
|
89
114
|
entityModuleTarget?: ResolvedTarget;
|
|
90
115
|
/** Auto-pluralize collection (table) variable names. Default true. */
|
|
@@ -93,6 +118,17 @@ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "
|
|
|
93
118
|
collectionNameOverrides?: Record<string, string>;
|
|
94
119
|
};
|
|
95
120
|
|
|
121
|
+
/** ADR-0042/#228 — the package a field's `@objectRef` resolves in: the FIELD's OWN
|
|
122
|
+
* declaring package (which differs from the referring object's when the field is
|
|
123
|
+
* inherited via `extends` from an abstract node in another package), falling back
|
|
124
|
+
* to `fallbackPkg` (the referring object's package). THE single source of truth for
|
|
125
|
+
* the referrer package passed to `RenderContext.resolveValueObjectName`, so every
|
|
126
|
+
* value-object reference site resolves a cross-package short-name collision
|
|
127
|
+
* identically (they cannot drift). Mirrors payload-codegen's `collectClosure`. */
|
|
128
|
+
export function fieldDeclaringPackage(field: MetaField, fallbackPkg: string | undefined): string | undefined {
|
|
129
|
+
return field.parent?.package ?? field.parent?.fileDefaultPackage ?? fallbackPkg;
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
/** Append the configured extension to a cross-entity module specifier (which is
|
|
97
133
|
* always a bare, extension-less relative path like `./Foo`). */
|
|
98
134
|
export function withExt(spec: string, style: ExtStyle): string {
|
|
@@ -129,6 +165,11 @@ export function makeRenderContext(opts: RenderContextInput): RenderContext {
|
|
|
129
165
|
pluralize: opts.pluralizeCollections ?? true,
|
|
130
166
|
overrides: opts.collectionNameOverrides ?? {},
|
|
131
167
|
};
|
|
168
|
+
// ADR-0044/#228 — the value-object emitted-name map + its two accessors. When
|
|
169
|
+
// absent (bare template unit-tests), the map is empty, so both accessors return
|
|
170
|
+
// bare names and every consumer is byte-identical to pre-#228 output.
|
|
171
|
+
const valueObjectNames = opts.valueObjectNames ?? new Map<string, string>();
|
|
172
|
+
const loadedRoot = opts.loadedRoot;
|
|
132
173
|
return {
|
|
133
174
|
...opts,
|
|
134
175
|
extStyle: opts.extStyle ?? "js",
|
|
@@ -139,6 +180,13 @@ export function makeRenderContext(opts: RenderContextInput): RenderContext {
|
|
|
139
180
|
emitAbstractShapes: opts.emitAbstractShapes ?? true,
|
|
140
181
|
outputLayout,
|
|
141
182
|
packageOf: opts.packageOf ?? new Map(),
|
|
183
|
+
valueObjectNames,
|
|
184
|
+
valueObjectEmittedName: (obj: MetaData) => valueObjectNames.get(obj.resolutionKey()) ?? obj.name,
|
|
185
|
+
resolveValueObjectName: (ref: string, referrerPkg: string | undefined) => {
|
|
186
|
+
const { node } = resolveObjectRef(loadedRoot, ref, referrerPkg ?? "");
|
|
187
|
+
const emitted = node !== undefined ? valueObjectNames.get(node.resolutionKey()) : undefined;
|
|
188
|
+
return emitted ?? stripPackage(ref);
|
|
189
|
+
},
|
|
142
190
|
selfTarget: defaultTarget,
|
|
143
191
|
entityModuleTarget: opts.entityModuleTarget ?? defaultTarget,
|
|
144
192
|
collectionName: (entityName: string) => variableNameFromEntity(entityName, collectionNameOpts),
|
package/src/runner.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import type { MetaData, MetaObject } from "@metaobjectsdev/metadata";
|
|
4
|
-
import { MetaRoot } from "@metaobjectsdev/metadata";
|
|
4
|
+
import { MetaRoot, OBJECT_SUBTYPE_VALUE } from "@metaobjectsdev/metadata";
|
|
5
|
+
import { assignEmittedNames } from "./naming/collision-names.js";
|
|
6
|
+
import { isAbstract } from "./instance-artifacts.js";
|
|
5
7
|
import type { Generator, GenContext, EmittedFile } from "./generator.js";
|
|
6
8
|
import type { MetaobjectsGenConfig } from "./metaobjects-config.js";
|
|
7
9
|
import { normalizeConfig, DEFAULT_TARGET_NAME } from "./metaobjects-config.js";
|
|
@@ -143,9 +145,38 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
143
145
|
// 3. Build shared render state once.
|
|
144
146
|
const pkMap = buildPkMap(root);
|
|
145
147
|
const relationMap = buildRelationMap(root);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
// ADR-0044/#228 — the ENTITY-tier collision domain is the run's EMITTED
|
|
149
|
+
// `object.value` SET (NOT any per-payload closure): value-object module
|
|
150
|
+
// filenames + `packageOf` are per-run/global, so the emitted-name map is built
|
|
151
|
+
// ONCE over every top-level `object.value` that actually produces a file, keyed
|
|
152
|
+
// by `resolutionKey()`. A bare short name unique across the set stays bare
|
|
153
|
+
// (byte-identical to pre-#228 output); a cross-package short-name collision
|
|
154
|
+
// qualifies every member (`AcmeAlphaNote`), and a still-colliding derived name
|
|
155
|
+
// fails loud (ERR_PAYLOAD_NAME_COLLISION, thrown by assignEmittedNames). A
|
|
156
|
+
// NON-emitted abstract value object (abstract + emitAbstractShapes off) produces
|
|
157
|
+
// no file/reference and is excluded — the entity-file generator's own emit gate
|
|
158
|
+
// (`isAbstract && !emitAbstractShapes` ⇒ skip) — so it can't over-qualify a
|
|
159
|
+
// concrete value object that merely shares its bare name in another package.
|
|
160
|
+
const isEmittedValueObject = (o: MetaObject): boolean =>
|
|
161
|
+
o.subType === OBJECT_SUBTYPE_VALUE && (!isAbstract(o) || config.emitAbstractShapes);
|
|
162
|
+
const valueObjectClosure = new Map<string, MetaData>();
|
|
163
|
+
for (const o of root.objects()) {
|
|
164
|
+
if (isEmittedValueObject(o)) valueObjectClosure.set(o.resolutionKey(), o);
|
|
165
|
+
}
|
|
166
|
+
const valueObjectNames = assignEmittedNames(valueObjectClosure);
|
|
167
|
+
// `packageOf` keys value objects by their EMITTED name (unique by construction
|
|
168
|
+
// via the backstop) so `valueObjectModuleSpecifier` resolves the right module
|
|
169
|
+
// even when two same-bare-named value objects live in different packages (the
|
|
170
|
+
// #244 misbinding disease). Non-value objects keep their bare name. With no
|
|
171
|
+
// collision, every emitted name equals its bare name, so this map is
|
|
172
|
+
// byte-identical to the pre-#228 `[o.name, o.package]` map.
|
|
173
|
+
const packageOf = new Map<string, string | undefined>();
|
|
174
|
+
for (const o of root.objects()) {
|
|
175
|
+
const key = o.subType === OBJECT_SUBTYPE_VALUE
|
|
176
|
+
? (valueObjectNames.get(o.resolutionKey()) ?? o.name)
|
|
177
|
+
: o.name;
|
|
178
|
+
packageOf.set(key, o.package);
|
|
179
|
+
}
|
|
149
180
|
|
|
150
181
|
// Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
|
|
151
182
|
// surface it on every generator's ctx.config so api-docs documents the Hono
|
|
@@ -192,6 +223,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
192
223
|
pkMap,
|
|
193
224
|
relationMap,
|
|
194
225
|
packageOf,
|
|
226
|
+
valueObjectNames,
|
|
195
227
|
selfTarget,
|
|
196
228
|
entityModuleTarget,
|
|
197
229
|
...(config.providedEnumModule !== undefined && { providedEnumModule: config.providedEnumModule }),
|
package/src/source-detect.ts
CHANGED
|
@@ -29,3 +29,23 @@ export function hasWritableRdbSource(entity: MetaObject): boolean {
|
|
|
29
29
|
}
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when the object declares (or inherits via extends — ADR-0039 resolving)
|
|
35
|
+
* at least one source.rdb child of ANY kind (writable OR read-only). Zero
|
|
36
|
+
* sources means "not backed by any store" (loader contract,
|
|
37
|
+
* validate-source-roles: zero sources is allowed, means not persisted) — the
|
|
38
|
+
* DB-artifact tier (queries/routes/api-model) must emit nothing for it, exactly
|
|
39
|
+
* as the table tier already refuses to emit a Drizzle table for it
|
|
40
|
+
* (hasWritableRdbSource, above — this is its any-kind sibling).
|
|
41
|
+
*/
|
|
42
|
+
export function hasAnyRdbSource(entity: MetaObject): boolean {
|
|
43
|
+
// ADR-0039: resolving — same rationale as hasWritableRdbSource.
|
|
44
|
+
for (const child of entity.children()) {
|
|
45
|
+
if (child.type !== TYPE_SOURCE) continue;
|
|
46
|
+
if (child.subType !== SOURCE_SUBTYPE_RDB) continue;
|
|
47
|
+
if (!(child instanceof MetaSource)) continue;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION,
|
|
10
10
|
GENERATION_INCREMENT, GENERATION_UUID,
|
|
11
11
|
FIELD_ATTR_AUTO_SET,
|
|
12
|
+
FIELD_ATTR_OBJECT_REF,
|
|
12
13
|
} from "@metaobjectsdev/metadata";
|
|
13
|
-
import { type RenderContext } from "../render-context.js";
|
|
14
|
+
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
14
15
|
import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
|
|
15
16
|
import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
|
|
16
17
|
import { tableNameFromEntity, columnNameFromField } from "../naming.js";
|
|
@@ -386,8 +387,18 @@ function renderColumn(
|
|
|
386
387
|
// first."
|
|
387
388
|
// Resolve a VO name → an imported type symbol (shared layout/package/extStyle-aware
|
|
388
389
|
// helper, so the .$type<VO> import matches the field's TS type + Zod schema).
|
|
389
|
-
|
|
390
|
-
|
|
390
|
+
// ADR-0044/#228 — resolve the field's @objectRef to the value object's EMITTED
|
|
391
|
+
// name (bare when unique in the run, package-qualified on a cross-package
|
|
392
|
+
// short-name collision), resolved package-locally from the FIELD's declaring
|
|
393
|
+
// package. `name` (the bare dollarTypeRef name) is the byte-identical fallback
|
|
394
|
+
// when the ref doesn't resolve to an emitted value object.
|
|
395
|
+
const voSym = (name: string) => {
|
|
396
|
+
const refRaw = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
397
|
+
const emitted = typeof refRaw === "string"
|
|
398
|
+
? ctx.resolveValueObjectName(refRaw, fieldDeclaringPackage(field, entityPackage))
|
|
399
|
+
: name;
|
|
400
|
+
return imp(`${emitted}@${valueObjectModuleSpecifier(emitted, ctx.packageOf, entityPackage, ctx.outputLayout, ctx.extStyle)}`);
|
|
401
|
+
};
|
|
391
402
|
|
|
392
403
|
let dollarTypeSegment: Code | string = "";
|
|
393
404
|
if (spec.dollarTypeRef !== undefined) {
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// vanilla / write-through entity → Drizzle table path
|
|
8
8
|
|
|
9
9
|
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
10
|
-
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
11
|
-
import
|
|
10
|
+
import type { MetaObject, MetaField } from "@metaobjectsdev/metadata";
|
|
11
|
+
import { FIELD_ATTR_OBJECT_REF } from "@metaobjectsdev/metadata";
|
|
12
|
+
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
12
13
|
import { renderDrizzleSchema } from "./drizzle-schema.js";
|
|
13
14
|
import { renderInferredTypes, renderEnumTypeAliases } from "./inferred-types.js";
|
|
14
15
|
import { renderZodValidators, isTphSubtype } from "./zod-validators.js";
|
|
@@ -23,7 +24,6 @@ import { projectionViewName } from "../projection/extract-view-spec.js";
|
|
|
23
24
|
import { renderExistingViewDecl, renderViewReadZodObject } from "./view-decl.js";
|
|
24
25
|
import { renderDocsFor } from "./jsdoc.js";
|
|
25
26
|
import { valueObjectModuleSpecifier } from "../import-path.js";
|
|
26
|
-
import { stripPackage } from "@metaobjectsdev/metadata";
|
|
27
27
|
import { hasWritableRdbSource } from "../source-detect.js";
|
|
28
28
|
import { renderValueObjectFile } from "./value-object-file.js";
|
|
29
29
|
import { isAbstract } from "../instance-artifacts.js";
|
|
@@ -131,10 +131,18 @@ export function renderEntityFile(
|
|
|
131
131
|
if (writeThrough) {
|
|
132
132
|
const camel = entity.name.charAt(0).toLowerCase() + entity.name.slice(1);
|
|
133
133
|
const fields = entity.fields();
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
// ADR-0044/#228 — resolve a view column's `@objectRef` to the value object's
|
|
135
|
+
// EMITTED name + module TOGETHER (lock-step), so the read-view artifact imports
|
|
136
|
+
// `AcmeAlphaNote` from `./AcmeAlphaNote.js` (not a bare `Note` → `./Note.js`)
|
|
137
|
+
// under a cross-package short-name collision.
|
|
138
|
+
const voRef = (field: MetaField): { name: string; module: string } => {
|
|
139
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
140
|
+
const name = ctx.resolveValueObjectName(typeof ref === "string" ? ref : "", fieldDeclaringPackage(field, entity.package));
|
|
141
|
+
const module = valueObjectModuleSpecifier(name, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle);
|
|
142
|
+
return { name, module };
|
|
143
|
+
};
|
|
136
144
|
const viewOpts = {
|
|
137
|
-
dialect: ctx.dialect, columnNamingStrategy: ctx.columnNamingStrategy, timestampMode: ctx.timestampMode,
|
|
145
|
+
dialect: ctx.dialect, columnNamingStrategy: ctx.columnNamingStrategy, timestampMode: ctx.timestampMode, voRef,
|
|
138
146
|
};
|
|
139
147
|
const z = imp("z@zod");
|
|
140
148
|
const docs = renderDocsFor(entity);
|