@metaobjectsdev/codegen-ts 0.24.5 → 1.0.0-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/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/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 +14 -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 -8
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +19 -14
- 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 +9 -11
- 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 +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -4
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +38 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +9 -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 +433 -0
- package/dist/names.js.map +1 -0
- 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/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/reference-templates.d.ts +1 -1
- package/dist/reference-templates.d.ts.map +1 -1
- package/dist/reference-templates.js +1 -1
- package/dist/reference-templates.js.map +1 -1
- package/dist/render-context.d.ts +12 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +1 -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 +109 -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/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/requirement-test.d.ts.map +1 -1
- package/dist/templates/requirement-test.js +13 -2
- 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/column-mapper.ts +13 -0
- package/src/constants.ts +91 -16
- package/src/db-emitting.ts +31 -0
- package/src/generator-registry.ts +14 -4
- 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 +25 -14
- package/src/generators/names-file.ts +98 -0
- package/src/generators/requirements-markdown.ts +36 -2
- package/src/generators/routes-file-hono.ts +19 -10
- package/src/generators/routes-file.ts +20 -7
- package/src/generators/template-generator.ts +20 -6
- package/src/index.ts +64 -4
- package/src/metaobjects-config.ts +48 -2
- package/src/names.ts +629 -0
- package/src/overwrite-policy.ts +15 -3
- package/src/projection/extract-view-spec.ts +85 -6
- package/src/reference/entity.ts +21 -2
- package/src/reference/names.ts +125 -0
- package/src/reference/queries.ts +19 -3
- package/src/reference/routes-hono.ts +19 -9
- package/src/reference/routes.ts +25 -9
- package/src/reference-templates.ts +1 -1
- package/src/render-context.ts +13 -1
- package/src/retired-codegen-attrs.ts +52 -0
- package/src/routes-expose.ts +86 -0
- package/src/runner.ts +122 -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/projection-decl.ts +72 -31
- package/src/templates/queries-file.ts +17 -2
- package/src/templates/requirement-test.ts +13 -2
- 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
|
@@ -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
|
|
|
@@ -139,10 +139,21 @@ export function renderRequirementTest(a: RequirementTestArgs): string {
|
|
|
139
139
|
body = ` // Intended, not built. Write the assertion when this becomes live.`;
|
|
140
140
|
}
|
|
141
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.
|
|
142
149
|
return (
|
|
143
150
|
`// ${GENERATED_HEADER}.\n` +
|
|
144
|
-
`// The test IDENTITY is generated from the requirement; the BODY below is yours
|
|
145
|
-
`//
|
|
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` +
|
|
146
157
|
`import { test, expect } from "bun:test";\n` +
|
|
147
158
|
`\n` +
|
|
148
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
|
|
|
@@ -23,6 +23,7 @@ import { renderEntityConstants } from "./entity-constants.js";
|
|
|
23
23
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
24
24
|
import { renderFilterType } from "./filter-type.js";
|
|
25
25
|
import { GENERATED_HEADER } from "../constants.js";
|
|
26
|
+
import { namesRef, namesConstArg } from "../names.js";
|
|
26
27
|
|
|
27
28
|
export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: RenderContext): string {
|
|
28
29
|
const enumAliases = renderEnumTypeAliases(obj, ctx);
|
|
@@ -34,7 +35,20 @@ export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: Ren
|
|
|
34
35
|
// FR-017 Tier 3: a TPH subtype also emits its field-metadata constants object
|
|
35
36
|
// (the `<Sub>` const), so the React form generator can render per-field
|
|
36
37
|
// labels / rules / inputs the same way it does for ordinary entities.
|
|
37
|
-
|
|
38
|
+
//
|
|
39
|
+
// §A6/§B2 — a TPH subtype declares no source.rdb of its own; it INHERITS the
|
|
40
|
+
// discriminator base's single shared table (`children()` is resolving — ADR-0039).
|
|
41
|
+
// `resolveObjectNames` therefore returns a DEFINED result for it (confirmed
|
|
42
|
+
// empirically: `name` is the BASE's physical table name, `fields` covers both the
|
|
43
|
+
// base's inherited columns and the subtype's own), and `namesFile()` really does emit
|
|
44
|
+
// a `<Sub>.names.ts` for it — this is not a case where the literal is correct and must
|
|
45
|
+
// stay. Same `namesRef` pair every other §A6 site builds; `ctx` is guaranteed present
|
|
46
|
+
// at every real call site (entity-file.ts and its reference/entity.ts mirror both
|
|
47
|
+
// always pass it) even though the parameter stays optional for a bare unit-test call.
|
|
48
|
+
const tphNames = tphSubtype && ctx !== undefined ? namesRef(obj, ctx) : undefined;
|
|
49
|
+
const tphConstants = tphSubtype
|
|
50
|
+
? renderEntityConstants(obj, apiPrefix, namesConstArg(tphNames))
|
|
51
|
+
: null;
|
|
38
52
|
// FR-017 Tier 3: per-subtype filter + sort allowlists, excluding the
|
|
39
53
|
// discriminator (it's pinned by the per-subtype route path, so a client must
|
|
40
54
|
// not filter on it). Included fields are the subtype's own + inherited base
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
15
15
|
import { mapColumnType } from "../column-mapper.js";
|
|
16
16
|
import { zodTypeFor } from "./field-meta.js";
|
|
17
|
+
import { columnExpr, type ObjectNames } from "../names.js";
|
|
17
18
|
|
|
18
19
|
export interface ViewDeclOpts {
|
|
19
20
|
readonly dialect: "postgres" | "sqlite";
|
|
@@ -38,6 +39,31 @@ export interface ViewDeclOpts {
|
|
|
38
39
|
* UpdateSchema, all of which already treat the PK as non-null.
|
|
39
40
|
*/
|
|
40
41
|
readonly pkFieldNames: ReadonlySet<string>;
|
|
42
|
+
/**
|
|
43
|
+
* §A6 — the resolved names artifact supplying this declaration's COLUMN constants,
|
|
44
|
+
* plus the ts-poet symbol reference (`imp()`'d import of `<Object>Names`), when the
|
|
45
|
+
* names generator is in the run. Absent ⇒ every column stays a literal.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately says nothing about the view's OWN physical name: the two answers come
|
|
48
|
+
* apart on a write-through entity, whose artifact holds the TABLE's name (its primary
|
|
49
|
+
* source) while this declaration binds the REPLICA view. Conflating them cost the
|
|
50
|
+
* write-through read model its column constants — it had to omit `names` entirely to
|
|
51
|
+
* stop the artifact's view name being emitted, and lost every
|
|
52
|
+
* `.fields.<f>.column` reference with it. The view's name is now the caller's to pass
|
|
53
|
+
* (see the `viewName` parameter), which is the only place that knows.
|
|
54
|
+
*/
|
|
55
|
+
readonly names?: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The view's `@schema` — a bare name, or an EXPRESSION for it (a names constant).
|
|
58
|
+
*
|
|
59
|
+
* Passed in rather than derived from `names`, and for the same reason `viewName` is: a
|
|
60
|
+
* write-through entity's REPLICA view is a different source node from the one
|
|
61
|
+
* `<Entity>Names` resolves, so the artifact's `schema` there is the WRITE TABLE's and
|
|
62
|
+
* would be a confidently wrong answer. A caller whose artifact genuinely names this view
|
|
63
|
+
* passes the constant expression; the replica-view caller passes its own literal, which
|
|
64
|
+
* is consistent with that view's NAME already being a known literal for the same reason.
|
|
65
|
+
*/
|
|
66
|
+
readonly schema?: string | Code | undefined;
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
/**
|
|
@@ -85,7 +111,10 @@ function viewColumnLine(f: MetaField, opts: ViewDeclOpts): Code {
|
|
|
85
111
|
dollarType = code`.$type<Record<string, ${imp(`${vo.name}@${vo.module}`)}>>()`;
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
|
-
|
|
114
|
+
// A6 — reference the constant whenever the artifact is in the run AND carries this
|
|
115
|
+
// field. A lookup MISS is normal, not a divergence — see ViewDeclOpts.names.
|
|
116
|
+
const dbNameExpr = columnExpr(opts.names, f.name, spec.dbName);
|
|
117
|
+
return code` ${f.name}: ${colSym}(${dbNameExpr}${optsArg})${dollarType}${viewModifiers}`;
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
/**
|
|
@@ -95,7 +124,13 @@ function viewColumnLine(f: MetaField, opts: ViewDeclOpts): Code {
|
|
|
95
124
|
*/
|
|
96
125
|
export function renderExistingViewDecl(
|
|
97
126
|
fields: readonly MetaField[],
|
|
98
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The view's own physical name: a plain string is emitted as a literal, a `Code` is
|
|
129
|
+
* emitted verbatim (a caller whose names artifact genuinely names THIS view passes
|
|
130
|
+
* `physicalNameExpr(names, viewName)`). See `ViewDeclOpts.names` for why this is not
|
|
131
|
+
* derived from that artifact here.
|
|
132
|
+
*/
|
|
133
|
+
viewName: string | Code,
|
|
99
134
|
viewVar: string,
|
|
100
135
|
opts: ViewDeclOpts,
|
|
101
136
|
): Code {
|
|
@@ -103,11 +138,26 @@ export function renderExistingViewDecl(
|
|
|
103
138
|
const viewModule = opts.dialect === "postgres" ? "drizzle-orm/pg-core" : "drizzle-orm/sqlite-core";
|
|
104
139
|
const viewSym = imp(`${viewFn}@${viewModule}`);
|
|
105
140
|
const viewColumnLines = fields.map((f) => viewColumnLine(f, opts));
|
|
141
|
+
const viewNameExpr = typeof viewName === "string" ? code`${JSON.stringify(viewName)}` : viewName;
|
|
142
|
+
|
|
143
|
+
// @schema — a view lands in a schema exactly as a table does, and migrate qualifies the
|
|
144
|
+
// `CREATE VIEW` it emits. Fixing the table binding and leaving this one would be the more
|
|
145
|
+
// dangerous half-job: the two would then disagree with each OTHER as well as with the
|
|
146
|
+
// database. Drizzle spells it with the same shape as the table case, `pgSchema(s).view(...)`
|
|
147
|
+
// — PgSchema carries `view` alongside `table`. sqlite is excluded for the same reason as
|
|
148
|
+
// there: no schema concept, and migrate refuses a non-default @schema on that dialect.
|
|
149
|
+
const viewSchemaExpr: Code | undefined =
|
|
150
|
+
opts.dialect !== "postgres" || opts.schema === undefined ? undefined
|
|
151
|
+
: typeof opts.schema === "string" ? code`${JSON.stringify(opts.schema)}`
|
|
152
|
+
: opts.schema;
|
|
153
|
+
const viewCall: Code = viewSchemaExpr === undefined
|
|
154
|
+
? code`${viewSym}`
|
|
155
|
+
: code`${imp(`pgSchema@${viewModule}`)}(${viewSchemaExpr}).view`;
|
|
106
156
|
return code`
|
|
107
157
|
// View declaration — Drizzle uses this for typed SELECT queries.
|
|
108
158
|
// The SQL view is created/managed by migrate-ts; .existing() tells Drizzle
|
|
109
159
|
// not to attempt DDL for this declaration.
|
|
110
|
-
export const ${viewVar} = ${
|
|
160
|
+
export const ${viewVar} = ${viewCall}(${viewNameExpr}, {
|
|
111
161
|
${joinCode(viewColumnLines, { on: ",\n" })}
|
|
112
162
|
}).existing();
|
|
113
163
|
`;
|
|
@@ -51,19 +51,55 @@ import { isRequired } from "../column-mapper.js";
|
|
|
51
51
|
* when the object is not a TPH subtype.
|
|
52
52
|
*/
|
|
53
53
|
export function tphDiscriminatorPin(obj: MetaObject): { fieldName: string; value: string } | undefined {
|
|
54
|
-
// ADR-0039: own —
|
|
55
|
-
//
|
|
56
|
-
// walking superResolved reading each level's OWN @discriminator.
|
|
54
|
+
// ADR-0039: own — a subtype declares its OWN @discriminatorValue (must not inherit
|
|
55
|
+
// one); the base level is found by the super-resolution walk in `tphDiscriminatorLevel`.
|
|
57
56
|
const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
|
|
58
57
|
if (typeof value !== "string" || value === "") return undefined;
|
|
58
|
+
const level = tphDiscriminatorLevel(obj);
|
|
59
|
+
if (level === undefined) return undefined;
|
|
60
|
+
return { fieldName: level.fieldName, value };
|
|
61
|
+
}
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The discriminator BASE an object extends — the nearest ancestor declaring
|
|
65
|
+
* `@discriminator` — or undefined when there is none.
|
|
66
|
+
*
|
|
67
|
+
* The base is NOT the topmost ancestor: a multi-level hierarchy can extend further up
|
|
68
|
+
* past the level that owns the discriminator, and a route path composed against the
|
|
69
|
+
* wrong level names an endpoint nothing mounts. One walk, read by `tphDiscriminatorPin`
|
|
70
|
+
* and by `restPath` (api-surface.ts), so the pin and the address agree on which object
|
|
71
|
+
* is the base.
|
|
72
|
+
*/
|
|
73
|
+
export function tphDiscriminatorBase(obj: MetaObject): MetaObject | undefined {
|
|
74
|
+
return tphDiscriminatorLevel(obj)?.base;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* True when this object itself DECLARES `@discriminator` — the level of a TPH hierarchy
|
|
79
|
+
* that owns the discriminator, whether or not any concrete subtype extends it yet.
|
|
80
|
+
*
|
|
81
|
+
* ADR-0039: own — `@discriminator` identifies a hierarchy LEVEL, so it is read own: a
|
|
82
|
+
* subtype inheriting the attr through `extends` must not be mistaken for a base. This is
|
|
83
|
+
* the one predicate the form generator's filter (`hasGeneratedForm`) and the `agent/ui.md`
|
|
84
|
+
* page both ask, so "is this a discriminator base?" has one spelling.
|
|
85
|
+
*/
|
|
86
|
+
export function declaresTphDiscriminator(obj: MetaObject): boolean {
|
|
87
|
+
return typeof obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR) === "string";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The ONE super-resolution walk behind `tphDiscriminatorBase` and `tphDiscriminatorPin`:
|
|
92
|
+
* the nearest ancestor declaring a non-empty `@discriminator`, with the field name it
|
|
93
|
+
* declares. Returning both from one walk is what keeps the pin's field name and the
|
|
94
|
+
* base's identity from ever being read at two different levels.
|
|
95
|
+
*/
|
|
96
|
+
function tphDiscriminatorLevel(obj: MetaObject): { base: MetaObject; fieldName: string } | undefined {
|
|
61
97
|
let cursor = obj.superResolved;
|
|
62
98
|
while (cursor !== undefined) {
|
|
63
99
|
// ADR-0039: own — super-resolution walk; read each level's OWN @discriminator.
|
|
64
100
|
const fieldName = cursor.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
65
101
|
if (typeof fieldName === "string" && fieldName !== "") {
|
|
66
|
-
return {
|
|
102
|
+
return { base: cursor as MetaObject, fieldName };
|
|
67
103
|
}
|
|
68
104
|
cursor = cursor.superResolved;
|
|
69
105
|
}
|