@metaobjectsdev/codegen-ts 0.6.0 → 0.7.0-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/README.md +161 -4
- package/dist/column-mapper.d.ts +16 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +73 -2
- package/dist/column-mapper.js.map +1 -1
- package/dist/generators/entity-file.d.ts +15 -0
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +2 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts +9 -0
- package/dist/generators/output-parser-file.d.ts.map +1 -0
- package/dist/generators/output-parser-file.js +37 -0
- package/dist/generators/output-parser-file.js.map +1 -0
- package/dist/generators/prompt-render-file.d.ts +9 -0
- package/dist/generators/prompt-render-file.d.ts.map +1 -0
- package/dist/generators/prompt-render-file.js +70 -0
- package/dist/generators/prompt-render-file.js.map +1 -0
- package/dist/generators/queries-file.d.ts +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +11 -3
- package/dist/generators/queries-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/metaobjects-config.d.ts +3 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +2 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/naming.d.ts +3 -12
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +14 -44
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts +8 -0
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +33 -3
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts +1 -1
- package/dist/projection/extract-view-spec.js +1 -1
- package/dist/source-detect.d.ts +10 -0
- package/dist/source-detect.d.ts.map +1 -0
- package/dist/source-detect.js +30 -0
- package/dist/source-detect.js.map +1 -0
- package/dist/templates/drizzle-schema.js +27 -3
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-file.d.ts +15 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +15 -5
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +9 -0
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +88 -2
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/output-parser.d.ts +8 -0
- package/dist/templates/output-parser.d.ts.map +1 -0
- package/dist/templates/output-parser.js +129 -0
- package/dist/templates/output-parser.js.map +1 -0
- package/dist/templates/projection-decl.d.ts +1 -1
- package/dist/templates/projection-decl.js +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +15 -4
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +11 -30
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/value-object-file.d.ts +3 -0
- package/dist/templates/value-object-file.d.ts.map +1 -0
- package/dist/templates/value-object-file.js +27 -0
- package/dist/templates/value-object-file.js.map +1 -0
- package/dist/templates/zod-validators.d.ts +10 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +108 -30
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +4 -4
- package/src/column-mapper.ts +86 -1
- package/src/generators/entity-file.ts +17 -1
- package/src/generators/index.ts +2 -0
- package/src/generators/output-parser-file.ts +50 -0
- package/src/generators/prompt-render-file.ts +95 -0
- package/src/generators/queries-file.ts +13 -4
- package/src/index.ts +1 -1
- package/src/metaobjects-config.ts +4 -2
- package/src/naming.ts +22 -46
- package/src/payload-codegen.ts +34 -2
- package/src/projection/extract-view-spec.ts +1 -1
- package/src/source-detect.ts +28 -0
- package/src/templates/drizzle-schema.ts +27 -3
- package/src/templates/entity-file.ts +36 -5
- package/src/templates/inferred-types.ts +117 -3
- package/src/templates/output-parser.ts +143 -0
- package/src/templates/projection-decl.ts +1 -1
- package/src/templates/queries-file.ts +18 -4
- package/src/templates/queries.ts +11 -33
- package/src/templates/value-object-file.ts +30 -0
- package/src/templates/zod-validators.ts +121 -35
package/src/index.ts
CHANGED
|
@@ -44,4 +44,4 @@ export { emitViewDdl } from "./projection/view-ddl-emit.js";
|
|
|
44
44
|
export type { EmitOptions as ViewDdlEmitOptions } from "./projection/view-ddl-emit.js";
|
|
45
45
|
export type { JoinNode, JoinTree, SelectColumn, SelectSpec, ViewSpec } from "./projection/view-spec.js";
|
|
46
46
|
// Prompt construction (FR-004): typed payload + render-handle codegen.
|
|
47
|
-
export { generatePayloadInterfaces, generateRenderHandle } from "./payload-codegen.js";
|
|
47
|
+
export { generatePayloadInterfaces, generatePayloadInterfacesBatch, generateRenderHandle } from "./payload-codegen.js";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { DEFAULT_COLUMN_NAMING_STRATEGY, type ColumnNamingStrategy } from "@metaobjectsdev/metadata";
|
|
1
2
|
import type { Generator } from "./generator.js";
|
|
2
3
|
import type { ExtStyle } from "./render-context.js";
|
|
3
4
|
import type { OutputLayout, ResolvedTarget } from "./import-path.js";
|
|
4
5
|
|
|
5
6
|
export type Dialect = "sqlite" | "postgres";
|
|
6
|
-
|
|
7
|
+
/** Re-exported from metadata so codegen-ts consumers see one canonical type. */
|
|
8
|
+
export type { ColumnNamingStrategy } from "@metaobjectsdev/metadata";
|
|
7
9
|
export type { ExtStyle };
|
|
8
10
|
export type { OutputLayout };
|
|
9
11
|
export type { ResolvedTarget };
|
|
@@ -87,7 +89,7 @@ export function resolveTargets(config: MetaobjectsGenConfig): Record<string, Res
|
|
|
87
89
|
export function normalizeConfig(config: MetaobjectsGenConfig): NormalizedMetaobjectsGenConfig {
|
|
88
90
|
return {
|
|
89
91
|
...config,
|
|
90
|
-
columnNamingStrategy: config.columnNamingStrategy ??
|
|
92
|
+
columnNamingStrategy: config.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
91
93
|
apiPrefix: config.apiPrefix ?? "",
|
|
92
94
|
outputLayout: config.outputLayout ?? "flat",
|
|
93
95
|
targets: resolveTargets(config),
|
package/src/naming.ts
CHANGED
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
// Naming helpers — case conversion + pluralization for codegen output.
|
|
2
|
-
// All functions are pure.
|
|
2
|
+
// All functions are pure. The strategy primitives (toSnakeCase, toKebabCase,
|
|
3
|
+
// applyColumnNamingStrategy, pluralize, DEFAULT_COLUMN_NAMING_STRATEGY) are
|
|
4
|
+
// re-exported from @metaobjectsdev/metadata so codegen + runtime + migrate
|
|
5
|
+
// share a single source of truth for how field/table names lower to columns.
|
|
3
6
|
|
|
4
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
applyColumnNamingStrategy,
|
|
9
|
+
DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
10
|
+
pluralize,
|
|
11
|
+
toKebabCase,
|
|
12
|
+
toSnakeCase,
|
|
13
|
+
type ColumnNamingStrategy,
|
|
14
|
+
} from "@metaobjectsdev/metadata";
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
* Convert PascalCase or camelCase to snake_case.
|
|
8
|
-
* Treats consecutive capitals (e.g., "APIKey") as a single word: "api_key".
|
|
9
|
-
*/
|
|
10
|
-
export function toSnakeCase(s: string): string {
|
|
11
|
-
return s
|
|
12
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
13
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
14
|
-
.toLowerCase();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Convert PascalCase or camelCase to kebab-case. */
|
|
18
|
-
function toKebabCase(s: string): string {
|
|
19
|
-
return toSnakeCase(s).replace(/_/g, "-");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Apply a ColumnNamingStrategy to a name. */
|
|
23
|
-
function applyStrategy(name: string, strategy: ColumnNamingStrategy): string {
|
|
24
|
-
switch (strategy) {
|
|
25
|
-
case "snake_case": return toSnakeCase(name);
|
|
26
|
-
case "literal": return name;
|
|
27
|
-
case "kebab-case": return toKebabCase(name);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
16
|
+
export { pluralize, toSnakeCase } from "@metaobjectsdev/metadata";
|
|
30
17
|
|
|
31
18
|
/**
|
|
32
19
|
* Convert snake_case to camelCase. Preserves already-camelCase input.
|
|
@@ -42,31 +29,20 @@ export function toPascalCase(s: string): string {
|
|
|
42
29
|
return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
43
30
|
}
|
|
44
31
|
|
|
45
|
-
/**
|
|
46
|
-
* Simple English pluralization. Documented imperfection per design §13 #1:
|
|
47
|
-
* irregular plurals (Person → Persons, not People) are not handled.
|
|
48
|
-
* Users override via source[dbTable]@name in metadata.
|
|
49
|
-
*/
|
|
50
|
-
export function pluralize(s: string): string {
|
|
51
|
-
if (/(s|x|z|ch|sh)$/i.test(s)) return s + "es";
|
|
52
|
-
if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
|
|
53
|
-
return s + "s";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
32
|
/** PascalCase entity → strategy-applied plural for DB table name. */
|
|
57
33
|
export function tableNameFromEntity(
|
|
58
34
|
entityName: string,
|
|
59
|
-
strategy: ColumnNamingStrategy =
|
|
35
|
+
strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
60
36
|
): string {
|
|
61
|
-
return
|
|
37
|
+
return applyColumnNamingStrategy(pluralize(entityName), strategy);
|
|
62
38
|
}
|
|
63
39
|
|
|
64
40
|
/** camelCase or PascalCase field → strategy-applied DB column name. */
|
|
65
41
|
export function columnNameFromField(
|
|
66
42
|
fieldName: string,
|
|
67
|
-
strategy: ColumnNamingStrategy =
|
|
43
|
+
strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
68
44
|
): string {
|
|
69
|
-
return
|
|
45
|
+
return applyColumnNamingStrategy(fieldName, strategy);
|
|
70
46
|
}
|
|
71
47
|
|
|
72
48
|
/**
|
|
@@ -78,14 +54,14 @@ export function viewNameFromProjection(
|
|
|
78
54
|
projectionName: string,
|
|
79
55
|
strategy: ColumnNamingStrategy,
|
|
80
56
|
): string {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
case "literal": return "v_" + projectionName;
|
|
84
|
-
case "kebab-case": return "v-" + toKebabCase(projectionName);
|
|
85
|
-
}
|
|
57
|
+
const sep = strategy === "kebab-case" ? "-" : "_";
|
|
58
|
+
return "v" + sep + applyColumnNamingStrategy(projectionName, strategy);
|
|
86
59
|
}
|
|
87
60
|
|
|
88
61
|
/** PascalCase entity → camelCase plural for the Drizzle table variable. */
|
|
89
62
|
export function variableNameFromEntity(entityName: string): string {
|
|
90
63
|
return pluralize(toCamelCase(entityName.charAt(0).toLowerCase() + entityName.slice(1)));
|
|
91
64
|
}
|
|
65
|
+
|
|
66
|
+
// Re-exported here for callers that import from codegen-ts's naming module.
|
|
67
|
+
export { toKebabCase };
|
package/src/payload-codegen.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
TYPE_TEMPLATE,
|
|
18
18
|
FIELD_SUBTYPE_OBJECT,
|
|
19
19
|
FIELD_ATTR_OBJECT_REF,
|
|
20
|
+
FIELD_ATTR_REQUIRED,
|
|
20
21
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
21
22
|
TEMPLATE_ATTR_TEXT_REF,
|
|
22
23
|
TEMPLATE_ATTR_FORMAT,
|
|
@@ -55,7 +56,13 @@ function fieldTsType(field: MetaData): { type: string; refVo?: string } {
|
|
|
55
56
|
if (typeof ref === "string") result.refVo = ref;
|
|
56
57
|
return result;
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
const scalar = SCALAR_TS[field.subType] ?? "unknown";
|
|
60
|
+
return { type: field.isArray ? `${scalar}[]` : scalar };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True iff the field's @required is explicitly set to true. */
|
|
64
|
+
function isFieldRequired(field: MetaData): boolean {
|
|
65
|
+
return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out: string[]): void {
|
|
@@ -67,7 +74,15 @@ function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out
|
|
|
67
74
|
const refs: string[] = [];
|
|
68
75
|
for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
69
76
|
const { type, refVo } = fieldTsType(f);
|
|
70
|
-
|
|
77
|
+
// Required fields: `name: T;`
|
|
78
|
+
// Optional fields: `name?: T | null;` — the `| null` lets values from
|
|
79
|
+
// Drizzle entity rows (which return `null` for nullable columns) flow
|
|
80
|
+
// straight in. Without it, TS treats undefined-vs-null as a hard error
|
|
81
|
+
// at the entity → payload boundary.
|
|
82
|
+
const isRequired = isFieldRequired(f);
|
|
83
|
+
const tsType = isRequired ? type : `${type} | null`;
|
|
84
|
+
const optional = isRequired ? "" : "?";
|
|
85
|
+
lines.push(` ${f.name}${optional}: ${tsType};`);
|
|
71
86
|
if (refVo) refs.push(refVo);
|
|
72
87
|
}
|
|
73
88
|
lines.push("}");
|
|
@@ -82,6 +97,23 @@ export function generatePayloadInterfaces(root: MetaData, voName: string): strin
|
|
|
82
97
|
return out.join("\n\n") + "\n";
|
|
83
98
|
}
|
|
84
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Emit interfaces for several payloads at once, using a single shared dedupe
|
|
102
|
+
* set so nested types (e.g. lens projections referenced by multiple payloads)
|
|
103
|
+
* appear exactly once in the combined output.
|
|
104
|
+
*
|
|
105
|
+
* Returns the empty string when `voNames` is empty.
|
|
106
|
+
*/
|
|
107
|
+
export function generatePayloadInterfacesBatch(root: MetaData, voNames: readonly string[]): string {
|
|
108
|
+
if (voNames.length === 0) return "";
|
|
109
|
+
const out: string[] = [];
|
|
110
|
+
const emitted = new Set<string>();
|
|
111
|
+
for (const name of voNames) {
|
|
112
|
+
emitInterface(root, name, emitted, out);
|
|
113
|
+
}
|
|
114
|
+
return out.length === 0 ? "" : out.join("\n\n") + "\n";
|
|
115
|
+
}
|
|
116
|
+
|
|
85
117
|
function pascal(s: string): string {
|
|
86
118
|
return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
87
119
|
}
|
|
@@ -348,7 +348,7 @@ function buildGroupBy(spec: SelectSpec): string[] {
|
|
|
348
348
|
* and extends a writable entity).
|
|
349
349
|
* @param root The loader's MetaRoot — all top-level objects are
|
|
350
350
|
* direct children of root (returned by `MetaDataLoader.load()`
|
|
351
|
-
*
|
|
351
|
+
* or `MetaDataLoader.fromDirectory()` as `result.root`).
|
|
352
352
|
* @param ctx Column naming strategy for SQL identifiers.
|
|
353
353
|
*/
|
|
354
354
|
export function extractViewSpec(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Source-detect helpers — discriminate table-backed entities from value-only
|
|
2
|
+
// objects (and other in-memory / transit shapes) by inspecting source.* children.
|
|
3
|
+
//
|
|
4
|
+
// Used by the entity-file composer to pick a streamlined "value-only" emission
|
|
5
|
+
// path for metaobjects that declare no writable relational source. Pure
|
|
6
|
+
// metadata-driven, not a typeId discriminator: any object subtype can opt out
|
|
7
|
+
// of Drizzle table emission simply by omitting source.rdb.
|
|
8
|
+
|
|
9
|
+
import { MetaSource } from "@metaobjectsdev/metadata";
|
|
10
|
+
import { TYPE_SOURCE, SOURCE_SUBTYPE_RDB } from "@metaobjectsdev/metadata";
|
|
11
|
+
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when the entity declares at least one writable source.rdb child.
|
|
15
|
+
* Discriminates table-backed entities (full Drizzle file: table + Insert/Update
|
|
16
|
+
* schemas + filter allowlists + constants) from value-only objects (TS
|
|
17
|
+
* interface + Zod schema only). Absence of source.rdb means in-memory /
|
|
18
|
+
* transit data — no migration, no ORM table to point at.
|
|
19
|
+
*/
|
|
20
|
+
export function hasWritableRdbSource(entity: MetaObject): boolean {
|
|
21
|
+
for (const child of entity.ownChildren()) {
|
|
22
|
+
if (child.type !== TYPE_SOURCE) continue;
|
|
23
|
+
if (child.subType !== SOURCE_SUBTYPE_RDB) continue;
|
|
24
|
+
if (!(child instanceof MetaSource)) continue;
|
|
25
|
+
if (child.isWritable()) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -187,9 +187,17 @@ function buildCompositeKeyCallback(
|
|
|
187
187
|
return code`${primaryKeySym}({ columns: [${columnRefs}] })`;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
/** Build a JS-style object literal string (not JSON.stringify which uses quoted keys).
|
|
190
|
+
/** Build a JS-style object literal string (not JSON.stringify which uses quoted keys).
|
|
191
|
+
* Array values get `as const` appended so Drizzle's text(...,{ enum: [...] })
|
|
192
|
+
* narrows the inferred column type to a literal union instead of bare `string`. */
|
|
191
193
|
function inlineObjectLiteral(obj: Record<string, unknown>): string {
|
|
192
|
-
const entries = Object.entries(obj).map(([k, v]) =>
|
|
194
|
+
const entries = Object.entries(obj).map(([k, v]) => {
|
|
195
|
+
const lit = JSON.stringify(v);
|
|
196
|
+
if (Array.isArray(v)) {
|
|
197
|
+
return `${k}: ${lit} as const`;
|
|
198
|
+
}
|
|
199
|
+
return `${k}: ${lit}`;
|
|
200
|
+
});
|
|
193
201
|
return `{ ${entries.join(", ")} }`;
|
|
194
202
|
}
|
|
195
203
|
|
|
@@ -300,7 +308,23 @@ function renderColumn(
|
|
|
300
308
|
? `.$defaultFn(() => new Date().toISOString())`
|
|
301
309
|
: "";
|
|
302
310
|
|
|
303
|
-
|
|
311
|
+
// $type<E[]>() chain — emitted as Code (not a string modifier) so ts-poet can
|
|
312
|
+
// hoist the cross-module type import for objectRef variants. Positioned
|
|
313
|
+
// immediately after the baseCall so the chain reads `.text(...).$type<...>().notNull()...`
|
|
314
|
+
// which Drizzle accepts in any order but is conventional for "type narrowing
|
|
315
|
+
// first."
|
|
316
|
+
let dollarTypeSegment: Code | string = "";
|
|
317
|
+
if (spec.dollarTypeRef !== undefined) {
|
|
318
|
+
const ref = spec.dollarTypeRef;
|
|
319
|
+
if (ref.kind === "scalar") {
|
|
320
|
+
dollarTypeSegment = `.$type<${ref.tsType}[]>()`;
|
|
321
|
+
} else {
|
|
322
|
+
const refSym = imp(`${ref.name}@${ref.module}`);
|
|
323
|
+
dollarTypeSegment = code`.$type<${refSym}[]>()`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const columnLine = code` ${field.name}: ${baseCall}${dollarTypeSegment}${modifiersStr}${autoSetSuffix}${sqlDefaultSegment ?? ""}${fkRefSegment ?? ""}`;
|
|
304
328
|
return spec.leadingComment !== undefined
|
|
305
329
|
? code` // ${spec.leadingComment}\n${columnLine}`
|
|
306
330
|
: columnLine;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// into one file with the @generated header. ts-poet deduplicates imports.
|
|
3
3
|
//
|
|
4
4
|
// Dispatch:
|
|
5
|
-
// isProjection(entity)
|
|
6
|
-
//
|
|
5
|
+
// isProjection(entity) → renderProjectionDecl (read-only: view declaration + Zod + filter sections)
|
|
6
|
+
// !hasWritableRdbSource(entity) → renderValueObjectFile (in-memory / transit shape: interface + Zod schema)
|
|
7
|
+
// vanilla / write-through entity → Drizzle table path
|
|
7
8
|
|
|
8
9
|
import { joinCode, type Code } from "ts-poet";
|
|
9
10
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
@@ -17,8 +18,31 @@ import { renderFilterType } from "./filter-type.js";
|
|
|
17
18
|
import { GENERATED_HEADER } from "../constants.js";
|
|
18
19
|
import { isProjection } from "../projection/projection-detector.js";
|
|
19
20
|
import { renderProjectionDecl } from "./projection-decl.js";
|
|
21
|
+
import { hasWritableRdbSource } from "../source-detect.js";
|
|
22
|
+
import { renderValueObjectFile } from "./value-object-file.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render-time options for the entity-file composer.
|
|
26
|
+
*
|
|
27
|
+
* `allowlists` (default `true`) controls whether the Fastify-flavored
|
|
28
|
+
* `<Entity>FilterAllowlist` + `<Entity>SortAllowlist` blocks (plus their
|
|
29
|
+
* `runtime-ts/drizzle-fastify` type-only imports) are emitted. Workers/Lambda
|
|
30
|
+
* consumers that don't mount Fastify-style server routes can pass `false` and
|
|
31
|
+
* drop `@metaobjectsdev/runtime-ts` from their deps entirely. The client-side
|
|
32
|
+
* `<Entity>Filter` type is always emitted — consumers still want it for typed
|
|
33
|
+
* client calls regardless of how the server is wired.
|
|
34
|
+
*/
|
|
35
|
+
export interface RenderEntityFileOpts {
|
|
36
|
+
readonly allowlists?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function renderEntityFile(
|
|
40
|
+
entity: MetaObject,
|
|
41
|
+
ctx: RenderContext,
|
|
42
|
+
opts?: RenderEntityFileOpts,
|
|
43
|
+
): string {
|
|
44
|
+
const allowlists = opts?.allowlists ?? true;
|
|
20
45
|
|
|
21
|
-
export function renderEntityFile(entity: MetaObject, ctx: RenderContext): string {
|
|
22
46
|
// --- Projection path (read-only: view-backed entity with no table source) ---
|
|
23
47
|
// Projections intentionally get the z.enum() validator but NOT a named enum
|
|
24
48
|
// type alias — emitting aliases here is a deliberate v1 scope decision.
|
|
@@ -30,6 +54,14 @@ export function renderEntityFile(entity: MetaObject, ctx: RenderContext): string
|
|
|
30
54
|
});
|
|
31
55
|
}
|
|
32
56
|
|
|
57
|
+
// --- Value-only path (no writable source.rdb: in-memory / transit shape) ---
|
|
58
|
+
// No Drizzle table, no migration footprint. Consumers that need to validate
|
|
59
|
+
// the shape (LLM tool_use input_schema, REST body parsing) use the Zod
|
|
60
|
+
// schema; consumers that need the type use the interface.
|
|
61
|
+
if (!hasWritableRdbSource(entity)) {
|
|
62
|
+
return renderValueObjectFile(entity);
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
// --- Vanilla / write-through entity path ---
|
|
34
66
|
const enumAliases = renderEnumTypeAliases(entity);
|
|
35
67
|
const sections: Code[] = [
|
|
@@ -38,8 +70,7 @@ export function renderEntityFile(entity: MetaObject, ctx: RenderContext): string
|
|
|
38
70
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
39
71
|
renderZodValidators(entity),
|
|
40
72
|
renderEntityConstants(entity, ctx.apiPrefix),
|
|
41
|
-
renderFilterAllowlist(entity),
|
|
42
|
-
renderSortAllowlist(entity),
|
|
73
|
+
...(allowlists ? [renderFilterAllowlist(entity), renderSortAllowlist(entity)] : []),
|
|
43
74
|
renderFilterType(entity),
|
|
44
75
|
];
|
|
45
76
|
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
// Inferred types template — emits Drizzle's InferSelectModel / InferInsertModel type aliases,
|
|
2
2
|
// plus named union types for field.enum fields.
|
|
3
|
+
//
|
|
4
|
+
// Also emits the structural TS interface for value-only objects (metaobjects
|
|
5
|
+
// with no writable source.rdb). That path side-steps Drizzle entirely: the
|
|
6
|
+
// interface is computed directly from the field tree, with `name?: T` for
|
|
7
|
+
// optional fields (matching Zod's `.optional()` inference — `T | undefined` —
|
|
8
|
+
// without the superfluous `| null` that Drizzle nullable columns introduce).
|
|
3
9
|
|
|
4
|
-
import { code, imp, type Code } from "ts-poet";
|
|
5
|
-
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
6
|
-
import {
|
|
10
|
+
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
11
|
+
import type { MetaObject, MetaField } from "@metaobjectsdev/metadata";
|
|
12
|
+
import {
|
|
13
|
+
FIELD_SUBTYPE_ENUM,
|
|
14
|
+
FIELD_SUBTYPE_OBJECT,
|
|
15
|
+
FIELD_SUBTYPE_STRING,
|
|
16
|
+
FIELD_SUBTYPE_INT,
|
|
17
|
+
FIELD_SUBTYPE_SHORT,
|
|
18
|
+
FIELD_SUBTYPE_BYTE,
|
|
19
|
+
FIELD_SUBTYPE_LONG,
|
|
20
|
+
FIELD_SUBTYPE_DOUBLE,
|
|
21
|
+
FIELD_SUBTYPE_FLOAT,
|
|
22
|
+
FIELD_SUBTYPE_DECIMAL,
|
|
23
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
24
|
+
FIELD_SUBTYPE_BOOLEAN,
|
|
25
|
+
FIELD_SUBTYPE_DATE,
|
|
26
|
+
FIELD_SUBTYPE_TIME,
|
|
27
|
+
FIELD_SUBTYPE_TIMESTAMP,
|
|
28
|
+
FIELD_SUBTYPE_CLASS,
|
|
29
|
+
FIELD_ATTR_REQUIRED,
|
|
30
|
+
FIELD_ATTR_OBJECT_REF,
|
|
31
|
+
} from "@metaobjectsdev/metadata";
|
|
7
32
|
import { variableNameFromEntity, toPascalCase } from "../naming.js";
|
|
8
33
|
import { enumValues } from "../enum-meta.js";
|
|
9
34
|
import { renderDocsFor } from "./jsdoc.js";
|
|
@@ -53,3 +78,92 @@ export function renderEnumTypeAliases(entity: MetaObject): Code | null {
|
|
|
53
78
|
|
|
54
79
|
return lines.length > 0 ? code`${lines.join("\n")}` : null;
|
|
55
80
|
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Value-object interface emitter
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
|
|
87
|
+
[FIELD_SUBTYPE_STRING]: "string",
|
|
88
|
+
[FIELD_SUBTYPE_CLASS]: "string",
|
|
89
|
+
[FIELD_SUBTYPE_INT]: "number",
|
|
90
|
+
[FIELD_SUBTYPE_SHORT]: "number",
|
|
91
|
+
[FIELD_SUBTYPE_BYTE]: "number",
|
|
92
|
+
[FIELD_SUBTYPE_LONG]: "number",
|
|
93
|
+
[FIELD_SUBTYPE_DOUBLE]: "number",
|
|
94
|
+
[FIELD_SUBTYPE_FLOAT]: "number",
|
|
95
|
+
[FIELD_SUBTYPE_DECIMAL]: "number",
|
|
96
|
+
[FIELD_SUBTYPE_CURRENCY]: "number",
|
|
97
|
+
[FIELD_SUBTYPE_BOOLEAN]: "boolean",
|
|
98
|
+
[FIELD_SUBTYPE_DATE]: "string",
|
|
99
|
+
[FIELD_SUBTYPE_TIME]: "string",
|
|
100
|
+
[FIELD_SUBTYPE_TIMESTAMP]: "string",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Type-alias name for a field.enum, mirroring renderEnumTypeAliases. */
|
|
104
|
+
function enumTypeAliasName(entity: MetaObject, field: MetaField): string {
|
|
105
|
+
const superField = field.resolveSuper();
|
|
106
|
+
return superField !== undefined
|
|
107
|
+
? toPascalCase(superField.name)
|
|
108
|
+
: `${entity.name}${toPascalCase(field.name)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One-line TS type expression for a field on a value-only object.
|
|
113
|
+
* Returns a `Code` so cross-module `field.object` refs can be hoisted via
|
|
114
|
+
* ts-poet `imp(...)` — matching how the Zod emitter hoists `<Ref>InsertSchema`.
|
|
115
|
+
*/
|
|
116
|
+
function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
|
|
117
|
+
// field.object: import the referenced TS interface from its sibling module
|
|
118
|
+
// so ts-poet hoists the import. Mirrors zod-validators.ts's `<Ref>InsertSchema`
|
|
119
|
+
// import strategy, just for the type alias instead of the schema constant.
|
|
120
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
121
|
+
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
122
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
123
|
+
const refImp = imp(`${ref}@./${ref}.js`);
|
|
124
|
+
return field.isArray ? code`${refImp}[]` : code`${refImp}`;
|
|
125
|
+
}
|
|
126
|
+
return field.isArray ? code`unknown[]` : code`unknown`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// field.enum: use the same type-alias name as renderEnumTypeAliases emits.
|
|
130
|
+
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
131
|
+
const values = enumValues(field);
|
|
132
|
+
if (values !== undefined) {
|
|
133
|
+
const alias = enumTypeAliasName(entity, field);
|
|
134
|
+
return field.isArray ? code`${alias}[]` : code`${alias}`;
|
|
135
|
+
}
|
|
136
|
+
return field.isArray ? code`string[]` : code`string`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
|
|
140
|
+
return field.isArray ? code`${scalar}[]` : code`${scalar}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Emit a structural `interface <Name> { ... }` for a value-only object.
|
|
145
|
+
*
|
|
146
|
+
* Optional fields use `name?: T` (matching the Zod `.optional()` inference
|
|
147
|
+
* `T | undefined`) instead of `name?: T | null`. Value objects never round-
|
|
148
|
+
* trip through Drizzle nullable columns, so the null-bridge is unnecessary
|
|
149
|
+
* here — and forces consumers into a residual cast at the call site.
|
|
150
|
+
*/
|
|
151
|
+
export function renderValueObjectInterface(entity: MetaObject): Code {
|
|
152
|
+
const docs = renderDocsFor(entity);
|
|
153
|
+
const docsPrefix = docs ? `${docs}\n` : "";
|
|
154
|
+
|
|
155
|
+
const lines: Code[] = [];
|
|
156
|
+
for (const field of entity.fields()) {
|
|
157
|
+
const required = field.ownAttr(FIELD_ATTR_REQUIRED) === true;
|
|
158
|
+
const optional = required ? "" : "?";
|
|
159
|
+
const tsType = valueObjectFieldType(entity, field);
|
|
160
|
+
lines.push(code` ${field.name}${optional}: ${tsType};`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// joinCode with "\n" interpolates each Code segment on its own line and
|
|
164
|
+
// keeps the imp() registrations intact so ts-poet hoists the imports.
|
|
165
|
+
return code`${docsPrefix}export interface ${entity.name} {
|
|
166
|
+
${joinCode(lines, { on: "\n" })}
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// server/typescript/packages/codegen-ts/src/templates/output-parser.ts
|
|
2
|
+
//
|
|
3
|
+
// Per-template renderer for template.output codegen. Walks the @payloadRef's
|
|
4
|
+
// value-object into a Zod schema and emits a dual-API parser (parse + safeParse)
|
|
5
|
+
// alongside the schema. The emitted file is self-contained: it derives a
|
|
6
|
+
// local data type via `z.infer<typeof Schema>` and exports it as
|
|
7
|
+
// `<TemplateName>Data`. Consumers wiring `promptRender()` get a structurally
|
|
8
|
+
// identical payload-VO interface in `prompts.ts`; either type can be used
|
|
9
|
+
// interchangeably with parse results.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type MetaData,
|
|
13
|
+
TYPE_OBJECT,
|
|
14
|
+
TYPE_FIELD,
|
|
15
|
+
TYPE_TEMPLATE,
|
|
16
|
+
TEMPLATE_SUBTYPE_OUTPUT,
|
|
17
|
+
FIELD_SUBTYPE_OBJECT,
|
|
18
|
+
FIELD_ATTR_OBJECT_REF,
|
|
19
|
+
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
20
|
+
} from "@metaobjectsdev/metadata";
|
|
21
|
+
|
|
22
|
+
const SCALAR_ZOD: Record<string, string> = {
|
|
23
|
+
string: "z.string()",
|
|
24
|
+
class: "z.string()",
|
|
25
|
+
int: "z.number().int()",
|
|
26
|
+
short: "z.number().int()",
|
|
27
|
+
byte: "z.number().int()",
|
|
28
|
+
long: "z.number().int()",
|
|
29
|
+
double: "z.number()",
|
|
30
|
+
float: "z.number()",
|
|
31
|
+
boolean: "z.boolean()",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
35
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
39
|
+
return root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Render the Zod expression for a single field; recurses on @objectRef. */
|
|
43
|
+
function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
|
|
44
|
+
// isArray is a native (reserved) property on MetaData, not an attr.
|
|
45
|
+
const isArray = field.isArray === true;
|
|
46
|
+
let base: string;
|
|
47
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
48
|
+
const refName = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
49
|
+
if (typeof refName !== "string") {
|
|
50
|
+
base = "z.unknown()";
|
|
51
|
+
} else if (seen.has(refName)) {
|
|
52
|
+
// Cycle guard — emit unknown for self-references (rare; lazy schemas not in scope for v1).
|
|
53
|
+
base = "z.unknown()";
|
|
54
|
+
} else {
|
|
55
|
+
const inner = findObject(root, refName);
|
|
56
|
+
base = inner ? renderObjectSchema(inner, root, new Set(seen).add(refName), depth + 1) : "z.unknown()";
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
base = SCALAR_ZOD[field.subType] ?? "z.unknown()";
|
|
60
|
+
}
|
|
61
|
+
return isArray ? `z.array(${base})` : base;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Render a `z.object({ ... })` for an object.value node.
|
|
65
|
+
* At depth 0 the schema starts at column 0 (consumer's `const Foo = z.object({`),
|
|
66
|
+
* so fields sit at 2 spaces and the closing `})` at 0 spaces — matching the
|
|
67
|
+
* surrounding `const NameSchema = ...` statement's indent. Nested schemas
|
|
68
|
+
* step in two spaces per depth level. */
|
|
69
|
+
function renderObjectSchema(vo: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
|
|
70
|
+
const fields = vo.children().filter((c) => c.type === TYPE_FIELD);
|
|
71
|
+
const fieldIndent = " ".repeat(depth + 1);
|
|
72
|
+
const closeIndent = " ".repeat(depth);
|
|
73
|
+
const lines = fields.map((f) => `${fieldIndent}${f.name}: ${fieldZod(f, root, seen, depth)},`);
|
|
74
|
+
return `z.object({\n${lines.join("\n")}\n${closeIndent}})`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Render the full output-parser file for one `template.output` node.
|
|
79
|
+
* Throws if the template isn't found, isn't a template.output, or its
|
|
80
|
+
* @payloadRef doesn't resolve to an object.value.
|
|
81
|
+
*/
|
|
82
|
+
export function renderOutputParser(root: MetaData, templateName: string): string {
|
|
83
|
+
const tmpl = findTemplate(root, templateName);
|
|
84
|
+
if (!tmpl) {
|
|
85
|
+
throw new Error(`template "${templateName}" not found in metadata root`);
|
|
86
|
+
}
|
|
87
|
+
if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
|
|
88
|
+
throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
|
|
89
|
+
}
|
|
90
|
+
const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
91
|
+
if (typeof payloadRef !== "string") {
|
|
92
|
+
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
93
|
+
}
|
|
94
|
+
const vo = findObject(root, payloadRef);
|
|
95
|
+
if (!vo) {
|
|
96
|
+
throw new Error(`template "${templateName}" @payloadRef "${payloadRef}" not found in metadata root`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const schema = renderObjectSchema(vo, root, new Set([payloadRef]), 0);
|
|
100
|
+
const schemaName = `${templateName}Schema`;
|
|
101
|
+
const dataName = `${templateName}Data`;
|
|
102
|
+
const errorName = `${templateName}ValidationError`;
|
|
103
|
+
const parseName = `parse${templateName}`;
|
|
104
|
+
const safeParseName = `safeParse${templateName}`;
|
|
105
|
+
|
|
106
|
+
return `import { z } from "zod";
|
|
107
|
+
|
|
108
|
+
const ${schemaName} = ${schema};
|
|
109
|
+
|
|
110
|
+
export type ${dataName} = z.infer<typeof ${schemaName}>;
|
|
111
|
+
export type ${errorName} = z.ZodError;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse an LLM response into a typed ${dataName}.
|
|
115
|
+
* @throws ZodError on validation failure.
|
|
116
|
+
*/
|
|
117
|
+
export function ${parseName}(text: string): ${dataName} {
|
|
118
|
+
return ${schemaName}.parse(JSON.parse(text));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse an LLM response with explicit error handling (Result-style).
|
|
123
|
+
* Does not throw on validation failure.
|
|
124
|
+
*/
|
|
125
|
+
export function ${safeParseName}(
|
|
126
|
+
text: string,
|
|
127
|
+
): { success: true; data: ${dataName} } | { success: false; error: ${errorName} } {
|
|
128
|
+
let parsed: unknown;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(text);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
error: new z.ZodError([{ code: "custom", path: [], message: \`invalid JSON: \${(err as Error).message}\` }]),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const result = ${schemaName}.safeParse(parsed);
|
|
138
|
+
return result.success
|
|
139
|
+
? { success: true, data: result.data }
|
|
140
|
+
: { success: false, error: result.error };
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
@@ -56,7 +56,7 @@ function pathFromProjectionName(name: string): string {
|
|
|
56
56
|
*
|
|
57
57
|
* @param projection The projection entity (has a source[dbView] child).
|
|
58
58
|
* @param root The loader's root (all top-level objects as direct children,
|
|
59
|
-
* from `MetaDataLoader.load()`
|
|
59
|
+
* from `MetaDataLoader.load()` or `MetaDataLoader.fromDirectory()` as `result.root`).
|
|
60
60
|
* @param opts Column naming strategy + dialect.
|
|
61
61
|
*/
|
|
62
62
|
export function renderProjectionDecl(
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { code, joinCode, type Code } from "ts-poet";
|
|
5
5
|
import { MetaObject } from "@metaobjectsdev/metadata";
|
|
6
6
|
import { type RenderContext } from "../render-context.js";
|
|
7
|
-
import { entityModuleSpecifier
|
|
7
|
+
import { entityModuleSpecifier } from "../import-path.js";
|
|
8
8
|
import {
|
|
9
9
|
renderFindByIdFn,
|
|
10
10
|
renderListFn,
|
|
@@ -26,13 +26,27 @@ export function renderQueriesFile(obj: MetaObject, ctx: RenderContext): string {
|
|
|
26
26
|
entityName,
|
|
27
27
|
ctx.extStyle,
|
|
28
28
|
);
|
|
29
|
-
const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, obj.package, ctx.dbImport);
|
|
30
29
|
const varName = variableNameFromEntity(entityName);
|
|
31
30
|
|
|
32
|
-
//
|
|
31
|
+
// The persistence-context `db` is parameter-passed into every generated CRUD
|
|
32
|
+
// helper (ADR-0008). Emit the dialect-correct Drizzle type alias so the
|
|
33
|
+
// signatures `findXxx(db: Db, ...)` typecheck without the consumer importing
|
|
34
|
+
// anything to construct one. Consumers pass any compatible Drizzle instance.
|
|
35
|
+
const dbTypeImport =
|
|
36
|
+
ctx.dialect === "postgres"
|
|
37
|
+
? `import type { NodePgDatabase } from "drizzle-orm/node-postgres";`
|
|
38
|
+
: `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
|
|
39
|
+
const dbTypeAlias =
|
|
40
|
+
ctx.dialect === "postgres"
|
|
41
|
+
? `type Db = NodePgDatabase<Record<string, never>>;`
|
|
42
|
+
: `type Db = BaseSQLiteDatabase<"async", Record<string, never>>;`;
|
|
43
|
+
|
|
44
|
+
// Literal imports (Db type + entity types) live in a code block so they sort
|
|
33
45
|
// alongside ts-poet's hoisted imp() imports at the top of the body.
|
|
34
46
|
const literalImports = code`
|
|
35
|
-
|
|
47
|
+
${dbTypeImport}
|
|
48
|
+
${dbTypeAlias}
|
|
49
|
+
|
|
36
50
|
import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON.stringify(entityFileName)};
|
|
37
51
|
`;
|
|
38
52
|
|