@metaobjectsdev/codegen-ts 0.5.0-rc.3 → 0.6.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 +3 -1
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +18 -2
- package/dist/column-mapper.js.map +1 -1
- package/dist/enum-meta.d.ts +14 -0
- package/dist/enum-meta.d.ts.map +1 -0
- package/dist/enum-meta.js +24 -0
- package/dist/enum-meta.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +1 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/mermaid-er.d.ts +14 -0
- package/dist/generators/mermaid-er.d.ts.map +1 -0
- package/dist/generators/mermaid-er.js +21 -0
- package/dist/generators/mermaid-er.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/naming.d.ts +4 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +6 -0
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts +6 -0
- package/dist/payload-codegen.d.ts.map +1 -0
- package/dist/payload-codegen.js +95 -0
- package/dist/payload-codegen.js.map +1 -0
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +8 -5
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/projection/projection-detector.d.ts.map +1 -1
- package/dist/projection/projection-detector.js +8 -7
- package/dist/projection/projection-detector.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +22 -3
- 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 +5 -1
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +6 -1
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +7 -0
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +38 -3
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/jsdoc.d.ts +26 -0
- package/dist/templates/jsdoc.d.ts.map +1 -0
- package/dist/templates/jsdoc.js +67 -0
- package/dist/templates/jsdoc.js.map +1 -0
- package/dist/templates/mermaid-er.d.ts +6 -0
- package/dist/templates/mermaid-er.d.ts.map +1 -0
- package/dist/templates/mermaid-er.js +117 -0
- package/dist/templates/mermaid-er.js.map +1 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +12 -3
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +4 -3
- package/src/column-mapper.ts +23 -3
- package/src/enum-meta.ts +26 -0
- package/src/generators/index.ts +1 -0
- package/src/generators/mermaid-er.ts +29 -0
- package/src/index.ts +2 -0
- package/src/naming.ts +7 -0
- package/src/payload-codegen.ts +106 -0
- package/src/projection/extract-view-spec.ts +9 -9
- package/src/projection/projection-detector.ts +11 -15
- package/src/templates/drizzle-schema.ts +24 -3
- package/src/templates/entity-file.ts +5 -1
- package/src/templates/field-meta.ts +6 -0
- package/src/templates/inferred-types.ts +42 -3
- package/src/templates/jsdoc.ts +88 -0
- package/src/templates/mermaid-er.ts +120 -0
- package/src/templates/zod-validators.ts +13 -2
package/src/column-mapper.ts
CHANGED
|
@@ -16,16 +16,17 @@ import {
|
|
|
16
16
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
17
17
|
FIELD_SUBTYPE_OBJECT,
|
|
18
18
|
FIELD_SUBTYPE_CLASS,
|
|
19
|
+
FIELD_SUBTYPE_ENUM,
|
|
19
20
|
VALIDATOR_SUBTYPE_REQUIRED,
|
|
20
21
|
VALIDATOR_SUBTYPE_LENGTH,
|
|
21
22
|
FIELD_ATTR_MAX_LENGTH,
|
|
22
23
|
FIELD_ATTR_REQUIRED,
|
|
23
|
-
FIELD_ATTR_DB_COLUMN,
|
|
24
24
|
FIELD_ATTR_UNIQUE,
|
|
25
25
|
FIELD_ATTR_DEFAULT,
|
|
26
26
|
VALIDATOR_ATTR_MAX,
|
|
27
27
|
} from "@metaobjectsdev/metadata";
|
|
28
28
|
import { columnNameFromField } from "./naming.js";
|
|
29
|
+
import { enumValues } from "./enum-meta.js";
|
|
29
30
|
import type { Dialect, ColumnNamingStrategy } from "./metaobjects-config.js";
|
|
30
31
|
|
|
31
32
|
export type { Dialect };
|
|
@@ -75,7 +76,7 @@ function canonicalizeSqlExpr(value: string): string {
|
|
|
75
76
|
export interface ColumnSpec {
|
|
76
77
|
/** Drizzle function name, e.g., "text", "integer", "varchar". */
|
|
77
78
|
fnName: string;
|
|
78
|
-
/** DB column name (snake_case from field name, or @
|
|
79
|
+
/** DB column name (snake_case from field name, or @column override). */
|
|
79
80
|
dbName: string;
|
|
80
81
|
/** Positional args after dbName (currently always empty; reserved). */
|
|
81
82
|
fnArgs: unknown[];
|
|
@@ -89,6 +90,8 @@ export interface ColumnSpec {
|
|
|
89
90
|
importModule: string;
|
|
90
91
|
/** Optional leading line-comment for the generated column (e.g., type-fallback notice). */
|
|
91
92
|
leadingComment?: string;
|
|
93
|
+
/** Optional CHECK constraint expression for the column (e.g., `status IN ('A', 'B')`). */
|
|
94
|
+
checkConstraint?: string;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
/** Resolve max length from validator.length child or @maxLength attr.
|
|
@@ -117,7 +120,7 @@ export function mapColumnType(
|
|
|
117
120
|
dialect: Dialect,
|
|
118
121
|
strategy: ColumnNamingStrategy = "snake_case",
|
|
119
122
|
): ColumnSpec {
|
|
120
|
-
const dbName =
|
|
123
|
+
const dbName = field.column ?? columnNameFromField(field.name, strategy);
|
|
121
124
|
const importModule = dialect === "sqlite" ? "drizzle-orm/sqlite-core" : "drizzle-orm/pg-core";
|
|
122
125
|
const subType = field.subType;
|
|
123
126
|
const isArray = field.isArray;
|
|
@@ -157,6 +160,7 @@ export function mapColumnType(
|
|
|
157
160
|
case FIELD_SUBTYPE_TIME:
|
|
158
161
|
case FIELD_SUBTYPE_TIMESTAMP:
|
|
159
162
|
case FIELD_SUBTYPE_STRING:
|
|
163
|
+
case FIELD_SUBTYPE_ENUM:
|
|
160
164
|
case FIELD_SUBTYPE_CLASS:
|
|
161
165
|
case FIELD_SUBTYPE_OBJECT:
|
|
162
166
|
default:
|
|
@@ -206,6 +210,7 @@ export function mapColumnType(
|
|
|
206
210
|
}
|
|
207
211
|
break;
|
|
208
212
|
}
|
|
213
|
+
case FIELD_SUBTYPE_ENUM:
|
|
209
214
|
case FIELD_SUBTYPE_CLASS:
|
|
210
215
|
case FIELD_SUBTYPE_OBJECT:
|
|
211
216
|
default:
|
|
@@ -262,5 +267,20 @@ export function mapColumnType(
|
|
|
262
267
|
if (fnOptions !== undefined) result.fnOptions = fnOptions;
|
|
263
268
|
if (defaultExpr !== undefined) result.defaultExpr = defaultExpr;
|
|
264
269
|
if (leadingComment !== undefined) result.leadingComment = leadingComment;
|
|
270
|
+
|
|
271
|
+
// Enum fields: emit a CHECK constraint listing the valid member values.
|
|
272
|
+
if (subType === FIELD_SUBTYPE_ENUM && !isArray) {
|
|
273
|
+
const values = enumValues(field);
|
|
274
|
+
if (values !== undefined && values.length > 0) {
|
|
275
|
+
// Single-quote escaping is belt-and-suspenders: the loader's
|
|
276
|
+
// ENUM_MEMBER_PATTERN already rejects quote-bearing members (members are
|
|
277
|
+
// validated to be identifier-safe), so this never fires in practice.
|
|
278
|
+
const list = values
|
|
279
|
+
.map((v) => `'${v.replace(/'/g, "''")}'`)
|
|
280
|
+
.join(", ");
|
|
281
|
+
result.checkConstraint = `${dbName} IN (${list})`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
265
285
|
return result;
|
|
266
286
|
}
|
package/src/enum-meta.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Shared field.enum metadata helpers — consumed by zod-validators.ts,
|
|
2
|
+
// field-meta.ts, inferred-types.ts, and column-mapper.ts so the @values
|
|
3
|
+
// extraction and the z.enum([...]) expression are derived in exactly one place.
|
|
4
|
+
|
|
5
|
+
import type { MetaField } from "@metaobjectsdev/metadata";
|
|
6
|
+
import { FIELD_ATTR_VALUES } from "@metaobjectsdev/metadata";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Effective enum member values (`@values`) for a field, as strings.
|
|
10
|
+
* Returns undefined when the field declares no `@values` array — callers then
|
|
11
|
+
* fall back to the untyped `z.string()` / `text` representation.
|
|
12
|
+
*
|
|
13
|
+
* `attr()` already returns the effective (own-winning-else-inherited) value, so
|
|
14
|
+
* a field that inherits `@values` from an abstract `field.enum` super resolves
|
|
15
|
+
* here too.
|
|
16
|
+
*/
|
|
17
|
+
export function enumValues(field: MetaField): string[] | undefined {
|
|
18
|
+
const values = field.attr(FIELD_ATTR_VALUES);
|
|
19
|
+
if (!Array.isArray(values)) return undefined;
|
|
20
|
+
return values.map((v) => String(v));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build the Zod expression for a set of enum members, e.g. `z.enum(["A", "B"])`. */
|
|
24
|
+
export function zodEnumExpr(values: string[]): string {
|
|
25
|
+
return `z.enum([${values.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
26
|
+
}
|
package/src/generators/index.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { entityFile, type EntityFileOpts } from "./entity-file.js";
|
|
|
2
2
|
export { queriesFile, type QueriesFileOpts } from "./queries-file.js";
|
|
3
3
|
export { routesFile, type RoutesFileOpts } from "./routes-file.js";
|
|
4
4
|
export { barrel, type BarrelOpts } from "./barrel.js";
|
|
5
|
+
export { mermaidErDiagram, type MermaidErOptions } from "./mermaid-er.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { oncePerRun, type Generator, type GeneratorFactory } from "../generator.js";
|
|
2
|
+
import { renderMermaidModel } from "../templates/mermaid-er.js";
|
|
3
|
+
|
|
4
|
+
export interface MermaidErOptions {
|
|
5
|
+
/** Output path relative to the target's outDir. Defaults to "docs/model.md". */
|
|
6
|
+
outFile?: string;
|
|
7
|
+
/** Named output target. */
|
|
8
|
+
target?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Emit a single Markdown file containing a Mermaid `erDiagram` plus per-entity
|
|
13
|
+
* prose sections. The renderer walks the loaded root for all entities; the
|
|
14
|
+
* default outFile is "docs/model.md".
|
|
15
|
+
*/
|
|
16
|
+
export const mermaidErDiagram = function mermaidErDiagram(
|
|
17
|
+
opts?: MermaidErOptions,
|
|
18
|
+
): Generator {
|
|
19
|
+
const outFile = opts?.outFile ?? "docs/model.md";
|
|
20
|
+
const generator: Generator = {
|
|
21
|
+
name: "mermaid-er-diagram",
|
|
22
|
+
generate: oncePerRun((_entities, ctx) => ({
|
|
23
|
+
path: outFile,
|
|
24
|
+
content: renderMermaidModel(ctx.loadedRoot),
|
|
25
|
+
})),
|
|
26
|
+
};
|
|
27
|
+
if (opts?.target) generator.target = opts.target;
|
|
28
|
+
return generator;
|
|
29
|
+
} as GeneratorFactory<MermaidErOptions>;
|
package/src/index.ts
CHANGED
|
@@ -43,3 +43,5 @@ export type { ExtractContext } from "./projection/extract-view-spec.js";
|
|
|
43
43
|
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
|
+
// Prompt construction (FR-004): typed payload + render-handle codegen.
|
|
47
|
+
export { generatePayloadInterfaces, generateRenderHandle } from "./payload-codegen.js";
|
package/src/naming.ts
CHANGED
|
@@ -35,6 +35,13 @@ export function toCamelCase(s: string): string {
|
|
|
35
35
|
return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Capitalize the first character of a string (camelCase → PascalCase).
|
|
40
|
+
*/
|
|
41
|
+
export function toPascalCase(s: string): string {
|
|
42
|
+
return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
/**
|
|
39
46
|
* Simple English pluralization. Documented imperfection per design §13 #1:
|
|
40
47
|
* irregular plurals (Person → Persons, not People) are not handled.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Payload-type + render-handle codegen for prompt construction (FR-004 Plan #3, B).
|
|
2
|
+
//
|
|
3
|
+
// Emits the TYPED PAYLOAD as a language-idiomatic type (a TS `interface` here;
|
|
4
|
+
// a record/POJO when this ports to Java) and a typed render handle. Types only —
|
|
5
|
+
// no class, no runtime ValueObject; the render engine consumes a plain object,
|
|
6
|
+
// and structural typing gives the caller-side compile-time guarantee.
|
|
7
|
+
//
|
|
8
|
+
// NOTE (slice): the field→TS-type map is local here; the production generator
|
|
9
|
+
// should reuse codegen-ts's canonical field mapping + ts-poet emit + the
|
|
10
|
+
// Generator/runner integration. Assembler (RDB materialization + host overlay)
|
|
11
|
+
// is out of scope — this only emits the contract.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type MetaData,
|
|
15
|
+
TYPE_OBJECT,
|
|
16
|
+
TYPE_FIELD,
|
|
17
|
+
TYPE_TEMPLATE,
|
|
18
|
+
FIELD_SUBTYPE_OBJECT,
|
|
19
|
+
FIELD_ATTR_OBJECT_REF,
|
|
20
|
+
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
21
|
+
TEMPLATE_ATTR_TEXT_REF,
|
|
22
|
+
TEMPLATE_ATTR_FORMAT,
|
|
23
|
+
} from "@metaobjectsdev/metadata";
|
|
24
|
+
|
|
25
|
+
const SCALAR_TS: Record<string, string> = {
|
|
26
|
+
string: "string",
|
|
27
|
+
class: "string",
|
|
28
|
+
int: "number",
|
|
29
|
+
short: "number",
|
|
30
|
+
byte: "number",
|
|
31
|
+
long: "number",
|
|
32
|
+
double: "number",
|
|
33
|
+
float: "number",
|
|
34
|
+
decimal: "number",
|
|
35
|
+
currency: "number",
|
|
36
|
+
boolean: "boolean",
|
|
37
|
+
date: "string",
|
|
38
|
+
time: "string",
|
|
39
|
+
timestamp: "string",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
43
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fieldTsType(field: MetaData): { type: string; refVo?: string } {
|
|
47
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
48
|
+
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
49
|
+
const refName = typeof ref === "string" ? ref : "unknown";
|
|
50
|
+
// isArray is a structural property on MetaData, not an attr.
|
|
51
|
+
const isArray = field.isArray;
|
|
52
|
+
const result: { type: string; refVo?: string } = {
|
|
53
|
+
type: isArray ? `${refName}[]` : refName,
|
|
54
|
+
};
|
|
55
|
+
if (typeof ref === "string") result.refVo = ref;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
return { type: SCALAR_TS[field.subType] ?? "unknown" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out: string[]): void {
|
|
62
|
+
if (emitted.has(voName)) return;
|
|
63
|
+
const vo = findObject(root, voName);
|
|
64
|
+
if (!vo) return;
|
|
65
|
+
emitted.add(voName);
|
|
66
|
+
const lines: string[] = [`export interface ${voName} {`];
|
|
67
|
+
const refs: string[] = [];
|
|
68
|
+
for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
69
|
+
const { type, refVo } = fieldTsType(f);
|
|
70
|
+
lines.push(` ${f.name}: ${type};`);
|
|
71
|
+
if (refVo) refs.push(refVo);
|
|
72
|
+
}
|
|
73
|
+
lines.push("}");
|
|
74
|
+
out.push(lines.join("\n"));
|
|
75
|
+
for (const r of refs) emitInterface(root, r, emitted, out);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Emit the payload `interface` (+ nested element interfaces) for an object.value view-object. */
|
|
79
|
+
export function generatePayloadInterfaces(root: MetaData, voName: string): string {
|
|
80
|
+
const out: string[] = [];
|
|
81
|
+
emitInterface(root, voName, new Set<string>(), out);
|
|
82
|
+
return out.join("\n\n") + "\n";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pascal(s: string): string {
|
|
86
|
+
return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Emit a typed render handle binding a template's @textRef + @format and typing its payload. */
|
|
90
|
+
export function generateRenderHandle(root: MetaData, templateName: string): string {
|
|
91
|
+
const tmpl = root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === templateName);
|
|
92
|
+
if (!tmpl) throw new Error(`template "${templateName}" not found`);
|
|
93
|
+
const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
94
|
+
const textRef = tmpl.ownAttr(TEMPLATE_ATTR_TEXT_REF);
|
|
95
|
+
const format = (tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
|
|
96
|
+
const fn = `render${pascal(templateName)}`;
|
|
97
|
+
return [
|
|
98
|
+
`import { render, type Provider } from "@metaobjectsdev/render";`,
|
|
99
|
+
`import type { ${payloadRef} } from "./payloads.js";`,
|
|
100
|
+
``,
|
|
101
|
+
`export function ${fn}(payload: ${payloadRef}, provider: Provider): string {`,
|
|
102
|
+
` return render({ ref: ${JSON.stringify(textRef)}, payload, format: ${JSON.stringify(format)}, provider });`,
|
|
103
|
+
`}`,
|
|
104
|
+
``,
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
@@ -2,9 +2,7 @@ import {
|
|
|
2
2
|
TYPE_FIELD,
|
|
3
3
|
TYPE_ORIGIN,
|
|
4
4
|
TYPE_RELATIONSHIP,
|
|
5
|
-
|
|
6
|
-
SOURCE_SUBTYPE_DB_VIEW,
|
|
7
|
-
SOURCE_DB_VIEW_ATTR_NAME,
|
|
5
|
+
MetaSource,
|
|
8
6
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
9
7
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
10
8
|
ORIGIN_PASSTHROUGH_ATTR_FROM,
|
|
@@ -15,7 +13,7 @@ import {
|
|
|
15
13
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
16
14
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
17
15
|
CARDINALITY_ONE,
|
|
18
|
-
|
|
16
|
+
FIELD_ATTR_COLUMN,
|
|
19
17
|
findReferenceBetween,
|
|
20
18
|
type AggregateFunction,
|
|
21
19
|
} from "@metaobjectsdev/metadata";
|
|
@@ -48,10 +46,11 @@ function findRelationship(obj: MetaData, name: string): MetaData | undefined {
|
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
function viewName(projection: MetaObject, ctx: ExtractContext): string {
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// The read-only source carries the physical view name (@table).
|
|
50
|
+
const viewSource = projection.ownChildren().find(
|
|
51
|
+
(c): c is MetaSource => c instanceof MetaSource && c.isReadOnly(),
|
|
53
52
|
);
|
|
54
|
-
const explicit =
|
|
53
|
+
const explicit = viewSource?.tableName;
|
|
55
54
|
return explicit ?? viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
|
|
56
55
|
}
|
|
57
56
|
|
|
@@ -81,8 +80,9 @@ function sourceColumnNameFor(
|
|
|
81
80
|
entityField: MetaData,
|
|
82
81
|
ctx: ExtractContext,
|
|
83
82
|
): string {
|
|
84
|
-
const
|
|
85
|
-
|
|
83
|
+
const col = entityField.ownAttr(FIELD_ATTR_COLUMN);
|
|
84
|
+
if (typeof col === "string" && col !== "") return col;
|
|
85
|
+
return columnNameFromField(entityField.name, ctx.columnNamingStrategy);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function shortAliasFor(entityName: string, used: Set<string>): string {
|
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TYPE_SOURCE,
|
|
3
|
-
SOURCE_SUBTYPE_DB_TABLE,
|
|
4
|
-
SOURCE_SUBTYPE_DB_VIEW,
|
|
5
|
-
} from "@metaobjectsdev/metadata";
|
|
1
|
+
import { MetaSource } from "@metaobjectsdev/metadata";
|
|
6
2
|
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
7
3
|
|
|
8
|
-
function
|
|
4
|
+
function hasReadOnlyKindSource(entity: MetaData): boolean {
|
|
9
5
|
return entity.ownChildren().some(
|
|
10
|
-
(c) => c
|
|
6
|
+
(c) => c instanceof MetaSource && c.isReadOnly(),
|
|
11
7
|
);
|
|
12
8
|
}
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
|
|
17
|
-
!hasSource(entity, SOURCE_SUBTYPE_DB_TABLE)
|
|
10
|
+
function hasWritableKindSource(entity: MetaData): boolean {
|
|
11
|
+
return entity.ownChildren().some(
|
|
12
|
+
(c) => c instanceof MetaSource && c.isWritable(),
|
|
18
13
|
);
|
|
19
14
|
}
|
|
20
15
|
|
|
16
|
+
export function isProjection(entity: MetaData): boolean {
|
|
17
|
+
return hasReadOnlyKindSource(entity) && !hasWritableKindSource(entity);
|
|
18
|
+
}
|
|
19
|
+
|
|
21
20
|
export function isWriteThrough(entity: MetaData): boolean {
|
|
22
|
-
return (
|
|
23
|
-
hasSource(entity, SOURCE_SUBTYPE_DB_VIEW) &&
|
|
24
|
-
hasSource(entity, SOURCE_SUBTYPE_DB_TABLE)
|
|
25
|
-
);
|
|
21
|
+
return hasReadOnlyKindSource(entity) && hasWritableKindSource(entity);
|
|
26
22
|
}
|
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
} from "@metaobjectsdev/metadata";
|
|
13
13
|
import { type RenderContext } from "../render-context.js";
|
|
14
14
|
import { crossEntitySpecifier } from "../import-path.js";
|
|
15
|
-
import { mapColumnType } from "../column-mapper.js";
|
|
15
|
+
import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
|
|
16
16
|
import { tableNameFromEntity, variableNameFromEntity, columnNameFromField } from "../naming.js";
|
|
17
17
|
import { renderRelationsBlock } from "./relations-block.js";
|
|
18
|
+
import { renderDocsFor } from "./jsdoc.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Render the Drizzle table definition for one entity, including:
|
|
@@ -62,11 +63,24 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
const columnLines: Code[] = [];
|
|
66
|
+
// Collect CHECK constraints for enum columns; emitted as table-level check() callbacks.
|
|
67
|
+
const checkConstraints: Array<{ name: string; expr: string }> = [];
|
|
65
68
|
for (const child of obj.fields()) {
|
|
66
69
|
const isPk = pkFieldNames.has(child.name);
|
|
67
70
|
const isUnique = uniqueFieldNames.has(child.name) && !isPk;
|
|
68
71
|
const fkInfo = fkMap.get(child.name);
|
|
69
|
-
|
|
72
|
+
// Compute the column spec once per field and reuse it for both the column
|
|
73
|
+
// line and the CHECK collection.
|
|
74
|
+
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy);
|
|
75
|
+
const fieldDocs = renderDocsFor(child);
|
|
76
|
+
const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package);
|
|
77
|
+
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
78
|
+
if (spec.checkConstraint !== undefined) {
|
|
79
|
+
checkConstraints.push({
|
|
80
|
+
name: `chk_${tableName}_${spec.dbName}`,
|
|
81
|
+
expr: spec.checkConstraint,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
// Build all table callback entries
|
|
@@ -93,6 +107,13 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
93
107
|
callbackEntries.push(code`${indexSym}(${JSON.stringify(indexName)}).on(${cols})`);
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
// Emit table-level CHECK constraints for enum fields.
|
|
111
|
+
for (const { name, expr } of checkConstraints) {
|
|
112
|
+
const checkSym = imp(`check@${importModule}`);
|
|
113
|
+
const sqlSym = imp("sql@drizzle-orm");
|
|
114
|
+
callbackEntries.push(code`${checkSym}(${JSON.stringify(name)}, ${sqlSym}\`${expr}\`)`);
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
let tableBlock: Code;
|
|
97
118
|
if (callbackEntries.length > 0) {
|
|
98
119
|
tableBlock = code`
|
|
@@ -174,6 +195,7 @@ function inlineObjectLiteral(obj: Record<string, unknown>): string {
|
|
|
174
195
|
|
|
175
196
|
/** Render one column line (field name + Drizzle column expression). */
|
|
176
197
|
function renderColumn(
|
|
198
|
+
spec: ColumnSpec,
|
|
177
199
|
field: MetaField,
|
|
178
200
|
ctx: RenderContext,
|
|
179
201
|
isPk: boolean,
|
|
@@ -183,7 +205,6 @@ function renderColumn(
|
|
|
183
205
|
isUnique: boolean = false,
|
|
184
206
|
entityPackage: string | undefined = undefined,
|
|
185
207
|
): Code {
|
|
186
|
-
const spec = mapColumnType(field, ctx.dialect, ctx.columnNamingStrategy);
|
|
187
208
|
const fnSym = imp(`${spec.fnName}@${spec.importModule}`);
|
|
188
209
|
|
|
189
210
|
const dbNameLit = JSON.stringify(spec.dbName);
|
|
@@ -9,7 +9,7 @@ import { joinCode, type Code } from "ts-poet";
|
|
|
9
9
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
10
10
|
import type { RenderContext } from "../render-context.js";
|
|
11
11
|
import { renderDrizzleSchema } from "./drizzle-schema.js";
|
|
12
|
-
import { renderInferredTypes } from "./inferred-types.js";
|
|
12
|
+
import { renderInferredTypes, renderEnumTypeAliases } from "./inferred-types.js";
|
|
13
13
|
import { renderZodValidators } from "./zod-validators.js";
|
|
14
14
|
import { renderEntityConstants } from "./entity-constants.js";
|
|
15
15
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
@@ -20,6 +20,8 @@ import { renderProjectionDecl } from "./projection-decl.js";
|
|
|
20
20
|
|
|
21
21
|
export function renderEntityFile(entity: MetaObject, ctx: RenderContext): string {
|
|
22
22
|
// --- Projection path (read-only: view-backed entity with no table source) ---
|
|
23
|
+
// Projections intentionally get the z.enum() validator but NOT a named enum
|
|
24
|
+
// type alias — emitting aliases here is a deliberate v1 scope decision.
|
|
23
25
|
if (isProjection(entity)) {
|
|
24
26
|
return renderProjectionDecl(entity, ctx.loadedRoot, {
|
|
25
27
|
columnNamingStrategy: ctx.columnNamingStrategy,
|
|
@@ -29,9 +31,11 @@ export function renderEntityFile(entity: MetaObject, ctx: RenderContext): string
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// --- Vanilla / write-through entity path ---
|
|
34
|
+
const enumAliases = renderEnumTypeAliases(entity);
|
|
32
35
|
const sections: Code[] = [
|
|
33
36
|
renderDrizzleSchema(entity, ctx),
|
|
34
37
|
renderInferredTypes(entity),
|
|
38
|
+
...(enumAliases !== null ? [enumAliases] : []),
|
|
35
39
|
renderZodValidators(entity),
|
|
36
40
|
renderEntityConstants(entity, ctx.apiPrefix),
|
|
37
41
|
renderFilterAllowlist(entity),
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
FIELD_SUBTYPE_TIME,
|
|
19
19
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
20
20
|
FIELD_SUBTYPE_CURRENCY,
|
|
21
|
+
FIELD_SUBTYPE_ENUM,
|
|
21
22
|
VIEW_SUBTYPE_TEXT,
|
|
22
23
|
VIEW_SUBTYPE_DATE,
|
|
23
24
|
VIEW_SUBTYPE_NUMBER,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
VIEW_CURRENCY_ATTR_LOCALE,
|
|
29
30
|
VIEW_CURRENCY_ATTR_LOCALE_DEFAULT,
|
|
30
31
|
} from "@metaobjectsdev/metadata";
|
|
32
|
+
import { enumValues, zodEnumExpr } from "../enum-meta.js";
|
|
31
33
|
|
|
32
34
|
// ---------------------------------------------------------------------------
|
|
33
35
|
// inferViewKind
|
|
@@ -96,6 +98,10 @@ export function zodTypeFor(field: MetaField): string {
|
|
|
96
98
|
case FIELD_SUBTYPE_FLOAT:
|
|
97
99
|
case FIELD_SUBTYPE_DECIMAL:
|
|
98
100
|
return "z.number()";
|
|
101
|
+
case FIELD_SUBTYPE_ENUM: {
|
|
102
|
+
const values = enumValues(field);
|
|
103
|
+
return values !== undefined ? zodEnumExpr(values) : "z.string()";
|
|
104
|
+
}
|
|
99
105
|
default:
|
|
100
106
|
return "z.unknown()";
|
|
101
107
|
}
|
|
@@ -1,16 +1,55 @@
|
|
|
1
|
-
// Inferred types template — emits Drizzle's InferSelectModel / InferInsertModel type aliases
|
|
1
|
+
// Inferred types template — emits Drizzle's InferSelectModel / InferInsertModel type aliases,
|
|
2
|
+
// plus named union types for field.enum fields.
|
|
2
3
|
|
|
3
4
|
import { code, imp, type Code } from "ts-poet";
|
|
4
5
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
5
|
-
import {
|
|
6
|
+
import { FIELD_SUBTYPE_ENUM } from "@metaobjectsdev/metadata";
|
|
7
|
+
import { variableNameFromEntity, toPascalCase } from "../naming.js";
|
|
8
|
+
import { enumValues } from "../enum-meta.js";
|
|
9
|
+
import { renderDocsFor } from "./jsdoc.js";
|
|
6
10
|
|
|
7
11
|
export function renderInferredTypes(entity: MetaObject): Code {
|
|
8
12
|
const varName = variableNameFromEntity(entity.name);
|
|
9
13
|
const selectSym = imp("InferSelectModel@drizzle-orm");
|
|
10
14
|
const insertSym = imp("InferInsertModel@drizzle-orm");
|
|
15
|
+
const docs = renderDocsFor(entity);
|
|
16
|
+
const docsPrefix = docs ? `${docs}\n` : "";
|
|
11
17
|
return code`
|
|
12
|
-
export type ${entity.name} = ${selectSym}<typeof ${varName}>;
|
|
18
|
+
${docsPrefix}export type ${entity.name} = ${selectSym}<typeof ${varName}>;
|
|
13
19
|
export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
|
|
14
20
|
export type ${entity.name}Update = Partial<${entity.name}Insert>;
|
|
15
21
|
`;
|
|
16
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Emit one `export type <Name> = "A" | "B";` line per field.enum field on the entity.
|
|
26
|
+
* - If the field extends an abstract field.enum (super), use the super field's PascalCase name.
|
|
27
|
+
* - Otherwise use `<Entity><FieldPascal>` for inline enums.
|
|
28
|
+
* Returns null if the entity has no enum fields.
|
|
29
|
+
*/
|
|
30
|
+
export function renderEnumTypeAliases(entity: MetaObject): Code | null {
|
|
31
|
+
// De-duplicate by type-alias name — multiple fields can extend the same abstract enum.
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
|
|
35
|
+
for (const field of entity.fields()) {
|
|
36
|
+
if (field.subType !== FIELD_SUBTYPE_ENUM) continue;
|
|
37
|
+
|
|
38
|
+
const values = enumValues(field);
|
|
39
|
+
if (values === undefined) continue;
|
|
40
|
+
|
|
41
|
+
// Derive the type-alias name.
|
|
42
|
+
const superField = field.resolveSuper();
|
|
43
|
+
const typeName = superField !== undefined
|
|
44
|
+
? toPascalCase(superField.name)
|
|
45
|
+
: `${entity.name}${toPascalCase(field.name)}`;
|
|
46
|
+
|
|
47
|
+
if (seen.has(typeName)) continue;
|
|
48
|
+
seen.add(typeName);
|
|
49
|
+
|
|
50
|
+
const union = values.map((v) => JSON.stringify(v)).join(" | ");
|
|
51
|
+
lines.push(`export type ${typeName} = ${union};`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.length > 0 ? code`${lines.join("\n")}` : null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DOC_ATTR_ALIASES,
|
|
3
|
+
DOC_ATTR_DEPRECATED,
|
|
4
|
+
DOC_ATTR_DESCRIPTION,
|
|
5
|
+
DOC_ATTR_REPLACED_BY,
|
|
6
|
+
DOC_ATTR_SEE_ALSO,
|
|
7
|
+
DOC_ATTR_TITLE,
|
|
8
|
+
} from "@metaobjectsdev/metadata";
|
|
9
|
+
|
|
10
|
+
export interface DocAttrs {
|
|
11
|
+
description?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Internal-only; NEVER emitted by this helper (D5 contract). */
|
|
14
|
+
notes?: string;
|
|
15
|
+
deprecated?: string;
|
|
16
|
+
replacedBy?: string;
|
|
17
|
+
seeAlso?: string[];
|
|
18
|
+
aliases?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render the seven doc common attrs as a JSDoc block. Returns "" if no
|
|
23
|
+
* relevant attrs are set. `notes` is intentionally NEVER emitted — it is
|
|
24
|
+
* the internal-only rationale slot per the Documentation Provider design
|
|
25
|
+
* (D5).
|
|
26
|
+
*/
|
|
27
|
+
export function renderJsDocBlock(attrs: DocAttrs): string {
|
|
28
|
+
const bodyLines: string[] = [];
|
|
29
|
+
|
|
30
|
+
// Description (primary text), falling back to title-only if no description.
|
|
31
|
+
if (attrs.description) {
|
|
32
|
+
bodyLines.push(...attrs.description.split("\n"));
|
|
33
|
+
} else if (attrs.title) {
|
|
34
|
+
bodyLines.push(attrs.title);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Tags
|
|
38
|
+
const tagLines: string[] = [];
|
|
39
|
+
if (attrs.deprecated !== undefined) {
|
|
40
|
+
const replaced = attrs.replacedBy ? ` Replaced by ${attrs.replacedBy}.` : "";
|
|
41
|
+
tagLines.push(`@deprecated ${attrs.deprecated}${replaced}`);
|
|
42
|
+
}
|
|
43
|
+
for (const url of attrs.seeAlso ?? []) tagLines.push(`@see ${url}`);
|
|
44
|
+
for (const alias of attrs.aliases ?? []) tagLines.push(`@alias ${alias}`);
|
|
45
|
+
|
|
46
|
+
if (bodyLines.length === 0 && tagLines.length === 0) return "";
|
|
47
|
+
|
|
48
|
+
// One-line shorthand: single body line + no tags
|
|
49
|
+
if (bodyLines.length === 1 && tagLines.length === 0) {
|
|
50
|
+
return `/** ${bodyLines[0]} */`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const out: string[] = ["/**"];
|
|
54
|
+
for (const line of bodyLines) out.push(line === "" ? " *" : ` * ${line}`);
|
|
55
|
+
if (bodyLines.length > 0 && tagLines.length > 0) out.push(" *");
|
|
56
|
+
for (const line of tagLines) out.push(` * ${line}`);
|
|
57
|
+
out.push(" */");
|
|
58
|
+
return out.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read the seven doc attrs from a MetaData node's `.attrs()` (effective). */
|
|
62
|
+
export function readDocAttrs(node: { attr: (n: string) => unknown }): DocAttrs {
|
|
63
|
+
const str = (v: unknown): string | undefined =>
|
|
64
|
+
typeof v === "string" ? v : undefined;
|
|
65
|
+
const arr = (v: unknown): string[] | undefined =>
|
|
66
|
+
Array.isArray(v) && v.every((x) => typeof x === "string") ? (v as string[]) : undefined;
|
|
67
|
+
|
|
68
|
+
const description = str(node.attr(DOC_ATTR_DESCRIPTION));
|
|
69
|
+
const title = str(node.attr(DOC_ATTR_TITLE));
|
|
70
|
+
const deprecated = str(node.attr(DOC_ATTR_DEPRECATED));
|
|
71
|
+
const replacedBy = str(node.attr(DOC_ATTR_REPLACED_BY));
|
|
72
|
+
const seeAlso = arr(node.attr(DOC_ATTR_SEE_ALSO));
|
|
73
|
+
const aliases = arr(node.attr(DOC_ATTR_ALIASES));
|
|
74
|
+
// notes intentionally NOT read here — codegen consumers should never receive it
|
|
75
|
+
return {
|
|
76
|
+
...(description !== undefined && { description }),
|
|
77
|
+
...(title !== undefined && { title }),
|
|
78
|
+
...(deprecated !== undefined && { deprecated }),
|
|
79
|
+
...(replacedBy !== undefined && { replacedBy }),
|
|
80
|
+
...(seeAlso !== undefined && { seeAlso }),
|
|
81
|
+
...(aliases !== undefined && { aliases }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Convenience: `renderJsDocBlock(readDocAttrs(node))`. Returns "" if no doc attrs. */
|
|
86
|
+
export function renderDocsFor(node: { attr: (n: string) => unknown }): string {
|
|
87
|
+
return renderJsDocBlock(readDocAttrs(node));
|
|
88
|
+
}
|