mutano 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,933 +1,158 @@
1
- import { readFileSync } from "node:fs";
2
- import path from "node:path";
3
- const enumDeclarations = {};
4
- import {
5
- createPrismaSchemaBuilder
6
- } from "@mrleebo/prisma-ast";
1
+ import * as path from "node:path";
7
2
  import camelCase from "camelcase";
8
- import fs from "fs-extra";
9
- import knex from "knex";
10
- const extractTypeExpression = (comment, prefix) => {
11
- const start = comment.indexOf(prefix);
12
- if (start === -1) return null;
13
- const typeLen = prefix.length;
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
- };
31
- const extractTSExpression = (comment) => extractTypeExpression(comment, "@ts(");
32
- const extractKyselyExpression = (comment) => extractTypeExpression(comment, "@kysely(");
33
- const extractZodExpression = (comment) => extractTypeExpression(comment, "@zod(");
34
- const prismaValidTypes = [
35
- "BigInt",
36
- "Boolean",
37
- "Bytes",
38
- "DateTime",
39
- "Decimal",
40
- "Float",
41
- "Int",
42
- "Json",
43
- "String",
44
- "Enum"
45
- ];
46
- const dateTypes = {
47
- mysql: ["date", "datetime", "timestamp"],
48
- postgres: [
49
- "date",
50
- "timestamp",
51
- "timestamptz",
52
- "timestamp without time zone",
53
- "timestamp with time zone"
54
- ],
55
- sqlite: ["datetime"],
56
- prisma: ["DateTime"]
57
- };
58
- const stringTypes = {
59
- mysql: [
60
- "tinytext",
61
- "text",
62
- "mediumtext",
63
- "longtext",
64
- "json",
65
- "time",
66
- "year",
67
- "char",
68
- "varchar"
69
- ],
70
- postgres: [
71
- "text",
72
- "character varying",
73
- "varchar",
74
- "char",
75
- "character",
76
- "json",
77
- "jsonb",
78
- "uuid",
79
- "time",
80
- "timetz",
81
- "interval",
82
- "name",
83
- "citext"
84
- ],
85
- sqlite: [
86
- "text",
87
- "character",
88
- "varchar",
89
- "varying character",
90
- "nchar",
91
- "native character",
92
- "nvarchar",
93
- "clob",
94
- "json"
95
- ],
96
- prisma: ["String", "Bytes", "Json"]
97
- };
98
- const bigIntTypes = {
99
- mysql: ["bigint"],
100
- postgres: ["bigint"],
101
- sqlite: ["bigint"],
102
- prisma: ["BigInt"]
103
- };
104
- const numberTypes = {
105
- mysql: ["smallint", "mediumint", "int", "float", "double"],
106
- postgres: [
107
- "smallint",
108
- "integer",
109
- "real",
110
- "double precision",
111
- "serial",
112
- "bigserial"
113
- ],
114
- sqlite: [
115
- "int",
116
- "integer",
117
- "tinyint",
118
- "smallint",
119
- "mediumint",
120
- "unsigned big int",
121
- "int2",
122
- "int8",
123
- "real",
124
- "double",
125
- "double precision",
126
- "float"
127
- ],
128
- prisma: ["Int", "Float"]
129
- };
130
- const decimalTypes = {
131
- mysql: ["decimal"],
132
- postgres: ["decimal", "numeric"],
133
- sqlite: ["numeric", "decimal"],
134
- prisma: ["Decimal"]
135
- };
136
- const booleanTypes = {
137
- mysql: ["tinyint"],
138
- postgres: ["boolean", "bool"],
139
- sqlite: ["boolean"],
140
- prisma: ["Boolean"]
141
- };
142
- const enumTypes = {
143
- mysql: ["enum"],
144
- postgres: ["USER-DEFINED"],
145
- sqlite: [],
146
- // SQLite doesn't have native enum types
147
- prisma: ["Enum"]
148
- };
149
- const enumRegex = /enum\(([^)]+)\)/;
150
- function getType(op, desc, config, destination, tableName) {
151
- const schemaType = config.origin.type;
152
- const { Default, Extra, Null, Type, Comment, EnumOptions } = desc;
153
- const isZodDestination = destination.type === "zod";
154
- const isTsDestination = destination.type === "ts";
155
- const isKyselyDestination = destination.type === "kysely";
156
- const isNullish = isZodDestination && destination.type === "zod" && destination.nullish === true;
157
- const isTrim = isZodDestination && destination.type === "zod" && destination.useTrim === true && op !== "selectable";
158
- const isUseDateType = isZodDestination && destination.type === "zod" && destination.useDateType === true;
159
- const hasDefaultValue = Default !== null && op !== "selectable";
160
- const isGenerated = ["DEFAULT_GENERATED", "auto_increment"].includes(Extra);
161
- const isNull = Null === "YES";
162
- if (isGenerated && !isNull && ["insertable", "updateable"].includes(op))
163
- return;
164
- const isRequiredString = destination.type === "zod" && destination.requiredString === true && op !== "selectable";
165
- const type = schemaType === "mysql" ? Type.split("(")[0].split(" ")[0] : Type;
166
- if (isTsDestination || isKyselyDestination) {
167
- if (isKyselyDestination && config.magicComments) {
168
- const kyselyOverrideType = extractKyselyExpression(Comment);
169
- if (kyselyOverrideType) {
170
- const shouldBeNullable2 = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
171
- return shouldBeNullable2 ? kyselyOverrideType.includes("| null") ? kyselyOverrideType : `${kyselyOverrideType} | null` : kyselyOverrideType;
172
- }
173
- }
174
- const tsOverrideType = config.magicComments ? extractTSExpression(Comment) : null;
175
- const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
176
- if (tsOverrideType) {
177
- return shouldBeNullable ? tsOverrideType.includes("| null") ? tsOverrideType : `${tsOverrideType} | null` : tsOverrideType;
178
- }
179
- if (dateTypes[schemaType].includes(type)) {
180
- return shouldBeNullable ? "Date | null" : "Date";
181
- }
182
- if (stringTypes[schemaType].includes(type)) {
183
- return shouldBeNullable ? "string | null" : "string";
184
- }
185
- if (numberTypes[schemaType].includes(type)) {
186
- return shouldBeNullable ? "number | null" : "number";
187
- }
188
- if (booleanTypes[schemaType].includes(type)) {
189
- return shouldBeNullable ? "boolean | null" : "boolean";
190
- }
191
- if (bigIntTypes[schemaType].includes(type) || type === "BigInt") {
192
- if (isKyselyDestination) {
193
- return shouldBeNullable ? "BigInt | null" : "BigInt";
194
- }
195
- return shouldBeNullable ? "string | null" : "string";
196
- }
197
- if (decimalTypes[schemaType].includes(type) || type === "Decimal") {
198
- if (isKyselyDestination) {
199
- return shouldBeNullable ? "Decimal | null" : "Decimal";
200
- }
201
- return shouldBeNullable ? "string | null" : "string";
202
- }
203
- if (schemaType !== "sqlite" && enumTypes[schemaType].includes(type)) {
204
- const enumType = destination.type === "ts" ? destination.enumType || "union" : "union";
205
- let enumValues = [];
206
- if (schemaType === "mysql") {
207
- const matches = Type.match(enumRegex);
208
- if (matches?.[1]) {
209
- enumValues = matches[1].split(",").map((v) => v.trim()).sort();
210
- }
211
- } else if (EnumOptions && EnumOptions.length > 0) {
212
- enumValues = EnumOptions.map((e) => `'${e}'`).sort();
213
- }
214
- if (enumValues.length === 0) {
215
- return isNull ? "string | null" : "string";
216
- }
217
- if (enumType === "enum") {
218
- const enumName = camelCase(`${desc.Field}_enum`, { pascalCase: true });
219
- const enumDeclaration = `enum ${enumName} {
220
- ${enumValues.map((v) => {
221
- const cleanName = v.replace(/['"]/g, "");
222
- return `${cleanName} = ${v}`;
223
- }).join(",\n ")}
224
- }`;
225
- if (tableName) {
226
- if (!enumDeclarations[tableName]) {
227
- enumDeclarations[tableName] = [];
228
- }
229
- if (!enumDeclarations[tableName].includes(enumDeclaration)) {
230
- enumDeclarations[tableName].push(enumDeclaration);
231
- }
232
- }
233
- return shouldBeNullable ? `${enumName} | null` : enumName;
234
- }
235
- const unionType = enumValues.join(" | ");
236
- return shouldBeNullable ? `(${unionType}) | null` : unionType;
237
- }
238
- return "any";
239
- }
240
- const zDate = [
241
- "z.union([z.number(), z.string(), z.date()]).pipe(z.coerce.date())"
242
- ];
243
- const string = [isTrim ? "z.string().trim()" : "z.string()"];
244
- const number = ["z.number()"];
245
- const boolean = [
246
- "z.union([z.number(),z.string(),z.boolean()]).pipe(z.coerce.boolean())"
247
- ];
248
- const dateField = isUseDateType ? zDate : string;
249
- const nullable = isNullish && op !== "selectable" ? "nullish()" : "nullable()";
250
- const optional = "optional()";
251
- const nonnegative = "nonnegative()";
252
- const isUpdateableFormat = op === "updateable" && !isNull && !hasDefaultValue;
253
- const min1 = "min(1)";
254
- const zodOverrideType = config.magicComments ? extractZodExpression(Comment) : null;
255
- let typeOverride = zodOverrideType;
256
- if (!typeOverride && config.origin.overrideTypes) {
257
- if (config.origin.type === "mysql") {
258
- typeOverride = config.origin.overrideTypes[type] || null;
259
- } else if (config.origin.type === "postgres") {
260
- typeOverride = config.origin.overrideTypes[type] || null;
261
- } else if (config.origin.type === "sqlite") {
262
- typeOverride = config.origin.overrideTypes[type] || null;
263
- } else if (config.origin.type === "prisma") {
264
- typeOverride = config.origin.overrideTypes[type] || null;
265
- }
266
- }
267
- const generateDateLikeField = () => {
268
- const field = typeOverride ? [typeOverride] : dateField;
269
- if (isNull && !typeOverride) field.push(nullable);
270
- else if (hasDefaultValue || !hasDefaultValue && isGenerated)
271
- field.push(optional);
272
- if (hasDefaultValue && !isGenerated) field.push(`default('${Default}')`);
273
- if (isUpdateableFormat) field.push(optional);
274
- return field.join(".");
275
- };
276
- const generateStringLikeField = () => {
277
- const field = typeOverride ? [typeOverride] : string;
278
- if (isNull && !typeOverride) field.push(nullable);
279
- else if (hasDefaultValue || !hasDefaultValue && isGenerated)
280
- field.push(optional);
281
- else if (isRequiredString && !typeOverride) field.push(min1);
282
- if (hasDefaultValue && !isGenerated) field.push(`default('${Default}')`);
283
- if (isUpdateableFormat) field.push(optional);
284
- return field.join(".");
285
- };
286
- const generateBooleanLikeField = () => {
287
- const field = typeOverride ? [typeOverride] : boolean;
288
- if (isNull && !typeOverride) field.push(nullable);
289
- else if (hasDefaultValue || !hasDefaultValue && isGenerated)
290
- field.push(optional);
291
- if (hasDefaultValue && !isGenerated) {
292
- if (Default === "true" || Default === "false") {
293
- field.push(`default(${Default})`);
294
- } else {
295
- field.push(`default(${Boolean(+Default)})`);
296
- }
297
- }
298
- if (isUpdateableFormat) field.push(optional);
299
- return field.join(".");
300
- };
301
- const generateNumberLikeField = () => {
302
- const unsigned = Type.endsWith(" unsigned");
303
- const field = typeOverride ? [typeOverride] : number;
304
- if (unsigned && !typeOverride) field.push(nonnegative);
305
- if (isNull && !typeOverride) field.push(nullable);
306
- else if (hasDefaultValue || !hasDefaultValue && isGenerated)
307
- field.push(optional);
308
- if (hasDefaultValue && !isGenerated) field.push(`default(${Default})`);
309
- if (isUpdateableFormat) field.push(optional);
310
- return field.join(".");
311
- };
312
- const generateEnumLikeField = () => {
313
- let enumValues = [];
314
- if (schemaType === "mysql") {
315
- const matches = Type.match(enumRegex);
316
- if (matches?.[1]) {
317
- enumValues = matches[1].split(",").map((v) => v.trim()).sort();
318
- }
319
- } else if (EnumOptions && EnumOptions.length > 0) {
320
- enumValues = [...EnumOptions].sort().map((e) => `'${e}'`);
321
- }
322
- const value = enumValues.join(",");
323
- const field = [`z.enum([${value}])`];
324
- if (isNull) field.push(nullable);
325
- else if (hasDefaultValue || !hasDefaultValue && isGenerated)
326
- field.push(optional);
327
- if (hasDefaultValue && !isGenerated) field.push(`default('${Default}')`);
328
- if (isUpdateableFormat) field.push(optional);
329
- return field.join(".");
330
- };
331
- if (dateTypes[schemaType].includes(type)) return generateDateLikeField();
332
- if (stringTypes[schemaType].includes(type)) return generateStringLikeField();
333
- if (numberTypes[schemaType].includes(type)) return generateNumberLikeField();
334
- if (bigIntTypes[schemaType].includes(type) || type === "BigInt") {
335
- if (isKyselyDestination) {
336
- const isNull2 = Null === "YES";
337
- const hasDefaultValue2 = Default !== null;
338
- const isGenerated2 = Extra.toLowerCase().includes("auto_increment") || Extra.toLowerCase().includes("default_generated");
339
- const shouldBeNullable = isNull2 || ["insertable", "updateable"].includes(op) && (hasDefaultValue2 || isGenerated2) || op === "updateable" && !isNull2 && !hasDefaultValue2;
340
- return shouldBeNullable ? "BigInt | null" : "BigInt";
341
- }
342
- return generateStringLikeField();
343
- }
344
- if (decimalTypes[schemaType].includes(type) || type === "Decimal") {
345
- if (isKyselyDestination) {
346
- const isNull2 = Null === "YES";
347
- const hasDefaultValue2 = Default !== null;
348
- const isGenerated2 = Extra.toLowerCase().includes("auto_increment") || Extra.toLowerCase().includes("default_generated");
349
- const shouldBeNullable = isNull2 || ["insertable", "updateable"].includes(op) && (hasDefaultValue2 || isGenerated2) || op === "updateable" && !isNull2 && !hasDefaultValue2;
350
- return shouldBeNullable ? "Decimal | null" : "Decimal";
351
- }
352
- return generateStringLikeField();
353
- }
354
- if (booleanTypes[schemaType].includes(type)) return generateBooleanLikeField();
355
- if (schemaType !== "sqlite" && enumTypes[schemaType].includes(type))
356
- return generateEnumLikeField();
357
- throw new Error(`Unsupported column type: ${type}`);
358
- }
359
- function generateContent({
360
- table,
361
- describes,
362
- config,
363
- destination,
364
- isCamelCase,
365
- enumDeclarations: enumDeclarations2,
366
- defaultZodHeader: defaultZodHeader2
367
- }) {
368
- let content = "";
369
- const schemaType = config.origin.type;
370
- if (destination.type === "kysely") {
371
- content += `// Kysely type definitions for ${table}
372
- `;
373
- content += `
374
- // This interface defines the structure of the '${table}' table
375
- export interface ${camelCase(table, { pascalCase: true })} {`;
376
- for (const desc of describes) {
377
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
378
- const type = getType("table", desc, config, destination, table);
379
- if (type) {
380
- let kyselyType = type;
381
- const isAutoIncrement = desc.Extra.toLowerCase().includes("auto_increment");
382
- const isDefaultGenerated = desc.Extra.toLowerCase().includes("default_generated");
383
- const isNullable = desc.Null === "YES";
384
- const isJsonField = desc.Type.toLowerCase().includes("json");
385
- const hasDefaultValue = desc.Default !== null;
386
- const isEnum = schemaType !== "sqlite" && enumTypes[schemaType].includes(
387
- schemaType === "mysql" ? desc.Type.split("(")[0].split(" ")[0] : desc.Type
388
- );
389
- const kyselyOverrideType = config.magicComments ? extractKyselyExpression(desc.Comment) : null;
390
- if (kyselyOverrideType) {
391
- kyselyType = kyselyOverrideType;
392
- if (isNullable && !kyselyType.includes("| null")) {
393
- kyselyType = `${kyselyType} | null`;
394
- }
395
- if (isAutoIncrement || isDefaultGenerated || hasDefaultValue && (isEnum || kyselyType === "string" || kyselyType === "boolean" || kyselyType === "number" || kyselyType === "Decimal" || kyselyType === "BigInt" || kyselyType.includes("boolean | null") || kyselyType.includes("string | null") || kyselyType.includes("number | null") || kyselyType.includes("Decimal | null") || kyselyType.includes("BigInt | null"))) {
396
- kyselyType = `Generated<${kyselyType}>`;
397
- }
398
- } else if (isJsonField) {
399
- kyselyType = isNullable ? "Json | null" : "Json";
400
- } else {
401
- if (isNullable && !isJsonField) {
402
- if (!kyselyType.includes("| null")) {
403
- kyselyType = `${kyselyType} | null`;
404
- }
405
- }
406
- if (isAutoIncrement || isDefaultGenerated || hasDefaultValue && (isEnum || kyselyType === "string" || kyselyType === "boolean" || kyselyType === "number" || kyselyType === "Decimal" || kyselyType === "BigInt" || kyselyType.includes("boolean | null") || kyselyType.includes("string | null") || kyselyType.includes("number | null") || kyselyType.includes("Decimal | null") || kyselyType.includes("BigInt | null"))) {
407
- kyselyType = `Generated<${kyselyType}>`;
408
- }
409
- }
410
- content = `${content}
411
- ${field}: ${kyselyType};`;
412
- }
413
- }
414
- content = `${content}
415
- }
416
-
417
- // Helper types for ${table}
418
- export type Selectable${camelCase(table, { pascalCase: true })} = Selectable<${camelCase(table, { pascalCase: true })}>;
419
- export type Insertable${camelCase(table, { pascalCase: true })} = Insertable<${camelCase(table, { pascalCase: true })}>;
420
- export type Updateable${camelCase(table, { pascalCase: true })} = Updateable<${camelCase(table, { pascalCase: true })}>;
421
- `;
422
- } else if (destination.type === "ts") {
423
- const modelType = destination.modelType || "interface";
424
- const isInterface = modelType === "interface";
425
- const header = destination.header;
426
- content = header ? `${header}
427
-
428
- ` : "";
429
- content += `// TypeScript ${isInterface ? "interfaces" : "types"} for ${table}`;
430
- if (enumDeclarations2[table] && enumDeclarations2[table].length > 0) {
431
- content += "\n\n// Enum declarations";
432
- for (const enumDecl of enumDeclarations2[table]) {
433
- content += `
434
- ${enumDecl}`;
435
- }
436
- content += "\n";
437
- }
438
- if (isInterface) {
439
- content += `
440
- export interface ${camelCase(table, { pascalCase: true })} {`;
441
- } else {
442
- content += `
443
- export type ${camelCase(table, { pascalCase: true })} = {`;
444
- }
445
- for (const desc of describes) {
446
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
447
- const type = getType("table", desc, config, destination, table);
448
- if (type) {
449
- content = `${content}
450
- ${field}: ${type};`;
451
- }
452
- }
453
- content = `${content}
454
- }
455
-
456
- `;
457
- if (isInterface) {
458
- content += `export interface Insertable${camelCase(table, { pascalCase: true })} {`;
459
- } else {
460
- content += `export type Insertable${camelCase(table, { pascalCase: true })} = {`;
461
- }
462
- for (const desc of describes) {
463
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
464
- const type = getType("insertable", desc, config, destination, table);
465
- if (type) {
466
- content = `${content}
467
- ${field}: ${type};`;
468
- }
469
- }
470
- content = `${content}
471
- }
472
-
473
- `;
474
- if (isInterface) {
475
- content += `export interface Updateable${camelCase(table, { pascalCase: true })} {`;
476
- } else {
477
- content += `export type Updateable${camelCase(table, { pascalCase: true })} = {`;
478
- }
479
- for (const desc of describes) {
480
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
481
- const type = getType("updateable", desc, config, destination, table);
482
- if (type) {
483
- content = `${content}
484
- ${field}: ${type};`;
485
- }
486
- }
487
- content = `${content}
488
- }
489
-
490
- `;
491
- if (isInterface) {
492
- content += `export interface Selectable${camelCase(table, { pascalCase: true })} {`;
493
- } else {
494
- content += `export type Selectable${camelCase(table, { pascalCase: true })} = {`;
495
- }
496
- for (const desc of describes) {
497
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
498
- const type = getType("selectable", desc, config, destination, table);
499
- if (type) {
500
- content = `${content}
501
- ${field}: ${type};`;
502
- }
503
- }
504
- content = `${content}
505
- }
506
- `;
507
- } else if (destination.type === "zod") {
508
- const header = destination.header;
509
- content = header ? header + "\n\n" : defaultZodHeader2(destination.version || 3);
510
- content += `export const ${table} = z.object({`;
511
- for (const desc of describes) {
512
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
513
- const type = getType("table", desc, config, destination, table);
514
- if (type) {
515
- content = `${content}
516
- ${field}: ${type},`;
517
- }
518
- }
519
- content = `${content}
520
- })
521
-
522
- export const insertable_${table} = z.object({`;
523
- for (const desc of describes) {
524
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
525
- const type = getType("insertable", desc, config, destination, table);
526
- if (type) {
527
- content = `${content}
528
- ${field}: ${type},`;
529
- }
530
- }
531
- content = `${content}
532
- })
533
-
534
- export const updateable_${table} = z.object({`;
535
- for (const desc of describes) {
536
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
537
- const type = getType("updateable", desc, config, destination, table);
538
- if (type) {
539
- content = `${content}
540
- ${field}: ${type},`;
541
- }
542
- }
543
- content = `${content}
544
- })
545
-
546
- export const selectable_${table} = z.object({`;
547
- for (const desc of describes) {
548
- const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
549
- const type = getType("selectable", desc, config, destination, table);
550
- if (type) {
551
- content = `${content}
552
- ${field}: ${type},`;
553
- }
554
- }
555
- content = `${content}
556
- })
557
-
558
- export type ${camelCase(`${table}Type`, {
559
- pascalCase: true
560
- })} = z.infer<typeof ${table}>
561
- export type Insertable${camelCase(`${table}Type`, {
562
- pascalCase: true
563
- })} = z.infer<typeof insertable_${table}>
564
- export type Updateable${camelCase(`${table}Type`, {
565
- pascalCase: true
566
- })} = z.infer<typeof updateable_${table}>
567
- export type Selectable${camelCase(`${table}Type`, {
568
- pascalCase: true
569
- })} = z.infer<typeof selectable_${table}>
570
- `;
571
- }
572
- return content;
573
- }
574
- const defaultKyselyHeader = "import { ColumnType, Selectable, Insertable, Updateable } from 'kysely';\n\n";
575
- const defaultZodHeader = (version) => "import { z } from 'zod" + (version === 3 ? "" : "/v4") + "';\n\n";
3
+ import * as fs from "fs-extra";
4
+ import { filterTables, filterViews, createEntityList } from "./utils/filters.js";
5
+ import { generateContent, generateViewContent } from "./generators/content-generator.js";
6
+ import {
7
+ createDatabaseConnection,
8
+ extractTables,
9
+ extractViews,
10
+ extractColumnDescriptions
11
+ } from "./database/connection.js";
12
+ import {
13
+ extractPrismaEntities,
14
+ extractPrismaColumnDescriptions
15
+ } from "./database/prisma.js";
16
+ import { defaultKyselyHeader, defaultZodHeader, kyselyJsonTypes } from "./constants.js";
17
+ import {
18
+ extractTypeExpression,
19
+ extractTSExpression,
20
+ extractKyselyExpression,
21
+ extractZodExpression
22
+ } from "./utils/magic-comments.js";
23
+ import { generateContent as generateContent2, generateViewContent as generateViewContent2 } from "./generators/content-generator.js";
24
+ import { getType } from "./generators/type-generator.js";
576
25
  async function generate(config) {
577
26
  let tables = [];
578
- let prismaTables = [];
579
- let schema = null;
27
+ let views = [];
28
+ let enumDeclarations = {};
580
29
  let db = null;
581
- const kyselyTableContents = {};
582
- if (config.destinations.length === 0) {
583
- throw new Error("Empty destinations object.");
584
- }
585
- const dryRunOutput = {};
586
- if (config.origin.type === "mysql") {
587
- db = knex({
588
- client: "mysql2",
589
- connection: {
590
- host: config.origin.host,
591
- port: config.origin.port,
592
- user: config.origin.user,
593
- password: config.origin.password,
594
- database: config.origin.database,
595
- ssl: config.origin.ssl
596
- }
597
- });
598
- } else if (config.origin.type === "postgres") {
599
- db = knex({
600
- client: "pg",
601
- connection: {
602
- host: config.origin.host,
603
- port: config.origin.port,
604
- user: config.origin.user,
605
- password: config.origin.password,
606
- database: config.origin.database,
607
- ssl: config.origin.ssl
608
- }
609
- });
610
- } else if (config.origin.type === "sqlite") {
611
- db = knex({
612
- client: "sqlite3",
613
- connection: {
614
- filename: config.origin.path
615
- },
616
- useNullAsDefault: true
617
- });
618
- }
619
- const isCamelCase = config.camelCase && config.camelCase === true;
620
- if (config.origin.type === "prisma") {
621
- const schemaContents = readFileSync(config.origin.path).toString();
622
- schema = createPrismaSchemaBuilder(schemaContents);
623
- prismaTables = schema.findAllByType("model", {});
624
- tables = prismaTables.filter((t) => t !== null).map((table) => table.name);
625
- } else if (config.origin.type === "mysql" && db) {
626
- const t = await db.raw(
627
- "SELECT table_name as table_name FROM information_schema.tables WHERE table_schema = ?",
628
- [config.origin.database]
629
- );
630
- tables = t[0].map((row) => row.table_name).sort();
631
- } else if (config.origin.type === "postgres" && db) {
632
- const schema2 = config.origin.schema || "public";
633
- const t = await db.raw(
634
- "SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_type = ?",
635
- [schema2, "BASE TABLE"]
636
- );
637
- tables = t.rows.map((row) => row.table_name).sort();
638
- } else if (config.origin.type === "sqlite" && db) {
639
- const t = await db.raw(
640
- "SELECT name as table_name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
641
- );
642
- tables = t.map((row) => row.table_name).sort();
643
- }
644
- const dests = [];
645
- const includedTables = config.tables;
646
- if (includedTables?.length)
647
- tables = tables.filter((table) => includedTables.includes(table));
648
- const allIgnoredTables = config.ignore;
649
- const ignoredTablesRegex = allIgnoredTables?.filter((ignoreString) => {
650
- return ignoreString.startsWith("/") && ignoreString.endsWith("/");
651
- });
652
- const ignoredTableNames = allIgnoredTables?.filter(
653
- (table) => !ignoredTablesRegex?.includes(table)
654
- );
655
- if (ignoredTableNames?.length)
656
- tables = tables.filter((table) => !ignoredTableNames.includes(table));
657
- if (ignoredTablesRegex?.length) {
658
- tables = tables.filter((table) => {
659
- let useTable = true;
660
- for (const text of ignoredTablesRegex) {
661
- const pattern = text.substring(1, text.length - 1);
662
- if (table.match(pattern) !== null) useTable = false;
663
- }
664
- return useTable;
665
- });
666
- }
667
- let describes = [];
668
- for (let table of tables.sort((a, b) => a.localeCompare(b))) {
669
- if (config.origin.type === "mysql" && db) {
670
- const d = await db.raw(`SHOW FULL COLUMNS FROM ${table}`);
671
- describes = d[0];
672
- } else if (config.origin.type === "postgres" && db) {
673
- const schema2 = config.origin.schema || "public";
674
- const d = await db.raw(
675
- `
676
- SELECT
677
- column_name as "Field",
678
- column_default as "Default",
679
- CASE WHEN is_nullable = 'YES' THEN 'YES' ELSE 'NO' END as "Null",
680
- data_type as "Type",
681
- CASE
682
- WHEN column_default LIKE 'nextval(%' THEN 'auto_increment'
683
- WHEN column_default IS NOT NULL AND (
684
- column_default LIKE 'now()%' OR
685
- column_default LIKE 'uuid_generate_v4()%' OR
686
- column_default LIKE 'gen_random_uuid()%' OR
687
- column_default LIKE 'current_timestamp%' OR
688
- column_default LIKE 'current_date%' OR
689
- column_default LIKE 'current_time%' OR
690
- column_default LIKE '(%' OR
691
- column_default LIKE 'array[%' OR
692
- column_default LIKE 'json_build_%'
693
- ) THEN 'DEFAULT_GENERATED'
694
- ELSE ''
695
- END as "Extra",
696
- col_description(('"'||$1||'"."'||$2||'"')::regclass::oid, ordinal_position) as "Comment"
697
- FROM
698
- information_schema.columns
699
- WHERE
700
- table_schema = $1 AND table_name = $2
701
- ORDER BY
702
- ordinal_position
703
- `,
704
- [schema2, table]
705
- );
706
- for (const column of d.rows) {
707
- if (column.Type === "USER-DEFINED") {
708
- const enumValues = await db.raw(
709
- `
710
- SELECT
711
- e.enumlabel
712
- FROM
713
- pg_type t
714
- JOIN pg_enum e ON t.oid = e.enumtypid
715
- JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
716
- WHERE
717
- t.typname = (
718
- SELECT udt_name
719
- FROM information_schema.columns
720
- WHERE table_schema = $1
721
- AND table_name = $2
722
- AND column_name = $3
723
- )
724
- ORDER BY
725
- e.enumlabel
726
- `,
727
- [schema2, table, column.Field]
728
- );
729
- column.EnumOptions = enumValues.rows.map(
730
- (row) => row.enumlabel
731
- );
732
- }
733
- }
734
- describes = d.rows;
735
- } else if (config.origin.type === "sqlite" && db) {
736
- const d = await db.raw(`PRAGMA table_info(${table})`);
737
- describes = d.map(
738
- (row) => {
739
- let extra = "";
740
- if (row.dflt_value !== null) {
741
- if (row.name === "rowid" || row.type.toLowerCase() === "integer primary key") {
742
- extra = "auto_increment";
743
- } 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_")) {
744
- extra = "DEFAULT_GENERATED";
745
- }
746
- }
747
- return {
748
- Field: row.name,
749
- Default: row.dflt_value,
750
- Null: row.notnull === 0 ? "YES" : "NO",
751
- Type: row.type.toLowerCase(),
752
- Extra: extra,
753
- Comment: ""
754
- };
755
- }
756
- );
30
+ try {
31
+ if (config.origin.type === "prisma") {
32
+ const prismaEntities = extractPrismaEntities(config);
33
+ tables = prismaEntities.tables;
34
+ views = prismaEntities.views;
35
+ enumDeclarations = prismaEntities.enumDeclarations;
757
36
  } else {
758
- const prismaTable = prismaTables.find((t) => t?.name === table);
759
- let enumOptions;
760
- describes = prismaTable.properties.filter(
761
- (p) => p.type === "field" && p.array !== true && !p.attributes?.find((a) => a.name === "relation")
762
- ).map((field) => {
763
- let defaultGenerated = false;
764
- const defaultValueField = field.attributes ? field.attributes.find((a) => a.name === "default") : null;
765
- const defaultValue = defaultValueField?.args?.[0].value;
766
- if (typeof defaultValue === "object" && // @ts-ignore
767
- defaultValue?.type === "function") {
768
- defaultGenerated = true;
769
- }
770
- const parsedDefaultValue = defaultValue !== void 0 && typeof defaultValue !== "object" ? defaultValue.toString().replace(/"/g, "") : null;
771
- let fieldType = field.fieldType.toString();
772
- if (!prismaValidTypes.includes(fieldType) && schema) {
773
- enumOptions = schema.findAllByType("enum", {
774
- name: fieldType
775
- })[0]?.enumerators.filter(
776
- (e) => e.type === "enumerator"
777
- ).map((e) => {
778
- const attrs = e.attributes?.find((a) => a.name === "map");
779
- return attrs?.args ? attrs.args[0].value.toString().replace(/"/g, "") : e.name;
780
- });
781
- fieldType = "Enum";
782
- }
783
- return {
784
- Field: field.name,
785
- Default: parsedDefaultValue,
786
- EnumOptions: enumOptions,
787
- Extra: defaultGenerated ? "DEFAULT_GENERATED" : "",
788
- Type: fieldType,
789
- Null: field.optional ? "YES" : "NO",
790
- Comment: field.comment ?? ""
791
- };
792
- });
793
- }
794
- if (isCamelCase) table = camelCase(table);
795
- if (!config.destinations || config.destinations.length === 0) {
796
- throw new Error("No destinations specified");
37
+ db = createDatabaseConnection(config);
38
+ tables = await extractTables(db, config);
39
+ views = await extractViews(db, config);
797
40
  }
798
- const kyselyDestinations = config.destinations.filter(
799
- (d) => d.type === "kysely"
800
- );
801
- const nonKyselyDestinations = config.destinations.filter(
802
- (d) => d.type !== "kysely"
803
- );
804
- for (const destination of nonKyselyDestinations) {
805
- const content = generateContent({
806
- table,
807
- describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
808
- config,
809
- destination,
810
- isCamelCase: isCamelCase === true,
811
- enumDeclarations,
812
- defaultZodHeader
813
- });
814
- const suffix = destination.suffix || "";
815
- const folder = destination.folder || ".";
816
- const file = suffix !== "" ? `${table}.${suffix}.ts` : `${table}.ts`;
817
- if (config.dryRun) {
818
- const absolutePath = path.resolve(path.join(folder, file));
819
- dryRunOutput[absolutePath] = content;
41
+ tables = filterTables(tables, config.tables, config.ignore);
42
+ if (!config.includeViews) {
43
+ views = [];
44
+ } else {
45
+ views = filterViews(views, config.views, config.ignoreViews);
46
+ }
47
+ const allEntities = createEntityList(tables, views);
48
+ const results = {};
49
+ const isCamelCase = config.camelCase === true;
50
+ const nonKyselyDestinations = config.destinations.filter((d) => d.type !== "kysely");
51
+ for (const entity of allEntities) {
52
+ const { name: entityName, type: entityType } = entity;
53
+ let describes;
54
+ if (config.origin.type === "prisma") {
55
+ describes = extractPrismaColumnDescriptions(config, entityName, enumDeclarations);
820
56
  } else {
821
- const dest = path.join(folder, file);
822
- dests.push(dest);
823
- if (!config.silent) console.log("Created:", dest);
824
- fs.outputFileSync(dest, content);
825
- }
826
- }
827
- for (const destination of kyselyDestinations) {
828
- const content = generateContent({
829
- table,
830
- describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
831
- config,
832
- destination,
833
- isCamelCase: isCamelCase === true,
834
- enumDeclarations,
835
- defaultZodHeader
836
- });
837
- const outFile = destination.outFile || "db.ts";
838
- if (!kyselyTableContents[outFile]) {
839
- kyselyTableContents[outFile] = [];
840
- }
841
- kyselyTableContents[outFile].push({
842
- table,
843
- content
844
- });
845
- if (config.dryRun) {
846
- const tempKey = path.resolve(`${table}.kysely.temp`);
847
- dryRunOutput[tempKey] = content;
848
- }
849
- }
850
- }
851
- if (db) await db.destroy();
852
- for (const [outFile, tableContents] of Object.entries(kyselyTableContents)) {
853
- if (tableContents.length === 0) continue;
854
- const kyselyDestination = config.destinations.find(
855
- (d) => d.type === "kysely"
856
- );
857
- const header = kyselyDestination?.header || defaultKyselyHeader;
858
- const schemaName = kyselyDestination?.schemaName || "DB";
859
- let consolidatedContent = `${header}
860
-
861
- // JSON type definitions
862
- export type Json = ColumnType<JsonValue, string, string>;
863
-
864
- export type JsonArray = JsonValue[];
865
-
866
- export type JsonObject = {
867
- [x: string]: JsonValue | undefined;
868
- };
869
-
870
- export type JsonPrimitive = boolean | number | string | null;
871
-
872
- export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
873
-
874
- export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
875
- ? ColumnType<S, I | undefined, U>
876
- : ColumnType<T, T | undefined, T>
877
-
878
- export type Decimal = ColumnType<string, number | string, number | string>
879
-
880
- export type BigInt = ColumnType<string, number | string, number | string>
881
- `;
882
- consolidatedContent += "// Table Interfaces\n";
883
- for (const { content } of tableContents) {
884
- consolidatedContent += `${content}
885
- `;
886
- }
887
- consolidatedContent += `
57
+ describes = await extractColumnDescriptions(db, config, entityName);
58
+ }
59
+ if (describes.length === 0) continue;
60
+ for (const destination of nonKyselyDestinations) {
61
+ const content = entityType === "view" ? generateViewContent({
62
+ view: entityName,
63
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
64
+ config,
65
+ destination,
66
+ isCamelCase,
67
+ enumDeclarations,
68
+ defaultZodHeader
69
+ }) : generateContent({
70
+ table: entityName,
71
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
72
+ config,
73
+ destination,
74
+ isCamelCase,
75
+ enumDeclarations,
76
+ defaultZodHeader
77
+ });
78
+ const suffix = destination.suffix || destination.type;
79
+ const folder = destination.folder || ".";
80
+ const fileName = `${entityName}.${suffix}.ts`;
81
+ const filePath = path.join(folder, fileName);
82
+ results[filePath] = (destination.header || "") + content;
83
+ }
84
+ }
85
+ const kyselyDestinations = config.destinations.filter((d) => d.type === "kysely");
86
+ for (const kyselyDestination of kyselyDestinations) {
87
+ const header = kyselyDestination.header || defaultKyselyHeader;
88
+ const schemaName = kyselyDestination.schemaName || "DB";
89
+ let consolidatedContent = `${header}
90
+ ${kyselyJsonTypes}`;
91
+ const tableContents = [];
92
+ for (const entity of allEntities) {
93
+ const { name: entityName, type: entityType } = entity;
94
+ let describes;
95
+ if (config.origin.type === "prisma") {
96
+ describes = extractPrismaColumnDescriptions(config, entityName, enumDeclarations);
97
+ } else {
98
+ describes = await extractColumnDescriptions(db, config, entityName);
99
+ }
100
+ if (describes.length === 0) continue;
101
+ const content = entityType === "view" ? generateViewContent({
102
+ view: entityName,
103
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
104
+ config,
105
+ destination: kyselyDestination,
106
+ isCamelCase,
107
+ enumDeclarations,
108
+ defaultZodHeader
109
+ }) : generateContent({
110
+ table: entityName,
111
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
112
+ config,
113
+ destination: kyselyDestination,
114
+ isCamelCase,
115
+ enumDeclarations,
116
+ defaultZodHeader
117
+ });
118
+ tableContents.push({ table: entityName, content });
119
+ consolidatedContent += content + "\n";
120
+ }
121
+ consolidatedContent += `
888
122
  // Database Interface
889
123
  export interface ${schemaName} {
890
124
  `;
891
- const sortedTableEntries = tableContents.map(({ table }) => {
892
- const pascalTable = camelCase(table, { pascalCase: true });
893
- const tableKey = isCamelCase ? camelCase(table) : table;
894
- return { tableKey, pascalTable };
895
- }).sort((a, b) => a.tableKey.localeCompare(b.tableKey));
896
- for (const { tableKey, pascalTable } of sortedTableEntries) {
897
- consolidatedContent += ` ${tableKey}: ${pascalTable};
125
+ const sortedTableEntries = tableContents.map(({ table, content }) => {
126
+ const isView = content.includes("(view");
127
+ const pascalTable = camelCase(table, { pascalCase: true }) + (isView ? "View" : "");
128
+ const tableKey = isCamelCase ? camelCase(table) : table;
129
+ return { tableKey, pascalTable, isView };
130
+ }).sort((a, b) => a.tableKey.localeCompare(b.tableKey));
131
+ for (const { tableKey, pascalTable } of sortedTableEntries) {
132
+ consolidatedContent += ` ${tableKey}: ${pascalTable};
898
133
  `;
134
+ }
135
+ consolidatedContent += "}\n";
136
+ const outputFile = kyselyDestination.outFile || path.join(kyselyDestination.folder || ".", "db.ts");
137
+ results[outputFile] = consolidatedContent;
899
138
  }
900
- consolidatedContent += "}\n";
901
139
  if (config.dryRun) {
902
- const absolutePath = path.resolve(outFile);
903
- dryRunOutput[absolutePath] = consolidatedContent;
904
- for (const key of Object.keys(dryRunOutput)) {
905
- if (key.endsWith(".kysely.temp")) {
906
- delete dryRunOutput[key];
907
- }
140
+ return results;
141
+ }
142
+ for (const [filePath, content] of Object.entries(results)) {
143
+ const fullPath = path.resolve(filePath);
144
+ await fs.ensureDir(path.dirname(fullPath));
145
+ await fs.writeFile(fullPath, content);
146
+ if (!config.silent) {
147
+ console.log(`Created: ${filePath}`);
908
148
  }
909
- } else {
910
- const dest = path.resolve(outFile);
911
- dests.push(dest);
912
- if (!config.silent) console.log("Created:", dest);
913
- fs.outputFileSync(dest, consolidatedContent);
914
149
  }
915
- }
916
- if (!config.dryRun) {
917
- const result2 = {};
918
- for (const dest of dests) {
919
- const absolutePath = path.resolve(dest);
920
- const content = fs.readFileSync(dest, "utf8");
921
- result2[absolutePath] = content;
150
+ return results;
151
+ } finally {
152
+ if (db) {
153
+ await db.destroy();
922
154
  }
923
- return result2;
924
- }
925
- const result = {};
926
- for (const [key, content] of Object.entries(dryRunOutput)) {
927
- const absolutePath = path.resolve(key);
928
- result[absolutePath] = content;
929
155
  }
930
- return result;
931
156
  }
932
157
  export {
933
158
  defaultKyselyHeader,
@@ -937,6 +162,7 @@ export {
937
162
  extractTypeExpression,
938
163
  extractZodExpression,
939
164
  generate,
940
- generateContent,
165
+ generateContent2 as generateContent,
166
+ generateViewContent2 as generateViewContent,
941
167
  getType
942
168
  };