@metaobjectsdev/codegen-ts 1.0.4 → 1.0.5-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +14 -0
- package/dist/column-mapper.js.map +1 -1
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +6 -6
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +7 -2
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +2 -1
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +4 -3
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +2 -1
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +4 -3
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +49 -3
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts +9 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +37 -49
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +9 -1
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/enum-int-codec.d.ts +43 -0
- package/dist/templates/enum-int-codec.d.ts.map +1 -0
- package/dist/templates/enum-int-codec.js +66 -0
- package/dist/templates/enum-int-codec.js.map +1 -0
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +7 -1
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/relations-block.d.ts.map +1 -1
- package/dist/templates/relations-block.js +13 -6
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +21 -4
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +8 -0
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +29 -4
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +32 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +49 -3
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/column-mapper.ts +13 -0
- package/src/generators/api-model.ts +6 -6
- package/src/generators/queries-file.ts +7 -2
- package/src/generators/routes-file-hono.ts +4 -3
- package/src/generators/routes-file.ts +4 -3
- package/src/index.ts +1 -1
- package/src/projection/extract-view-spec.ts +55 -6
- package/src/reference/queries.ts +4 -2
- package/src/reference/routes-hono.ts +2 -2
- package/src/reference/routes.ts +4 -3
- package/src/templates/drizzle-schema.ts +37 -50
- package/src/templates/entity-file.ts +9 -1
- package/src/templates/enum-int-codec.ts +67 -0
- package/src/templates/inferred-types.ts +7 -1
- package/src/templates/relations-block.ts +19 -11
- package/src/templates/routes-file.ts +29 -12
- package/src/templates/view-decl.ts +43 -5
- package/src/templates/zod-validators.ts +50 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The int-backed `field.enum` codec emitter, shared by the two artifacts that can
|
|
3
|
+
* reference one: the entity's Drizzle table (`drizzle-schema.ts`) and any view-backed
|
|
4
|
+
* read model (`view-decl.ts` — a projection or an entity read-view).
|
|
5
|
+
*
|
|
6
|
+
* It lives in its own module because the codec is a MODULE-LOCAL const in whatever file
|
|
7
|
+
* declares the column, so every emitter that can produce such a column must be able to
|
|
8
|
+
* declare it too. While only the table template owned it, the view template emitted the
|
|
9
|
+
* call site and let the const name fall through to `imp(fnName@drizzle-orm/*-core)` —
|
|
10
|
+
* importing a member that package does not export, which `meta gen` reported as success
|
|
11
|
+
* and only `tsc` caught.
|
|
12
|
+
*
|
|
13
|
+
* The invariant this module exists to protect: the codec is declared ONCE PER MODULE, and
|
|
14
|
+
* it must precede every use. A write-through entity puts its table and its replica view in
|
|
15
|
+
* one file and both reference the same consts, so `entity-file.ts` renders the table first,
|
|
16
|
+
* collects the names it declared, and hands them to the view — which then references them
|
|
17
|
+
* instead of re-declaring. Two module-scope `const`s of one name is a JS parse error, and a
|
|
18
|
+
* declaration emitted after its use is a TDZ ReferenceError at import: both are worse than
|
|
19
|
+
* the missing-codec bug, because neither lets the module load at all.
|
|
20
|
+
*/
|
|
21
|
+
import { code, imp, type Code } from "ts-poet";
|
|
22
|
+
import type { EnumIntCustomType } from "../column-mapper.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render an int-backed `field.enum`'s Drizzle `customType` helper plus its two
|
|
26
|
+
* lookup maps.
|
|
27
|
+
*
|
|
28
|
+
* The codec lives HERE, in the column definition, so nothing downstream needs to
|
|
29
|
+
* know about it: `db.insert().values()` encodes on bind, a selected row decodes on
|
|
30
|
+
* read, and a filter comparison encodes because Drizzle binds through the column
|
|
31
|
+
* type. That is why this shape was chosen over a Zod write-transform plus a
|
|
32
|
+
* generated read-decode — TS's generated queries return raw Drizzle rows and have
|
|
33
|
+
* no decode seam, so the query-layer approach meant inventing one and wrapping
|
|
34
|
+
* every generated read. It is also the direct analogue of what the other four
|
|
35
|
+
* ports already do (EF Core `HasConversion`, OMDB `JdbcFieldCodec`, Exposed
|
|
36
|
+
* `customEnumeration`, Python `ObjectManager` coercion).
|
|
37
|
+
*
|
|
38
|
+
* `fromDriver` throws on an unmapped integer rather than returning undefined: a
|
|
39
|
+
* value outside the map means the DB holds data the model says is impossible
|
|
40
|
+
* (a hand-written INSERT, or a member removed without a migration), and silently
|
|
41
|
+
* yielding `undefined` for a non-nullable field would surface far from the cause.
|
|
42
|
+
*/
|
|
43
|
+
export function renderEnumIntCustomType(t: EnumIntCustomType, importModule: string): Code {
|
|
44
|
+
const customTypeSym = imp(`customType@${importModule}`);
|
|
45
|
+
const union = t.members.map((m) => JSON.stringify(m)).join(" | ");
|
|
46
|
+
const toEntries = t.members
|
|
47
|
+
.map((m) => `${JSON.stringify(m)}: ${t.intByMember[m]}`)
|
|
48
|
+
.join(", ");
|
|
49
|
+
const fromEntries = t.members
|
|
50
|
+
.map((m) => `${t.intByMember[m]}: ${JSON.stringify(m)}`)
|
|
51
|
+
.join(", ");
|
|
52
|
+
return code`
|
|
53
|
+
const ${t.toIntConstName} = { ${toEntries} } as const satisfies Record<${union}, number>;
|
|
54
|
+
const ${t.fromIntConstName}: Record<number, ${union}> = { ${fromEntries} };
|
|
55
|
+
const ${t.fnConstName} = ${customTypeSym}<{ data: ${union}; driverData: number }>({
|
|
56
|
+
dataType: () => ${JSON.stringify(t.dataType)},
|
|
57
|
+
toDriver: (value) => ${t.toIntConstName}[value],
|
|
58
|
+
fromDriver: (value) => {
|
|
59
|
+
const member = ${t.fromIntConstName}[value];
|
|
60
|
+
if (member === undefined) {
|
|
61
|
+
throw new Error(\`unmapped ${t.fnConstName} value: \${value}\`);
|
|
62
|
+
}
|
|
63
|
+
return member;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
@@ -39,6 +39,7 @@ import { stripPackage } from "@metaobjectsdev/metadata";
|
|
|
39
39
|
import { enumValues } from "../enum-meta.js";
|
|
40
40
|
import { sharedEnumZodConstName } from "./enums-file.js";
|
|
41
41
|
import { renderDocsFor } from "./jsdoc.js";
|
|
42
|
+
import { isTphReadNullTolerant } from "./zod-validators.js";
|
|
42
43
|
import { sharedEnumForField } from "../enum-shared.js";
|
|
43
44
|
import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
|
|
44
45
|
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
@@ -387,7 +388,12 @@ export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderConte
|
|
|
387
388
|
const required = field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
388
389
|
const optional = required ? "" : "?";
|
|
389
390
|
const tsType = valueObjectFieldType(entity, field, ctx);
|
|
390
|
-
|
|
391
|
+
// A TPH subtype's read schema accepts `null` for a column that is NULL on a
|
|
392
|
+
// sibling subtype's row; the declared type has to admit the same values, or
|
|
393
|
+
// `parse<Base>()` returns something this interface rejects. One predicate
|
|
394
|
+
// answers it for both emitters (ADR-style single source, see its docblock).
|
|
395
|
+
const nullable = isTphReadNullTolerant(entity, field) ? " | null" : "";
|
|
396
|
+
lines.push(code` ${field.name}${optional}: ${tsType}${nullable};`);
|
|
391
397
|
}
|
|
392
398
|
|
|
393
399
|
// joinCode with "\n" interpolates each Code segment on its own line and
|
|
@@ -8,6 +8,7 @@ import { type RenderContext } from "../render-context.js";
|
|
|
8
8
|
import { crossEntitySpecifier } from "../import-path.js";
|
|
9
9
|
import type { RelationEntry } from "../relation-resolver.js";
|
|
10
10
|
import { effectivePackage } from "../docs-paths.js";
|
|
11
|
+
import { tphStorageName } from "./zod-validators.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Render the relations() block for one entity.
|
|
@@ -31,7 +32,7 @@ export function renderRelationsBlock(entity: MetaObject, ctx: RenderContext): Co
|
|
|
31
32
|
|
|
32
33
|
const thisEntityPackage = effectivePackage(entity);
|
|
33
34
|
const lines: Code[] = entries.map((entry) =>
|
|
34
|
-
renderRelationEntry(entry, ctx, varName, thisEntityPackage),
|
|
35
|
+
renderRelationEntry(entry, ctx, entity.name, varName, thisEntityPackage),
|
|
35
36
|
);
|
|
36
37
|
|
|
37
38
|
return code`export const ${relationsVarName} = ${relationsFn}(${varName}, (${params}) => ({
|
|
@@ -43,6 +44,7 @@ ${joinCode(lines, { on: ",\n", trim: false })}
|
|
|
43
44
|
function renderRelationEntry(
|
|
44
45
|
entry: RelationEntry,
|
|
45
46
|
ctx: RenderContext,
|
|
47
|
+
thisEntityName: string,
|
|
46
48
|
thisVarName: string,
|
|
47
49
|
thisEntityPackage: string | undefined,
|
|
48
50
|
): Code {
|
|
@@ -63,18 +65,24 @@ function renderRelationEntry(
|
|
|
63
65
|
return code` ${entry.name}: many(${junctionVarSym})`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
// Bind to the table that stores the target's rows: a TPH subtype's module has no
|
|
69
|
+
// table const, so a navigation onto `Carrier` binds `parties` from `Party`.
|
|
70
|
+
const targetTableEntity = tphStorageName(entry.targetEntity, ctx.loadedRoot);
|
|
71
|
+
// A navigation onto this entity's OWN table (`Order.parent`, or a TPH base onto one of
|
|
72
|
+
// its subtypes) names the local const: importing it from this very module clashes with
|
|
73
|
+
// its declaration (TS2440). Otherwise imp() tracks and emits the cross-entity import.
|
|
74
|
+
const targetVarSym = targetTableEntity === thisEntityName
|
|
75
|
+
? thisVarName
|
|
76
|
+
: imp(`${ctx.collectionName(targetTableEntity)}@${crossEntitySpecifier(
|
|
77
|
+
ctx.outputLayout,
|
|
78
|
+
thisEntityPackage,
|
|
79
|
+
ctx.packageOf.get(targetTableEntity),
|
|
80
|
+
targetTableEntity,
|
|
81
|
+
ctx.extStyle,
|
|
82
|
+
)}`);
|
|
75
83
|
|
|
76
84
|
if (entry.cardinality === CARDINALITY_ONE) {
|
|
77
|
-
const pkInfo = ctx.pkMap.get(
|
|
85
|
+
const pkInfo = ctx.pkMap.get(targetTableEntity);
|
|
78
86
|
const targetPkField = pkInfo?.fieldName ?? "id";
|
|
79
87
|
return code` ${entry.name}: one(${targetVarSym}, { fields: [${thisVarName}.${entry.fkField ?? "id"}], references: [${targetVarSym}.${targetPkField}] })`;
|
|
80
88
|
}
|
|
@@ -31,6 +31,7 @@ import type { RelationEntry } from "../relation-resolver.js";
|
|
|
31
31
|
import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
|
|
32
32
|
import { authSeamJsDoc, type CrudVerb, exposeLine, intersectExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js";
|
|
33
33
|
import { effectivePackage } from "../docs-paths.js";
|
|
34
|
+
import { tphDiscriminatorPin, tphStorageObject } from "./zod-validators.js";
|
|
34
35
|
|
|
35
36
|
export function renderRoutesFile(
|
|
36
37
|
entity: MetaObject,
|
|
@@ -288,18 +289,30 @@ function renderM2mMount(
|
|
|
288
289
|
ctx.extStyle,
|
|
289
290
|
)}`,
|
|
290
291
|
);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
292
|
+
// An M:N onto a TPH subtype traverses into its discriminator BASE's table — the
|
|
293
|
+
// subtype has no table const, and the junction FK can only point at the base table —
|
|
294
|
+
// and filters the rows to the subtype, because a Broker id in that FK column is not a
|
|
295
|
+
// Carrier. Every other target binds to itself, and emits no filter.
|
|
296
|
+
const declaredTarget = ctx.loadedRoot.findObject(entry.targetEntity);
|
|
297
|
+
const target = declaredTarget === undefined ? undefined : tphStorageObject(declaredTarget);
|
|
298
|
+
const targetTableEntity = target?.name ?? entry.targetEntity;
|
|
299
|
+
const pin = declaredTarget === undefined ? undefined : tphDiscriminatorPin(declaredTarget);
|
|
300
|
+
// A self-join's target table IS the source table, which this file already imports from
|
|
301
|
+
// the entity module; a second import of the same binding is TS2300, and a SyntaxError
|
|
302
|
+
// when Node loads the module.
|
|
303
|
+
const targetVarSym = targetTableEntity === source.name
|
|
304
|
+
? ctx.collectionName(source.name)
|
|
305
|
+
: imp(
|
|
306
|
+
`${ctx.collectionName(targetTableEntity)}@${crossEntitySpecifier(
|
|
307
|
+
ctx.outputLayout,
|
|
308
|
+
sourcePkg,
|
|
309
|
+
ctx.packageOf.get(targetTableEntity),
|
|
310
|
+
targetTableEntity,
|
|
311
|
+
ctx.extStyle,
|
|
312
|
+
)}`,
|
|
313
|
+
);
|
|
300
314
|
const mountM2mRouteSym = imp("mountM2mRoute@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
301
315
|
const junction = ctx.loadedRoot.findObject(entry.junctionEntity);
|
|
302
|
-
const target = ctx.loadedRoot.findObject(entry.targetEntity);
|
|
303
316
|
// fromPackage = source.package: this routes file is SOURCE's own module, never the
|
|
304
317
|
// junction's or the target's — see resolveJunctionColumn's doc comment (B1).
|
|
305
318
|
const sourceColumn: Code = junction
|
|
@@ -309,8 +322,12 @@ function renderM2mMount(
|
|
|
309
322
|
? resolveJunctionColumn(junction, entry.targetJoinField!, ctx, sourcePkg)
|
|
310
323
|
: code`${JSON.stringify(entry.targetJoinField!)}`;
|
|
311
324
|
const targetPkColumn: Code = target
|
|
312
|
-
? resolveJunctionColumn(target, ctx.pkMap.get(
|
|
325
|
+
? resolveJunctionColumn(target, ctx.pkMap.get(targetTableEntity)?.fieldName ?? "id", ctx, sourcePkg)
|
|
313
326
|
: code`${JSON.stringify("id")}`;
|
|
327
|
+
const discriminatorLine: Code | string = pin !== undefined && target !== undefined
|
|
328
|
+
? code`
|
|
329
|
+
targetDiscriminator: { column: ${resolveJunctionColumn(target, pin.fieldName, ctx, sourcePkg)}, value: ${JSON.stringify(pin.value)} },`
|
|
330
|
+
: "";
|
|
314
331
|
|
|
315
332
|
return code` ${mountM2mRouteSym}({
|
|
316
333
|
fastify: ${fastifyVar},
|
|
@@ -322,7 +339,7 @@ function renderM2mMount(
|
|
|
322
339
|
sourceColumn: ${sourceColumn},
|
|
323
340
|
targetColumn: ${targetColumn},
|
|
324
341
|
targetPkColumn: ${targetPkColumn},
|
|
325
|
-
symmetric: ${entry.symmetric ? "true" : "false"}
|
|
342
|
+
symmetric: ${entry.symmetric ? "true" : "false"},${discriminatorLine}
|
|
326
343
|
});`;
|
|
327
344
|
}
|
|
328
345
|
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
type MetaField, FIELD_SUBTYPE_OBJECT, FIELD_ATTR_OBJECT_REF,
|
|
13
13
|
} from "@metaobjectsdev/metadata";
|
|
14
14
|
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
15
|
-
import { mapColumnType } from "../column-mapper.js";
|
|
15
|
+
import { mapColumnType, type EnumIntCustomType } from "../column-mapper.js";
|
|
16
|
+
import { renderEnumIntCustomType } from "./enum-int-codec.js";
|
|
16
17
|
import { zodTypeFor } from "./field-meta.js";
|
|
17
18
|
import { columnExpr, type ObjectNames } from "../names.js";
|
|
18
19
|
|
|
@@ -21,6 +22,14 @@ export interface ViewDeclOpts {
|
|
|
21
22
|
readonly columnNamingStrategy: ColumnNamingStrategy;
|
|
22
23
|
/** Drives the timestamp column TS type (Date vs string) in the view declaration. */
|
|
23
24
|
readonly timestampMode: "date" | "string";
|
|
25
|
+
/**
|
|
26
|
+
* Int-backed-enum codec const names ALREADY declared in the module this view lands in.
|
|
27
|
+
* A write-through entity's table and its replica view share one file, and both reference
|
|
28
|
+
* the same codecs — so the view references these by name and declares only what the table
|
|
29
|
+
* did not (a derived field's codec, which the write table omits). Absent = the view owns
|
|
30
|
+
* every codec it needs, which is the projection case.
|
|
31
|
+
*/
|
|
32
|
+
readonly declaredEnumIntCodecs?: ReadonlySet<string>;
|
|
24
33
|
/**
|
|
25
34
|
* ADR-0044/#228 — resolve a `field.object` / `field.map`'s `@objectRef` to the
|
|
26
35
|
* value object's EMITTED name (bare when unique in the run, package-qualified on
|
|
@@ -72,10 +81,26 @@ export interface ViewDeclOpts {
|
|
|
72
81
|
* so `db.select().from(<view>)` is typed. Honors `@dbColumnType`; `.existing()`
|
|
73
82
|
* views carry type + physical name only (no PK/default/notNull DDL modifiers).
|
|
74
83
|
*/
|
|
75
|
-
function viewColumnLine(
|
|
84
|
+
function viewColumnLine(
|
|
85
|
+
f: MetaField,
|
|
86
|
+
opts: ViewDeclOpts,
|
|
87
|
+
/** Codecs this view needs, collected for the caller to DECLARE — see below. */
|
|
88
|
+
enumIntTypes: Map<string, EnumIntCustomType>,
|
|
89
|
+
): Code {
|
|
76
90
|
const { dialect, columnNamingStrategy, timestampMode } = opts;
|
|
77
91
|
const spec = mapColumnType(f, dialect, columnNamingStrategy, timestampMode);
|
|
78
|
-
|
|
92
|
+
// An int-backed field.enum's column function is a LOCAL generated const (the
|
|
93
|
+
// customType helper the caller emits into this same file), so it must not be
|
|
94
|
+
// imported from drizzle-orm/*-core like a built-in column type would be. Same
|
|
95
|
+
// rule the table template applies in `renderColumn`; a view reads the very same
|
|
96
|
+
// integer column and needs the very same decode on the way out.
|
|
97
|
+
if (spec.enumIntCustomType !== undefined) {
|
|
98
|
+
enumIntTypes.set(spec.enumIntCustomType.fnConstName, spec.enumIntCustomType);
|
|
99
|
+
}
|
|
100
|
+
const colSym =
|
|
101
|
+
spec.enumIntCustomType !== undefined
|
|
102
|
+
? spec.enumIntCustomType.fnConstName
|
|
103
|
+
: imp(`${spec.fnName}@${spec.importModule}`);
|
|
79
104
|
const optsArg =
|
|
80
105
|
spec.fnOptions && Object.keys(spec.fnOptions).length > 0
|
|
81
106
|
? `, ${JSON.stringify(spec.fnOptions)}`
|
|
@@ -137,7 +162,8 @@ export function renderExistingViewDecl(
|
|
|
137
162
|
const viewFn = opts.dialect === "postgres" ? "pgView" : "sqliteView";
|
|
138
163
|
const viewModule = opts.dialect === "postgres" ? "drizzle-orm/pg-core" : "drizzle-orm/sqlite-core";
|
|
139
164
|
const viewSym = imp(`${viewFn}@${viewModule}`);
|
|
140
|
-
const
|
|
165
|
+
const enumIntTypes = new Map<string, EnumIntCustomType>();
|
|
166
|
+
const viewColumnLines = fields.map((f) => viewColumnLine(f, opts, enumIntTypes));
|
|
141
167
|
const viewNameExpr = typeof viewName === "string" ? code`${JSON.stringify(viewName)}` : viewName;
|
|
142
168
|
|
|
143
169
|
// @schema — a view lands in a schema exactly as a table does, and migrate qualifies the
|
|
@@ -153,7 +179,16 @@ export function renderExistingViewDecl(
|
|
|
153
179
|
const viewCall: Code = viewSchemaExpr === undefined
|
|
154
180
|
? code`${viewSym}`
|
|
155
181
|
: code`${imp(`pgSchema@${viewModule}`)}(${viewSchemaExpr}).view`;
|
|
156
|
-
|
|
182
|
+
// Int-backed enum codecs are declared BEFORE the view that references them, sorted
|
|
183
|
+
// by const name so output is deterministic regardless of field order — the same
|
|
184
|
+
// contract the table template holds.
|
|
185
|
+
const alreadyDeclared = opts.declaredEnumIntCodecs;
|
|
186
|
+
const enumIntBlocks = [...enumIntTypes.values()]
|
|
187
|
+
.filter((t) => alreadyDeclared === undefined || !alreadyDeclared.has(t.fnConstName))
|
|
188
|
+
.sort((a, b) => a.fnConstName.localeCompare(b.fnConstName))
|
|
189
|
+
.map((t) => renderEnumIntCustomType(t, viewModule));
|
|
190
|
+
|
|
191
|
+
const viewBlock = code`
|
|
157
192
|
// View declaration — Drizzle uses this for typed SELECT queries.
|
|
158
193
|
// The SQL view is created/managed by migrate-ts; .existing() tells Drizzle
|
|
159
194
|
// not to attempt DDL for this declaration.
|
|
@@ -161,6 +196,9 @@ export const ${viewVar} = ${viewCall}(${viewNameExpr}, {
|
|
|
161
196
|
${joinCode(viewColumnLines, { on: ",\n" })}
|
|
162
197
|
}).existing();
|
|
163
198
|
`;
|
|
199
|
+
return enumIntBlocks.length === 0
|
|
200
|
+
? viewBlock
|
|
201
|
+
: joinCode([...enumIntBlocks, viewBlock], { on: "\n" });
|
|
164
202
|
}
|
|
165
203
|
|
|
166
204
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// downstream (e.g. to LLM tool_use input_schema) lost the nested object shape.
|
|
11
11
|
|
|
12
12
|
import { code, joinCode, imp, type Code } from "ts-poet";
|
|
13
|
-
import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
|
|
13
|
+
import { MetaObject, MetaField, type MetaRoot, stripPackage } from "@metaobjectsdev/metadata";
|
|
14
14
|
import {
|
|
15
15
|
FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
|
|
16
16
|
FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
|
|
@@ -75,6 +75,28 @@ export function tphDiscriminatorBase(obj: MetaObject): MetaObject | undefined {
|
|
|
75
75
|
return tphDiscriminatorLevel(obj)?.base;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The object whose TABLE stores `obj`'s rows: the discriminator base for anything beneath
|
|
80
|
+
* a TPH base — a concrete subtype or an abstract level in between — otherwise `obj`.
|
|
81
|
+
*
|
|
82
|
+
* Neither a subtype's module nor an abstract level's emits a table const, so anything
|
|
83
|
+
* that binds to "the other side" of a reference or relationship — an FK's `.references()`,
|
|
84
|
+
* a `relations()` entry, an M:N traversal's target table — must bind here and never to the
|
|
85
|
+
* target's own name. Binding to the subtype imported `carriers` from a `Carrier.ts` that
|
|
86
|
+
* does not export it.
|
|
87
|
+
*/
|
|
88
|
+
export function tphStorageObject(obj: MetaObject): MetaObject {
|
|
89
|
+
if (declaresTphDiscriminator(obj)) return obj;
|
|
90
|
+
return tphDiscriminatorBase(obj) ?? obj;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** {@link tphStorageObject} by entity name, for the name-keyed relation map. A name the
|
|
94
|
+
* root does not resolve is returned unchanged. */
|
|
95
|
+
export function tphStorageName(entityName: string, root: MetaRoot): string {
|
|
96
|
+
const obj = root.findObject(entityName);
|
|
97
|
+
return obj === undefined ? entityName : tphStorageObject(obj).name;
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
/**
|
|
79
101
|
* True when this object itself DECLARES `@discriminator` — the level of a TPH hierarchy
|
|
80
102
|
* that owns the discriminator, whether or not any concrete subtype extends it yet.
|
|
@@ -126,6 +148,28 @@ export function hasAutoSetFields(obj: MetaObject): boolean {
|
|
|
126
148
|
return false;
|
|
127
149
|
}
|
|
128
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Is this field NULL-tolerant in a TPH subtype's READ shape?
|
|
153
|
+
*
|
|
154
|
+
* A TPH subtype shares one physical table with its siblings, so a column only one
|
|
155
|
+
* subtype declares is NULL on every other subtype's row, and a non-`@required` column
|
|
156
|
+
* of this subtype's own is NULL when unset. Either way the value read back is `null`,
|
|
157
|
+
* not `undefined`. The PRIMARY KEY is the exception — it is the shared base table's
|
|
158
|
+
* key and is present on every row.
|
|
159
|
+
*
|
|
160
|
+
* ONE predicate, because TWO emitters answer this question about the same field: the
|
|
161
|
+
* Zod read schema (`renderTphSubtypeReadSchema`) and the declared TS type
|
|
162
|
+
* (`renderValueObjectInterface`). They answered it differently, so the value
|
|
163
|
+
* `parse<Base>()` returns was not assignable to the base union and the generated
|
|
164
|
+
* module did not compile (TS2322). A second answer to one question is the defect;
|
|
165
|
+
* keeping the two call sites pointed here is the fix.
|
|
166
|
+
*/
|
|
167
|
+
export function isTphReadNullTolerant(obj: MetaObject, field: MetaField): boolean {
|
|
168
|
+
if (!isTphSubtype(obj)) return false;
|
|
169
|
+
if (!fieldWillBeOptional(field)) return false;
|
|
170
|
+
return !primaryIdentityFieldNames(obj).includes(field.name);
|
|
171
|
+
}
|
|
172
|
+
|
|
129
173
|
/**
|
|
130
174
|
* FR-017 Tier 2 — the per-subtype FULL read schema `<Sub>Schema`. Unlike the
|
|
131
175
|
* insert schema, this includes every effective field (PK included) so a raw DB
|
|
@@ -149,10 +193,12 @@ export function renderTphSubtypeReadSchema(obj: MetaObject, ctx?: RenderContext)
|
|
|
149
193
|
}
|
|
150
194
|
const expr = zodFieldExpr(child, obj, ctx);
|
|
151
195
|
// zodFieldExpr already appends `.optional()` for non-required fields; add
|
|
152
|
-
// `.nullable()` on top so a NULL column value
|
|
153
|
-
//
|
|
196
|
+
// `.nullable()` on top so a NULL column value parses cleanly. The declared
|
|
197
|
+
// interface widens the SAME fields — see isTphReadNullTolerant.
|
|
154
198
|
fieldLines.push(
|
|
155
|
-
|
|
199
|
+
isTphReadNullTolerant(obj, child)
|
|
200
|
+
? code` ${child.name}: ${expr}.nullable()`
|
|
201
|
+
: code` ${child.name}: ${expr}`,
|
|
156
202
|
);
|
|
157
203
|
}
|
|
158
204
|
|