@vibeorm/dsl 0.3.0 → 2.0.0-alpha.1
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 +62 -471
- package/dist/column.d.ts +150 -0
- package/dist/column.d.ts.map +1 -0
- package/dist/enum.d.ts +27 -0
- package/dist/enum.d.ts.map +1 -0
- package/dist/generator/index.d.ts +5 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/prisma.d.ts +19 -0
- package/dist/generator/prisma.d.ts.map +1 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1546 -0
- package/dist/index.js.map +22 -0
- package/dist/model.d.ts +44 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/naming.d.ts +23 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/relation-plan.d.ts +101 -0
- package/dist/relation-plan.d.ts.map +1 -0
- package/dist/relation.d.ts +23 -0
- package/dist/relation.d.ts.map +1 -0
- package/dist/relations.d.ts +29 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/schema.d.ts +38 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/templates.d.ts +79 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/to-ir.d.ts +41 -0
- package/dist/to-ir.d.ts.map +1 -0
- package/dist/types.d.ts +286 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/validate.d.ts +22 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +29 -26
- package/src/column.ts +0 -389
- package/src/enum.ts +0 -71
- package/src/generator/index.ts +0 -5
- package/src/generator/prisma.ts +0 -728
- package/src/index.ts +0 -247
- package/src/model.ts +0 -330
- package/src/relation.ts +0 -75
- package/src/relations.ts +0 -293
- package/src/schema.ts +0 -184
- package/src/templates.ts +0 -134
- package/src/types.ts +0 -381
- package/src/validate.ts +0 -141
package/src/generator/prisma.ts
DELETED
|
@@ -1,728 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prisma schema generator for @vibeorm/dsl
|
|
3
|
-
*
|
|
4
|
-
* Generates a valid Prisma schema string from a SchemaDef.
|
|
5
|
-
* Includes fix for 1:1 relation merging: when both sides of a one-to-one
|
|
6
|
-
* define r.one pointing at the same FK column, they are merged into a
|
|
7
|
-
* single Prisma relation pair instead of generating two separate relations.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
ColumnDef,
|
|
12
|
-
IndexType,
|
|
13
|
-
ModelDef,
|
|
14
|
-
PrismaGeneratorConfig,
|
|
15
|
-
RelationsMap,
|
|
16
|
-
SchemaDef,
|
|
17
|
-
StoredRelationDef,
|
|
18
|
-
} from "../types.ts";
|
|
19
|
-
import { evaluateIndexes } from "../model.ts";
|
|
20
|
-
|
|
21
|
-
// ─── Types ─────────────────────────────────
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Describes which role a relation plays in a merged 1:1 pair
|
|
25
|
-
*/
|
|
26
|
-
type MergedOneToOneSide = {
|
|
27
|
-
role: "fkOwner" | "backRelation";
|
|
28
|
-
/** The relation name on the paired (other) side */
|
|
29
|
-
pairedRelName: string;
|
|
30
|
-
/** The model name on the paired (other) side */
|
|
31
|
-
pairedModelName: string;
|
|
32
|
-
/** Merged onDelete (prefer FK owner's value) */
|
|
33
|
-
onDelete?: string;
|
|
34
|
-
/** Merged onUpdate (prefer FK owner's value) */
|
|
35
|
-
onUpdate?: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// ─── Column Formatting ─────────────────────────────────
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Maps DSL column types to Prisma types
|
|
42
|
-
*/
|
|
43
|
-
function getPrismaType(column: ColumnDef): string {
|
|
44
|
-
const isArray = column.__config.array ? "[]" : "";
|
|
45
|
-
|
|
46
|
-
switch (column.__columnType) {
|
|
47
|
-
case "string":
|
|
48
|
-
return `String${isArray}`;
|
|
49
|
-
case "int":
|
|
50
|
-
return `Int${isArray}`;
|
|
51
|
-
case "bigint":
|
|
52
|
-
return `BigInt${isArray}`;
|
|
53
|
-
case "float":
|
|
54
|
-
return `Float${isArray}`;
|
|
55
|
-
case "decimal":
|
|
56
|
-
return `Decimal${isArray}`;
|
|
57
|
-
case "boolean":
|
|
58
|
-
return `Boolean${isArray}`;
|
|
59
|
-
case "datetime":
|
|
60
|
-
return `DateTime${isArray}`;
|
|
61
|
-
case "json":
|
|
62
|
-
return `Json${isArray}`;
|
|
63
|
-
case "id": {
|
|
64
|
-
const idGenType = column.__config.type as string;
|
|
65
|
-
return idGenType === "autoincrement" ? "Int" : "String";
|
|
66
|
-
}
|
|
67
|
-
case "bytes":
|
|
68
|
-
return `Bytes${isArray}`;
|
|
69
|
-
case "enum": {
|
|
70
|
-
const enumName = column.__enumName;
|
|
71
|
-
return enumName ? `${enumName}${isArray}` : `String${isArray}`;
|
|
72
|
-
}
|
|
73
|
-
case "unsupported": {
|
|
74
|
-
const dbType = column.__config.dbType as string;
|
|
75
|
-
return `Unsupported("${dbType}")`;
|
|
76
|
-
}
|
|
77
|
-
default:
|
|
78
|
-
return `String${isArray}`;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Formats a column definition to Prisma syntax
|
|
84
|
-
*/
|
|
85
|
-
function formatColumn(name: string, column: ColumnDef): string {
|
|
86
|
-
const commentLines: string[] = [];
|
|
87
|
-
|
|
88
|
-
// Triple-slash comments (for Zod generators, docs, etc.)
|
|
89
|
-
if (column.__config.comment) {
|
|
90
|
-
const comment = column.__config.comment as string;
|
|
91
|
-
for (const line of comment.split("\n")) {
|
|
92
|
-
commentLines.push(` /// ${line}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const type = getPrismaType(column);
|
|
97
|
-
const optional = column.__config.optional ? "?" : "";
|
|
98
|
-
const parts: string[] = [` ${name} ${type}${optional}`];
|
|
99
|
-
|
|
100
|
-
// Handle ID column
|
|
101
|
-
if (column.__columnType === "id") {
|
|
102
|
-
parts.push("@id");
|
|
103
|
-
const idType = column.__config.type as string;
|
|
104
|
-
if (idType === "ulid") {
|
|
105
|
-
parts.push("@default(ulid())");
|
|
106
|
-
} else if (idType === "cuid") {
|
|
107
|
-
parts.push("@default(cuid())");
|
|
108
|
-
} else if (idType === "uuid") {
|
|
109
|
-
parts.push("@default(uuid())");
|
|
110
|
-
} else if (idType === "autoincrement") {
|
|
111
|
-
parts.push("@default(autoincrement())");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Handle default values
|
|
116
|
-
if (column.__config.default !== undefined && column.__columnType !== "id") {
|
|
117
|
-
const defaultVal = column.__config.default;
|
|
118
|
-
if (column.__columnType === "datetime" && defaultVal === "now") {
|
|
119
|
-
parts.push("@default(now())");
|
|
120
|
-
} else if (column.__columnType === "enum" && typeof defaultVal === "string") {
|
|
121
|
-
parts.push(`@default(${defaultVal})`);
|
|
122
|
-
} else if (typeof defaultVal === "string") {
|
|
123
|
-
parts.push(`@default("${defaultVal}")`);
|
|
124
|
-
} else if (typeof defaultVal === "boolean" || typeof defaultVal === "number") {
|
|
125
|
-
parts.push(`@default(${defaultVal})`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Handle @updatedAt
|
|
130
|
-
if (column.__columnType === "datetime" && column.__config.updatedAt) {
|
|
131
|
-
parts.push("@updatedAt");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Handle @db.Timestamptz
|
|
135
|
-
if (column.__columnType === "datetime" && column.__config.tz) {
|
|
136
|
-
parts.push("@db.Timestamptz()");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Handle unique
|
|
140
|
-
if (column.__config.unique) {
|
|
141
|
-
parts.push("@unique");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Handle @map for column name mapping
|
|
145
|
-
if (column.__config.map) {
|
|
146
|
-
parts.push(`@map("${column.__config.map}")`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Handle @default(dbgenerated(...)) for database-native defaults
|
|
150
|
-
if (column.__config.dbgenerated) {
|
|
151
|
-
parts.push(`@default(dbgenerated("${column.__config.dbgenerated}"))`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Handle database-level string type annotations
|
|
155
|
-
if (column.__columnType === "string") {
|
|
156
|
-
const maxLen = column.__config.maxLength as number | undefined;
|
|
157
|
-
const isText = column.__config.text as boolean | undefined;
|
|
158
|
-
if (maxLen) {
|
|
159
|
-
parts.push(`@db.VarChar(${maxLen})`);
|
|
160
|
-
} else if (isText) {
|
|
161
|
-
parts.push("@db.Text");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Handle decimal precision/scale
|
|
166
|
-
if (column.__columnType === "decimal") {
|
|
167
|
-
const precision = column.__config.precision as number | undefined;
|
|
168
|
-
const scale = column.__config.scale as number | undefined;
|
|
169
|
-
if (precision !== undefined) {
|
|
170
|
-
parts.push(`@db.Decimal(${precision}, ${scale ?? 0})`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const columnLine = parts.join(" ");
|
|
175
|
-
if (commentLines.length > 0) {
|
|
176
|
-
return [...commentLines, columnLine].join("\n");
|
|
177
|
-
}
|
|
178
|
-
return columnLine;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ─── 1:1 Relation Merge Detection ─────────────────────────────────
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Detects pairs of r.one relations between two models that describe the same
|
|
185
|
-
* logical 1:1 relationship and determines which side owns the FK.
|
|
186
|
-
*
|
|
187
|
-
* Two r.one relations form a pair when:
|
|
188
|
-
* relA: fromModel=A, fromColumn=colA, toModel=B, toColumn=colB
|
|
189
|
-
* relB: fromModel=B, fromColumn=colB, toModel=A, toColumn=colA
|
|
190
|
-
*
|
|
191
|
-
* The FK owner is the side whose fromColumn is NOT a primary key (id) column.
|
|
192
|
-
*
|
|
193
|
-
* Returns a Map keyed by "ModelName:relationName" with merge metadata.
|
|
194
|
-
*/
|
|
195
|
-
function detectOneToOnePairs(params: {
|
|
196
|
-
relations: RelationsMap;
|
|
197
|
-
allModels: Map<string, ModelDef>;
|
|
198
|
-
}): Map<string, MergedOneToOneSide> {
|
|
199
|
-
const { relations, allModels } = params;
|
|
200
|
-
const mergeMap = new Map<string, MergedOneToOneSide>();
|
|
201
|
-
|
|
202
|
-
// Collect all "one" type relations into a flat list
|
|
203
|
-
type OneRelEntry = {
|
|
204
|
-
modelName: string;
|
|
205
|
-
relName: string;
|
|
206
|
-
rel: StoredRelationDef;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const allOnes: OneRelEntry[] = [];
|
|
210
|
-
for (const [modelName, modelRels] of Object.entries(relations)) {
|
|
211
|
-
for (const [relName, rel] of Object.entries(modelRels)) {
|
|
212
|
-
if (rel.__relationType === "one") {
|
|
213
|
-
allOnes.push({ modelName, relName, rel });
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Track already-paired entries to avoid double-processing
|
|
219
|
-
const paired = new Set<string>();
|
|
220
|
-
|
|
221
|
-
for (let i = 0; i < allOnes.length; i++) {
|
|
222
|
-
const a = allOnes[i]!;
|
|
223
|
-
const keyA = `${a.modelName}:${a.relName}`;
|
|
224
|
-
if (paired.has(keyA)) continue;
|
|
225
|
-
|
|
226
|
-
for (let j = i + 1; j < allOnes.length; j++) {
|
|
227
|
-
const b = allOnes[j]!;
|
|
228
|
-
const keyB = `${b.modelName}:${b.relName}`;
|
|
229
|
-
if (paired.has(keyB)) continue;
|
|
230
|
-
|
|
231
|
-
// Check if they describe the same FK column pair (cross-match)
|
|
232
|
-
const sameColumnPair =
|
|
233
|
-
a.rel.__fromModel === b.rel.__toModel &&
|
|
234
|
-
a.rel.__fromColumn === b.rel.__toColumn &&
|
|
235
|
-
a.rel.__toModel === b.rel.__fromModel &&
|
|
236
|
-
a.rel.__toColumn === b.rel.__fromColumn;
|
|
237
|
-
|
|
238
|
-
if (!sameColumnPair) continue;
|
|
239
|
-
|
|
240
|
-
// They're a pair! Determine which side owns the FK.
|
|
241
|
-
// The FK owner is the side whose __fromColumn is NOT a primary key column.
|
|
242
|
-
const modelA = allModels.get(a.modelName);
|
|
243
|
-
const modelB = allModels.get(b.modelName);
|
|
244
|
-
|
|
245
|
-
const aFromColIsId = modelA?.__columns.get(a.rel.__fromColumn)?.__columnType === "id";
|
|
246
|
-
const bFromColIsId = modelB?.__columns.get(b.rel.__fromColumn)?.__columnType === "id";
|
|
247
|
-
|
|
248
|
-
let fkOwner: OneRelEntry;
|
|
249
|
-
let backRelSide: OneRelEntry;
|
|
250
|
-
|
|
251
|
-
if (aFromColIsId && !bFromColIsId) {
|
|
252
|
-
// A's from is a PK, B's from is a FK → B owns the FK
|
|
253
|
-
fkOwner = b;
|
|
254
|
-
backRelSide = a;
|
|
255
|
-
} else if (!aFromColIsId && bFromColIsId) {
|
|
256
|
-
// B's from is a PK, A's from is a FK → A owns the FK
|
|
257
|
-
fkOwner = a;
|
|
258
|
-
backRelSide = b;
|
|
259
|
-
} else {
|
|
260
|
-
// Ambiguous: neither or both are PKs. Fall back to: the side that
|
|
261
|
-
// specifies onDelete/onUpdate is the FK owner, otherwise pick the
|
|
262
|
-
// second one (the one whose fromColumn looks like a FK name - contains "Id").
|
|
263
|
-
if (a.rel.__onDelete || a.rel.__onUpdate) {
|
|
264
|
-
fkOwner = a;
|
|
265
|
-
backRelSide = b;
|
|
266
|
-
} else if (b.rel.__onDelete || b.rel.__onUpdate) {
|
|
267
|
-
fkOwner = b;
|
|
268
|
-
backRelSide = a;
|
|
269
|
-
} else {
|
|
270
|
-
// Last resort: pick the side whose fromColumn contains "Id" (typical FK naming)
|
|
271
|
-
const aLooksFk = a.rel.__fromColumn.toLowerCase().includes("id") && a.rel.__fromColumn !== "id";
|
|
272
|
-
const bLooksFk = b.rel.__fromColumn.toLowerCase().includes("id") && b.rel.__fromColumn !== "id";
|
|
273
|
-
if (aLooksFk && !bLooksFk) {
|
|
274
|
-
fkOwner = a;
|
|
275
|
-
backRelSide = b;
|
|
276
|
-
} else {
|
|
277
|
-
fkOwner = b;
|
|
278
|
-
backRelSide = a;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const fkKey = `${fkOwner.modelName}:${fkOwner.relName}`;
|
|
284
|
-
const backKey = `${backRelSide.modelName}:${backRelSide.relName}`;
|
|
285
|
-
|
|
286
|
-
// Merge onDelete/onUpdate: prefer FK owner's, fall back to back-relation's
|
|
287
|
-
const mergedOnDelete = fkOwner.rel.__onDelete ?? backRelSide.rel.__onDelete;
|
|
288
|
-
const mergedOnUpdate = fkOwner.rel.__onUpdate ?? backRelSide.rel.__onUpdate;
|
|
289
|
-
|
|
290
|
-
mergeMap.set(fkKey, {
|
|
291
|
-
role: "fkOwner",
|
|
292
|
-
pairedRelName: backRelSide.relName,
|
|
293
|
-
pairedModelName: backRelSide.modelName,
|
|
294
|
-
onDelete: mergedOnDelete,
|
|
295
|
-
onUpdate: mergedOnUpdate,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
mergeMap.set(backKey, {
|
|
299
|
-
role: "backRelation",
|
|
300
|
-
pairedRelName: fkOwner.relName,
|
|
301
|
-
pairedModelName: fkOwner.modelName,
|
|
302
|
-
onDelete: mergedOnDelete,
|
|
303
|
-
onUpdate: mergedOnUpdate,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
paired.add(fkKey);
|
|
307
|
-
paired.add(backKey);
|
|
308
|
-
break; // relA is paired, move to next i
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return mergeMap;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ─── Relation Formatting ─────────────────────────────────
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Capitalizes the first letter of a string (for PascalCase relation names)
|
|
319
|
-
*/
|
|
320
|
-
function capitalize(str: string): string {
|
|
321
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Determines if this model owns the FK for a "one" relation.
|
|
326
|
-
*/
|
|
327
|
-
function isOwningFkSide(relation: StoredRelationDef, modelName: string): boolean {
|
|
328
|
-
return relation.__fromModel === modelName;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Formats a "one" relation to Prisma syntax.
|
|
333
|
-
*
|
|
334
|
-
* If the relation is part of a merged 1:1 pair, uses the merge metadata
|
|
335
|
-
* to determine correct output. Otherwise falls back to standard behavior.
|
|
336
|
-
*/
|
|
337
|
-
function formatOneRelation(params: {
|
|
338
|
-
name: string;
|
|
339
|
-
relation: StoredRelationDef;
|
|
340
|
-
modelName: string;
|
|
341
|
-
model: ModelDef;
|
|
342
|
-
mergeMap: Map<string, MergedOneToOneSide>;
|
|
343
|
-
}): string {
|
|
344
|
-
const { name, relation, modelName, model, mergeMap } = params;
|
|
345
|
-
const targetName = relation.__targetModel;
|
|
346
|
-
const pascalName = capitalize(name);
|
|
347
|
-
const mergeKey = `${modelName}:${name}`;
|
|
348
|
-
const mergeInfo = mergeMap.get(mergeKey);
|
|
349
|
-
|
|
350
|
-
// ─── Merged 1:1 pair handling ─────
|
|
351
|
-
if (mergeInfo) {
|
|
352
|
-
if (mergeInfo.role === "fkOwner") {
|
|
353
|
-
// This side owns the FK — emit @relation(fields/references)
|
|
354
|
-
const fkName = relation.__fromColumn;
|
|
355
|
-
const fkColumn = model.__columns.get(fkName);
|
|
356
|
-
const isOptional = fkColumn?.__config.optional || relation.__optional;
|
|
357
|
-
const optional = isOptional ? "?" : "";
|
|
358
|
-
|
|
359
|
-
const relationParts: string[] = [];
|
|
360
|
-
if (relation.__alias) {
|
|
361
|
-
relationParts.push(`"${relation.__alias}"`);
|
|
362
|
-
}
|
|
363
|
-
relationParts.push(`fields: [${fkName}]`);
|
|
364
|
-
relationParts.push(`references: [${relation.__toColumn}]`);
|
|
365
|
-
|
|
366
|
-
// Use merged cascade actions
|
|
367
|
-
if (mergeInfo.onDelete) {
|
|
368
|
-
relationParts.push(`onDelete: ${mergeInfo.onDelete}`);
|
|
369
|
-
}
|
|
370
|
-
if (mergeInfo.onUpdate) {
|
|
371
|
-
relationParts.push(`onUpdate: ${mergeInfo.onUpdate}`);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return ` ${pascalName} ${targetName}${optional} @relation(${relationParts.join(", ")})`;
|
|
375
|
-
} else {
|
|
376
|
-
// This is the back-relation side of a 1:1 — emit simple optional reference
|
|
377
|
-
const relationAttr = relation.__alias ? ` @relation("${relation.__alias}")` : "";
|
|
378
|
-
return ` ${pascalName} ${targetName}?${relationAttr}`;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// ─── Standard (non-merged) one relation handling ─────
|
|
383
|
-
const ownsFk = isOwningFkSide(relation, modelName);
|
|
384
|
-
|
|
385
|
-
if (ownsFk) {
|
|
386
|
-
const fkName = relation.__fromColumn;
|
|
387
|
-
const fkColumn = model.__columns.get(fkName);
|
|
388
|
-
const isOptional = fkColumn?.__config.optional || relation.__optional;
|
|
389
|
-
const optional = isOptional ? "?" : "";
|
|
390
|
-
|
|
391
|
-
const relationParts: string[] = [];
|
|
392
|
-
if (relation.__alias) {
|
|
393
|
-
relationParts.push(`"${relation.__alias}"`);
|
|
394
|
-
}
|
|
395
|
-
relationParts.push(`fields: [${fkName}]`);
|
|
396
|
-
relationParts.push(`references: [${relation.__toColumn}]`);
|
|
397
|
-
|
|
398
|
-
if (relation.__onDelete) {
|
|
399
|
-
relationParts.push(`onDelete: ${relation.__onDelete}`);
|
|
400
|
-
}
|
|
401
|
-
if (relation.__onUpdate) {
|
|
402
|
-
relationParts.push(`onUpdate: ${relation.__onUpdate}`);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return ` ${pascalName} ${targetName}${optional} @relation(${relationParts.join(", ")})`;
|
|
406
|
-
} else {
|
|
407
|
-
// Reverse side of a one-to-one — simple back-relation
|
|
408
|
-
const optional = relation.__optional ? "?" : "";
|
|
409
|
-
const relationAttr = relation.__alias ? ` @relation("${relation.__alias}")` : "";
|
|
410
|
-
return ` ${pascalName} ${targetName}${optional}${relationAttr}`;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Formats a "many" relation to Prisma syntax.
|
|
416
|
-
* "many" relations never have fields/references.
|
|
417
|
-
*/
|
|
418
|
-
function formatManyRelation(name: string, relation: StoredRelationDef): string {
|
|
419
|
-
const targetName = relation.__targetModel;
|
|
420
|
-
const pascalName = capitalize(name);
|
|
421
|
-
const relationAttr = relation.__alias ? ` @relation("${relation.__alias}")` : "";
|
|
422
|
-
return ` ${pascalName} ${targetName}[]${relationAttr}`;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// ─── Index Collection ─────────────────────────────────
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Collects all single-column indexes from a model
|
|
429
|
-
*/
|
|
430
|
-
function collectSingleColumnIndexes(model: ModelDef): string[] {
|
|
431
|
-
const indexedColumns: string[] = [];
|
|
432
|
-
|
|
433
|
-
for (const [name, column] of model.__columns) {
|
|
434
|
-
if (column.__columnType === "id" || column.__config.unique) {
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
if (column.__config.index) {
|
|
438
|
-
indexedColumns.push(name);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return indexedColumns;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// ─── Missing Reverse Relations ─────────────────────────────────
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Finds missing reverse relations that need to be auto-generated.
|
|
449
|
-
*
|
|
450
|
-
* For each "one" relation pointing TO this model, check if there's a
|
|
451
|
-
* corresponding "many" relation FROM this model back. If not, generate it.
|
|
452
|
-
*
|
|
453
|
-
* IMPORTANT: Skips any relation that is part of a merged 1:1 pair, since
|
|
454
|
-
* those are fully handled by the merge logic and should NOT get array reverses.
|
|
455
|
-
*/
|
|
456
|
-
function findMissingReverseRelations(params: {
|
|
457
|
-
modelName: string;
|
|
458
|
-
relations: RelationsMap;
|
|
459
|
-
allModels: Map<string, ModelDef>;
|
|
460
|
-
mergeMap: Map<string, MergedOneToOneSide>;
|
|
461
|
-
}): Array<{ name: string; targetModel: string; alias?: string }> {
|
|
462
|
-
const { modelName, relations, allModels, mergeMap } = params;
|
|
463
|
-
const missing: Array<{ name: string; targetModel: string; alias?: string }> = [];
|
|
464
|
-
|
|
465
|
-
// Track existing "many" relations by target+alias combo
|
|
466
|
-
const existingManyRelations = new Set<string>();
|
|
467
|
-
// Track existing field names to avoid duplicates
|
|
468
|
-
const existingFieldNames = new Set<string>();
|
|
469
|
-
|
|
470
|
-
// Collect existing relations from this model
|
|
471
|
-
const modelRelations = relations[modelName] ?? {};
|
|
472
|
-
for (const [relName, rel] of Object.entries(modelRelations)) {
|
|
473
|
-
existingFieldNames.add(relName);
|
|
474
|
-
if (rel.__relationType === "many") {
|
|
475
|
-
const key = `${rel.__targetModel}:${rel.__alias || ""}`;
|
|
476
|
-
existingManyRelations.add(key);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Check ALL models (including self) for relations pointing TO this model
|
|
481
|
-
for (const [sourceModelName, sourceModelRels] of Object.entries(relations)) {
|
|
482
|
-
for (const [relName, rel] of Object.entries(sourceModelRels)) {
|
|
483
|
-
if (rel.__targetModel === modelName && rel.__relationType === "one") {
|
|
484
|
-
// Skip if this relation is part of a merged 1:1 pair
|
|
485
|
-
const sourceKey = `${sourceModelName}:${relName}`;
|
|
486
|
-
if (mergeMap.has(sourceKey)) {
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const reverseKey = `${sourceModelName}:${rel.__alias || ""}`;
|
|
491
|
-
|
|
492
|
-
// For self-referential relations, check if there's already an explicit
|
|
493
|
-
// "many" relation with the same alias defined
|
|
494
|
-
if (sourceModelName === modelName) {
|
|
495
|
-
const hasExplicitReverse = Object.values(sourceModelRels).some(
|
|
496
|
-
(r): r is StoredRelationDef =>
|
|
497
|
-
r.__relationType === "many" &&
|
|
498
|
-
r.__targetModel === modelName &&
|
|
499
|
-
r.__alias === rel.__alias
|
|
500
|
-
);
|
|
501
|
-
if (hasExplicitReverse) continue;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (!existingManyRelations.has(reverseKey)) {
|
|
505
|
-
let baseName = sourceModelName;
|
|
506
|
-
let fieldName = baseName;
|
|
507
|
-
|
|
508
|
-
if (rel.__alias) {
|
|
509
|
-
fieldName = rel.__alias;
|
|
510
|
-
baseName = fieldName;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
let counter = 1;
|
|
514
|
-
while (existingFieldNames.has(fieldName)) {
|
|
515
|
-
counter++;
|
|
516
|
-
fieldName = `${baseName}${counter}`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
missing.push({
|
|
520
|
-
name: fieldName,
|
|
521
|
-
targetModel: sourceModelName,
|
|
522
|
-
alias: rel.__alias,
|
|
523
|
-
});
|
|
524
|
-
existingManyRelations.add(reverseKey);
|
|
525
|
-
existingFieldNames.add(fieldName);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return missing;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// ─── Model Formatting ─────────────────────────────────
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Formats a single model to Prisma syntax
|
|
538
|
-
*/
|
|
539
|
-
function formatModel(params: {
|
|
540
|
-
model: ModelDef;
|
|
541
|
-
relations: RelationsMap;
|
|
542
|
-
allModels: Map<string, ModelDef>;
|
|
543
|
-
mergeMap: Map<string, MergedOneToOneSide>;
|
|
544
|
-
}): string {
|
|
545
|
-
const { model, relations, allModels, mergeMap } = params;
|
|
546
|
-
const lines: string[] = [];
|
|
547
|
-
const modelName = model.__name;
|
|
548
|
-
lines.push(`model ${modelName} {`);
|
|
549
|
-
|
|
550
|
-
// Section: Columns
|
|
551
|
-
lines.push(" //> Columns");
|
|
552
|
-
for (const [name, column] of model.__columns) {
|
|
553
|
-
lines.push(formatColumn(name, column));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Section: Relations
|
|
557
|
-
lines.push("");
|
|
558
|
-
lines.push(" //> Relations");
|
|
559
|
-
|
|
560
|
-
const outputManyRelations = new Set<string>();
|
|
561
|
-
|
|
562
|
-
// Relations from the centralized relations map
|
|
563
|
-
const modelRelations = relations[modelName] ?? {};
|
|
564
|
-
for (const [name, relation] of Object.entries(modelRelations)) {
|
|
565
|
-
if (relation.__relationType === "one") {
|
|
566
|
-
lines.push(formatOneRelation({
|
|
567
|
-
name,
|
|
568
|
-
relation,
|
|
569
|
-
modelName,
|
|
570
|
-
model,
|
|
571
|
-
mergeMap,
|
|
572
|
-
}));
|
|
573
|
-
} else if (relation.__relationType === "many") {
|
|
574
|
-
lines.push(formatManyRelation(name, relation));
|
|
575
|
-
const key = `${relation.__targetModel}:${relation.__alias || ""}`;
|
|
576
|
-
outputManyRelations.add(key);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Auto-generate missing reverse relations (skips merged 1:1 pairs)
|
|
581
|
-
const missingReverses = findMissingReverseRelations({
|
|
582
|
-
modelName,
|
|
583
|
-
relations,
|
|
584
|
-
allModels,
|
|
585
|
-
mergeMap,
|
|
586
|
-
});
|
|
587
|
-
for (const reverse of missingReverses) {
|
|
588
|
-
const key = `${reverse.targetModel}:${reverse.alias || ""}`;
|
|
589
|
-
if (!outputManyRelations.has(key)) {
|
|
590
|
-
const relationAttr = reverse.alias ? ` @relation("${reverse.alias}")` : "";
|
|
591
|
-
lines.push(` ${reverse.name} ${reverse.targetModel}[]${relationAttr}`);
|
|
592
|
-
outputManyRelations.add(key);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Children (inline nested models)
|
|
597
|
-
for (const [name, childModel] of model.__children) {
|
|
598
|
-
lines.push(` ${name} ${childModel.__name}[]`);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Parent FK relations for child models
|
|
602
|
-
for (const [parentModelName, parentModel] of allModels) {
|
|
603
|
-
for (const [childKey, childModel] of parentModel.__children) {
|
|
604
|
-
if (childModel.__name === modelName) {
|
|
605
|
-
const parentFkName = `${parentModelName.charAt(0).toLowerCase()}${parentModelName.slice(1)}Id`;
|
|
606
|
-
const parentRelName = parentModelName;
|
|
607
|
-
const parentPkCol = parentModel.__primaryKey?.[0]
|
|
608
|
-
?? Array.from(parentModel.__columns.entries()).find(([, col]) => col.__columnType === "id")?.[0]
|
|
609
|
-
?? "id";
|
|
610
|
-
lines.push(
|
|
611
|
-
` ${parentRelName} ${parentModelName} @relation(fields: [${parentFkName}], references: [${parentPkCol}])`
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Section: Indexes
|
|
618
|
-
lines.push("");
|
|
619
|
-
lines.push(" //> Indexes");
|
|
620
|
-
lines.push("");
|
|
621
|
-
|
|
622
|
-
const allIndexes = new Map<string, { type: "index" | "unique"; indexType?: IndexType }>();
|
|
623
|
-
|
|
624
|
-
// Single-column indexes from column config
|
|
625
|
-
const singleIndexes = collectSingleColumnIndexes(model);
|
|
626
|
-
for (const colName of singleIndexes) {
|
|
627
|
-
allIndexes.set(colName, { type: "index" });
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Composite/explicit indexes from indexes callback
|
|
631
|
-
const compositeIndexes = evaluateIndexes(model);
|
|
632
|
-
for (const idx of compositeIndexes) {
|
|
633
|
-
const key = idx.__columns.join(",");
|
|
634
|
-
const existing = allIndexes.get(key);
|
|
635
|
-
if (!existing || idx.__type === "unique") {
|
|
636
|
-
allIndexes.set(key, { type: idx.__type, indexType: idx.__indexType });
|
|
637
|
-
} else if (idx.__indexType && !existing.indexType) {
|
|
638
|
-
allIndexes.set(key, { ...existing, indexType: idx.__indexType });
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Output deduplicated indexes
|
|
643
|
-
for (const [columns, { type, indexType }] of allIndexes) {
|
|
644
|
-
const colList = columns.split(",").join(", ");
|
|
645
|
-
const typeArg = indexType ? `, type: ${indexType}` : "";
|
|
646
|
-
if (type === "unique") {
|
|
647
|
-
lines.push(` @@unique([${colList}])`);
|
|
648
|
-
} else {
|
|
649
|
-
lines.push(` @@index([${colList}]${typeArg})`);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Handle @@id for compound primary keys
|
|
654
|
-
if (model.__primaryKey && model.__primaryKey.length > 0) {
|
|
655
|
-
const pkCols = model.__primaryKey.join(", ");
|
|
656
|
-
lines.push(` @@id([${pkCols}])`);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Handle @@map for table name mapping
|
|
660
|
-
if (model.__map) {
|
|
661
|
-
lines.push(` @@map("${model.__map}")`);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
lines.push("}");
|
|
665
|
-
return lines.join("\n");
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// ─── Enum Formatting ─────────────────────────────────
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Formats an enum to Prisma syntax
|
|
672
|
-
*/
|
|
673
|
-
function formatEnum(name: string, values: readonly string[]): string {
|
|
674
|
-
const lines: string[] = [];
|
|
675
|
-
lines.push(`enum ${name} {`);
|
|
676
|
-
for (const value of values) {
|
|
677
|
-
lines.push(` ${value}`);
|
|
678
|
-
}
|
|
679
|
-
lines.push("}");
|
|
680
|
-
return lines.join("\n");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// ─── Public API ─────────────────────────────────
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Generates a complete Prisma schema from a SchemaDef.
|
|
687
|
-
*
|
|
688
|
-
* This only generates models and enums. Developers should manage their own
|
|
689
|
-
* datasource and generator blocks in a separate base.prisma file.
|
|
690
|
-
*/
|
|
691
|
-
export function generatePrisma(schema: SchemaDef, config: PrismaGeneratorConfig = {}): string {
|
|
692
|
-
const sections: string[] = [];
|
|
693
|
-
|
|
694
|
-
// Pre-process: detect 1:1 relation pairs and build merge map
|
|
695
|
-
const mergeMap = detectOneToOnePairs({
|
|
696
|
-
relations: schema.__relations,
|
|
697
|
-
allModels: schema.__models,
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
// Enums
|
|
701
|
-
if (schema.__enums.size > 0) {
|
|
702
|
-
sections.push("// ============================================");
|
|
703
|
-
sections.push("// Enums");
|
|
704
|
-
sections.push("// ============================================");
|
|
705
|
-
sections.push("");
|
|
706
|
-
for (const [name, values] of schema.__enums) {
|
|
707
|
-
sections.push(formatEnum(name, values));
|
|
708
|
-
sections.push("");
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Models
|
|
713
|
-
sections.push("// ============================================");
|
|
714
|
-
sections.push("// Models");
|
|
715
|
-
sections.push("// ============================================");
|
|
716
|
-
sections.push("");
|
|
717
|
-
for (const [name, model] of schema.__models) {
|
|
718
|
-
sections.push(formatModel({
|
|
719
|
-
model,
|
|
720
|
-
relations: schema.__relations,
|
|
721
|
-
allModels: schema.__models,
|
|
722
|
-
mergeMap,
|
|
723
|
-
}));
|
|
724
|
-
sections.push("");
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
return sections.join("\n").trim();
|
|
728
|
-
}
|