mutano 3.0.1 → 3.0.3
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 +4 -12
- package/dist/main.d.ts +103 -33
- package/dist/main.js +838 -760
- package/package.json +8 -8
package/dist/main.js
CHANGED
|
@@ -1,58 +1,61 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
} from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import camelCase from 'camelcase';
|
|
3
|
+
import * as fs from 'fs-extra';
|
|
4
|
+
import knex from 'knex';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { createPrismaSchemaBuilder } from '@mrleebo/prisma-ast';
|
|
7
|
+
|
|
8
|
+
function filterEntities(entities, included, ignored) {
|
|
9
|
+
let filtered = [...entities];
|
|
10
|
+
if (included?.length) {
|
|
11
|
+
filtered = filtered.filter((entity) => included.includes(entity));
|
|
12
|
+
}
|
|
13
|
+
if (ignored?.length) {
|
|
14
|
+
const ignoredRegex = ignored.filter((ignoreString) => {
|
|
15
|
+
return ignoreString.startsWith("/") && ignoreString.endsWith("/");
|
|
16
|
+
});
|
|
17
|
+
const ignoredNames = ignored.filter(
|
|
18
|
+
(entity) => !ignoredRegex.includes(entity)
|
|
19
|
+
);
|
|
20
|
+
if (ignoredNames.length) {
|
|
21
|
+
filtered = filtered.filter((entity) => !ignoredNames.includes(entity));
|
|
22
|
+
}
|
|
23
|
+
if (ignoredRegex.length) {
|
|
24
|
+
filtered = filtered.filter((entity) => {
|
|
25
|
+
let useEntity = true;
|
|
26
|
+
for (const text of ignoredRegex) {
|
|
27
|
+
const pattern = text.substring(1, text.length - 1);
|
|
28
|
+
if (entity.match(pattern) !== null) useEntity = false;
|
|
29
|
+
}
|
|
30
|
+
return useEntity;
|
|
31
|
+
});
|
|
26
32
|
}
|
|
27
|
-
position++;
|
|
28
33
|
}
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
];
|
|
34
|
+
return filtered;
|
|
35
|
+
}
|
|
36
|
+
function filterTables(tables, includedTables, ignoredTables) {
|
|
37
|
+
return filterEntities(tables, includedTables, ignoredTables);
|
|
38
|
+
}
|
|
39
|
+
function filterViews(views, includedViews, ignoredViews) {
|
|
40
|
+
return filterEntities(views, includedViews, ignoredViews);
|
|
41
|
+
}
|
|
42
|
+
function createEntityList(tables, views) {
|
|
43
|
+
const allEntities = [
|
|
44
|
+
...tables.map((name) => ({ name, type: "table" })),
|
|
45
|
+
...views.map((name) => ({ name, type: "view" }))
|
|
46
|
+
];
|
|
47
|
+
return allEntities.sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
const dateTypes = {
|
|
47
51
|
mysql: ["date", "datetime", "timestamp"],
|
|
48
52
|
postgres: [
|
|
49
|
-
"date",
|
|
50
53
|
"timestamp",
|
|
51
|
-
"
|
|
54
|
+
"timestamp with time zone",
|
|
52
55
|
"timestamp without time zone",
|
|
53
|
-
"
|
|
56
|
+
"date"
|
|
54
57
|
],
|
|
55
|
-
sqlite: ["datetime"],
|
|
58
|
+
sqlite: ["date", "datetime"],
|
|
56
59
|
prisma: ["DateTime"]
|
|
57
60
|
};
|
|
58
61
|
const stringTypes = {
|
|
@@ -98,767 +101,767 @@ const stringTypes = {
|
|
|
98
101
|
const bigIntTypes = {
|
|
99
102
|
mysql: ["bigint"],
|
|
100
103
|
postgres: ["bigint"],
|
|
101
|
-
sqlite: ["bigint"],
|
|
104
|
+
sqlite: ["bigint", "unsigned big int", "int8"],
|
|
102
105
|
prisma: ["BigInt"]
|
|
103
106
|
};
|
|
104
107
|
const numberTypes = {
|
|
105
|
-
mysql: [
|
|
108
|
+
mysql: [
|
|
109
|
+
"tinyint",
|
|
110
|
+
"smallint",
|
|
111
|
+
"mediumint",
|
|
112
|
+
"int",
|
|
113
|
+
"float",
|
|
114
|
+
"double",
|
|
115
|
+
"bit",
|
|
116
|
+
"year"
|
|
117
|
+
],
|
|
106
118
|
postgres: [
|
|
107
119
|
"smallint",
|
|
108
120
|
"integer",
|
|
109
121
|
"real",
|
|
110
122
|
"double precision",
|
|
123
|
+
"smallserial",
|
|
111
124
|
"serial",
|
|
112
|
-
"bigserial"
|
|
125
|
+
"bigserial",
|
|
126
|
+
"bit",
|
|
127
|
+
"bit varying"
|
|
113
128
|
],
|
|
114
129
|
sqlite: [
|
|
115
|
-
"int",
|
|
116
130
|
"integer",
|
|
117
|
-
"tinyint",
|
|
118
|
-
"smallint",
|
|
119
|
-
"mediumint",
|
|
120
|
-
"unsigned big int",
|
|
121
|
-
"int2",
|
|
122
|
-
"int8",
|
|
123
131
|
"real",
|
|
132
|
+
"numeric",
|
|
124
133
|
"double",
|
|
125
134
|
"double precision",
|
|
126
|
-
"float"
|
|
135
|
+
"float",
|
|
136
|
+
"int",
|
|
137
|
+
"int2",
|
|
138
|
+
"mediumint",
|
|
139
|
+
"tinyint",
|
|
140
|
+
"smallint"
|
|
127
141
|
],
|
|
128
142
|
prisma: ["Int", "Float"]
|
|
129
143
|
};
|
|
130
144
|
const decimalTypes = {
|
|
131
145
|
mysql: ["decimal"],
|
|
132
|
-
postgres: ["decimal", "numeric"],
|
|
133
|
-
sqlite: ["
|
|
146
|
+
postgres: ["decimal", "numeric", "money"],
|
|
147
|
+
sqlite: ["decimal"],
|
|
134
148
|
prisma: ["Decimal"]
|
|
135
149
|
};
|
|
136
150
|
const booleanTypes = {
|
|
137
|
-
mysql: ["
|
|
138
|
-
postgres: ["boolean"
|
|
151
|
+
mysql: ["boolean"],
|
|
152
|
+
postgres: ["boolean"],
|
|
139
153
|
sqlite: ["boolean"],
|
|
140
154
|
prisma: ["Boolean"]
|
|
141
155
|
};
|
|
142
156
|
const enumTypes = {
|
|
143
157
|
mysql: ["enum"],
|
|
144
|
-
postgres: ["USER-DEFINED"],
|
|
158
|
+
postgres: ["enum", "USER-DEFINED"],
|
|
145
159
|
sqlite: [],
|
|
146
|
-
|
|
147
|
-
prisma: ["Enum"]
|
|
160
|
+
prisma: []
|
|
148
161
|
};
|
|
149
162
|
const enumRegex = /enum\(([^)]+)\)/;
|
|
150
|
-
function
|
|
151
|
-
|
|
163
|
+
function getTypeMappings(dbType) {
|
|
164
|
+
return {
|
|
165
|
+
dateTypes: dateTypes[dbType],
|
|
166
|
+
stringTypes: stringTypes[dbType],
|
|
167
|
+
bigIntTypes: bigIntTypes[dbType],
|
|
168
|
+
numberTypes: numberTypes[dbType],
|
|
169
|
+
decimalTypes: decimalTypes[dbType],
|
|
170
|
+
booleanTypes: booleanTypes[dbType],
|
|
171
|
+
enumTypes: enumTypes[dbType]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function isJsonType(type) {
|
|
175
|
+
return type.toLowerCase().includes("json");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const extractTypeExpression = (comment, prefix) => {
|
|
179
|
+
const start = comment.indexOf(prefix);
|
|
180
|
+
if (start === -1) return null;
|
|
181
|
+
const typeLen = prefix.length;
|
|
182
|
+
let position = start + typeLen;
|
|
183
|
+
let depth = 1;
|
|
184
|
+
while (position < comment.length && depth > 0) {
|
|
185
|
+
const char = comment[position];
|
|
186
|
+
if (char === "(" || char === "{" || char === "<" || char === "[") {
|
|
187
|
+
depth++;
|
|
188
|
+
} else if (char === ")" || char === "}" || char === ">" || char === "]") {
|
|
189
|
+
depth--;
|
|
190
|
+
if (depth === 0) {
|
|
191
|
+
const extracted = comment.substring(start + typeLen, position);
|
|
192
|
+
return extracted;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
position++;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
199
|
+
const extractTSExpression = (comment) => extractTypeExpression(comment, "@ts(");
|
|
200
|
+
const extractKyselyExpression = (comment) => extractTypeExpression(comment, "@kysely(");
|
|
201
|
+
const extractZodExpression = (comment) => extractTypeExpression(comment, "@zod(");
|
|
202
|
+
|
|
203
|
+
function getType(op, desc, config, destination) {
|
|
152
204
|
const { Default, Extra, Null, Type, Comment, EnumOptions } = desc;
|
|
153
|
-
const
|
|
205
|
+
const schemaType = config.origin.type;
|
|
206
|
+
const type = schemaType === "prisma" ? Type : Type.toLowerCase();
|
|
207
|
+
const isNull = Null === "YES";
|
|
208
|
+
const hasDefaultValue = Default !== null;
|
|
209
|
+
const isGenerated = Extra.toLowerCase().includes("auto_increment") || Extra.toLowerCase().includes("default_generated");
|
|
154
210
|
const isTsDestination = destination.type === "ts";
|
|
155
211
|
const isKyselyDestination = destination.type === "kysely";
|
|
156
|
-
const
|
|
157
|
-
const
|
|
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;
|
|
212
|
+
const isZodDestination = destination.type === "zod";
|
|
213
|
+
const typeMappings = getTypeMappings(schemaType);
|
|
166
214
|
if (isTsDestination || isKyselyDestination) {
|
|
215
|
+
const isJsonField = isJsonType(type);
|
|
216
|
+
if (isKyselyDestination && isJsonField) {
|
|
217
|
+
if (config.magicComments) {
|
|
218
|
+
const kyselyOverrideType = extractKyselyExpression(Comment);
|
|
219
|
+
if (kyselyOverrideType) {
|
|
220
|
+
const shouldBeNullable2 = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
221
|
+
return shouldBeNullable2 ? kyselyOverrideType.includes("| null") ? kyselyOverrideType : `${kyselyOverrideType} | null` : kyselyOverrideType;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
225
|
+
return shouldBeNullable ? "Json | null" : "Json";
|
|
226
|
+
}
|
|
167
227
|
if (isKyselyDestination && config.magicComments) {
|
|
168
228
|
const kyselyOverrideType = extractKyselyExpression(Comment);
|
|
169
229
|
if (kyselyOverrideType) {
|
|
170
|
-
const
|
|
171
|
-
return
|
|
230
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
231
|
+
return shouldBeNullable ? kyselyOverrideType.includes("| null") ? kyselyOverrideType : `${kyselyOverrideType} | null` : kyselyOverrideType;
|
|
172
232
|
}
|
|
173
233
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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";
|
|
234
|
+
if ((isTsDestination || isKyselyDestination) && config.magicComments) {
|
|
235
|
+
const tsOverrideType = extractTSExpression(Comment);
|
|
236
|
+
if (tsOverrideType) {
|
|
237
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
238
|
+
return shouldBeNullable ? tsOverrideType.includes("| null") ? tsOverrideType : `${tsOverrideType} | null` : tsOverrideType;
|
|
194
239
|
}
|
|
195
|
-
return shouldBeNullable ? "string | null" : "string";
|
|
196
240
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
241
|
+
}
|
|
242
|
+
if (isZodDestination && config.magicComments) {
|
|
243
|
+
const zodOverrideType = extractZodExpression(Comment);
|
|
244
|
+
if (zodOverrideType) {
|
|
245
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
246
|
+
const nullishOption = destination.nullish;
|
|
247
|
+
const nullableMethod = nullishOption ? "nullish" : "nullable";
|
|
248
|
+
return shouldBeNullable ? zodOverrideType.includes(`.${nullableMethod}()`) || zodOverrideType.includes(".optional()") ? zodOverrideType : `${zodOverrideType}.${nullableMethod}()` : zodOverrideType;
|
|
202
249
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
}
|
|
251
|
+
const overrideTypes = config.origin.overrideTypes;
|
|
252
|
+
if (overrideTypes && overrideTypes[Type]) {
|
|
253
|
+
const overrideType = overrideTypes[Type];
|
|
254
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
255
|
+
if (isZodDestination) {
|
|
256
|
+
const nullishOption = destination.nullish;
|
|
257
|
+
const nullableMethod = nullishOption ? "nullish" : "nullable";
|
|
258
|
+
return shouldBeNullable ? `${overrideType}.${nullableMethod}()` : overrideType;
|
|
259
|
+
} else {
|
|
260
|
+
return shouldBeNullable ? `${overrideType} | null` : overrideType;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const enumTypesForSchema = typeMappings.enumTypes[schemaType] || [];
|
|
264
|
+
const isEnum = enumTypesForSchema.includes(type);
|
|
265
|
+
if (isEnum) {
|
|
266
|
+
let enumValues = [];
|
|
267
|
+
if (schemaType === "mysql" && type === "enum") {
|
|
268
|
+
const match = Type.match(enumRegex);
|
|
269
|
+
if (match) {
|
|
270
|
+
enumValues = match[1].split(",").map((v) => v.trim().replace(/'/g, ""));
|
|
216
271
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}).join("
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
272
|
+
} else if (schemaType === "postgres" && EnumOptions) {
|
|
273
|
+
enumValues = EnumOptions;
|
|
274
|
+
}
|
|
275
|
+
if (enumValues.length > 0) {
|
|
276
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
277
|
+
if (isZodDestination) {
|
|
278
|
+
const enumString = `z.enum([${enumValues.map((v) => `'${v}'`).join(",")}])`;
|
|
279
|
+
const nullishOption = destination.nullish;
|
|
280
|
+
const nullableMethod = nullishOption ? "nullish" : "nullable";
|
|
281
|
+
return shouldBeNullable ? `${enumString}.${nullableMethod}()` : enumString;
|
|
282
|
+
} else if (isTsDestination) {
|
|
283
|
+
const enumType = destination.enumType;
|
|
284
|
+
if (enumType === "enum") {
|
|
285
|
+
const enumString = enumValues.map((v) => `'${v}'`).join(" | ");
|
|
286
|
+
return shouldBeNullable ? `${enumString} | null` : enumString;
|
|
287
|
+
} else {
|
|
288
|
+
const enumString = enumValues.map((v) => `'${v}'`).join(" | ");
|
|
289
|
+
return shouldBeNullable ? `${enumString} | null` : enumString;
|
|
232
290
|
}
|
|
233
|
-
|
|
291
|
+
} else if (isKyselyDestination) {
|
|
292
|
+
const enumString = enumValues.map((v) => `'${v}'`).join(" | ");
|
|
293
|
+
return shouldBeNullable ? `${enumString} | null` : enumString;
|
|
234
294
|
}
|
|
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
295
|
}
|
|
266
296
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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})`);
|
|
297
|
+
return generateStandardType(op, desc, config, destination, typeMappings);
|
|
298
|
+
}
|
|
299
|
+
function generateStandardType(op, desc, config, destination, typeMappings) {
|
|
300
|
+
const { Default, Extra, Null, Type } = desc;
|
|
301
|
+
const schemaType = config.origin.type;
|
|
302
|
+
const type = schemaType === "prisma" ? Type : Type.toLowerCase();
|
|
303
|
+
const isNull = Null === "YES";
|
|
304
|
+
const hasDefaultValue = Default !== null;
|
|
305
|
+
const isGenerated = Extra.toLowerCase().includes("auto_increment") || Extra.toLowerCase().includes("default_generated");
|
|
306
|
+
const isZodDestination = destination.type === "zod";
|
|
307
|
+
const isKyselyDestination = destination.type === "kysely";
|
|
308
|
+
const shouldBeNullable = isNull || ["insertable", "updateable"].includes(op) && (hasDefaultValue || isGenerated) || op === "updateable" && !isNull && !hasDefaultValue;
|
|
309
|
+
let baseType;
|
|
310
|
+
if (typeMappings.dateTypes.includes(type)) {
|
|
311
|
+
if (isZodDestination) {
|
|
312
|
+
const useDateType = destination.useDateType;
|
|
313
|
+
if (useDateType) {
|
|
314
|
+
baseType = "z.union([z.number(), z.string(), z.date()]).pipe(z.coerce.date())";
|
|
294
315
|
} else {
|
|
295
|
-
|
|
316
|
+
baseType = "z.date()";
|
|
296
317
|
}
|
|
318
|
+
} else {
|
|
319
|
+
baseType = "Date";
|
|
297
320
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
} else if (typeMappings.bigIntTypes.includes(type)) {
|
|
322
|
+
if (isZodDestination) {
|
|
323
|
+
baseType = "z.string()";
|
|
324
|
+
} else if (isKyselyDestination) {
|
|
325
|
+
baseType = "BigInt";
|
|
326
|
+
} else {
|
|
327
|
+
baseType = "string";
|
|
321
328
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
else if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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";
|
|
329
|
+
} else if (typeMappings.decimalTypes.includes(type)) {
|
|
330
|
+
if (isZodDestination) {
|
|
331
|
+
baseType = "z.string()";
|
|
332
|
+
} else if (isKyselyDestination) {
|
|
333
|
+
baseType = "Decimal";
|
|
334
|
+
} else {
|
|
335
|
+
baseType = "string";
|
|
341
336
|
}
|
|
342
|
-
|
|
337
|
+
} else if (typeMappings.numberTypes.includes(type)) {
|
|
338
|
+
if (isZodDestination) {
|
|
339
|
+
baseType = "z.number()";
|
|
340
|
+
if (!shouldBeNullable && !hasDefaultValue) {
|
|
341
|
+
baseType += ".nonnegative()";
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
baseType = "number";
|
|
345
|
+
}
|
|
346
|
+
} else if (typeMappings.booleanTypes.includes(type)) {
|
|
347
|
+
baseType = isZodDestination ? "z.boolean()" : "boolean";
|
|
348
|
+
} else if (typeMappings.stringTypes.includes(type)) {
|
|
349
|
+
if (isZodDestination) {
|
|
350
|
+
const useTrim = destination.useTrim;
|
|
351
|
+
const requiredString = destination.requiredString;
|
|
352
|
+
baseType = "z.string()";
|
|
353
|
+
if (useTrim) baseType += ".trim()";
|
|
354
|
+
if (requiredString && !shouldBeNullable) baseType += ".min(1)";
|
|
355
|
+
} else {
|
|
356
|
+
baseType = "string";
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
baseType = isZodDestination ? "z.string()" : "string";
|
|
343
360
|
}
|
|
344
|
-
if (
|
|
345
|
-
if (
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return
|
|
361
|
+
if (shouldBeNullable) {
|
|
362
|
+
if (isZodDestination) {
|
|
363
|
+
const nullishOption = destination.nullish;
|
|
364
|
+
const nullableMethod = nullishOption ? "nullish" : "nullable";
|
|
365
|
+
return `${baseType}.${nullableMethod}()`;
|
|
366
|
+
} else {
|
|
367
|
+
return `${baseType} | null`;
|
|
351
368
|
}
|
|
352
|
-
return generateStringLikeField();
|
|
353
369
|
}
|
|
354
|
-
|
|
355
|
-
if (schemaType !== "sqlite" && enumTypes[schemaType].includes(type))
|
|
356
|
-
return generateEnumLikeField();
|
|
357
|
-
throw new Error(`Unsupported column type: ${type}`);
|
|
370
|
+
return baseType;
|
|
358
371
|
}
|
|
359
|
-
|
|
360
|
-
|
|
372
|
+
|
|
373
|
+
function generateViewContent({
|
|
374
|
+
view,
|
|
361
375
|
describes,
|
|
362
376
|
config,
|
|
363
377
|
destination,
|
|
364
378
|
isCamelCase,
|
|
365
|
-
enumDeclarations:
|
|
366
|
-
defaultZodHeader
|
|
379
|
+
enumDeclarations: _enumDeclarations,
|
|
380
|
+
defaultZodHeader
|
|
367
381
|
}) {
|
|
368
382
|
let content = "";
|
|
369
|
-
const schemaType = config.origin.type;
|
|
370
383
|
if (destination.type === "kysely") {
|
|
371
|
-
|
|
384
|
+
const pascalView = camelCase(view, { pascalCase: true });
|
|
385
|
+
content += `// Kysely type definitions for ${view} (view)
|
|
386
|
+
|
|
387
|
+
`;
|
|
388
|
+
content += `// This interface defines the structure of the '${view}' view (read-only)
|
|
389
|
+
`;
|
|
390
|
+
content += `export interface ${pascalView}View {
|
|
372
391
|
`;
|
|
373
|
-
content += `
|
|
374
|
-
// This interface defines the structure of the '${table}' table
|
|
375
|
-
export interface ${camelCase(table, { pascalCase: true })} {`;
|
|
376
392
|
for (const desc of describes) {
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
}
|
|
393
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
394
|
+
const fieldType = getType("selectable", desc, config, destination);
|
|
395
|
+
content += ` ${fieldName}: ${fieldType};
|
|
396
|
+
`;
|
|
413
397
|
}
|
|
414
|
-
content
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
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 })}>;
|
|
398
|
+
content += "}\n\n";
|
|
399
|
+
content += `// Helper types for ${view} (view - read-only)
|
|
400
|
+
`;
|
|
401
|
+
content += `export type Selectable${pascalView}View = Selectable<${pascalView}View>;
|
|
421
402
|
`;
|
|
422
403
|
} else if (destination.type === "ts") {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
content
|
|
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
|
-
}
|
|
404
|
+
const pascalView = camelCase(view, { pascalCase: true });
|
|
405
|
+
content += `// TypeScript interface for ${view} (view - read-only)
|
|
406
|
+
`;
|
|
407
|
+
content += `export interface ${pascalView}View {
|
|
408
|
+
`;
|
|
445
409
|
for (const desc of describes) {
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
content = `${content}
|
|
450
|
-
${field}: ${type};`;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
content = `${content}
|
|
454
|
-
}
|
|
455
|
-
|
|
410
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
411
|
+
const fieldType = getType("selectable", desc, config, destination);
|
|
412
|
+
content += ` ${fieldName}: ${fieldType};
|
|
456
413
|
`;
|
|
457
|
-
if (isInterface) {
|
|
458
|
-
content += `export interface Insertable${camelCase(table, { pascalCase: true })} {`;
|
|
459
|
-
} else {
|
|
460
|
-
content += `export type Insertable${camelCase(table, { pascalCase: true })} = {`;
|
|
461
414
|
}
|
|
415
|
+
content += "}\n";
|
|
416
|
+
} else if (destination.type === "zod") {
|
|
417
|
+
const version = destination.version || 3;
|
|
418
|
+
const header = destination.header || defaultZodHeader(version);
|
|
419
|
+
if (!content.includes(header)) {
|
|
420
|
+
content += header;
|
|
421
|
+
}
|
|
422
|
+
content += `// View schema (read-only)
|
|
423
|
+
`;
|
|
424
|
+
content += `export const ${view}_view = z.object({
|
|
425
|
+
`;
|
|
462
426
|
for (const desc of describes) {
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
${field}: ${type};`;
|
|
468
|
-
}
|
|
427
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
428
|
+
const fieldType = getType("selectable", desc, config, destination);
|
|
429
|
+
content += ` ${fieldName}: ${fieldType},
|
|
430
|
+
`;
|
|
469
431
|
}
|
|
470
|
-
content
|
|
432
|
+
content += "})\n\n";
|
|
433
|
+
const pascalView = camelCase(view, { pascalCase: true });
|
|
434
|
+
content += `export type ${camelCase(`${pascalView}ViewType`, {
|
|
435
|
+
pascalCase: true
|
|
436
|
+
})} = z.infer<typeof ${view}_view>
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
return content;
|
|
471
440
|
}
|
|
441
|
+
function generateContent({
|
|
442
|
+
table,
|
|
443
|
+
describes,
|
|
444
|
+
config,
|
|
445
|
+
destination,
|
|
446
|
+
isCamelCase,
|
|
447
|
+
enumDeclarations: _enumDeclarations,
|
|
448
|
+
defaultZodHeader
|
|
449
|
+
}) {
|
|
450
|
+
let content = "";
|
|
451
|
+
if (destination.type === "ts") {
|
|
452
|
+
return generateTypeScriptContent({
|
|
453
|
+
table,
|
|
454
|
+
describes,
|
|
455
|
+
config,
|
|
456
|
+
destination,
|
|
457
|
+
isCamelCase
|
|
458
|
+
});
|
|
459
|
+
} else if (destination.type === "kysely") {
|
|
460
|
+
return generateKyselyContent({
|
|
461
|
+
table,
|
|
462
|
+
describes,
|
|
463
|
+
config,
|
|
464
|
+
destination,
|
|
465
|
+
isCamelCase
|
|
466
|
+
});
|
|
467
|
+
} else if (destination.type === "zod") {
|
|
468
|
+
return generateZodContent({
|
|
469
|
+
table,
|
|
470
|
+
describes,
|
|
471
|
+
config,
|
|
472
|
+
destination,
|
|
473
|
+
isCamelCase,
|
|
474
|
+
defaultZodHeader
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return content;
|
|
478
|
+
}
|
|
479
|
+
function generateTypeScriptContent({
|
|
480
|
+
table,
|
|
481
|
+
describes,
|
|
482
|
+
config,
|
|
483
|
+
destination,
|
|
484
|
+
isCamelCase
|
|
485
|
+
}) {
|
|
486
|
+
let content = "";
|
|
487
|
+
const pascalTable = camelCase(table, { pascalCase: true });
|
|
488
|
+
content += `// TypeScript interfaces for ${table}
|
|
472
489
|
|
|
473
490
|
`;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
491
|
+
content += `export interface ${pascalTable} {
|
|
492
|
+
`;
|
|
493
|
+
for (const desc of describes) {
|
|
494
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
495
|
+
const fieldType = getType("table", desc, config, destination);
|
|
496
|
+
content += ` ${fieldName}: ${fieldType};
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
content += "}\n\n";
|
|
500
|
+
content += `export interface Insertable${pascalTable} {
|
|
501
|
+
`;
|
|
502
|
+
for (const desc of describes) {
|
|
503
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
504
|
+
const fieldType = getType("insertable", desc, config, destination);
|
|
505
|
+
content += ` ${fieldName}: ${fieldType};
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
content += "}\n\n";
|
|
509
|
+
content += `export interface Updateable${pascalTable} {
|
|
510
|
+
`;
|
|
511
|
+
for (const desc of describes) {
|
|
512
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
513
|
+
const fieldType = getType("updateable", desc, config, destination);
|
|
514
|
+
content += ` ${fieldName}: ${fieldType};
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
content += "}\n\n";
|
|
518
|
+
content += `export interface Selectable${pascalTable} {
|
|
519
|
+
`;
|
|
520
|
+
for (const desc of describes) {
|
|
521
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
522
|
+
const fieldType = getType("selectable", desc, config, destination);
|
|
523
|
+
content += ` ${fieldName}: ${fieldType};
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
content += "}\n";
|
|
527
|
+
return content;
|
|
488
528
|
}
|
|
529
|
+
function generateKyselyContent({
|
|
530
|
+
table,
|
|
531
|
+
describes,
|
|
532
|
+
config,
|
|
533
|
+
destination,
|
|
534
|
+
isCamelCase
|
|
535
|
+
}) {
|
|
536
|
+
let content = "";
|
|
537
|
+
const pascalTable = camelCase(table, { pascalCase: true });
|
|
538
|
+
content += `// Kysely type definitions for ${table}
|
|
489
539
|
|
|
490
540
|
`;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
541
|
+
content += `// This interface defines the structure of the '${table}' table
|
|
542
|
+
`;
|
|
543
|
+
content += `export interface ${pascalTable}Table {
|
|
544
|
+
`;
|
|
545
|
+
for (const desc of describes) {
|
|
546
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
547
|
+
let fieldType = getType("table", desc, config, destination);
|
|
548
|
+
const isAutoIncrement = desc.Extra.toLowerCase().includes("auto_increment");
|
|
549
|
+
const isDefaultGenerated = desc.Extra.toLowerCase().includes("default_generated");
|
|
550
|
+
if (isAutoIncrement || isDefaultGenerated) {
|
|
551
|
+
fieldType = `Generated<${fieldType.replace(" | null", "")}>${fieldType.includes(" | null") ? " | null" : ""}`;
|
|
552
|
+
}
|
|
553
|
+
content += ` ${fieldName}: ${fieldType};
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
556
|
+
content += "}\n\n";
|
|
557
|
+
content += `// Use these types for inserting, selecting and updating the table
|
|
558
|
+
`;
|
|
559
|
+
content += `export type ${pascalTable} = Selectable<${pascalTable}Table>;
|
|
560
|
+
`;
|
|
561
|
+
content += `export type New${pascalTable} = Insertable<${pascalTable}Table>;
|
|
562
|
+
`;
|
|
563
|
+
content += `export type ${pascalTable}Update = Updateable<${pascalTable}Table>;
|
|
564
|
+
`;
|
|
565
|
+
return content;
|
|
505
566
|
}
|
|
567
|
+
function generateZodContent({
|
|
568
|
+
table,
|
|
569
|
+
describes,
|
|
570
|
+
config,
|
|
571
|
+
destination,
|
|
572
|
+
isCamelCase,
|
|
573
|
+
defaultZodHeader
|
|
574
|
+
}) {
|
|
575
|
+
let content = "";
|
|
576
|
+
const version = destination.version || 3;
|
|
577
|
+
const header = destination.header || defaultZodHeader(version);
|
|
578
|
+
if (!content.includes(header)) {
|
|
579
|
+
content += header;
|
|
580
|
+
}
|
|
581
|
+
content += `export const ${table} = z.object({
|
|
506
582
|
`;
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
content += `
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
content
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
${
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
583
|
+
for (const desc of describes) {
|
|
584
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
585
|
+
const fieldType = getType("table", desc, config, destination);
|
|
586
|
+
content += ` ${fieldName}: ${fieldType},
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
content += "})\n\n";
|
|
590
|
+
content += `export const insertable_${table} = z.object({
|
|
591
|
+
`;
|
|
592
|
+
for (const desc of describes) {
|
|
593
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
594
|
+
const fieldType = getType("insertable", desc, config, destination);
|
|
595
|
+
content += ` ${fieldName}: ${fieldType},
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
content += "})\n\n";
|
|
599
|
+
content += `export const updateable_${table} = z.object({
|
|
600
|
+
`;
|
|
601
|
+
for (const desc of describes) {
|
|
602
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
603
|
+
const fieldType = getType("updateable", desc, config, destination);
|
|
604
|
+
content += ` ${fieldName}: ${fieldType},
|
|
605
|
+
`;
|
|
606
|
+
}
|
|
607
|
+
content += "})\n\n";
|
|
608
|
+
content += `export const selectable_${table} = z.object({
|
|
609
|
+
`;
|
|
610
|
+
for (const desc of describes) {
|
|
611
|
+
const fieldName = isCamelCase ? camelCase(desc.Field) : desc.Field;
|
|
612
|
+
const fieldType = getType("selectable", desc, config, destination);
|
|
613
|
+
content += ` ${fieldName}: ${fieldType},
|
|
614
|
+
`;
|
|
615
|
+
}
|
|
616
|
+
content += "})\n\n";
|
|
617
|
+
content += `export type ${camelCase(`${table}Type`, { pascalCase: true })} = z.infer<typeof ${table}>
|
|
618
|
+
`;
|
|
619
|
+
content += `export type Insertable${camelCase(`${table}Type`, { pascalCase: true })} = z.infer<typeof insertable_${table}>
|
|
620
|
+
`;
|
|
621
|
+
content += `export type Updateable${camelCase(`${table}Type`, { pascalCase: true })} = z.infer<typeof updateable_${table}>
|
|
622
|
+
`;
|
|
623
|
+
content += `export type Selectable${camelCase(`${table}Type`, { pascalCase: true })} = z.infer<typeof selectable_${table}>
|
|
624
|
+
`;
|
|
625
|
+
return content;
|
|
626
|
+
}
|
|
533
627
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
628
|
+
function createDatabaseConnection(config) {
|
|
629
|
+
const { origin } = config;
|
|
630
|
+
switch (origin.type) {
|
|
631
|
+
case "mysql":
|
|
632
|
+
return knex({
|
|
633
|
+
client: "mysql2",
|
|
634
|
+
connection: {
|
|
635
|
+
host: origin.host,
|
|
636
|
+
port: origin.port,
|
|
637
|
+
user: origin.user,
|
|
638
|
+
password: origin.password,
|
|
639
|
+
database: origin.database,
|
|
640
|
+
ssl: origin.ssl
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
case "postgres":
|
|
644
|
+
return knex({
|
|
645
|
+
client: "pg",
|
|
646
|
+
connection: {
|
|
647
|
+
host: origin.host,
|
|
648
|
+
port: origin.port,
|
|
649
|
+
user: origin.user,
|
|
650
|
+
password: origin.password,
|
|
651
|
+
database: origin.database,
|
|
652
|
+
ssl: origin.ssl
|
|
653
|
+
},
|
|
654
|
+
searchPath: origin.schema ? [origin.schema] : ["public"]
|
|
655
|
+
});
|
|
656
|
+
case "sqlite":
|
|
657
|
+
return knex({
|
|
658
|
+
client: "sqlite3",
|
|
659
|
+
connection: {
|
|
660
|
+
filename: origin.path
|
|
661
|
+
},
|
|
662
|
+
useNullAsDefault: true
|
|
663
|
+
});
|
|
664
|
+
default:
|
|
665
|
+
throw new Error(`Unsupported database type: ${origin.type}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function extractTables(db, config) {
|
|
669
|
+
const { origin } = config;
|
|
670
|
+
switch (origin.type) {
|
|
671
|
+
case "mysql":
|
|
672
|
+
const mysqlTables = await db.raw(`
|
|
673
|
+
SELECT table_name
|
|
674
|
+
FROM information_schema.tables
|
|
675
|
+
WHERE table_schema = ? AND table_type = 'BASE TABLE'
|
|
676
|
+
`, [origin.database]);
|
|
677
|
+
return mysqlTables[0].map((row) => row.table_name);
|
|
678
|
+
case "postgres":
|
|
679
|
+
const schema = origin.schema || "public";
|
|
680
|
+
const postgresTables = await db.raw(`
|
|
681
|
+
SELECT table_name
|
|
682
|
+
FROM information_schema.tables
|
|
683
|
+
WHERE table_schema = ? AND table_type = 'BASE TABLE'
|
|
684
|
+
`, [schema]);
|
|
685
|
+
return postgresTables.rows.map((row) => row.table_name);
|
|
686
|
+
case "sqlite":
|
|
687
|
+
const sqliteTables = await db.raw(`
|
|
688
|
+
SELECT name
|
|
689
|
+
FROM sqlite_master
|
|
690
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
691
|
+
`);
|
|
692
|
+
return sqliteTables.map((row) => row.name);
|
|
693
|
+
default:
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function extractViews(db, config) {
|
|
698
|
+
const { origin } = config;
|
|
699
|
+
switch (origin.type) {
|
|
700
|
+
case "mysql":
|
|
701
|
+
const mysqlViews = await db.raw(`
|
|
702
|
+
SELECT table_name
|
|
703
|
+
FROM information_schema.tables
|
|
704
|
+
WHERE table_schema = ? AND table_type = 'VIEW'
|
|
705
|
+
`, [origin.database]);
|
|
706
|
+
return mysqlViews[0].map((row) => row.table_name);
|
|
707
|
+
case "postgres":
|
|
708
|
+
const schema = origin.schema || "public";
|
|
709
|
+
const postgresViews = await db.raw(`
|
|
710
|
+
SELECT table_name
|
|
711
|
+
FROM information_schema.tables
|
|
712
|
+
WHERE table_schema = ? AND table_type = 'VIEW'
|
|
713
|
+
`, [schema]);
|
|
714
|
+
return postgresViews.rows.map((row) => row.table_name);
|
|
715
|
+
case "sqlite":
|
|
716
|
+
const sqliteViews = await db.raw(`
|
|
717
|
+
SELECT name
|
|
718
|
+
FROM sqlite_master
|
|
719
|
+
WHERE type = 'view'
|
|
720
|
+
`);
|
|
721
|
+
return sqliteViews.map((row) => row.name);
|
|
722
|
+
default:
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function extractColumnDescriptions(db, config, tableName) {
|
|
727
|
+
const { origin } = config;
|
|
728
|
+
switch (origin.type) {
|
|
729
|
+
case "mysql":
|
|
730
|
+
const mysqlColumns = await db.raw(`
|
|
731
|
+
SELECT
|
|
732
|
+
column_name as \`Field\`,
|
|
733
|
+
column_default as \`Default\`,
|
|
734
|
+
extra as \`Extra\`,
|
|
735
|
+
is_nullable as \`Null\`,
|
|
736
|
+
column_type as \`Type\`,
|
|
737
|
+
column_comment as \`Comment\`
|
|
738
|
+
FROM information_schema.columns
|
|
739
|
+
WHERE table_schema = ? AND table_name = ?
|
|
740
|
+
ORDER BY ordinal_position
|
|
741
|
+
`, [origin.database, tableName]);
|
|
742
|
+
return mysqlColumns[0].map((row) => ({
|
|
743
|
+
Field: row.Field,
|
|
744
|
+
Default: row.Default,
|
|
745
|
+
Extra: row.Extra || "",
|
|
746
|
+
Null: row.Null,
|
|
747
|
+
Type: row.Type,
|
|
748
|
+
Comment: row.Comment || ""
|
|
749
|
+
}));
|
|
750
|
+
case "postgres":
|
|
751
|
+
const schema = origin.schema || "public";
|
|
752
|
+
const postgresColumns = await db.raw(`
|
|
753
|
+
SELECT
|
|
754
|
+
column_name as "Field",
|
|
755
|
+
column_default as "Default",
|
|
756
|
+
'' as "Extra",
|
|
757
|
+
is_nullable as "Null",
|
|
758
|
+
data_type as "Type",
|
|
759
|
+
'' as "Comment"
|
|
760
|
+
FROM information_schema.columns
|
|
761
|
+
WHERE table_schema = ? AND table_name = ?
|
|
762
|
+
ORDER BY ordinal_position
|
|
763
|
+
`, [schema, tableName]);
|
|
764
|
+
return postgresColumns.rows.map((row) => ({
|
|
765
|
+
Field: row.Field,
|
|
766
|
+
Default: row.Default,
|
|
767
|
+
Extra: row.Extra || "",
|
|
768
|
+
Null: row.Null,
|
|
769
|
+
Type: row.Type,
|
|
770
|
+
Comment: row.Comment || ""
|
|
771
|
+
}));
|
|
772
|
+
case "sqlite":
|
|
773
|
+
const sqliteColumns = await db.raw(`PRAGMA table_info(${tableName})`);
|
|
774
|
+
return sqliteColumns.map((row) => ({
|
|
775
|
+
Field: row.name,
|
|
776
|
+
Default: row.dflt_value,
|
|
777
|
+
Extra: row.pk ? "PRIMARY KEY" : "",
|
|
778
|
+
Null: row.notnull ? "NO" : "YES",
|
|
779
|
+
Type: row.type,
|
|
780
|
+
Comment: ""
|
|
781
|
+
}));
|
|
782
|
+
default:
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
545
786
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
787
|
+
function extractPrismaEntities(config) {
|
|
788
|
+
if (config.origin.type !== "prisma") {
|
|
789
|
+
return { tables: [], views: [], enumDeclarations: {} };
|
|
790
|
+
}
|
|
791
|
+
const schemaContent = readFileSync(config.origin.path, "utf-8");
|
|
792
|
+
const schema = createPrismaSchemaBuilder(schemaContent);
|
|
793
|
+
const prismaModels = schema.findAllByType("model", {});
|
|
794
|
+
const tables = prismaModels.filter((m) => m !== null).map((model) => model.name);
|
|
795
|
+
const prismaViews = schema.findAllByType("view", {});
|
|
796
|
+
const views = prismaViews.filter((v) => v !== null).map((view) => view.name);
|
|
797
|
+
const enumDeclarations = {};
|
|
798
|
+
const prismaEnums = schema.findAllByType("enum", {});
|
|
799
|
+
for (const prismaEnum of prismaEnums) {
|
|
800
|
+
if (prismaEnum && "name" in prismaEnum && "enumerators" in prismaEnum) {
|
|
801
|
+
const enumName = prismaEnum.name;
|
|
802
|
+
const enumerators = prismaEnum.enumerators;
|
|
803
|
+
enumDeclarations[enumName] = enumerators.map((e) => e.name);
|
|
554
804
|
}
|
|
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
805
|
}
|
|
572
|
-
return
|
|
806
|
+
return { tables, views, enumDeclarations };
|
|
573
807
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
let tables = [];
|
|
578
|
-
let prismaTables = [];
|
|
579
|
-
let schema = null;
|
|
580
|
-
let db = null;
|
|
581
|
-
const kyselyTableContents = {};
|
|
582
|
-
if (config.destinations.length === 0) {
|
|
583
|
-
throw new Error("Empty destinations object.");
|
|
808
|
+
function extractPrismaColumnDescriptions(config, entityName, enumDeclarations) {
|
|
809
|
+
if (config.origin.type !== "prisma") {
|
|
810
|
+
return [];
|
|
584
811
|
}
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
});
|
|
812
|
+
const schemaContent = readFileSync(config.origin.path, "utf-8");
|
|
813
|
+
const schema = createPrismaSchemaBuilder(schemaContent);
|
|
814
|
+
let entity = schema.findByType("model", { name: entityName });
|
|
815
|
+
if (!entity) {
|
|
816
|
+
entity = schema.findByType("view", { name: entityName });
|
|
618
817
|
}
|
|
619
|
-
|
|
620
|
-
|
|
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();
|
|
818
|
+
if (!entity || !("properties" in entity)) {
|
|
819
|
+
return [];
|
|
643
820
|
}
|
|
644
|
-
const
|
|
645
|
-
|
|
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)
|
|
821
|
+
const fields = entity.properties.filter(
|
|
822
|
+
(p) => p.type === "field" && p.array !== true && !p.attributes?.find((a) => a.name === "relation")
|
|
654
823
|
);
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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";
|
|
824
|
+
return fields.map((field) => {
|
|
825
|
+
let defaultGenerated = false;
|
|
826
|
+
let defaultValue = null;
|
|
827
|
+
if (field.attributes) {
|
|
828
|
+
for (const attr of field.attributes) {
|
|
829
|
+
if (attr.name === "default") {
|
|
830
|
+
if (attr.args && attr.args.length > 0) {
|
|
831
|
+
const arg = attr.args[0];
|
|
832
|
+
if (typeof arg === "object" && "value" in arg) {
|
|
833
|
+
if (arg.value === "autoincrement()" || arg.value === "cuid()" || arg.value === "uuid()") {
|
|
834
|
+
defaultGenerated = true;
|
|
835
|
+
} else {
|
|
836
|
+
defaultValue = String(arg.value);
|
|
837
|
+
}
|
|
745
838
|
}
|
|
746
839
|
}
|
|
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
|
-
);
|
|
757
|
-
} 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
840
|
}
|
|
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");
|
|
797
|
-
}
|
|
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;
|
|
820
|
-
} 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
841
|
}
|
|
826
842
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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}
|
|
843
|
+
const isOptional = field.optional === true;
|
|
844
|
+
let enumOptions;
|
|
845
|
+
const fieldType = String(field.fieldType);
|
|
846
|
+
if (enumDeclarations[fieldType]) {
|
|
847
|
+
enumOptions = enumDeclarations[fieldType];
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
Field: field.name,
|
|
851
|
+
Default: defaultValue,
|
|
852
|
+
Extra: defaultGenerated ? "auto_increment" : "",
|
|
853
|
+
Null: isOptional ? "YES" : "NO",
|
|
854
|
+
Type: fieldType,
|
|
855
|
+
Comment: "",
|
|
856
|
+
// Prisma doesn't have column comments in the same way
|
|
857
|
+
EnumOptions: enumOptions
|
|
858
|
+
};
|
|
859
|
+
});
|
|
860
|
+
}
|
|
860
861
|
|
|
861
|
-
|
|
862
|
+
const defaultKyselyHeader = "import { Generated, Insertable, Selectable, Updateable, ColumnType } from 'kysely';\n\n";
|
|
863
|
+
const defaultZodHeader = (version) => "import { z } from 'zod" + (version === 3 ? "" : "/v4") + "';\n\n";
|
|
864
|
+
const kyselyJsonTypes = `// JSON type definitions
|
|
862
865
|
export type Json = ColumnType<JsonValue, string, string>;
|
|
863
866
|
|
|
864
867
|
export type JsonArray = JsonValue[];
|
|
@@ -878,65 +881,140 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
|
|
878
881
|
export type Decimal = ColumnType<string, number | string, number | string>
|
|
879
882
|
|
|
880
883
|
export type BigInt = ColumnType<string, number | string, number | string>
|
|
884
|
+
|
|
881
885
|
`;
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
+
|
|
887
|
+
async function generate(config) {
|
|
888
|
+
let tables = [];
|
|
889
|
+
let views = [];
|
|
890
|
+
let enumDeclarations = {};
|
|
891
|
+
let db = null;
|
|
892
|
+
try {
|
|
893
|
+
if (config.origin.type === "prisma") {
|
|
894
|
+
const prismaEntities = extractPrismaEntities(config);
|
|
895
|
+
tables = prismaEntities.tables;
|
|
896
|
+
views = prismaEntities.views;
|
|
897
|
+
enumDeclarations = prismaEntities.enumDeclarations;
|
|
898
|
+
} else {
|
|
899
|
+
db = createDatabaseConnection(config);
|
|
900
|
+
tables = await extractTables(db, config);
|
|
901
|
+
views = await extractViews(db, config);
|
|
902
|
+
}
|
|
903
|
+
tables = filterTables(tables, config.tables, config.ignore);
|
|
904
|
+
if (!config.includeViews) {
|
|
905
|
+
views = [];
|
|
906
|
+
} else {
|
|
907
|
+
views = filterViews(views, config.views, config.ignoreViews);
|
|
908
|
+
}
|
|
909
|
+
const allEntities = createEntityList(tables, views);
|
|
910
|
+
const results = {};
|
|
911
|
+
const isCamelCase = config.camelCase === true;
|
|
912
|
+
const nonKyselyDestinations = config.destinations.filter((d) => d.type !== "kysely");
|
|
913
|
+
for (const entity of allEntities) {
|
|
914
|
+
const { name: entityName, type: entityType } = entity;
|
|
915
|
+
let describes;
|
|
916
|
+
if (config.origin.type === "prisma") {
|
|
917
|
+
describes = extractPrismaColumnDescriptions(config, entityName, enumDeclarations);
|
|
918
|
+
} else {
|
|
919
|
+
describes = await extractColumnDescriptions(db, config, entityName);
|
|
920
|
+
}
|
|
921
|
+
if (describes.length === 0) continue;
|
|
922
|
+
for (const destination of nonKyselyDestinations) {
|
|
923
|
+
const content = entityType === "view" ? generateViewContent({
|
|
924
|
+
view: entityName,
|
|
925
|
+
describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
|
|
926
|
+
config,
|
|
927
|
+
destination,
|
|
928
|
+
isCamelCase,
|
|
929
|
+
enumDeclarations,
|
|
930
|
+
defaultZodHeader
|
|
931
|
+
}) : generateContent({
|
|
932
|
+
table: entityName,
|
|
933
|
+
describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
|
|
934
|
+
config,
|
|
935
|
+
destination,
|
|
936
|
+
isCamelCase,
|
|
937
|
+
enumDeclarations,
|
|
938
|
+
defaultZodHeader
|
|
939
|
+
});
|
|
940
|
+
const suffix = destination.suffix || destination.type;
|
|
941
|
+
const folder = destination.folder || ".";
|
|
942
|
+
const fileName = `${entityName}.${suffix}.ts`;
|
|
943
|
+
const filePath = path.join(folder, fileName);
|
|
944
|
+
results[filePath] = (destination.header || "") + content;
|
|
945
|
+
}
|
|
886
946
|
}
|
|
887
|
-
|
|
947
|
+
const kyselyDestinations = config.destinations.filter((d) => d.type === "kysely");
|
|
948
|
+
for (const kyselyDestination of kyselyDestinations) {
|
|
949
|
+
const header = kyselyDestination.header || defaultKyselyHeader;
|
|
950
|
+
const schemaName = kyselyDestination.schemaName || "DB";
|
|
951
|
+
let consolidatedContent = `${header}
|
|
952
|
+
${kyselyJsonTypes}`;
|
|
953
|
+
const tableContents = [];
|
|
954
|
+
for (const entity of allEntities) {
|
|
955
|
+
const { name: entityName, type: entityType } = entity;
|
|
956
|
+
let describes;
|
|
957
|
+
if (config.origin.type === "prisma") {
|
|
958
|
+
describes = extractPrismaColumnDescriptions(config, entityName, enumDeclarations);
|
|
959
|
+
} else {
|
|
960
|
+
describes = await extractColumnDescriptions(db, config, entityName);
|
|
961
|
+
}
|
|
962
|
+
if (describes.length === 0) continue;
|
|
963
|
+
const content = entityType === "view" ? generateViewContent({
|
|
964
|
+
view: entityName,
|
|
965
|
+
describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
|
|
966
|
+
config,
|
|
967
|
+
destination: kyselyDestination,
|
|
968
|
+
isCamelCase,
|
|
969
|
+
enumDeclarations,
|
|
970
|
+
defaultZodHeader
|
|
971
|
+
}) : generateContent({
|
|
972
|
+
table: entityName,
|
|
973
|
+
describes: describes.sort((a, b) => a.Field.localeCompare(b.Field)),
|
|
974
|
+
config,
|
|
975
|
+
destination: kyselyDestination,
|
|
976
|
+
isCamelCase,
|
|
977
|
+
enumDeclarations,
|
|
978
|
+
defaultZodHeader
|
|
979
|
+
});
|
|
980
|
+
tableContents.push({ table: entityName, content });
|
|
981
|
+
consolidatedContent += content + "\n";
|
|
982
|
+
}
|
|
983
|
+
consolidatedContent += `
|
|
888
984
|
// Database Interface
|
|
889
985
|
export interface ${schemaName} {
|
|
890
986
|
`;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
987
|
+
const sortedTableEntries = tableContents.map(({ table, content }) => {
|
|
988
|
+
const isView = content.includes("(view");
|
|
989
|
+
const pascalTable = camelCase(table, { pascalCase: true }) + (isView ? "View" : "");
|
|
990
|
+
const tableKey = isCamelCase ? camelCase(table) : table;
|
|
991
|
+
return { tableKey, pascalTable, isView };
|
|
992
|
+
}).sort((a, b) => a.tableKey.localeCompare(b.tableKey));
|
|
993
|
+
for (const { tableKey, pascalTable } of sortedTableEntries) {
|
|
994
|
+
consolidatedContent += ` ${tableKey}: ${pascalTable};
|
|
898
995
|
`;
|
|
996
|
+
}
|
|
997
|
+
consolidatedContent += "}\n";
|
|
998
|
+
const outputFile = kyselyDestination.outFile || path.join(kyselyDestination.folder || ".", "db.ts");
|
|
999
|
+
results[outputFile] = consolidatedContent;
|
|
899
1000
|
}
|
|
900
|
-
consolidatedContent += "}\n";
|
|
901
1001
|
if (config.dryRun) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1002
|
+
return results;
|
|
1003
|
+
}
|
|
1004
|
+
for (const [filePath, content] of Object.entries(results)) {
|
|
1005
|
+
const fullPath = path.resolve(filePath);
|
|
1006
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
1007
|
+
await fs.writeFile(fullPath, content);
|
|
1008
|
+
if (!config.silent) {
|
|
1009
|
+
console.log(`Created: ${filePath}`);
|
|
908
1010
|
}
|
|
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
1011
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const absolutePath = path.resolve(dest);
|
|
920
|
-
const content = fs.readFileSync(dest, "utf8");
|
|
921
|
-
result2[absolutePath] = content;
|
|
1012
|
+
return results;
|
|
1013
|
+
} finally {
|
|
1014
|
+
if (db) {
|
|
1015
|
+
await db.destroy();
|
|
922
1016
|
}
|
|
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
1017
|
}
|
|
930
|
-
return result;
|
|
931
1018
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
defaultZodHeader,
|
|
935
|
-
extractKyselyExpression,
|
|
936
|
-
extractTSExpression,
|
|
937
|
-
extractTypeExpression,
|
|
938
|
-
extractZodExpression,
|
|
939
|
-
generate,
|
|
940
|
-
generateContent,
|
|
941
|
-
getType
|
|
942
|
-
};
|
|
1019
|
+
|
|
1020
|
+
export { defaultKyselyHeader, defaultZodHeader, extractKyselyExpression, extractTSExpression, extractTypeExpression, extractZodExpression, generate, generateContent, generateViewContent, getType };
|