mutano 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +412 -33
- package/dist/main.d.ts +70 -14
- package/dist/main.js +579 -96
- package/package.json +31 -29
package/dist/main.js
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
const enumDeclarations = {};
|
|
3
4
|
import {
|
|
4
5
|
createPrismaSchemaBuilder
|
|
5
6
|
} from "@mrleebo/prisma-ast";
|
|
6
7
|
import camelCase from "camelcase";
|
|
7
8
|
import fs from "fs-extra";
|
|
8
9
|
import knex from "knex";
|
|
10
|
+
const extractTSExpression = (comment) => {
|
|
11
|
+
const start = comment.indexOf("@ts(");
|
|
12
|
+
if (start === -1) return null;
|
|
13
|
+
const typeLen = 4;
|
|
14
|
+
let position = start + typeLen;
|
|
15
|
+
let depth = 1;
|
|
16
|
+
while (position < comment.length && depth > 0) {
|
|
17
|
+
const char = comment[position];
|
|
18
|
+
if (char === "(" || char === "{" || char === "<" || char === "[") {
|
|
19
|
+
depth++;
|
|
20
|
+
} else if (char === ")" || char === "}" || char === ">" || char === "]") {
|
|
21
|
+
depth--;
|
|
22
|
+
if (depth === 0) {
|
|
23
|
+
const extracted = comment.substring(start + typeLen, position);
|
|
24
|
+
return extracted;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
position++;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
9
31
|
function extractZodExpression(comment) {
|
|
10
32
|
const zodStart = comment.indexOf("@zod(");
|
|
11
33
|
if (zodStart === -1) return null;
|
|
@@ -38,6 +60,14 @@ const prismaValidTypes = [
|
|
|
38
60
|
];
|
|
39
61
|
const dateTypes = {
|
|
40
62
|
mysql: ["date", "datetime", "timestamp"],
|
|
63
|
+
postgres: [
|
|
64
|
+
"date",
|
|
65
|
+
"timestamp",
|
|
66
|
+
"timestamptz",
|
|
67
|
+
"timestamp without time zone",
|
|
68
|
+
"timestamp with time zone"
|
|
69
|
+
],
|
|
70
|
+
sqlite: ["datetime"],
|
|
41
71
|
prisma: ["DateTime"]
|
|
42
72
|
};
|
|
43
73
|
const stringTypes = {
|
|
@@ -53,27 +83,153 @@ const stringTypes = {
|
|
|
53
83
|
"char",
|
|
54
84
|
"varchar"
|
|
55
85
|
],
|
|
86
|
+
postgres: [
|
|
87
|
+
"text",
|
|
88
|
+
"character varying",
|
|
89
|
+
"varchar",
|
|
90
|
+
"char",
|
|
91
|
+
"character",
|
|
92
|
+
"json",
|
|
93
|
+
"jsonb",
|
|
94
|
+
"uuid",
|
|
95
|
+
"time",
|
|
96
|
+
"timetz",
|
|
97
|
+
"interval",
|
|
98
|
+
"name",
|
|
99
|
+
"citext",
|
|
100
|
+
"numeric",
|
|
101
|
+
"decimal"
|
|
102
|
+
],
|
|
103
|
+
sqlite: [
|
|
104
|
+
"text",
|
|
105
|
+
"character",
|
|
106
|
+
"varchar",
|
|
107
|
+
"varying character",
|
|
108
|
+
"nchar",
|
|
109
|
+
"native character",
|
|
110
|
+
"nvarchar",
|
|
111
|
+
"clob",
|
|
112
|
+
"json"
|
|
113
|
+
],
|
|
56
114
|
prisma: ["String", "Decimal", "BigInt", "Bytes", "Json"]
|
|
57
115
|
};
|
|
58
116
|
const numberTypes = {
|
|
59
117
|
mysql: ["smallint", "mediumint", "int", "bigint", "float", "double"],
|
|
118
|
+
postgres: [
|
|
119
|
+
"smallint",
|
|
120
|
+
"integer",
|
|
121
|
+
"bigint",
|
|
122
|
+
"decimal",
|
|
123
|
+
"numeric",
|
|
124
|
+
"real",
|
|
125
|
+
"double precision",
|
|
126
|
+
"serial",
|
|
127
|
+
"bigserial"
|
|
128
|
+
],
|
|
129
|
+
sqlite: [
|
|
130
|
+
"int",
|
|
131
|
+
"integer",
|
|
132
|
+
"tinyint",
|
|
133
|
+
"smallint",
|
|
134
|
+
"mediumint",
|
|
135
|
+
"bigint",
|
|
136
|
+
"unsigned big int",
|
|
137
|
+
"int2",
|
|
138
|
+
"int8",
|
|
139
|
+
"real",
|
|
140
|
+
"double",
|
|
141
|
+
"double precision",
|
|
142
|
+
"float",
|
|
143
|
+
"numeric",
|
|
144
|
+
"decimal"
|
|
145
|
+
],
|
|
60
146
|
prisma: ["Int", "Float"]
|
|
61
147
|
};
|
|
62
|
-
const booleanTypes = {
|
|
63
|
-
|
|
64
|
-
|
|
148
|
+
const booleanTypes = {
|
|
149
|
+
mysql: ["tinyint"],
|
|
150
|
+
postgres: ["boolean", "bool"],
|
|
151
|
+
sqlite: ["boolean"],
|
|
152
|
+
prisma: ["Boolean"]
|
|
153
|
+
};
|
|
154
|
+
const enumTypes = {
|
|
155
|
+
mysql: ["enum"],
|
|
156
|
+
postgres: ["USER-DEFINED"],
|
|
157
|
+
sqlite: [],
|
|
158
|
+
// SQLite doesn't have native enum types
|
|
159
|
+
prisma: ["Enum"]
|
|
160
|
+
};
|
|
161
|
+
const enumRegex = /enum\(([^)]+)\)/;
|
|
162
|
+
function getType(op, desc, config, destination, tableName) {
|
|
65
163
|
const schemaType = config.origin.type;
|
|
66
164
|
const { Default, Extra, Null, Type, Comment, EnumOptions } = desc;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
165
|
+
const isZodDestination = destination.type === "zod";
|
|
166
|
+
const isTsDestination = destination.type === "ts";
|
|
167
|
+
const isKyselyDestination = destination.type === "kysely";
|
|
168
|
+
const isNullish = isZodDestination && destination.type === "zod" && destination.nullish === true;
|
|
169
|
+
const isTrim = isZodDestination && destination.type === "zod" && destination.useTrim === true && op !== "selectable";
|
|
170
|
+
const isUseDateType = isZodDestination && destination.type === "zod" && destination.useDateType === true;
|
|
69
171
|
const hasDefaultValue = Default !== null && op !== "selectable";
|
|
70
172
|
const isGenerated = ["DEFAULT_GENERATED", "auto_increment"].includes(Extra);
|
|
71
173
|
const isNull = Null === "YES";
|
|
72
174
|
if (isGenerated && !isNull && ["insertable", "updateable"].includes(op))
|
|
73
175
|
return;
|
|
74
|
-
const isRequiredString =
|
|
75
|
-
const isUseDateType = config.useDateType && config.useDateType === true;
|
|
176
|
+
const isRequiredString = destination.type === "zod" && destination.requiredString === true && op !== "selectable";
|
|
76
177
|
const type = schemaType === "mysql" ? Type.split("(")[0].split(" ")[0] : Type;
|
|
178
|
+
if (isTsDestination || isKyselyDestination) {
|
|
179
|
+
const tsOverrideType = config.magicComments ? extractTSExpression(Comment) : null;
|
|
180
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
181
|
+
if (tsOverrideType) {
|
|
182
|
+
return shouldBeNullable ? tsOverrideType.includes("| null") ? tsOverrideType : `${tsOverrideType} | null` : tsOverrideType;
|
|
183
|
+
}
|
|
184
|
+
if (dateTypes[schemaType].includes(type)) {
|
|
185
|
+
return shouldBeNullable ? "Date | null" : "Date";
|
|
186
|
+
}
|
|
187
|
+
if (stringTypes[schemaType].includes(type)) {
|
|
188
|
+
return shouldBeNullable ? "string | null" : "string";
|
|
189
|
+
}
|
|
190
|
+
if (numberTypes[schemaType].includes(type)) {
|
|
191
|
+
return shouldBeNullable ? "number | null" : "number";
|
|
192
|
+
}
|
|
193
|
+
if (booleanTypes[schemaType].includes(type)) {
|
|
194
|
+
return shouldBeNullable ? "boolean | null" : "boolean";
|
|
195
|
+
}
|
|
196
|
+
if (schemaType !== "sqlite" && enumTypes[schemaType].includes(type)) {
|
|
197
|
+
const enumType = destination.type === "ts" ? destination.enumType || "union" : "union";
|
|
198
|
+
let enumValues = [];
|
|
199
|
+
if (schemaType === "mysql") {
|
|
200
|
+
const matches = Type.match(enumRegex);
|
|
201
|
+
if (matches?.[1]) {
|
|
202
|
+
enumValues = matches[1].split(",").map((v) => v.trim());
|
|
203
|
+
}
|
|
204
|
+
} else if (EnumOptions && EnumOptions.length > 0) {
|
|
205
|
+
enumValues = EnumOptions.map((e) => `'${e}'`);
|
|
206
|
+
}
|
|
207
|
+
if (enumValues.length === 0) {
|
|
208
|
+
return isNull ? "string | null" : "string";
|
|
209
|
+
}
|
|
210
|
+
if (enumType === "enum") {
|
|
211
|
+
const enumName = camelCase(`${desc.Field}_enum`, { pascalCase: true });
|
|
212
|
+
const enumDeclaration = `enum ${enumName} {
|
|
213
|
+
${enumValues.map((v) => {
|
|
214
|
+
const cleanName = v.replace(/['"]/g, "");
|
|
215
|
+
return `${cleanName} = ${v}`;
|
|
216
|
+
}).join(",\n ")}
|
|
217
|
+
}`;
|
|
218
|
+
if (tableName) {
|
|
219
|
+
if (!enumDeclarations[tableName]) {
|
|
220
|
+
enumDeclarations[tableName] = [];
|
|
221
|
+
}
|
|
222
|
+
if (!enumDeclarations[tableName].includes(enumDeclaration)) {
|
|
223
|
+
enumDeclarations[tableName].push(enumDeclaration);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return shouldBeNullable ? `${enumName} | null` : enumName;
|
|
227
|
+
}
|
|
228
|
+
const unionType = enumValues.join(" | ");
|
|
229
|
+
return shouldBeNullable ? `(${unionType}) | null` : unionType;
|
|
230
|
+
}
|
|
231
|
+
return "any";
|
|
232
|
+
}
|
|
77
233
|
const zDate = [
|
|
78
234
|
"z.union([z.number(), z.string(), z.date()]).pipe(z.coerce.date())"
|
|
79
235
|
];
|
|
@@ -88,8 +244,19 @@ function getType(op, desc, config) {
|
|
|
88
244
|
const nonnegative = "nonnegative()";
|
|
89
245
|
const isUpdateableFormat = op === "updateable" && !isNull && !hasDefaultValue;
|
|
90
246
|
const min1 = "min(1)";
|
|
91
|
-
const zodOverrideType = config.
|
|
92
|
-
|
|
247
|
+
const zodOverrideType = config.magicComments ? extractZodExpression(Comment) : null;
|
|
248
|
+
let typeOverride = zodOverrideType;
|
|
249
|
+
if (!typeOverride && config.origin.overrideTypes) {
|
|
250
|
+
if (config.origin.type === "mysql") {
|
|
251
|
+
typeOverride = config.origin.overrideTypes[type] || null;
|
|
252
|
+
} else if (config.origin.type === "postgres") {
|
|
253
|
+
typeOverride = config.origin.overrideTypes[type] || null;
|
|
254
|
+
} else if (config.origin.type === "sqlite") {
|
|
255
|
+
typeOverride = config.origin.overrideTypes[type] || null;
|
|
256
|
+
} else if (config.origin.type === "prisma") {
|
|
257
|
+
typeOverride = config.origin.overrideTypes[type] || null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
93
260
|
const generateDateLikeField = () => {
|
|
94
261
|
const field = typeOverride ? [typeOverride] : dateField;
|
|
95
262
|
if (isNull && !typeOverride) field.push(nullable);
|
|
@@ -114,8 +281,13 @@ function getType(op, desc, config) {
|
|
|
114
281
|
if (isNull && !typeOverride) field.push(nullable);
|
|
115
282
|
else if (hasDefaultValue || !hasDefaultValue && isGenerated)
|
|
116
283
|
field.push(optional);
|
|
117
|
-
if (hasDefaultValue && !isGenerated)
|
|
118
|
-
|
|
284
|
+
if (hasDefaultValue && !isGenerated) {
|
|
285
|
+
if (Default === "true" || Default === "false") {
|
|
286
|
+
field.push(`default(${Default})`);
|
|
287
|
+
} else {
|
|
288
|
+
field.push(`default(${Boolean(+Default)})`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
119
291
|
if (isUpdateableFormat) field.push(optional);
|
|
120
292
|
return field.join(".");
|
|
121
293
|
};
|
|
@@ -144,36 +316,304 @@ function getType(op, desc, config) {
|
|
|
144
316
|
if (stringTypes[schemaType].includes(type)) return generateStringLikeField();
|
|
145
317
|
if (numberTypes[schemaType].includes(type)) return generateNumberLikeField();
|
|
146
318
|
if (booleanTypes[schemaType].includes(type)) return generateBooleanLikeField();
|
|
147
|
-
if (enumTypes[schemaType].includes(type))
|
|
319
|
+
if (schemaType !== "sqlite" && enumTypes[schemaType].includes(type))
|
|
320
|
+
return generateEnumLikeField();
|
|
148
321
|
throw new Error(`Unsupported column type: ${type}`);
|
|
149
322
|
}
|
|
323
|
+
function generateContent({
|
|
324
|
+
table,
|
|
325
|
+
describes,
|
|
326
|
+
config,
|
|
327
|
+
destination,
|
|
328
|
+
isCamelCase,
|
|
329
|
+
enumDeclarations: enumDeclarations2,
|
|
330
|
+
defaultZodHeader: defaultZodHeader2,
|
|
331
|
+
defaultKyselyHeader: defaultKyselyHeader2
|
|
332
|
+
}) {
|
|
333
|
+
let content = "";
|
|
334
|
+
if (destination.type === "kysely") {
|
|
335
|
+
const header = destination.header;
|
|
336
|
+
const schemaName = destination.schemaName || "DB";
|
|
337
|
+
content = header ? `${header}
|
|
338
|
+
|
|
339
|
+
` : defaultKyselyHeader2;
|
|
340
|
+
content += `// JSON type definitions
|
|
341
|
+
export type Json = ColumnType<JsonValue, string, string>;
|
|
342
|
+
|
|
343
|
+
export type JsonArray = JsonValue[];
|
|
344
|
+
|
|
345
|
+
export type JsonObject = {
|
|
346
|
+
[x: string]: JsonValue | undefined;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export type JsonPrimitive = boolean | number | string | null;
|
|
350
|
+
|
|
351
|
+
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
|
352
|
+
|
|
353
|
+
`;
|
|
354
|
+
content += `// Kysely type definitions for ${table}
|
|
355
|
+
`;
|
|
356
|
+
content += `
|
|
357
|
+
// This interface defines the structure of the '${table}' table
|
|
358
|
+
export interface ${camelCase(table, { pascalCase: true })}Table {`;
|
|
359
|
+
for (const desc of describes) {
|
|
360
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
361
|
+
const type = getType("table", desc, config, destination, table);
|
|
362
|
+
if (type) {
|
|
363
|
+
let kyselyType = type;
|
|
364
|
+
const isAutoIncrement = desc.Extra.toLowerCase().includes("auto_increment");
|
|
365
|
+
const isDefaultGenerated = desc.Extra.toLowerCase().includes("default_generated");
|
|
366
|
+
const isNullable = desc.Null === "YES";
|
|
367
|
+
const isJsonField = desc.Type.toLowerCase().includes("json");
|
|
368
|
+
if (isJsonField) {
|
|
369
|
+
kyselyType = "Json";
|
|
370
|
+
} else if (isAutoIncrement || isDefaultGenerated) {
|
|
371
|
+
kyselyType = `Generated<${kyselyType}>`;
|
|
372
|
+
}
|
|
373
|
+
if (isNullable && !isJsonField) {
|
|
374
|
+
if (!kyselyType.includes("| null")) {
|
|
375
|
+
kyselyType = `${kyselyType} | null`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
content = `${content}
|
|
379
|
+
${field}: ${kyselyType};`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
content = `${content}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Define the database interface
|
|
386
|
+
export interface ${schemaName} {
|
|
387
|
+
${table}: ${camelCase(table, { pascalCase: true })}Table;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Use these types for inserting, selecting and updating the table
|
|
391
|
+
export type ${camelCase(table, { pascalCase: true })} = Selectable<${camelCase(table, { pascalCase: true })}Table>;
|
|
392
|
+
export type New${camelCase(table, { pascalCase: true })} = Insertable<${camelCase(table, { pascalCase: true })}Table>;
|
|
393
|
+
export type ${camelCase(table, { pascalCase: true })}Update = Updateable<${camelCase(table, { pascalCase: true })}Table>;
|
|
394
|
+
`;
|
|
395
|
+
} else if (destination.type === "ts") {
|
|
396
|
+
const modelType = destination.modelType || "interface";
|
|
397
|
+
const isInterface = modelType === "interface";
|
|
398
|
+
const header = destination.header;
|
|
399
|
+
content = header ? `${header}
|
|
400
|
+
|
|
401
|
+
` : "";
|
|
402
|
+
content += `// TypeScript ${isInterface ? "interfaces" : "types"} for ${table}`;
|
|
403
|
+
if (enumDeclarations2[table] && enumDeclarations2[table].length > 0) {
|
|
404
|
+
content += "\n\n// Enum declarations";
|
|
405
|
+
for (const enumDecl of enumDeclarations2[table]) {
|
|
406
|
+
content += `
|
|
407
|
+
${enumDecl}`;
|
|
408
|
+
}
|
|
409
|
+
content += "\n";
|
|
410
|
+
}
|
|
411
|
+
if (isInterface) {
|
|
412
|
+
content += `
|
|
413
|
+
export interface ${camelCase(table, { pascalCase: true })} {`;
|
|
414
|
+
} else {
|
|
415
|
+
content += `
|
|
416
|
+
export type ${camelCase(table, { pascalCase: true })} = {`;
|
|
417
|
+
}
|
|
418
|
+
for (const desc of describes) {
|
|
419
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
420
|
+
const type = getType("table", desc, config, destination, table);
|
|
421
|
+
if (type) {
|
|
422
|
+
content = `${content}
|
|
423
|
+
${field}: ${type};`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
content = `${content}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
`;
|
|
430
|
+
if (isInterface) {
|
|
431
|
+
content += `export interface Insertable${camelCase(table, { pascalCase: true })} {`;
|
|
432
|
+
} else {
|
|
433
|
+
content += `export type Insertable${camelCase(table, { pascalCase: true })} = {`;
|
|
434
|
+
}
|
|
435
|
+
for (const desc of describes) {
|
|
436
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
437
|
+
const type = getType("insertable", desc, config, destination, table);
|
|
438
|
+
if (type) {
|
|
439
|
+
content = `${content}
|
|
440
|
+
${field}: ${type};`;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
content = `${content}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
`;
|
|
447
|
+
if (isInterface) {
|
|
448
|
+
content += `export interface Updateable${camelCase(table, { pascalCase: true })} {`;
|
|
449
|
+
} else {
|
|
450
|
+
content += `export type Updateable${camelCase(table, { pascalCase: true })} = {`;
|
|
451
|
+
}
|
|
452
|
+
for (const desc of describes) {
|
|
453
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
454
|
+
const type = getType("updateable", desc, config, destination, table);
|
|
455
|
+
if (type) {
|
|
456
|
+
content = `${content}
|
|
457
|
+
${field}: ${type};`;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
content = `${content}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
`;
|
|
464
|
+
if (isInterface) {
|
|
465
|
+
content += `export interface Selectable${camelCase(table, { pascalCase: true })} {`;
|
|
466
|
+
} else {
|
|
467
|
+
content += `export type Selectable${camelCase(table, { pascalCase: true })} = {`;
|
|
468
|
+
}
|
|
469
|
+
for (const desc of describes) {
|
|
470
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
471
|
+
const type = getType("selectable", desc, config, destination, table);
|
|
472
|
+
if (type) {
|
|
473
|
+
content = `${content}
|
|
474
|
+
${field}: ${type};`;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
content = `${content}
|
|
478
|
+
}
|
|
479
|
+
`;
|
|
480
|
+
} else if (destination.type === "zod") {
|
|
481
|
+
const header = destination.header;
|
|
482
|
+
content = header ? `${header}
|
|
483
|
+
|
|
484
|
+
` : defaultZodHeader2;
|
|
485
|
+
content += `export const ${table} = z.object({`;
|
|
486
|
+
for (const desc of describes) {
|
|
487
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
488
|
+
const type = getType("table", desc, config, destination, table);
|
|
489
|
+
if (type) {
|
|
490
|
+
content = `${content}
|
|
491
|
+
${field}: ${type},`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
content = `${content}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
export const insertable_${table} = z.object({`;
|
|
498
|
+
for (const desc of describes) {
|
|
499
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
500
|
+
const type = getType("insertable", desc, config, destination, table);
|
|
501
|
+
if (type) {
|
|
502
|
+
content = `${content}
|
|
503
|
+
${field}: ${type},`;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
content = `${content}
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
export const updateable_${table} = z.object({`;
|
|
510
|
+
for (const desc of describes) {
|
|
511
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
512
|
+
const type = getType("updateable", desc, config, destination, table);
|
|
513
|
+
if (type) {
|
|
514
|
+
content = `${content}
|
|
515
|
+
${field}: ${type},`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
content = `${content}
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
export const selectable_${table} = z.object({`;
|
|
522
|
+
for (const desc of describes) {
|
|
523
|
+
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
524
|
+
const type = getType("selectable", desc, config, destination, table);
|
|
525
|
+
if (type) {
|
|
526
|
+
content = `${content}
|
|
527
|
+
${field}: ${type},`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
content = `${content}
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
export type ${camelCase(`${table}Type`, {
|
|
534
|
+
pascalCase: true
|
|
535
|
+
})} = z.infer<typeof ${table}>
|
|
536
|
+
export type Insertable${camelCase(`${table}Type`, {
|
|
537
|
+
pascalCase: true
|
|
538
|
+
})} = z.infer<typeof insertable_${table}>
|
|
539
|
+
export type Updateable${camelCase(`${table}Type`, {
|
|
540
|
+
pascalCase: true
|
|
541
|
+
})} = z.infer<typeof updateable_${table}>
|
|
542
|
+
export type Selectable${camelCase(`${table}Type`, {
|
|
543
|
+
pascalCase: true
|
|
544
|
+
})} = z.infer<typeof selectable_${table}>
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
return content;
|
|
548
|
+
}
|
|
549
|
+
const defaultKyselyHeader = "import { Generated, ColumnType, Selectable, Insertable, Updateable } from 'kysely';\n\n";
|
|
550
|
+
const defaultZodHeader = "import { z } from 'zod';\n\n";
|
|
150
551
|
async function generate(config) {
|
|
151
552
|
let tables = [];
|
|
152
553
|
let prismaTables = [];
|
|
153
554
|
let schema = null;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
555
|
+
let db = null;
|
|
556
|
+
if (config.destinations.length === 0) {
|
|
557
|
+
throw new Error("Empty destinations object.");
|
|
558
|
+
}
|
|
559
|
+
const dryRunOutput = {};
|
|
560
|
+
if (config.origin.type === "mysql") {
|
|
561
|
+
db = knex({
|
|
562
|
+
client: "mysql2",
|
|
563
|
+
connection: {
|
|
564
|
+
host: config.origin.host,
|
|
565
|
+
port: config.origin.port,
|
|
566
|
+
user: config.origin.user,
|
|
567
|
+
password: config.origin.password,
|
|
568
|
+
database: config.origin.database,
|
|
569
|
+
ssl: config.origin.ssl
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
} else if (config.origin.type === "postgres") {
|
|
573
|
+
db = knex({
|
|
574
|
+
client: "pg",
|
|
575
|
+
connection: {
|
|
576
|
+
host: config.origin.host,
|
|
577
|
+
port: config.origin.port,
|
|
578
|
+
user: config.origin.user,
|
|
579
|
+
password: config.origin.password,
|
|
580
|
+
database: config.origin.database,
|
|
581
|
+
ssl: config.origin.ssl
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
} else if (config.origin.type === "sqlite") {
|
|
585
|
+
db = knex({
|
|
586
|
+
client: "sqlite3",
|
|
587
|
+
connection: {
|
|
588
|
+
filename: config.origin.path
|
|
589
|
+
},
|
|
590
|
+
useNullAsDefault: true
|
|
591
|
+
});
|
|
592
|
+
}
|
|
165
593
|
const isCamelCase = config.camelCase && config.camelCase === true;
|
|
166
594
|
if (config.origin.type === "prisma") {
|
|
167
595
|
const schemaContents = readFileSync(config.origin.path).toString();
|
|
168
596
|
schema = createPrismaSchemaBuilder(schemaContents);
|
|
169
597
|
prismaTables = schema.findAllByType("model", {});
|
|
170
598
|
tables = prismaTables.filter((t) => t !== null).map((table) => table.name);
|
|
171
|
-
} else {
|
|
599
|
+
} else if (config.origin.type === "mysql" && db) {
|
|
172
600
|
const t = await db.raw(
|
|
173
601
|
"SELECT table_name as table_name FROM information_schema.tables WHERE table_schema = ?",
|
|
174
602
|
[config.origin.database]
|
|
175
603
|
);
|
|
176
604
|
tables = t[0].map((row) => row.table_name).sort();
|
|
605
|
+
} else if (config.origin.type === "postgres" && db) {
|
|
606
|
+
const schema2 = config.origin.schema || "public";
|
|
607
|
+
const t = await db.raw(
|
|
608
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_type = ?",
|
|
609
|
+
[schema2, "BASE TABLE"]
|
|
610
|
+
);
|
|
611
|
+
tables = t.rows.map((row) => row.table_name).sort();
|
|
612
|
+
} else if (config.origin.type === "sqlite" && db) {
|
|
613
|
+
const t = await db.raw(
|
|
614
|
+
"SELECT name as table_name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
|
|
615
|
+
);
|
|
616
|
+
tables = t.map((row) => row.table_name).sort();
|
|
177
617
|
}
|
|
178
618
|
const dests = [];
|
|
179
619
|
const includedTables = config.tables;
|
|
@@ -200,9 +640,94 @@ async function generate(config) {
|
|
|
200
640
|
}
|
|
201
641
|
let describes = [];
|
|
202
642
|
for (let table of tables) {
|
|
203
|
-
if (config.origin.type === "mysql") {
|
|
643
|
+
if (config.origin.type === "mysql" && db) {
|
|
204
644
|
const d = await db.raw(`SHOW FULL COLUMNS FROM ${table}`);
|
|
205
645
|
describes = d[0];
|
|
646
|
+
} else if (config.origin.type === "postgres" && db) {
|
|
647
|
+
const schema2 = config.origin.schema || "public";
|
|
648
|
+
const d = await db.raw(
|
|
649
|
+
`
|
|
650
|
+
SELECT
|
|
651
|
+
column_name as "Field",
|
|
652
|
+
column_default as "Default",
|
|
653
|
+
CASE WHEN is_nullable = 'YES' THEN 'YES' ELSE 'NO' END as "Null",
|
|
654
|
+
data_type as "Type",
|
|
655
|
+
CASE
|
|
656
|
+
WHEN column_default LIKE 'nextval(%' THEN 'auto_increment'
|
|
657
|
+
WHEN column_default IS NOT NULL AND (
|
|
658
|
+
column_default LIKE 'now()%' OR
|
|
659
|
+
column_default LIKE 'uuid_generate_v4()%' OR
|
|
660
|
+
column_default LIKE 'gen_random_uuid()%' OR
|
|
661
|
+
column_default LIKE 'current_timestamp%' OR
|
|
662
|
+
column_default LIKE 'current_date%' OR
|
|
663
|
+
column_default LIKE 'current_time%' OR
|
|
664
|
+
column_default LIKE '(%' OR
|
|
665
|
+
column_default LIKE 'array[%' OR
|
|
666
|
+
column_default LIKE 'json_build_%'
|
|
667
|
+
) THEN 'DEFAULT_GENERATED'
|
|
668
|
+
ELSE ''
|
|
669
|
+
END as "Extra",
|
|
670
|
+
col_description(('"'||$1||'"."'||$2||'"')::regclass::oid, ordinal_position) as "Comment"
|
|
671
|
+
FROM
|
|
672
|
+
information_schema.columns
|
|
673
|
+
WHERE
|
|
674
|
+
table_schema = $1 AND table_name = $2
|
|
675
|
+
ORDER BY
|
|
676
|
+
ordinal_position
|
|
677
|
+
`,
|
|
678
|
+
[schema2, table]
|
|
679
|
+
);
|
|
680
|
+
for (const column of d.rows) {
|
|
681
|
+
if (column.Type === "USER-DEFINED") {
|
|
682
|
+
const enumValues = await db.raw(
|
|
683
|
+
`
|
|
684
|
+
SELECT
|
|
685
|
+
e.enumlabel
|
|
686
|
+
FROM
|
|
687
|
+
pg_type t
|
|
688
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
689
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
690
|
+
WHERE
|
|
691
|
+
t.typname = (
|
|
692
|
+
SELECT udt_name
|
|
693
|
+
FROM information_schema.columns
|
|
694
|
+
WHERE table_schema = $1
|
|
695
|
+
AND table_name = $2
|
|
696
|
+
AND column_name = $3
|
|
697
|
+
)
|
|
698
|
+
ORDER BY
|
|
699
|
+
e.enumsortorder
|
|
700
|
+
`,
|
|
701
|
+
[schema2, table, column.Field]
|
|
702
|
+
);
|
|
703
|
+
column.EnumOptions = enumValues.rows.map(
|
|
704
|
+
(row) => row.enumlabel
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
describes = d.rows;
|
|
709
|
+
} else if (config.origin.type === "sqlite" && db) {
|
|
710
|
+
const d = await db.raw(`PRAGMA table_info(${table})`);
|
|
711
|
+
describes = d.map(
|
|
712
|
+
(row) => {
|
|
713
|
+
let extra = "";
|
|
714
|
+
if (row.dflt_value !== null) {
|
|
715
|
+
if (row.name === "rowid" || row.type.toLowerCase() === "integer primary key") {
|
|
716
|
+
extra = "auto_increment";
|
|
717
|
+
} else if (row.dflt_value.includes("CURRENT_TIMESTAMP") || row.dflt_value.includes("CURRENT_DATE") || row.dflt_value.includes("CURRENT_TIME") || row.dflt_value.includes("DATETIME") || row.dflt_value.includes("strftime") || row.dflt_value.includes("random()") || row.dflt_value.includes("(") || row.dflt_value.includes("uuid") || row.dflt_value.includes("json_")) {
|
|
718
|
+
extra = "DEFAULT_GENERATED";
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
Field: row.name,
|
|
723
|
+
Default: row.dflt_value,
|
|
724
|
+
Null: row.notnull === 0 ? "YES" : "NO",
|
|
725
|
+
Type: row.type.toLowerCase(),
|
|
726
|
+
Extra: extra,
|
|
727
|
+
Comment: ""
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
);
|
|
206
731
|
} else {
|
|
207
732
|
const prismaTable = prismaTables.find((t) => t?.name === table);
|
|
208
733
|
let enumOptions;
|
|
@@ -218,7 +743,7 @@ async function generate(config) {
|
|
|
218
743
|
}
|
|
219
744
|
const parsedDefaultValue = defaultValue !== void 0 && typeof defaultValue !== "object" ? defaultValue.toString().replace(/"/g, "") : null;
|
|
220
745
|
let fieldType = field.fieldType.toString();
|
|
221
|
-
if (!prismaValidTypes.includes(fieldType)) {
|
|
746
|
+
if (!prismaValidTypes.includes(fieldType) && schema) {
|
|
222
747
|
enumOptions = schema.findAllByType("enum", {
|
|
223
748
|
name: fieldType
|
|
224
749
|
})[0]?.enumerators.filter(
|
|
@@ -241,82 +766,40 @@ async function generate(config) {
|
|
|
241
766
|
});
|
|
242
767
|
}
|
|
243
768
|
if (isCamelCase) table = camelCase(table);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
export const ${table} = z.object({`;
|
|
247
|
-
for (const desc of describes) {
|
|
248
|
-
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
249
|
-
const type = getType("table", desc, config);
|
|
250
|
-
if (type) {
|
|
251
|
-
content = `${content}
|
|
252
|
-
${field}: ${type},`;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
content = `${content}
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
export const insertable_${table} = z.object({`;
|
|
259
|
-
for (const desc of describes) {
|
|
260
|
-
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
261
|
-
const type = getType("insertable", desc, config);
|
|
262
|
-
if (type) {
|
|
263
|
-
content = `${content}
|
|
264
|
-
${field}: ${type},`;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
content = `${content}
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
export const updateable_${table} = z.object({`;
|
|
271
|
-
for (const desc of describes) {
|
|
272
|
-
const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
273
|
-
const type = getType("updateable", desc, config);
|
|
274
|
-
if (type) {
|
|
275
|
-
content = `${content}
|
|
276
|
-
${field}: ${type},`;
|
|
277
|
-
}
|
|
769
|
+
if (!config.destinations || config.destinations.length === 0) {
|
|
770
|
+
throw new Error("No destinations specified");
|
|
278
771
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
772
|
+
for (const destination of config.destinations) {
|
|
773
|
+
const content = generateContent({
|
|
774
|
+
table,
|
|
775
|
+
describes,
|
|
776
|
+
config,
|
|
777
|
+
destination,
|
|
778
|
+
isCamelCase: isCamelCase === true,
|
|
779
|
+
enumDeclarations,
|
|
780
|
+
defaultZodHeader,
|
|
781
|
+
defaultKyselyHeader
|
|
782
|
+
});
|
|
783
|
+
const suffix = destination.suffix || "";
|
|
784
|
+
const folder = destination.folder || ".";
|
|
785
|
+
const file = suffix !== "" ? `${table}.${suffix}.ts` : `${table}.ts`;
|
|
786
|
+
if (config.dryRun) {
|
|
787
|
+
dryRunOutput[file] = content;
|
|
788
|
+
} else {
|
|
789
|
+
const dest = path.join(folder, file);
|
|
790
|
+
dests.push(dest);
|
|
791
|
+
if (!config.silent) console.log("Created:", dest);
|
|
792
|
+
fs.outputFileSync(dest, content);
|
|
289
793
|
}
|
|
290
794
|
}
|
|
291
|
-
content = `${content}
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
export type ${camelCase(`${table}Type`, {
|
|
295
|
-
pascalCase: true
|
|
296
|
-
})} = z.infer<typeof ${table}>
|
|
297
|
-
export type Insertable${camelCase(`${table}Type`, {
|
|
298
|
-
pascalCase: true
|
|
299
|
-
})} = z.infer<typeof insertable_${table}>
|
|
300
|
-
export type Updateable${camelCase(`${table}Type`, {
|
|
301
|
-
pascalCase: true
|
|
302
|
-
})} = z.infer<typeof updateable_${table}>
|
|
303
|
-
export type Selectable${camelCase(`${table}Type`, {
|
|
304
|
-
pascalCase: true
|
|
305
|
-
})} = z.infer<typeof selectable_${table}>
|
|
306
|
-
`;
|
|
307
|
-
const dir = config.folder && config.folder !== "" ? config.folder : ".";
|
|
308
|
-
const file = config.suffix && config.suffix !== "" ? `${table}.${config.suffix}.ts` : `${table}.ts`;
|
|
309
|
-
const dest = path.join(dir, file);
|
|
310
|
-
dests.push(dest);
|
|
311
|
-
if (!config.silent) console.log("Created:", dest);
|
|
312
|
-
fs.outputFileSync(dest, content);
|
|
313
|
-
}
|
|
314
|
-
if (config.origin.type === "mysql") {
|
|
315
|
-
await db.destroy();
|
|
316
795
|
}
|
|
317
|
-
|
|
796
|
+
if (db) await db.destroy();
|
|
797
|
+
return config.dryRun ? dryRunOutput : dests;
|
|
318
798
|
}
|
|
319
799
|
export {
|
|
800
|
+
defaultKyselyHeader,
|
|
801
|
+
defaultZodHeader,
|
|
320
802
|
generate,
|
|
803
|
+
generateContent,
|
|
321
804
|
getType
|
|
322
805
|
};
|