mutano 3.3.0 → 3.4.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 CHANGED
@@ -115,22 +115,19 @@ export type UpdateableUser = Updateable<User>;
115
115
  password: string,
116
116
  database: string,
117
117
  schema?: string, // PostgreSQL only
118
- ssl?: { ca, cert, key },
119
- overrideTypes?: Record<string, string>
118
+ ssl?: { ca, cert, key }
120
119
  }
121
120
 
122
121
  // SQLite
123
122
  {
124
123
  type: 'sqlite',
125
- path: string,
126
- overrideTypes?: Record<string, string>
124
+ path: string
127
125
  }
128
126
 
129
127
  // Prisma
130
128
  {
131
129
  type: 'prisma',
132
- path: string,
133
- overrideTypes?: Record<string, string>
130
+ path: string
134
131
  }
135
132
  ```
136
133
 
@@ -181,6 +178,8 @@ export type UpdateableUser = Updateable<User>;
181
178
  | `dryRun` | Return content without writing files |
182
179
  | `magicComments` | Enable @zod/@ts/@kysely comments (Obs.: no SQLite support) |
183
180
  | `inflection` | Transform model names: `'singular'`, `'plural'`, or `'none'` (default) |
181
+ | `overrideTypes` | Override types globally per destination (see below) |
182
+ | `overrideColumns` | Override specific columns per table (see below) |
184
183
 
185
184
  ### Inflection
186
185
 
@@ -307,24 +306,103 @@ Generated types will only include: `id`, `email`, `name`, and `metadata`
307
306
 
308
307
  ## Type Overrides
309
308
 
310
- Override default types globally in your origin config:
309
+ Override default types globally. Define destination-specific overrides for each output type:
311
310
 
312
311
  ```typescript
