@metaobjectsdev/codegen-ts 0.24.4 → 0.25.0
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/README.md +37 -12
- package/dist/api-surface.d.ts +29 -0
- package/dist/api-surface.d.ts.map +1 -1
- package/dist/api-surface.js +47 -0
- package/dist/api-surface.js.map +1 -1
- package/dist/client-directive.d.ts +18 -0
- package/dist/client-directive.d.ts.map +1 -0
- package/dist/client-directive.js +37 -0
- package/dist/client-directive.js.map +1 -0
- package/dist/column-mapper.d.ts +12 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +1 -0
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +24 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +54 -19
- package/dist/constants.js.map +1 -1
- package/dist/db-emitting.d.ts +6 -0
- package/dist/db-emitting.d.ts.map +1 -0
- package/dist/db-emitting.js +26 -0
- package/dist/db-emitting.js.map +1 -0
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +7 -1
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +5 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/agent-docs-file.d.ts +19 -0
- package/dist/generators/agent-docs-file.d.ts.map +1 -0
- package/dist/generators/agent-docs-file.js +257 -0
- package/dist/generators/agent-docs-file.js.map +1 -0
- package/dist/generators/agent-requirements-page.d.ts +22 -0
- package/dist/generators/agent-requirements-page.d.ts.map +1 -0
- package/dist/generators/agent-requirements-page.js +175 -0
- package/dist/generators/agent-requirements-page.js.map +1 -0
- package/dist/generators/agent-schema-input.d.ts +69 -0
- package/dist/generators/agent-schema-input.d.ts.map +1 -0
- package/dist/generators/agent-schema-input.js +23 -0
- package/dist/generators/agent-schema-input.js.map +1 -0
- package/dist/generators/agent-schema-page.d.ts +21 -0
- package/dist/generators/agent-schema-page.d.ts.map +1 -0
- package/dist/generators/agent-schema-page.js +241 -0
- package/dist/generators/agent-schema-page.js.map +1 -0
- package/dist/generators/agent-ui-page.d.ts +22 -0
- package/dist/generators/agent-ui-page.d.ts.map +1 -0
- package/dist/generators/agent-ui-page.js +253 -0
- package/dist/generators/agent-ui-page.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -1
- package/dist/generators/api-docs-file.js +13 -6
- package/dist/generators/api-docs-file.js.map +1 -1
- package/dist/generators/api-model.d.ts +6 -0
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +54 -35
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/callable-file.js +1 -1
- package/dist/generators/callable-file.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +47 -2
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +6 -2
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/names-file.d.ts +17 -0
- package/dist/generators/names-file.d.ts.map +1 -0
- package/dist/generators/names-file.js +95 -0
- package/dist/generators/names-file.js.map +1 -0
- package/dist/generators/requirements-markdown.d.ts +18 -1
- package/dist/generators/requirements-markdown.d.ts.map +1 -1
- package/dist/generators/requirements-markdown.js +11 -9
- package/dist/generators/requirements-markdown.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +17 -2
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +15 -12
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +17 -2
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +10 -8
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +20 -6
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/index.d.ts +21 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -4
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +67 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +10 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/names.d.ts +275 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +434 -0
- package/dist/names.js.map +1 -0
- package/dist/naming.d.ts +17 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +21 -0
- package/dist/naming.js.map +1 -1
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +15 -3
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +4 -3
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts +25 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +79 -6
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/prompt-generator-gate.d.ts +27 -0
- package/dist/prompt-generator-gate.d.ts.map +1 -0
- package/dist/prompt-generator-gate.js +62 -0
- package/dist/prompt-generator-gate.js.map +1 -0
- package/dist/reference-templates.d.ts +22 -5
- package/dist/reference-templates.d.ts.map +1 -1
- package/dist/reference-templates.js +55 -21
- package/dist/reference-templates.js.map +1 -1
- package/dist/render-context.d.ts +20 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +2 -0
- package/dist/render-context.js.map +1 -1
- package/dist/retired-codegen-attrs.d.ts +32 -0
- package/dist/retired-codegen-attrs.d.ts.map +1 -0
- package/dist/retired-codegen-attrs.js +47 -0
- package/dist/retired-codegen-attrs.js.map +1 -0
- package/dist/routes-expose.d.ts +63 -0
- package/dist/routes-expose.d.ts.map +1 -0
- package/dist/routes-expose.js +57 -0
- package/dist/routes-expose.js.map +1 -0
- package/dist/runner.d.ts +18 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +115 -7
- package/dist/runner.js.map +1 -1
- package/dist/templates/callable-file.d.ts +7 -2
- package/dist/templates/callable-file.d.ts.map +1 -1
- package/dist/templates/callable-file.js +54 -9
- package/dist/templates/callable-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +236 -65
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +15 -6
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +85 -161
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +38 -4
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/entity-meta-file.d.ts +15 -1
- package/dist/templates/entity-meta-file.d.ts.map +1 -1
- package/dist/templates/entity-meta-file.js +12 -2
- package/dist/templates/entity-meta-file.js.map +1 -1
- package/dist/templates/entity-ui-descriptor.d.ts +112 -0
- package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
- package/dist/templates/entity-ui-descriptor.js +238 -0
- package/dist/templates/entity-ui-descriptor.js.map +1 -0
- package/dist/templates/field-meta.d.ts +46 -9
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +95 -21
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/names-decl.d.ts +38 -0
- package/dist/templates/names-decl.d.ts.map +1 -0
- package/dist/templates/names-decl.js +104 -0
- package/dist/templates/names-decl.js.map +1 -0
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +54 -8
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/projection-decl.d.ts +21 -0
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +53 -29
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +17 -2
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +3 -1
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/requirement-test.d.ts.map +1 -1
- package/dist/templates/requirement-test.js +48 -21
- package/dist/templates/requirement-test.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts +2 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +11 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts +2 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +54 -21
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +15 -1
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +37 -1
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +27 -3
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +21 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +40 -5
- package/dist/templates/zod-validators.js.map +1 -1
- package/dist/view-context.d.ts +24 -0
- package/dist/view-context.d.ts.map +1 -0
- package/dist/view-context.js +81 -0
- package/dist/view-context.js.map +1 -0
- package/package.json +6 -6
- package/src/api-surface.ts +53 -0
- package/src/client-directive.ts +36 -0
- package/src/column-mapper.ts +13 -0
- package/src/constants.ts +91 -16
- package/src/db-emitting.ts +31 -0
- package/src/generator-registry.ts +7 -0
- package/src/generator.ts +5 -0
- package/src/generators/agent-docs-file.ts +297 -0
- package/src/generators/agent-requirements-page.ts +199 -0
- package/src/generators/agent-schema-input.ts +88 -0
- package/src/generators/agent-schema-page.ts +297 -0
- package/src/generators/agent-ui-page.ts +290 -0
- package/src/generators/api-docs-file.ts +13 -6
- package/src/generators/api-model.ts +63 -34
- package/src/generators/callable-file.ts +1 -1
- package/src/generators/docs-data-builder.ts +46 -2
- package/src/generators/docs-data.ts +6 -2
- package/src/generators/index.ts +10 -0
- package/src/generators/names-file.ts +98 -0
- package/src/generators/requirements-markdown.ts +36 -2
- package/src/generators/routes-file-hono.ts +26 -15
- package/src/generators/routes-file.ts +20 -7
- package/src/generators/template-generator.ts +20 -6
- package/src/index.ts +63 -5
- package/src/metaobjects-config.ts +78 -2
- package/src/names.ts +630 -0
- package/src/naming.ts +23 -0
- package/src/overwrite-policy.ts +15 -3
- package/src/payload-codegen.ts +4 -3
- package/src/projection/extract-view-spec.ts +85 -6
- package/src/prompt-generator-gate.ts +73 -0
- package/src/reference/barrel.ts +3 -0
- package/src/reference/entity.ts +28 -2
- package/src/reference/names.ts +125 -0
- package/src/reference/queries.ts +14 -2
- package/src/reference/routes-hono.ts +113 -0
- package/src/reference/routes.ts +30 -9
- package/src/reference-templates.ts +61 -21
- package/src/render-context.ts +22 -1
- package/src/retired-codegen-attrs.ts +52 -0
- package/src/routes-expose.ts +86 -0
- package/src/runner.ts +129 -11
- package/src/templates/callable-file.ts +60 -8
- package/src/templates/drizzle-schema.ts +248 -60
- package/src/templates/entity-constants.ts +95 -184
- package/src/templates/entity-file.ts +46 -4
- package/src/templates/entity-meta-file.ts +16 -3
- package/src/templates/entity-ui-descriptor.ts +305 -0
- package/src/templates/field-meta.ts +100 -19
- package/src/templates/names-decl.ts +165 -0
- package/src/templates/output-parser.ts +54 -7
- package/src/templates/projection-decl.ts +72 -31
- package/src/templates/queries-file.ts +17 -2
- package/src/templates/render-helper.ts +3 -1
- package/src/templates/requirement-test.ts +49 -18
- package/src/templates/routes-file-hono.ts +14 -2
- package/src/templates/routes-file.ts +69 -24
- package/src/templates/value-object-file.ts +15 -1
- package/src/templates/view-decl.ts +53 -3
- package/src/templates/zod-validators.ts +41 -5
- package/src/view-context.ts +88 -0
|
@@ -15,11 +15,15 @@ import {
|
|
|
15
15
|
TYPE_TEMPLATE,
|
|
16
16
|
TEMPLATE_SUBTYPE_PROMPT,
|
|
17
17
|
FIELD_SUBTYPE_OBJECT,
|
|
18
|
+
FIELD_SUBTYPE_ENUM,
|
|
18
19
|
FIELD_ATTR_OBJECT_REF,
|
|
19
20
|
TEMPLATE_ATTR_RESPONSE_REF,
|
|
20
21
|
RESPONSE_FORMAT_XML,
|
|
21
22
|
resolveObjectRef,
|
|
22
23
|
} from "@metaobjectsdev/metadata";
|
|
24
|
+
import type { MetaField } from "@metaobjectsdev/metadata";
|
|
25
|
+
import { enumValues, zodEnumExpr } from "../enum-meta.js";
|
|
26
|
+
import { templateSymbolBase } from "../naming.js";
|
|
23
27
|
import { responseShape } from "./find-inbound.js";
|
|
24
28
|
import { isRequired } from "./fr010-field-mapping.js";
|
|
25
29
|
import {
|
|
@@ -32,6 +36,29 @@ import {
|
|
|
32
36
|
} from "./extract-delegate-emitter.js";
|
|
33
37
|
import type { RenderContext } from "../render-context.js";
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Wire-shape validators per field subtype, for a JSON reply from a model.
|
|
41
|
+
*
|
|
42
|
+
* This map used to hold only the nine entries above `uuid`, so EVERY other subtype fell
|
|
43
|
+
* through the `?? "z.unknown()"` below — enum, uuid, date, time, timestamp, decimal,
|
|
44
|
+
* currency, uri and inet. `z.unknown()` accepts anything, including `null` on a `@required`
|
|
45
|
+
* field, so a declared response payload of eleven fields validated three of them. It
|
|
46
|
+
* mapped `class`, `short` and `byte` — the three subtypes this project CUT as
|
|
47
|
+
* non-functional registration-only stubs — while missing `currency` and `uuid`, which are
|
|
48
|
+
* exactly where a hallucinated value costs money or corrupts an identifier.
|
|
49
|
+
*
|
|
50
|
+
* The inversion is what makes it serious: validation was strongest on the payload we
|
|
51
|
+
* control and absent on the reply we do not, in the tier whose entire name is
|
|
52
|
+
* parser-on-receipt. The tolerant extractor in this same generated file reads the live
|
|
53
|
+
* metadata and DOES reject a non-member; so did Python's `FieldSpec.enum_field`. Only this
|
|
54
|
+
* strict path — the one called `parse<Name>`, documented `@throws on validation failure`,
|
|
55
|
+
* and the first one an adopter reaches for — threw the domain away.
|
|
56
|
+
*
|
|
57
|
+
* Temporal and numeric subtypes stay STRING-shaped on the wire deliberately: these
|
|
58
|
+
* validate raw JSON, where a timestamp is an ISO string and a decimal is a string to
|
|
59
|
+
* avoid float loss (the same reasoning as `zod-validators.ts`'s `z.coerce.date()` note).
|
|
60
|
+
* Coercion into native types is the caller's job, after the shape is known good.
|
|
61
|
+
*/
|
|
35
62
|
const SCALAR_ZOD: Record<string, string> = {
|
|
36
63
|
string: "z.string()",
|
|
37
64
|
class: "z.string()",
|
|
@@ -42,6 +69,17 @@ const SCALAR_ZOD: Record<string, string> = {
|
|
|
42
69
|
double: "z.number()",
|
|
43
70
|
float: "z.number()",
|
|
44
71
|
boolean: "z.boolean()",
|
|
72
|
+
uuid: "z.string().uuid()",
|
|
73
|
+
date: "z.string()",
|
|
74
|
+
time: "z.string()",
|
|
75
|
+
timestamp: "z.string()",
|
|
76
|
+
// A decimal crosses the wire as a string precisely so it does not become a float;
|
|
77
|
+
// accepting a number here would re-admit the loss the subtype exists to prevent.
|
|
78
|
+
decimal: "z.string()",
|
|
79
|
+
// Integer minor units — the wire contract every port shares. Never a float.
|
|
80
|
+
currency: "z.number().int()",
|
|
81
|
+
uri: "z.string()",
|
|
82
|
+
inet: "z.string()",
|
|
45
83
|
};
|
|
46
84
|
|
|
47
85
|
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
@@ -84,6 +122,12 @@ function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, de
|
|
|
84
122
|
const inner = findObject(root, refName, field.parent?.package ?? field.parent?.fileDefaultPackage ?? "");
|
|
85
123
|
base = inner ? renderObjectSchema(inner, root, new Set(seen).add(refName), depth + 1) : "z.unknown()";
|
|
86
124
|
}
|
|
125
|
+
} else if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
126
|
+
// The members are declared, resolving (so an `extends`-inherited @values is seen),
|
|
127
|
+
// and already reachable — the send-side type in prompts.ts is built from them. The
|
|
128
|
+
// one closed domain in an untrusted reply is the one worth checking hardest.
|
|
129
|
+
const values = enumValues(field as MetaField);
|
|
130
|
+
base = values !== undefined && values.length > 0 ? zodEnumExpr(values) : "z.unknown()";
|
|
87
131
|
} else {
|
|
88
132
|
base = SCALAR_ZOD[field.subType] ?? "z.unknown()";
|
|
89
133
|
}
|
|
@@ -137,11 +181,14 @@ export function renderOutputParser(root: MetaData, templateName: string, ctx?: R
|
|
|
137
181
|
const { vo, ref: payloadRef } = shape;
|
|
138
182
|
|
|
139
183
|
const schema = renderObjectSchema(vo, root, new Set([payloadRef]), 0);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
184
|
+
// One base for every symbol in this file, shared with promptRender()'s handle — the
|
|
185
|
+
// three template emitters used to spell the same node three ways (see naming.ts).
|
|
186
|
+
const base = templateSymbolBase(templateName);
|
|
187
|
+
const schemaName = `${base}Schema`;
|
|
188
|
+
const dataName = `${base}Data`;
|
|
189
|
+
const errorName = `${base}ValidationError`;
|
|
190
|
+
const parseName = `parse${base}`;
|
|
191
|
+
const safeParseName = `safeParse${base}`;
|
|
145
192
|
|
|
146
193
|
// FR-010: emit the tolerant extract() API alongside the strict Zod parser.
|
|
147
194
|
//
|
|
@@ -209,8 +256,8 @@ export function ${safeParseName}(
|
|
|
209
256
|
// Unconditional since ADR-0052: a declared @responseRef IS the request for the
|
|
210
257
|
// tolerant path, and @responseFormat is a closed json|xml set, so there is no
|
|
211
258
|
// longer a third case to gate on.
|
|
212
|
-
const extractedName = `${templateName}Extracted`;
|
|
213
|
-
const extractLenientWithName = `extractLenient${templateName}WithLoader`;
|
|
259
|
+
const extractedName = `${templateSymbolBase(templateName)}Extracted`;
|
|
260
|
+
const extractLenientWithName = `extractLenient${templateSymbolBase(templateName)}WithLoader`;
|
|
214
261
|
const payloadFqnConst = `${templateName.toUpperCase()}_PAYLOAD_NAME`;
|
|
215
262
|
const formatEnum = format === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON";
|
|
216
263
|
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
11
11
|
import {
|
|
12
12
|
MetaField, MetaObject, type MetaRoot, isMetaObject,
|
|
13
|
-
FIELD_ATTR_OBJECT_REF, stripPackage,
|
|
13
|
+
FIELD_ATTR_OBJECT_REF, stripPackage, resolveColumnName, resolveTableSchema,
|
|
14
14
|
} from "@metaobjectsdev/metadata";
|
|
15
15
|
import { projectionViewName } from "../projection/extract-view-spec.js";
|
|
16
|
-
import { columnNameFromField, toSnakeCase, pluralize } from "../naming.js";
|
|
17
16
|
import { GENERATED_HEADER } from "../constants.js";
|
|
18
17
|
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
19
18
|
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
20
19
|
import { valueObjectModuleSpecifier } from "../import-path.js";
|
|
20
|
+
import { physicalNameExpr, sourceSchemaExpr, type ObjectNames } from "../names.js";
|
|
21
21
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
22
22
|
import { renderFilterType } from "./filter-type.js";
|
|
23
|
-
import {
|
|
23
|
+
import { buildUiFieldDescriptor, resourcePath } from "./entity-ui-descriptor.js";
|
|
24
24
|
import { renderExistingViewDecl, renderViewReadZodObject } from "./view-decl.js";
|
|
25
25
|
import { primaryIdentityFieldNames } from "./zod-validators.js";
|
|
26
26
|
|
|
@@ -31,6 +31,11 @@ import { primaryIdentityFieldNames } from "./zod-validators.js";
|
|
|
31
31
|
export interface ProjectionDeclOpts {
|
|
32
32
|
readonly columnNamingStrategy: ColumnNamingStrategy;
|
|
33
33
|
readonly dialect: "postgres" | "sqlite";
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Accepted and IGNORED — a projection's descriptor no longer carries the
|
|
36
|
+
* API base URL, same as an entity's. Kept on the options object because removing a
|
|
37
|
+
* field makes every caller still passing it fail TypeScript's excess-property check.
|
|
38
|
+
*/
|
|
34
39
|
readonly apiPrefix?: string;
|
|
35
40
|
/** Drives the timestamp column TS type (Date vs string) in the view declaration. */
|
|
36
41
|
readonly timestampMode?: "date" | "string";
|
|
@@ -53,24 +58,23 @@ export interface ProjectionDeclOpts {
|
|
|
53
58
|
* e.g. a shared types package consumed by a web client that has no Drizzle.
|
|
54
59
|
*/
|
|
55
60
|
readonly includeViewDecl?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* §A6 — the resolved names artifact for this projection plus the ts-poet symbol
|
|
63
|
+
* reference (`imp()`'d import of `<Projection>Names`), when the names generator is
|
|
64
|
+
* in the run. Built by the caller via `resolveObjectNames` + `imp(...)` — the same
|
|
65
|
+
* pair `drizzle-schema.ts` builds — so this file never resolves names itself; it
|
|
66
|
+
* only decides, per field, whether to reference the constant or fall back to the
|
|
67
|
+
* literal (a lookup MISS, never a divergence — `primaryRdbSource` already refused any
|
|
68
|
+
* object whose `@role: primary` sources disagree on a physical name). Threaded into `renderExistingViewDecl`
|
|
69
|
+
* too, since a projection's primary source IS the view its `sources.primary.view` names.
|
|
70
|
+
*/
|
|
71
|
+
readonly names?: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
// ---------------------------------------------------------------------------
|
|
59
75
|
// Helpers
|
|
60
76
|
// ---------------------------------------------------------------------------
|
|
61
77
|
|
|
62
|
-
/**
|
|
63
|
-
* Convert a PascalCase projection name to a kebab-pluralized URL path.
|
|
64
|
-
* "ProgramSummary" → "/program-summaries"
|
|
65
|
-
* "CustomerSummary" → "/customer-summaries"
|
|
66
|
-
* "Box" → "/boxes"
|
|
67
|
-
* "Wish" → "/wishes"
|
|
68
|
-
*/
|
|
69
|
-
function pathFromProjectionName(name: string): string {
|
|
70
|
-
const kebab = toSnakeCase(pluralize(name)).replace(/_/g, "-");
|
|
71
|
-
return `/${kebab}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
78
|
// ---------------------------------------------------------------------------
|
|
75
79
|
// Public API
|
|
76
80
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +96,7 @@ export function renderProjectionDecl(
|
|
|
92
96
|
root: MetaRoot,
|
|
93
97
|
opts: ProjectionDeclOpts,
|
|
94
98
|
): string {
|
|
95
|
-
const { dialect, columnNamingStrategy,
|
|
99
|
+
const { dialect, columnNamingStrategy, timestampMode = "string", allowlists = true, ctx, includeViewDecl = true, names } = opts;
|
|
96
100
|
|
|
97
101
|
// ADR-0044/#228 — resolve a projection field's `@objectRef` to the value object's
|
|
98
102
|
// EMITTED name + module TOGETHER (lock-step): bare when unique in the run,
|
|
@@ -137,29 +141,67 @@ export function renderProjectionDecl(
|
|
|
137
141
|
// inherited fields are not duplicated.
|
|
138
142
|
for (const f of projection.ownFields()) allFields.push(f);
|
|
139
143
|
|
|
140
|
-
const constFieldLines:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
const constFieldLines: Code[] = allFields.map((f) => {
|
|
145
|
+
// §A4: resolveColumnName, NOT columnNameFromField — the latter takes a string and so
|
|
146
|
+
// cannot read @column, silently substituting the naming strategy's answer for a
|
|
147
|
+
// declared or inherited physical name. ADR-0039: resolving accessor, so a projection
|
|
148
|
+
// field inheriting @column through `extends` resolves it.
|
|
149
|
+
const dbCol = resolveColumnName(f, columnNamingStrategy);
|
|
150
|
+
// A6 — reference the constant whenever the artifact is in the run AND carries this
|
|
151
|
+
// field. A lookup MISS (a TPH subtype field the base's names artifact never saw) is
|
|
152
|
+
// normal, not a divergence — fall back to the literal, same as every other §A6 site.
|
|
153
|
+
const namesEntry = names?.resolved.fields[f.name];
|
|
154
|
+
const dbColExpr: Code =
|
|
155
|
+
names !== undefined && namesEntry !== undefined
|
|
156
|
+
? code`${names.symbol}.fields.${f.name}.column`
|
|
157
|
+
: code`${JSON.stringify(dbCol)}`;
|
|
158
|
+
// #356 — a projection's descriptor is the same shape the entity descriptor emits,
|
|
159
|
+
// and now literally the SAME derivation: `buildUiFieldDescriptor` is what
|
|
160
|
+
// `renderEntityConstants` reads, so the view kind, the label and the currency meta
|
|
161
|
+
// cannot come out differently here. A projection is read-only, so the members that
|
|
162
|
+
// describe a FORM (htmlType / placeholder / helpText / rules) are not emitted — a
|
|
163
|
+
// subset of one descriptor rather than a second answer — and `dbCol`, which only a
|
|
164
|
+
// view-backed const carries, is added.
|
|
165
|
+
const ui = buildUiFieldDescriptor(f);
|
|
166
|
+
const baseEntry = code`name: ${JSON.stringify(ui.name)}, label: ${JSON.stringify(ui.label)}, view: ${JSON.stringify(ui.view)}, dbCol: ${dbColExpr}`;
|
|
167
|
+
if (ui.currency !== undefined) {
|
|
168
|
+
return code` ${f.name}: { ${baseEntry}, currency: ${JSON.stringify(ui.currency.currency)}, locale: ${JSON.stringify(ui.currency.locale)} },`;
|
|
148
169
|
}
|
|
149
|
-
return ` ${f.name}: { ${baseEntry} },`;
|
|
170
|
+
return code` ${f.name}: { ${baseEntry} },`;
|
|
150
171
|
});
|
|
151
172
|
|
|
152
173
|
const projName = projection.name;
|
|
153
174
|
const camelName = projName.charAt(0).toLowerCase() + projName.slice(1);
|
|
154
|
-
|
|
175
|
+
// ONE derivation of `$path`, shared with the entity const and with every page that
|
|
176
|
+
// documents an endpoint — `resourcePath` owns the projection/entity split. The
|
|
177
|
+
// kebab-pluralized spelling this module used to compute for itself is what it still
|
|
178
|
+
// returns for a projection; the second copy is what let `agent/ui.md` and
|
|
179
|
+
// `api/AGENT-API.md` both print the snake-cased entity spelling for a view.
|
|
180
|
+
const path = resourcePath(projection);
|
|
181
|
+
|
|
182
|
+
// A6 Task 1 fix round — the descriptor's $view is a SECOND, independent embedding
|
|
183
|
+
// of the view's physical name (the Drizzle .existing() decl in view-decl.ts is the
|
|
184
|
+
// first, already converted). Same "is the artifact in this run" condition, no
|
|
185
|
+
// equality guard: `names.resolved.name` is always the PRIMARY source's physical
|
|
186
|
+
// name, while `viewName` is derived by `viewName()` in extract-view-spec.ts, which
|
|
187
|
+
// for a projection declaring a role:replica read-only source BEFORE its role:primary
|
|
188
|
+
// one used to return a different, declaration-order-dependent string. That is why
|
|
189
|
+
// both spellings here must come from the constant rather than from two resolvers.
|
|
190
|
+
const viewLine: Code = code`$view: ${physicalNameExpr(names, viewName, projection)}`;
|
|
191
|
+
// @schema for the view binding below. postgres-only downstream (view-decl gates on dialect).
|
|
192
|
+
const projectionSchema = resolveTableSchema(projection);
|
|
155
193
|
|
|
156
194
|
// The projection's primary-identity fields type non-null in the view decl +
|
|
157
195
|
// read schema even without @required (a PK is never NULL; see ViewDeclOpts).
|
|
158
196
|
const pkFieldNames: ReadonlySet<string> = new Set(primaryIdentityFieldNames(projection));
|
|
159
197
|
const sections: Code[] = [
|
|
160
198
|
...(includeViewDecl
|
|
161
|
-
? [renderExistingViewDecl(allFields, viewName, `${camelName}View`, {
|
|
162
|
-
dialect, columnNamingStrategy, timestampMode, voRef, pkFieldNames,
|
|
199
|
+
? [renderExistingViewDecl(allFields, physicalNameExpr(names, viewName, projection), `${camelName}View`, {
|
|
200
|
+
dialect, columnNamingStrategy, timestampMode, voRef, pkFieldNames, names,
|
|
201
|
+
// A projection's own artifact names its own view, so the constant IS this view's
|
|
202
|
+
// schema — unlike the write-through replica case in entity-file.ts.
|
|
203
|
+
schema: projectionSchema === undefined ? undefined
|
|
204
|
+
: sourceSchemaExpr(names) ?? projectionSchema,
|
|
163
205
|
})]
|
|
164
206
|
: []),
|
|
165
207
|
code`
|
|
@@ -173,10 +215,9 @@ export type ${projName} = ${z}.infer<typeof ${projName}Schema>;
|
|
|
173
215
|
code`
|
|
174
216
|
export const ${projName} = {
|
|
175
217
|
$entity: ${JSON.stringify(projName)},
|
|
176
|
-
$
|
|
218
|
+
${viewLine},
|
|
177
219
|
$path: ${JSON.stringify(path)},
|
|
178
|
-
|
|
179
|
-
${constFieldLines.join("\n")}
|
|
220
|
+
${joinCode(constFieldLines, { on: "\n" })}
|
|
180
221
|
} as const;
|
|
181
222
|
`,
|
|
182
223
|
...(allowlists
|
|
@@ -35,16 +35,31 @@ import { hasAutoSetFields } from "./zod-validators.js";
|
|
|
35
35
|
* postgres.js, Neon, Vercel, pglite) — accepts whichever driver the consumer chose.
|
|
36
36
|
* - SQLite: `BaseSQLiteDatabase<"sync" | "async", …>` accepts BOTH sync (better-sqlite3)
|
|
37
37
|
* and async (libsql/Turso/D1) drivers; the generated queries `await` results, valid on either.
|
|
38
|
+
*
|
|
39
|
+
* EVERY type argument here must stay as OPEN as Drizzle's own constraint allows — a `Db`
|
|
40
|
+
* this alias cannot name is a helper the consumer cannot call, and generated code that
|
|
41
|
+
* does not compile is indistinguishable from generated code nobody imports. Two axes have
|
|
42
|
+
* now been re-pinned and re-widened in turn, so neither may be narrowed again:
|
|
43
|
+
* - DRIVER: pinning `NodePgDatabase` / `<"async">` rejected postgres.js and
|
|
44
|
+
* better-sqlite3. Fixed by dropping to the base classes above.
|
|
45
|
+
* - SCHEMA (`TFullSchema`): the idiomatic setup is `drizzle(client, { schema })`, which
|
|
46
|
+
* yields `PgDatabase<…, typeof schema>` / `BaseSQLiteDatabase<…, typeof schema>`.
|
|
47
|
+
* Leaving this parameter at Drizzle's `Record<string, never>` DEFAULT — spelled out on
|
|
48
|
+
* postgres, silently inherited on sqlite by supplying only two arguments — rejected
|
|
49
|
+
* every such db with TS2345 ("Type '\"work_item\"' is not assignable to type 'never'").
|
|
50
|
+
* `Record<string, unknown>` is verbatim Drizzle's own declared bound
|
|
51
|
+
* (`TFullSchema extends Record<string, unknown>`), so it admits both a schema-carrying
|
|
52
|
+
* and a schema-less db without reaching for `any`.
|
|
38
53
|
*/
|
|
39
54
|
function dbTypeBlock(dialect: "postgres" | "sqlite"): { import: string; alias: string } {
|
|
40
55
|
return dialect === "postgres"
|
|
41
56
|
? {
|
|
42
57
|
import: `import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";`,
|
|
43
|
-
alias: `type Db = PgDatabase<PgQueryResultHKT, Record<string,
|
|
58
|
+
alias: `type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;`,
|
|
44
59
|
}
|
|
45
60
|
: {
|
|
46
61
|
import: `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`,
|
|
47
|
-
alias: `type Db = BaseSQLiteDatabase<"sync" | "async", unknown
|
|
62
|
+
alias: `type Db = BaseSQLiteDatabase<"sync" | "async", unknown, Record<string, unknown>>;`,
|
|
48
63
|
};
|
|
49
64
|
}
|
|
50
65
|
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
type PayloadField,
|
|
53
53
|
type VerifyError,
|
|
54
54
|
} from "@metaobjectsdev/render";
|
|
55
|
+
import { templateSymbolBase } from "../naming.js";
|
|
55
56
|
|
|
56
57
|
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
57
58
|
// ADR-0042: package-local — resolveObjectRef binds a bare @objectRef in `referrerPkg` first (else root-level), an FQN exactly.
|
|
@@ -175,7 +176,8 @@ export function renderRenderHelper(
|
|
|
175
176
|
|
|
176
177
|
const fields = derivePayloadFieldTree(root, payloadRef, tmplPkg);
|
|
177
178
|
const ft = fieldTreeLiteral(fields);
|
|
178
|
-
|
|
179
|
+
// Same base as promptRender()'s handle and outputParser()'s symbols (naming.ts).
|
|
180
|
+
const fnName = `render${templateSymbolBase(templateName)}`;
|
|
179
181
|
// @payloadRef may arrive package-qualified (FQN) once resolved — a bare
|
|
180
182
|
// `::`-free name is required everywhere it's emitted as a TS identifier /
|
|
181
183
|
// import specifier below (mirrors the same stripPackage() call every other
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
// claims the capability works, so an empty green test asserts the opposite of the
|
|
11
11
|
// claim — the original defect recreated in a new place.
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
REQUIREMENT_STATUSES,
|
|
15
|
+
REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES,
|
|
16
|
+
REQUIREMENT_STATUS_RETIRED,
|
|
17
|
+
} from "@metaobjectsdev/metadata";
|
|
13
18
|
import { GENERATED_HEADER } from "../constants.js";
|
|
14
19
|
import type { RequirementView, ResolvedClaim } from "../requirement-walk.js";
|
|
15
20
|
|
|
@@ -27,21 +32,22 @@ export interface RequirementTestArgs {
|
|
|
27
32
|
* Statuses whose stub is SKIPPED rather than failing.
|
|
28
33
|
*
|
|
29
34
|
* The rule is "does this entry claim the capability works right now?" — only `live`
|
|
30
|
-
* and `partial` do. `planned` is intended-not-built; `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* wholesale — taking the `live` stubs with it.
|
|
35
|
+
* and `partial` do. `planned` is intended-not-built; `retired` is built-then-
|
|
36
|
+
* deliberately-removed. Emitting a failing stub for either reddens an application's
|
|
37
|
+
* suite forever for something nobody intends to build, which is the noise an app
|
|
38
|
+
* silences wholesale — taking the `live` stubs with it.
|
|
35
39
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* This set is derived from the LOADER's enum, not restated from it. It used to be a
|
|
41
|
+
* literal naming `abandoned` and `superseded`, and when 0.24.0 retired both and
|
|
42
|
+
* 0.24.2 put `retired` in their place, the set was left behind: it skipped two
|
|
43
|
+
* statuses the loader had begun REFUSING and failed on the one that replaced them,
|
|
44
|
+
* so every retired entry emitted a permanently red stub — the exact noise this set
|
|
45
|
+
* exists to prevent. Deriving it means the next status move cannot leave it behind:
|
|
46
|
+
* a status that is neither live nor partial is skipped by construction.
|
|
39
47
|
*/
|
|
40
|
-
const SKIPPED_STATUSES: ReadonlySet<string> = new Set(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"superseded",
|
|
44
|
-
]);
|
|
48
|
+
const SKIPPED_STATUSES: ReadonlySet<string> = new Set(
|
|
49
|
+
REQUIREMENT_STATUSES.filter((s) => !REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES.includes(s)),
|
|
50
|
+
);
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* Escape an author-supplied value for a double-quoted TS string literal.
|
|
@@ -112,17 +118,42 @@ export function renderRequirementTest(a: RequirementTestArgs): string {
|
|
|
112
118
|
// A `live` or `partial` stub asserts FAILURE until someone writes the real
|
|
113
119
|
// assertion over it. `expect.unreachable` names the requirement in the failure
|
|
114
120
|
// message, so a red run says which claim is unproven rather than just "failed".
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
// The two skipped statuses mean OPPOSITE things, so they must not share a body.
|
|
122
|
+
// `planned` is intended-not-built and the instruction is to write the assertion when
|
|
123
|
+
// it becomes live. `retired` is built-then-deliberately-removed, and telling a reader
|
|
124
|
+
// to write it "when this becomes live" instructs them to revive the capability —
|
|
125
|
+
// inverting the one guardrail 0.24.2 restored `retired` for. The repo's own harness
|
|
126
|
+
// generator already branches here; the shipped renderer now does too.
|
|
127
|
+
let body: string;
|
|
128
|
+
if (!skipped) {
|
|
129
|
+
body =
|
|
130
|
+
` expect.unreachable(\n` +
|
|
118
131
|
` "unimplemented requirement stub: ${testName} — " +\n` +
|
|
119
132
|
` "replace this with an assertion that fails when: ${forStringLiteral(a.counterexample)}",\n` +
|
|
120
133
|
` );`;
|
|
134
|
+
} else if (a.view.status === REQUIREMENT_STATUS_RETIRED) {
|
|
135
|
+
body =
|
|
136
|
+
` // Retired: this capability was deliberately removed and must not be rebuilt.\n` +
|
|
137
|
+
` // If you assert anything here, assert that it STAYS removed.`;
|
|
138
|
+
} else {
|
|
139
|
+
body = ` // Intended, not built. Write the assertion when this becomes live.`;
|
|
140
|
+
}
|
|
121
141
|
|
|
142
|
+
// The header states the CONDITION on the survival promise, not just the promise.
|
|
143
|
+
// It used to say the body "survives regeneration" flat, which is true of the body and
|
|
144
|
+
// false of the run: a stub is worthless until hand-edited, and the moment it is edited
|
|
145
|
+
// and pushed, every clone that did not generate it refuses the file and exits 1 —
|
|
146
|
+
// demonstrable by deleting the gitignored `.gen-state` bodies and re-running. Stating
|
|
147
|
+
// the machine-local half here is what keeps a reader from reading a red CI run as a
|
|
148
|
+
// broken promise. The remedy itself lives on the refusal, not in every stub.
|
|
122
149
|
return (
|
|
123
150
|
`// ${GENERATED_HEADER}.\n` +
|
|
124
|
-
`// The test IDENTITY is generated from the requirement; the BODY below is yours
|
|
125
|
-
`//
|
|
151
|
+
`// The test IDENTITY is generated from the requirement; the BODY below is yours.\n` +
|
|
152
|
+
`// Do not rename the test — the name is the link.\n` +
|
|
153
|
+
`// Your body is never overwritten: MERGED where .metaobjects/.gen-state/ holds this\n` +
|
|
154
|
+
`// file's snapshot body, REFUSED (run exits 1, body kept) where it does not. Those\n` +
|
|
155
|
+
`// bodies are gitignored, so a fresh clone or CI is always the second case — see\n` +
|
|
156
|
+
`// docs/features/own-your-codegen.md for the recovery.\n` +
|
|
126
157
|
`import { test, expect } from "bun:test";\n` +
|
|
127
158
|
`\n` +
|
|
128
159
|
`/**\n` +
|
|
@@ -30,8 +30,19 @@ import { type RenderContext } from "../render-context.js";
|
|
|
30
30
|
import { entityModuleSpecifier } from "../import-path.js";
|
|
31
31
|
import { GENERATED_HEADER } from "../constants.js";
|
|
32
32
|
import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
|
|
33
|
+
import { type CrudVerb, exposeLine } from "../routes-expose.js";
|
|
33
34
|
|
|
34
|
-
export function renderRoutesFileHono(
|
|
35
|
+
export function renderRoutesFileHono(
|
|
36
|
+
entity: MetaObject,
|
|
37
|
+
ctx: RenderContext,
|
|
38
|
+
// #348 — resolved by the generator from its `expose` option. undefined = all five verbs,
|
|
39
|
+
// emitting no `expose` key at all, so output stays byte-identical without it.
|
|
40
|
+
//
|
|
41
|
+
// Applies to the CRUD mount only. The projection branch above uses
|
|
42
|
+
// mountReadOnlyCrudRoutes, a different helper that is read-only by construction and
|
|
43
|
+
// takes no `expose` — there is no writable surface there to narrow.
|
|
44
|
+
expose?: readonly CrudVerb[],
|
|
45
|
+
): string {
|
|
35
46
|
const entityName = entity.name;
|
|
36
47
|
const handlerName = `register${entityName}Routes`;
|
|
37
48
|
|
|
@@ -107,6 +118,7 @@ export function ${handlerName}(app: ${HonoSym}<any, any, any>, deps: { db: unkno
|
|
|
107
118
|
const writeThrough = isWriteThrough(entity);
|
|
108
119
|
const camelName = entityName.charAt(0).toLowerCase() + entityName.slice(1);
|
|
109
120
|
const readViewLine = writeThrough ? `\n readView: ${camelName}View,` : "";
|
|
121
|
+
const exposeLineHono = exposeLine(expose, " ");
|
|
110
122
|
|
|
111
123
|
const HonoSym = imp("t:Hono@hono");
|
|
112
124
|
const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/hono");
|
|
@@ -142,7 +154,7 @@ export function ${handlerName}(app: ${HonoSym}<any, any, any>, deps: { db: unkno
|
|
|
142
154
|
updateSchema: ${entityName}UpdateSchema,
|
|
143
155
|
filterAllowlist: ${entityName}FilterAllowlist,
|
|
144
156
|
sortAllowlist: ${entityName}SortAllowlist,
|
|
145
|
-
dialect: ${JSON.stringify(ctx.dialect)}
|
|
157
|
+
dialect: ${JSON.stringify(ctx.dialect)},${exposeLineHono}
|
|
146
158
|
});
|
|
147
159
|
}
|
|
148
160
|
`;
|
|
@@ -23,19 +23,28 @@ import {
|
|
|
23
23
|
} from "@metaobjectsdev/metadata";
|
|
24
24
|
import { type RenderContext } from "../render-context.js";
|
|
25
25
|
import { crossEntitySpecifier, entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
|
|
26
|
+
import { namesRef, columnExpr } from "../names.js";
|
|
26
27
|
import { GENERATED_HEADER } from "../constants.js";
|
|
27
28
|
import { routesHandlerName } from "../naming.js";
|
|
28
29
|
import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
|
|
29
30
|
import type { RelationEntry } from "../relation-resolver.js";
|
|
30
31
|
import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
|
|
32
|
+
import { type CrudVerb, exposeLine, intersectExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js";
|
|
31
33
|
|
|
32
|
-
export function renderRoutesFile(
|
|
34
|
+
export function renderRoutesFile(
|
|
35
|
+
entity: MetaObject,
|
|
36
|
+
ctx: RenderContext,
|
|
37
|
+
// #348 — resolved by the generator from its `expose` option. undefined = all five verbs,
|
|
38
|
+
// which emits no `expose` key at all and so keeps output byte-identical for projects
|
|
39
|
+
// that do not use it.
|
|
40
|
+
expose?: readonly CrudVerb[],
|
|
41
|
+
): string {
|
|
33
42
|
// FR-017 Tier 2 — a TPH discriminator base mounts polymorphic list/get at the
|
|
34
43
|
// base path plus a full per-subtype CRUD route set scoped to each
|
|
35
44
|
// discriminator value. (Subtype entities are filtered out of the routes
|
|
36
45
|
// generator entirely — their routes live here.)
|
|
37
46
|
if (isTphDiscriminatorBase(entity, ctx.loadedRoot)) {
|
|
38
|
-
return renderTphRoutesFile(entity, ctx);
|
|
47
|
+
return renderTphRoutesFile(entity, ctx, expose);
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
const entityName = entity.name;
|
|
@@ -134,6 +143,8 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
134
143
|
const viewImportLine = writeThrough ? `\n ${camelName}View,` : "";
|
|
135
144
|
const readViewLinePrefixed = writeThrough ? `\n readView: ${camelName}View,` : "";
|
|
136
145
|
const readViewLineFlat = writeThrough ? `\n readView: ${camelName}View,` : "";
|
|
146
|
+
const exposeLinePrefixed = exposeLine(expose, " ");
|
|
147
|
+
const exposeLineFlat = exposeLine(expose, " ");
|
|
137
148
|
|
|
138
149
|
const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
|
|
139
150
|
const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
@@ -184,7 +195,7 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
184
195
|
updateSchema: ${entityName}UpdateSchema,
|
|
185
196
|
filterAllowlist: ${entityName}FilterAllowlist,
|
|
186
197
|
sortAllowlist: ${entityName}SortAllowlist,
|
|
187
|
-
dialect: ${JSON.stringify(ctx.dialect)}
|
|
198
|
+
dialect: ${JSON.stringify(ctx.dialect)},${exposeLinePrefixed}
|
|
188
199
|
});
|
|
189
200
|
${m2mMountsPrefixed} }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
|
|
190
201
|
}
|
|
@@ -208,7 +219,7 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
208
219
|
updateSchema: ${entityName}UpdateSchema,
|
|
209
220
|
filterAllowlist: ${entityName}FilterAllowlist,
|
|
210
221
|
sortAllowlist: ${entityName}SortAllowlist,
|
|
211
|
-
dialect: ${JSON.stringify(ctx.dialect)}
|
|
222
|
+
dialect: ${JSON.stringify(ctx.dialect)},${exposeLineFlat}
|
|
212
223
|
});
|
|
213
224
|
${m2mMountsFlat}}
|
|
214
225
|
`;
|
|
@@ -270,15 +281,17 @@ function renderM2mMount(
|
|
|
270
281
|
const mountM2mRouteSym = imp("mountM2mRoute@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
271
282
|
const junction = ctx.loadedRoot.findObject(entry.junctionEntity);
|
|
272
283
|
const target = ctx.loadedRoot.findObject(entry.targetEntity);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
// fromPackage = source.package: this routes file is SOURCE's own module, never the
|
|
285
|
+
// junction's or the target's — see resolveJunctionColumn's doc comment (B1).
|
|
286
|
+
const sourceColumn: Code = junction
|
|
287
|
+
? resolveJunctionColumn(junction, entry.sourceJoinField!, ctx, source.package)
|
|
288
|
+
: code`${JSON.stringify(entry.sourceJoinField!)}`;
|
|
289
|
+
const targetColumn: Code = junction
|
|
290
|
+
? resolveJunctionColumn(junction, entry.targetJoinField!, ctx, source.package)
|
|
291
|
+
: code`${JSON.stringify(entry.targetJoinField!)}`;
|
|
292
|
+
const targetPkColumn: Code = target
|
|
293
|
+
? resolveJunctionColumn(target, ctx.pkMap.get(entry.targetEntity)?.fieldName ?? "id", ctx, source.package)
|
|
294
|
+
: code`${JSON.stringify("id")}`;
|
|
282
295
|
|
|
283
296
|
return code` ${mountM2mRouteSym}({
|
|
284
297
|
fastify: ${fastifyVar},
|
|
@@ -287,19 +300,44 @@ function renderM2mMount(
|
|
|
287
300
|
db,
|
|
288
301
|
junctionTable: ${junctionVarSym},
|
|
289
302
|
targetTable: ${targetVarSym},
|
|
290
|
-
sourceColumn: ${
|
|
291
|
-
targetColumn: ${
|
|
292
|
-
targetPkColumn: ${
|
|
303
|
+
sourceColumn: ${sourceColumn},
|
|
304
|
+
targetColumn: ${targetColumn},
|
|
305
|
+
targetPkColumn: ${targetPkColumn},
|
|
293
306
|
symmetric: ${entry.symmetric ? "true" : "false"},
|
|
294
307
|
});`;
|
|
295
308
|
}
|
|
296
309
|
|
|
297
|
-
/**
|
|
298
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Resolve a field's physical column name on an entity (defaults if missing), as a
|
|
312
|
+
* ts-poet Code fragment.
|
|
313
|
+
*
|
|
314
|
+
* §A6/§B2 — references `<Entity>Names.fields.<field>.column` whenever the names
|
|
315
|
+
* artifact is in this run AND carries the field (the same two-condition shape as every
|
|
316
|
+
* other §A6 site: is the artifact in the run, is the field in it — both PRESENCE
|
|
317
|
+
* guards). A literal otherwise.
|
|
318
|
+
*
|
|
319
|
+
* B1 — `entity` here is the JUNCTION or TARGET object, never the routes file's own
|
|
320
|
+
* SOURCE entity (see renderM2mMount) — so `fromPackage` MUST be the source entity's
|
|
321
|
+
* package, not `entity`'s own. A same-package caller cannot tell the two apart (they're
|
|
322
|
+
* equal), which is exactly how this went unnoticed until a package-layout M:N golden
|
|
323
|
+
* existed: a plain sibling specifier (assuming fromPackage === entity.package, the
|
|
324
|
+
* shape `namesRef`'s default deliberately does NOT special-case) silently produced a
|
|
325
|
+
* same-directory import for a names artifact that lives in a DIFFERENT directory,
|
|
326
|
+
* because the object whose file we're building and the object whose names we're
|
|
327
|
+
* importing are two different objects.
|
|
328
|
+
*/
|
|
329
|
+
function resolveJunctionColumn(
|
|
330
|
+
entity: MetaObject,
|
|
331
|
+
fieldName: string,
|
|
332
|
+
ctx: RenderContext,
|
|
333
|
+
fromPackage: string | undefined,
|
|
334
|
+
): Code {
|
|
299
335
|
// ADR-0039: resolving — a junction FK field may be inherited via extends.
|
|
300
336
|
const field = entity.children().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
|
|
301
|
-
if (!field) return fieldName
|
|
302
|
-
|
|
337
|
+
if (!field) return code`${JSON.stringify(fieldName)}`;
|
|
338
|
+
const dbCol = resolveColumnName(field, ctx.columnNamingStrategy);
|
|
339
|
+
const names = namesRef(entity, ctx, fromPackage);
|
|
340
|
+
return columnExpr(names, field.name, dbCol);
|
|
303
341
|
}
|
|
304
342
|
|
|
305
343
|
/**
|
|
@@ -318,7 +356,11 @@ function resolveJunctionColumn(entity: MetaObject, fieldName: string, ctx: Rende
|
|
|
318
356
|
* FR-017 design's `/auths/bridge` examples. Fastify resolves the static
|
|
319
357
|
* `/auths/bridge` ahead of the parametric `/auths/:id`, so the two coexist.
|
|
320
358
|
*/
|
|
321
|
-
function renderTphRoutesFile(
|
|
359
|
+
function renderTphRoutesFile(
|
|
360
|
+
base: MetaObject,
|
|
361
|
+
ctx: RenderContext,
|
|
362
|
+
expose?: readonly CrudVerb[],
|
|
363
|
+
): string {
|
|
322
364
|
const baseName = base.name;
|
|
323
365
|
const handlerName = routesHandlerName(baseName);
|
|
324
366
|
// Single source of truth for the discriminator field + subtypes + route segments.
|
|
@@ -344,6 +386,10 @@ function renderTphRoutesFile(base: MetaObject, ctx: RenderContext): string {
|
|
|
344
386
|
const fastifyRef = ctx.apiPrefix ? "instance" : "fastify";
|
|
345
387
|
const dialectLit = JSON.stringify(ctx.dialect);
|
|
346
388
|
|
|
389
|
+
// The polymorphic mount is read-only BY CONSTRUCTION — the discriminated union has no
|
|
390
|
+
// single writable shape — so an author-supplied `expose` intersects with that fixed set
|
|
391
|
+
// rather than replacing it: it may narrow to just `list`, never widen to `create`.
|
|
392
|
+
const polymorphicExposeLine = exposeLine(intersectExpose([...TPH_POLYMORPHIC_VERBS], expose), " ");
|
|
347
393
|
const polymorphic = code`
|
|
348
394
|
${mountCrudRoutesSym}({
|
|
349
395
|
fastify: ${fastifyRef},
|
|
@@ -354,8 +400,7 @@ function renderTphRoutesFile(base: MetaObject, ctx: RenderContext): string {
|
|
|
354
400
|
updateSchema: ${baseUpdateSym},
|
|
355
401
|
filterAllowlist: ${baseFilterSym},
|
|
356
402
|
sortAllowlist: ${baseSortSym},
|
|
357
|
-
dialect: ${dialectLit}
|
|
358
|
-
expose: ["list", "get"],
|
|
403
|
+
dialect: ${dialectLit},${polymorphicExposeLine}
|
|
359
404
|
});`;
|
|
360
405
|
|
|
361
406
|
const subtypeMounts: Code[] = plan.subtypes.map(({ entity: sub, value, routeSegment: segment }) => {
|
|
@@ -386,7 +431,7 @@ function renderTphRoutesFile(base: MetaObject, ctx: RenderContext): string {
|
|
|
386
431
|
filterAllowlist: ${subFilterSym},
|
|
387
432
|
sortAllowlist: ${subSortSym},
|
|
388
433
|
dialect: ${dialectLit},
|
|
389
|
-
discriminator: { column: ${JSON.stringify(discField)}, value: ${JSON.stringify(value)} },
|
|
434
|
+
discriminator: { column: ${JSON.stringify(discField)}, value: ${JSON.stringify(value)} },${exposeLine(expose, " ")}
|
|
390
435
|
});`;
|
|
391
436
|
});
|
|
392
437
|
|