@metaobjectsdev/codegen-ts 1.0.3 → 1.0.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.
- package/dist/catalog-gates.d.ts +52 -0
- package/dist/catalog-gates.d.ts.map +1 -0
- package/dist/catalog-gates.js +183 -0
- package/dist/catalog-gates.js.map +1 -0
- package/dist/constants.d.ts +24 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +26 -0
- package/dist/constants.js.map +1 -1
- package/dist/generator-registry.d.ts +73 -0
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +174 -40
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +17 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/trace-helper-file.d.ts.map +1 -1
- package/dist/generators/trace-helper-file.js +54 -11
- package/dist/generators/trace-helper-file.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +0 -15
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +111 -4
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/relation-resolver.d.ts.map +1 -1
- package/dist/relation-resolver.js +11 -9
- package/dist/relation-resolver.js.map +1 -1
- package/dist/routes-expose.d.ts +30 -0
- package/dist/routes-expose.d.ts.map +1 -1
- package/dist/routes-expose.js +46 -0
- package/dist/routes-expose.js.map +1 -1
- package/dist/runner.d.ts +20 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +23 -0
- package/dist/runner.js.map +1 -1
- package/dist/templates/entity-file.js +2 -2
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/queries-file.js +5 -5
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +10 -3
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +21 -4
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.js +2 -2
- package/dist/templates/value-object-file.js.map +1 -1
- package/package.json +6 -6
- package/src/catalog-gates.ts +222 -0
- package/src/constants.ts +27 -0
- package/src/generator-registry.ts +245 -44
- package/src/generator.ts +17 -0
- package/src/generators/trace-helper-file.ts +55 -12
- package/src/index.ts +17 -3
- package/src/metaobjects-config.ts +0 -15
- package/src/projection/extract-view-spec.ts +126 -5
- package/src/reference/entity.ts +2 -2
- package/src/reference/queries.ts +2 -2
- package/src/relation-resolver.ts +14 -8
- package/src/routes-expose.ts +57 -0
- package/src/runner.ts +49 -0
- package/src/templates/entity-file.ts +2 -2
- package/src/templates/queries-file.ts +5 -5
- package/src/templates/routes-file-hono.ts +10 -3
- package/src/templates/routes-file.ts +21 -4
- package/src/templates/value-object-file.ts +2 -2
|
@@ -42,16 +42,19 @@ import {
|
|
|
42
42
|
SORT_ORDER_DESC,
|
|
43
43
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
44
44
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
45
|
+
RELATIONSHIP_ATTR_SOURCE_REF_FIELD,
|
|
45
46
|
CARDINALITY_ONE,
|
|
46
47
|
IDENTITY_SUBTYPE_REFERENCE,
|
|
47
48
|
IDENTITY_REFERENCE_ATTR_REFERENCES,
|
|
48
49
|
FIELD_ATTR_COLUMN,
|
|
49
50
|
OBJECT_PROJECTION_ATTR_FILTER,
|
|
50
|
-
|
|
51
|
+
findReferencesBetween,
|
|
52
|
+
resolveRelationshipReference,
|
|
51
53
|
resolveObjectRef,
|
|
52
54
|
type AggregateFunction,
|
|
55
|
+
type ReferenceLookup,
|
|
53
56
|
} from "@metaobjectsdev/metadata";
|
|
54
|
-
import { type MetaData, type MetaField, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
|
|
57
|
+
import { type MetaData, type MetaField, type MetaReferenceIdentity, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
|
|
55
58
|
import { intValueMapOf } from "../enum-meta.js";
|
|
56
59
|
import {
|
|
57
60
|
columnNameFromField,
|
|
@@ -330,6 +333,43 @@ function resolveHop(
|
|
|
330
333
|
return undefined;
|
|
331
334
|
}
|
|
332
335
|
|
|
336
|
+
/**
|
|
337
|
+
* #368: which identity.reference does a resolved `@via` hop actually mean?
|
|
338
|
+
*
|
|
339
|
+
* `resolveHop` already found the EXACT node the hop segment named — a
|
|
340
|
+
* reference hop names the reference itself (nothing to disambiguate: two
|
|
341
|
+
* references onto the same target can coexist, but the hop picked one of them
|
|
342
|
+
* by name), and a relationship hop names a relationship whose backing
|
|
343
|
+
* reference `resolveRelationshipReference` resolves via the same SSOT ladder
|
|
344
|
+
* (unique candidate -> @sourceRefField -> unique name-pairing) that
|
|
345
|
+
* relation-resolver.ts already uses for the identical question elsewhere.
|
|
346
|
+
* Discarding `hop` and re-deriving purely from `holder`/`target` — as this
|
|
347
|
+
* file did before — throws away that specificity and reintroduces the exact
|
|
348
|
+
* ambiguity a named hop exists to resolve.
|
|
349
|
+
*
|
|
350
|
+
* Returns a single `ReferenceLookup` once resolved unambiguously, or the full
|
|
351
|
+
* candidate list when even the ladder cannot choose (a relationship hop whose
|
|
352
|
+
* name pairs with none of its candidates and no `@sourceRefField`) — the
|
|
353
|
+
* caller reports that list as a genuine ambiguity.
|
|
354
|
+
*/
|
|
355
|
+
function resolveHopReference(
|
|
356
|
+
holder: MetaObject,
|
|
357
|
+
hop: MetaData,
|
|
358
|
+
hopName: string,
|
|
359
|
+
target: MetaObject,
|
|
360
|
+
): ReferenceLookup | ReferenceLookup[] {
|
|
361
|
+
if (hop.type === TYPE_IDENTITY && hop.subType === IDENTITY_SUBTYPE_REFERENCE) {
|
|
362
|
+
return { holder, other: target, referenceIdentity: hop as unknown as MetaReferenceIdentity };
|
|
363
|
+
}
|
|
364
|
+
// ADR-0039: resolving — @sourceRefField may be inherited via extends.
|
|
365
|
+
const sourceRefField = hop.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD) as string | undefined;
|
|
366
|
+
const matching = resolveRelationshipReference(holder, hopName, target.name, sourceRefField);
|
|
367
|
+
if (matching) {
|
|
368
|
+
return { holder, other: target, referenceIdentity: matching };
|
|
369
|
+
}
|
|
370
|
+
return findReferencesBetween(holder, target);
|
|
371
|
+
}
|
|
372
|
+
|
|
333
373
|
function viewName(projection: MetaObject, ctx: ExtractContext): string {
|
|
334
374
|
// The read-only source carries the physical view name. FR-016: physicalName
|
|
335
375
|
// implements the four-step rule (kind-matching alias → legacy @table →
|
|
@@ -750,14 +790,62 @@ function buildJoinTree(
|
|
|
750
790
|
// a traversed relationship/reference may inherit its target via extends.
|
|
751
791
|
const resolved = resolveHop(currentObj, relName);
|
|
752
792
|
if (!resolved) break;
|
|
753
|
-
const { targetName, cardinality } = resolved;
|
|
793
|
+
const { hop, targetName, cardinality } = resolved;
|
|
754
794
|
// @objectRef/@references may be package-qualified ("pkg::Entity"); resolve it
|
|
755
795
|
// package-aware relative to the hop's source entity (the loader qualifies a
|
|
756
796
|
// same-package ref even when authored bare), so the join binds the exact target.
|
|
757
797
|
const target = resolveEntityRef(root, targetName, packageOf(currentObj));
|
|
758
798
|
if (!target) break;
|
|
759
799
|
|
|
760
|
-
|
|
800
|
+
// #368: two identity.reference declarations onto the same target are legal
|
|
801
|
+
// (e.g. Match.homeTeamRef/awayTeamRef -> Team) — resolveHopReference prefers
|
|
802
|
+
// the SPECIFIC reference/relationship the hop already named over re-deriving
|
|
803
|
+
// one from the target alone, so an explicit `@via: "Match.homeTeamRef"` (or a
|
|
804
|
+
// relationship disambiguated by @sourceRefField/name-pairing) resolves cleanly.
|
|
805
|
+
// Only a relationship hop that even the ladder cannot choose reaches the throw.
|
|
806
|
+
const resolvedRef = resolveHopReference(currentObj as MetaObject, hop, relName, target);
|
|
807
|
+
let ref: ReferenceLookup | undefined;
|
|
808
|
+
if (Array.isArray(resolvedRef)) {
|
|
809
|
+
if (resolvedRef.length > 1) {
|
|
810
|
+
// #368 round 2: @sourceRefField cannot fix this, but WHY differs by shape, and
|
|
811
|
+
// asserting the wrong reason for a given shape is itself a bug (fix round 1 of
|
|
812
|
+
// this cleanup caught exactly that). resolveRelationshipReference's ladder reads
|
|
813
|
+
// ONLY the hop's own entity's candidates (referenceCandidatesFor(currentObj, ...));
|
|
814
|
+
// it never even looks at `target`'s references. So:
|
|
815
|
+
// - If `currentObj` itself holds one of the ambiguous candidates, resolution
|
|
816
|
+
// already tried @sourceRefField/name-pairing against it and failed — and that
|
|
817
|
+
// is only reachable at all when @cardinality isn't "one": a @cardinality "one"
|
|
818
|
+
// relationship with 2+ own-side candidates is rejected at LOAD by rule (e)
|
|
819
|
+
// (validateOneSideReferenceResolution) using this exact same ladder, so if we
|
|
820
|
+
// got this far with an own-side candidate, @cardinality is provably not "one",
|
|
821
|
+
// and @sourceRefField is provably illegal here (rule (d)).
|
|
822
|
+
// - If NONE of the candidates are `currentObj`'s own, @sourceRefField could not
|
|
823
|
+
// have mattered regardless of @cardinality — it only ever consults the hop's
|
|
824
|
+
// OWN identity.reference children, and it has none targeting `target`. This is
|
|
825
|
+
// rule (e)'s zero-candidate gap (validation-passes.ts:2226, `<= 1` skips 0 too):
|
|
826
|
+
// a @cardinality "one" relationship can reach here with the FK entirely on the
|
|
827
|
+
// far side, so @cardinality itself must NOT be asserted in this branch.
|
|
828
|
+
const holderName = (currentObj as MetaObject).name;
|
|
829
|
+
const holderOwnsACandidate = resolvedRef.some((r) => r.holder.name === holderName);
|
|
830
|
+
const whySourceRefFieldCannotHelp = holderOwnsACandidate
|
|
831
|
+
? `it only disambiguates a @cardinality "${CARDINALITY_ONE}" relationship, and this ` +
|
|
832
|
+
`relationship's @cardinality is not "${CARDINALITY_ONE}" (declaring @sourceRefField on it ` +
|
|
833
|
+
`is itself a load error)`
|
|
834
|
+
: `it only consults "${holderName}"'s own identity.reference children, and "${holderName}" ` +
|
|
835
|
+
`declares none targeting "${target.name}" -- every candidate above belongs to the other side ` +
|
|
836
|
+
`of this join`;
|
|
837
|
+
throw new Error(
|
|
838
|
+
`projection join hop "${relName}" from "${holderName}" to "${target.name}" is ambiguous: ` +
|
|
839
|
+
`${resolvedRef.map((r) => r.referenceIdentity.name).join(", ")}. ` +
|
|
840
|
+
`@sourceRefField cannot resolve this: ${whySourceRefFieldCannotHelp}. There is no attribute ` +
|
|
841
|
+
`that disambiguates a hop like this -- remove the extra identity.reference between these two ` +
|
|
842
|
+
`entities, or restructure the model so only one remains.`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
ref = resolvedRef[0];
|
|
846
|
+
} else {
|
|
847
|
+
ref = resolvedRef;
|
|
848
|
+
}
|
|
761
849
|
if (!ref) break;
|
|
762
850
|
|
|
763
851
|
const fkField = ref.referenceIdentity.fields[0];
|
|
@@ -1090,7 +1178,40 @@ function buildSelectSpec(
|
|
|
1090
1178
|
// The base↔child correlation FK — resolved exactly as buildJoinTree resolves a
|
|
1091
1179
|
// single hop (the identity.reference is the FK-direction SSOT). Single-hop @via;
|
|
1092
1180
|
// a multi-hop @via on origin.first is not lowered here (rare, and validated away).
|
|
1093
|
-
|
|
1181
|
+
// #368: two identity.reference declarations onto the same target are legal, so
|
|
1182
|
+
// this correlation cannot silently take the first — refuse rather than guess.
|
|
1183
|
+
// Unlike buildJoinTree's @via hop (a named relationship/reference this file can
|
|
1184
|
+
// resolve via resolveHopReference), origin.first's OWN @via (ORIGIN_FIRST_ATTR_VIA)
|
|
1185
|
+
// is never read anywhere in this file — buildJoinTree explicitly skips it
|
|
1186
|
+
// (ORIGIN_SUBTYPE_FIRST falls through to `continue` there) and this branch derives
|
|
1187
|
+
// childEntity from @of alone, so there is no hop name here to prefer. Fixing that is
|
|
1188
|
+
// a separate, larger change (wiring @via/single-hop-unique inference into this
|
|
1189
|
+
// branch to match _validateViaPath/_inferViaSingleHop) — out of scope for #368's
|
|
1190
|
+
// silent-first-match fix; the message below reflects the real, narrower remedy.
|
|
1191
|
+
const refs = findReferencesBetween(base, childEntity);
|
|
1192
|
+
// findReferencesBetween walks BOTH directions ([base,child] then [child,base]),
|
|
1193
|
+
// so ">1 entry" is not by itself #368 ambiguity: a mutual 1:1 —
|
|
1194
|
+
// Customer.primaryAddressRef -> Address PLUS Address.customerRef -> Customer —
|
|
1195
|
+
// returns two entries pointing in OPPOSITE directions. That shape is legal
|
|
1196
|
+
// (findReferenceBetween's own contract calls it "rare, but legal", and the
|
|
1197
|
+
// referenceHolder: "source" | "target" branch below exists to serve it), and
|
|
1198
|
+
// refusing it broke codegen for a model #368 says nothing about. The real
|
|
1199
|
+
// ambiguity is two references declared by the SAME holder onto the other side —
|
|
1200
|
+
// that is what the first-match below cannot choose between.
|
|
1201
|
+
const ambiguousHolder = [base, childEntity].find(
|
|
1202
|
+
(holder) => refs.filter((r) => r.holder === holder).length > 1,
|
|
1203
|
+
);
|
|
1204
|
+
if (ambiguousHolder !== undefined) {
|
|
1205
|
+
const ambiguous = refs.filter((r) => r.holder === ambiguousHolder);
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`origin.first correlation from "${base.name}" to "${childEntity.name}" is ambiguous: ` +
|
|
1208
|
+
`"${ambiguousHolder.name}" declares ${ambiguous.length} identity.reference nodes ` +
|
|
1209
|
+
`onto the other side (${ambiguous.map((r) => r.referenceIdentity.name).join(", ")}). ` +
|
|
1210
|
+
`origin.first's own @via is not consulted for this correlation — reduce to a ` +
|
|
1211
|
+
`single identity.reference between these two entities.`,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const ref = refs[0];
|
|
1094
1215
|
if (!ref) continue;
|
|
1095
1216
|
const fkField = ref.referenceIdentity.fields[0];
|
|
1096
1217
|
if (!fkField) continue;
|
package/src/reference/entity.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
namesRef,
|
|
71
71
|
namesConstArg,
|
|
72
72
|
GENERATED_HEADER,
|
|
73
|
+
sidecarLine,
|
|
73
74
|
} from "@metaobjectsdev/codegen-ts";
|
|
74
75
|
|
|
75
76
|
export interface RenderEntityOpts {
|
|
@@ -143,8 +144,7 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
143
144
|
const body = joinCode(sections, { on: "\n" }).toString();
|
|
144
145
|
const header =
|
|
145
146
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
146
|
-
`// Source metadata: ${entity.name} (${entity.fqn()})\n`
|
|
147
|
-
`// Customize via ${entity.name}.extra.ts in this directory.\n`;
|
|
147
|
+
`// Source metadata: ${entity.name} (${entity.fqn()})\n${sidecarLine(`${entity.name}.extra.ts`)}`;
|
|
148
148
|
return header + body;
|
|
149
149
|
}
|
|
150
150
|
|
package/src/reference/queries.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
formatTs,
|
|
57
57
|
entityOutputPath,
|
|
58
58
|
GENERATED_HEADER,
|
|
59
|
+
sidecarLine,
|
|
59
60
|
} from "@metaobjectsdev/codegen-ts";
|
|
60
61
|
|
|
61
62
|
// --- composition (OWNED for the common case) ---
|
|
@@ -128,8 +129,7 @@ import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}I
|
|
|
128
129
|
const body = joinCode(sections, { on: "\n" }).toString();
|
|
129
130
|
const header =
|
|
130
131
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
131
|
-
`// Source metadata: ${entityName} (${obj.fqn()})\n`
|
|
132
|
-
`// Customize via ${entityName}.extra.ts in this directory (additional queries, custom logic).\n`;
|
|
132
|
+
`// Source metadata: ${entityName} (${obj.fqn()})\n${sidecarLine(`${entityName}.extra.ts`)}`;
|
|
133
133
|
return header + body;
|
|
134
134
|
}
|
|
135
135
|
|
package/src/relation-resolver.ts
CHANGED
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
11
11
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
12
12
|
RELATIONSHIP_ATTR_THROUGH,
|
|
13
|
+
RELATIONSHIP_ATTR_SOURCE_REF_FIELD,
|
|
13
14
|
CARDINALITY_ONE,
|
|
14
15
|
CARDINALITY_MANY,
|
|
15
16
|
deriveM2MFields,
|
|
17
|
+
resolveRelationshipReference,
|
|
16
18
|
stripPackage,
|
|
17
19
|
} from "@metaobjectsdev/metadata";
|
|
18
20
|
import { variableNameFromEntity } from "./naming.js";
|
|
@@ -89,16 +91,20 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
|
89
91
|
if (!targetEntityRaw) continue;
|
|
90
92
|
const targetEntity = stripPackage(targetEntityRaw);
|
|
91
93
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// #368: an entity may hold more than one identity.reference onto the same
|
|
95
|
+
// target, so the target alone does not identify the FK. Resolve through the
|
|
96
|
+
// shared ladder (unique candidate -> @sourceRefField -> name pairing); the
|
|
97
|
+
// loader has already refused anything it cannot resolve, so a miss here
|
|
98
|
+
// means an unloadable model reached codegen — skip rather than guess.
|
|
99
|
+
// ADR-0039: resolving — @sourceRefField may be inherited via extends.
|
|
100
|
+
const declaredRefField = child.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD) as string | undefined;
|
|
101
|
+
const matching = resolveRelationshipReference(
|
|
102
|
+
obj, child.name, targetEntity, declaredRefField,
|
|
103
|
+
);
|
|
97
104
|
if (!matching) continue;
|
|
98
105
|
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
const fkField = fkFields[0]!;
|
|
106
|
+
const fkField = matching.fields[0];
|
|
107
|
+
if (!fkField) continue;
|
|
102
108
|
|
|
103
109
|
ensure(obj.name).push({
|
|
104
110
|
name: child.name,
|
package/src/routes-expose.ts
CHANGED
|
@@ -84,3 +84,60 @@ export function exposeLine(verbs: readonly CrudVerb[] | undefined, indent: strin
|
|
|
84
84
|
if (verbs === undefined) return "";
|
|
85
85
|
return `\n${indent}expose: [${verbs.map((v) => JSON.stringify(v)).join(", ")}],`;
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The auth-seam paragraph in a generated routes handler's JSDoc.
|
|
90
|
+
*
|
|
91
|
+
* Stock CRUD is unauthenticated, and until #367 the only thing a generated routes file
|
|
92
|
+
* said about that was a header line pointing at `<Entity>.extra.ts` "(e.g., auth)" — a
|
|
93
|
+
* sibling module nothing imports. An agent building a real API read it, went looking for
|
|
94
|
+
* the seam, found none, and deleted `routesFile()` from its config rather than mount five
|
|
95
|
+
* open endpoints over a password-hash table. The seam it needed already existed in both
|
|
96
|
+
* frameworks; nothing told it where.
|
|
97
|
+
*
|
|
98
|
+
* Both recipes are gated by `runtime-ts/test/route-auth-seam.test.ts` against the real
|
|
99
|
+
* mount helpers, because a recipe in generated output that does not work is the defect
|
|
100
|
+
* this replaces, not a smaller version of it. Hono's trailing wildcard is load-bearing
|
|
101
|
+
* and counter-intuitive: `"/users/*"` matches the collection path `/users` itself, so one
|
|
102
|
+
* `app.use` covers list, get and every write.
|
|
103
|
+
*
|
|
104
|
+
* The closing sentence is the part an adopter most needs and no knob can supply: a
|
|
105
|
+
* row-ownership rule ("only the owner may read this row") is not expressible in a mount,
|
|
106
|
+
* so the honest move is to hand-write those verbs and narrow the generated file with
|
|
107
|
+
* `expose` — not to mount them and hope.
|
|
108
|
+
*/
|
|
109
|
+
export function authSeamJsDoc(opts: {
|
|
110
|
+
framework: "fastify" | "hono";
|
|
111
|
+
/** The emitted handler's name, so the worked example is callable as written. */
|
|
112
|
+
handlerName: string;
|
|
113
|
+
/** Hono only: the source expression for this file's mount path, e.g. "`/api${User.$path}`". */
|
|
114
|
+
mountPathExpr?: string;
|
|
115
|
+
/** False for a read-only mount, which `expose` cannot narrow further. */
|
|
116
|
+
narrowable: boolean;
|
|
117
|
+
}): string {
|
|
118
|
+
const recipe =
|
|
119
|
+
opts.framework === "fastify"
|
|
120
|
+
? ` * app.register(async (s) => {\n` +
|
|
121
|
+
` * s.addHook("preHandler", requireAuth);\n` +
|
|
122
|
+
` * await ${opts.handlerName}(s);\n` +
|
|
123
|
+
` * });`
|
|
124
|
+
: ` * app.use(${opts.mountPathExpr}, requireAuth); // matches the collection path too\n` +
|
|
125
|
+
` * ${opts.handlerName}(app, { db });`;
|
|
126
|
+
const guarded =
|
|
127
|
+
opts.framework === "fastify"
|
|
128
|
+
? `Register this inside a scope that\n` +
|
|
129
|
+
` * carries your hook and every verb below is guarded — routes outside that scope\n` +
|
|
130
|
+
` * are not:`
|
|
131
|
+
: `Guard the mount path with\n` + ` * middleware before calling this:`;
|
|
132
|
+
// Name the generator that actually emitted this file — `routesFile` does not exist in
|
|
133
|
+
// a Hono project's config, and a recipe naming the wrong symbol is the same defect
|
|
134
|
+
// one size smaller.
|
|
135
|
+
const generatorName = opts.framework === "fastify" ? "routesFile" : "routesFileHono";
|
|
136
|
+
const narrowNote = opts.narrowable
|
|
137
|
+
? `\n *\n * A row-ownership rule ("only the owner may read this row") is not expressible in a\n` +
|
|
138
|
+
` * mount. Hand-write those verbs and narrow this file with\n` +
|
|
139
|
+
` * ${generatorName}({ expose: ["list", "get"] }) rather than mounting them open.`
|
|
140
|
+
: `\n *\n * A row-ownership rule ("only the owner may read this row") is not expressible in a\n` +
|
|
141
|
+
` * mount. Hand-write those reads rather than exposing this one open.`;
|
|
142
|
+
return ` *\n * Auth: these endpoints are unauthenticated. ${guarded}\n *\n${recipe}${narrowNote}`;
|
|
143
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -36,6 +36,11 @@ import {
|
|
|
36
36
|
type BaselineMode,
|
|
37
37
|
type DecideAndWriteOpts,
|
|
38
38
|
} from "./overwrite-policy.js";
|
|
39
|
+
import {
|
|
40
|
+
warnUnsatisfiedRequires, warnMixedApiFrameworks, warnLibrarySelectionMismatch,
|
|
41
|
+
} from "./catalog-gates.js";
|
|
42
|
+
import { libraryManifests, splitLayerToken } from "@metaobjectsdev/metadata/library";
|
|
43
|
+
import { generatorRegistry, type GeneratorRegistryEntry } from "./generator-registry.js";
|
|
39
44
|
|
|
40
45
|
/** JS-identifier-shape only. Prevents filesystem traversal when metadata comes
|
|
41
46
|
* from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
|
|
@@ -74,6 +79,15 @@ export interface RunGenOpts {
|
|
|
74
79
|
* `--dry-run`, and watching it reappear.
|
|
75
80
|
*/
|
|
76
81
|
dryRun?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* The catalog the `requires` / api-framework gates check against.
|
|
84
|
+
*
|
|
85
|
+
* Injected because the COMPOSITION lives in the CLI: `codegen-ts` cannot import its
|
|
86
|
+
* own dependents, so it cannot see the react and tanstack registry slices. Defaults
|
|
87
|
+
* to this package's own slice, which keeps a programmatic embedder that never
|
|
88
|
+
* composes correct — it simply has nothing to say about client-tier generators.
|
|
89
|
+
*/
|
|
90
|
+
catalog?: Record<string, GeneratorRegistryEntry>;
|
|
77
91
|
/**
|
|
78
92
|
* Output scope — an object is generated only when this predicate returns true
|
|
79
93
|
* for its fully-qualified name (`obj.resolutionKey()`, `<package>::<name>`).
|
|
@@ -118,6 +132,16 @@ export interface RunGenOpts {
|
|
|
118
132
|
* option explicitly) can omit this with no behavior change elsewhere.
|
|
119
133
|
*/
|
|
120
134
|
sourceFiles?: readonly string[];
|
|
135
|
+
/**
|
|
136
|
+
* FR-043 §6 — the project's shipped-library selection TOKENS (`Collection.libraries`,
|
|
137
|
+
* e.g. `["ai", "ai/db"]`), resolved to manifests and threaded onto
|
|
138
|
+
* `GenContext.libraries`, and read by the post-selection audit.
|
|
139
|
+
*
|
|
140
|
+
* Omitted means the caller never said; `[]` means it looked and this project opted
|
|
141
|
+
* into none. The two differ for a generator keying on a library anchor — see
|
|
142
|
+
* `GenContext.libraries`.
|
|
143
|
+
*/
|
|
144
|
+
libraries?: readonly string[];
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
export interface RunGenResult {
|
|
@@ -386,6 +410,16 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
386
410
|
// 2. Resolve targets + entity-module target.
|
|
387
411
|
const config = normalizeConfig(opts.config);
|
|
388
412
|
|
|
413
|
+
// FR-043 §6 — the opted-in manifests, resolved once per run. A token names its
|
|
414
|
+
// library whether or not it carries a layer, and an unknown name contributes nothing
|
|
415
|
+
// (the config reader is what refuses a typo, by name — see `knownLibraryTokens`).
|
|
416
|
+
const selectedManifests =
|
|
417
|
+
opts.libraries === undefined
|
|
418
|
+
? undefined
|
|
419
|
+
: [...new Set(opts.libraries.map((t) => splitLayerToken(t)[0]))]
|
|
420
|
+
.map((name) => libraryManifests()[name])
|
|
421
|
+
.filter((m): m is NonNullable<typeof m> => m !== undefined);
|
|
422
|
+
|
|
389
423
|
// FR-023 §4.3 — the run's composed registry, threaded onto every GenContext.
|
|
390
424
|
// Mirrors sdk's `loadMemory` exactly (`defaultLoadMemoryProviders` = core
|
|
391
425
|
// providers, then the project's own `config.providers` appended) so
|
|
@@ -528,6 +562,20 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
528
562
|
warnMissingPromptGenerators(root, config.generators, (m) => warnings.push(m));
|
|
529
563
|
warnUnmarkedUiGenerators(config.generators, (m) => warnings.push(m));
|
|
530
564
|
|
|
565
|
+
// Codegen is opt-in, so `meta gen` is the post-selection audit: the two ways a legal
|
|
566
|
+
// selection still surprises you. Warnings only — see catalog-gates.ts.
|
|
567
|
+
const catalog = opts.catalog ?? generatorRegistry;
|
|
568
|
+
warnUnsatisfiedRequires(config.generators, catalog, (m) => warnings.push(m));
|
|
569
|
+
warnMixedApiFrameworks(config.generators, catalog, (m) => warnings.push(m));
|
|
570
|
+
// FR-043 §6 — only when the caller actually told us what was selected. A programmatic
|
|
571
|
+
// caller that never threads `libraries` gets no library warnings at all, rather than
|
|
572
|
+
// "you opted into nothing", which would be a claim we cannot support.
|
|
573
|
+
if (opts.libraries !== undefined) {
|
|
574
|
+
warnLibrarySelectionMismatch(
|
|
575
|
+
config.generators, catalog, libraryManifests(), opts.libraries, (m) => warnings.push(m),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
531
579
|
// <Entity>Names is opt-in on TypeScript and an existing project gets no signal that
|
|
532
580
|
// it exists. Fires ONCE, on the first gen after crossing the release that made it the
|
|
533
581
|
// doctrine — see shouldNoteNamesArtifactAbsent for why it is keyed on the engine stamp
|
|
@@ -644,6 +692,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
644
692
|
...(projectRoot !== undefined && { projectRoot }),
|
|
645
693
|
registry,
|
|
646
694
|
...(opts.sourceFiles !== undefined && { sourceFiles: opts.sourceFiles }),
|
|
695
|
+
...(selectedManifests !== undefined && { libraries: selectedManifests }),
|
|
647
696
|
warn: (msg) => warnings.push(`[${generator.name}] ${msg}`),
|
|
648
697
|
};
|
|
649
698
|
|
|
@@ -17,7 +17,7 @@ import { renderEntityConstants } from "./entity-constants.js";
|
|
|
17
17
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
18
18
|
import { renderFilterType } from "./filter-type.js";
|
|
19
19
|
import { renderTphDiscriminatorUnion, isTphDiscriminatorBase } from "./tph-discriminator.js";
|
|
20
|
-
import { GENERATED_HEADER } from "../constants.js";
|
|
20
|
+
import { GENERATED_HEADER, sidecarLine } from "../constants.js";
|
|
21
21
|
import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
|
|
22
22
|
import { renderProjectionDecl } from "./projection-decl.js";
|
|
23
23
|
import {
|
|
@@ -224,6 +224,6 @@ ${docsPrefix}export type ${entity.name} = ${z}.infer<typeof ${entity.name}Schema
|
|
|
224
224
|
const header =
|
|
225
225
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
226
226
|
`// Source metadata: ${entity.name} (${entity.fqn()})\n` +
|
|
227
|
-
|
|
227
|
+
sidecarLine(`${entity.name}.extra.ts`);
|
|
228
228
|
return header + body;
|
|
229
229
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
getPkFields,
|
|
23
23
|
} from "./queries.js";
|
|
24
24
|
import { pluralize, findByIdFnName, listFnName, createFnName, insertPreservingFnName, updateFnName } from "../naming.js";
|
|
25
|
-
import { GENERATED_HEADER } from "../constants.js";
|
|
25
|
+
import { GENERATED_HEADER, sidecarLine } from "../constants.js";
|
|
26
26
|
import { isTphDiscriminatorBase, tphConcreteSubtypes } from "./tph-discriminator.js";
|
|
27
27
|
import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
|
|
28
28
|
import { hasAutoSetFields } from "./zod-validators.js";
|
|
@@ -147,7 +147,7 @@ import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}I
|
|
|
147
147
|
const header =
|
|
148
148
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
149
149
|
`// Source metadata: ${entityName} (${obj.fqn()})\n` +
|
|
150
|
-
|
|
150
|
+
sidecarLine(`${entityName}.extra.ts`);
|
|
151
151
|
return header + body;
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -196,7 +196,7 @@ export async function ${listFnName(entityName)}(db: Db, opts?: { limit?: number;
|
|
|
196
196
|
const header =
|
|
197
197
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
198
198
|
`// Source metadata: ${entityName} (${obj.fqn()}) — projection (read-only)\n` +
|
|
199
|
-
|
|
199
|
+
sidecarLine(`${entityName}.extra.ts`);
|
|
200
200
|
return header + body;
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -286,7 +286,7 @@ export async function ${updateFnName(entityName)}(db: Db, ${pkField}: ${pkType},
|
|
|
286
286
|
const header =
|
|
287
287
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
288
288
|
`// Source metadata: ${entityName} (${obj.fqn()}) — write-through entity read-view (reads → view, writes → table)\n` +
|
|
289
|
-
|
|
289
|
+
sidecarLine(`${entityName}.extra.ts`);
|
|
290
290
|
return header + body;
|
|
291
291
|
}
|
|
292
292
|
|
|
@@ -396,6 +396,6 @@ ${dbTypeAlias}
|
|
|
396
396
|
const header =
|
|
397
397
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
398
398
|
`// Source metadata: ${baseName} (${base.fqn()}) — TPH discriminator base\n` +
|
|
399
|
-
|
|
399
|
+
sidecarLine(`${baseName}.extra.ts`);
|
|
400
400
|
return header + body;
|
|
401
401
|
}
|
|
@@ -28,9 +28,9 @@ import { code, imp } from "ts-poet";
|
|
|
28
28
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
29
29
|
import { type RenderContext } from "../render-context.js";
|
|
30
30
|
import { entityModuleSpecifier } from "../import-path.js";
|
|
31
|
-
import { GENERATED_HEADER } from "../constants.js";
|
|
31
|
+
import { GENERATED_HEADER, sidecarLine } from "../constants.js";
|
|
32
32
|
import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
|
|
33
|
-
import { type CrudVerb, exposeLine } from "../routes-expose.js";
|
|
33
|
+
import { authSeamJsDoc, type CrudVerb, exposeLine } from "../routes-expose.js";
|
|
34
34
|
import { effectivePackage } from "../docs-paths.js";
|
|
35
35
|
|
|
36
36
|
export function renderRoutesFileHono(
|
|
@@ -58,12 +58,17 @@ export function renderRoutesFileHono(
|
|
|
58
58
|
const header =
|
|
59
59
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
60
60
|
`// Source metadata: ${entityName} (${entity.fqn()})\n` +
|
|
61
|
-
|
|
61
|
+
sidecarLine(`${entityName}.extra.ts`);
|
|
62
62
|
|
|
63
63
|
// Path composition: apiPrefix is a literal string in the URL.
|
|
64
64
|
const pathExpr = ctx.apiPrefix
|
|
65
65
|
? `\`${ctx.apiPrefix}\${${entityName}.$path}\``
|
|
66
66
|
: `${entityName}.$path`;
|
|
67
|
+
// The same path with Hono's trailing wildcard, for the auth recipe in the JSDoc.
|
|
68
|
+
// `"/users/*"` matches `/users` itself, so one app.use covers list, get and writes.
|
|
69
|
+
const authPathExpr = ctx.apiPrefix
|
|
70
|
+
? `\`${ctx.apiPrefix}\${${entityName}.$path}/*\``
|
|
71
|
+
: `\`\${${entityName}.$path}/*\``;
|
|
67
72
|
|
|
68
73
|
// --- Projection path: read-only routes (GET list + GET :id) ---
|
|
69
74
|
if (isProjection(entity)) {
|
|
@@ -89,6 +94,7 @@ import {
|
|
|
89
94
|
* Exposes GET list + GET :id only. POST/PATCH/DELETE return 405.
|
|
90
95
|
* Customize: register this as-is, or import individual route helpers from
|
|
91
96
|
* @metaobjectsdev/runtime-ts/hono.
|
|
97
|
+
${authSeamJsDoc({ framework: "hono", handlerName, mountPathExpr: authPathExpr, narrowable: false })}
|
|
92
98
|
*/
|
|
93
99
|
// biome-ignore lint/suspicious/noExplicitAny: consumer-defined Hono bindings/variables
|
|
94
100
|
export function ${handlerName}(app: ${HonoSym}<any, any, any>, deps: { db: unknown }): void {
|
|
@@ -143,6 +149,7 @@ import {
|
|
|
143
149
|
* helpers (mountListRoute, mountGetRoute, ...) from
|
|
144
150
|
* @metaobjectsdev/runtime-ts/hono and mix with your own handlers
|
|
145
151
|
* (auth, side effects, etc.).
|
|
152
|
+
${authSeamJsDoc({ framework: "hono", handlerName, mountPathExpr: authPathExpr, narrowable: true })}
|
|
146
153
|
*/
|
|
147
154
|
// biome-ignore lint/suspicious/noExplicitAny: consumer-defined Hono bindings/variables
|
|
148
155
|
export function ${handlerName}(app: ${HonoSym}<any, any, any>, deps: { db: unknown }): void {
|
|
@@ -24,12 +24,12 @@ import {
|
|
|
24
24
|
import { type RenderContext } from "../render-context.js";
|
|
25
25
|
import { crossEntitySpecifier, entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
|
|
26
26
|
import { namesRef, columnExpr } from "../names.js";
|
|
27
|
-
import { GENERATED_HEADER } from "../constants.js";
|
|
27
|
+
import { GENERATED_HEADER, sidecarLine } from "../constants.js";
|
|
28
28
|
import { routesHandlerName } from "../naming.js";
|
|
29
29
|
import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
|
|
30
30
|
import type { RelationEntry } from "../relation-resolver.js";
|
|
31
31
|
import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
|
|
32
|
-
import { type CrudVerb, exposeLine, intersectExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js";
|
|
32
|
+
import { authSeamJsDoc, type CrudVerb, exposeLine, intersectExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js";
|
|
33
33
|
import { effectivePackage } from "../docs-paths.js";
|
|
34
34
|
|
|
35
35
|
export function renderRoutesFile(
|
|
@@ -65,7 +65,7 @@ export function renderRoutesFile(
|
|
|
65
65
|
const header =
|
|
66
66
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
67
67
|
`// Source metadata: ${entityName} (${entity.fqn()})\n` +
|
|
68
|
-
|
|
68
|
+
sidecarLine(`${entityName}.extra.ts`);
|
|
69
69
|
|
|
70
70
|
// --- Projection path: read-only routes (GET list + GET :id) ---
|
|
71
71
|
if (isProjection(entity)) {
|
|
@@ -74,6 +74,13 @@ export function renderRoutesFile(
|
|
|
74
74
|
const mountReadOnlyCrudRoutesSym = imp(
|
|
75
75
|
"mountReadOnlyCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify",
|
|
76
76
|
);
|
|
77
|
+
// A projection mount is read-only by construction, so `expose` cannot narrow it —
|
|
78
|
+
// the paragraph drops the narrowing advice and keeps the auth seam.
|
|
79
|
+
const readOnlyAuthJsDoc = authSeamJsDoc({
|
|
80
|
+
framework: "fastify",
|
|
81
|
+
handlerName,
|
|
82
|
+
narrowable: false,
|
|
83
|
+
});
|
|
77
84
|
|
|
78
85
|
const literalImports = code`
|
|
79
86
|
import { db } from ${JSON.stringify(dbImportSpec)};
|
|
@@ -93,6 +100,7 @@ import {
|
|
|
93
100
|
* Exposes GET list + GET :id only. POST/PATCH/DELETE return 405.
|
|
94
101
|
* Customize: register this as-is, or import individual route helpers from
|
|
95
102
|
* @metaobjectsdev/runtime-ts/drizzle-fastify.
|
|
103
|
+
${readOnlyAuthJsDoc}
|
|
96
104
|
*/
|
|
97
105
|
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
98
106
|
await fastify.register(async (instance) => {
|
|
@@ -115,6 +123,7 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
115
123
|
* Exposes GET list + GET :id only. POST/PATCH/DELETE return 405.
|
|
116
124
|
* Customize: register this as-is, or import individual route helpers from
|
|
117
125
|
* @metaobjectsdev/runtime-ts/drizzle-fastify.
|
|
126
|
+
${readOnlyAuthJsDoc}
|
|
118
127
|
*/
|
|
119
128
|
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
120
129
|
${mountReadOnlyCrudRoutesSym}({
|
|
@@ -147,6 +156,7 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
147
156
|
const readViewLineFlat = writeThrough ? `\n readView: ${camelName}View,` : "";
|
|
148
157
|
const exposeLinePrefixed = exposeLine(expose, " ");
|
|
149
158
|
const exposeLineFlat = exposeLine(expose, " ");
|
|
159
|
+
const crudAuthJsDoc = authSeamJsDoc({ framework: "fastify", handlerName, narrowable: true });
|
|
150
160
|
|
|
151
161
|
const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
|
|
152
162
|
const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
@@ -185,6 +195,7 @@ import {
|
|
|
185
195
|
* helpers (mountListRoute, mountGetRoute, ...) from
|
|
186
196
|
* @metaobjectsdev/runtime-ts/drizzle-fastify and mix with your own handlers
|
|
187
197
|
* (auth, side effects, etc.).
|
|
198
|
+
${crudAuthJsDoc}
|
|
188
199
|
*/
|
|
189
200
|
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
190
201
|
await fastify.register(async (instance) => {
|
|
@@ -210,6 +221,7 @@ ${m2mMountsPrefixed} }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
|
|
|
210
221
|
* helpers (mountListRoute, mountGetRoute, ...) from
|
|
211
222
|
* @metaobjectsdev/runtime-ts/drizzle-fastify and mix with your own handlers
|
|
212
223
|
* (auth, side effects, etc.).
|
|
224
|
+
${crudAuthJsDoc}
|
|
213
225
|
*/
|
|
214
226
|
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
215
227
|
${mountCrudRoutesSym}({
|
|
@@ -444,6 +456,9 @@ function renderTphRoutesFile(
|
|
|
444
456
|
});
|
|
445
457
|
|
|
446
458
|
const mounts = joinCode([polymorphic, ...subtypeMounts], { on: "\n" });
|
|
459
|
+
// The base path is read-only by construction (TPH_POLYMORPHIC_VERBS), but the
|
|
460
|
+
// per-subtype mounts below it are full CRUD — so `expose` does narrow this file.
|
|
461
|
+
const tphAuthJsDoc = authSeamJsDoc({ framework: "fastify", handlerName, narrowable: true });
|
|
447
462
|
|
|
448
463
|
const fn = ctx.apiPrefix
|
|
449
464
|
? code`
|
|
@@ -452,6 +467,7 @@ function renderTphRoutesFile(
|
|
|
452
467
|
*
|
|
453
468
|
* GET ${baseName}.$path (+ /:id) lists/gets the discriminated union; each
|
|
454
469
|
* /${baseName}.$path/<subtype> path is a full per-subtype CRUD set.
|
|
470
|
+
${tphAuthJsDoc}
|
|
455
471
|
*/
|
|
456
472
|
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
457
473
|
await fastify.register(async (instance) => {
|
|
@@ -465,6 +481,7 @@ ${mounts}
|
|
|
465
481
|
*
|
|
466
482
|
* GET ${baseName}.$path (+ /:id) lists/gets the discriminated union; each
|
|
467
483
|
* /${baseName}.$path/<subtype> path is a full per-subtype CRUD set.
|
|
484
|
+
${tphAuthJsDoc}
|
|
468
485
|
*/
|
|
469
486
|
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
470
487
|
${mounts}
|
|
@@ -474,6 +491,6 @@ ${mounts}
|
|
|
474
491
|
const header =
|
|
475
492
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
476
493
|
`// Source metadata: ${baseName} (${base.fqn()}) — TPH discriminator base\n` +
|
|
477
|
-
|
|
494
|
+
sidecarLine(`${baseName}.extra.ts`);
|
|
478
495
|
return header + fn.toString();
|
|
479
496
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { renderEntityConstants } from "./entity-constants.js";
|
|
23
23
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
24
24
|
import { renderFilterType } from "./filter-type.js";
|
|
25
|
-
import { GENERATED_HEADER } from "../constants.js";
|
|
25
|
+
import { GENERATED_HEADER, sidecarLine } from "../constants.js";
|
|
26
26
|
import { namesRef, namesConstArg } from "../names.js";
|
|
27
27
|
|
|
28
28
|
export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: RenderContext): string {
|
|
@@ -84,6 +84,6 @@ export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: Ren
|
|
|
84
84
|
const header =
|
|
85
85
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
86
86
|
`// Source metadata: ${obj.name} (${obj.fqn()})\n` +
|
|
87
|
-
|
|
87
|
+
sidecarLine(`${emittedName}.extra.ts`);
|
|
88
88
|
return header + body;
|
|
89
89
|
}
|