313
312
  {
314
313
  origin: {
315
314
  type: 'mysql',
316
315
  // ... connection config
317
- overrideTypes: {
316
+ },
317
+ overrideTypes: {
318
+ zod: {
318
319
  json: 'z.record(z.string())',
319
320
  text: 'z.string().max(1000)',
320
321
  decimal: 'z.number().positive()'
322
+ },
323
+ ts: {
324
+ json: 'Record<string, string>',
325
+ text: 'string',
326
+ decimal: 'number'
327
+ },
328
+ kysely: {
329
+ json: 'Json',
330
+ text: 'string',
331
+ decimal: 'Decimal'
321
332
  }
322
333
  }
323
334
  }
324
335
  ```
325
336
 
337
+ **Single destination with multiple outputs:**
338
+
339
+ ```typescript
340
+ {
341
+ origin: {
342
+ type: 'postgres',
343
+ // ...
344
+ },
345
+ destinations: [
346
+ { type: 'zod', folder: './zod' },
347
+ { type: 'ts', folder: './types' },
348
+ { type: 'kysely', outFile: './db.ts' }
349
+ ],
350
+ overrideTypes: {
351
+ zod: { jsonb: 'z.record(z.string())' },
352
+ ts: { jsonb: 'Record<string, string>' },
353
+ kysely: { jsonb: 'Json' }
354
+ }
355
+ }
356
+ ```
357
+
326
358
  **Common Overrides:**
327
359
  - **MySQL**: `json`, `text`, `decimal`, `enum`
328
360
  - **PostgreSQL**: `jsonb`, `uuid`, `text`, `numeric`
329
361
  - **SQLite**: `json`, `text`, `real`
330
- - **Prisma**: `Json`, `String`, `Decimal`
362
+ - **Prisma**: `Json`, `String`, `Decimal`
363
+
364
+ ### Override Columns
365
+
366
+ Override specific columns from specific tables. Takes priority over magic comments:
367
+
368
+ ```typescript
369
+ {
370
+ origin: {
371
+ type: 'mysql',
372
+ // ... connection config
373
+ },
374
+ destinations: [
375
+ { type: 'zod', folder: './zod' },
376
+ { type: 'ts', folder: './types' },
377
+ { type: 'kysely', outFile: './db.ts' }
378
+ ],
379
+ overrideColumns: {
380
+ zod: {
381
+ users: {
382
+ email: 'z.string().email()',
383
+ metadata: 'z.record(z.unknown())'
384
+ },
385
+ posts: {
386
+ content: 'z.string().max(10000)'
387
+ }
388
+ },
389
+ ts: {
390
+ users: {
391
+ email: 'EmailAddress',
392
+ metadata: 'Record<string, unknown>'
393
+ }
394
+ },
395
+ kysely: {
396
+ users: {
397
+ metadata: 'CustomJsonType'
398
+ }
399
+ }
400
+ }
401
+ }
402
+ ```
403
+
404
+ **Priority order** (highest to lowest):
405
+ 1. `overrideColumns` - Specific column overrides
406
+ 2. Magic comments (`@zod`, `@ts`, `@kysely`) - Column-level comments
407
+ 3. `overrideTypes` - Global type overrides
408
+ 4. Default type mappings
package/dist/main.d.ts CHANGED
@@ -40,9 +40,6 @@ interface Config {
40
40
  origin: {
41
41
  type: 'prisma';
42
42
  path: string;
43
- overrideTypes?: {
44
- [k in PrismaValidTypes]?: string;
45
- };
46
43
  } | {
47
44
  type: 'mysql';
48
45
  host: string;
@@ -50,9 +47,6 @@ interface Config {
50
47
  user: string;
51
48
  password: string;
52
49
  database: string;
53
- overrideTypes?: {
54
- [k in MySQLValidTypes]?: string;
55
- };
56
50
  ssl?: Record<string, any>;
57
51
  } | {
58
52
  type: 'postgres';
@@ -62,16 +56,10 @@ interface Config {
62
56
  password: string;
63
57
  database: string;
64
58
  schema?: string;
65
- overrideTypes?: {
66
- [k in PostgresValidTypes]?: string;
67
- };
68
59
  ssl?: Record<string, any>;
69
60
  } | {
70
61
  type: 'sqlite';
71
62
  path: string;
72
- overrideTypes?: {
73
- [k in SQLiteValidTypes]?: string;
74
- };
75
63
  };
76
64
  destinations: Destination[];
77
65
  tables?: string[];
@@ -82,6 +70,16 @@ interface Config {
82
70
  silent?: boolean;
83
71
  dryRun?: boolean;
84
72
  magicComments?: boolean;
73
+ overrideTypes?: {
74
+ zod?: Record<string, string>;
75
+ ts?: Record<string, string>;
76
+ kysely?: Record<string, string>;
77
+ };
78
+ overrideColumns?: {
79
+ zod?: Record<string, Record<string, string>>;
80
+ ts?: Record<string, Record<string, string>>;
81
+ kysely?: Record<string, Record<string, string>>;
82
+ };
85
83
  includeViews?: boolean;
86
84
  enumDeclarations?: Record<string, string[]>;
87
85
  inflection?: 'singular' | 'plural' | 'none';
@@ -104,10 +102,6 @@ interface GenerateViewContentParams {
104
102
  enumDeclarations: Record<string, string[]>;
105
103
  defaultZodHeader: (version: 3 | 4) => string;
106
104
  }
107
- type MySQLValidTypes = 'tinyint' | 'smallint' | 'mediumint' | 'int' | 'bigint' | 'decimal' | 'float' | 'double' | 'bit' | 'char' | 'varchar' | 'binary' | 'varbinary' | 'tinyblob' | 'blob' | 'mediumblob' | 'longblob' | 'tinytext' | 'text' | 'mediumtext' | 'longtext' | 'enum' | 'set' | 'date' | 'time' | 'datetime' | 'timestamp' | 'year' | 'json';
108
- type PostgresValidTypes = 'smallint' | 'integer' | 'bigint' | 'decimal' | 'numeric' | 'real' | 'double precision' | 'smallserial' | 'serial' | 'bigserial' | 'money' | 'character varying' | 'varchar' | 'character' | 'char' | 'text' | 'bytea' | 'timestamp' | 'timestamp with time zone' | 'timestamp without time zone' | 'date' | 'time' | 'time with time zone' | 'time without time zone' | 'interval' | 'boolean' | 'enum' | 'point' | 'line' | 'lseg' | 'box' | 'path' | 'polygon' | 'circle' | 'cidr' | 'inet' | 'macaddr' | 'bit' | 'bit varying' | 'uuid' | 'xml' | 'json' | 'jsonb' | 'int4range' | 'int8range' | 'numrange' | 'tsrange' | 'tstzrange' | 'daterange' | 'name' | 'citext';
109
- type SQLiteValidTypes = 'integer' | 'real' | 'text' | 'blob' | 'numeric' | 'boolean' | 'date' | 'datetime' | 'character' | 'varchar' | 'varying character' | 'nchar' | 'native character' | 'nvarchar' | 'clob' | 'double' | 'double precision' | 'float' | 'int' | 'int2' | 'int8' | 'bigint' | 'unsigned big int' | 'mediumint' | 'tinyint' | 'smallint' | 'decimal' | 'json';
110
- type PrismaValidTypes = 'String' | 'Boolean' | 'Int' | 'BigInt' | 'Float' | 'Decimal' | 'DateTime' | 'Json' | 'Bytes' | 'Unsupported';
111
105
 
112
106
  /**
113
107
  * Constants and default headers for code generation
@@ -157,7 +151,7 @@ type OperationType = 'table' | 'insertable' | 'updateable' | 'selectable';
157
151
  /**
158
152
  * Generate the appropriate type for a database field
159
153
  */
160
- declare function getType(op: OperationType, desc: Desc, config: Config, destination: Destination): string;
154
+ declare function getType(op: OperationType, desc: Desc, config: Config, destination: Destination, entityName?: string): string;
161
155
 
162
156
  /**
163
157
  * Mutano - Database schema to TypeScript/Zod/Kysely converter
package/dist/main.js CHANGED
@@ -218,7 +218,7 @@ const hasTableIgnoreDirective = (comment) => {
218
218
  return comment.includes("@@ignore");
219
219
  };
220
220
 
221
- function getType(op, desc, config, destination) {
221
+ function getType(op, desc, config, destination, entityName) {
222
222
  const { Default, Extra, Null, Type, Comment, EnumOptions } = desc;
223
223
  const schemaType = config.origin.type;
224
224
  const type = schemaType === "prisma" ? Type : Type.toLowerCase();
@@ -229,28 +229,18 @@ function getType(op, desc, config, destination) {
229
229
  const isKyselyDestination = destination.type === "kysely";
230
230
  const isZodDestination = destination.type === "zod";
231
231
  const typeMappings = getTypeMappings(schemaType);
232
- if (isTsDestination || isKyselyDestination) {
233
- const isJsonField = isJsonType(type);
234
- if (isKyselyDestination && isJsonField) {
235
- if (config.magicComments) {
236
- const kyselyOverrideType = extractKyselyExpression(Comment);
237
- if (kyselyOverrideType) {
238
- return kyselyOverrideType;
239
- }
240
- }
232
+ const destKey = isZodDestination ? "zod" : isTsDestination ? "ts" : "kysely";
233
+ if (entityName && config.overrideColumns) {
234
+ const destOverrides = config.overrideColumns[destKey];
235
+ if (destOverrides && destOverrides[entityName] && destOverrides[entityName][desc.Field]) {
236
+ const columnOverride = destOverrides[entityName][desc.Field];
241
237
  const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
242
- return shouldBeNullable ? "Json | null" : "Json";
243
- }
244
- if (isKyselyDestination && config.magicComments) {
245
- const kyselyOverrideType = extractKyselyExpression(Comment);
246
- if (kyselyOverrideType) {
247
- return kyselyOverrideType;
248
- }
249
- }
250
- if ((isTsDestination || isKyselyDestination) && config.magicComments) {
251
- const tsOverrideType = extractTSExpression(Comment);
252
- if (tsOverrideType) {
253
- return tsOverrideType;
238
+ if (isZodDestination) {
239
+ const nullishOption = destination.nullish;
240
+ const nullableMethod = nullishOption && op !== "selectable" ? "nullish" : "nullable";
241
+ return shouldBeNullable ? `${columnOverride}.${nullableMethod}()` : columnOverride;
242
+ } else {
243
+ return shouldBeNullable ? `${columnOverride} | null` : columnOverride;
254
244
  }
255
245
  }
256
246
  }
@@ -260,9 +250,24 @@ function getType(op, desc, config, destination) {
260
250
  return zodOverrideType;
261
251
  }
262
252
  }
263
- const overrideTypes = config.origin.overrideTypes;
264
- if (overrideTypes && overrideTypes[Type]) {
265
- const overrideType = overrideTypes[Type];
253
+ if (isTsDestination && config.magicComments) {
254
+ const tsOverrideType = extractTSExpression(Comment);
255
+ if (tsOverrideType) {
256
+ return tsOverrideType;
257
+ }
258
+ }
259
+ if (isKyselyDestination && config.magicComments) {
260
+ const kyselyOverrideType = extractKyselyExpression(Comment);
261
+ if (kyselyOverrideType) {
262
+ return kyselyOverrideType;
263
+ }
264
+ const tsOverrideType = extractTSExpression(Comment);
265
+ if (tsOverrideType) {
266
+ return tsOverrideType;
267
+ }
268
+ }
269
+ const overrideType = config.overrideTypes?.[destKey]?.[Type];
270
+ if (overrideType) {
266
271
  const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
267
272
  if (isZodDestination) {
268
273
  const nullishOption = destination.nullish;
@@ -272,6 +277,13 @@ function getType(op, desc, config, destination) {
272
277
  return shouldBeNullable ? `${overrideType} | null` : overrideType;
273
278
  }
274
279
  }
280
+ if (isTsDestination || isKyselyDestination) {
281
+ const isJsonField = isJsonType(type);
282
+ if (isKyselyDestination && isJsonField) {
283
+ const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
284
+ return shouldBeNullable ? "Json | null" : "Json";
285
+ }
286
+ }
275
287
  const enumTypesForSchema = typeMappings.enumTypes[schemaType] || [];
276
288
  const isEnum = enumTypesForSchema.includes(type);
277
289
  const isPrismaEnum = schemaType === "prisma" && config.enumDeclarations && config.enumDeclarations[type];
@@ -495,7 +507,7 @@ function generateViewContent({
495
507
  `;
496
508
  for (const desc of describes) {
497
509
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
498
- const fieldType = getType("selectable", desc, config, destination);
510
+ const fieldType = getType("selectable", desc, config, destination, view);
499
511
  content += ` ${fieldName}: ${fieldType};
500
512
  `;
501
513
  }
@@ -512,7 +524,7 @@ function generateViewContent({
512
524
  `;
513
525
  for (const desc of describes) {
514
526
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
515
- const fieldType = getType("selectable", desc, config, destination);
527
+ const fieldType = getType("selectable", desc, config, destination, view);
516
528
  content += ` ${fieldName}: ${fieldType};
517
529
  `;
518
530
  }
@@ -530,7 +542,7 @@ function generateViewContent({
530
542
  `;
531
543
  for (const desc of describes) {
532
544
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
533
- const fieldType = getType("selectable", desc, config, destination);
545
+ const fieldType = getType("selectable", desc, config, destination, view);
534
546
  content += ` ${fieldName}: ${fieldType},
535
547
  `;
536
548
  }
@@ -598,7 +610,7 @@ function generateTypeScriptContent({
598
610
  `;
599
611
  for (const desc of describes) {
600
612
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
601
- const fieldType = getType("table", desc, config, destination);
613
+ const fieldType = getType("table", desc, config, destination, table);
602
614
  content += ` ${fieldName}: ${fieldType};
603
615
  `;
604
616
  }
@@ -607,7 +619,7 @@ function generateTypeScriptContent({
607
619
  `;
608
620
  for (const desc of describes) {
609
621
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
610
- const fieldType = getType("insertable", desc, config, destination);
622
+ const fieldType = getType("insertable", desc, config, destination, table);
611
623
  content += ` ${fieldName}: ${fieldType};
612
624
  `;
613
625
  }
@@ -616,7 +628,7 @@ function generateTypeScriptContent({
616
628
  `;
617
629
  for (const desc of describes) {
618
630
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
619
- const fieldType = getType("updateable", desc, config, destination);
631
+ const fieldType = getType("updateable", desc, config, destination, table);
620
632
  content += ` ${fieldName}: ${fieldType};
621
633
  `;
622
634
  }
@@ -625,7 +637,7 @@ function generateTypeScriptContent({
625
637
  `;
626
638
  for (const desc of describes) {
627
639
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
628
- const fieldType = getType("selectable", desc, config, destination);
640
+ const fieldType = getType("selectable", desc, config, destination, table);
629
641
  content += ` ${fieldName}: ${fieldType};
630
642
  `;
631
643
  }
@@ -651,7 +663,7 @@ function generateKyselyContent({
651
663
  `;
652
664
  for (const desc of describes) {
653
665
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
654
- let fieldType = getType("table", desc, config, destination);
666
+ let fieldType = getType("table", desc, config, destination, table);
655
667
  const hasMagicComment = config.magicComments && (desc.Comment.includes("@kysely(") || desc.Comment.includes("@ts("));
656
668
  if (!hasMagicComment) {
657
669
  const isAutoIncrement = desc.Extra.toLowerCase().includes("auto_increment");
@@ -695,7 +707,7 @@ function generateZodContent({
695
707
  `;
696
708
  for (const desc of describes) {
697
709
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
698
- const fieldType = getType("table", desc, config, destination);
710
+ const fieldType = getType("table", desc, config, destination, table);
699
711
  content += ` ${fieldName}: ${fieldType},
700
712
  `;
701
713
  }
@@ -708,7 +720,7 @@ function generateZodContent({
708
720
  continue;
709
721
  }
710
722
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
711
- const fieldType = getType("insertable", desc, config, destination);
723
+ const fieldType = getType("insertable", desc, config, destination, table);
712
724
  content += ` ${fieldName}: ${fieldType},
713
725
  `;
714
726
  }
@@ -721,7 +733,7 @@ function generateZodContent({
721
733
  continue;
722
734
  }
723
735
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
724
- const fieldType = getType("updateable", desc, config, destination);
736
+ const fieldType = getType("updateable", desc, config, destination, table);
725
737
  content += ` ${fieldName}: ${fieldType},
726
738
  `;
727
739
  }
@@ -730,7 +742,7 @@ function generateZodContent({
730
742
  `;
731
743
  for (const desc of describes) {
732
744
  const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
733
- const fieldType = getType("selectable", desc, config, destination);
745
+ const fieldType = getType("selectable", desc, config, destination, table);
734
746
  content += ` ${fieldName}: ${fieldType},
735
747
  `;
736
748
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mutano",
3
3
  "type": "module",
4
- "version": "3.3.0",
4
+ "version": "3.4.0",
5
5
  "description": "Converts Prisma/MySQL/PostgreSQL/SQLite schemas to Zod/TS/Kysely interfaces",
6
6
  "author": "Alisson Cavalcante Agiani <thelinuxlich@gmail.com>",
7
7
  "license": "MIT",
@@ -16,22 +16,22 @@
16
16
  "test": "vitest run"
17
17
  },
18
18
  "dependencies": {
19
- "@mrleebo/prisma-ast": "^0.13.0",
19
+ "@mrleebo/prisma-ast": "^0.13.1",
20
20
  "@types/fs-extra": "^11.0.4",
21
- "@types/pg": "^8.15.5",
21
+ "@types/pg": "^8.16.0",
22
22
  "@types/pluralize": "^0.0.33",
23
- "camelcase": "^8.0.0",
24
- "fs-extra": "^11.3.2",
23
+ "camelcase": "^9.0.0",
24
+ "fs-extra": "^11.3.3",
25
25
  "knex": "^3.1.0",
26
- "mysql2": "^3.14.4",
27
- "pg": "^8.16.3",
26
+ "mysql2": "^3.16.3",
27
+ "pg": "^8.18.0",
28
28
  "pluralize": "^8.0.0",
29
29
  "sqlite3": "^5.1.7"
30
30
  },
31
31
  "devDependencies": {
32
- "@electric-sql/pglite": "^0.3.8",
33
- "pkgroll": "^2.15.4",
34
- "typescript": "^5.9.2",
35
- "vitest": "^3.2.4"
32
+ "@electric-sql/pglite": "^0.3.15",
33
+ "pkgroll": "^2.23.0",
34
+ "typescript": "^5.9.3",
35
+ "vitest": "^4.0.18"
36
36
  }
37
37
  }