mutano 2.0.0 → 2.2.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
@@ -9,6 +9,7 @@ Converts Prisma/MySQL/PostgreSQL/SQLite schemas to Zod schemas, TypeScript inter
9
9
  - Handles nullable, default, auto-increment and enum fields
10
10
  - Supports custom type overrides via configuration or database comments
11
11
  - Intelligently handles field nullability based on operation type (table, insertable, updateable, selectable)
12
+ - All fields in updateable schemas are automatically made optional
12
13
 
13
14
  ## Installation
14
15
 
@@ -57,7 +58,7 @@ await generate({
57
58
  type: 'zod',
58
59
  useDateType: true,
59
60
  useTrim: false,
60
- nullish: false,
61
+ nullish: false, // When true, nullable fields use nullish() instead of nullable()
61
62
  folder: './generated',
62
63
  suffix: 'schema'
63
64
  }]
@@ -303,16 +304,16 @@ export const insertable_user = z.object({
303
304
  })
304
305
 
305
306
  export const updateable_user = z.object({
306
- name: z.string().min(10).max(255),
307
- username: z.string(),
308
- password: z.string(),
309
- profile_picture: z.string().nullable(),
310
- role: z.enum(['admin', 'user']),
307
+ name: z.string().min(10).max(255).optional(),
308
+ username: z.string().optional(),
309
+ password: z.string().optional(),
310
+ profile_picture: z.string().nullable().optional(),
311
+ role: z.enum(['admin', 'user']).optional(),
311
312
  })
312
313
 
