hekireki 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -14
- package/dist/generator/ajv/index.d.ts +6 -0
- package/dist/generator/ajv/index.js +87 -0
- package/dist/generator/arktype/index.js +17 -3
- package/dist/generator/dbml/index.js +43 -33
- package/dist/generator/drizzle/index.js +15 -24
- package/dist/generator/ecto/index.js +34 -9
- package/dist/generator/effect/index.js +17 -3
- package/dist/generator/gorm/index.d.ts +6 -0
- package/dist/generator/gorm/index.js +370 -0
- package/dist/generator/mermaid-er/index.js +6 -12
- package/dist/generator/sea-orm/index.d.ts +6 -0
- package/dist/generator/sea-orm/index.js +444 -0
- package/dist/generator/sqlalchemy/index.d.ts +6 -0
- package/dist/generator/sqlalchemy/index.js +458 -0
- package/dist/generator/typebox/index.d.ts +6 -0
- package/dist/generator/typebox/index.js +93 -0
- package/dist/generator/valibot/index.js +15 -5
- package/dist/generator/zod/index.js +15 -5
- package/dist/{prisma-Cc0YxSiO.js → prisma-ChsFqlYX.js} +6 -6
- package/dist/utils-COHZyQue.js +116 -0
- package/package.json +18 -7
- package/dist/utils-DeZn2r_T.js +0 -287
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { d as mkdir, f as writeFile, o as makeSnakeCase } from "../../utils-COHZyQue.js";
|
|
3
|
+
import path, { dirname } from "node:path";
|
|
4
|
+
import pkg from "@prisma/generator-helper";
|
|
5
|
+
|
|
6
|
+
//#region src/helper/sqlalchemy.ts
|
|
7
|
+
const PRISMA_TO_PYTHON = {
|
|
8
|
+
String: "str",
|
|
9
|
+
Int: "int",
|
|
10
|
+
BigInt: "int",
|
|
11
|
+
Float: "float",
|
|
12
|
+
Decimal: "Decimal",
|
|
13
|
+
Boolean: "bool",
|
|
14
|
+
DateTime: "datetime",
|
|
15
|
+
Json: "dict",
|
|
16
|
+
Bytes: "bytes"
|
|
17
|
+
};
|
|
18
|
+
const PRISMA_TO_SQLALCHEMY = {
|
|
19
|
+
String: "String",
|
|
20
|
+
Int: "Integer",
|
|
21
|
+
BigInt: "BigInteger",
|
|
22
|
+
Float: "Float",
|
|
23
|
+
Decimal: "Numeric",
|
|
24
|
+
Boolean: "Boolean",
|
|
25
|
+
DateTime: "DateTime",
|
|
26
|
+
Json: "JSON",
|
|
27
|
+
Bytes: "LargeBinary"
|
|
28
|
+
};
|
|
29
|
+
function prismaTypeToSQLAlchemyType(type) {
|
|
30
|
+
return PRISMA_TO_SQLALCHEMY[type] ?? "String";
|
|
31
|
+
}
|
|
32
|
+
function prismaTypeToPythonType(type) {
|
|
33
|
+
return PRISMA_TO_PYTHON[type] ?? "str";
|
|
34
|
+
}
|
|
35
|
+
function resolveNativeType(field) {
|
|
36
|
+
const baseType = prismaTypeToSQLAlchemyType(field.type);
|
|
37
|
+
if (!field.nativeType) return baseType;
|
|
38
|
+
const [nativeName, nativeArgs] = field.nativeType;
|
|
39
|
+
const args = nativeArgs ?? [];
|
|
40
|
+
switch (nativeName) {
|
|
41
|
+
case "VarChar":
|
|
42
|
+
case "Char": return args.length > 0 ? `String(${args[0]})` : "String";
|
|
43
|
+
case "Text":
|
|
44
|
+
case "MediumText":
|
|
45
|
+
case "LongText":
|
|
46
|
+
case "TinyText": return "Text";
|
|
47
|
+
case "SmallInt":
|
|
48
|
+
case "TinyInt": return "SmallInteger";
|
|
49
|
+
case "MediumInt": return "Integer";
|
|
50
|
+
case "DoublePrecision":
|
|
51
|
+
case "Double":
|
|
52
|
+
case "Real": return "Double";
|
|
53
|
+
case "Decimal":
|
|
54
|
+
case "Money": return args.length >= 2 ? `Numeric(precision=${args[0]}, scale=${args[1]})` : "Numeric";
|
|
55
|
+
case "Uuid": return "Uuid";
|
|
56
|
+
case "Timestamp": return "DateTime";
|
|
57
|
+
case "Date": return "Date";
|
|
58
|
+
case "Time": return "Time";
|
|
59
|
+
case "JsonB": return "JSON";
|
|
60
|
+
case "Xml": return "String";
|
|
61
|
+
default: return baseType;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function needsExplicitSaType(field) {
|
|
65
|
+
if (field.kind === "enum") return true;
|
|
66
|
+
if (field.nativeType) {
|
|
67
|
+
if (resolveNativeType(field) !== prismaTypeToSQLAlchemyType(field.type)) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
function pythonTypeForNative(field) {
|
|
72
|
+
if (!field.nativeType) {
|
|
73
|
+
const raw = prismaTypeToPythonType(field.type);
|
|
74
|
+
return raw === "Decimal" ? "DecimalType" : raw;
|
|
75
|
+
}
|
|
76
|
+
const [nativeName] = field.nativeType;
|
|
77
|
+
if (nativeName === "Uuid") return "uuid_mod.UUID";
|
|
78
|
+
if (nativeName === "Date") return "date";
|
|
79
|
+
if (nativeName === "Time") return "time_type";
|
|
80
|
+
const raw = prismaTypeToPythonType(field.type);
|
|
81
|
+
return raw === "Decimal" ? "DecimalType" : raw;
|
|
82
|
+
}
|
|
83
|
+
function getAssociations(model, allModels) {
|
|
84
|
+
const belongsTo = [];
|
|
85
|
+
const hasMany = [];
|
|
86
|
+
const hasOne = [];
|
|
87
|
+
const manyToMany = [];
|
|
88
|
+
for (const field of model.fields) {
|
|
89
|
+
if (field.kind !== "object") continue;
|
|
90
|
+
if (field.relationFromFields && field.relationFromFields.length > 0) belongsTo.push({
|
|
91
|
+
name: field.name,
|
|
92
|
+
targetModel: field.type,
|
|
93
|
+
foreignKey: field.relationFromFields[0],
|
|
94
|
+
references: field.relationToFields?.[0] ?? "id"
|
|
95
|
+
});
|
|
96
|
+
else if (field.isList) {
|
|
97
|
+
const targetModel = allModels.find((m) => m.name === field.type);
|
|
98
|
+
if (!targetModel) continue;
|
|
99
|
+
if (targetModel.fields.find((f) => f.relationName === field.relationName && f.kind === "object")?.isList) manyToMany.push({
|
|
100
|
+
name: field.name,
|
|
101
|
+
targetModel: field.type,
|
|
102
|
+
relationName: field.relationName ?? `${model.name}To${field.type}`
|
|
103
|
+
});
|
|
104
|
+
else {
|
|
105
|
+
const foreignKey = targetModel.fields.find((f) => f.relationName === field.relationName && f.relationFromFields && f.relationFromFields.length > 0)?.relationFromFields?.[0];
|
|
106
|
+
if (!foreignKey) continue;
|
|
107
|
+
hasMany.push({
|
|
108
|
+
name: field.name,
|
|
109
|
+
targetModel: field.type,
|
|
110
|
+
foreignKey,
|
|
111
|
+
isList: true
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
const targetModel = allModels.find((m) => m.name === field.type);
|
|
116
|
+
if (!targetModel) continue;
|
|
117
|
+
const foreignKey = targetModel.fields.find((f) => f.relationName === field.relationName && f.relationFromFields && f.relationFromFields.length > 0)?.relationFromFields?.[0];
|
|
118
|
+
if (!foreignKey) continue;
|
|
119
|
+
hasOne.push({
|
|
120
|
+
name: field.name,
|
|
121
|
+
targetModel: field.type,
|
|
122
|
+
foreignKey,
|
|
123
|
+
isList: false
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
belongsTo,
|
|
129
|
+
hasMany,
|
|
130
|
+
hasOne,
|
|
131
|
+
manyToMany
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function collectManyToManyTables(allModels) {
|
|
135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
136
|
+
const tables = [];
|
|
137
|
+
for (const model of allModels) for (const field of model.fields) {
|
|
138
|
+
if (field.kind !== "object" || !field.isList) continue;
|
|
139
|
+
const targetModel = allModels.find((m) => m.name === field.type);
|
|
140
|
+
if (!targetModel) continue;
|
|
141
|
+
if (!targetModel.fields.find((f) => f.relationName === field.relationName && f.kind === "object")?.isList) continue;
|
|
142
|
+
const relationName = field.relationName ?? `${model.name}To${field.type}`;
|
|
143
|
+
if (seen.has(relationName)) continue;
|
|
144
|
+
seen.add(relationName);
|
|
145
|
+
const [leftName, rightName] = model.name < field.type ? [model.name, field.type] : [field.type, model.name];
|
|
146
|
+
const leftModelObj = allModels.find((m) => m.name === leftName);
|
|
147
|
+
const rightModelObj = allModels.find((m) => m.name === rightName);
|
|
148
|
+
tables.push({
|
|
149
|
+
tableName: `_${leftName}To${rightName}`,
|
|
150
|
+
varName: `${makeSnakeCase(leftName)}_to_${makeSnakeCase(rightName)}`,
|
|
151
|
+
leftModel: leftName,
|
|
152
|
+
leftTable: leftModelObj?.dbName ?? makeSnakeCase(leftName),
|
|
153
|
+
leftPkField: leftModelObj?.fields.find((f) => f.isId),
|
|
154
|
+
rightModel: rightName,
|
|
155
|
+
rightTable: rightModelObj?.dbName ?? makeSnakeCase(rightName),
|
|
156
|
+
rightPkField: rightModelObj?.fields.find((f) => f.isId)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return tables;
|
|
160
|
+
}
|
|
161
|
+
function generateAssociationTable(info) {
|
|
162
|
+
const leftSaType = info.leftPkField ? resolveNativeType(info.leftPkField) : "String";
|
|
163
|
+
const rightSaType = info.rightPkField ? resolveNativeType(info.rightPkField) : "String";
|
|
164
|
+
const leftPkCol = info.leftPkField?.dbName ?? (info.leftPkField ? makeSnakeCase(info.leftPkField.name) : "id");
|
|
165
|
+
const rightPkCol = info.rightPkField?.dbName ?? (info.rightPkField ? makeSnakeCase(info.rightPkField.name) : "id");
|
|
166
|
+
return [
|
|
167
|
+
`${info.varName} = Table(`,
|
|
168
|
+
` "${info.tableName}",`,
|
|
169
|
+
" Base.metadata,",
|
|
170
|
+
` Column("A", ${leftSaType}, ForeignKey("${info.leftTable}.${leftPkCol}"), primary_key=True),`,
|
|
171
|
+
` Column("B", ${rightSaType}, ForeignKey("${info.rightTable}.${rightPkCol}"), primary_key=True),`,
|
|
172
|
+
")"
|
|
173
|
+
].join("\n");
|
|
174
|
+
}
|
|
175
|
+
function formatDefault(def) {
|
|
176
|
+
if (def === void 0 || def === null) return null;
|
|
177
|
+
if (typeof def === "boolean") return def ? "True" : "False";
|
|
178
|
+
if (typeof def === "number") return String(def);
|
|
179
|
+
if (typeof def === "string") return `"${def}"`;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function isFunctionDefault(def) {
|
|
183
|
+
return def !== null && typeof def === "object" && "name" in def;
|
|
184
|
+
}
|
|
185
|
+
function isAutoincrement(field) {
|
|
186
|
+
return isFunctionDefault(field.default) && field.default.name === "autoincrement";
|
|
187
|
+
}
|
|
188
|
+
function needsForeignKeysParam(targetModel, assocs) {
|
|
189
|
+
return assocs.filter((a) => a.targetModel === targetModel).length > 1;
|
|
190
|
+
}
|
|
191
|
+
function findBackPopulates(targetModelName, sourceModelName, foreignKey, allModels) {
|
|
192
|
+
const targetModel = allModels.find((m) => m.name === targetModelName);
|
|
193
|
+
if (!targetModel) return makeSnakeCase(sourceModelName);
|
|
194
|
+
const backField = targetModel.fields.find((f) => {
|
|
195
|
+
if (f.kind !== "object") return false;
|
|
196
|
+
if (f.type !== sourceModelName) return false;
|
|
197
|
+
if (f.relationFromFields?.includes(foreignKey)) return true;
|
|
198
|
+
const sourceModel = allModels.find((m) => m.name === sourceModelName);
|
|
199
|
+
if (!sourceModel) return false;
|
|
200
|
+
return sourceModel.fields.some((sf) => sf.relationName === f.relationName && sf.relationFromFields && sf.relationFromFields.includes(foreignKey));
|
|
201
|
+
});
|
|
202
|
+
return backField ? makeSnakeCase(backField.name) : makeSnakeCase(sourceModelName);
|
|
203
|
+
}
|
|
204
|
+
function findM2MBackPopulates(targetModelName, sourceModelName, relationName, allModels) {
|
|
205
|
+
const targetModel = allModels.find((m) => m.name === targetModelName);
|
|
206
|
+
if (!targetModel) return makeSnakeCase(sourceModelName);
|
|
207
|
+
const backField = targetModel.fields.find((f) => f.kind === "object" && f.type === sourceModelName && f.relationName === relationName);
|
|
208
|
+
return backField ? makeSnakeCase(backField.name) : makeSnakeCase(sourceModelName);
|
|
209
|
+
}
|
|
210
|
+
function generateColumn(field, isPk, isFk, associations, allModels, enumMap) {
|
|
211
|
+
const snakeName = field.dbName ?? makeSnakeCase(field.name);
|
|
212
|
+
const pythonType = field.kind === "enum" ? "str" : pythonTypeForNative(field);
|
|
213
|
+
const typeHint = field.isRequired ? pythonType : `Optional[${pythonType}]`;
|
|
214
|
+
const colArgs = [];
|
|
215
|
+
if (field.kind === "enum") {
|
|
216
|
+
const values = enumMap.get(field.type);
|
|
217
|
+
const valuesStr = values ? values.map((v) => `"${v}"`).join(", ") : "";
|
|
218
|
+
colArgs.push(`Enum(${valuesStr}, name="${makeSnakeCase(field.type)}")`);
|
|
219
|
+
} else if (needsExplicitSaType(field)) colArgs.push(resolveNativeType(field));
|
|
220
|
+
if (isFk) {
|
|
221
|
+
const assoc = associations.belongsTo.find((a) => a.foreignKey === field.name);
|
|
222
|
+
if (assoc) {
|
|
223
|
+
const targetTable = allModels.find((m) => m.name === assoc.targetModel)?.dbName ?? makeSnakeCase(assoc.targetModel);
|
|
224
|
+
const targetCol = makeSnakeCase(assoc.references);
|
|
225
|
+
colArgs.push(`ForeignKey("${targetTable}.${targetCol}")`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (isPk) colArgs.push("primary_key=True");
|
|
229
|
+
if (isPk && isAutoincrement(field)) colArgs.push("autoincrement=True");
|
|
230
|
+
if (field.isUnique) colArgs.push("unique=True");
|
|
231
|
+
if (field.type === "DateTime" && isFunctionDefault(field.default) && field.default.name === "now") colArgs.push("server_default=func.now()");
|
|
232
|
+
else if (!isPk || isAutoincrement(field)) {
|
|
233
|
+
const defaultVal = formatDefault(field.default);
|
|
234
|
+
if (defaultVal !== null && !isPk) colArgs.push(`default=${defaultVal}`);
|
|
235
|
+
}
|
|
236
|
+
if (field.isUpdatedAt) colArgs.push("onupdate=func.now()");
|
|
237
|
+
if (colArgs.length === 0) return ` ${snakeName}: Mapped[${typeHint}]`;
|
|
238
|
+
return ` ${snakeName}: Mapped[${typeHint}] = mapped_column(${colArgs.join(", ")})`;
|
|
239
|
+
}
|
|
240
|
+
function generateTableArgs(model, indexes) {
|
|
241
|
+
const uniqueConstraints = model.uniqueFields.map((fields) => {
|
|
242
|
+
return `UniqueConstraint(${fields.map((f) => {
|
|
243
|
+
return `"${model.fields.find((mf) => mf.name === f)?.dbName ?? makeSnakeCase(f)}"`;
|
|
244
|
+
}).join(", ")})`;
|
|
245
|
+
});
|
|
246
|
+
const indexConstraints = indexes.filter((idx) => idx.model === model.name && (idx.type === "normal" || idx.type === "fulltext")).map((idx) => {
|
|
247
|
+
return `Index("${idx.dbName ?? idx.name ?? `idx_${idx.fields.map((f) => makeSnakeCase(f.name)).join("_")}`}", ${idx.fields.map((f) => {
|
|
248
|
+
return `"${model.fields.find((mf) => mf.name === f.name)?.dbName ?? makeSnakeCase(f.name)}"`;
|
|
249
|
+
}).join(", ")})`;
|
|
250
|
+
});
|
|
251
|
+
const allConstraints = [...uniqueConstraints, ...indexConstraints];
|
|
252
|
+
if (allConstraints.length === 0) return [];
|
|
253
|
+
return [
|
|
254
|
+
"",
|
|
255
|
+
" __table_args__ = (",
|
|
256
|
+
...allConstraints.map((c) => ` ${c},`),
|
|
257
|
+
" )"
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
function generateBelongsToRelationships(associations, model, allModels) {
|
|
261
|
+
return associations.belongsTo.map((assoc) => {
|
|
262
|
+
const snakeName = makeSnakeCase(assoc.name);
|
|
263
|
+
const snakeFk = model.fields.find((f) => f.name === assoc.foreignKey)?.dbName ?? makeSnakeCase(assoc.foreignKey);
|
|
264
|
+
const backPop = findBackPopulates(assoc.targetModel, model.name, assoc.foreignKey, allModels);
|
|
265
|
+
const fkClause = needsForeignKeysParam(assoc.targetModel, associations.belongsTo) ? `foreign_keys=[${snakeFk}], ` : "";
|
|
266
|
+
return ` ${snakeName}: Mapped["${assoc.targetModel}"] = relationship(${fkClause}back_populates="${backPop}")`;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function generateHasManyRelationships(associations, model, allModels) {
|
|
270
|
+
return associations.hasMany.map((assoc) => {
|
|
271
|
+
const snakeName = makeSnakeCase(assoc.name);
|
|
272
|
+
const backPop = findBackPopulates(assoc.targetModel, model.name, assoc.foreignKey, allModels);
|
|
273
|
+
const targetModel = allModels.find((m) => m.name === assoc.targetModel);
|
|
274
|
+
const needsFkParam = targetModel ? needsForeignKeysParam(model.name, getAssociations(targetModel, allModels).belongsTo) : false;
|
|
275
|
+
const targetFkSnake = makeSnakeCase(assoc.foreignKey);
|
|
276
|
+
const fkClause = needsFkParam ? `foreign_keys="${assoc.targetModel}.${targetFkSnake}", ` : "";
|
|
277
|
+
return ` ${snakeName}: Mapped[list["${assoc.targetModel}"]] = relationship(${fkClause}back_populates="${backPop}")`;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function generateHasOneRelationships(associations, model, allModels) {
|
|
281
|
+
return associations.hasOne.map((assoc) => {
|
|
282
|
+
const snakeName = makeSnakeCase(assoc.name);
|
|
283
|
+
const backPop = findBackPopulates(assoc.targetModel, model.name, assoc.foreignKey, allModels);
|
|
284
|
+
const targetModel = allModels.find((m) => m.name === assoc.targetModel);
|
|
285
|
+
const needsFkParam = targetModel ? needsForeignKeysParam(model.name, getAssociations(targetModel, allModels).belongsTo) : false;
|
|
286
|
+
const targetFkSnake = makeSnakeCase(assoc.foreignKey);
|
|
287
|
+
const fkClause = needsFkParam ? `foreign_keys="${assoc.targetModel}.${targetFkSnake}", ` : "";
|
|
288
|
+
return ` ${snakeName}: Mapped["${assoc.targetModel}"] = relationship(${fkClause}back_populates="${backPop}", uselist=False)`;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function generateManyToManyRelationships(associations, model, allModels, m2mTables) {
|
|
292
|
+
return associations.manyToMany.map((assoc) => {
|
|
293
|
+
const snakeName = makeSnakeCase(assoc.name);
|
|
294
|
+
const backPop = findM2MBackPopulates(assoc.targetModel, model.name, assoc.relationName, allModels);
|
|
295
|
+
const [leftName, rightName] = model.name < assoc.targetModel ? [model.name, assoc.targetModel] : [assoc.targetModel, model.name];
|
|
296
|
+
const secondaryVar = m2mTables.find((t) => t.leftModel === leftName && t.rightModel === rightName)?.varName ?? `${makeSnakeCase(leftName)}_to_${makeSnakeCase(rightName)}`;
|
|
297
|
+
return ` ${snakeName}: Mapped[list["${assoc.targetModel}"]] = relationship(secondary=${secondaryVar}, back_populates="${backPop}")`;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function generateModelBody(model, allModels, enums, indexes, m2mTables) {
|
|
301
|
+
const idField = model.fields.find((f) => f.isId);
|
|
302
|
+
const compositePkFieldNames = new Set(model.primaryKey?.fields ?? []);
|
|
303
|
+
const isCompositePk = !idField && compositePkFieldNames.size > 0;
|
|
304
|
+
if (!(idField || isCompositePk)) return null;
|
|
305
|
+
const associations = getAssociations(model, allModels);
|
|
306
|
+
const belongsToFkFields = new Set(associations.belongsTo.map((a) => a.foreignKey));
|
|
307
|
+
const enumMap = new Map((enums ?? []).map((e) => [e.name, e.values.map((v) => v.name)]));
|
|
308
|
+
const tableName = model.dbName ?? makeSnakeCase(model.name);
|
|
309
|
+
const columnLines = model.fields.filter((f) => f.kind !== "object").map((field) => {
|
|
310
|
+
return generateColumn(field, field.isId || compositePkFieldNames.has(field.name), belongsToFkFields.has(field.name), associations, allModels, enumMap);
|
|
311
|
+
});
|
|
312
|
+
const tableArgsLines = generateTableArgs(model, indexes);
|
|
313
|
+
const relationLines = [
|
|
314
|
+
...generateBelongsToRelationships(associations, model, allModels),
|
|
315
|
+
...generateHasManyRelationships(associations, model, allModels),
|
|
316
|
+
...generateHasOneRelationships(associations, model, allModels),
|
|
317
|
+
...generateManyToManyRelationships(associations, model, allModels, m2mTables)
|
|
318
|
+
];
|
|
319
|
+
const hasRelations = relationLines.length > 0;
|
|
320
|
+
return [
|
|
321
|
+
`class ${model.name}(Base):`,
|
|
322
|
+
` __tablename__ = "${tableName}"`,
|
|
323
|
+
"",
|
|
324
|
+
...columnLines,
|
|
325
|
+
...tableArgsLines,
|
|
326
|
+
...hasRelations ? [""] : [],
|
|
327
|
+
...relationLines
|
|
328
|
+
].join("\n");
|
|
329
|
+
}
|
|
330
|
+
function collectGlobalImports(models, _enums, indexes, m2mTables) {
|
|
331
|
+
const saImports = /* @__PURE__ */ new Set();
|
|
332
|
+
const needsOptional = models.some((m) => m.fields.some((f) => f.kind !== "object" && !f.isRequired));
|
|
333
|
+
const hasRelationship = models.some((m) => m.fields.some((f) => f.kind === "object"));
|
|
334
|
+
const needsFunc = models.some((m) => m.fields.some((f) => f.type === "DateTime" && isFunctionDefault(f.default) && f.default.name === "now" || f.isUpdatedAt));
|
|
335
|
+
const needsDecimal = models.some((m) => m.fields.some((f) => f.type === "Decimal"));
|
|
336
|
+
const needsDatetime = models.some((m) => m.fields.some((f) => f.type === "DateTime"));
|
|
337
|
+
const needsUuid = models.some((m) => m.fields.some((f) => {
|
|
338
|
+
if (!f.nativeType) return false;
|
|
339
|
+
const [n] = f.nativeType;
|
|
340
|
+
return n === "Uuid";
|
|
341
|
+
}));
|
|
342
|
+
const needsDate = models.some((m) => m.fields.some((f) => {
|
|
343
|
+
if (!f.nativeType) return false;
|
|
344
|
+
const [n] = f.nativeType;
|
|
345
|
+
return n === "Date";
|
|
346
|
+
}));
|
|
347
|
+
const needsTime = models.some((m) => m.fields.some((f) => {
|
|
348
|
+
if (!f.nativeType) return false;
|
|
349
|
+
const [n] = f.nativeType;
|
|
350
|
+
return n === "Time";
|
|
351
|
+
}));
|
|
352
|
+
for (const model of models) {
|
|
353
|
+
for (const field of model.fields) {
|
|
354
|
+
if (field.kind === "object") continue;
|
|
355
|
+
if (field.kind === "enum") {
|
|
356
|
+
saImports.add("Enum");
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (needsExplicitSaType(field)) {
|
|
360
|
+
const resolved = resolveNativeType(field);
|
|
361
|
+
saImports.add(resolved.replace(/\(.*\)$/, ""));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const associations = getAssociations(model, models);
|
|
365
|
+
if (associations.belongsTo.length > 0) saImports.add("ForeignKey");
|
|
366
|
+
if (!model.fields.find((f) => f.isId) && (model.primaryKey?.fields ?? []).length > 0) {
|
|
367
|
+
if (associations.belongsTo.length > 0) saImports.add("ForeignKey");
|
|
368
|
+
}
|
|
369
|
+
if (model.uniqueFields.length > 0) saImports.add("UniqueConstraint");
|
|
370
|
+
if (indexes.some((idx) => idx.model === model.name && (idx.type === "normal" || idx.type === "fulltext"))) saImports.add("Index");
|
|
371
|
+
}
|
|
372
|
+
if (m2mTables.length > 0) {
|
|
373
|
+
saImports.add("Column");
|
|
374
|
+
saImports.add("ForeignKey");
|
|
375
|
+
saImports.add("Table");
|
|
376
|
+
for (const info of m2mTables) {
|
|
377
|
+
const leftType = info.leftPkField ? resolveNativeType(info.leftPkField) : "String";
|
|
378
|
+
const rightType = info.rightPkField ? resolveNativeType(info.rightPkField) : "String";
|
|
379
|
+
saImports.add(leftType.replace(/\(.*\)$/, ""));
|
|
380
|
+
saImports.add(rightType.replace(/\(.*\)$/, ""));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (needsFunc) saImports.add("func");
|
|
384
|
+
const lines = [];
|
|
385
|
+
const sortedSa = [...saImports].sort();
|
|
386
|
+
if (sortedSa.length > 0) lines.push(`from sqlalchemy import ${sortedSa.join(", ")}`);
|
|
387
|
+
const ormImports = [
|
|
388
|
+
"DeclarativeBase",
|
|
389
|
+
"Mapped",
|
|
390
|
+
"mapped_column"
|
|
391
|
+
];
|
|
392
|
+
if (hasRelationship) ormImports.push("relationship");
|
|
393
|
+
lines.push(`from sqlalchemy.orm import ${ormImports.sort().join(", ")}`);
|
|
394
|
+
if (needsOptional) lines.push("from typing import Optional");
|
|
395
|
+
if (needsDecimal) lines.push("from decimal import Decimal as DecimalType");
|
|
396
|
+
const dtParts = [];
|
|
397
|
+
if (needsDatetime) dtParts.push("datetime");
|
|
398
|
+
if (needsDate) dtParts.push("date");
|
|
399
|
+
if (needsTime) dtParts.push("time as time_type");
|
|
400
|
+
if (dtParts.length > 0) lines.push(`from datetime import ${dtParts.join(", ")}`);
|
|
401
|
+
if (needsUuid) lines.push("import uuid as uuid_mod");
|
|
402
|
+
return lines;
|
|
403
|
+
}
|
|
404
|
+
function generateSingleFile(models, enums, indexes) {
|
|
405
|
+
const idx = indexes ?? [];
|
|
406
|
+
const m2mTables = collectManyToManyTables(models);
|
|
407
|
+
const importLines = collectGlobalImports(models, enums, idx, m2mTables);
|
|
408
|
+
const m2mLines = m2mTables.length > 0 ? m2mTables.flatMap((t, i) => i === 0 ? ["", generateAssociationTable(t)] : ["", generateAssociationTable(t)]) : [];
|
|
409
|
+
const modelBodies = models.map((model) => generateModelBody(model, models, enums, idx, m2mTables)).filter((body) => body !== null);
|
|
410
|
+
return [
|
|
411
|
+
...importLines,
|
|
412
|
+
"",
|
|
413
|
+
"",
|
|
414
|
+
"class Base(DeclarativeBase):",
|
|
415
|
+
" pass",
|
|
416
|
+
...m2mLines,
|
|
417
|
+
"",
|
|
418
|
+
"",
|
|
419
|
+
...modelBodies.join("\n\n").split("\n"),
|
|
420
|
+
""
|
|
421
|
+
].join("\n");
|
|
422
|
+
}
|
|
423
|
+
async function writeSQLAlchemyFile(models, outPath, enums, indexes) {
|
|
424
|
+
const mkdirResult = await mkdir(dirname(outPath));
|
|
425
|
+
if (!mkdirResult.ok) return mkdirResult;
|
|
426
|
+
const writeResult = await writeFile(outPath, generateSingleFile(models, enums, indexes));
|
|
427
|
+
if (!writeResult.ok) return writeResult;
|
|
428
|
+
console.log(`wrote ${outPath}`);
|
|
429
|
+
return {
|
|
430
|
+
ok: true,
|
|
431
|
+
value: void 0
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/generator/sqlalchemy/index.ts
|
|
437
|
+
const { generatorHandler } = pkg;
|
|
438
|
+
async function main(options) {
|
|
439
|
+
if (!(options.generator.isCustomOutput && options.generator.output?.value)) throw new Error("output is required for Hekireki-SQLAlchemy. Please specify output in your generator config.");
|
|
440
|
+
const output = options.generator.output.value;
|
|
441
|
+
const resolved = path.extname(output) ? output : path.join(output, "models.py");
|
|
442
|
+
const enums = options.dmmf.datamodel.enums;
|
|
443
|
+
const indexes = options.dmmf.datamodel.indexes;
|
|
444
|
+
const result = await writeSQLAlchemyFile(options.dmmf.datamodel.models, resolved, enums, indexes);
|
|
445
|
+
if (!result.ok) throw new Error(`Failed to write SQLAlchemy models: ${result.error}`);
|
|
446
|
+
}
|
|
447
|
+
generatorHandler({
|
|
448
|
+
onManifest() {
|
|
449
|
+
return {
|
|
450
|
+
defaultOutput: ".",
|
|
451
|
+
prettyName: "Hekireki-SQLAlchemy"
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
onGenerate: main
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
//#endregion
|
|
458
|
+
export { main };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as fmt } from "../../format-CzXgkLDe.js";
|
|
3
|
+
import { c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, l as schemaFromFields, s as makeValidationExtractor, t as getBool } from "../../utils-COHZyQue.js";
|
|
4
|
+
import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-ChsFqlYX.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import pkg from "@prisma/generator-helper";
|
|
7
|
+
|
|
8
|
+
//#region src/helper/typebox.ts
|
|
9
|
+
function makeTypeBoxInfer(modelName) {
|
|
10
|
+
return `export type ${modelName} = Static<typeof ${modelName}Schema>`;
|
|
11
|
+
}
|
|
12
|
+
function makeTypeBoxSchema(modelName, fields) {
|
|
13
|
+
return `export const ${modelName}Schema = Type.Object({\n${fields}\n})`;
|
|
14
|
+
}
|
|
15
|
+
function makeTypeBoxProperties(fields, comment) {
|
|
16
|
+
return fields.map((field) => {
|
|
17
|
+
const commentLines = comment && field.comment.length > 0 ? `${field.comment.map((c) => ` /** ${c} */`).join("\n")}\n` : "";
|
|
18
|
+
const expr = field.validation ?? "Type.Unknown()";
|
|
19
|
+
const wrapped = field.isRequired ? expr : `Type.Optional(${expr})`;
|
|
20
|
+
return `${commentLines} ${field.fieldName}: ${wrapped},`;
|
|
21
|
+
}).join("\n");
|
|
22
|
+
}
|
|
23
|
+
function makeTypeBoxEnumExpression(values) {
|
|
24
|
+
return `Type.Union([${values.map((v) => `Type.Literal('${v}')`).join(", ")}])`;
|
|
25
|
+
}
|
|
26
|
+
const PRISMA_TO_TYPEBOX = {
|
|
27
|
+
String: "Type.String()",
|
|
28
|
+
Int: "Type.Integer()",
|
|
29
|
+
Float: "Type.Number()",
|
|
30
|
+
Boolean: "Type.Boolean()",
|
|
31
|
+
DateTime: "Type.Date()",
|
|
32
|
+
BigInt: "Type.BigInt()",
|
|
33
|
+
Decimal: "Type.Number()",
|
|
34
|
+
Json: "Type.Unknown()",
|
|
35
|
+
Bytes: "Type.Any()"
|
|
36
|
+
};
|
|
37
|
+
function makeTypeBoxSchemas(modelFields, comment) {
|
|
38
|
+
return schemaFromFields(modelFields, comment, makeTypeBoxSchema, makeTypeBoxProperties);
|
|
39
|
+
}
|
|
40
|
+
function makeTypeBoxRelations(model, relProps, options) {
|
|
41
|
+
if (relProps.length === 0) return null;
|
|
42
|
+
const base = ` ...${model.name}Schema.properties,`;
|
|
43
|
+
const rels = relProps.map((r) => ` ${r.key}: ${r.isMany ? `Type.Array(${r.targetModel}Schema)` : `${r.targetModel}Schema`},`).join("\n");
|
|
44
|
+
const typeLine = options?.includeType ? `\n\nexport type ${model.name}Relations = Static<typeof ${model.name}RelationsSchema>` : "";
|
|
45
|
+
return `export const ${model.name}RelationsSchema = Type.Object({\n${base}\n${rels}\n})${typeLine}`;
|
|
46
|
+
}
|
|
47
|
+
function typebox(models, type, comment, enums) {
|
|
48
|
+
return validationSchemas(models, type, comment, {
|
|
49
|
+
importStatement: type ? `import { type Static, Type } from '@sinclair/typebox'` : `import { Type } from '@sinclair/typebox'`,
|
|
50
|
+
annotationPrefix: "@t.",
|
|
51
|
+
parseDocument: parseDocumentWithoutAnnotations,
|
|
52
|
+
extractValidation: makeValidationExtractor("@t."),
|
|
53
|
+
inferType: makeTypeBoxInfer,
|
|
54
|
+
schemas: makeTypeBoxSchemas,
|
|
55
|
+
typeMapping: PRISMA_TO_TYPEBOX,
|
|
56
|
+
enums,
|
|
57
|
+
formatEnum: makeTypeBoxEnumExpression
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/generator/typebox/index.ts
|
|
63
|
+
const { generatorHandler } = pkg;
|
|
64
|
+
async function main(options) {
|
|
65
|
+
if (!(options.generator.isCustomOutput && options.generator.output?.value)) throw new Error("output is required for Hekireki-TypeBox. Please specify output in your generator config.");
|
|
66
|
+
const output = options.generator.output.value;
|
|
67
|
+
const resolved = path.extname(output) ? {
|
|
68
|
+
dir: path.dirname(output),
|
|
69
|
+
file: output
|
|
70
|
+
} : {
|
|
71
|
+
dir: output,
|
|
72
|
+
file: path.join(output, "index.ts")
|
|
73
|
+
};
|
|
74
|
+
const enableRelation = getBool(options.generator.config?.relation);
|
|
75
|
+
const fmtResult = await fmt([typebox(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeTypeBoxRelations) : ""].filter(Boolean).join("\n\n"));
|
|
76
|
+
if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
|
|
77
|
+
const mkdirResult = await mkdir(resolved.dir);
|
|
78
|
+
if (!mkdirResult.ok) throw new Error(`Failed to create directory: ${mkdirResult.error}`);
|
|
79
|
+
const writeResult = await writeFile(resolved.file, fmtResult.value);
|
|
80
|
+
if (!writeResult.ok) throw new Error(`Failed to write file: ${writeResult.error}`);
|
|
81
|
+
}
|
|
82
|
+
generatorHandler({
|
|
83
|
+
onManifest() {
|
|
84
|
+
return {
|
|
85
|
+
defaultOutput: ".",
|
|
86
|
+
prettyName: "Hekireki-TypeBox"
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
onGenerate: main
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
export { main };
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as fmt } from "../../format-CzXgkLDe.js";
|
|
3
|
-
import {
|
|
4
|
-
import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-
|
|
3
|
+
import { a as makePropertiesGenerator, c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, l as schemaFromFields, s as makeValidationExtractor, t as getBool } from "../../utils-COHZyQue.js";
|
|
4
|
+
import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-ChsFqlYX.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pkg from "@prisma/generator-helper";
|
|
7
7
|
|
|
8
8
|
//#region src/helper/valibot.ts
|
|
9
|
+
function makeValibotInfer(modelName) {
|
|
10
|
+
return `export type ${modelName} = v.InferInput<typeof ${modelName}Schema>`;
|
|
11
|
+
}
|
|
12
|
+
function makeValibotSchema(modelName, fields) {
|
|
13
|
+
return `export const ${modelName}Schema = v.object({\n${fields}\n})`;
|
|
14
|
+
}
|
|
15
|
+
function makeValibotEnumExpression(values) {
|
|
16
|
+
return `picklist([${values.map((v) => `'${v}'`).join(", ")}])`;
|
|
17
|
+
}
|
|
9
18
|
const PRISMA_TO_VALIBOT = {
|
|
10
19
|
String: "string()",
|
|
11
20
|
Int: "number()",
|
|
@@ -22,9 +31,10 @@ function makeValibotSchemas(modelFields, comment) {
|
|
|
22
31
|
}
|
|
23
32
|
function makeValibotRelations(model, relProps, options) {
|
|
24
33
|
if (relProps.length === 0) return null;
|
|
25
|
-
const
|
|
34
|
+
const base = ` ...${model.name}Schema.entries,`;
|
|
35
|
+
const rels = relProps.map((r) => ` ${r.key}: ${r.isMany ? `v.array(${r.targetModel}Schema)` : `${r.targetModel}Schema`},`).join("\n");
|
|
26
36
|
const typeLine = options?.includeType ? `\n\nexport type ${model.name}Relations = v.InferInput<typeof ${model.name}RelationsSchema>` : "";
|
|
27
|
-
return `export const ${model.name}RelationsSchema = v.object({\n${
|
|
37
|
+
return `export const ${model.name}RelationsSchema = v.object({\n${base}\n${rels}\n})${typeLine}`;
|
|
28
38
|
}
|
|
29
39
|
function valibot(models, type, comment, enums) {
|
|
30
40
|
return validationSchemas(models, type, comment, {
|
|
@@ -53,7 +63,7 @@ async function main(options) {
|
|
|
53
63
|
dir: output,
|
|
54
64
|
file: path.join(output, "index.ts")
|
|
55
65
|
};
|
|
56
|
-
const enableRelation =
|
|
66
|
+
const enableRelation = getBool(options.generator.config?.relation);
|
|
57
67
|
const fmtResult = await fmt([valibot(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeValibotRelations) : ""].filter(Boolean).join("\n\n"));
|
|
58
68
|
if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
|
|
59
69
|
const mkdirResult = await mkdir(resolved.dir);
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as fmt } from "../../format-CzXgkLDe.js";
|
|
3
|
-
import {
|
|
4
|
-
import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-
|
|
3
|
+
import { a as makePropertiesGenerator, c as parseDocumentWithoutAnnotations, d as mkdir, f as writeFile, l as schemaFromFields, n as getString, s as makeValidationExtractor, t as getBool } from "../../utils-COHZyQue.js";
|
|
4
|
+
import { n as validationSchemas, t as makeRelationsOnly } from "../../prisma-ChsFqlYX.js";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pkg from "@prisma/generator-helper";
|
|
7
7
|
|
|
8
8
|
//#region src/helper/zod.ts
|
|
9
|
+
function makeZodInfer(modelName) {
|
|
10
|
+
return `export type ${modelName} = z.infer<typeof ${modelName}Schema>`;
|
|
11
|
+
}
|
|
12
|
+
function makeZodSchema(modelName, fields) {
|
|
13
|
+
return `export const ${modelName}Schema = z.object({\n${fields}\n})`;
|
|
14
|
+
}
|
|
15
|
+
function makeZodEnumExpression(values) {
|
|
16
|
+
return `enum([${values.map((v) => `'${v}'`).join(", ")}])`;
|
|
17
|
+
}
|
|
9
18
|
const PRISMA_TO_ZOD = {
|
|
10
19
|
String: "string()",
|
|
11
20
|
Int: "number()",
|
|
@@ -22,9 +31,10 @@ function makeZodSchemas(modelFields, comment) {
|
|
|
22
31
|
}
|
|
23
32
|
function makeZodRelations(model, relProps, options) {
|
|
24
33
|
if (relProps.length === 0) return null;
|
|
25
|
-
const
|
|
34
|
+
const base = ` ...${model.name}Schema.shape,`;
|
|
35
|
+
const rels = relProps.map((r) => ` ${r.key}: ${r.isMany ? `z.array(${r.targetModel}Schema)` : `${r.targetModel}Schema`},`).join("\n");
|
|
26
36
|
const typeLine = options?.includeType ? `\n\nexport type ${model.name}Relations = z.infer<typeof ${model.name}RelationsSchema>` : "";
|
|
27
|
-
return `export const ${model.name}RelationsSchema = z.object({\n${
|
|
37
|
+
return `export const ${model.name}RelationsSchema = z.object({\n${base}\n${rels}\n})${typeLine}`;
|
|
28
38
|
}
|
|
29
39
|
function zod(models, type, comment, zodVersion, enums) {
|
|
30
40
|
return validationSchemas(models, type, comment, {
|
|
@@ -54,7 +64,7 @@ async function main(options) {
|
|
|
54
64
|
file: path.join(output, "index.ts")
|
|
55
65
|
};
|
|
56
66
|
const zodVersion = getString(options.generator.config?.zod, "v4");
|
|
57
|
-
const enableRelation =
|
|
67
|
+
const enableRelation = getBool(options.generator.config?.relation);
|
|
58
68
|
const fmtResult = await fmt([zod(options.dmmf.datamodel.models, getBool(options.generator.config?.type), getBool(options.generator.config?.comment), zodVersion, options.dmmf.datamodel.enums), enableRelation ? makeRelationsOnly(options.dmmf, getBool(options.generator.config?.type), makeZodRelations) : ""].filter(Boolean).join("\n\n"));
|
|
59
69
|
if (!fmtResult.ok) throw new Error(`Format error: ${fmtResult.error}`);
|
|
60
70
|
const mkdirResult = await mkdir(resolved.dir);
|