@metaobjectsdev/codegen-ts 0.10.0 → 0.11.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/dist/column-mapper.d.ts +12 -6
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +48 -24
- package/dist/column-mapper.js.map +1 -1
- package/dist/enum-shared.js +1 -1
- package/dist/enum-shared.js.map +1 -1
- package/dist/generators/docs-data-builder.js +14 -14
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/template-payload-tree.js +1 -1
- package/dist/generators/template-payload-tree.js.map +1 -1
- package/dist/import-path.d.ts +18 -0
- package/dist/import-path.d.ts.map +1 -1
- package/dist/import-path.js +21 -0
- package/dist/import-path.js.map +1 -1
- package/dist/metaobjects-config.d.ts +26 -0
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +3 -0
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/naming.d.ts +20 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +11 -3
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.js +2 -2
- package/dist/payload-codegen.js.map +1 -1
- package/dist/pk-resolver.d.ts.map +1 -1
- package/dist/pk-resolver.js +4 -2
- package/dist/pk-resolver.js.map +1 -1
- package/dist/projection/extract-view-spec.js +1 -1
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/render-context.d.ts +21 -2
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +7 -0
- package/dist/render-context.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +44 -23
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.js +2 -2
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.js +1 -1
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +1 -1
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extractor.js +2 -2
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/field-meta.js +1 -1
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/filter-allowlist.js +2 -2
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-shared.js +2 -2
- package/dist/templates/filter-shared.js.map +1 -1
- package/dist/templates/filter-type.js +1 -1
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/fr010-field-mapping.js +6 -6
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +1 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +18 -7
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/mermaid-er.js +1 -1
- package/dist/templates/mermaid-er.js.map +1 -1
- package/dist/templates/output-format-spec-emitter.js +1 -1
- package/dist/templates/output-format-spec-emitter.js.map +1 -1
- package/dist/templates/output-parser.js +1 -1
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/queries-file.js +3 -3
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +2 -2
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +9 -9
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/relations-block.d.ts.map +1 -1
- package/dist/templates/relations-block.js +3 -4
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/render-helper.js +1 -1
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +1 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.js +5 -5
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.js +2 -2
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +2 -2
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +29 -22
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/column-mapper.ts +58 -28
- package/src/enum-shared.ts +1 -1
- package/src/generators/docs-data-builder.ts +14 -14
- package/src/generators/template-payload-tree.ts +1 -1
- package/src/import-path.ts +28 -0
- package/src/metaobjects-config.ts +29 -0
- package/src/naming.ts +25 -3
- package/src/payload-codegen.ts +2 -2
- package/src/pk-resolver.ts +4 -2
- package/src/projection/extract-view-spec.ts +1 -1
- package/src/render-context.ts +28 -2
- package/src/runner.ts +3 -0
- package/src/templates/drizzle-schema.ts +51 -29
- package/src/templates/entity-constants.ts +2 -2
- package/src/templates/entity-file.ts +1 -1
- package/src/templates/extract-delegate-emitter.ts +1 -1
- package/src/templates/extractor.ts +2 -2
- package/src/templates/field-meta.ts +1 -1
- package/src/templates/filter-allowlist.ts +2 -2
- package/src/templates/filter-shared.ts +2 -2
- package/src/templates/filter-type.ts +1 -1
- package/src/templates/fr010-field-mapping.ts +6 -6
- package/src/templates/inferred-types.ts +18 -7
- package/src/templates/mermaid-er.ts +1 -1
- package/src/templates/output-format-spec-emitter.ts +1 -1
- package/src/templates/output-parser.ts +1 -1
- package/src/templates/queries-file.ts +3 -3
- package/src/templates/queries.ts +8 -10
- package/src/templates/relations-block.ts +3 -4
- package/src/templates/render-helper.ts +1 -1
- package/src/templates/routes-file-hono.ts +1 -2
- package/src/templates/routes-file.ts +5 -5
- package/src/templates/value-object-file.ts +2 -2
- package/src/templates/zod-validators.ts +29 -22
- package/dist/templates/extract-schema-emitter.d.ts +0 -8
- package/dist/templates/extract-schema-emitter.d.ts.map +0 -1
- package/dist/templates/extract-schema-emitter.js +0 -85
- package/dist/templates/extract-schema-emitter.js.map +0 -1
- package/dist/templates/recover-schema-emitter.d.ts +0 -8
- package/dist/templates/recover-schema-emitter.d.ts.map +0 -1
- package/dist/templates/recover-schema-emitter.js +0 -64
- package/dist/templates/recover-schema-emitter.js.map +0 -1
|
@@ -82,7 +82,7 @@ export interface BuildDocDataOpts {
|
|
|
82
82
|
* the documented model-field optionality. Exported so the field-shape builder
|
|
83
83
|
* reuses the EXACT same rule rather than re-deriving it. */
|
|
84
84
|
export function isFieldRequired(field: MetaField): boolean {
|
|
85
|
-
if (field.
|
|
85
|
+
if (field.attr(FIELD_ATTR_REQUIRED) === true) return true;
|
|
86
86
|
return field.validators().some((v) => v.subType === VALIDATOR_SUBTYPE_REQUIRED);
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -104,7 +104,7 @@ interface ValidatorParts {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function collectValidatorParts(field: MetaField): ValidatorParts {
|
|
107
|
-
const maxLenAttr = field.
|
|
107
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
108
108
|
const regexParts: string[] = [];
|
|
109
109
|
const lengthParts: string[] = [];
|
|
110
110
|
const numericParts: string[] = [];
|
|
@@ -143,7 +143,7 @@ function collectValidatorParts(field: MetaField): ValidatorParts {
|
|
|
143
143
|
export function neutralTypeStr(field: MetaField): string {
|
|
144
144
|
let base: string;
|
|
145
145
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
146
|
-
const ref = field.
|
|
146
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
147
147
|
base = typeof ref === "string" && ref.length > 0 ? stripPackage(ref) : "object";
|
|
148
148
|
} else {
|
|
149
149
|
base = field.subType;
|
|
@@ -165,7 +165,7 @@ function neutralTypeCell(field: MetaField): string {
|
|
|
165
165
|
* uses. Deliberately does NOT derive ANSI/ORM SQL so it can't drift vs the
|
|
166
166
|
* migrate engine or re-introduce language-specific DDL. Wrapped in backticks. */
|
|
167
167
|
function storageTypeCell(field: MetaField): string {
|
|
168
|
-
const dbColumnType = field.
|
|
168
|
+
const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
169
169
|
if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
|
|
170
170
|
return `\`${dbColumnType.toUpperCase()}\``;
|
|
171
171
|
}
|
|
@@ -190,7 +190,7 @@ function buildConstraintRow(
|
|
|
190
190
|
const rules: string[] = [];
|
|
191
191
|
|
|
192
192
|
if (isPk) rules.push("primary key");
|
|
193
|
-
if (field.
|
|
193
|
+
if (field.attr(FIELD_ATTR_UNIQUE) === true) rules.push("unique");
|
|
194
194
|
|
|
195
195
|
if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
|
|
196
196
|
const values = enumValues(field);
|
|
@@ -212,7 +212,7 @@ function buildConstraintRow(
|
|
|
212
212
|
rules.push(`references \`${fk.targetEntity}.${fk.targetField}\``);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
const def = field.
|
|
215
|
+
const def = field.attr(FIELD_ATTR_DEFAULT);
|
|
216
216
|
if (def !== undefined) rules.push(`default: \`${String(def)}\``);
|
|
217
217
|
|
|
218
218
|
const sup = field.resolveSuper();
|
|
@@ -271,7 +271,7 @@ function buildFieldRow(
|
|
|
271
271
|
// Otherwise empty. Keeps the column noise-free for the 90% case where
|
|
272
272
|
// field name and column name agree.
|
|
273
273
|
const columnName = field.column;
|
|
274
|
-
const dbColumnType = field.
|
|
274
|
+
const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
275
275
|
const columnDiffers = typeof columnName === "string" && columnName !== field.name;
|
|
276
276
|
const hasPhysicalOverride = typeof dbColumnType === "string" && dbColumnType.length > 0;
|
|
277
277
|
let storageCell = "";
|
|
@@ -287,7 +287,7 @@ function buildFieldRow(
|
|
|
287
287
|
// column, plus the maxLength/length/numeric limits that used to live in
|
|
288
288
|
// the separate Limits cell (collapsed in to keep the table to 5 columns).
|
|
289
289
|
const rules: string[] = [];
|
|
290
|
-
if (field.
|
|
290
|
+
if (field.attr(FIELD_ATTR_UNIQUE) === true) rules.push("unique");
|
|
291
291
|
|
|
292
292
|
if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
|
|
293
293
|
const values = enumValues(field);
|
|
@@ -303,7 +303,7 @@ function buildFieldRow(
|
|
|
303
303
|
rules.push(...lengthParts, ...numericParts);
|
|
304
304
|
|
|
305
305
|
// The FK reference is already encoded in typeCell — don't repeat it in rules.
|
|
306
|
-
const def = field.
|
|
306
|
+
const def = field.attr(FIELD_ATTR_DEFAULT);
|
|
307
307
|
if (def !== undefined) rules.push(`default: \`${String(def)}\``);
|
|
308
308
|
|
|
309
309
|
const sup = field.resolveSuper();
|
|
@@ -340,10 +340,10 @@ function buildFieldDetail(
|
|
|
340
340
|
const hasSummary = typeof summary === "string" && summary.length > 0;
|
|
341
341
|
const sup = field.resolveSuper();
|
|
342
342
|
const fk = fkMap.get(field.name);
|
|
343
|
-
const def = field.
|
|
343
|
+
const def = field.attr(FIELD_ATTR_DEFAULT);
|
|
344
344
|
const columnName = field.column;
|
|
345
|
-
const dbColumnType = field.
|
|
346
|
-
const isUnique = field.
|
|
345
|
+
const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
346
|
+
const isUnique = field.attr(FIELD_ATTR_UNIQUE) === true;
|
|
347
347
|
const isEnum = field.subType === FIELD_SUBTYPE_ENUM && !field.isArray;
|
|
348
348
|
const enumVals = isEnum ? enumValues(field) : undefined;
|
|
349
349
|
const validators = field.validators();
|
|
@@ -352,7 +352,7 @@ function buildFieldDetail(
|
|
|
352
352
|
|| v.subType === VALIDATOR_SUBTYPE_REGEX
|
|
353
353
|
|| v.subType === VALIDATOR_SUBTYPE_NUMERIC,
|
|
354
354
|
);
|
|
355
|
-
const maxLenAttr = field.
|
|
355
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
356
356
|
|
|
357
357
|
// "Interesting enough to render a detail block" predicate. Plain typed
|
|
358
358
|
// fields with no authored annotations get skipped — the at-a-glance Fields
|
|
@@ -503,7 +503,7 @@ function describeIdentity(id: MetaIdentity): string {
|
|
|
503
503
|
: `(${fields.map((f) => `\`${f}\``).join(", ")})`;
|
|
504
504
|
|
|
505
505
|
if (id.subType === IDENTITY_SUBTYPE_PRIMARY) {
|
|
506
|
-
const gen = id.
|
|
506
|
+
const gen = id.attr(IDENTITY_ATTR_GENERATION);
|
|
507
507
|
const genSuffix = typeof gen === "string" ? ` — generation: \`${gen}\`` : "";
|
|
508
508
|
return `**Primary key:** ${fieldList}${genSuffix}`;
|
|
509
509
|
}
|
|
@@ -58,7 +58,7 @@ export function buildEnrichedPayloadTree(
|
|
|
58
58
|
required: isFieldRequired(f),
|
|
59
59
|
};
|
|
60
60
|
if (f.subType === FIELD_SUBTYPE_OBJECT) {
|
|
61
|
-
const ref = f.
|
|
61
|
+
const ref = f.attr(FIELD_ATTR_OBJECT_REF);
|
|
62
62
|
if (typeof ref === "string" && ref.length > 0) {
|
|
63
63
|
// Owner switches to the nested VO for its fields (the recursion below
|
|
64
64
|
// re-derives `owner` from the resolved VO's own name).
|
package/src/import-path.ts
CHANGED
|
@@ -49,6 +49,34 @@ export function crossEntitySpecifier(
|
|
|
49
49
|
return withExt(`${prefix}${toEntity}`, extStyle);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Module specifier to import a value-object's emitted module (`<VO>.ts`) from a
|
|
54
|
+
* file that lives in `fromPkg`. Value objects are emitted by the entity-file
|
|
55
|
+
* generator into the SAME target as entities, so this is the same same-target,
|
|
56
|
+
* package- + extStyle-aware resolution that FK references use.
|
|
57
|
+
*
|
|
58
|
+
* This is the SINGLE source of truth shared by the three places that reference a
|
|
59
|
+
* VO — the field's TS type (inferred-types), its runtime Zod schema
|
|
60
|
+
* (zod-validators), and its Drizzle `.$type<>()` (drizzle-schema). They MUST
|
|
61
|
+
* resolve a given VO to the identical module or they would import two distinct
|
|
62
|
+
* symbols; routing all three through this function makes divergence impossible.
|
|
63
|
+
*
|
|
64
|
+
* `voPkg` is looked up from `packageOf` (which includes `object.value` nodes —
|
|
65
|
+
* `root.objects()` returns every `TYPE_OBJECT` child). An unknown VO falls back
|
|
66
|
+
* to `fromPkg`, yielding a same-dir `./<VO>` specifier (correct for flat layout
|
|
67
|
+
* and for a same-package reference).
|
|
68
|
+
*/
|
|
69
|
+
export function valueObjectModuleSpecifier(
|
|
70
|
+
voName: string,
|
|
71
|
+
packageOf: ReadonlyMap<string, string | undefined>,
|
|
72
|
+
fromPkg: string | undefined,
|
|
73
|
+
layout: OutputLayout,
|
|
74
|
+
extStyle: ExtStyle,
|
|
75
|
+
): string {
|
|
76
|
+
const voPkg = packageOf.has(voName) ? packageOf.get(voName) : fromPkg;
|
|
77
|
+
return crossEntitySpecifier(layout, fromPkg, voPkg, voName, extStyle);
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
/** Barrel (at outDir root) re-export specifier for an entity.
|
|
53
81
|
* Equivalent to crossEntitySpecifier with fromPkg=undefined (barrel is always at root). */
|
|
54
82
|
export function barrelEntrySpecifier(
|
|
@@ -68,6 +68,29 @@ export interface MetaobjectsGenConfig extends ResolvedGenConfig {
|
|
|
68
68
|
generators: GeneratorSpec[];
|
|
69
69
|
/** How field names map to DB column names when @dbColumn is omitted. Defaults to "snake_case". */
|
|
70
70
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
71
|
+
/**
|
|
72
|
+
* Auto-pluralize the Drizzle collection (table) variable name derived from
|
|
73
|
+
* each entity (`AgentConfig` → `agentConfigs`). Defaults to `true`. Set
|
|
74
|
+
* `false` to keep collection vars singular. Per-entity exceptions go in
|
|
75
|
+
* {@link collectionNameOverrides}. Naming is a per-port codegen concern
|
|
76
|
+
* (ADR-0001), so this is config — not a metadata attribute — and carries no
|
|
77
|
+
* cross-port conformance cost.
|
|
78
|
+
*/
|
|
79
|
+
pluralizeCollections?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Per-entity exact collection-var-name overrides, keyed by the bare entity
|
|
82
|
+
* name. Wins over {@link pluralizeCollections} — the escape hatch for the
|
|
83
|
+
* handful of tables a global rule gets wrong
|
|
84
|
+
* (e.g. `{ AuditLog: "auditLog", LlmTierConfig: "llmTierConfig" }`).
|
|
85
|
+
*/
|
|
86
|
+
collectionNameOverrides?: Record<string, string>;
|
|
87
|
+
/**
|
|
88
|
+
* Drizzle timestamp column mode. "string" (default) types timestamp columns as
|
|
89
|
+
* ISO-8601 strings (matches the generated Zod + cross-port wire contract); "date"
|
|
90
|
+
* uses drizzle's native JS-Date mode (for consumers whose hand-written code works
|
|
91
|
+
* with `Date`).
|
|
92
|
+
*/
|
|
93
|
+
timestampMode?: "date" | "string";
|
|
71
94
|
/** Path prefix applied to generated route registrations + hook fetch URLs. Defaults to "". */
|
|
72
95
|
apiPrefix?: string;
|
|
73
96
|
/**
|
|
@@ -102,6 +125,9 @@ export interface NormalizedMetaobjectsGenConfig extends Omit<MetaobjectsGenConfi
|
|
|
102
125
|
/** Fully resolved — every string spec has been mapped to its factory result. */
|
|
103
126
|
generators: Generator[];
|
|
104
127
|
columnNamingStrategy: ColumnNamingStrategy;
|
|
128
|
+
pluralizeCollections: boolean;
|
|
129
|
+
collectionNameOverrides: Record<string, string>;
|
|
130
|
+
timestampMode: "date" | "string";
|
|
105
131
|
apiPrefix: string;
|
|
106
132
|
emitAbstractShapes: boolean;
|
|
107
133
|
outputLayout: OutputLayout;
|
|
@@ -222,6 +248,9 @@ export function normalizeConfig(config: MetaobjectsGenConfig): NormalizedMetaobj
|
|
|
222
248
|
...config,
|
|
223
249
|
generators: resolveGenerators(config.generators),
|
|
224
250
|
columnNamingStrategy: config.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
251
|
+
pluralizeCollections: config.pluralizeCollections ?? true,
|
|
252
|
+
collectionNameOverrides: config.collectionNameOverrides ?? {},
|
|
253
|
+
timestampMode: config.timestampMode ?? "string",
|
|
225
254
|
apiPrefix: config.apiPrefix ?? "",
|
|
226
255
|
emitAbstractShapes: config.emitAbstractShapes ?? true,
|
|
227
256
|
outputLayout: config.outputLayout ?? "flat",
|
package/src/naming.ts
CHANGED
|
@@ -58,9 +58,31 @@ export function viewNameFromProjection(
|
|
|
58
58
|
return "v" + sep + applyColumnNamingStrategy(projectionName, strategy);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
/** Codegen control over how an entity name lowers to its collection (table)
|
|
62
|
+
* variable name. Both knobs are project-level codegen config (ADR-0001 —
|
|
63
|
+
* naming is a per-port codegen concern, NOT a metadata attribute), so they
|
|
64
|
+
* carry no cross-port conformance cost. */
|
|
65
|
+
export interface CollectionNameOptions {
|
|
66
|
+
/** Auto-pluralize the camelCase entity name. Default `true` (e.g.
|
|
67
|
+
* `AgentConfig` → `agentConfigs`). Set `false` to keep it singular
|
|
68
|
+
* (`agentConfig`). */
|
|
69
|
+
pluralize?: boolean;
|
|
70
|
+
/** Per-entity exact var-name overrides, keyed by the bare entity name. Wins
|
|
71
|
+
* over `pluralize` — the escape hatch for the handful of tables a global
|
|
72
|
+
* rule gets wrong (e.g. `{ AuditLog: "auditLog", LlmTierConfig: "llmTierConfig" }`). */
|
|
73
|
+
overrides?: Record<string, string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** PascalCase entity → camelCase Drizzle table variable. Auto-pluralizes by
|
|
77
|
+
* default; `opts` lets a project turn pluralization off globally and/or pin
|
|
78
|
+
* exact names per entity. With no `opts` the behavior is the historical
|
|
79
|
+
* always-pluralize (callers like the relation-resolver that only need the
|
|
80
|
+
* cosmetic query-API member name pass nothing). */
|
|
81
|
+
export function variableNameFromEntity(entityName: string, opts?: CollectionNameOptions): string {
|
|
82
|
+
const override = opts?.overrides?.[entityName];
|
|
83
|
+
if (override !== undefined && override.length > 0) return override;
|
|
84
|
+
const camel = toCamelCase(entityName.charAt(0).toLowerCase() + entityName.slice(1));
|
|
85
|
+
return opts?.pluralize === false ? camel : pluralize(camel);
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
// ---------------------------------------------------------------------------
|
package/src/payload-codegen.ts
CHANGED
|
@@ -65,7 +65,7 @@ function fieldTsType(
|
|
|
65
65
|
ownerName: string,
|
|
66
66
|
): { type: string; refVo?: string; enumAlias?: { name: string; decl: string } } {
|
|
67
67
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
68
|
-
const ref = field.
|
|
68
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
69
69
|
const refName = typeof ref === "string" ? ref : "unknown";
|
|
70
70
|
// isArray is a structural property on MetaData, not an attr.
|
|
71
71
|
const isArray = field.isArray;
|
|
@@ -96,7 +96,7 @@ function fieldTsType(
|
|
|
96
96
|
|
|
97
97
|
/** True iff the field's @required is explicitly set to true. */
|
|
98
98
|
function isFieldRequired(field: MetaData): boolean {
|
|
99
|
-
return field.
|
|
99
|
+
return field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function emitInterface(
|
package/src/pk-resolver.ts
CHANGED
|
@@ -30,7 +30,9 @@ export function buildPkMap(root: MetaRoot): Map<string, PkInfo> {
|
|
|
30
30
|
// primaryIdentity() resolves the primary identity across the super-chain.
|
|
31
31
|
const primary = obj.primaryIdentity();
|
|
32
32
|
if (!primary) continue;
|
|
33
|
-
|
|
33
|
+
// attr() (effective) not ownAttr() — @fields/@generation can be inherited when the
|
|
34
|
+
// identity node-level `extends` a base identity without restating them (#56).
|
|
35
|
+
const fields = primary.attr(IDENTITY_ATTR_FIELDS);
|
|
34
36
|
if (!Array.isArray(fields) && typeof fields !== "string") continue;
|
|
35
37
|
const fieldsList = Array.isArray(fields) ? fields : [fields];
|
|
36
38
|
if (fieldsList.length === 0) continue;
|
|
@@ -38,7 +40,7 @@ export function buildPkMap(root: MetaRoot): Map<string, PkInfo> {
|
|
|
38
40
|
// findField() resolves the field across the super-chain (handles extends:).
|
|
39
41
|
const pkField = obj.findField(pkFieldName);
|
|
40
42
|
const fieldSubType = pkField?.subType ?? FIELD_SUBTYPE_LONG; // sane default
|
|
41
|
-
const generation = primary.
|
|
43
|
+
const generation = primary.attr(IDENTITY_ATTR_GENERATION);
|
|
42
44
|
const info: PkInfo = { fieldName: pkFieldName, fieldSubType };
|
|
43
45
|
if (typeof generation === "string") info.generation = generation;
|
|
44
46
|
result.set(obj.name, info);
|
|
@@ -135,7 +135,7 @@ function sourceColumnNameFor(
|
|
|
135
135
|
entityField: MetaData,
|
|
136
136
|
ctx: ExtractContext,
|
|
137
137
|
): string {
|
|
138
|
-
const col = entityField.
|
|
138
|
+
const col = entityField.attr(FIELD_ATTR_COLUMN);
|
|
139
139
|
if (typeof col === "string" && col !== "") return col;
|
|
140
140
|
return columnNameFromField(entityField.name, ctx.columnNamingStrategy);
|
|
141
141
|
}
|
package/src/render-context.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { PkInfo } from "./pk-resolver.js";
|
|
|
6
6
|
import type { RelationMap } from "./relation-resolver.js";
|
|
7
7
|
import type { ColumnNamingStrategy } from "./metaobjects-config.js";
|
|
8
8
|
import type { OutputLayout, ResolvedTarget } from "./import-path.js";
|
|
9
|
+
import { variableNameFromEntity } from "./naming.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* How to format cross-entity import specifiers in generated files.
|
|
@@ -37,12 +38,26 @@ export interface RenderContext {
|
|
|
37
38
|
extStyle: ExtStyle;
|
|
38
39
|
/** Column naming strategy: how field names map to DB column names. Defaults to "snake_case". */
|
|
39
40
|
columnNamingStrategy: ColumnNamingStrategy;
|
|
41
|
+
/**
|
|
42
|
+
* Drizzle timestamp column mode. "string" (default) types timestamp columns as
|
|
43
|
+
* ISO-8601 strings (matching the generated Zod + cross-port wire contract);
|
|
44
|
+
* "date" uses drizzle's native JS-Date mode (for consumers whose hand-written
|
|
45
|
+
* code works with `Date`). Opt in via `codegen.timestampMode`.
|
|
46
|
+
*/
|
|
47
|
+
timestampMode: "date" | "string";
|
|
40
48
|
/** Path prefix applied to generated route registrations + hook fetch URLs. Defaults to "". */
|
|
41
49
|
apiPrefix: string;
|
|
42
50
|
/** Whether abstract entities emit their shape artifact (type-only interface / value-object file). Defaults to true. Instance/write artifacts are never emitted for abstract entities regardless. */
|
|
43
51
|
emitAbstractShapes: boolean;
|
|
44
52
|
/** Output layout mode: "flat" (default) — all files in outDir; "package" — sub-paths from entity metadata package. */
|
|
45
53
|
outputLayout: OutputLayout;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve an entity name to its Drizzle collection (table) variable name,
|
|
56
|
+
* applying the project's pluralization config + per-entity overrides. Every
|
|
57
|
+
* template that emits or references a table var goes through this so the
|
|
58
|
+
* declaration and all references agree. Defaults to always-pluralize.
|
|
59
|
+
*/
|
|
60
|
+
collectionName: (entityName: string) => string;
|
|
46
61
|
/** The target THIS generator emits to (drives path layout + same-target imports). */
|
|
47
62
|
selfTarget: ResolvedTarget;
|
|
48
63
|
/** Where entity files live (drives cross-target entity imports). */
|
|
@@ -58,17 +73,22 @@ export interface RenderContext {
|
|
|
58
73
|
providedEnumModule?: string;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
/** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). */
|
|
62
|
-
export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "selfTarget" | "entityModuleTarget"> & {
|
|
76
|
+
/** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). `collectionName` is built from `pluralizeCollections` + `collectionNameOverrides` (both default to always-pluralize). */
|
|
77
|
+
export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
|
|
63
78
|
extStyle?: ExtStyle;
|
|
64
79
|
omImport?: string;
|
|
65
80
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
81
|
+
timestampMode?: "date" | "string";
|
|
66
82
|
apiPrefix?: string;
|
|
67
83
|
emitAbstractShapes?: boolean;
|
|
68
84
|
outputLayout?: OutputLayout;
|
|
69
85
|
packageOf?: Map<string, string | undefined>;
|
|
70
86
|
selfTarget?: ResolvedTarget;
|
|
71
87
|
entityModuleTarget?: ResolvedTarget;
|
|
88
|
+
/** Auto-pluralize collection (table) variable names. Default true. */
|
|
89
|
+
pluralizeCollections?: boolean;
|
|
90
|
+
/** Per-entity exact collection-var-name overrides, keyed by bare entity name. */
|
|
91
|
+
collectionNameOverrides?: Record<string, string>;
|
|
72
92
|
};
|
|
73
93
|
|
|
74
94
|
/** Append the configured extension to a cross-entity module specifier. */
|
|
@@ -86,16 +106,22 @@ export function makeRenderContext(opts: RenderContextInput): RenderContext {
|
|
|
86
106
|
outputLayout,
|
|
87
107
|
dbImport: opts.dbImport,
|
|
88
108
|
};
|
|
109
|
+
const collectionNameOpts = {
|
|
110
|
+
pluralize: opts.pluralizeCollections ?? true,
|
|
111
|
+
overrides: opts.collectionNameOverrides ?? {},
|
|
112
|
+
};
|
|
89
113
|
return {
|
|
90
114
|
...opts,
|
|
91
115
|
extStyle: opts.extStyle ?? "none",
|
|
92
116
|
omImport: opts.omImport ?? "../index",
|
|
93
117
|
columnNamingStrategy: opts.columnNamingStrategy ?? "snake_case",
|
|
118
|
+
timestampMode: opts.timestampMode ?? "string",
|
|
94
119
|
apiPrefix: opts.apiPrefix ?? "",
|
|
95
120
|
emitAbstractShapes: opts.emitAbstractShapes ?? true,
|
|
96
121
|
outputLayout,
|
|
97
122
|
packageOf: opts.packageOf ?? new Map(),
|
|
98
123
|
selfTarget: defaultTarget,
|
|
99
124
|
entityModuleTarget: opts.entityModuleTarget ?? defaultTarget,
|
|
125
|
+
collectionName: (entityName: string) => variableNameFromEntity(entityName, collectionNameOpts),
|
|
100
126
|
};
|
|
101
127
|
}
|
package/src/runner.ts
CHANGED
|
@@ -173,6 +173,9 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
173
173
|
dbImport: selfTarget.dbImport,
|
|
174
174
|
extStyle: config.extStyle,
|
|
175
175
|
columnNamingStrategy: config.columnNamingStrategy,
|
|
176
|
+
pluralizeCollections: config.pluralizeCollections,
|
|
177
|
+
collectionNameOverrides: config.collectionNameOverrides,
|
|
178
|
+
timestampMode: config.timestampMode,
|
|
176
179
|
apiPrefix: config.apiPrefix,
|
|
177
180
|
emitAbstractShapes: config.emitAbstractShapes,
|
|
178
181
|
outputLayout: selfTarget.outputLayout,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// plus the relations() block auto-emitted at the end.
|
|
4
4
|
|
|
5
5
|
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
6
|
-
import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
|
|
6
|
+
import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
|
|
7
7
|
import {
|
|
8
8
|
IDENTITY_SUBTYPE_SECONDARY, FIELD_SUBTYPE_LONG,
|
|
9
9
|
IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION, IDENTITY_ATTR_UNIQUE,
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
FIELD_ATTR_AUTO_SET,
|
|
12
12
|
} from "@metaobjectsdev/metadata";
|
|
13
13
|
import { type RenderContext } from "../render-context.js";
|
|
14
|
-
import { crossEntitySpecifier } from "../import-path.js";
|
|
14
|
+
import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
|
|
15
15
|
import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
|
|
16
|
-
import { tableNameFromEntity,
|
|
16
|
+
import { tableNameFromEntity, columnNameFromField } from "../naming.js";
|
|
17
17
|
import { renderRelationsBlock } from "./relations-block.js";
|
|
18
18
|
import { renderDocsFor } from "./jsdoc.js";
|
|
19
19
|
import { collectTphSubtypeFields } from "./tph-discriminator.js";
|
|
@@ -33,17 +33,17 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
33
33
|
const tableFnSym = imp(`${tableFn}@${importModule}`);
|
|
34
34
|
|
|
35
35
|
const tableName = obj.dbTable ?? tableNameFromEntity(obj.name, ctx.columnNamingStrategy);
|
|
36
|
-
const varName =
|
|
36
|
+
const varName = ctx.collectionName(obj.name);
|
|
37
37
|
|
|
38
38
|
const primary = obj.primaryIdentity();
|
|
39
|
-
const rawPkFields = primary?.
|
|
39
|
+
const rawPkFields = primary?.attr(IDENTITY_ATTR_FIELDS);
|
|
40
40
|
const pkFieldsList: string[] = Array.isArray(rawPkFields)
|
|
41
41
|
? rawPkFields as string[]
|
|
42
42
|
: typeof rawPkFields === "string"
|
|
43
43
|
? rawPkFields.split(",").map((f) => f.trim()).filter(Boolean)
|
|
44
44
|
: [];
|
|
45
45
|
const pkFieldNames = new Set<string>(pkFieldsList);
|
|
46
|
-
const pkGeneration = primary?.
|
|
46
|
+
const pkGeneration = primary?.attr(IDENTITY_ATTR_GENERATION) as string | undefined;
|
|
47
47
|
|
|
48
48
|
const fkMap = buildFkMapForEntity(obj, ctx);
|
|
49
49
|
|
|
@@ -56,9 +56,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
56
56
|
const secondaryIdentities = obj.secondaryIdentities();
|
|
57
57
|
const uniqueFieldNames = new Set<string>();
|
|
58
58
|
for (const sec of secondaryIdentities) {
|
|
59
|
-
const uniqueAttr = sec.
|
|
59
|
+
const uniqueAttr = sec.attr(IDENTITY_ATTR_UNIQUE);
|
|
60
60
|
if (uniqueAttr === false) continue; // explicit non-unique → don't mark column
|
|
61
|
-
const fields = sec.
|
|
61
|
+
const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
|
|
62
62
|
if (!Array.isArray(fields) || fields.length !== 1) continue; // multi-col uniques use a callback index, not a column flag
|
|
63
63
|
uniqueFieldNames.add(fields[0]!);
|
|
64
64
|
}
|
|
@@ -72,9 +72,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
72
72
|
const fkInfo = fkMap.get(child.name);
|
|
73
73
|
// Compute the column spec once per field and reuse it for both the column
|
|
74
74
|
// line and the CHECK collection.
|
|
75
|
-
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy);
|
|
75
|
+
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy, ctx.timestampMode);
|
|
76
76
|
const fieldDocs = renderDocsFor(child);
|
|
77
|
-
const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package);
|
|
77
|
+
const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package, obj.name);
|
|
78
78
|
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
79
79
|
if (spec.checkConstraint !== undefined) {
|
|
80
80
|
checkConstraints.push({
|
|
@@ -91,10 +91,10 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
91
91
|
// stamp onto other-subtype inserts), regardless of the field's @required.
|
|
92
92
|
// Subtype entities emit no table of their own (the value-object path).
|
|
93
93
|
for (const child of collectTphSubtypeFields(obj, ctx.loadedRoot)) {
|
|
94
|
-
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy);
|
|
94
|
+
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy, ctx.timestampMode);
|
|
95
95
|
const fieldDocs = renderDocsFor(child);
|
|
96
96
|
const columnLine = renderColumn(
|
|
97
|
-
spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, true,
|
|
97
|
+
spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, obj.name, true,
|
|
98
98
|
);
|
|
99
99
|
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
100
100
|
// Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
|
|
@@ -115,13 +115,13 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
for (const sec of secondaryIdentities) {
|
|
118
|
-
const fields = sec.
|
|
118
|
+
const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
|
|
119
119
|
if (!Array.isArray(fields) || fields.length === 0) continue;
|
|
120
120
|
const indexName = `idx_${tableName}_${fields.map((f) => columnNameFromField(f, ctx.columnNamingStrategy)).join("_")}`;
|
|
121
121
|
// @unique on the identity defaults to true (preserves back-compat with
|
|
122
122
|
// foundations fixtures that assumed secondary identities were always
|
|
123
123
|
// unique). Explicit @unique: false → ordinary non-unique index.
|
|
124
|
-
const uniqueAttr = sec.
|
|
124
|
+
const uniqueAttr = sec.attr(IDENTITY_ATTR_UNIQUE);
|
|
125
125
|
const isUnique = uniqueAttr !== false;
|
|
126
126
|
const indexFn = isUnique ? "uniqueIndex" : "index";
|
|
127
127
|
const indexSym = imp(`${indexFn}@${importModule}`);
|
|
@@ -184,11 +184,15 @@ function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, F
|
|
|
184
184
|
const fkField = fkFieldNames[0]!;
|
|
185
185
|
const targetName = ref.targetEntity;
|
|
186
186
|
if (!targetName) continue;
|
|
187
|
-
|
|
187
|
+
// @references may be authored bare OR package-qualified, and the loader can
|
|
188
|
+
// resolve it to an FQN (e.g. the YAML front-end qualifies it). Strip the
|
|
189
|
+
// package so the lookup matches the object's bare name — mirrors the
|
|
190
|
+
// relation-resolver, which already does this.
|
|
191
|
+
const targetObj = ctx.loadedRoot.findObject(stripPackage(targetName));
|
|
188
192
|
if (!targetObj) continue;
|
|
189
193
|
const targetPkField = ref.resolvedTargetPkField(ctx.loadedRoot) ?? "id";
|
|
190
194
|
result.set(fkField, {
|
|
191
|
-
targetVarName:
|
|
195
|
+
targetVarName: ctx.collectionName(targetObj.name),
|
|
192
196
|
targetEntityName: targetObj.name,
|
|
193
197
|
targetPkField,
|
|
194
198
|
});
|
|
@@ -236,6 +240,9 @@ function renderColumn(
|
|
|
236
240
|
isComposite: boolean,
|
|
237
241
|
isUnique: boolean = false,
|
|
238
242
|
entityPackage: string | undefined = undefined,
|
|
243
|
+
// Name of the entity this column belongs to — used to detect a self-referential
|
|
244
|
+
// FK (target entity === this entity), which Drizzle emits without a self-import.
|
|
245
|
+
currentEntityName: string = "",
|
|
239
246
|
// FR-017 Tier 2 — TPH subtype-only column: force nullable (drop .notNull())
|
|
240
247
|
// and suppress any DB default (other-subtype rows must stay NULL here).
|
|
241
248
|
forceNullable: boolean = false,
|
|
@@ -319,21 +326,30 @@ function renderColumn(
|
|
|
319
326
|
// FK .references() uses imp() so ts-poet tracks the cross-entity import.
|
|
320
327
|
let fkRefSegment: Code | null = null;
|
|
321
328
|
if (fkInfo !== undefined && !isPk) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
if (fkInfo.targetEntityName === currentEntityName) {
|
|
330
|
+
// Self-referential FK (e.g. createdBy → this same table). Drizzle requires
|
|
331
|
+
// referencing the local table const directly — NOT a self-import — with an
|
|
332
|
+
// explicit `Any*Column` return type to break the circular type inference.
|
|
333
|
+
const anyColType = ctx.dialect === "sqlite" ? "AnySQLiteColumn" : "AnyPgColumn";
|
|
334
|
+
const anyColSym = imp(`${anyColType}@${spec.importModule}`);
|
|
335
|
+
fkRefSegment = code`.references((): ${anyColSym} => ${fkInfo.targetVarName}.${fkInfo.targetPkField})`;
|
|
336
|
+
} else {
|
|
337
|
+
const targetSpec = crossEntitySpecifier(
|
|
338
|
+
ctx.outputLayout,
|
|
339
|
+
entityPackage,
|
|
340
|
+
ctx.packageOf.get(fkInfo.targetEntityName),
|
|
341
|
+
fkInfo.targetEntityName,
|
|
342
|
+
ctx.extStyle,
|
|
343
|
+
);
|
|
344
|
+
const targetVarSym = imp(`${fkInfo.targetVarName}@${targetSpec}`);
|
|
345
|
+
fkRefSegment = code`.references(() => ${targetVarSym}.${fkInfo.targetPkField})`;
|
|
346
|
+
}
|
|
331
347
|
}
|
|
332
348
|
|
|
333
349
|
// @autoSet fields: emit .$defaultFn(() => new Date().toISOString()) so Drizzle
|
|
334
350
|
// inserts stamp the server-side timestamp automatically. This means callers don't
|
|
335
351
|
// need to supply createdAt / updatedAt in INSERT calls — Drizzle fills them in.
|
|
336
|
-
const autoSet = field.
|
|
352
|
+
const autoSet = field.attr(FIELD_ATTR_AUTO_SET);
|
|
337
353
|
const autoSetSuffix = (autoSet === "onCreate" || autoSet === "onUpdate")
|
|
338
354
|
? `.$defaultFn(() => new Date().toISOString())`
|
|
339
355
|
: "";
|
|
@@ -346,11 +362,17 @@ function renderColumn(
|
|
|
346
362
|
let dollarTypeSegment: Code | string = "";
|
|
347
363
|
if (spec.dollarTypeRef !== undefined) {
|
|
348
364
|
const ref = spec.dollarTypeRef;
|
|
365
|
+
const suffix = ref.array ? "[]" : "";
|
|
349
366
|
if (ref.kind === "scalar") {
|
|
350
|
-
dollarTypeSegment = `.$type<${ref.tsType}
|
|
367
|
+
dollarTypeSegment = `.$type<${ref.tsType}${suffix}>()`;
|
|
351
368
|
} else {
|
|
352
|
-
|
|
353
|
-
|
|
369
|
+
// Resolve the VO module through the shared layout/package/extStyle-aware
|
|
370
|
+
// helper so the .$type<VO> import matches the field's TS type + Zod schema.
|
|
371
|
+
const moduleSpec = valueObjectModuleSpecifier(
|
|
372
|
+
ref.name, ctx.packageOf, entityPackage, ctx.outputLayout, ctx.extStyle,
|
|
373
|
+
);
|
|
374
|
+
const refSym = imp(`${ref.name}@${moduleSpec}`);
|
|
375
|
+
dollarTypeSegment = ref.array ? code`.$type<${refSym}[]>()` : code`.$type<${refSym}>()`;
|
|
354
376
|
}
|
|
355
377
|
}
|
|
356
378
|
|
|
@@ -160,12 +160,12 @@ function renderFieldRules(field: MetaField): string | undefined {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// Field-level @required attr (if not already covered by validator).
|
|
163
|
-
if (!hasRequired && field.
|
|
163
|
+
if (!hasRequired && field.attr(FIELD_ATTR_REQUIRED) === true) {
|
|
164
164
|
ruleParts.push(`required: ${JSON.stringify(`${humanize(field.name)} is required`)}`);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// Field-level @maxLength attr (if not already covered).
|
|
168
|
-
const maxLenAttr = field.
|
|
168
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
169
169
|
if (!hasMaxLength && typeof maxLenAttr === "number") {
|
|
170
170
|
ruleParts.push(
|
|
171
171
|
`maxLength: { value: ${maxLenAttr}, message: ${JSON.stringify(`Must be ${maxLenAttr} characters or fewer`)} }`,
|
|
@@ -89,7 +89,7 @@ export function renderEntityFile(
|
|
|
89
89
|
const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
|
|
90
90
|
const sections: Code[] = [
|
|
91
91
|
renderDrizzleSchema(entity, ctx),
|
|
92
|
-
renderInferredTypes(entity, tphBase),
|
|
92
|
+
renderInferredTypes(entity, tphBase, ctx),
|
|
93
93
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
94
94
|
renderZodValidators(entity, ctx),
|
|
95
95
|
renderEntityConstants(entity, ctx.apiPrefix),
|
|
@@ -37,7 +37,7 @@ function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
|
37
37
|
|
|
38
38
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
39
39
|
function refVo(field: MetaData, root: MetaData): MetaData | undefined {
|
|
40
|
-
const ref = field.
|
|
40
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
41
41
|
if (typeof ref !== "string") return undefined;
|
|
42
42
|
const direct = findObject(root, ref);
|
|
43
43
|
if (direct !== undefined) return direct;
|
|
@@ -50,7 +50,7 @@ function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
|
50
50
|
|
|
51
51
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
52
52
|
function refVo(field: MetaData, root: MetaData): MetaData | undefined {
|
|
53
|
-
const ref = field.
|
|
53
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
54
54
|
if (typeof ref !== "string") return undefined;
|
|
55
55
|
const direct = findObject(root, ref);
|
|
56
56
|
if (direct !== undefined) return direct;
|
|
@@ -85,7 +85,7 @@ function enumAlias(field: MetaData, ownerName: string): string | undefined {
|
|
|
85
85
|
* required test (boolean `true` only) so the two never skew.
|
|
86
86
|
*/
|
|
87
87
|
function isFieldRequired(field: MetaData): boolean {
|
|
88
|
-
return field.
|
|
88
|
+
return field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** The mirror→strict mapper name for a value-object (`toStrict<Name>`). */
|
|
@@ -114,7 +114,7 @@ export function zodTypeFor(field: MetaField): string {
|
|
|
114
114
|
export function currencyMetaFor(field: MetaField): { currency: string; locale: string } | null {
|
|
115
115
|
if (field.subType !== FIELD_SUBTYPE_CURRENCY) return null;
|
|
116
116
|
const currency =
|
|
117
|
-
(field.
|
|
117
|
+
(field.attr(FIELD_ATTR_CURRENCY) as string | undefined) ?? FIELD_ATTR_CURRENCY_DEFAULT;
|
|
118
118
|
const viewChild = field.views().find((c) => c.subType === VIEW_SUBTYPE_CURRENCY);
|
|
119
119
|
const locale =
|
|
120
120
|
(viewChild?.ownAttr(VIEW_CURRENCY_ATTR_LOCALE) as string | undefined) ??
|