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