313
314
  export const selectable_user = z.object({
314
315
  id: z.number().nonnegative(),
315
- name: z.string().min(10).max(255),
316
+ name: z.string(),
316
317
  username: z.string(),
317
318
  password: z.string(),
318
319
  profile_picture: z.string().nullable(),
@@ -372,92 +373,6 @@ export interface SelectableUser {
372
373
  }
373
374
  ```
374
375
 
375
- ### TypeScript Interface Output Example (Enum Type for Enums)
376
-
377
- ```typescript
378
- // TypeScript interfaces for user
379
-
380
- // Enum declarations
381
- enum RoleEnum {
382
- admin = 'admin',
383
- user = 'user'
384
- }
385
-
386
- export interface User {
387
- id: number;
388
- name: string;
389
- username: string;
390
- password: string;
391
- profile_picture: string | null;
392
- role: RoleEnum;
393
- }
394
-
395
- export interface InsertableUser {
396
- name: string | null; // Optional because it has a default value
397
- username: string;
398
- password: string;
399
- profile_picture: string | null;
400
- role: RoleEnum;
401
- }
402
-
403
- export interface UpdateableUser {
404
- name: string | null; // Optional for updates
405
- username: string | null; // Optional for updates
406
- password: string | null; // Optional for updates
407
- profile_picture: string | null;
408
- role: RoleEnum | null; // Optional for updates
409
- }
410
-
411
- export interface SelectableUser {
412
- id: number;
413
- name: string;
414
- username: string;
415
- password: string;
416
- profile_picture: string | null;
417
- role: RoleEnum;
418
- }
419
- ```
420
-
421
- ### TypeScript Type Alias Output Example
422
-
423
- ```typescript
424
- // TypeScript types for user
425
-
426
- export type User = {
427
- id: number;
428
- name: string;
429
- username: string;
430
- password: string;
431
- profile_picture: string | null;
432
- role: 'admin' | 'user';
433
- }
434
-
435
- export type InsertableUser = {
436
- name: string | null; // Optional because it has a default value
437
- username: string;
438
- password: string;
439
- profile_picture: string | null;
440
- role: 'admin' | 'user';
441
- }
442
-
443
- export type UpdateableUser = {
444
- name: string | null; // Optional for updates
445
- username: string | null; // Optional for updates
446
- password: string | null; // Optional for updates
447
- profile_picture: string | null;
448
- role: 'admin' | 'user' | null; // Optional for updates
449
- }
450
-
451
- export type SelectableUser = {
452
- id: number;
453
- name: string;
454
- username: string;
455
- password: string;
456
- profile_picture: string | null;
457
- role: 'admin' | 'user';
458
- }
459
- ```
460
-
461
376
  ### Kysely Type Definitions Output Example
462
377
 
463
378
  ```typescript
@@ -500,49 +415,6 @@ export type NewUser = Insertable<UserTable>;
500
415
  export type UserUpdate = Updateable<UserTable>;
501
416
  ```
502
417
 
503
- ### Kysely Type Definitions Output Example with Custom Schema Name
504
-
505
- ```typescript
506
- import { Generated, ColumnType, Selectable, Insertable, Updateable } from 'kysely';
507
- import { CustomTypes } from './types';
508
-
509
- // JSON type definitions
510
- export type Json = ColumnType<JsonValue, string, string>;
511
-
512
- export type JsonArray = JsonValue[];
513
-
514
- export type JsonObject = {
515
- [x: string]: JsonValue | undefined;
516
- };
517
-
518
- export type JsonPrimitive = boolean | number | string | null;
519
-
520
- export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
521
-
522
- // Kysely type definitions for user
523
-
524
- // This interface defines the structure of the 'user' table
525
- export interface UserTable {
526
- id: Generated<number>;
527
- name: string;
528
- username: string;
529
- password: string;
530
- profile_picture: string | null;
531
- metadata: Json;
532
- role: 'admin' | 'user';
533
- }
534
-
535
- // Define the database interface
536
- export interface Database {
537
- user: UserTable;
538
- }
539
-
540
- // Use these types for inserting, selecting and updating the table
541
- export type User = Selectable<UserTable>;
542
- export type NewUser = Insertable<UserTable>;
543
- export type UserUpdate = Updateable<UserTable>;
544
- ```
545
-
546
418
  ## Config
547
419
 
548
420
  ```json
@@ -596,8 +468,8 @@ export type UserUpdate = Updateable<UserTable>;
596
468
  "type": "zod",
597
469
  "useDateType": true,
598
470
  "useTrim": false,
599
- "nullish": false,
600
- "requiredString": false,
471
+ "nullish": false, // When true, nullable fields use nullish() instead of nullable()
472
+ "requiredString": false, // When true, adds min(1) validation to non-nullable string fields
601
473
  "header": "import { z } from 'zod';\nimport { CustomValidator } from './validators';",
602
474
  "folder": "@zod",
603
475
  "suffix": "table"
@@ -633,8 +505,8 @@ export type UserUpdate = Updateable<UserTable>;
633
505
  | destinations[].type | The type of output to generate: "zod", "ts", or "kysely" |
634
506
  | destinations[].useDateType | (Zod only) Use a specialized Zod type for date-like fields instead of string |
635
507
  | destinations[].useTrim | (Zod only) Use `z.string().trim()` instead of `z.string()` |
636
- | destinations[].nullish | (Zod only) Set schema as `nullish` instead of `nullable` |
637
- | destinations[].requiredString | (Zod only) Add `min(1)` for string schema |
508
+ | destinations[].nullish | (Zod only) Use `nullish()` instead of `nullable()` for nullable fields. In updateable schemas, fields that were already nullable will become nullish |
509
+ | destinations[].requiredString | (Zod only) Add `min(1)` for non-nullable string fields |
638
510
  | destinations[].enumType | (TypeScript only) How to represent enum types: "union" (default) or "enum" |
639
511
  | destinations[].modelType | (TypeScript only) How to represent models: "interface" (default) or "type" |
640
512
  | destinations[].schemaName | (Kysely only) Name of the database interface (default: "DB") |
package/dist/main.d.ts CHANGED
@@ -7,9 +7,8 @@ export interface GenerateContentParams {
7
7
  isCamelCase: boolean;
8
8
  enumDeclarations: Record<string, string[]>;
9
9
  defaultZodHeader: string;
10
- defaultKyselyHeader: string;
11
10
  }
12
- export declare function generateContent({ table, describes, config, destination, isCamelCase, enumDeclarations, defaultZodHeader, defaultKyselyHeader, }: GenerateContentParams): string;
11
+ export declare function generateContent({ table, describes, config, destination, isCamelCase, enumDeclarations, defaultZodHeader, }: GenerateContentParams): string;
13
12
  export declare const defaultKyselyHeader = "import { Generated, ColumnType, Selectable, Insertable, Updateable } from 'kysely';\n\n";
14
13
  export declare const defaultZodHeader = "import { z } from 'zod';\n\n";
15
14
  export declare function generate(config: Config): Promise<string[] | Record<string, string>>;
@@ -46,8 +45,7 @@ export type Destination = {
46
45
  type: 'kysely';
47
46
  header?: string;
48
47
  schemaName?: string;
49
- folder?: string;
50
- suffix?: string;
48
+ outFile?: string;
51
49
  };
52
50
  export interface Config {
53
51
  origin: {
package/dist/main.js CHANGED
@@ -164,6 +164,7 @@ function getType(op, desc, config, destination, tableName) {
164
164
  const { Default, Extra, Null, Type, Comment, EnumOptions } = desc;
165
165
  const isZodDestination = destination.type === "zod";
166
166
  const isTsDestination = destination.type === "ts";
167
+ const isKyselyDestination = destination.type === "kysely";
167
168
  const isNullish = isZodDestination && destination.type === "zod" && destination.nullish === true;
168
169
  const isTrim = isZodDestination && destination.type === "zod" && destination.useTrim === true && op !== "selectable";
169
170
  const isUseDateType = isZodDestination && destination.type === "zod" && destination.useDateType === true;
@@ -174,11 +175,11 @@ function getType(op, desc, config, destination, tableName) {
174
175
  return;
175
176
  const isRequiredString = destination.type === "zod" && destination.requiredString === true && op !== "selectable";
176
177
  const type = schemaType === "mysql" ? Type.split("(")[0].split(" ")[0] : Type;
177
- if (isTsDestination) {
178
+ if (isTsDestination || isKyselyDestination) {
178
179
  const tsOverrideType = config.magicComments ? extractTSExpression(Comment) : null;
179
180
  const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
180
181
  if (tsOverrideType) {
181
- return shouldBeNullable ? `${tsOverrideType} | null` : tsOverrideType;
182
+ return shouldBeNullable ? tsOverrideType.includes("| null") ? tsOverrideType : `${tsOverrideType} | null` : tsOverrideType;
182
183
  }
183
184
  if (dateTypes[schemaType].includes(type)) {
184
185
  return shouldBeNullable ? "Date | null" : "Date";
@@ -198,10 +199,10 @@ function getType(op, desc, config, destination, tableName) {
198
199
  if (schemaType === "mysql") {
199
200
  const matches = Type.match(enumRegex);
200
201
  if (matches?.[1]) {
201
- enumValues = matches[1].split(",").map((v) => v.trim());
202
+ enumValues = matches[1].split(",").map((v) => v.trim()).sort();
202
203
  }
203
204
  } else if (EnumOptions && EnumOptions.length > 0) {
204
- enumValues = EnumOptions.map((e) => `'${e}'`);
205
+ enumValues = EnumOptions.map((e) => `'${e}'`).sort();
205
206
  }
206
207
  if (enumValues.length === 0) {
207
208
  return isNull ? "string | null" : "string";
@@ -302,7 +303,16 @@ function getType(op, desc, config, destination, tableName) {
302
303
  return field.join(".");
303
304
  };
304
305
  const generateEnumLikeField = () => {
305
- const value = schemaType === "mysql" ? Type.replace("enum(", "").replace(")", "").replace(/,/g, ",") : EnumOptions?.map((e) => `'${e}'`).join(",");
306
+ let enumValues = [];
307
+ if (schemaType === "mysql") {
308
+ const matches = Type.match(enumRegex);
309
+ if (matches?.[1]) {
310
+ enumValues = matches[1].split(",").map((v) => v.trim()).sort();
311
+ }
312
+ } else if (EnumOptions && EnumOptions.length > 0) {
313
+ enumValues = [...EnumOptions].sort().map((e) => `'${e}'`);
314
+ }
315
+ const value = enumValues.join(",");
306
316
  const field = [`z.enum([${value}])`];
307
317
  if (isNull) field.push(nullable);
308
318
  else if (hasDefaultValue || !hasDefaultValue && isGenerated)
@@ -326,35 +336,16 @@ function generateContent({
326
336
  destination,
327
337
  isCamelCase,
328
338
  enumDeclarations: enumDeclarations2,
329
- defaultZodHeader: defaultZodHeader2,
330
- defaultKyselyHeader: defaultKyselyHeader2
339
+ defaultZodHeader: defaultZodHeader2
331
340
  }) {
332
341
  let content = "";
342
+ const schemaType = config.origin.type;
333
343
  if (destination.type === "kysely") {
334
- const header = destination.header;
335
- const schemaName = destination.schemaName || "DB";
336
- content = header ? `${header}
337
-
338
- ` : defaultKyselyHeader2;
339
- content += `// JSON type definitions
340
- export type Json = ColumnType<JsonValue, string, string>;
341
-
342
- export type JsonArray = JsonValue[];
343
-
344
- export type JsonObject = {
345
- [x: string]: JsonValue | undefined;
346
- };
347
-
348
- export type JsonPrimitive = boolean | number | string | null;
349
-
350
- export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
351
-
352
- `;
353
344
  content += `// Kysely type definitions for ${table}
354
345
  `;
355
346
  content += `
356
347
  // This interface defines the structure of the '${table}' table
357
- export interface ${camelCase(table, { pascalCase: true })}Table {`;
348
+ export interface ${camelCase(table, { pascalCase: true })} {`;
358
349
  for (const desc of describes) {
359
350
  const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
360
351
  const type = getType("table", desc, config, destination, table);
@@ -364,9 +355,13 @@ export interface ${camelCase(table, { pascalCase: true })}Table {`;
364
355
  const isDefaultGenerated = desc.Extra.toLowerCase().includes("default_generated");
365
356
  const isNullable = desc.Null === "YES";
366
357
  const isJsonField = desc.Type.toLowerCase().includes("json");
358
+ const hasDefaultValue = desc.Default !== null;
359
+ const isEnum = schemaType !== "sqlite" && enumTypes[schemaType].includes(
360
+ schemaType === "mysql" ? desc.Type.split("(")[0].split(" ")[0] : desc.Type
361
+ );
367
362
  if (isJsonField) {
368
363
  kyselyType = "Json";
369
- } else if (isAutoIncrement || isDefaultGenerated) {
364
+ } else if (isAutoIncrement || isDefaultGenerated || isEnum && hasDefaultValue) {
370
365
  kyselyType = `Generated<${kyselyType}>`;
371
366
  }
372
367
  if (isNullable && !isJsonField) {
@@ -381,15 +376,10 @@ export interface ${camelCase(table, { pascalCase: true })}Table {`;
381
376
  content = `${content}
382
377
  }
383
378
 
384
- // Define the database interface
385
- export interface ${schemaName} {
386
- ${table}: ${camelCase(table, { pascalCase: true })}Table;
387
- }
388
-
389
- // Use these types for inserting, selecting and updating the table
390
- export type ${camelCase(table, { pascalCase: true })} = Selectable<${camelCase(table, { pascalCase: true })}Table>;
391
- export type New${camelCase(table, { pascalCase: true })} = Insertable<${camelCase(table, { pascalCase: true })}Table>;
392
- export type ${camelCase(table, { pascalCase: true })}Update = Updateable<${camelCase(table, { pascalCase: true })}Table>;
379
+ // Helper types for ${table}
380
+ export type Selectable${camelCase(table, { pascalCase: true })} = Selectable<${camelCase(table, { pascalCase: true })}>;
381
+ export type Insertable${camelCase(table, { pascalCase: true })} = Insertable<${camelCase(table, { pascalCase: true })}>;
382
+ export type Updateable${camelCase(table, { pascalCase: true })} = Updateable<${camelCase(table, { pascalCase: true })}>;
393
383
  `;
394
384
  } else if (destination.type === "ts") {
395
385
  const modelType = destination.modelType || "interface";
@@ -552,6 +542,10 @@ async function generate(config) {
552
542
  let prismaTables = [];
553
543
  let schema = null;
554
544
  let db = null;
545
+ const kyselyTableContents = {};
546
+ if (config.destinations.length === 0) {
547
+ throw new Error("Empty destinations object.");
548
+ }
555
549
  const dryRunOutput = {};
556
550
  if (config.origin.type === "mysql") {
557
551
  db = knex({
@@ -692,7 +686,7 @@ async function generate(config) {
692
686
  AND column_name = $3
693
687
  )
694
688
  ORDER BY
695
- e.enumsortorder
689
+ e.enumlabel
696
690
  `,
697
691
  [schema2, table, column.Field]
698
692
  );
@@ -765,16 +759,21 @@ async function generate(config) {
765
759
  if (!config.destinations || config.destinations.length === 0) {
766
760
  throw new Error("No destinations specified");
767
761
  }
768
- for (const destination of config.destinations) {
762
+ const kyselyDestinations = config.destinations.filter(
763
+ (d) => d.type === "kysely"
764
+ );
765
+ const nonKyselyDestinations = config.destinations.filter(
766
+ (d) => d.type !== "kysely"
767
+ );
768
+ for (const destination of nonKyselyDestinations) {
769
769
  const content = generateContent({
770
770
  table,
771
- describes,
771
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
772
772
  config,
773
773
  destination,
774
774
  isCamelCase: isCamelCase === true,
775
775
  enumDeclarations,
776
- defaultZodHeader,
777
- defaultKyselyHeader
776
+ defaultZodHeader
778
777
  });
779
778
  const suffix = destination.suffix || "";
780
779
  const folder = destination.folder || ".";
@@ -788,8 +787,88 @@ async function generate(config) {
788
787
  fs.outputFileSync(dest, content);
789
788
  }
790
789
  }
790
+ for (const destination of kyselyDestinations) {
791
+ const content = generateContent({
792
+ table,
793
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
794
+ config,
795
+ destination,
796
+ isCamelCase: isCamelCase === true,
797
+ enumDeclarations,
798
+ defaultZodHeader
799
+ });
800
+ const outFile = destination.outFile || "db.ts";
801
+ if (!kyselyTableContents[outFile]) {
802
+ kyselyTableContents[outFile] = [];
803
+ }
804
+ kyselyTableContents[outFile].push({
805
+ table,
806
+ content
807
+ });
808
+ if (config.dryRun) {
809
+ const tempKey = `${table}.kysely.temp`;
810
+ dryRunOutput[tempKey] = content;
811
+ }
812
+ }
791
813
  }
792
814
  if (db) await db.destroy();
815
+ for (const [outFile, tableContents] of Object.entries(kyselyTableContents)) {
816
+ if (tableContents.length === 0) continue;
817
+ const kyselyDestination = config.destinations.find(
818
+ (d) => d.type === "kysely"
819
+ );
820
+ const header = kyselyDestination?.header || defaultKyselyHeader;
821
+ const schemaName = kyselyDestination?.schemaName || "DB";
822
+ let consolidatedContent = `${header}
823
+
824
+ // JSON type definitions
825
+ export type Json = ColumnType<JsonValue, string, string>;
826
+
827
+ export type JsonArray = JsonValue[];
828
+
829
+ export type JsonObject = {
830
+ [x: string]: JsonValue | undefined;
831
+ };
832
+
833
+ export type JsonPrimitive = boolean | number | string | null;
834
+
835
+ export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
836
+
837
+ `;
838
+ consolidatedContent += "// Table Interfaces\n";
839
+ for (const { content } of tableContents) {
840
+ consolidatedContent += `${content}
841
+ `;
842
+ }
843
+ consolidatedContent += `
844
+ // Database Interface
845
+ export interface ${schemaName} {
846
+ `;
847
+ const sortedTableEntries = tableContents.map(({ table }) => {
848
+ const pascalTable = camelCase(table, { pascalCase: true });
849
+ const tableKey = isCamelCase ? camelCase(table) : table;
850
+ return { tableKey, pascalTable };
851
+ }).sort((a, b) => a.tableKey.localeCompare(b.tableKey));
852
+ for (const { tableKey, pascalTable } of sortedTableEntries) {
853
+ consolidatedContent += ` ${tableKey}: ${pascalTable};
854
+ `;
855
+ }
856
+ consolidatedContent += "}\n";
857
+ if (config.dryRun) {
858
+ const fileName = path.basename(outFile);
859
+ dryRunOutput[fileName] = consolidatedContent;
860
+ for (const key of Object.keys(dryRunOutput)) {
861
+ if (key.endsWith(".kysely.temp")) {
862
+ delete dryRunOutput[key];
863
+ }
864
+ }
865
+ } else {
866
+ const dest = path.resolve(outFile);
867
+ dests.push(dest);
868
+ if (!config.silent) console.log("Created:", dest);
869
+ fs.outputFileSync(dest, consolidatedContent);
870
+ }
871
+ }
793
872
  return config.dryRun ? dryRunOutput : dests;
794
873
  }
795
874
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mutano",
3
3
  "type": "module",
4
- "version": "2.0.0",
4
+ "version": "2.2.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",
@@ -19,13 +19,15 @@
19
19
  "fs-extra": "^11.3.0",
20
20
  "knex": "^3.1.0",
21
21
  "mysql2": "^3.14.1",
22
- "pg": "^8.15.6",
22
+ "pg": "^8.16.0",
23
23
  "sqlite3": "^5.1.7"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/fs-extra": "^11.0.4",
27
- "esbuild": "^0.25.3",
27
+ "esbuild": "^0.25.4",
28
+ "ts-node": "^10.9.2",
29
+ "tsx": "^4.19.4",
28
30
  "typescript": "^5.8.3",
29
- "vitest": "^3.1.2"
31
+ "vitest": "^3.1.3"
30
32
  }
31
33
  }