@metaobjectsdev/codegen-ts 1.0.2 → 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/enum-shared.d.ts +9 -2
- package/dist/enum-shared.d.ts.map +1 -1
- package/dist/enum-shared.js +12 -3
- package/dist/enum-shared.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 +182 -38
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +48 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +4 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/shared-model-file.d.ts +23 -0
- package/dist/generators/shared-model-file.d.ts.map +1 -0
- package/dist/generators/shared-model-file.js +235 -0
- package/dist/generators/shared-model-file.js.map +1 -0
- 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 +29 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +38 -1
- 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/enums-file.d.ts +11 -1
- package/dist/templates/enums-file.d.ts.map +1 -1
- package/dist/templates/enums-file.js +10 -2
- package/dist/templates/enums-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/enum-shared.ts +17 -3
- package/src/generator-registry.ts +253 -41
- package/src/generator.ts +48 -1
- package/src/generators/entity-file.ts +7 -1
- package/src/generators/index.ts +11 -0
- package/src/generators/shared-model-file.ts +292 -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 +12 -3
- 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 +77 -1
- package/src/templates/entity-file.ts +2 -2
- package/src/templates/enums-file.ts +13 -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
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
|
@@ -6,7 +6,10 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import type { MetaData, MetaObject } from "@metaobjectsdev/metadata";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isMetaRoot, OBJECT_SUBTYPE_VALUE, FIELD_SUBTYPE_TIMESTAMP, FIELD_ATTR_FILTERABLE,
|
|
11
|
+
composeRegistry, coreProviders,
|
|
12
|
+
} from "@metaobjectsdev/metadata";
|
|
10
13
|
import { assignEmittedNames } from "./naming/collision-names.js";
|
|
11
14
|
import { isAbstract } from "./instance-artifacts.js";
|
|
12
15
|
import { dbEmittingObjects, missingDialectMessage } from "./db-emitting.js";
|
|
@@ -33,6 +36,11 @@ import {
|
|
|
33
36
|
type BaselineMode,
|
|
34
37
|
type DecideAndWriteOpts,
|
|
35
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";
|
|
36
44
|
|
|
37
45
|
/** JS-identifier-shape only. Prevents filesystem traversal when metadata comes
|
|
38
46
|
* from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
|
|
@@ -71,6 +79,15 @@ export interface RunGenOpts {
|
|
|
71
79
|
* `--dry-run`, and watching it reappear.
|
|
72
80
|
*/
|
|
73
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>;
|
|
74
91
|
/**
|
|
75
92
|
* Output scope — an object is generated only when this predicate returns true
|
|
76
93
|
* for its fully-qualified name (`obj.resolutionKey()`, `<package>::<name>`).
|
|
@@ -106,6 +123,25 @@ export interface RunGenOpts {
|
|
|
106
123
|
* they pass straight through.
|
|
107
124
|
*/
|
|
108
125
|
scope?: (fqn: string) => boolean;
|
|
126
|
+
/**
|
|
127
|
+
* FR-023 §4.3 — the collection's own source files (`Collection.ownFiles`; never
|
|
128
|
+
* a dependency's snapshot artifact), threaded onto `GenContext.sourceFiles` for
|
|
129
|
+
* `sharedModelFile()`'s default `files` selection. The CLI's `gen` command
|
|
130
|
+
* always passes `genCollection.ownFiles`; a programmatic caller that never
|
|
131
|
+
* wires `sharedModelFile()` (or always passes that generator's own `files`
|
|
132
|
+
* option explicitly) can omit this with no behavior change elsewhere.
|
|
133
|
+
*/
|
|
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[];
|
|
109
145
|
}
|
|
110
146
|
|
|
111
147
|
export interface RunGenResult {
|
|
@@ -374,6 +410,23 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
374
410
|
// 2. Resolve targets + entity-module target.
|
|
375
411
|
const config = normalizeConfig(opts.config);
|
|
376
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
|
+
|
|
423
|
+
// FR-023 §4.3 — the run's composed registry, threaded onto every GenContext.
|
|
424
|
+
// Mirrors sdk's `loadMemory` exactly (`defaultLoadMemoryProviders` = core
|
|
425
|
+
// providers, then the project's own `config.providers` appended) so
|
|
426
|
+
// `sharedModelFile()`'s standalone re-load of a `files` subset sees the SAME
|
|
427
|
+
// vocabulary `opts.metadata` was originally loaded with.
|
|
428
|
+
const registry = composeRegistry([...coreProviders, ...(config.providers ?? [])]);
|
|
429
|
+
|
|
377
430
|
// The compile break (`value` → `fetcher` on the provider) sends an adopter to the right
|
|
378
431
|
// line; this names the value to put there, once, for the projects that had one.
|
|
379
432
|
if (shouldNoteBaseUrlMove(config.apiPrefix, recordedEngine)) {
|
|
@@ -509,6 +562,20 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
509
562
|
warnMissingPromptGenerators(root, config.generators, (m) => warnings.push(m));
|
|
510
563
|
warnUnmarkedUiGenerators(config.generators, (m) => warnings.push(m));
|
|
511
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
|
+
|
|
512
579
|
// <Entity>Names is opt-in on TypeScript and an existing project gets no signal that
|
|
513
580
|
// it exists. Fires ONCE, on the first gen after crossing the release that made it the
|
|
514
581
|
// doctrine — see shouldNoteNamesArtifactAbsent for why it is keyed on the engine stamp
|
|
@@ -605,6 +672,12 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
605
672
|
entities: safeEntities,
|
|
606
673
|
loadedRoot: root,
|
|
607
674
|
matches: (e) => generator.filter?.(e) ?? true,
|
|
675
|
+
// FR-023 §11.1 item 2 — the SAME `scope` predicate that already narrowed
|
|
676
|
+
// `entities` above, re-exposed for a template that renders once over the
|
|
677
|
+
// WHOLE loaded root (the shared-enums module, so far the only one) rather
|
|
678
|
+
// than per matched entity, where `matches` never runs. Absent scope ⇒ no
|
|
679
|
+
// `select` at all, byte-identical to a project with no `scope` declared.
|
|
680
|
+
...(scope !== undefined && { select: scope }),
|
|
608
681
|
config: {
|
|
609
682
|
outDir: selfTarget.outDir,
|
|
610
683
|
extStyle: config.extStyle,
|
|
@@ -617,6 +690,9 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
617
690
|
},
|
|
618
691
|
renderContext,
|
|
619
692
|
...(projectRoot !== undefined && { projectRoot }),
|
|
693
|
+
registry,
|
|
694
|
+
...(opts.sourceFiles !== undefined && { sourceFiles: opts.sourceFiles }),
|
|
695
|
+
...(selectedManifests !== undefined && { libraries: selectedManifests }),
|
|
620
696
|
warn: (msg) => warnings.push(`[${generator.name}] ${msg}`),
|
|
621
697
|
};
|
|
622
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
|
}
|
|
@@ -37,9 +37,20 @@ export const ${sharedEnumZodConstName(e.name)} = ${z}.enum([${members}]);
|
|
|
37
37
|
/**
|
|
38
38
|
* The full shared-enums module body, or null when the model has no materialized
|
|
39
39
|
* shared enums (so the generator emits no file at all).
|
|
40
|
+
*
|
|
41
|
+
* `opts.select` (FR-023 §11.1 item 2) — the model-wide scope predicate over a
|
|
42
|
+
* CONSUMING entity's `resolutionKey()`: an enum extended only by entities
|
|
43
|
+
* `select` excludes is not materialized. This module renders once per run from
|
|
44
|
+
* the WHOLE loaded root (never per-generator-filtered — see #266), so it is the
|
|
45
|
+
* one shared-enums call site that must be threaded the scope predicate
|
|
46
|
+
* separately from `ctx.matches`. Omitted ⇒ every entity counts, byte-identical
|
|
47
|
+
* to a caller that never passes `opts` at all.
|
|
40
48
|
*/
|
|
41
|
-
export function renderSharedEnumsFile(
|
|
42
|
-
|
|
49
|
+
export function renderSharedEnumsFile(
|
|
50
|
+
root: MetaRoot,
|
|
51
|
+
opts?: { select?: (fqn: string) => boolean },
|
|
52
|
+
): string | null {
|
|
53
|
+
const enums = materializedSharedEnums(root, opts?.select);
|
|
43
54
|
if (enums.length === 0) return null;
|
|
44
55
|
|
|
45
56
|
const body = joinCode(enums.map(renderOneSharedEnum), { on: "\n" }).toString();
|
|
@@ -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
|
}
|