mutano 2.1.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/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
@@ -199,10 +199,10 @@ function getType(op, desc, config, destination, tableName) {
199
199
  if (schemaType === "mysql") {
200
200
  const matches = Type.match(enumRegex);
201
201
  if (matches?.[1]) {
202
- enumValues = matches[1].split(",").map((v) => v.trim());
202
+ enumValues = matches[1].split(",").map((v) => v.trim()).sort();
203
203
  }
204
204
  } else if (EnumOptions && EnumOptions.length > 0) {
205
- enumValues = EnumOptions.map((e) => `'${e}'`);
205
+ enumValues = EnumOptions.map((e) => `'${e}'`).sort();
206
206
  }
207
207
  if (enumValues.length === 0) {
208
208
  return isNull ? "string | null" : "string";
@@ -303,7 +303,16 @@ function getType(op, desc, config, destination, tableName) {
303
303
  return field.join(".");
304
304
  };
305
305
  const generateEnumLikeField = () => {
306
- 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(",");
307
316
  const field = [`z.enum([${value}])`];
308
317
  if (isNull) field.push(nullable);
309
318
  else if (hasDefaultValue || !hasDefaultValue && isGenerated)
@@ -327,35 +336,16 @@ function generateContent({
327
336
  destination,
328
337
  isCamelCase,
329
338
  enumDeclarations: enumDeclarations2,
330
- defaultZodHeader: defaultZodHeader2,
331
- defaultKyselyHeader: defaultKyselyHeader2
339
+ defaultZodHeader: defaultZodHeader2
332
340
  }) {
333
341
  let content = "";
342
+ const schemaType = config.origin.type;
334
343
  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
344
  content += `// Kysely type definitions for ${table}
355
345
  `;
356
346
  content += `
357
347
  // This interface defines the structure of the '${table}' table
358
- export interface ${camelCase(table, { pascalCase: true })}Table {`;
348
+ export interface ${camelCase(table, { pascalCase: true })} {`;
359
349
  for (const desc of describes) {
360
350
  const field = isCamelCase ? camelCase(desc.Field) : desc.Field;
361
351
  const type = getType("table", desc, config, destination, table);
@@ -365,9 +355,13 @@ export interface ${camelCase(table, { pascalCase: true })}Table {`;
365
355
  const isDefaultGenerated = desc.Extra.toLowerCase().includes("default_generated");
366
356
  const isNullable = desc.Null === "YES";
367
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
+ );
368
362
  if (isJsonField) {
369
363
  kyselyType = "Json";
370
- } else if (isAutoIncrement || isDefaultGenerated) {
364
+ } else if (isAutoIncrement || isDefaultGenerated || isEnum && hasDefaultValue) {
371
365
  kyselyType = `Generated<${kyselyType}>`;
372
366
  }
373
367
  if (isNullable && !isJsonField) {
@@ -382,15 +376,10 @@ export interface ${camelCase(table, { pascalCase: true })}Table {`;
382
376
  content = `${content}
383
377
  }
384
378
 
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>;
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 })}>;
394
383
  `;
395
384
  } else if (destination.type === "ts") {
396
385
  const modelType = destination.modelType || "interface";
@@ -553,6 +542,7 @@ async function generate(config) {
553
542
  let prismaTables = [];
554
543
  let schema = null;
555
544
  let db = null;
545
+ const kyselyTableContents = {};
556
546
  if (config.destinations.length === 0) {
557
547
  throw new Error("Empty destinations object.");
558
548
  }
@@ -696,7 +686,7 @@ async function generate(config) {
696
686
  AND column_name = $3
697
687
  )
698
688
  ORDER BY
699
- e.enumsortorder
689
+ e.enumlabel
700
690
  `,
701
691
  [schema2, table, column.Field]
702
692
  );
@@ -769,16 +759,21 @@ async function generate(config) {
769
759
  if (!config.destinations || config.destinations.length === 0) {
770
760
  throw new Error("No destinations specified");
771
761
  }
772
- 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) {
773
769
  const content = generateContent({
774
770
  table,
775
- describes,
771
+ describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
776
772
  config,
777
773
  destination,
778
774
  isCamelCase: isCamelCase === true,
779
775
  enumDeclarations,
780
- defaultZodHeader,
781
- defaultKyselyHeader
776
+ defaultZodHeader
782
777
  });
783
778
  const suffix = destination.suffix || "";
784
779
  const folder = destination.folder || ".";
@@ -792,8 +787,88 @@ async function generate(config) {
792
787
  fs.outputFileSync(dest, content);
793
788
  }
794
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
+ }
795
813
  }
796
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
+ }
797
872
  return config.dryRun ? dryRunOutput : dests;
798
873
  }
799
874
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mutano",
3
3
  "type": "module",
4
- "version": "2.1.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,15 +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
28
  "ts-node": "^10.9.2",
29
29
  "tsx": "^4.19.4",
30
30
  "typescript": "^5.8.3",
31
- "vitest": "^3.1.2"
31
+ "vitest": "^3.1.3"
32
32
  }
33
33
  }