@pylonts/dsl 1.0.5 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/asset.d.ts +48 -0
- package/dist/asset.js +31 -0
- package/dist/bases.d.ts +6 -2
- package/dist/bases.js +10 -6
- package/dist/check-inheritance.js +1 -4
- package/dist/curd.d.ts +60 -0
- package/dist/curd.js +31 -0
- package/dist/db-config.d.ts +8 -0
- package/dist/db-config.js +1 -0
- package/dist/db.d.ts +52 -0
- package/dist/db.js +91 -0
- package/dist/dictionary.d.ts +26 -5
- package/dist/dictionary.js +21 -8
- package/dist/dsl.d.ts +5 -49
- package/dist/dsl.js +12 -103
- package/dist/dto.d.ts +14 -9
- package/dist/dto.js +28 -38
- package/dist/enum-driver.d.ts +1 -1
- package/dist/enum-driver.js +1 -4
- package/dist/flow.d.ts +1 -1
- package/dist/flow.js +3 -8
- package/dist/import-base.d.ts +15 -0
- package/dist/import-base.js +1 -0
- package/dist/index.d.ts +21 -17
- package/dist/index.js +21 -33
- package/dist/mermaid-driver.d.ts +2 -2
- package/dist/mermaid-driver.js +2 -6
- package/dist/mock.d.ts +30 -0
- package/dist/mock.js +18 -0
- package/dist/mysql-driver.d.ts +1 -1
- package/dist/mysql-driver.js +20 -10
- package/dist/page-flow.d.ts +2 -2
- package/dist/page-flow.js +2 -6
- package/dist/page.d.ts +6 -6
- package/dist/page.js +3 -8
- package/dist/pattern.js +2 -6
- package/dist/patterns/retry.d.ts +1 -1
- package/dist/patterns/retry.js +2 -6
- package/dist/project.d.ts +18 -13
- package/dist/project.js +41 -6
- package/dist/prototype.d.ts +1 -1
- package/dist/prototype.js +1 -4
- package/dist/typebox-driver.d.ts +3 -3
- package/dist/typebox-driver.js +28 -24
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -4
- package/docs/curd.md +111 -0
- package/docs/dictionary.md +42 -31
- package/docs/driver.md +42 -42
- package/docs/dto.md +66 -66
- package/docs/enum.md +24 -24
- package/docs/project.md +2 -2
- package/docs/table.md +50 -15
- package/package.json +6 -4
- package/src/asset.ts +63 -0
- package/src/bases.ts +12 -2
- package/src/curd.ts +92 -0
- package/src/db-config.ts +8 -0
- package/src/db.ts +142 -0
- package/src/dictionary.ts +45 -19
- package/src/dsl.ts +182 -281
- package/src/dto.ts +247 -234
- package/src/enum-driver.ts +1 -1
- package/src/flow.ts +1 -1
- package/src/import-base.ts +15 -0
- package/src/index.ts +21 -17
- package/src/mermaid-driver.ts +3 -3
- package/src/mock.ts +45 -0
- package/src/mysql-driver.ts +19 -6
- package/src/page-flow.ts +3 -3
- package/src/page.ts +7 -7
- package/src/patterns/retry.ts +1 -1
- package/src/project.ts +90 -54
- package/src/prototype.ts +1 -1
- package/src/typebox-driver.ts +192 -183
- package/src/utils.ts +5 -0
- package/src/check-inheritance.ts +0 -86
package/src/typebox-driver.ts
CHANGED
|
@@ -1,184 +1,193 @@
|
|
|
1
|
-
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportRef } from './dto';
|
|
2
|
-
import { Field } from './dsl';
|
|
3
|
-
|
|
4
|
-
// TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
|
|
5
|
-
// Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
|
|
6
|
-
//
|
|
7
|
-
// export const RegisterUserInput = Type.Object({...});
|
|
8
|
-
// export type RegisterUserInput = Static<typeof RegisterUserInput>;
|
|
9
|
-
//
|
|
10
|
-
// ENUM fields reference a generated enum (see enum-driver) by its import
|
|
11
|
-
// location — the resolver maps a jsName to its product import.
|
|
12
|
-
// DTO bases (.include()) render as Type.Intersect([...bases, Type.Object({...})]).
|
|
13
|
-
|
|
14
|
-
export type EnumResolver = (enumName: string) =>
|
|
15
|
-
|
|
16
|
-
function renderString(s: string): string {
|
|
17
|
-
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderDefault(v: unknown): string {
|
|
21
|
-
if (typeof v === 'string') return renderString(v);
|
|
22
|
-
return JSON.stringify(v);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function renderBasic(
|
|
26
|
-
field: Field,
|
|
27
|
-
pattern: string | undefined,
|
|
28
|
-
defaultValue: unknown,
|
|
29
|
-
resolver: EnumResolver | undefined,
|
|
30
|
-
): string {
|
|
31
|
-
if (pattern !== undefined && field.type !== 'string') {
|
|
32
|
-
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
33
|
-
}
|
|
34
|
-
const def = defaultValue !== undefined ? `default: ${renderDefault(defaultValue)}` : undefined;
|
|
35
|
-
switch (field.type) {
|
|
36
|
-
case 'string': {
|
|
37
|
-
const opts: string[] = [];
|
|
38
|
-
if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
|
|
39
|
-
if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
|
|
40
|
-
if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
|
|
41
|
-
if (def !== undefined) opts.push(def);
|
|
42
|
-
return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
|
|
43
|
-
}
|
|
44
|
-
case 'text':
|
|
45
|
-
return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
|
|
46
|
-
case 'integer': {
|
|
47
|
-
const opts: string[] = [];
|
|
48
|
-
if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
|
|
49
|
-
if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
|
|
50
|
-
if (def !== undefined) opts.push(def);
|
|
51
|
-
return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
|
|
52
|
-
}
|
|
53
|
-
case 'bigint':
|
|
54
|
-
case 'decimal':
|
|
55
|
-
case 'time':
|
|
56
|
-
case 'date':
|
|
57
|
-
case 'datetime':
|
|
58
|
-
// Transmitted as string over HTTP: bigint/decimal keep full precision,
|
|
59
|
-
// date/time serialize to string.
|
|
60
|
-
return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
|
|
61
|
-
case 'boolean':
|
|
62
|
-
return def !== undefined ? `Type.Boolean({ ${def} })` : 'Type.Boolean()';
|
|
63
|
-
case 'json':
|
|
64
|
-
return def !== undefined ? `Type.Unknown({ ${def} })` : 'Type.Unknown()';
|
|
65
|
-
case 'enum': {
|
|
66
|
-
const ref = resolver?.(field.enum.jsName);
|
|
67
|
-
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
|
|
68
|
-
if (def !== undefined) {
|
|
69
|
-
const member = field.enum.values.find((v) => v.value === defaultValue);
|
|
70
|
-
if (!member) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`enum field ${field.name}: default ${renderDefault(defaultValue)} is not a member of ${field.enum.jsName}`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return `Type.Enum(${ref.name}, { default: ${ref.name}.${member.symbol} })`;
|
|
76
|
-
}
|
|
77
|
-
return `Type.Enum(${ref.name})`;
|
|
78
|
-
}
|
|
79
|
-
default:
|
|
80
|
-
// Field union is exhaustive; this branch is unreachable at runtime.
|
|
81
|
-
throw new Error(`unsupported field type: ${String((field as Field).type)}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function renderObject(fields: Record<string, DtoField>, indent: number, resolver: EnumResolver | undefined): string {
|
|
86
|
-
const pad = ' '.repeat(indent);
|
|
87
|
-
const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
|
|
88
|
-
return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
92
|
-
const base = renderValue(f, indent, resolver);
|
|
93
|
-
return f.isOptional() ? `Type.Optional(${base})` : base;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
97
|
-
if (f
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (f.field.type === '
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
schema
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'',
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef } from './dto.js';
|
|
2
|
+
import { Field } from './dsl.js';
|
|
3
|
+
|
|
4
|
+
// TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
|
|
5
|
+
// Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
|
|
6
|
+
//
|
|
7
|
+
// export const RegisterUserInput = Type.Object({...});
|
|
8
|
+
// export type RegisterUserInput = Static<typeof RegisterUserInput>;
|
|
9
|
+
//
|
|
10
|
+
// ENUM fields reference a generated enum (see enum-driver) by its import
|
|
11
|
+
// location — the resolver maps a jsName to its product import.
|
|
12
|
+
// DTO bases (.include()) render as Type.Intersect([...bases, Type.Object({...})]).
|
|
13
|
+
|
|
14
|
+
export type EnumResolver = (enumName: string) => ImportBase | undefined;
|
|
15
|
+
|
|
16
|
+
function renderString(s: string): string {
|
|
17
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderDefault(v: unknown): string {
|
|
21
|
+
if (typeof v === 'string') return renderString(v);
|
|
22
|
+
return JSON.stringify(v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderBasic(
|
|
26
|
+
field: Field,
|
|
27
|
+
pattern: string | undefined,
|
|
28
|
+
defaultValue: unknown,
|
|
29
|
+
resolver: EnumResolver | undefined,
|
|
30
|
+
): string {
|
|
31
|
+
if (pattern !== undefined && field.type !== 'string') {
|
|
32
|
+
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
33
|
+
}
|
|
34
|
+
const def = defaultValue !== undefined ? `default: ${renderDefault(defaultValue)}` : undefined;
|
|
35
|
+
switch (field.type) {
|
|
36
|
+
case 'string': {
|
|
37
|
+
const opts: string[] = [];
|
|
38
|
+
if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
|
|
39
|
+
if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
|
|
40
|
+
if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
|
|
41
|
+
if (def !== undefined) opts.push(def);
|
|
42
|
+
return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
|
|
43
|
+
}
|
|
44
|
+
case 'text':
|
|
45
|
+
return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
|
|
46
|
+
case 'integer': {
|
|
47
|
+
const opts: string[] = [];
|
|
48
|
+
if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
|
|
49
|
+
if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
|
|
50
|
+
if (def !== undefined) opts.push(def);
|
|
51
|
+
return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
|
|
52
|
+
}
|
|
53
|
+
case 'bigint':
|
|
54
|
+
case 'decimal':
|
|
55
|
+
case 'time':
|
|
56
|
+
case 'date':
|
|
57
|
+
case 'datetime':
|
|
58
|
+
// Transmitted as string over HTTP: bigint/decimal keep full precision,
|
|
59
|
+
// date/time serialize to string.
|
|
60
|
+
return def !== undefined ? `Type.String({ ${def} })` : 'Type.String()';
|
|
61
|
+
case 'boolean':
|
|
62
|
+
return def !== undefined ? `Type.Boolean({ ${def} })` : 'Type.Boolean()';
|
|
63
|
+
case 'json':
|
|
64
|
+
return def !== undefined ? `Type.Unknown({ ${def} })` : 'Type.Unknown()';
|
|
65
|
+
case 'enum': {
|
|
66
|
+
const ref = resolver?.(field.enum.jsName);
|
|
67
|
+
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
|
|
68
|
+
if (def !== undefined) {
|
|
69
|
+
const member = field.enum.values.find((v) => v.value === defaultValue);
|
|
70
|
+
if (!member) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`enum field ${field.name}: default ${renderDefault(defaultValue)} is not a member of ${field.enum.jsName}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return `Type.Enum(${ref.name}, { default: ${ref.name}.${member.symbol} })`;
|
|
76
|
+
}
|
|
77
|
+
return `Type.Enum(${ref.name})`;
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
// Field union is exhaustive; this branch is unreachable at runtime.
|
|
81
|
+
throw new Error(`unsupported field type: ${String((field as Field).type)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderObject(fields: Record<string, DtoField>, indent: number, resolver: EnumResolver | undefined): string {
|
|
86
|
+
const pad = ' '.repeat(indent);
|
|
87
|
+
const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
|
|
88
|
+
return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
92
|
+
const base = renderValue(f, indent, resolver);
|
|
93
|
+
return f.isOptional() ? `Type.Optional(${base})` : base;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
97
|
+
if (f.field.type === 'array') {
|
|
98
|
+
const items = f.field.items;
|
|
99
|
+
// Referenced DTO element — render by name (same-file export), not expanded.
|
|
100
|
+
if (isDtoMessage(items)) return `Type.Array(${items.name})`;
|
|
101
|
+
return `Type.Array(${renderField(items, indent + 1, resolver)})`;
|
|
102
|
+
}
|
|
103
|
+
if (f.field.type === 'object') {
|
|
104
|
+
return renderObject(f.field.properties, indent + 1, resolver);
|
|
105
|
+
}
|
|
106
|
+
// DtoField only wraps a database Field; array/object defs live in the subclasses.
|
|
107
|
+
// Only DTO-level defaults (setDefault) are emitted as TypeBox default
|
|
108
|
+
// annotations; DB field defaults are not carried into the API contract.
|
|
109
|
+
return renderBasic(f.field as Field, f.pattern, f.default, resolver);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
|
|
113
|
+
function isDtoMessage(v: unknown): v is DtoMessage {
|
|
114
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
115
|
+
return (v as Record<string, unknown>).type === 'dto';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectEnumImports(
|
|
119
|
+
f: DtoField,
|
|
120
|
+
resolver: EnumResolver | undefined,
|
|
121
|
+
out: Map<string, ImportBase>,
|
|
122
|
+
): void {
|
|
123
|
+
if (f.field.type === 'array') {
|
|
124
|
+
const items = f.field.items;
|
|
125
|
+
if (!isDtoMessage(items)) collectEnumImports(items, resolver, out);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (f.field.type === 'object') {
|
|
129
|
+
for (const child of Object.values(f.field.properties)) collectEnumImports(child, resolver, out);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (f.field.type === 'enum') {
|
|
133
|
+
const ref = resolver?.(f.field.enum.jsName);
|
|
134
|
+
if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
|
|
135
|
+
out.set(`${ref.from}#${ref.name}`, ref);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Collect all imports needed to render a DTO: include() bases + enum references. */
|
|
140
|
+
export function collectDtoImports(
|
|
141
|
+
schema: DtoMessage,
|
|
142
|
+
resolver: EnumResolver | undefined,
|
|
143
|
+
out: Map<string, ImportBase>,
|
|
144
|
+
): void {
|
|
145
|
+
for (const base of schema.bases ?? []) out.set(`${base.from}#${base.name}`, base);
|
|
146
|
+
for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, out);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Render one DTO export (const + type) — no file header, for file-level generation. */
|
|
150
|
+
export function renderDtoExport(schema: DtoMessage, resolver: EnumResolver | undefined): string {
|
|
151
|
+
const object = renderObject(schema.fields, 1, resolver);
|
|
152
|
+
const bases = schema.bases ?? [];
|
|
153
|
+
const body =
|
|
154
|
+
bases.length > 0
|
|
155
|
+
? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
|
|
156
|
+
: object;
|
|
157
|
+
return `export const ${schema.name} = ${body};`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Render the Static type export for a DTO. */
|
|
161
|
+
export function renderDtoTypeExport(name: string): string {
|
|
162
|
+
return `export type ${name} = Static<typeof ${name}>;`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function renderDtoMessage(
|
|
166
|
+
schema: DtoMessage,
|
|
167
|
+
options: { resolver?: EnumResolver; source?: string } = {},
|
|
168
|
+
): string {
|
|
169
|
+
const { resolver, source } = options;
|
|
170
|
+
const imports = new Map<string, ImportBase>();
|
|
171
|
+
collectDtoImports(schema, resolver, imports);
|
|
172
|
+
|
|
173
|
+
const header = [
|
|
174
|
+
'// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
|
|
175
|
+
...(source !== undefined ? [`// Source: ${source}`] : []),
|
|
176
|
+
"import { Type, Static } from '@sinclair/typebox';",
|
|
177
|
+
...[...imports.values()].map((r) => `import${r.type ? ' type' : ''} { ${r.name} } from '${r.from}';`),
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
return [
|
|
181
|
+
...header,
|
|
182
|
+
'',
|
|
183
|
+
renderDtoExport(schema, resolver),
|
|
184
|
+
renderDtoTypeExport(schema.name),
|
|
185
|
+
'',
|
|
186
|
+
].join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderBase(base: ImportRef): string {
|
|
190
|
+
if (base.args === undefined || base.args.length === 0) return base.name;
|
|
191
|
+
const args = base.args.map((a) => (typeof a === 'string' ? a : a.name));
|
|
192
|
+
return `${base.name}(${args.join(', ')})`;
|
|
184
193
|
}
|
package/src/utils.ts
CHANGED
|
@@ -4,3 +4,8 @@
|
|
|
4
4
|
export function toCamelCase(name: string): string {
|
|
5
5
|
return name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
/** snake_case → PascalCase: mer_id → MerId */
|
|
9
|
+
export function toPascalCase(snake: string): string {
|
|
10
|
+
return snake.replace(/(^|_)([a-z])/g, (_m, _p, c: string) => c.toUpperCase());
|
|
11
|
+
}
|
package/src/check-inheritance.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { DtoMessage } from './dto';
|
|
2
|
-
import type { TableSchema } from './dsl';
|
|
3
|
-
|
|
4
|
-
// Inheritance check — find DTO fields that should inherit from a DB column
|
|
5
|
-
// (via from()) but were written by hand, so they miss the column's
|
|
6
|
-
// type / semantic / optionality backfill.
|
|
7
|
-
//
|
|
8
|
-
// Inference: the tables a DSL file is about are inferred from the referenced
|
|
9
|
-
// fields in the same file (each Field carries schema identity via .schema).
|
|
10
|
-
// A field without a table ref whose name matches a column of an inferred table
|
|
11
|
-
// is a candidate for inheritance. Fields with an explicit semantic or enum type
|
|
12
|
-
// are treated as author-intent and skipped.
|
|
13
|
-
//
|
|
14
|
-
// False-positive boundary: when a DSL file has zero referenced fields, no table
|
|
15
|
-
// can be inferred and nothing is reported (never guess which table a hand-written
|
|
16
|
-
// field belongs to).
|
|
17
|
-
|
|
18
|
-
export interface InheritanceIssue {
|
|
19
|
-
file: string;
|
|
20
|
-
container: string;
|
|
21
|
-
field: string;
|
|
22
|
-
/** e.g. ["t_order.order_no"] or ["t_order.id", "t_merchant.id"] when several inferred tables share the name */
|
|
23
|
-
candidates: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** snake_case → camelCase: order_no → orderNo; names without underscores are unchanged */
|
|
27
|
-
function toCamelCase(name: string): string {
|
|
28
|
-
return name.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isDtoMessage(v: unknown): v is DtoMessage {
|
|
32
|
-
if (typeof v !== 'object' || v === null) return false;
|
|
33
|
-
const o = v as Record<string, unknown>;
|
|
34
|
-
return o.type === 'dto' && typeof o.name === 'string' && typeof o.fields === 'object' && o.fields !== null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface FieldEntry {
|
|
38
|
-
name: string;
|
|
39
|
-
field: { schema?: { type?: string }; semantic?: string; type?: string };
|
|
40
|
-
container: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function collectFields(mod: Record<string, unknown>): FieldEntry[] {
|
|
44
|
-
const out: FieldEntry[] = [];
|
|
45
|
-
for (const [name, v] of Object.entries(mod)) {
|
|
46
|
-
if (isDtoMessage(v)) {
|
|
47
|
-
const container = v as { fields: Record<string, { field: FieldEntry['field'] }> };
|
|
48
|
-
for (const [fname, f] of Object.entries(container.fields)) {
|
|
49
|
-
out.push({ name: fname, field: f.field, container: name });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return out;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Check one loaded DSL module (its exports) for inheritance gaps. */
|
|
57
|
-
export function checkInheritance(mod: Record<string, unknown>, file: string): InheritanceIssue[] {
|
|
58
|
-
const fields = collectFields(mod);
|
|
59
|
-
if (fields.length === 0) return [];
|
|
60
|
-
|
|
61
|
-
// Infer the tables this file is about from the referenced fields' schema identity.
|
|
62
|
-
const tables = new Map<string, Map<string, string>>(); // table -> camelCol -> origCol
|
|
63
|
-
for (const f of fields) {
|
|
64
|
-
const schema = f.field.schema;
|
|
65
|
-
if (schema?.type !== 'table') continue;
|
|
66
|
-
const table = schema as TableSchema;
|
|
67
|
-
if (!tables.has(table.name)) tables.set(table.name, new Map());
|
|
68
|
-
tables.get(table.name)!.set(toCamelCase(f.name), f.name);
|
|
69
|
-
}
|
|
70
|
-
if (tables.size === 0) return [];
|
|
71
|
-
|
|
72
|
-
const issues: InheritanceIssue[] = [];
|
|
73
|
-
for (const f of fields) {
|
|
74
|
-
if (f.field.schema?.type === 'table') continue;
|
|
75
|
-
if (f.field.semantic !== undefined || f.field.type === 'enum') continue;
|
|
76
|
-
const candidates: string[] = [];
|
|
77
|
-
for (const [t, cols] of tables) {
|
|
78
|
-
const orig = cols.get(f.name);
|
|
79
|
-
if (orig !== undefined) candidates.push(`${t}.${orig}`);
|
|
80
|
-
}
|
|
81
|
-
if (candidates.length > 0) {
|
|
82
|
-
issues.push({ file, container: f.container, field: f.name, candidates });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return issues;
|
|
86
|
-
}
|