@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.
Files changed (46) hide show
  1. package/README.md +62 -471
  2. package/dist/column.d.ts +150 -0
  3. package/dist/column.d.ts.map +1 -0
  4. package/dist/enum.d.ts +27 -0
  5. package/dist/enum.d.ts.map +1 -0
  6. package/dist/generator/index.d.ts +5 -0
  7. package/dist/generator/index.d.ts.map +1 -0
  8. package/dist/generator/prisma.d.ts +19 -0
  9. package/dist/generator/prisma.d.ts.map +1 -0
  10. package/dist/index.d.ts +116 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +1546 -0
  13. package/dist/index.js.map +22 -0
  14. package/dist/model.d.ts +44 -0
  15. package/dist/model.d.ts.map +1 -0
  16. package/dist/naming.d.ts +23 -0
  17. package/dist/naming.d.ts.map +1 -0
  18. package/dist/relation-plan.d.ts +101 -0
  19. package/dist/relation-plan.d.ts.map +1 -0
  20. package/dist/relation.d.ts +23 -0
  21. package/dist/relation.d.ts.map +1 -0
  22. package/dist/relations.d.ts +29 -0
  23. package/dist/relations.d.ts.map +1 -0
  24. package/dist/schema.d.ts +38 -0
  25. package/dist/schema.d.ts.map +1 -0
  26. package/dist/templates.d.ts +79 -0
  27. package/dist/templates.d.ts.map +1 -0
  28. package/dist/to-ir.d.ts +41 -0
  29. package/dist/to-ir.d.ts.map +1 -0
  30. package/dist/types.d.ts +286 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/validate.d.ts +22 -0
  33. package/dist/validate.d.ts.map +1 -0
  34. package/package.json +29 -26
  35. package/src/column.ts +0 -389
  36. package/src/enum.ts +0 -71
  37. package/src/generator/index.ts +0 -5
  38. package/src/generator/prisma.ts +0 -728
  39. package/src/index.ts +0 -247
  40. package/src/model.ts +0 -330
  41. package/src/relation.ts +0 -75
  42. package/src/relations.ts +0 -293
  43. package/src/schema.ts +0 -184
  44. package/src/templates.ts +0 -134
  45. package/src/types.ts +0 -381
  46. package/src/validate.ts +0 -141
@@ -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
- }