sizuku 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +541 -1323
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -1,361 +1,73 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import fsp from "node:fs/promises";
|
|
3
1
|
import path, { dirname } from "node:path";
|
|
4
|
-
import { format } from "oxfmt";
|
|
5
2
|
import { Node, Project } from "ts-morph";
|
|
3
|
+
import { format } from "oxfmt";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import fsp from "node:fs/promises";
|
|
6
6
|
import { Resvg } from "@resvg/resvg-js";
|
|
7
7
|
import { run } from "@softwaretechnik/dbml-renderer";
|
|
8
|
-
|
|
9
|
-
//#region src/fsp/index.ts
|
|
10
|
-
function readFileSync(path) {
|
|
11
|
-
try {
|
|
12
|
-
return {
|
|
13
|
-
ok: true,
|
|
14
|
-
value: fs.readFileSync(path, "utf-8")
|
|
15
|
-
};
|
|
16
|
-
} catch (e) {
|
|
17
|
-
return {
|
|
18
|
-
ok: false,
|
|
19
|
-
error: e instanceof Error ? e.message : String(e)
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Creates a directory if it does not already exist.
|
|
25
|
-
*
|
|
26
|
-
* @param dir - Directory path to create.
|
|
27
|
-
* @returns A `Result` that is `ok` on success, otherwise an error message.
|
|
28
|
-
*/
|
|
29
|
-
async function mkdir(dir) {
|
|
30
|
-
try {
|
|
31
|
-
await fsp.mkdir(dir, { recursive: true });
|
|
32
|
-
return {
|
|
33
|
-
ok: true,
|
|
34
|
-
value: void 0
|
|
35
|
-
};
|
|
36
|
-
} catch (e) {
|
|
37
|
-
return {
|
|
38
|
-
ok: false,
|
|
39
|
-
error: e instanceof Error ? e.message : String(e)
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Writes UTF-8 text to a file, creating it if necessary.
|
|
45
|
-
*
|
|
46
|
-
* @param path - File path to write.
|
|
47
|
-
* @param data - Text data to write.
|
|
48
|
-
* @returns A `Result` that is `ok` on success, otherwise an error message.
|
|
49
|
-
*/
|
|
50
|
-
async function writeFile(path, data) {
|
|
51
|
-
try {
|
|
52
|
-
await fsp.writeFile(path, data, "utf-8");
|
|
53
|
-
return {
|
|
54
|
-
ok: true,
|
|
55
|
-
value: void 0
|
|
56
|
-
};
|
|
57
|
-
} catch (e) {
|
|
58
|
-
return {
|
|
59
|
-
ok: false,
|
|
60
|
-
error: e instanceof Error ? e.message : String(e)
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Writes binary data to a file, creating it if necessary.
|
|
66
|
-
*
|
|
67
|
-
* @param path - File path to write.
|
|
68
|
-
* @param data - Binary data to write.
|
|
69
|
-
* @returns A `Result` that is `ok` on success, otherwise an error message.
|
|
70
|
-
*/
|
|
71
|
-
async function writeFileBinary(path, data) {
|
|
72
|
-
try {
|
|
73
|
-
await fsp.writeFile(path, data);
|
|
74
|
-
return {
|
|
75
|
-
ok: true,
|
|
76
|
-
value: void 0
|
|
77
|
-
};
|
|
78
|
-
} catch (e) {
|
|
79
|
-
return {
|
|
80
|
-
ok: false,
|
|
81
|
-
error: e instanceof Error ? e.message : String(e)
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
//#endregion
|
|
87
|
-
//#region src/format/index.ts
|
|
88
|
-
/**
|
|
89
|
-
* Format TypeScript code using oxfmt
|
|
90
|
-
* @param input - The code to format
|
|
91
|
-
* @returns The formatted code, or throws on format errors (I/O boundary)
|
|
92
|
-
*/
|
|
93
|
-
async function fmt(input) {
|
|
94
|
-
const { code, errors } = await format("<stdin>.ts", input, {
|
|
95
|
-
printWidth: 100,
|
|
96
|
-
singleQuote: true,
|
|
97
|
-
semi: false
|
|
98
|
-
});
|
|
99
|
-
if (errors.length > 0) throw new Error(errors.map((e) => e.message).join("\n"));
|
|
100
|
-
return code;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
//#endregion
|
|
104
8
|
//#region src/utils/index.ts
|
|
105
|
-
/**
|
|
106
|
-
* Capitalize the first character of a string.
|
|
107
|
-
*
|
|
108
|
-
* @param str - The input string.
|
|
109
|
-
* @returns String with first character capitalized.
|
|
110
|
-
*/
|
|
111
9
|
function makeCapitalized(str) {
|
|
112
10
|
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
|
|
113
11
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
12
|
+
function stripImports(content) {
|
|
13
|
+
const lines = content.split("\n");
|
|
14
|
+
const codeStart = lines.findIndex((line) => !line.trim().startsWith("import") && line.trim() !== "");
|
|
15
|
+
return lines.slice(codeStart);
|
|
16
|
+
}
|
|
117
17
|
function resolveWrapperType(objectType) {
|
|
118
18
|
if (objectType === "strict") return "strictObject";
|
|
119
19
|
if (objectType === "loose") return "looseObject";
|
|
120
20
|
return "object";
|
|
121
21
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Resolve ArkType undeclared key handling prefix.
|
|
124
|
-
*
|
|
125
|
-
* ArkType uses `"+"` key to control unknown property behavior:
|
|
126
|
-
* - strict → `"+": "reject"` (reject unknown keys)
|
|
127
|
-
* - loose → `"+": "ignore"` (preserve unknown keys, which is ArkType's default)
|
|
128
|
-
* - undefined → no prefix (default behavior: preserve unknown keys)
|
|
129
|
-
*/
|
|
130
|
-
function resolveArktypeUndeclared(objectType) {
|
|
131
|
-
if (objectType === "strict") return "\"+\":\"reject\",";
|
|
132
|
-
if (objectType === "loose") return "\"+\":\"ignore\",";
|
|
133
|
-
return "";
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Join relation schema fields into a comma-separated string.
|
|
137
|
-
*/
|
|
138
22
|
function makeRelationFields(fields) {
|
|
139
23
|
return fields.map((f) => `${f.name}:${f.definition}`).join(",");
|
|
140
24
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Generates a Zod object wrapper.
|
|
143
|
-
*
|
|
144
|
-
* @param inner - The inner field definitions string.
|
|
145
|
-
* @param wrapperType - The object wrapper type.
|
|
146
|
-
* @returns The generated Zod object string.
|
|
147
|
-
*/
|
|
148
|
-
function makeZodObject(inner, wrapperType = "object") {
|
|
149
|
-
switch (wrapperType) {
|
|
150
|
-
case "strictObject": return `z.strictObject({${inner}})`;
|
|
151
|
-
case "looseObject": return `z.looseObject({${inner}})`;
|
|
152
|
-
default: return `z.object({${inner}})`;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Generates a Valibot object wrapper.
|
|
157
|
-
*
|
|
158
|
-
* @param inner - The inner field definitions string.
|
|
159
|
-
* @param wrapperType - The object wrapper type.
|
|
160
|
-
* @returns The generated Valibot object string.
|
|
161
|
-
*/
|
|
162
|
-
function makeValibotObject(inner, wrapperType = "object") {
|
|
163
|
-
switch (wrapperType) {
|
|
164
|
-
case "strictObject": return `v.strictObject({${inner}})`;
|
|
165
|
-
case "looseObject": return `v.looseObject({${inner}})`;
|
|
166
|
-
default: return `v.object({${inner}})`;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Check if a string starts with a prefix.
|
|
171
|
-
*
|
|
172
|
-
* @param str - The input string.
|
|
173
|
-
* @param prefix - The prefix to check.
|
|
174
|
-
* @returns True if string starts with prefix.
|
|
175
|
-
*/
|
|
176
|
-
function startsWith(str, prefix) {
|
|
177
|
-
return str.indexOf(prefix) === 0;
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Trim whitespace from string.
|
|
181
|
-
*
|
|
182
|
-
* @param str - The input string.
|
|
183
|
-
* @returns Trimmed string.
|
|
184
|
-
*/
|
|
185
|
-
function trimString(str) {
|
|
186
|
-
return str.trim();
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Clean comment lines by removing triple slash prefix and trimming.
|
|
190
|
-
*
|
|
191
|
-
* @param commentLines - Raw comment lines
|
|
192
|
-
* @returns Cleaned comment lines
|
|
193
|
-
*/
|
|
194
25
|
function cleanCommentLines(commentLines) {
|
|
195
26
|
return commentLines.map((line) => line.replace(/^\/\/\/\s*/, "").trim()).filter(Boolean);
|
|
196
27
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
*
|
|
200
|
-
* @param cleaned - Cleaned comment lines
|
|
201
|
-
* @param tag - The tag to look for
|
|
202
|
-
* @returns The object type if found
|
|
203
|
-
*/
|
|
204
|
-
function extractObjectType(cleaned, tag) {
|
|
28
|
+
function parseFieldComments(commentLines, tag) {
|
|
29
|
+
const cleaned = cleanCommentLines(commentLines);
|
|
205
30
|
const tagWithoutAt = tag.slice(1);
|
|
206
31
|
const objectTypeLine = cleaned.find((line) => line.includes(`${tagWithoutAt}strictObject`) || line.includes(`${tagWithoutAt}looseObject`));
|
|
207
|
-
|
|
208
|
-
if (objectTypeLine.includes("strictObject")) return "strict";
|
|
209
|
-
if (objectTypeLine.includes("looseObject")) return "loose";
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Extract definition from comment lines.
|
|
213
|
-
*
|
|
214
|
-
* @param cleaned - Cleaned comment lines
|
|
215
|
-
* @param tag - The tag to look for
|
|
216
|
-
* @returns The definition string
|
|
217
|
-
*/
|
|
218
|
-
function extractDefinition(cleaned, tag) {
|
|
32
|
+
const objectType = !objectTypeLine ? void 0 : objectTypeLine.includes("strictObject") ? "strict" : "loose";
|
|
219
33
|
const definitionLine = cleaned.find((line) => line.startsWith(tag) && !line.includes("strictObject") && !line.includes("looseObject"));
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
*
|
|
231
|
-
* @param cleaned - Cleaned comment lines
|
|
232
|
-
* @returns The description if found
|
|
233
|
-
*/
|
|
234
|
-
function extractDescription(cleaned) {
|
|
235
|
-
const descriptionLines = cleaned.filter((line) => !(line.includes("@z.") || line.includes("@v.") || line.includes("@a.") || line.includes("@e.") || line.includes("@relation.")));
|
|
236
|
-
return descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Parse field comments and extract definition line and description.
|
|
240
|
-
*
|
|
241
|
-
* @param commentLines - Raw comment lines (e.g., from source text)
|
|
242
|
-
* @param tag - The tag to look for (e.g., '@v.', '@z.', '@a.', or '@e.')
|
|
243
|
-
* @returns Parsed definition and description
|
|
244
|
-
*/
|
|
245
|
-
function parseFieldComments(commentLines, tag) {
|
|
246
|
-
const cleaned = cleanCommentLines(commentLines);
|
|
247
|
-
const objectType = extractObjectType(cleaned, tag);
|
|
34
|
+
const definition = (() => {
|
|
35
|
+
if (!definitionLine) return "";
|
|
36
|
+
const withoutAt = definitionLine.startsWith("@") ? definitionLine.substring(1) : definitionLine;
|
|
37
|
+
if (tag === "@a." || tag === "@e.") {
|
|
38
|
+
const prefix = tag.substring(1);
|
|
39
|
+
return withoutAt.startsWith(prefix) ? withoutAt.substring(prefix.length) : withoutAt;
|
|
40
|
+
}
|
|
41
|
+
return withoutAt;
|
|
42
|
+
})();
|
|
43
|
+
const descriptionLines = cleaned.filter((line) => !line.includes("@z.") && !line.includes("@v.") && !line.includes("@a.") && !line.includes("@e.") && !line.includes("@relation."));
|
|
248
44
|
return {
|
|
249
|
-
definition
|
|
250
|
-
description:
|
|
45
|
+
definition,
|
|
46
|
+
description: descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0,
|
|
251
47
|
objectType
|
|
252
48
|
};
|
|
253
49
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Process a single line during comment extraction.
|
|
256
|
-
*
|
|
257
|
-
* @param acc - The accumulator
|
|
258
|
-
* @param line - The line to process
|
|
259
|
-
* @returns Updated accumulator
|
|
260
|
-
*/
|
|
261
|
-
function processCommentLine(acc, line) {
|
|
262
|
-
if (acc.shouldStop) return acc;
|
|
263
|
-
if (line.startsWith("///")) return {
|
|
264
|
-
commentLines: [line, ...acc.commentLines],
|
|
265
|
-
shouldStop: false
|
|
266
|
-
};
|
|
267
|
-
if (line === "") return acc;
|
|
268
|
-
return {
|
|
269
|
-
commentLines: acc.commentLines,
|
|
270
|
-
shouldStop: true
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Extract field comments from source text.
|
|
275
|
-
*
|
|
276
|
-
* @param sourceText - The source text to extract comments from.
|
|
277
|
-
* @param fieldStartPos - The position of the field in the source text.
|
|
278
|
-
* @returns An array of comment lines.
|
|
279
|
-
*/
|
|
280
50
|
function extractFieldComments(sourceText, fieldStartPos) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
})).reverse().reduce((acc, { line }) => processCommentLine(acc, line), {
|
|
285
|
-
commentLines: [],
|
|
286
|
-
shouldStop: false
|
|
287
|
-
}).commentLines;
|
|
51
|
+
const reversed = sourceText.substring(0, fieldStartPos).split("\n").map((l) => l.trim()).reverse();
|
|
52
|
+
const stopIdx = reversed.findIndex((l) => l !== "" && !l.startsWith("///"));
|
|
53
|
+
return (stopIdx === -1 ? reversed : reversed.slice(0, stopIdx)).filter((l) => l.startsWith("///")).reverse();
|
|
288
54
|
}
|
|
289
|
-
/**
|
|
290
|
-
* Creates `z.infer` type for the specified model.
|
|
291
|
-
*
|
|
292
|
-
* @param name - The model name
|
|
293
|
-
* @returns The generated TypeScript type definition line using Zod.
|
|
294
|
-
*/
|
|
295
|
-
function infer(name) {
|
|
296
|
-
const modelName = makeCapitalized(name);
|
|
297
|
-
return `export type ${modelName} = z.infer<typeof ${modelName}Schema>`;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Creates `v.InferOutput` type for the specified model.
|
|
301
|
-
*
|
|
302
|
-
* @param name - The model name
|
|
303
|
-
* @returns The generated TypeScript type definition line using Valibot.
|
|
304
|
-
*/
|
|
305
|
-
function inferOutput(name) {
|
|
306
|
-
const modelName = makeCapitalized(name);
|
|
307
|
-
return `export type ${modelName} = v.InferOutput<typeof ${modelName}Schema>`;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Generate a JSDoc comment block from a description
|
|
311
|
-
* @param description - The description text
|
|
312
|
-
* @returns The formatted JSDoc comment block
|
|
313
|
-
*/
|
|
314
|
-
function makeCommentBlock(description) {
|
|
315
|
-
if (!description) return "";
|
|
316
|
-
return `/**\n * ${description}\n */\n`;
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Generate field definitions with optional JSDoc comments
|
|
320
|
-
* @param schema - The schema with fields to generate definitions for
|
|
321
|
-
* @param comment - Whether to include JSDoc comments
|
|
322
|
-
*/
|
|
323
55
|
function fieldDefinitions(schema, comment) {
|
|
324
56
|
return schema.fields.map(({ name, definition, description }) => {
|
|
325
|
-
return `${description && comment ?
|
|
57
|
+
return `${description && comment ? `/**\n * ${description}\n */\n` : ""}${name}:${definition}`;
|
|
326
58
|
}).join(",\n");
|
|
327
59
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Creates ArkType infer type for the specified model.
|
|
330
|
-
*
|
|
331
|
-
* @param name - The model name
|
|
332
|
-
* @returns The generated TypeScript type definition line using ArkType.
|
|
333
|
-
*/
|
|
334
|
-
function inferArktype(name) {
|
|
335
|
-
const capitalized = makeCapitalized(name);
|
|
336
|
-
return `export type ${capitalized} = typeof ${capitalized}Schema.infer`;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Creates Effect Schema infer type for the specified model.
|
|
340
|
-
*
|
|
341
|
-
* @param name - The model name
|
|
342
|
-
* @returns The generated TypeScript type definition line using Effect Schema.
|
|
343
|
-
*/
|
|
344
|
-
function inferEffect(name) {
|
|
345
|
-
const capitalized = makeCapitalized(name);
|
|
346
|
-
return `export type ${capitalized}Encoded = typeof ${capitalized}Schema.Encoded`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
60
|
//#endregion
|
|
350
61
|
//#region src/helper/extract-schemas.ts
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
62
|
+
function makeSourceFile(sourceCode) {
|
|
63
|
+
return new Project({
|
|
64
|
+
useInMemoryFileSystem: true,
|
|
65
|
+
compilerOptions: {
|
|
66
|
+
allowJs: true,
|
|
67
|
+
skipLibCheck: true
|
|
68
|
+
}
|
|
69
|
+
}).createSourceFile("temp.ts", sourceCode);
|
|
70
|
+
}
|
|
359
71
|
function generateRelationDefinition(fnName, refTable, prefix) {
|
|
360
72
|
const schema = `${makeCapitalized(refTable)}Schema`;
|
|
361
73
|
if (fnName === "many") {
|
|
@@ -366,471 +78,196 @@ function generateRelationDefinition(fnName, refTable, prefix) {
|
|
|
366
78
|
if (fnName === "one") return schema;
|
|
367
79
|
return "";
|
|
368
80
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Processes arrow function body to find object literal expression.
|
|
371
|
-
*
|
|
372
|
-
* @param body - The arrow function body node
|
|
373
|
-
* @returns The found object literal expression, or null if not found
|
|
374
|
-
*/
|
|
375
|
-
function processArrowFunctionBody(body) {
|
|
376
|
-
if (Node.isObjectLiteralExpression(body)) return body;
|
|
377
|
-
if (Node.isParenthesizedExpression(body)) return findObjectLiteralExpression(body.getExpression());
|
|
378
|
-
if (Node.isBlock(body)) {
|
|
379
|
-
const ret = body.getStatements().find(Node.isReturnStatement);
|
|
380
|
-
if (ret && Node.isReturnStatement(ret)) {
|
|
381
|
-
const re = ret.getExpression();
|
|
382
|
-
return re && Node.isObjectLiteralExpression(re) ? re : null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Recursively extracts an `ObjectLiteralExpression` from a given AST node.
|
|
389
|
-
*
|
|
390
|
-
* @param expr - The root `Node` to search for object literals
|
|
391
|
-
* @returns The found `ObjectLiteralExpression`, or `null` if not found
|
|
392
|
-
*/
|
|
393
81
|
function findObjectLiteralExpression(expr) {
|
|
394
82
|
if (Node.isObjectLiteralExpression(expr)) return expr;
|
|
395
83
|
if (Node.isParenthesizedExpression(expr)) return findObjectLiteralExpression(expr.getExpression());
|
|
396
|
-
if (Node.isArrowFunction(expr))
|
|
84
|
+
if (Node.isArrowFunction(expr)) {
|
|
85
|
+
const body = expr.getBody();
|
|
86
|
+
if (Node.isObjectLiteralExpression(body)) return body;
|
|
87
|
+
if (Node.isParenthesizedExpression(body)) return findObjectLiteralExpression(body.getExpression());
|
|
88
|
+
if (Node.isBlock(body)) {
|
|
89
|
+
const re = body.getStatements().find(Node.isReturnStatement)?.getExpression();
|
|
90
|
+
return re && Node.isObjectLiteralExpression(re) ? re : null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
397
93
|
return null;
|
|
398
94
|
}
|
|
399
|
-
|
|
400
|
-
* Finds an object literal expression in call expression arguments.
|
|
401
|
-
*
|
|
402
|
-
* @param call - The call expression to search for object literals in its arguments
|
|
403
|
-
* @param finder - Function to find object literal in a node
|
|
404
|
-
* @returns The found object literal, or `null` if not found in any argument
|
|
405
|
-
*/
|
|
406
|
-
function findObjectLiteralInArgs(call, finder) {
|
|
95
|
+
function findObjectLiteralInArgs(call) {
|
|
407
96
|
for (const arg of call.getArguments()) {
|
|
408
|
-
const obj =
|
|
97
|
+
const obj = findObjectLiteralExpression(arg);
|
|
409
98
|
if (obj) return obj;
|
|
410
99
|
}
|
|
411
100
|
return null;
|
|
412
101
|
}
|
|
413
|
-
/**
|
|
414
|
-
* Determines whether a given `CallExpression` is a relation-related function call.
|
|
415
|
-
*
|
|
416
|
-
* @param callExpr - The call expression node to check for relation functions
|
|
417
|
-
* @returns `true` if the function is a relation function; otherwise, `false`
|
|
418
|
-
*/
|
|
419
102
|
function isRelationFunctionCall(callExpr) {
|
|
420
103
|
const expression = callExpr.getExpression();
|
|
421
104
|
if (!Node.isIdentifier(expression)) return false;
|
|
422
|
-
const
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
* @param extractRelationFieldFromProperty - Function to extract relation fields
|
|
491
|
-
* @param sourceText - The source text for comment extraction
|
|
492
|
-
* @returns Array of extracted field results
|
|
493
|
-
*/
|
|
494
|
-
function extractFieldsFromProperties(properties, isRelation, extractFieldFromProperty, extractRelationFieldFromProperty, sourceText) {
|
|
495
|
-
return properties.map((prop) => isRelation ? extractRelationFieldFromProperty(prop, sourceText) : extractFieldFromProperty(prop, sourceText)).filter((field) => field !== null);
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Creates a field extractor for call expressions with customizable strategies.
|
|
499
|
-
*
|
|
500
|
-
* @param extractFieldFromProperty - Function to extract field from property
|
|
501
|
-
* @param extractRelationFieldFromProperty - Function to extract relation field from property
|
|
502
|
-
* @param findObjectLiteralExpression - Function to find object literal expression
|
|
503
|
-
* @param findObjectLiteralInArgs - Function to find object literal in call arguments
|
|
504
|
-
* @param isRelationFunctionCall - Function to check if call is relation function
|
|
505
|
-
* @returns Function that extracts fields from call expression
|
|
506
|
-
*/
|
|
507
|
-
function createExtractFieldsFromCallExpression(extractFieldFromProperty, extractRelationFieldFromProperty, findObjectLiteralExpression, findObjectLiteralInArgs, isRelationFunctionCall) {
|
|
508
|
-
return (callExpr, sourceText) => {
|
|
509
|
-
const objectLiteral = findObjectLiteralInArgs(callExpr, findObjectLiteralExpression);
|
|
510
|
-
if (!objectLiteral) return [];
|
|
511
|
-
const isRelation = isRelationFunctionCall(callExpr);
|
|
512
|
-
return extractFieldsFromProperties(objectLiteral.getProperties(), isRelation, extractFieldFromProperty, extractRelationFieldFromProperty, sourceText);
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Creates a schema extractor from customizable strategies.
|
|
517
|
-
*
|
|
518
|
-
* @param extractFieldsFromCall - Function to extract fields from a call expression
|
|
519
|
-
* @param extractFieldFromProperty - Function to extract a single field from an object literal property
|
|
520
|
-
* @param parseFieldComments - Function to parse field comments with object type support
|
|
521
|
-
* @param commentPrefix - The comment prefix to use for parsing
|
|
522
|
-
* @returns A function that extracts a schema from a variable declaration node
|
|
523
|
-
*/
|
|
524
|
-
function makeSchemaExtractor(extractFieldsFromCall, extractFieldFromProperty, parseFieldComments, commentPrefix) {
|
|
525
|
-
return (variableStatement, sourceText, originalSourceCode) => {
|
|
526
|
-
if (!Node.isVariableStatement(variableStatement)) return null;
|
|
527
|
-
const declarations = variableStatement.getDeclarations();
|
|
528
|
-
if (declarations.length === 0) return null;
|
|
529
|
-
const declaration = declarations[0];
|
|
530
|
-
const name = declaration.getName();
|
|
531
|
-
if (!name) return null;
|
|
532
|
-
const statementStart = variableStatement.getStart();
|
|
533
|
-
const originalSourceLines = originalSourceCode.split("\n");
|
|
534
|
-
const commentLines = [];
|
|
535
|
-
const lineNumber = originalSourceLines.reduce((state, line, i) => state.found ? state : state.acc >= statementStart ? {
|
|
536
|
-
acc: state.acc,
|
|
537
|
-
index: i,
|
|
538
|
-
found: true
|
|
539
|
-
} : {
|
|
540
|
-
acc: state.acc + line.length + 1,
|
|
541
|
-
index: 0,
|
|
542
|
-
found: false
|
|
543
|
-
}, {
|
|
544
|
-
acc: 0,
|
|
545
|
-
index: 0,
|
|
546
|
-
found: false
|
|
547
|
-
}).index;
|
|
548
|
-
for (let i = lineNumber - 1; i >= 0; i--) {
|
|
549
|
-
const line = originalSourceLines[i];
|
|
550
|
-
const trimmedLine = trimString(line);
|
|
551
|
-
if (trimmedLine === "") continue;
|
|
552
|
-
if (startsWith(trimmedLine, "///")) commentLines.unshift(line);
|
|
553
|
-
else break;
|
|
554
|
-
}
|
|
555
|
-
const { objectType } = parseFieldComments(commentLines, commentPrefix);
|
|
556
|
-
const initializer = declaration.getInitializer();
|
|
557
|
-
if (Node.isCallExpression(initializer)) {
|
|
558
|
-
if (isRelationFunctionCall(initializer)) return null;
|
|
559
|
-
return {
|
|
560
|
-
name,
|
|
561
|
-
fields: extractFieldsFromCall(initializer, sourceText),
|
|
562
|
-
objectType
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
if (Node.isObjectLiteralExpression(initializer)) return {
|
|
566
|
-
name,
|
|
567
|
-
fields: initializer.getProperties().map((prop) => extractFieldFromProperty(prop, sourceText)).filter((field) => field !== null),
|
|
568
|
-
objectType
|
|
569
|
-
};
|
|
105
|
+
const fnName = expression.getText();
|
|
106
|
+
return fnName === "relations" || fnName.includes("relation");
|
|
107
|
+
}
|
|
108
|
+
function extractFieldFromProperty(property, sourceText, tag) {
|
|
109
|
+
if (!Node.isPropertyAssignment(property)) return null;
|
|
110
|
+
const name = property.getName();
|
|
111
|
+
if (!name) return null;
|
|
112
|
+
const { definition, description } = parseFieldComments(extractFieldComments(sourceText, property.getStart()), tag);
|
|
113
|
+
return {
|
|
114
|
+
name,
|
|
115
|
+
definition,
|
|
116
|
+
description
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function extractRelationFieldFromProperty(property, sourceText, tag, prefix) {
|
|
120
|
+
if (!Node.isPropertyAssignment(property)) return null;
|
|
121
|
+
const name = property.getName();
|
|
122
|
+
if (!name) return null;
|
|
123
|
+
const init = property.getInitializer();
|
|
124
|
+
if (!Node.isCallExpression(init)) return {
|
|
125
|
+
name,
|
|
126
|
+
definition: "",
|
|
127
|
+
description: void 0
|
|
128
|
+
};
|
|
129
|
+
const expr = init.getExpression();
|
|
130
|
+
if (!Node.isIdentifier(expr)) return {
|
|
131
|
+
name,
|
|
132
|
+
definition: "",
|
|
133
|
+
description: void 0
|
|
134
|
+
};
|
|
135
|
+
const fnName = expr.getText();
|
|
136
|
+
const args = init.getArguments();
|
|
137
|
+
if (!(args.length && Node.isIdentifier(args[0]))) return {
|
|
138
|
+
name,
|
|
139
|
+
definition: "",
|
|
140
|
+
description: void 0
|
|
141
|
+
};
|
|
142
|
+
const definition = generateRelationDefinition(fnName, args[0].getText(), prefix);
|
|
143
|
+
const { description } = parseFieldComments(extractFieldComments(sourceText, property.getStart()), tag);
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
definition,
|
|
147
|
+
description
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function extractFieldsFromCall(call, sourceText, tag, prefix) {
|
|
151
|
+
const objectLiteral = findObjectLiteralInArgs(call);
|
|
152
|
+
if (!objectLiteral) return [];
|
|
153
|
+
const isRelation = isRelationFunctionCall(call);
|
|
154
|
+
return objectLiteral.getProperties().map((prop) => isRelation ? extractRelationFieldFromProperty(prop, sourceText, tag, prefix) : extractFieldFromProperty(prop, sourceText, tag)).filter((f) => f !== null);
|
|
155
|
+
}
|
|
156
|
+
function findLeadingComments(sourceCode, statementStart) {
|
|
157
|
+
const lines = sourceCode.split("\n");
|
|
158
|
+
const lineNumber = sourceCode.slice(0, statementStart).split("\n").length - 1;
|
|
159
|
+
const reversed = lines.slice(0, lineNumber).reverse();
|
|
160
|
+
const stopIdx = reversed.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("///"));
|
|
161
|
+
return (stopIdx === -1 ? reversed : reversed.slice(0, stopIdx)).filter((l) => l.trim().startsWith("///")).reverse();
|
|
162
|
+
}
|
|
163
|
+
function extractSchema(variableStatement, sourceText, sourceCode, tag, prefix) {
|
|
164
|
+
if (!Node.isVariableStatement(variableStatement)) return null;
|
|
165
|
+
const declaration = variableStatement.getDeclarations()[0];
|
|
166
|
+
if (!declaration) return null;
|
|
167
|
+
const name = declaration.getName();
|
|
168
|
+
if (!name) return null;
|
|
169
|
+
const { objectType } = parseFieldComments(findLeadingComments(sourceCode, variableStatement.getStart()), tag);
|
|
170
|
+
const initializer = declaration.getInitializer();
|
|
171
|
+
if (Node.isCallExpression(initializer)) {
|
|
172
|
+
if (isRelationFunctionCall(initializer)) return null;
|
|
570
173
|
return {
|
|
571
174
|
name,
|
|
572
|
-
fields:
|
|
175
|
+
fields: extractFieldsFromCall(initializer, sourceText, tag, prefix),
|
|
573
176
|
objectType
|
|
574
177
|
};
|
|
178
|
+
}
|
|
179
|
+
if (Node.isObjectLiteralExpression(initializer)) return {
|
|
180
|
+
name,
|
|
181
|
+
fields: initializer.getProperties().map((prop) => extractFieldFromProperty(prop, sourceText, tag)).filter((f) => f !== null),
|
|
182
|
+
objectType
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
name,
|
|
186
|
+
fields: [],
|
|
187
|
+
objectType
|
|
575
188
|
};
|
|
576
189
|
}
|
|
577
|
-
/**
|
|
578
|
-
* Extracts schemas from TypeScript source code using AST analysis.
|
|
579
|
-
*
|
|
580
|
-
* This function processes exported variable declarations to extract table schemas
|
|
581
|
-
* with their field definitions and comments. It supports both Zod and Valibot schema extraction.
|
|
582
|
-
*
|
|
583
|
-
* @param lines - Array of source code lines to process
|
|
584
|
-
* @param library - The validation library to extract schemas for ('zod' or 'valibot')
|
|
585
|
-
* @returns Array of extracted schemas with field definitions
|
|
586
|
-
*
|
|
587
|
-
* @example
|
|
588
|
-
* ```typescript
|
|
589
|
-
* // For Zod schemas
|
|
590
|
-
* const zodSchemas = extractSchemas(sourceLines, 'zod')
|
|
591
|
-
*
|
|
592
|
-
* // For Valibot schemas
|
|
593
|
-
* const valibotSchemas = extractSchemas(sourceLines, 'valibot')
|
|
594
|
-
* ```
|
|
595
|
-
*/
|
|
596
190
|
function extractSchemas(lines, library) {
|
|
597
191
|
const sourceCode = lines.join("\n");
|
|
598
|
-
const sourceFile =
|
|
599
|
-
useInMemoryFileSystem: true,
|
|
600
|
-
compilerOptions: {
|
|
601
|
-
allowJs: true,
|
|
602
|
-
skipLibCheck: true
|
|
603
|
-
}
|
|
604
|
-
}).createSourceFile("temp.ts", sourceCode);
|
|
192
|
+
const sourceFile = makeSourceFile(sourceCode);
|
|
605
193
|
const sourceText = sourceFile.getFullText();
|
|
606
|
-
const
|
|
194
|
+
const tag = {
|
|
607
195
|
zod: "@z.",
|
|
608
196
|
valibot: "@v.",
|
|
609
197
|
arktype: "@a.",
|
|
610
198
|
effect: "@e."
|
|
611
|
-
};
|
|
612
|
-
const
|
|
199
|
+
}[library];
|
|
200
|
+
const prefix = {
|
|
613
201
|
zod: "z",
|
|
614
202
|
valibot: "v",
|
|
615
203
|
arktype: "type",
|
|
616
204
|
effect: "Schema"
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
const schemaPrefix = schemaPrefixMap[library];
|
|
620
|
-
const extractField = createExtractFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix));
|
|
621
|
-
const extractSchema = makeSchemaExtractor(createExtractFieldsFromCallExpression(extractField, createExtractRelationFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix), schemaPrefix), findObjectLiteralExpression, findObjectLiteralInArgs, isRelationFunctionCall), extractField, parseFieldComments, commentPrefix);
|
|
622
|
-
return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).map((stmt) => extractSchema(stmt, sourceText, sourceCode)).filter((schema) => schema !== null);
|
|
205
|
+
}[library];
|
|
206
|
+
return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).map((stmt) => extractSchema(stmt, sourceText, sourceCode, tag, prefix)).filter((schema) => schema !== null);
|
|
623
207
|
}
|
|
624
|
-
/**
|
|
625
|
-
* Extracts relation schemas from `relations(...)` declarations using AST analysis.
|
|
626
|
-
*
|
|
627
|
-
* This returns entries like `userRelations` and `postRelations` with fields
|
|
628
|
-
* resolved to either `z.array(OtherSchema)` / `v.array(OtherSchema)` or direct
|
|
629
|
-
* `OtherSchema` based on `many`/`one`.
|
|
630
|
-
*
|
|
631
|
-
* Note: Base table schemas are not included here; use `extractSchemas` for those.
|
|
632
|
-
*/
|
|
633
208
|
function extractRelationSchemas(lines, library) {
|
|
634
|
-
const
|
|
635
|
-
const sourceFile = new Project({
|
|
636
|
-
useInMemoryFileSystem: true,
|
|
637
|
-
compilerOptions: {
|
|
638
|
-
allowJs: true,
|
|
639
|
-
skipLibCheck: true
|
|
640
|
-
}
|
|
641
|
-
}).createSourceFile("temp.ts", sourceCode);
|
|
209
|
+
const sourceFile = makeSourceFile(lines.join("\n"));
|
|
642
210
|
const sourceText = sourceFile.getFullText();
|
|
643
|
-
const
|
|
211
|
+
const tag = {
|
|
644
212
|
zod: "@z.",
|
|
645
213
|
valibot: "@v.",
|
|
646
214
|
arktype: "@a.",
|
|
647
215
|
effect: "@e."
|
|
648
|
-
};
|
|
649
|
-
const
|
|
216
|
+
}[library];
|
|
217
|
+
const prefix = {
|
|
650
218
|
zod: "z",
|
|
651
219
|
valibot: "v",
|
|
652
220
|
arktype: "type",
|
|
653
221
|
effect: "Schema"
|
|
654
|
-
};
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
const baseSchemas = extractSchemas(lines, library);
|
|
658
|
-
const baseSchemaMap = new Map(baseSchemas.map((schema) => [schema.name, schema.objectType]));
|
|
659
|
-
const extractFieldsFromCall = createExtractFieldsFromCallExpression(createExtractFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix)), createExtractRelationFieldFromProperty((lines) => parseFieldComments(lines, commentPrefix), schemaPrefix), findObjectLiteralExpression, findObjectLiteralInArgs, isRelationFunctionCall);
|
|
660
|
-
function extract(declaration) {
|
|
222
|
+
}[library];
|
|
223
|
+
const baseSchemaMap = new Map(extractSchemas(lines, library).map((schema) => [schema.name, schema.objectType]));
|
|
224
|
+
return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).flatMap((stmt) => stmt.getDeclarations()).map((declaration) => {
|
|
661
225
|
if (!Node.isVariableDeclaration(declaration)) return null;
|
|
662
226
|
const name = declaration.getName();
|
|
663
227
|
if (!name) return null;
|
|
664
228
|
const initializer = declaration.getInitializer();
|
|
665
229
|
if (!Node.isCallExpression(initializer)) return null;
|
|
666
230
|
if (!isRelationFunctionCall(initializer)) return null;
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
if (!baseIdentifier) return null;
|
|
231
|
+
const baseIdentifier = initializer.getArguments()[0];
|
|
232
|
+
if (!baseIdentifier || !Node.isIdentifier(baseIdentifier)) return null;
|
|
670
233
|
const baseName = baseIdentifier.getText();
|
|
671
234
|
return {
|
|
672
235
|
name,
|
|
673
236
|
baseName,
|
|
674
|
-
fields: extractFieldsFromCall(initializer, sourceText),
|
|
237
|
+
fields: extractFieldsFromCall(initializer, sourceText, tag, prefix),
|
|
675
238
|
objectType: baseSchemaMap.get(baseName)
|
|
676
239
|
};
|
|
677
|
-
}
|
|
678
|
-
return sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword()).flatMap((stmt) => stmt.getDeclarations()).map((decl) => extract(decl)).filter((schema) => schema !== null);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
//#endregion
|
|
682
|
-
//#region src/generator/arktype/index.ts
|
|
683
|
-
/**
|
|
684
|
-
* Generate ArkType type() definition from a schema
|
|
685
|
-
* @param schema - The schema to generate ArkType definition from
|
|
686
|
-
* @param comment - Whether to include JSDoc comments in the generated code
|
|
687
|
-
*/
|
|
688
|
-
function arktype(schema, comment) {
|
|
689
|
-
const inner = fieldDefinitions(schema, comment);
|
|
690
|
-
const undeclared = resolveArktypeUndeclared(schema.objectType);
|
|
691
|
-
return `export const ${makeCapitalized(schema.name)}Schema = type({${undeclared}${inner}})`;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Generate ArkType schema code with optional type inference
|
|
695
|
-
* @param schema - The schema to generate code from
|
|
696
|
-
* @param comment - Whether to include JSDoc comments in the generated code
|
|
697
|
-
* @param type - Whether to include type inference
|
|
698
|
-
*/
|
|
699
|
-
function arktypeCode(schema, comment, type) {
|
|
700
|
-
const arktypeSchema = arktype(schema, comment);
|
|
701
|
-
if (type) return `${arktypeSchema}\n\n${inferArktype(schema.name)}\n`;
|
|
702
|
-
return `${arktypeSchema}\n`;
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Generate ArkType relation schema code with spread base schema
|
|
706
|
-
* @param schema - The relation schema with base model reference
|
|
707
|
-
* @param withType - Whether to include type inference
|
|
708
|
-
*/
|
|
709
|
-
function makeRelationArktypeCode(schema, withType) {
|
|
710
|
-
const relName = `${schema.name}Schema`;
|
|
711
|
-
const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
|
|
712
|
-
const fields = makeRelationFields(schema.fields);
|
|
713
|
-
const undeclared = resolveArktypeUndeclared(schema.objectType);
|
|
714
|
-
const obj = `\nexport const ${makeCapitalized(relName)} = type({${undeclared}...${baseSchema}.t,${fields}})`;
|
|
715
|
-
if (withType) return `${obj}\n\n${inferArktype(schema.name)}\n`;
|
|
716
|
-
return `${obj}`;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Generate ArkType schema
|
|
720
|
-
* @param code - The code to generate ArkType schema from
|
|
721
|
-
* @param output - The output file path
|
|
722
|
-
* @param comment - Whether to include comments in the generated code
|
|
723
|
-
* @param type - Whether to include type information in the generated code
|
|
724
|
-
* @param relation - Whether to include relation schemas in the generated code
|
|
725
|
-
*/
|
|
726
|
-
async function sizukuArktype(code, output, comment, type, relation) {
|
|
727
|
-
const importLine = `import { type } from 'arktype'`;
|
|
728
|
-
const baseSchemas = extractSchemas(code, "arktype");
|
|
729
|
-
const relationSchemas = extractRelationSchemas(code, "arktype");
|
|
730
|
-
const arktypeGeneratedCode = [
|
|
731
|
-
importLine,
|
|
732
|
-
"",
|
|
733
|
-
...baseSchemas.map((schema) => arktypeCode(schema, comment ?? false, type ?? false)),
|
|
734
|
-
...relation ? relationSchemas.map((schema) => makeRelationArktypeCode(schema, type ?? false)) : []
|
|
735
|
-
].join("\n");
|
|
736
|
-
const mkdirResult = await mkdir(path.dirname(output));
|
|
737
|
-
if (!mkdirResult.ok) return {
|
|
738
|
-
ok: false,
|
|
739
|
-
error: mkdirResult.error
|
|
740
|
-
};
|
|
741
|
-
const writeFileResult = await writeFile(output, await fmt(arktypeGeneratedCode));
|
|
742
|
-
if (!writeFileResult.ok) return {
|
|
743
|
-
ok: false,
|
|
744
|
-
error: writeFileResult.error
|
|
745
|
-
};
|
|
746
|
-
return {
|
|
747
|
-
ok: true,
|
|
748
|
-
value: void 0
|
|
749
|
-
};
|
|
240
|
+
}).filter((schema) => schema !== null);
|
|
750
241
|
}
|
|
751
|
-
|
|
752
242
|
//#endregion
|
|
753
|
-
//#region src/
|
|
754
|
-
/**
|
|
755
|
-
* Create a unique key for relation deduplication.
|
|
756
|
-
*/
|
|
757
|
-
function relationKey(r) {
|
|
758
|
-
return `${r.fromModel}.${r.fromField}->${r.toModel}.${r.toField}`;
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Extract base builder name from expression.
|
|
762
|
-
*
|
|
763
|
-
* @param expr - The expression to extract name from
|
|
764
|
-
* @returns The base builder name
|
|
765
|
-
*/
|
|
243
|
+
//#region src/helper/extract-tables.ts
|
|
766
244
|
function baseBuilderName(expr) {
|
|
767
245
|
if (Node.isIdentifier(expr)) return expr.getText();
|
|
768
246
|
if (Node.isCallExpression(expr) || Node.isPropertyAccessExpression(expr)) return baseBuilderName(expr.getExpression());
|
|
769
247
|
return "";
|
|
770
248
|
}
|
|
771
|
-
/**
|
|
772
|
-
* Type guard for FieldInfo.
|
|
773
|
-
*
|
|
774
|
-
* @param v - The value to check
|
|
775
|
-
* @returns True if value is FieldInfo
|
|
776
|
-
*/
|
|
777
|
-
function isFieldInfo(v) {
|
|
778
|
-
return v !== null;
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Extract key type based on field definition.
|
|
782
|
-
*
|
|
783
|
-
* @param initText - The initializer text
|
|
784
|
-
* @returns The key type (PK, FK, or null)
|
|
785
|
-
*/
|
|
786
249
|
function extractKeyType(initText) {
|
|
787
250
|
if (initText.includes(".primaryKey()")) return "PK";
|
|
788
251
|
if (initText.includes(".references(")) return "FK";
|
|
789
252
|
return null;
|
|
790
253
|
}
|
|
791
|
-
/**
|
|
792
|
-
* Find immediate comment for a field.
|
|
793
|
-
*
|
|
794
|
-
* @param code - The source code lines
|
|
795
|
-
* @param lineIdx - The line index of the field
|
|
796
|
-
* @returns The immediate comment or empty string
|
|
797
|
-
*/
|
|
798
254
|
function findImmediateComment(code, lineIdx) {
|
|
799
255
|
return code.slice(0, lineIdx).reverse().find((line) => {
|
|
800
256
|
const t = line.trim();
|
|
801
257
|
return t.startsWith("///") && !t.includes("@z.") && !t.includes("@v.") && !t.includes("@a.") && !t.includes("@e.") && !t.includes("@relation");
|
|
802
258
|
})?.replace(/^\s*\/\/\/\s*/, "") ?? "";
|
|
803
259
|
}
|
|
804
|
-
/**
|
|
805
|
-
* Extract reference info from a .references() call.
|
|
806
|
-
*
|
|
807
|
-
* @param initExpr - The call expression to analyze
|
|
808
|
-
* @returns The reference info or null
|
|
809
|
-
*/
|
|
810
260
|
function extractReferenceInfo(initExpr) {
|
|
811
261
|
const match = initExpr.getText().match(/\.references\(\s*\(\)\s*=>\s*(\w+)\.(\w+)\s*\)/);
|
|
812
|
-
if (match) return
|
|
262
|
+
if (!match) return null;
|
|
263
|
+
return {
|
|
813
264
|
referencedTable: match[1],
|
|
814
265
|
referencedField: match[2]
|
|
815
266
|
};
|
|
816
|
-
return null;
|
|
817
267
|
}
|
|
818
|
-
/**
|
|
819
|
-
* Check if field is required (notNull).
|
|
820
|
-
*
|
|
821
|
-
* @param initText - The initializer text
|
|
822
|
-
* @returns True if field is required
|
|
823
|
-
*/
|
|
824
268
|
function isFieldRequired(initText) {
|
|
825
269
|
return initText.includes(".notNull()");
|
|
826
270
|
}
|
|
827
|
-
/**
|
|
828
|
-
* Extract field info from property assignment.
|
|
829
|
-
*
|
|
830
|
-
* @param prop - The property assignment node
|
|
831
|
-
* @param code - The source code lines
|
|
832
|
-
* @returns Field info or null
|
|
833
|
-
*/
|
|
834
271
|
function extractFieldInfo(prop, code) {
|
|
835
272
|
const keyNode = prop.getNameNode();
|
|
836
273
|
if (!Node.isIdentifier(keyNode)) return null;
|
|
@@ -847,13 +284,6 @@ function extractFieldInfo(prop, code) {
|
|
|
847
284
|
description: immediateComment || null
|
|
848
285
|
};
|
|
849
286
|
}
|
|
850
|
-
/**
|
|
851
|
-
* Extract relation info from a property assignment with .references().
|
|
852
|
-
*
|
|
853
|
-
* @param prop - The property assignment node
|
|
854
|
-
* @param tableName - The name of the current table
|
|
855
|
-
* @returns Relation info or null
|
|
856
|
-
*/
|
|
857
287
|
function extractRelationFromField(prop, tableName) {
|
|
858
288
|
const keyNode = prop.getNameNode();
|
|
859
289
|
if (!Node.isIdentifier(keyNode)) return null;
|
|
@@ -873,139 +303,88 @@ function extractRelationFromField(prop, tableName) {
|
|
|
873
303
|
isRequired
|
|
874
304
|
};
|
|
875
305
|
}
|
|
876
|
-
/**
|
|
877
|
-
* Extract relations from foreignKey() constraints in the third argument.
|
|
878
|
-
*
|
|
879
|
-
* Pattern:
|
|
880
|
-
* foreignKey({
|
|
881
|
-
* columns: [TableName.fieldName],
|
|
882
|
-
* foreignColumns: [OtherTable.fieldName],
|
|
883
|
-
* })
|
|
884
|
-
*
|
|
885
|
-
* @param tableName - The name of the current table
|
|
886
|
-
* @param constraintArg - The third argument (arrow function returning constraints object)
|
|
887
|
-
* @returns Array of relation info
|
|
888
|
-
*/
|
|
889
306
|
function extractRelationsFromForeignKeyConstraints(tableName, constraintArg) {
|
|
890
|
-
|
|
891
|
-
if (!Node.isArrowFunction(constraintArg)) return relations;
|
|
307
|
+
if (!Node.isArrowFunction(constraintArg)) return [];
|
|
892
308
|
const body = constraintArg.getBody();
|
|
893
|
-
if (!body) return
|
|
309
|
+
if (!body) return [];
|
|
894
310
|
const objExpr = Node.isParenthesizedExpression(body) ? body.getExpression() : body;
|
|
895
|
-
if (!Node.isObjectLiteralExpression(objExpr)) return
|
|
896
|
-
|
|
897
|
-
if (!Node.isPropertyAssignment(prop))
|
|
311
|
+
if (!Node.isObjectLiteralExpression(objExpr)) return [];
|
|
312
|
+
return objExpr.getProperties().flatMap((prop) => {
|
|
313
|
+
if (!Node.isPropertyAssignment(prop)) return [];
|
|
898
314
|
const initExpr = prop.getInitializer();
|
|
899
|
-
if (!initExpr)
|
|
315
|
+
if (!initExpr) return [];
|
|
900
316
|
const text = initExpr.getText();
|
|
901
|
-
if (!text.includes("foreignKey("))
|
|
317
|
+
if (!text.includes("foreignKey(")) return [];
|
|
902
318
|
const columnsMatch = text.match(/columns:\s*\[\s*(\w+)\.(\w+)\s*\]/);
|
|
903
319
|
const foreignColumnsMatch = text.match(/foreignColumns:\s*\[\s*(\w+)\.(\w+)\s*\]/);
|
|
904
|
-
if (columnsMatch
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
return relations;
|
|
320
|
+
if (!columnsMatch || !foreignColumnsMatch) return [];
|
|
321
|
+
return [{
|
|
322
|
+
fromModel: foreignColumnsMatch[1],
|
|
323
|
+
toModel: tableName,
|
|
324
|
+
fromField: foreignColumnsMatch[2],
|
|
325
|
+
toField: columnsMatch[2],
|
|
326
|
+
isRequired: !text.includes(".nullable()")
|
|
327
|
+
}];
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function isFkLikeName(name) {
|
|
331
|
+
const lower = name.toLowerCase();
|
|
332
|
+
return lower.endsWith("id") && lower !== "id" || lower.endsWith("_id");
|
|
919
333
|
}
|
|
920
|
-
/**
|
|
921
|
-
* Extract relations from relations() helper blocks.
|
|
922
|
-
*
|
|
923
|
-
* Pattern:
|
|
924
|
-
* relations(TableRef, ({ one, many }) => ({
|
|
925
|
-
* user: one(User, {
|
|
926
|
-
* fields: [Post.userId],
|
|
927
|
-
* references: [User.id],
|
|
928
|
-
* }),
|
|
929
|
-
* posts: many(Post),
|
|
930
|
-
* }))
|
|
931
|
-
*
|
|
932
|
-
* @param file - The source file
|
|
933
|
-
* @returns Array of relation info
|
|
934
|
-
*/
|
|
935
334
|
function extractRelationsFromRelationBlocks(file) {
|
|
936
|
-
|
|
937
|
-
for (const stmt of file.getVariableStatements().filter((stmt) => stmt.isExported())) {
|
|
335
|
+
return file.getVariableStatements().filter((stmt) => stmt.isExported()).flatMap((stmt) => {
|
|
938
336
|
const decl = stmt.getDeclarations()[0];
|
|
939
|
-
if (!Node.isVariableDeclaration(decl))
|
|
940
|
-
if (!decl.getName().toLowerCase().includes("relation"))
|
|
337
|
+
if (!Node.isVariableDeclaration(decl)) return [];
|
|
338
|
+
if (!decl.getName().toLowerCase().includes("relation")) return [];
|
|
941
339
|
const init = decl.getInitializer();
|
|
942
|
-
if (!(init && Node.isCallExpression(init)))
|
|
943
|
-
if (init.getExpression().getText() !== "relations")
|
|
340
|
+
if (!(init && Node.isCallExpression(init))) return [];
|
|
341
|
+
if (init.getExpression().getText() !== "relations") return [];
|
|
944
342
|
const args = init.getArguments();
|
|
945
|
-
if (args.length < 2)
|
|
343
|
+
if (args.length < 2) return [];
|
|
946
344
|
const tableRef = args[0];
|
|
947
|
-
if (!Node.isIdentifier(tableRef)) continue;
|
|
948
|
-
const tableName = tableRef.getText();
|
|
949
345
|
const arrowFn = args[1];
|
|
950
|
-
if (!Node.isArrowFunction(arrowFn))
|
|
346
|
+
if (!Node.isIdentifier(tableRef) || !Node.isArrowFunction(arrowFn)) return [];
|
|
347
|
+
const tableName = tableRef.getText();
|
|
951
348
|
const body = arrowFn.getBody();
|
|
952
|
-
if (!body)
|
|
349
|
+
if (!body) return [];
|
|
953
350
|
const objExpr = Node.isParenthesizedExpression(body) ? body.getExpression() : body;
|
|
954
|
-
if (!Node.isObjectLiteralExpression(objExpr))
|
|
955
|
-
|
|
956
|
-
if (!Node.isPropertyAssignment(prop))
|
|
351
|
+
if (!Node.isObjectLiteralExpression(objExpr)) return [];
|
|
352
|
+
return objExpr.getProperties().flatMap((prop) => {
|
|
353
|
+
if (!Node.isPropertyAssignment(prop)) return [];
|
|
957
354
|
const initExpr = prop.getInitializer();
|
|
958
|
-
if (!(initExpr && Node.isCallExpression(initExpr)))
|
|
959
|
-
if (initExpr.getExpression().getText() !== "one")
|
|
355
|
+
if (!(initExpr && Node.isCallExpression(initExpr))) return [];
|
|
356
|
+
if (initExpr.getExpression().getText() !== "one") return [];
|
|
960
357
|
const relArgs = initExpr.getArguments();
|
|
961
|
-
if (relArgs.length < 2)
|
|
358
|
+
if (relArgs.length < 2) return [];
|
|
962
359
|
const refTableArg = relArgs[0];
|
|
963
|
-
if (!Node.isIdentifier(refTableArg)) continue;
|
|
964
|
-
const refTable = refTableArg.getText();
|
|
965
360
|
const configArg = relArgs[1];
|
|
966
|
-
if (!Node.isObjectLiteralExpression(configArg))
|
|
361
|
+
if (!Node.isIdentifier(refTableArg) || !Node.isObjectLiteralExpression(configArg)) return [];
|
|
362
|
+
const refTable = refTableArg.getText();
|
|
967
363
|
const configText = configArg.getText();
|
|
968
364
|
const fieldsMatch = configText.match(/fields:\s*\[\s*(\w+)\.(\w+)\s*\]/);
|
|
969
365
|
const referencesMatch = configText.match(/references:\s*\[\s*(\w+)\.(\w+)\s*\]/);
|
|
970
|
-
if (fieldsMatch
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
else relations.push({
|
|
992
|
-
fromModel: refTable,
|
|
993
|
-
toModel: tableName,
|
|
994
|
-
fromField: refField,
|
|
995
|
-
toField: currentField,
|
|
996
|
-
isRequired: true
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return relations;
|
|
366
|
+
if (!fieldsMatch || !referencesMatch) return [];
|
|
367
|
+
const currentField = fieldsMatch[2];
|
|
368
|
+
const refField = referencesMatch[2];
|
|
369
|
+
const currentIsFk = isFkLikeName(currentField);
|
|
370
|
+
const refIsFk = isFkLikeName(refField);
|
|
371
|
+
if (!currentIsFk && refIsFk) return [{
|
|
372
|
+
fromModel: tableName,
|
|
373
|
+
toModel: refTable,
|
|
374
|
+
fromField: currentField,
|
|
375
|
+
toField: refField,
|
|
376
|
+
isRequired: true
|
|
377
|
+
}];
|
|
378
|
+
return [{
|
|
379
|
+
fromModel: refTable,
|
|
380
|
+
toModel: tableName,
|
|
381
|
+
fromField: refField,
|
|
382
|
+
toField: currentField,
|
|
383
|
+
isRequired: true
|
|
384
|
+
}];
|
|
385
|
+
});
|
|
386
|
+
});
|
|
1002
387
|
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Parse table information from Drizzle schema code.
|
|
1005
|
-
*
|
|
1006
|
-
* @param code - Array of source code lines
|
|
1007
|
-
* @returns Array of table information
|
|
1008
|
-
*/
|
|
1009
388
|
function parseTableInfo(code) {
|
|
1010
389
|
const source = code.join("\n");
|
|
1011
390
|
return new Project({ useInMemoryFileSystem: true }).createSourceFile("temp.ts", source).getVariableStatements().filter((stmt) => stmt.isExported()).flatMap((stmt) => {
|
|
@@ -1021,62 +400,71 @@ function parseTableInfo(code) {
|
|
|
1021
400
|
if (!(objLit && Node.isObjectLiteralExpression(objLit))) return [];
|
|
1022
401
|
return [{
|
|
1023
402
|
name: varName,
|
|
1024
|
-
fields: objLit.getProperties().filter(Node.isPropertyAssignment).map((prop) => extractFieldInfo(prop, code)).filter(
|
|
403
|
+
fields: objLit.getProperties().filter(Node.isPropertyAssignment).map((prop) => extractFieldInfo(prop, code)).filter((f) => f !== null)
|
|
1025
404
|
}];
|
|
1026
405
|
});
|
|
1027
406
|
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Extract relations from Drizzle schema code by analyzing:
|
|
1030
|
-
* 1. .references() calls on fields
|
|
1031
|
-
* 2. foreignKey() constraints in table definition
|
|
1032
|
-
* 3. relations() helper blocks
|
|
1033
|
-
*
|
|
1034
|
-
* @param code - Array of source code lines
|
|
1035
|
-
* @returns Array of relation information (deduplicated)
|
|
1036
|
-
*/
|
|
1037
407
|
function extractRelationsFromSchema(code) {
|
|
1038
408
|
const source = code.join("\n");
|
|
1039
409
|
const file = new Project({ useInMemoryFileSystem: true }).createSourceFile("temp.ts", source);
|
|
1040
|
-
const
|
|
1041
|
-
for (const stmt of file.getVariableStatements().filter((stmt) => stmt.isExported())) {
|
|
410
|
+
const allRelations = [...file.getVariableStatements().filter((stmt) => stmt.isExported()).flatMap((stmt) => {
|
|
1042
411
|
const decl = stmt.getDeclarations()[0];
|
|
1043
|
-
if (!Node.isVariableDeclaration(decl))
|
|
412
|
+
if (!Node.isVariableDeclaration(decl)) return [];
|
|
1044
413
|
const varName = decl.getName();
|
|
1045
|
-
if (varName.toLowerCase().includes("relation"))
|
|
414
|
+
if (varName.toLowerCase().includes("relation")) return [];
|
|
1046
415
|
const init = decl.getInitializer();
|
|
1047
|
-
if (!(init && Node.isCallExpression(init)))
|
|
416
|
+
if (!(init && Node.isCallExpression(init))) return [];
|
|
1048
417
|
const callee = init.getExpression().getText();
|
|
1049
|
-
if (!callee.endsWith("Table") || callee === "relations")
|
|
418
|
+
if (!callee.endsWith("Table") || callee === "relations") return [];
|
|
1050
419
|
const args = init.getArguments();
|
|
1051
420
|
const objLit = args[1];
|
|
1052
|
-
if (objLit && Node.isObjectLiteralExpression(objLit)) for (const prop of objLit.getProperties().filter(Node.isPropertyAssignment)) {
|
|
1053
|
-
const relation = extractRelationFromField(prop, varName);
|
|
1054
|
-
if (relation) relations.push(relation);
|
|
1055
|
-
}
|
|
1056
421
|
const constraintArg = args[2];
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
}
|
|
1062
|
-
const relationBlockRelations = extractRelationsFromRelationBlocks(file);
|
|
1063
|
-
relations.push(...relationBlockRelations);
|
|
422
|
+
const fieldRelations = objLit && Node.isObjectLiteralExpression(objLit) ? objLit.getProperties().filter(Node.isPropertyAssignment).map((prop) => extractRelationFromField(prop, varName)).filter((r) => r !== null) : [];
|
|
423
|
+
const fkRelations = constraintArg && Node.isExpression(constraintArg) ? extractRelationsFromForeignKeyConstraints(varName, constraintArg) : [];
|
|
424
|
+
return [...fieldRelations, ...fkRelations];
|
|
425
|
+
}), ...extractRelationsFromRelationBlocks(file)];
|
|
1064
426
|
const seen = /* @__PURE__ */ new Set();
|
|
1065
|
-
return
|
|
1066
|
-
const key =
|
|
427
|
+
return allRelations.filter((r) => {
|
|
428
|
+
const key = `${r.fromModel}.${r.fromField}->${r.toModel}.${r.toField}`;
|
|
1067
429
|
if (seen.has(key)) return false;
|
|
1068
430
|
seen.add(key);
|
|
1069
431
|
return true;
|
|
1070
432
|
});
|
|
1071
433
|
}
|
|
1072
|
-
|
|
1073
434
|
//#endregion
|
|
1074
|
-
//#region src/generator/
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
435
|
+
//#region src/generator/arktype.ts
|
|
436
|
+
function undeclared(objectType) {
|
|
437
|
+
if (objectType === "strict") return "\"+\":\"reject\",";
|
|
438
|
+
if (objectType === "loose") return "\"+\":\"ignore\",";
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
function arktype(code, comment, type, relation) {
|
|
442
|
+
const c = comment ?? false;
|
|
443
|
+
const t = type ?? false;
|
|
444
|
+
const baseLines = extractSchemas(code, "arktype").map((schema) => {
|
|
445
|
+
const cap = makeCapitalized(schema.name);
|
|
446
|
+
const head = `export const ${cap}Schema = type({${undeclared(schema.objectType)}${fieldDefinitions(schema, c)}})`;
|
|
447
|
+
return t ? `${head}\n\nexport type ${cap} = typeof ${cap}Schema.infer\n` : `${head}\n`;
|
|
448
|
+
});
|
|
449
|
+
const relationLines = relation ? extractRelationSchemas(code, "arktype").map((schema) => {
|
|
450
|
+
const relSchema = makeCapitalized(`${schema.name}Schema`);
|
|
451
|
+
const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
|
|
452
|
+
const obj = `\nexport const ${relSchema} = type({${undeclared(schema.objectType)}...${baseSchema}.t,${makeRelationFields(schema.fields)}})`;
|
|
453
|
+
if (!t) return obj;
|
|
454
|
+
const cap = makeCapitalized(schema.name);
|
|
455
|
+
return `${obj}\n\nexport type ${cap} = typeof ${cap}Schema.infer\n`;
|
|
456
|
+
}) : [];
|
|
457
|
+
return [
|
|
458
|
+
`import { type } from 'arktype'`,
|
|
459
|
+
"",
|
|
460
|
+
...baseLines,
|
|
461
|
+
...relationLines
|
|
462
|
+
].join("\n");
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/generator/dbml.ts
|
|
466
|
+
function dbml(code) {
|
|
467
|
+
const TYPE_MAP = {
|
|
1080
468
|
serial: "serial",
|
|
1081
469
|
smallserial: "smallserial",
|
|
1082
470
|
bigserial: "bigserial",
|
|
@@ -1102,268 +490,160 @@ function mapDrizzleType(drizzleType) {
|
|
|
1102
490
|
uuid: "uuid",
|
|
1103
491
|
blob: "blob",
|
|
1104
492
|
bytea: "bytea"
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
493
|
+
};
|
|
494
|
+
const tables = parseTableInfo(code);
|
|
495
|
+
const relations = extractRelationsFromSchema(code);
|
|
496
|
+
const tableSections = tables.map((table) => {
|
|
497
|
+
const columns = table.fields.map((field) => {
|
|
498
|
+
const type = TYPE_MAP[field.type] ?? field.type;
|
|
499
|
+
const constraints = [
|
|
500
|
+
field.keyType === "PK" ? "pk" : null,
|
|
501
|
+
field.type.includes("serial") ? "increment" : null,
|
|
502
|
+
field.description ? `note: '${field.description.replace(/'/g, "\\'")}'` : null
|
|
503
|
+
].filter((c) => c !== null);
|
|
504
|
+
const constraintStr = constraints.length > 0 ? ` [${constraints.join(", ")}]` : "";
|
|
505
|
+
return ` ${field.name} ${type}${constraintStr}`;
|
|
506
|
+
});
|
|
507
|
+
return [
|
|
508
|
+
`Table ${table.name} {`,
|
|
509
|
+
...columns,
|
|
510
|
+
"}"
|
|
511
|
+
].join("\n");
|
|
512
|
+
});
|
|
513
|
+
const refSections = relations.map((r) => {
|
|
514
|
+
const fromTable = r.toModel;
|
|
515
|
+
const fromColumn = r.toField;
|
|
516
|
+
const toTable = r.fromModel;
|
|
517
|
+
const toColumn = r.fromField;
|
|
518
|
+
return `Ref ${`${fromTable}_${fromColumn}_${toTable}_${toColumn}_fk`}: ${fromTable}.${fromColumn} > ${toTable}.${toColumn}`;
|
|
519
|
+
});
|
|
520
|
+
return [...tableSections, ...refSections].join("\n\n");
|
|
1122
521
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/generator/effect.ts
|
|
524
|
+
function effect(code, comment, type, relation) {
|
|
525
|
+
const c = comment ?? false;
|
|
526
|
+
const t = type ?? false;
|
|
527
|
+
const baseLines = extractSchemas(code, "effect").map((schema) => {
|
|
528
|
+
const cap = makeCapitalized(schema.name);
|
|
529
|
+
const head = `export const ${cap}Schema = Schema.Struct({${fieldDefinitions(schema, c)}})`;
|
|
530
|
+
return t ? `${head}\n\nexport type ${cap}Encoded = typeof ${cap}Schema.Encoded\n` : `${head}\n`;
|
|
531
|
+
});
|
|
532
|
+
const relationLines = relation ? extractRelationSchemas(code, "effect").map((schema) => {
|
|
533
|
+
const obj = `\nexport const ${makeCapitalized(`${schema.name}Schema`)} = Schema.Struct({...${`${makeCapitalized(schema.baseName)}Schema`}.fields,${makeRelationFields(schema.fields)}})`;
|
|
534
|
+
if (!t) return obj;
|
|
535
|
+
const cap = makeCapitalized(schema.name);
|
|
536
|
+
return `${obj}\n\nexport type ${cap}Encoded = typeof ${cap}Schema.Encoded\n`;
|
|
537
|
+
}) : [];
|
|
538
|
+
return [
|
|
539
|
+
`import { Schema } from 'effect'`,
|
|
540
|
+
"",
|
|
541
|
+
...baseLines,
|
|
542
|
+
...relationLines
|
|
543
|
+
].join("\n");
|
|
1125
544
|
}
|
|
1126
|
-
function makeColumn(column) {
|
|
1127
|
-
const constraints = makeColumnConstraints(column);
|
|
1128
|
-
return ` ${column.name} ${column.type}${formatConstraints(constraints)}`;
|
|
1129
|
-
}
|
|
1130
|
-
function makeTable(table) {
|
|
1131
|
-
const lines = [];
|
|
1132
|
-
lines.push(`Table ${table.name} {`);
|
|
1133
|
-
for (const column of table.columns) lines.push(makeColumn(column));
|
|
1134
|
-
if (table.note) {
|
|
1135
|
-
lines.push("");
|
|
1136
|
-
lines.push(` Note: ${quote(table.note)}`);
|
|
1137
|
-
}
|
|
1138
|
-
lines.push("}");
|
|
1139
|
-
return lines.join("\n");
|
|
1140
|
-
}
|
|
1141
|
-
function makeEnum(enumDef) {
|
|
1142
|
-
const lines = [];
|
|
1143
|
-
lines.push(`Enum ${enumDef.name} {`);
|
|
1144
|
-
for (const value of enumDef.values) lines.push(` ${value}`);
|
|
1145
|
-
lines.push("}");
|
|
1146
|
-
return lines.join("\n");
|
|
1147
|
-
}
|
|
1148
|
-
function makeRefName(ref) {
|
|
1149
|
-
return ref.name ?? `${ref.fromTable}_${ref.fromColumn}_${ref.toTable}_${ref.toColumn}_fk`;
|
|
1150
|
-
}
|
|
1151
|
-
function makeRef(ref) {
|
|
1152
|
-
const name = makeRefName(ref);
|
|
1153
|
-
const operator = ref.type ?? ">";
|
|
1154
|
-
const actions = [];
|
|
1155
|
-
if (ref.onDelete) actions.push(`delete: ${ref.onDelete}`);
|
|
1156
|
-
if (ref.onUpdate) actions.push(`update: ${ref.onUpdate}`);
|
|
1157
|
-
const actionStr = actions.length > 0 ? ` [${actions.join(", ")}]` : "";
|
|
1158
|
-
return `Ref ${name}: ${ref.fromTable}.${ref.fromColumn} ${operator} ${ref.toTable}.${ref.toColumn}${actionStr}`;
|
|
1159
|
-
}
|
|
1160
|
-
function makeDBMLContent(options) {
|
|
1161
|
-
const sections = [];
|
|
1162
|
-
if (options.enums) for (const enumDef of options.enums) sections.push(makeEnum(enumDef));
|
|
1163
|
-
for (const table of options.tables) sections.push(makeTable(table));
|
|
1164
|
-
if (options.refs) for (const ref of options.refs) sections.push(makeRef(ref));
|
|
1165
|
-
return sections.join("\n\n");
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Convert internal FieldInfo to DBMLColumn
|
|
1169
|
-
*/
|
|
1170
|
-
function toDBMLColumn(field) {
|
|
1171
|
-
return {
|
|
1172
|
-
name: field.name,
|
|
1173
|
-
type: mapDrizzleType(field.type),
|
|
1174
|
-
isPrimaryKey: field.keyType === "PK",
|
|
1175
|
-
isIncrement: field.type.includes("serial"),
|
|
1176
|
-
note: field.description ?? void 0
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* Convert internal TableInfo to DBMLTable
|
|
1181
|
-
*/
|
|
1182
|
-
function toDBMLTable(table) {
|
|
1183
|
-
return {
|
|
1184
|
-
name: table.name,
|
|
1185
|
-
columns: table.fields.map(toDBMLColumn)
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Convert internal RelationInfo to DBMLRef
|
|
1190
|
-
*/
|
|
1191
|
-
function toDBMLRef(relation) {
|
|
1192
|
-
return {
|
|
1193
|
-
fromTable: relation.toModel,
|
|
1194
|
-
fromColumn: relation.toField,
|
|
1195
|
-
toTable: relation.fromModel,
|
|
1196
|
-
toColumn: relation.fromField
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Generate complete DBML content
|
|
1201
|
-
*
|
|
1202
|
-
* @param relations - The relations extracted from the schema
|
|
1203
|
-
* @param tables - The tables to generate DBML from
|
|
1204
|
-
* @returns The generated DBML content
|
|
1205
|
-
*/
|
|
1206
|
-
function dbmlContent(relations, tables) {
|
|
1207
|
-
return makeDBMLContent({
|
|
1208
|
-
tables: tables.map(toDBMLTable),
|
|
1209
|
-
refs: relations.map(toDBMLRef)
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
545
|
//#endregion
|
|
1214
|
-
//#region src/
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
/**
|
|
1223
|
-
* Generate DBML file from Drizzle schema code
|
|
1224
|
-
*
|
|
1225
|
-
* @param code - The code to generate DBML from
|
|
1226
|
-
* @param output - The output file path (must end with .dbml)
|
|
1227
|
-
*/
|
|
1228
|
-
async function sizukuDbmlFile(code, output) {
|
|
1229
|
-
const content = generateContent(code);
|
|
1230
|
-
const mkdirResult = await mkdir(dirname(output));
|
|
1231
|
-
if (!mkdirResult.ok) return {
|
|
1232
|
-
ok: false,
|
|
1233
|
-
error: `❌ Failed to create directory: ${mkdirResult.error}`
|
|
1234
|
-
};
|
|
1235
|
-
const writeResult = await writeFile(output, content);
|
|
1236
|
-
if (!writeResult.ok) return {
|
|
1237
|
-
ok: false,
|
|
1238
|
-
error: `❌ Failed to write DBML: ${writeResult.error}`
|
|
1239
|
-
};
|
|
1240
|
-
return {
|
|
1241
|
-
ok: true,
|
|
1242
|
-
value: void 0
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Generate PNG ER diagram from Drizzle schema code
|
|
1247
|
-
*
|
|
1248
|
-
* @param code - The code to generate PNG from
|
|
1249
|
-
* @param output - The output file path (must end with .png)
|
|
1250
|
-
*/
|
|
1251
|
-
/**
|
|
1252
|
-
* Generate ERD output (DBML or PNG) based on file extension
|
|
1253
|
-
*/
|
|
1254
|
-
async function sizukuDbml(code, output) {
|
|
1255
|
-
if (output.endsWith(".png")) return sizukuPng(code, output);
|
|
1256
|
-
return sizukuDbmlFile(code, output);
|
|
1257
|
-
}
|
|
1258
|
-
async function sizukuPng(code, output) {
|
|
1259
|
-
const pngBuffer = new Resvg(run(generateContent(code), "svg"), { font: { loadSystemFonts: true } }).render().asPng();
|
|
1260
|
-
const mkdirResult = await mkdir(dirname(output));
|
|
1261
|
-
if (!mkdirResult.ok) return {
|
|
1262
|
-
ok: false,
|
|
1263
|
-
error: `❌ Failed to create directory: ${mkdirResult.error}`
|
|
1264
|
-
};
|
|
1265
|
-
const writeResult = await writeFileBinary(output, pngBuffer);
|
|
1266
|
-
if (!writeResult.ok) return {
|
|
546
|
+
//#region src/format/index.ts
|
|
547
|
+
async function fmt(input) {
|
|
548
|
+
const { code, errors } = await format("<stdin>.ts", input, {
|
|
549
|
+
printWidth: 100,
|
|
550
|
+
singleQuote: true,
|
|
551
|
+
semi: false
|
|
552
|
+
});
|
|
553
|
+
if (errors.length > 0) return {
|
|
1267
554
|
ok: false,
|
|
1268
|
-
error:
|
|
555
|
+
error: errors.map((e) => e.message).join("\n")
|
|
1269
556
|
};
|
|
1270
557
|
return {
|
|
1271
558
|
ok: true,
|
|
1272
|
-
value:
|
|
559
|
+
value: code
|
|
1273
560
|
};
|
|
1274
561
|
}
|
|
1275
|
-
|
|
1276
562
|
//#endregion
|
|
1277
|
-
//#region src/
|
|
1278
|
-
function
|
|
1279
|
-
|
|
1280
|
-
|
|
563
|
+
//#region src/fsp/index.ts
|
|
564
|
+
function readFileSync(path) {
|
|
565
|
+
try {
|
|
566
|
+
return {
|
|
567
|
+
ok: true,
|
|
568
|
+
value: fs.readFileSync(path, "utf-8")
|
|
569
|
+
};
|
|
570
|
+
} catch (e) {
|
|
571
|
+
return {
|
|
572
|
+
ok: false,
|
|
573
|
+
error: e instanceof Error ? e.message : String(e)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function mkdir(dir) {
|
|
578
|
+
try {
|
|
579
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
580
|
+
return {
|
|
581
|
+
ok: true,
|
|
582
|
+
value: void 0
|
|
583
|
+
};
|
|
584
|
+
} catch (e) {
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: e instanceof Error ? e.message : String(e)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
1281
590
|
}
|
|
1282
|
-
function
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
591
|
+
async function writeFile(path, data) {
|
|
592
|
+
try {
|
|
593
|
+
await fsp.writeFile(path, data, "utf-8");
|
|
594
|
+
return {
|
|
595
|
+
ok: true,
|
|
596
|
+
value: void 0
|
|
597
|
+
};
|
|
598
|
+
} catch (e) {
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
error: e instanceof Error ? e.message : String(e)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
1286
604
|
}
|
|
1287
|
-
function
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
605
|
+
async function writeFileBinary(path, data) {
|
|
606
|
+
try {
|
|
607
|
+
await fsp.writeFile(path, data);
|
|
608
|
+
return {
|
|
609
|
+
ok: true,
|
|
610
|
+
value: void 0
|
|
611
|
+
};
|
|
612
|
+
} catch (e) {
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: e instanceof Error ? e.message : String(e)
|
|
616
|
+
};
|
|
617
|
+
}
|
|
1294
618
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
async function sizukuEffect(code, output, comment, type, relation) {
|
|
1304
|
-
const importLine = `import { Schema } from 'effect'`;
|
|
1305
|
-
const baseSchemas = extractSchemas(code, "effect");
|
|
1306
|
-
const relationSchemas = extractRelationSchemas(code, "effect");
|
|
1307
|
-
const effectGeneratedCode = [
|
|
1308
|
-
importLine,
|
|
1309
|
-
"",
|
|
1310
|
-
...baseSchemas.map((schema) => effectCode(schema, comment ?? false, type ?? false)),
|
|
1311
|
-
...relation ? relationSchemas.map((schema) => makeRelationEffectCode(schema, type ?? false)) : []
|
|
1312
|
-
].join("\n");
|
|
1313
|
-
const mkdirResult = await mkdir(path.dirname(output));
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/generator/emit.ts
|
|
621
|
+
async function emit(code, dir, output) {
|
|
622
|
+
const [fmtResult, mkdirResult] = await Promise.all([fmt(code), mkdir(dir)]);
|
|
623
|
+
if (!fmtResult.ok) return {
|
|
624
|
+
ok: false,
|
|
625
|
+
error: fmtResult.error
|
|
626
|
+
};
|
|
1314
627
|
if (!mkdirResult.ok) return {
|
|
1315
628
|
ok: false,
|
|
1316
629
|
error: mkdirResult.error
|
|
1317
630
|
};
|
|
1318
|
-
const
|
|
1319
|
-
if (!
|
|
631
|
+
const writeResult = await writeFile(output, fmtResult.value);
|
|
632
|
+
if (!writeResult.ok) return {
|
|
1320
633
|
ok: false,
|
|
1321
|
-
error:
|
|
634
|
+
error: writeResult.error
|
|
1322
635
|
};
|
|
1323
636
|
return {
|
|
1324
637
|
ok: true,
|
|
1325
638
|
value: void 0
|
|
1326
639
|
};
|
|
1327
640
|
}
|
|
1328
|
-
|
|
1329
641
|
//#endregion
|
|
1330
|
-
//#region src/generator/mermaid-er
|
|
1331
|
-
|
|
1332
|
-
const
|
|
1333
|
-
const
|
|
1334
|
-
"
|
|
1335
|
-
one: "||",
|
|
1336
|
-
"zero-many": "}o",
|
|
1337
|
-
many: "}|"
|
|
1338
|
-
};
|
|
1339
|
-
/**
|
|
1340
|
-
* Generate relation line from relation info.
|
|
1341
|
-
*
|
|
1342
|
-
* @param relation - The relation info
|
|
1343
|
-
* @returns The Mermaid ER relation line
|
|
1344
|
-
*/
|
|
1345
|
-
function makeRelationLine(relation) {
|
|
1346
|
-
const fromSymbol = RELATIONSHIPS.one;
|
|
1347
|
-
const toSymbol = relation.isRequired ? RELATIONSHIPS.many : RELATIONSHIPS["zero-many"];
|
|
1348
|
-
return ` ${relation.fromModel} ${fromSymbol}--${toSymbol} ${relation.toModel} : "(${relation.fromField}) - (${relation.toField})"`;
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Remove duplicate relations.
|
|
1352
|
-
*
|
|
1353
|
-
* @param relations - The relations to deduplicate
|
|
1354
|
-
* @returns The deduplicated relations
|
|
1355
|
-
*/
|
|
1356
|
-
function removeDuplicateRelations(relations) {
|
|
1357
|
-
return [...new Set(relations)];
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Generate ER content
|
|
1361
|
-
* @param relations - The relations extracted from .references() calls
|
|
1362
|
-
* @param tables - The tables to generate the ER content from
|
|
1363
|
-
* @returns The generated ER content
|
|
1364
|
-
*/
|
|
1365
|
-
function erContent(relations, tables) {
|
|
1366
|
-
const relationLines = removeDuplicateRelations(relations.map(makeRelationLine));
|
|
642
|
+
//#region src/generator/mermaid-er.ts
|
|
643
|
+
function mermaidER(code) {
|
|
644
|
+
const tables = parseTableInfo(code);
|
|
645
|
+
const relations = extractRelationsFromSchema(code);
|
|
646
|
+
const relationLines = [...new Set(relations.map((r) => ` ${r.fromModel} ||--${r.isRequired ? "}|" : "}o"} ${r.toModel} : "(${r.fromField}) - (${r.toField})"`))];
|
|
1367
647
|
const tableDefinitions = tables.flatMap((table) => [
|
|
1368
648
|
` ${table.name} {`,
|
|
1369
649
|
...table.fields.map((field) => {
|
|
@@ -1374,137 +654,114 @@ function erContent(relations, tables) {
|
|
|
1374
654
|
" }"
|
|
1375
655
|
]);
|
|
1376
656
|
return [
|
|
1377
|
-
|
|
657
|
+
"```mermaid",
|
|
658
|
+
"erDiagram",
|
|
1378
659
|
...relationLines,
|
|
1379
660
|
...tableDefinitions,
|
|
1380
|
-
|
|
661
|
+
"```"
|
|
1381
662
|
].join("\n");
|
|
1382
663
|
}
|
|
1383
|
-
|
|
1384
664
|
//#endregion
|
|
1385
|
-
//#region src/generator/
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
665
|
+
//#region src/generator/valibot.ts
|
|
666
|
+
function valibot(code, comment, type, relation) {
|
|
667
|
+
const c = comment ?? false;
|
|
668
|
+
const t = type ?? false;
|
|
669
|
+
const baseLines = extractSchemas(code, "valibot").map((schema) => {
|
|
670
|
+
const cap = makeCapitalized(schema.name);
|
|
671
|
+
const head = `export const ${cap}Schema = v.${resolveWrapperType(schema.objectType)}({${fieldDefinitions(schema, c)}})`;
|
|
672
|
+
return t ? `${head}\n\nexport type ${cap} = v.InferOutput<typeof ${cap}Schema>\n` : `${head}\n`;
|
|
673
|
+
});
|
|
674
|
+
const relationLines = relation ? extractRelationSchemas(code, "valibot").map((schema) => {
|
|
675
|
+
const relSchema = makeCapitalized(`${schema.name}Schema`);
|
|
676
|
+
const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
|
|
677
|
+
const obj = `\nexport const ${relSchema} = v.${resolveWrapperType(schema.objectType)}({...${baseSchema}.entries,${makeRelationFields(schema.fields)}})`;
|
|
678
|
+
if (!t) return obj;
|
|
679
|
+
const cap = makeCapitalized(schema.name);
|
|
680
|
+
return `${obj}\n\nexport type ${cap} = v.InferOutput<typeof ${cap}Schema>\n`;
|
|
681
|
+
}) : [];
|
|
682
|
+
return [
|
|
683
|
+
"import * as v from 'valibot'",
|
|
684
|
+
"",
|
|
685
|
+
...baseLines,
|
|
686
|
+
...relationLines
|
|
687
|
+
].join("\n");
|
|
1408
688
|
}
|
|
1409
|
-
|
|
1410
689
|
//#endregion
|
|
1411
|
-
//#region src/generator/
|
|
1412
|
-
function
|
|
1413
|
-
const
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
return
|
|
1430
|
-
|
|
1431
|
-
/**
|
|
1432
|
-
* Generate Valibot schema
|
|
1433
|
-
* @param code - The code to generate Valibot schema from
|
|
1434
|
-
* @param output - The output file path
|
|
1435
|
-
* @param comment - Whether to include comments in the generated code
|
|
1436
|
-
* @param type - Whether to include type information in the generated code
|
|
1437
|
-
*/
|
|
1438
|
-
async function sizukuValibot(code, output, comment, type, relation) {
|
|
1439
|
-
const baseSchemas = extractSchemas(code, "valibot");
|
|
1440
|
-
const relationSchemas = extractRelationSchemas(code, "valibot");
|
|
1441
|
-
const valibotGeneratedCode = [
|
|
1442
|
-
"import * as v from 'valibot'",
|
|
690
|
+
//#region src/generator/zod.ts
|
|
691
|
+
function zod(code, comment, type, version, relation) {
|
|
692
|
+
const c = comment ?? false;
|
|
693
|
+
const t = type ?? false;
|
|
694
|
+
const importLine = version === "mini" ? `import * as z from 'zod/mini'` : version === "@hono/zod-openapi" ? `import { z } from '@hono/zod-openapi'` : `import * as z from 'zod'`;
|
|
695
|
+
const baseLines = extractSchemas(code, "zod").map((schema) => {
|
|
696
|
+
const cap = makeCapitalized(schema.name);
|
|
697
|
+
const head = `export const ${cap}Schema = z.${resolveWrapperType(schema.objectType)}({${fieldDefinitions(schema, c)}})`;
|
|
698
|
+
return t ? `${head}\n\nexport type ${cap} = z.infer<typeof ${cap}Schema>\n` : `${head}\n`;
|
|
699
|
+
});
|
|
700
|
+
const relationLines = relation ? extractRelationSchemas(code, "zod").map((schema) => {
|
|
701
|
+
const relSchema = makeCapitalized(`${schema.name}Schema`);
|
|
702
|
+
const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
|
|
703
|
+
const obj = `\nexport const ${relSchema} = z.${resolveWrapperType(schema.objectType)}({...${baseSchema}.shape,${makeRelationFields(schema.fields)}})`;
|
|
704
|
+
if (!t) return obj;
|
|
705
|
+
const cap = makeCapitalized(schema.name);
|
|
706
|
+
return `${obj}\n\nexport type ${cap} = z.infer<typeof ${cap}Schema>\n`;
|
|
707
|
+
}) : [];
|
|
708
|
+
return [
|
|
709
|
+
importLine,
|
|
1443
710
|
"",
|
|
1444
|
-
...
|
|
1445
|
-
...
|
|
711
|
+
...baseLines,
|
|
712
|
+
...relationLines
|
|
1446
713
|
].join("\n");
|
|
1447
|
-
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region src/core/arktype.ts
|
|
717
|
+
function sizukuArktype(code, output, comment, type, relation) {
|
|
718
|
+
return emit(arktype(code, comment, type, relation), path.dirname(output), output);
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/core/dbml.ts
|
|
722
|
+
async function sizukuDbml(code, output) {
|
|
723
|
+
const content = dbml(code);
|
|
724
|
+
const mkdirResult = await mkdir(dirname(output));
|
|
1448
725
|
if (!mkdirResult.ok) return {
|
|
1449
726
|
ok: false,
|
|
1450
|
-
error: mkdirResult.error
|
|
727
|
+
error: `❌ Failed to create directory: ${mkdirResult.error}`
|
|
1451
728
|
};
|
|
1452
|
-
|
|
1453
|
-
|
|
729
|
+
if (output.endsWith(".png")) {
|
|
730
|
+
const writeResult = await writeFileBinary(output, new Resvg(run(content, "svg"), { font: { loadSystemFonts: true } }).render().asPng());
|
|
731
|
+
if (!writeResult.ok) return {
|
|
732
|
+
ok: false,
|
|
733
|
+
error: `❌ Failed to write PNG: ${writeResult.error}`
|
|
734
|
+
};
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
value: void 0
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const writeResult = await writeFile(output, content);
|
|
741
|
+
if (!writeResult.ok) return {
|
|
1454
742
|
ok: false,
|
|
1455
|
-
error:
|
|
743
|
+
error: `❌ Failed to write DBML: ${writeResult.error}`
|
|
1456
744
|
};
|
|
1457
745
|
return {
|
|
1458
746
|
ok: true,
|
|
1459
747
|
value: void 0
|
|
1460
748
|
};
|
|
1461
749
|
}
|
|
1462
|
-
|
|
1463
750
|
//#endregion
|
|
1464
|
-
//#region src/
|
|
1465
|
-
function
|
|
1466
|
-
|
|
1467
|
-
const objectCode = makeZodObject(fieldDefinitions(schema, comment), wrapperType);
|
|
1468
|
-
return `export const ${makeCapitalized(schema.name)}Schema = ${objectCode}`;
|
|
751
|
+
//#region src/core/effect.ts
|
|
752
|
+
function sizukuEffect(code, output, comment, type, relation) {
|
|
753
|
+
return emit(effect(code, comment, type, relation), path.dirname(output), output);
|
|
1469
754
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
function relationZodCode(schema, withType) {
|
|
1476
|
-
const relName = `${schema.name}Schema`;
|
|
1477
|
-
const baseSchema = `${makeCapitalized(schema.baseName)}Schema`;
|
|
1478
|
-
const fields = makeRelationFields(schema.fields);
|
|
1479
|
-
const objectType = resolveWrapperType(schema.objectType);
|
|
1480
|
-
const obj = `\nexport const ${makeCapitalized(relName)} = z.${objectType}({...${baseSchema}.shape,${fields}})`;
|
|
1481
|
-
if (withType) return `${obj}\n\n${infer(schema.name)}\n`;
|
|
1482
|
-
return `${obj}`;
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* Generate Zod schema
|
|
1486
|
-
* @param code - The code to generate Zod schema from
|
|
1487
|
-
* @param output - The output file path
|
|
1488
|
-
* @param comment - Whether to include comments in the generated code
|
|
1489
|
-
* @param type - Whether to include type information in the generated code
|
|
1490
|
-
* @param zod - The Zod version to use
|
|
1491
|
-
*/
|
|
1492
|
-
async function sizukuZod(code, output, comment, type, zod, relation) {
|
|
1493
|
-
const importLine = zod === "mini" ? `import * as z from 'zod/mini'` : zod === "@hono/zod-openapi" ? `import { z } from '@hono/zod-openapi'` : `import * as z from 'zod'`;
|
|
1494
|
-
const baseSchemas = extractSchemas(code, "zod");
|
|
1495
|
-
const relationSchemas = extractRelationSchemas(code, "zod");
|
|
1496
|
-
const zodGeneratedCode = [
|
|
1497
|
-
importLine,
|
|
1498
|
-
"",
|
|
1499
|
-
...baseSchemas.map((schema) => zodCode(schema, comment ?? false, type ?? false)),
|
|
1500
|
-
...relation ? relationSchemas.map((schema) => relationZodCode(schema, type ?? false)) : []
|
|
1501
|
-
].join("\n");
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/core/mermaid-er.ts
|
|
757
|
+
async function sizukuMermaidER(code, output) {
|
|
758
|
+
const content = mermaidER(code);
|
|
1502
759
|
const mkdirResult = await mkdir(path.dirname(output));
|
|
1503
760
|
if (!mkdirResult.ok) return {
|
|
1504
761
|
ok: false,
|
|
1505
762
|
error: mkdirResult.error
|
|
1506
763
|
};
|
|
1507
|
-
const writeFileResult = await writeFile(output,
|
|
764
|
+
const writeFileResult = await writeFile(output, content);
|
|
1508
765
|
if (!writeFileResult.ok) return {
|
|
1509
766
|
ok: false,
|
|
1510
767
|
error: writeFileResult.error
|
|
@@ -1514,7 +771,16 @@ async function sizukuZod(code, output, comment, type, zod, relation) {
|
|
|
1514
771
|
value: void 0
|
|
1515
772
|
};
|
|
1516
773
|
}
|
|
1517
|
-
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/core/valibot.ts
|
|
776
|
+
function sizukuValibot(code, output, comment, type, relation) {
|
|
777
|
+
return emit(valibot(code, comment, type, relation), path.dirname(output), output);
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/core/zod.ts
|
|
781
|
+
function sizukuZod(code, output, comment, type, version, relation) {
|
|
782
|
+
return emit(zod(code, comment, type, version, relation), path.dirname(output), output);
|
|
783
|
+
}
|
|
1518
784
|
//#endregion
|
|
1519
785
|
//#region src/cli/index.ts
|
|
1520
786
|
const HELP_TEXT = `💧 sizuku - Drizzle ORM schema tools
|
|
@@ -1533,124 +799,25 @@ Options:
|
|
|
1533
799
|
--no-with-comment Do not add JSDoc comments
|
|
1534
800
|
--no-with-relation Do not generate relation schemas
|
|
1535
801
|
-h, --help Display this help message`;
|
|
1536
|
-
/**
|
|
1537
|
-
* Detect output type from file extension
|
|
1538
|
-
*/
|
|
1539
|
-
function detectOutputType(output) {
|
|
1540
|
-
if (output.endsWith(".dbml")) return "dbml";
|
|
1541
|
-
if (output.endsWith(".png")) return "png";
|
|
1542
|
-
if (output.endsWith(".md")) return "mermaid";
|
|
1543
|
-
if (output.endsWith(".ts")) return "typescript";
|
|
1544
|
-
return null;
|
|
1545
|
-
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Parse CLI flags from argv (pure function)
|
|
1548
|
-
*/
|
|
1549
802
|
function parseFlags(argv) {
|
|
1550
|
-
const
|
|
803
|
+
const ZOD_VERSIONS = [
|
|
804
|
+
"v4",
|
|
805
|
+
"mini",
|
|
806
|
+
"@hono/zod-openapi"
|
|
807
|
+
];
|
|
1551
808
|
const zodVersionIndex = argv.findIndex((a) => a === "--zod-version" || a.startsWith("--zod-version="));
|
|
1552
|
-
const
|
|
1553
|
-
if (zodVersionIndex === -1) return void 0;
|
|
1554
|
-
const arg = argv[zodVersionIndex];
|
|
1555
|
-
const value = arg.includes("=") ? arg.split("=")[1] : argv[zodVersionIndex + 1];
|
|
1556
|
-
if (value === "v4" || value === "mini" || value === "@hono/zod-openapi") return value;
|
|
1557
|
-
})();
|
|
809
|
+
const zodVersionValue = zodVersionIndex === -1 ? void 0 : argv[zodVersionIndex].includes("=") ? argv[zodVersionIndex].split("=")[1] : argv[zodVersionIndex + 1];
|
|
1558
810
|
return {
|
|
1559
|
-
zod:
|
|
1560
|
-
valibot:
|
|
1561
|
-
arktype:
|
|
1562
|
-
effect:
|
|
1563
|
-
zodVersion,
|
|
1564
|
-
exportTypes: !
|
|
1565
|
-
withComment: !
|
|
1566
|
-
withRelation: !
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
/**
|
|
1570
|
-
* Strip import lines from source code and return only schema definition lines
|
|
1571
|
-
*/
|
|
1572
|
-
function stripImports(content) {
|
|
1573
|
-
const lines = content.split("\n");
|
|
1574
|
-
const codeStart = lines.findIndex((line) => !line.trim().startsWith("import") && line.trim() !== "");
|
|
1575
|
-
return lines.slice(codeStart);
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Run sizuku in direct CLI mode for diagram output
|
|
1579
|
-
*/
|
|
1580
|
-
async function sizukuDirectDiagram(input, output, outputType) {
|
|
1581
|
-
const contentResult = readFileSync(input);
|
|
1582
|
-
if (!contentResult.ok) return {
|
|
1583
|
-
ok: false,
|
|
1584
|
-
error: `Failed to read input: ${contentResult.error}`
|
|
1585
|
-
};
|
|
1586
|
-
const code = stripImports(contentResult.value);
|
|
1587
|
-
if (outputType === "dbml" || outputType === "png") {
|
|
1588
|
-
const result = await sizukuDbml(code, output);
|
|
1589
|
-
if (!result.ok) return result;
|
|
1590
|
-
return {
|
|
1591
|
-
ok: true,
|
|
1592
|
-
value: `💧 Generated ${outputType === "png" ? "PNG" : "DBML"} at: ${output}`
|
|
1593
|
-
};
|
|
1594
|
-
}
|
|
1595
|
-
const result = await sizukuMermaidER(code, output);
|
|
1596
|
-
if (!result.ok) return result;
|
|
1597
|
-
return {
|
|
1598
|
-
ok: true,
|
|
1599
|
-
value: `💧 Generated Mermaid ER at: ${output}`
|
|
811
|
+
zod: argv.includes("--zod"),
|
|
812
|
+
valibot: argv.includes("--valibot"),
|
|
813
|
+
arktype: argv.includes("--arktype"),
|
|
814
|
+
effect: argv.includes("--effect"),
|
|
815
|
+
zodVersion: ZOD_VERSIONS.find((v) => v === zodVersionValue),
|
|
816
|
+
exportTypes: !argv.includes("--no-export-types"),
|
|
817
|
+
withComment: !argv.includes("--no-with-comment"),
|
|
818
|
+
withRelation: !argv.includes("--no-with-relation")
|
|
1600
819
|
};
|
|
1601
820
|
}
|
|
1602
|
-
/**
|
|
1603
|
-
* Resolve which schema library to use from flags
|
|
1604
|
-
*/
|
|
1605
|
-
function resolveSchemaLibrary(flags) {
|
|
1606
|
-
if (flags.zod) return {
|
|
1607
|
-
name: "zod",
|
|
1608
|
-
label: "Zod"
|
|
1609
|
-
};
|
|
1610
|
-
if (flags.valibot) return {
|
|
1611
|
-
name: "valibot",
|
|
1612
|
-
label: "Valibot"
|
|
1613
|
-
};
|
|
1614
|
-
if (flags.arktype) return {
|
|
1615
|
-
name: "arktype",
|
|
1616
|
-
label: "ArkType"
|
|
1617
|
-
};
|
|
1618
|
-
if (flags.effect) return {
|
|
1619
|
-
name: "effect",
|
|
1620
|
-
label: "Effect"
|
|
1621
|
-
};
|
|
1622
|
-
return null;
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* Run sizuku in direct CLI mode for validation schema output
|
|
1626
|
-
*/
|
|
1627
|
-
async function sizukuDirectSchema(input, output, flags) {
|
|
1628
|
-
const lib = resolveSchemaLibrary(flags);
|
|
1629
|
-
if (!lib) return {
|
|
1630
|
-
ok: false,
|
|
1631
|
-
error: "Specify --zod, --valibot, --arktype, or --effect for .ts output"
|
|
1632
|
-
};
|
|
1633
|
-
const contentResult = readFileSync(input);
|
|
1634
|
-
if (!contentResult.ok) return {
|
|
1635
|
-
ok: false,
|
|
1636
|
-
error: `Failed to read input: ${contentResult.error}`
|
|
1637
|
-
};
|
|
1638
|
-
const code = stripImports(contentResult.value);
|
|
1639
|
-
const result = await {
|
|
1640
|
-
zod: () => sizukuZod(code, output, flags.withComment, flags.exportTypes, flags.zodVersion, flags.withRelation),
|
|
1641
|
-
valibot: () => sizukuValibot(code, output, flags.withComment, flags.exportTypes, flags.withRelation),
|
|
1642
|
-
arktype: () => sizukuArktype(code, output, flags.withComment, flags.exportTypes, flags.withRelation),
|
|
1643
|
-
effect: () => sizukuEffect(code, output, flags.withComment, flags.exportTypes, flags.withRelation)
|
|
1644
|
-
}[lib.name]();
|
|
1645
|
-
if (!result.ok) return result;
|
|
1646
|
-
return {
|
|
1647
|
-
ok: true,
|
|
1648
|
-
value: `💧 Generated ${lib.label} schema at: ${output}`
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* Main entry point
|
|
1653
|
-
*/
|
|
1654
821
|
async function sizuku() {
|
|
1655
822
|
const argv = process.argv.slice(2);
|
|
1656
823
|
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) return {
|
|
@@ -1672,21 +839,72 @@ async function sizuku() {
|
|
|
1672
839
|
ok: false,
|
|
1673
840
|
error: "Missing output file path after -o"
|
|
1674
841
|
};
|
|
1675
|
-
const outputType =
|
|
842
|
+
const outputType = output.endsWith(".dbml") ? "dbml" : output.endsWith(".png") ? "png" : output.endsWith(".md") ? "mermaid" : output.endsWith(".ts") ? "typescript" : null;
|
|
1676
843
|
if (!outputType) return {
|
|
1677
844
|
ok: false,
|
|
1678
845
|
error: `Unsupported output format: ${output}. Supported: .dbml, .png, .md, .ts`
|
|
1679
846
|
};
|
|
1680
|
-
|
|
1681
|
-
|
|
847
|
+
const flags = outputType === "typescript" ? parseFlags(argv) : null;
|
|
848
|
+
const lib = flags === null ? null : flags.zod ? {
|
|
849
|
+
name: "zod",
|
|
850
|
+
label: "Zod"
|
|
851
|
+
} : flags.valibot ? {
|
|
852
|
+
name: "valibot",
|
|
853
|
+
label: "Valibot"
|
|
854
|
+
} : flags.arktype ? {
|
|
855
|
+
name: "arktype",
|
|
856
|
+
label: "ArkType"
|
|
857
|
+
} : flags.effect ? {
|
|
858
|
+
name: "effect",
|
|
859
|
+
label: "Effect"
|
|
860
|
+
} : null;
|
|
861
|
+
if (outputType === "typescript" && !lib) return {
|
|
862
|
+
ok: false,
|
|
863
|
+
error: "Specify --zod, --valibot, --arktype, or --effect for .ts output"
|
|
864
|
+
};
|
|
865
|
+
const contentResult = readFileSync(input);
|
|
866
|
+
if (!contentResult.ok) return {
|
|
867
|
+
ok: false,
|
|
868
|
+
error: `Failed to read input: ${contentResult.error}`
|
|
869
|
+
};
|
|
870
|
+
const code = stripImports(contentResult.value);
|
|
871
|
+
if (outputType === "dbml" || outputType === "png") {
|
|
872
|
+
const result = await sizukuDbml(code, output);
|
|
873
|
+
if (!result.ok) return result;
|
|
874
|
+
return {
|
|
875
|
+
ok: true,
|
|
876
|
+
value: `💧 Generated ${outputType === "png" ? "PNG" : "DBML"} at: ${output}`
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (outputType === "mermaid") {
|
|
880
|
+
const result = await sizukuMermaidER(code, output);
|
|
881
|
+
if (!result.ok) return result;
|
|
882
|
+
return {
|
|
883
|
+
ok: true,
|
|
884
|
+
value: `💧 Generated Mermaid ER at: ${output}`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (!lib || !flags) return {
|
|
888
|
+
ok: false,
|
|
889
|
+
error: "internal: missing lib/flags"
|
|
890
|
+
};
|
|
891
|
+
const result = await (() => {
|
|
892
|
+
if (lib.name === "zod") return sizukuZod(code, output, flags.withComment, flags.exportTypes, flags.zodVersion, flags.withRelation);
|
|
893
|
+
if (lib.name === "valibot") return sizukuValibot(code, output, flags.withComment, flags.exportTypes, flags.withRelation);
|
|
894
|
+
if (lib.name === "arktype") return sizukuArktype(code, output, flags.withComment, flags.exportTypes, flags.withRelation);
|
|
895
|
+
return sizukuEffect(code, output, flags.withComment, flags.exportTypes, flags.withRelation);
|
|
896
|
+
})();
|
|
897
|
+
if (!result.ok) return result;
|
|
898
|
+
return {
|
|
899
|
+
ok: true,
|
|
900
|
+
value: `💧 Generated ${lib.label} schema at: ${output}`
|
|
901
|
+
};
|
|
1682
902
|
}
|
|
1683
|
-
|
|
1684
903
|
//#endregion
|
|
1685
904
|
//#region src/index.ts
|
|
1686
905
|
sizuku().then((result) => {
|
|
1687
906
|
if (result?.ok) console.log(result.value);
|
|
1688
907
|
else console.error(result?.error);
|
|
1689
908
|
});
|
|
1690
|
-
|
|
1691
909
|
//#endregion
|
|
1692
|
-
export {
|
|
910
|
+
export {};
|