@vibeorm/dsl 2.0.0-alpha.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -137,8 +137,15 @@ class ChainableEnumColumn extends ChainableColumn {
137
137
  function string(config = {}) {
138
138
  return new ChainableStringColumn(config);
139
139
  }
140
+
141
+ class ChainableIntegerColumn extends ChainableColumn {
142
+ autoincrement() {
143
+ this.__config.autoincrement = true;
144
+ return this;
145
+ }
146
+ }
140
147
  function int(config = {}) {
141
- return new ChainableColumn("int", config);
148
+ return new ChainableIntegerColumn("int", config);
142
149
  }
143
150
  function float(config = {}) {
144
151
  return new ChainableColumn("float", config);
@@ -173,7 +180,7 @@ function bytes(config = {}) {
173
180
  return new ChainableColumn("bytes", config);
174
181
  }
175
182
  function bigint(config = {}) {
176
- return new ChainableColumn("bigint", config);
183
+ return new ChainableIntegerColumn("bigint", config);
177
184
  }
178
185
  function unsupported(dbType) {
179
186
  const config = { dbType };
@@ -217,7 +224,224 @@ function createEnum(config) {
217
224
  }
218
225
 
219
226
  // packages/dsl/src/generator/prisma.ts
220
- import { VibeError, encodePrismaString, internalError } from "@vibeorm/schema";
227
+ import { VibeError as VibeError5, encodePrismaString, internalError, providerToDialect as providerToDialect2 } from "@vibeorm/schema";
228
+ import { getSqlDialect as getSqlDialect2 } from "@vibeorm/sql";
229
+
230
+ // packages/dsl/src/predicate.ts
231
+ import { VibeError } from "@vibeorm/schema";
232
+ import { compileExclusion, renderPredicate } from "@vibeorm/sql";
233
+ function isFieldExpr(operand) {
234
+ return typeof operand === "object" && operand !== null && "kind" in operand && operand.kind === "field";
235
+ }
236
+ function toRef(params) {
237
+ return { kind: "field", name: params.expr.name };
238
+ }
239
+ function toValue(params) {
240
+ const { input, at } = params;
241
+ if (typeof input === "bigint")
242
+ return { kind: "value", value: input.toString() };
243
+ if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") {
244
+ return { kind: "value", value: input };
245
+ }
246
+ throw new VibeError({
247
+ code: "VIBE_SCHEMA_INVALID",
248
+ message: `${at}: ${input === null ? "null" : typeof input} is not a comparable literal \u2014 use isNull()/isNotNull() for null, and a string, number, bigint or boolean otherwise`,
249
+ meta: { rule: "predicate-literal-invalid", at }
250
+ });
251
+ }
252
+ function toOperand(params) {
253
+ const { operand, at } = params;
254
+ return isFieldExpr(operand) ? toRef({ expr: operand }) : toValue({ input: operand, at });
255
+ }
256
+ function requireFieldExpr(params) {
257
+ const { value, at } = params;
258
+ if (isFieldExpr(value))
259
+ return value;
260
+ throw new VibeError({
261
+ code: "VIBE_SCHEMA_INVALID",
262
+ message: `${at}: the left side of a comparison must be c.field("\u2026"), not a bare value`,
263
+ meta: { rule: "predicate-operand-invalid", at }
264
+ });
265
+ }
266
+ function requireOperands(params) {
267
+ const { operands, at } = params;
268
+ if (operands.length === 0) {
269
+ throw new VibeError({
270
+ code: "VIBE_SCHEMA_INVALID",
271
+ message: `${at}: an empty boolean group has no honest SQL spelling \u2014 pass at least one operand`,
272
+ meta: { rule: "predicate-empty", at }
273
+ });
274
+ }
275
+ return operands;
276
+ }
277
+ function createPredicateBuilder() {
278
+ const compare = (op) => (left, right) => ({
279
+ kind: "compare",
280
+ op,
281
+ left: toRef({ expr: requireFieldExpr({ value: left, at: `c.${op}()` }) }),
282
+ right: toOperand({ operand: right, at: `c.${op}()` })
283
+ });
284
+ const membership = (negate) => (field, values) => {
285
+ const at = negate ? "c.notInList()" : "c.inList()";
286
+ const ref = toRef({ expr: requireFieldExpr({ value: field, at }) });
287
+ if (values.length === 0) {
288
+ throw new VibeError({
289
+ code: "VIBE_SCHEMA_INVALID",
290
+ message: `${at}: an empty IN list is UNKNOWN on every row and is never what an author means`,
291
+ meta: { rule: "predicate-empty", at, field: ref.name }
292
+ });
293
+ }
294
+ return {
295
+ kind: "inList",
296
+ field: ref,
297
+ values: values.map((value) => toValue({ input: value, at })),
298
+ ...negate ? { negate: true } : {}
299
+ };
300
+ };
301
+ return {
302
+ field: (name) => ({ kind: "field", name, __kind: "String" }),
303
+ eq: compare("eq"),
304
+ ne: compare("ne"),
305
+ gt: compare("gt"),
306
+ gte: compare("gte"),
307
+ lt: compare("lt"),
308
+ lte: compare("lte"),
309
+ inList: membership(false),
310
+ notInList: membership(true),
311
+ isNull: (name) => ({ kind: "isNull", field: { kind: "field", name } }),
312
+ isNotNull: (name) => ({ kind: "isNull", field: { kind: "field", name }, negate: true }),
313
+ and: (...operands) => ({ kind: "and", operands: requireOperands({ operands, at: "c.and()" }) }),
314
+ or: (...operands) => ({ kind: "or", operands: requireOperands({ operands, at: "c.or()" }) }),
315
+ not: (operand) => ({ kind: "not", operand }),
316
+ named: (name, predicate) => {
317
+ if (name.trim() === "") {
318
+ throw new VibeError({
319
+ code: "VIBE_SCHEMA_INVALID",
320
+ message: "c.named(): a CHECK needs a non-empty name \u2014 the name is what keys the migration diff",
321
+ meta: { rule: "predicate-unnamed-check" }
322
+ });
323
+ }
324
+ return { __type: "check", name, predicate };
325
+ }
326
+ };
327
+ }
328
+ function evaluateChecks(params) {
329
+ const { checks, model } = params;
330
+ if (checks === undefined)
331
+ return [];
332
+ if (typeof checks !== "function") {
333
+ return checks.map((check) => ({ kind: "raw", name: check.name, expression: check.expression }));
334
+ }
335
+ const declared = checks(createPredicateBuilder());
336
+ return declared.map((entry, index) => {
337
+ if (entry === null || typeof entry !== "object" || entry.__type !== "check") {
338
+ throw new VibeError({
339
+ code: "VIBE_SCHEMA_INVALID",
340
+ message: `Model "${model}": checks[${index}] is not a named CHECK \u2014 wrap the expression in c.named("name", \u2026)`,
341
+ meta: { rule: "predicate-unnamed-check", model, index }
342
+ });
343
+ }
344
+ return { kind: "typed", name: entry.name, predicate: entry.predicate };
345
+ });
346
+ }
347
+ function compileExclusions(params) {
348
+ const specs = typeof params.exclusions === "function" ? params.exclusions(createPredicateBuilder()) : params.exclusions ?? [];
349
+ return specs.map((exclusion) => compileExclusion({ exclusion, model: params.model, dialect: params.dialect }));
350
+ }
351
+ function compileChecks(params) {
352
+ const { checks, model, dialect, enums } = params;
353
+ return evaluateChecks({ checks, model: model.name }).map((check) => check.kind === "raw" ? { name: check.name, expression: check.expression } : {
354
+ name: check.name,
355
+ expression: renderPredicate({
356
+ predicate: check.predicate,
357
+ model,
358
+ dialect,
359
+ enums,
360
+ usage: "check",
361
+ site: `CHECK "${check.name}"`
362
+ }),
363
+ predicate: check.predicate
364
+ });
365
+ }
366
+ function compileIndexPredicate(params) {
367
+ const { where, wherePredicate, columns, model, dialect, enums } = params;
368
+ if (where !== undefined && wherePredicate !== undefined) {
369
+ throw new VibeError({
370
+ code: "VIBE_SCHEMA_INVALID",
371
+ message: `Index on "${model.name}" [${columns.join(", ")}] carries both a raw where: string and a typed .where() \u2014 an index has ONE predicate`,
372
+ meta: { rule: "predicate-duplicate-where", model: model.name, columns: [...columns] }
373
+ });
374
+ }
375
+ if (where !== undefined)
376
+ return { where };
377
+ if (wherePredicate === undefined)
378
+ return;
379
+ return {
380
+ where: renderPredicate({
381
+ predicate: wherePredicate,
382
+ model,
383
+ dialect,
384
+ enums,
385
+ usage: "indexPredicate",
386
+ site: `partial index on [${columns.join(", ")}]`
387
+ }),
388
+ wherePredicate
389
+ };
390
+ }
391
+
392
+ // packages/dsl/src/to-ir.ts
393
+ import {
394
+ EMPTY_EXTENSION_REGISTRY,
395
+ VibeError as VibeError4,
396
+ assertValidSchemaIR,
397
+ exactIntegerLiteral,
398
+ providerToDialect,
399
+ scalarListDefault,
400
+ serializeJsonDefault,
401
+ mergeExtensionDeclarations
402
+ } from "@vibeorm/schema";
403
+ import { getSqlDialect } from "@vibeorm/sql";
404
+
405
+ // packages/dsl/src/field-type.ts
406
+ import { fieldTypeIdentity } from "@vibeorm/schema";
407
+ function columnFieldType(params) {
408
+ const { column } = params;
409
+ switch (column.__columnType) {
410
+ case "string":
411
+ return "String";
412
+ case "int":
413
+ return "Int";
414
+ case "bigint":
415
+ return "BigInt";
416
+ case "float":
417
+ return "Float";
418
+ case "decimal":
419
+ return "Decimal";
420
+ case "boolean":
421
+ return "Boolean";
422
+ case "datetime":
423
+ return "DateTime";
424
+ case "json":
425
+ return "Json";
426
+ case "bytes":
427
+ return "Bytes";
428
+ case "id":
429
+ return column.__config.type === "autoincrement" ? "Int" : "String";
430
+ case "enum":
431
+ return column.__enumName === undefined ? "String" : { enum: column.__enumName };
432
+ case "unsupported":
433
+ return "String";
434
+ }
435
+ }
436
+ function fieldTypeKey(params) {
437
+ return fieldTypeIdentity({ type: columnFieldType({ column: params.column }) });
438
+ }
439
+
440
+ // packages/dsl/src/relation-plan.ts
441
+ import { VibeError as VibeError3, predicateKey } from "@vibeorm/schema";
442
+
443
+ // packages/dsl/src/model.ts
444
+ import { VibeError as VibeError2 } from "@vibeorm/schema";
221
445
 
222
446
  // packages/dsl/src/naming.ts
223
447
  function capitalize(params) {
@@ -256,47 +480,67 @@ function isChildrenDef(value) {
256
480
  return typeof value === "object" && value !== null && "__type" in value && value.__type === "children";
257
481
  }
258
482
  function createIndexBuilder() {
259
- const createIndex = (columns, type, where) => ({
483
+ const partial = (index) => {
484
+ const where = (predicate) => {
485
+ if (index.__where !== undefined) {
486
+ throw new VibeError2({
487
+ code: "VIBE_SCHEMA_INVALID",
488
+ message: `Index on [${index.__columns.join(", ")}] already carries a raw where: predicate \u2014 an index has ONE predicate, so use either the raw string or .where(), not both`,
489
+ meta: { rule: "predicate-duplicate-where", columns: [...index.__columns] }
490
+ });
491
+ }
492
+ return { ...index, __wherePredicate: predicate(createPredicateBuilder()) };
493
+ };
494
+ return Object.defineProperty({ ...index }, "where", {
495
+ value: where,
496
+ enumerable: false,
497
+ writable: false,
498
+ configurable: true
499
+ });
500
+ };
501
+ const createIndex = (columns, type, where, map) => ({
260
502
  __type: "index",
261
503
  __columns: [...columns],
262
504
  __indexType: type,
505
+ ...map === undefined ? {} : { __map: map },
263
506
  ...where === undefined ? {} : { __where: where }
264
507
  });
265
508
  return {
266
509
  on(...args) {
267
510
  const [first, second] = args;
268
511
  if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
269
- return createIndex(first, second.type, second.where);
512
+ return partial(createIndex(first, second.type, second.where, second.map));
270
513
  }
271
- return createIndex(args);
514
+ return partial(createIndex(args));
272
515
  },
273
516
  index(...columns) {
274
- return createIndex(columns);
517
+ return partial(createIndex(columns));
275
518
  },
276
519
  unique(...args) {
277
520
  const [first, second] = args;
278
521
  if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
279
- return {
522
+ return partial({
280
523
  __type: "unique",
281
524
  __columns: [...first],
525
+ ...second.map === undefined ? {} : { __map: second.map },
282
526
  ...second.where === undefined ? {} : { __where: second.where },
283
527
  ...second.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
284
528
  ...second.name === undefined ? {} : { __clientName: second.name }
285
- };
529
+ });
286
530
  }
287
- return { __type: "unique", __columns: [...args] };
531
+ return partial({ __type: "unique", __columns: [...args] });
288
532
  },
289
533
  gin(...columns) {
290
- return createIndex(columns, "Gin");
534
+ return partial(createIndex(columns, "Gin"));
291
535
  },
292
536
  gist(...columns) {
293
- return createIndex(columns, "Gist");
537
+ return partial(createIndex(columns, "Gist"));
294
538
  },
295
539
  hash(...columns) {
296
- return createIndex(columns, "Hash");
540
+ return partial(createIndex(columns, "Hash"));
297
541
  },
298
542
  brin(...columns) {
299
- return createIndex(columns, "Brin");
543
+ return partial(createIndex(columns, "Brin"));
300
544
  }
301
545
  };
302
546
  }
@@ -338,6 +582,7 @@ function create(config) {
338
582
  __children: children2,
339
583
  __map: config.map,
340
584
  __primaryKey: config.primaryKey,
585
+ __exclusions: config.exclusions,
341
586
  __checks: config.checks,
342
587
  __extensions: config.extensions,
343
588
  __policy: config.policy,
@@ -362,6 +607,9 @@ function detectOneToOnePairs(params) {
362
607
  const allOnes = [];
363
608
  for (const [modelName, modelRelations] of Object.entries(relations)) {
364
609
  for (const [relName, rel] of Object.entries(modelRelations)) {
610
+ if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
611
+ throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
612
+ }
365
613
  if (rel.__relationType === "one") {
366
614
  allOnes.push({ modelName, relName, rel });
367
615
  }
@@ -382,12 +630,13 @@ function detectOneToOnePairs(params) {
382
630
  const keyB = `${b.modelName}:${b.relName}`;
383
631
  if (paired.has(keyB))
384
632
  continue;
385
- const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && a.rel.__fromColumn === b.rel.__toColumn && a.rel.__toModel === b.rel.__fromModel && a.rel.__toColumn === b.rel.__fromColumn;
386
- if (!sameColumnPair)
633
+ const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && sameColumnTuple({ left: a.rel.__fromColumns, right: b.rel.__toColumns }) && a.rel.__toModel === b.rel.__fromModel && sameColumnTuple({ left: a.rel.__toColumns, right: b.rel.__fromColumns });
634
+ if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
387
635
  continue;
388
636
  const { fkOwner, backRelation } = resolveFkOwner({ a, b, models });
389
637
  const onDelete = fkOwner.rel.__onDelete ?? backRelation.rel.__onDelete;
390
638
  const onUpdate = fkOwner.rel.__onUpdate ?? backRelation.rel.__onUpdate;
639
+ const constraintName = fkOwner.rel.__constraintName ?? backRelation.rel.__constraintName;
391
640
  const fkKey = `${fkOwner.modelName}:${fkOwner.relName}`;
392
641
  const backKey = `${backRelation.modelName}:${backRelation.relName}`;
393
642
  mergeMap.set(fkKey, {
@@ -395,14 +644,16 @@ function detectOneToOnePairs(params) {
395
644
  pairedRelName: backRelation.relName,
396
645
  pairedModelName: backRelation.modelName,
397
646
  onDelete,
398
- onUpdate
647
+ onUpdate,
648
+ ...constraintName === undefined ? {} : { constraintName }
399
649
  });
400
650
  mergeMap.set(backKey, {
401
651
  role: "backRelation",
402
652
  pairedRelName: fkOwner.relName,
403
653
  pairedModelName: fkOwner.modelName,
404
654
  onDelete,
405
- onUpdate
655
+ onUpdate,
656
+ ...constraintName === undefined ? {} : { constraintName }
406
657
  });
407
658
  paired.add(fkKey);
408
659
  paired.add(backKey);
@@ -411,23 +662,54 @@ function detectOneToOnePairs(params) {
411
662
  }
412
663
  return mergeMap;
413
664
  }
665
+ function sameColumnTuple(params) {
666
+ const { left, right } = params;
667
+ return left.length === right.length && left.every((name, index) => name === right[index]);
668
+ }
414
669
  function resolveFkOwner(params) {
415
670
  const { a, b, models } = params;
416
- const aFromIsId = models.get(a.modelName)?.__columns.get(a.rel.__fromColumn)?.__columnType === "id";
417
- const bFromIsId = models.get(b.modelName)?.__columns.get(b.rel.__fromColumn)?.__columnType === "id";
671
+ const ownershipError = () => {
672
+ throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `Ambiguous foreign-key ownership between ${a.modelName}.${a.relName} and ${b.modelName}.${b.relName}; set ownsForeignKey: true on the foreign-key side`, meta: { rule: "ambiguous-foreign-key-owner" } });
673
+ };
674
+ const aOwns = a.rel.__ownsForeignKey;
675
+ const bOwns = b.rel.__ownsForeignKey;
676
+ if (aOwns !== undefined || bOwns !== undefined) {
677
+ if (aOwns === bOwns)
678
+ return ownershipError();
679
+ return aOwns === true || bOwns === false ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
680
+ }
681
+ const aFromIsId = fromIsPrimaryKey({ model: models.get(a.modelName), columns: a.rel.__fromColumns });
682
+ const bFromIsId = fromIsPrimaryKey({ model: models.get(b.modelName), columns: b.rel.__fromColumns });
418
683
  if (aFromIsId && !bFromIsId)
419
684
  return { fkOwner: b, backRelation: a };
420
685
  if (!aFromIsId && bFromIsId)
421
686
  return { fkOwner: a, backRelation: b };
422
- if (a.rel.__onDelete !== undefined || a.rel.__onUpdate !== undefined) {
687
+ const aActions = a.rel.__onDelete !== undefined || a.rel.__onUpdate !== undefined;
688
+ const bActions = b.rel.__onDelete !== undefined || b.rel.__onUpdate !== undefined;
689
+ if (aActions && !bActions) {
423
690
  return { fkOwner: a, backRelation: b };
424
691
  }
425
- if (b.rel.__onDelete !== undefined || b.rel.__onUpdate !== undefined) {
692
+ if (bActions && !aActions) {
426
693
  return { fkOwner: b, backRelation: a };
427
694
  }
428
- const aLooksFk = looksLikeForeignKey({ column: a.rel.__fromColumn });
429
- const bLooksFk = looksLikeForeignKey({ column: b.rel.__fromColumn });
430
- return aLooksFk && !bLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
695
+ const aLooksFk = a.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
696
+ const bLooksFk = b.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
697
+ if (aLooksFk === bLooksFk)
698
+ return ownershipError();
699
+ return aLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
700
+ }
701
+ function fromIsPrimaryKey(params) {
702
+ const { model, columns } = params;
703
+ if (model === undefined)
704
+ return false;
705
+ if (columns.length > 1) {
706
+ const primaryKey = model.__primaryKey;
707
+ return primaryKey !== undefined && primaryKey.length === columns.length && primaryKey.every((name) => columns.includes(name));
708
+ }
709
+ const column = columns[0];
710
+ if (column === undefined)
711
+ return false;
712
+ return model.__columns.get(column)?.__columnType === "id" || model.__primaryKey?.includes(column) === true;
431
713
  }
432
714
  function looksLikeForeignKey(params) {
433
715
  const { column } = params;
@@ -453,19 +735,21 @@ function planExplicitRelations(params) {
453
735
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: true });
454
736
  continue;
455
737
  }
456
- const ownsFk = mergeInfo?.role === "fkOwner" || rel.__fromModel === modelName;
738
+ const ownsFk = mergeInfo?.role === "fkOwner" || rel.__ownsForeignKey !== false && rel.__fromModel === modelName;
457
739
  if (!ownsFk) {
458
740
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: rel.__optional });
459
741
  continue;
460
742
  }
461
- const fkColumn = model.__columns.get(rel.__fromColumn);
743
+ const anyMemberOptional = rel.__fromColumns.some((name2) => model.__columns.get(name2)?.__config.optional === true);
744
+ const constraintName = mergeInfo?.constraintName ?? rel.__constraintName;
462
745
  planned.push({
463
746
  ...base,
464
747
  kind: "fkOwner",
465
748
  isList: false,
466
- isOptional: fkColumn?.__config.optional === true || rel.__optional,
467
- fields: [rel.__fromColumn],
468
- references: [rel.__toColumn],
749
+ isOptional: anyMemberOptional || rel.__optional,
750
+ fields: [...rel.__fromColumns],
751
+ references: [...rel.__toColumns],
752
+ ...constraintName === undefined ? {} : { constraintName },
469
753
  onDelete: mergeInfo?.onDelete ?? rel.__onDelete,
470
754
  onUpdate: mergeInfo?.onUpdate ?? rel.__onUpdate
471
755
  });
@@ -562,7 +846,8 @@ function planIndexes(params) {
562
846
  }
563
847
  }
564
848
  for (const index of evaluateIndexes({ model })) {
565
- const key = index.__columns.join(",") + (index.__where === undefined ? "" : `|where:${index.__where}`);
849
+ const predicateSuffix = index.__where !== undefined ? `|where:${index.__where}` : index.__wherePredicate === undefined ? "" : `|where:${predicateKey({ predicate: index.__wherePredicate })}`;
850
+ const key = index.__columns.join(",") + predicateSuffix;
566
851
  const existing = byKey.get(key);
567
852
  const isUnique = index.__type === "unique";
568
853
  if (existing === undefined || isUnique) {
@@ -570,16 +855,91 @@ function planIndexes(params) {
570
855
  isUnique,
571
856
  columns: [...index.__columns],
572
857
  indexType: index.__indexType,
858
+ ...index.__map === undefined ? {} : { map: index.__map },
573
859
  ...index.__where === undefined ? {} : { where: index.__where },
860
+ ...index.__wherePredicate === undefined ? {} : { wherePredicate: index.__wherePredicate },
574
861
  ...index.__nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
575
862
  ...index.__clientName === undefined ? {} : { clientName: index.__clientName }
576
863
  });
864
+ } else if (index.__map !== undefined) {
865
+ byKey.set(key, { ...existing, map: index.__map, indexType: index.__indexType ?? existing.indexType });
577
866
  } else if (index.__indexType !== undefined && existing.indexType === undefined) {
578
867
  byKey.set(key, { ...existing, indexType: index.__indexType });
579
868
  }
580
869
  }
581
870
  return Array.from(byKey.values());
582
871
  }
872
+ function referenceableKeys(params) {
873
+ const { model } = params;
874
+ const keys = [];
875
+ for (const [name, column] of model.__columns) {
876
+ if (column.__columnType === "id" || column.__config.unique === true)
877
+ keys.push([name]);
878
+ }
879
+ if (model.__primaryKey !== undefined && model.__primaryKey.length > 0)
880
+ keys.push([...model.__primaryKey]);
881
+ for (const index of evaluateIndexes({ model })) {
882
+ if (index.__type === "unique" && index.__where === undefined && index.__wherePredicate === undefined) {
883
+ keys.push([...index.__columns]);
884
+ }
885
+ }
886
+ return keys;
887
+ }
888
+ function assertForeignKeyIntegrity(params) {
889
+ const { relation, models } = params;
890
+ const fields = relation.fields ?? [];
891
+ const references = relation.references ?? [];
892
+ const at = `${relation.model}.${relation.name}`;
893
+ const refuse = (message, rule, meta = {}) => {
894
+ throw new VibeError3({
895
+ code: "VIBE_SCHEMA_INVALID",
896
+ message: `Relation ${at}: ${message}`,
897
+ meta: { rule, model: relation.model, relation: relation.name, ...meta }
898
+ });
899
+ };
900
+ if (fields.length !== references.length) {
901
+ refuse(`pairs ${fields.length} foreign-key column(s) with ${references.length} referenced column(s)`, "relation-tuple-arity", { fields, references });
902
+ }
903
+ if (new Set(fields).size !== fields.length) {
904
+ refuse(`repeats a foreign-key column (${fields.join(", ")})`, "relation-duplicate-column", { fields });
905
+ }
906
+ if (new Set(references).size !== references.length) {
907
+ refuse(`repeats a referenced column (${references.join(", ")})`, "relation-duplicate-column", { references });
908
+ }
909
+ const owner = models.get(relation.model);
910
+ const target = models.get(relation.targetModel);
911
+ if (owner === undefined || target === undefined)
912
+ return;
913
+ fields.forEach((name, index) => {
914
+ const referenced = references[index];
915
+ const column = owner.__columns.get(name);
916
+ const refColumn = target.__columns.get(referenced);
917
+ if (column === undefined) {
918
+ refuse(`uses foreign-key column "${name}", which ${relation.model} does not declare`, "unknown-relation-field", { field: name });
919
+ return;
920
+ }
921
+ if (refColumn === undefined) {
922
+ refuse(`references "${relation.targetModel}.${referenced}", which does not exist`, "unknown-relation-reference", { reference: referenced });
923
+ return;
924
+ }
925
+ if (column.__config.array === true || refColumn.__config.array === true) {
926
+ refuse(`pairs a scalar list (${name} \u2194 ${referenced}) \u2014 a foreign key compares single values`, "relation-list-column", { field: name, reference: referenced });
927
+ }
928
+ if (column.__columnType === "json" || refColumn.__columnType === "json" || column.__columnType === "unsupported" || refColumn.__columnType === "unsupported") {
929
+ refuse(`pairs ${name} \u2194 ${referenced}, and Json/unsupported columns cannot be a foreign key`, "relation-unsupported-column", { field: name, reference: referenced });
930
+ }
931
+ const ownType = fieldTypeKey({ column });
932
+ const refType = fieldTypeKey({ column: refColumn });
933
+ if (ownType !== refType) {
934
+ refuse(`pairs ${relation.model}.${name} (${ownType}) with ${relation.targetModel}.${referenced} (${refType}) \u2014 both ends of a foreign key need the same scalar type`, "relation-type-mismatch", { field: name, reference: referenced, fieldType: ownType, referenceType: refType });
935
+ }
936
+ });
937
+ const keys = referenceableKeys({ model: target });
938
+ const matched = keys.some((key) => key.length === references.length && key.every((name) => references.includes(name)));
939
+ if (!matched) {
940
+ refuse(`references ${relation.targetModel}.(${references.join(", ")}), which is not a unique key there \u2014 declare those columns as the primary key, a unique column, or a total @@unique index`, "relation-reference-not-unique", { references, candidates: keys.map((key) => [...key]) });
941
+ }
942
+ }
583
943
  function planSchema(params) {
584
944
  const { schema } = params;
585
945
  const mergeMap = detectOneToOnePairs({
@@ -602,7 +962,12 @@ function planSchema(params) {
602
962
  existing: explicit
603
963
  });
604
964
  const children2 = planChildRelations({ modelName, model, models: schema.__models });
605
- relations.set(modelName, [...explicit, ...reverses, ...children2]);
965
+ const planned = [...explicit, ...reverses, ...children2];
966
+ for (const relation of planned) {
967
+ if (relation.kind === "fkOwner")
968
+ assertForeignKeyIntegrity({ relation, models: schema.__models });
969
+ }
970
+ relations.set(modelName, planned);
606
971
  indexes.set(modelName, planIndexes({ model }));
607
972
  }
608
973
  return { relations, indexes };
@@ -627,142 +992,454 @@ function findCounterparts(params) {
627
992
  return (plan.relations.get(relation.targetModel) ?? []).filter((candidate) => candidate !== relation && candidate.targetModel === relation.model && (candidate.relationName ?? "") === nameKey);
628
993
  }
629
994
 
630
- // packages/dsl/src/generator/prisma.ts
631
- function prismaType(params) {
632
- const { column } = params;
633
- const list = column.__config.array === true ? "[]" : "";
634
- switch (column.__columnType) {
635
- case "string":
636
- return `String${list}`;
637
- case "int":
638
- return `Int${list}`;
639
- case "bigint":
640
- return `BigInt${list}`;
641
- case "float":
642
- return `Float${list}`;
643
- case "decimal":
644
- return `Decimal${list}`;
645
- case "boolean":
646
- return `Boolean${list}`;
647
- case "datetime":
648
- return `DateTime${list}`;
649
- case "json":
650
- return `Json${list}`;
651
- case "bytes":
652
- return `Bytes${list}`;
653
- case "id":
654
- return column.__config.type === "autoincrement" ? "Int" : "String";
655
- case "enum":
656
- return column.__enumName === undefined ? `String${list}` : `${column.__enumName}${list}`;
657
- case "unsupported": {
658
- const extensionTypeName = column.__config["extensionType"];
659
- if (typeof extensionTypeName === "string") {
660
- const rawArgs = column.__config["extensionArgs"];
661
- const args = Array.isArray(rawArgs) ? rawArgs.map(String).join(", ") : "";
662
- return `${extensionTypeName}${args.length > 0 ? `(${args})` : ""}${list}`;
663
- }
664
- return `Unsupported(${encodePrismaString({ value: String(column.__config.dbType) })})${list}`;
665
- }
666
- }
667
- }
668
- function idDefaultAttribute(params) {
669
- const idType = params.column.__config.type;
670
- switch (idType) {
995
+ // packages/dsl/src/to-ir.ts
996
+ var fieldType = columnFieldType;
997
+ function idDefault(params) {
998
+ switch (params.column.__config.type) {
671
999
  case "ulid":
1000
+ return { kind: "ulid" };
672
1001
  case "cuid":
1002
+ return { kind: "cuid" };
673
1003
  case "uuid":
1004
+ return { kind: "uuid" };
674
1005
  case "nanoid":
1006
+ return { kind: "nanoid" };
675
1007
  case "autoincrement":
676
- return `@default(${idType}())`;
1008
+ return { kind: "autoincrement" };
677
1009
  default:
678
1010
  return;
679
1011
  }
680
1012
  }
681
- function defaultAttribute(params) {
1013
+ function fieldDefault(params) {
682
1014
  const { column } = params;
683
- const { dbgenerated, default: value } = column.__config;
684
- if (typeof dbgenerated === "string") {
685
- return `@default(dbgenerated(${encodePrismaString({ value: dbgenerated })}))`;
1015
+ if (column.__columnType === "id") {
1016
+ if (column.__config.default !== undefined || column.__config.dbgenerated !== undefined || column.__config.autoincrement !== undefined)
1017
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
1018
+ return idDefault({ column });
1019
+ }
1020
+ const { dbgenerated, default: value, autoincrement } = column.__config;
1021
+ if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
1022
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
1023
+ }
1024
+ if (autoincrement === true) {
1025
+ if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
1026
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
1027
+ }
1028
+ return { kind: "autoincrement" };
686
1029
  }
1030
+ if (typeof dbgenerated === "string")
1031
+ return { kind: "dbgenerated", sql: dbgenerated };
687
1032
  if (value === undefined)
688
1033
  return;
689
- if (column.__columnType === "datetime" && value === "now") {
690
- return "@default(now())";
1034
+ if (column.__config.array === true) {
1035
+ if (!Array.isArray(value))
1036
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
1037
+ return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
691
1038
  }
1039
+ if (column.__columnType === "json" && typeof value !== "string") {
1040
+ return { kind: "literal", value: serializeJsonDefault({ value }) };
1041
+ }
1042
+ if (column.__columnType === "datetime" && value === "now")
1043
+ return { kind: "now" };
692
1044
  if (column.__columnType === "enum" && typeof value === "string") {
693
- return `@default(${value})`;
1045
+ return { kind: "enumValue", value };
1046
+ }
1047
+ if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
1048
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
1049
+ }
1050
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
1051
+ return { kind: "literal", value };
694
1052
  }
695
- if (typeof value === "string")
696
- return `@default(${encodePrismaString({ value })})`;
697
- if (typeof value === "boolean" || typeof value === "number")
698
- return `@default(${value})`;
699
1053
  if (typeof value === "bigint")
700
- return `@default(${value.toString()})`;
701
- return;
1054
+ return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
1055
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
702
1056
  }
703
- function nativeTypeAttribute(params) {
1057
+ function nativeType(params) {
704
1058
  const { column } = params;
705
- if (column.__columnType === "string") {
1059
+ if (column.__columnType === "enum") {
1060
+ const { maxLength, text } = column.__config;
1061
+ if (maxLength !== undefined || text !== undefined) {
1062
+ const invalid = column.__enumStorage !== "string" || maxLength !== undefined && (typeof maxLength !== "number" || !Number.isSafeInteger(maxLength) || maxLength <= 0) || text !== undefined && typeof text !== "boolean" || text === true && maxLength !== undefined;
1063
+ if (invalid) {
1064
+ throw new VibeError4({
1065
+ code: "VIBE_SCHEMA_INVALID",
1066
+ message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
1067
+ meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
1068
+ });
1069
+ }
1070
+ }
1071
+ }
1072
+ if (column.__columnType === "string" || column.__columnType === "enum" && column.__enumStorage === "string") {
706
1073
  const { maxLength, text } = column.__config;
707
1074
  if (typeof maxLength === "number")
708
- return `@db.VarChar(${maxLength})`;
1075
+ return `VarChar(${maxLength})`;
709
1076
  if (text === true)
710
- return "@db.Text";
1077
+ return "Text";
711
1078
  return;
712
1079
  }
713
1080
  if (column.__columnType === "decimal") {
714
1081
  const { precision, scale } = column.__config;
715
1082
  if (typeof precision === "number")
716
- return `@db.Decimal(${precision}, ${scale ?? 0})`;
1083
+ return `Decimal(${precision}, ${scale ?? 0})`;
717
1084
  return;
718
1085
  }
719
1086
  if (column.__columnType === "datetime" && column.__config.tz === true) {
720
- return "@db.Timestamptz";
721
- }
722
- return;
723
- }
724
- function formatColumn(params) {
725
- const { name, column } = params;
726
- const parts = [` ${name} ${prismaType({ column })}${column.__config.optional === true ? "?" : ""}`];
727
- if (column.__columnType === "id") {
728
- parts.push("@id");
729
- const idDefault = idDefaultAttribute({ column });
730
- if (idDefault !== undefined)
731
- parts.push(idDefault);
732
- } else {
733
- const columnDefault = defaultAttribute({ column });
734
- if (columnDefault !== undefined)
735
- parts.push(columnDefault);
736
- }
737
- if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
738
- parts.push("@updatedAt");
739
- }
740
- if (column.__config.unique === true) {
741
- parts.push("@unique");
742
- }
743
- if (typeof column.__config.map === "string") {
744
- parts.push(`@map(${encodePrismaString({ value: column.__config.map })})`);
745
- }
746
- const nativeType = nativeTypeAttribute({ column });
747
- if (nativeType !== undefined)
748
- parts.push(nativeType);
749
- if (typeof column.__config.collation === "string") {
750
- parts.push(`@collation(${encodePrismaString({ value: column.__config.collation })})`);
1087
+ return "Timestamptz";
751
1088
  }
752
- const columnLine = parts.join(" ");
753
- const comment = column.__config.comment;
754
- if (typeof comment === "string") {
755
- const commentLines = comment.split(`
756
- `).map((line) => ` /// ${line}`);
757
- return [...commentLines, columnLine].join(`
758
- `);
1089
+ if (column.__columnType === "unsupported") {
1090
+ return String(column.__config.dbType);
759
1091
  }
760
- return columnLine;
1092
+ return;
761
1093
  }
762
- function formatRelation(params) {
763
- const { relation } = params;
764
- const { name, targetModel, relationName } = relation;
765
- if (relation.kind === "list") {
1094
+ function buildField(params) {
1095
+ const { name, column, extensionNativeType } = params;
1096
+ const config = column.__config;
1097
+ const isList = column.__columnType !== "id" && config.array === true;
1098
+ const dbName = typeof config.map === "string" ? config.map : undefined;
1099
+ const documentation = typeof config.comment === "string" ? config.comment : undefined;
1100
+ const collation = typeof config.collation === "string" ? config.collation : undefined;
1101
+ const defaultValue = fieldDefault({ column });
1102
+ const native = extensionNativeType ?? nativeType({ column });
1103
+ return {
1104
+ name,
1105
+ type: fieldType({ column }),
1106
+ isList,
1107
+ isOptional: config.optional === true,
1108
+ isId: column.__columnType === "id",
1109
+ isUnique: config.unique === true,
1110
+ isUpdatedAt: column.__columnType === "datetime" && config.updatedAt === true,
1111
+ ...collation === undefined ? {} : { collation },
1112
+ ...dbName === undefined ? {} : { dbName },
1113
+ ...defaultValue === undefined ? {} : { default: defaultValue },
1114
+ ...native === undefined ? {} : { nativeType: native },
1115
+ ...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
1116
+ ...documentation === undefined ? {} : { documentation }
1117
+ };
1118
+ }
1119
+ function buildRelation(params) {
1120
+ const { relation, plan } = params;
1121
+ return {
1122
+ name: relation.name,
1123
+ model: relation.model,
1124
+ targetModel: relation.targetModel,
1125
+ type: resolveRelationType({ relation, plan }),
1126
+ isList: relation.isList,
1127
+ isOptional: relation.isOptional,
1128
+ ...relation.relationName === undefined ? {} : { relationName: relation.relationName },
1129
+ ...relation.constraintName === undefined ? {} : { constraintName: relation.constraintName },
1130
+ ...relation.fields === undefined ? {} : { fields: [...relation.fields] },
1131
+ ...relation.references === undefined ? {} : { references: [...relation.references] },
1132
+ ...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
1133
+ ...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
1134
+ };
1135
+ }
1136
+ var INDEX_KINDS = {
1137
+ BTree: "btree",
1138
+ Hash: "hash",
1139
+ Gin: "gin",
1140
+ Gist: "gist",
1141
+ SpGist: "spgist",
1142
+ Brin: "brin"
1143
+ };
1144
+ function buildIndex(params) {
1145
+ const { index, model, dialect, enums } = params;
1146
+ const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
1147
+ const predicate = compileIndexPredicate({
1148
+ where: index.where,
1149
+ wherePredicate: index.wherePredicate,
1150
+ columns: index.columns,
1151
+ model,
1152
+ dialect,
1153
+ enums
1154
+ });
1155
+ return {
1156
+ kind,
1157
+ fields: index.columns.map((name) => ({ name })),
1158
+ ...index.map === undefined ? {} : { name: index.map },
1159
+ ...predicate === undefined ? {} : { where: predicate.where },
1160
+ ...predicate?.wherePredicate === undefined ? {} : { wherePredicate: predicate.wherePredicate },
1161
+ ...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
1162
+ ...index.clientName === undefined ? {} : { clientName: index.clientName }
1163
+ };
1164
+ }
1165
+ function buildModel(params) {
1166
+ const { model, plan, extensions, declarations, issues, dialect, enums } = params;
1167
+ const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
1168
+ const policy = model.__policy === undefined ? undefined : { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field };
1169
+ const fields = [];
1170
+ for (const [name, column] of model.__columns) {
1171
+ const extensionTypeName = column.__config["extensionType"];
1172
+ if (typeof extensionTypeName !== "string") {
1173
+ fields.push(buildField({ name, column }));
1174
+ continue;
1175
+ }
1176
+ const registered = extensions.fieldTypes.get(extensionTypeName);
1177
+ if (registered === undefined) {
1178
+ issues.push({
1179
+ rule: "extension-attribute",
1180
+ message: `Field "${model.__name}.${name}" uses extension type "${extensionTypeName}" but no configured extension provides it`,
1181
+ model: model.__name,
1182
+ field: name
1183
+ });
1184
+ continue;
1185
+ }
1186
+ const rawArgs = column.__config["extensionArgs"];
1187
+ const lowered = registered.fieldType.lower({
1188
+ model: model.__name,
1189
+ field: name,
1190
+ args: Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string" || typeof entry === "number") : []
1191
+ });
1192
+ if ("error" in lowered) {
1193
+ issues.push({
1194
+ rule: "extension-attribute",
1195
+ message: `Field "${model.__name}.${name}": ${lowered.error}`,
1196
+ model: model.__name,
1197
+ field: name
1198
+ });
1199
+ continue;
1200
+ }
1201
+ fields.push(buildField({ name, column, extensionNativeType: lowered.nativeType }));
1202
+ if (lowered.declaration !== undefined) {
1203
+ declarations.add({
1204
+ extension: registered.extension.name,
1205
+ model: model.__name,
1206
+ declaration: lowered.declaration
1207
+ });
1208
+ }
1209
+ }
1210
+ for (const use of model.__extensions ?? []) {
1211
+ const registered = extensions.modelAttributes.get(use.attribute);
1212
+ if (registered === undefined) {
1213
+ issues.push({
1214
+ rule: "unknown-attribute",
1215
+ message: `Unknown block attribute "@@${use.attribute}" on model "${model.__name}" \u2014 no extension provides it`,
1216
+ model: model.__name
1217
+ });
1218
+ continue;
1219
+ }
1220
+ const parsed = registered.attribute.parse({
1221
+ model: model.__name,
1222
+ args: {
1223
+ named: { ...use.args ?? {} },
1224
+ ...use.fields === undefined ? {} : { fields: use.fields }
1225
+ },
1226
+ fieldNames: fields.map((field) => field.name)
1227
+ });
1228
+ if ("error" in parsed) {
1229
+ issues.push({
1230
+ rule: "extension-attribute",
1231
+ message: `@@${use.attribute} on "${model.__name}": ${parsed.error}`,
1232
+ model: model.__name
1233
+ });
1234
+ continue;
1235
+ }
1236
+ declarations.add({
1237
+ extension: registered.extension.name,
1238
+ model: model.__name,
1239
+ declaration: parsed.declaration
1240
+ });
1241
+ }
1242
+ const predicateModel = { name: model.__name, fields };
1243
+ const exclusions = compileExclusions({ exclusions: model.__exclusions, model: predicateModel, dialect });
1244
+ const checks = compileChecks({ checks: model.__checks, model: predicateModel, dialect, enums });
1245
+ return {
1246
+ name: model.__name,
1247
+ ...model.__map === undefined ? {} : { dbName: model.__map },
1248
+ fields,
1249
+ relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
1250
+ indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index, model: predicateModel, dialect, enums })),
1251
+ ...exclusions.length === 0 ? {} : { exclusions },
1252
+ ...checks.length === 0 ? {} : { checks },
1253
+ ...policy === undefined ? {} : { policy },
1254
+ ...primaryKey === undefined ? {} : { primaryKey },
1255
+ isView: false,
1256
+ ...model.__comment === undefined ? {} : { documentation: model.__comment }
1257
+ };
1258
+ }
1259
+
1260
+ class ExtensionDeclarationCollector {
1261
+ byExtension = new Map;
1262
+ add(params) {
1263
+ const perModel = this.byExtension.get(params.extension) ?? new Map;
1264
+ this.byExtension.set(params.extension, perModel);
1265
+ const existing = perModel.get(params.model) ?? {};
1266
+ perModel.set(params.model, mergeExtensionDeclarations({ base: existing, next: params.declaration }));
1267
+ }
1268
+ toIR() {
1269
+ return [...this.byExtension.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, perModel]) => ({
1270
+ name,
1271
+ options: {
1272
+ models: Object.fromEntries([...perModel.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
1273
+ }
1274
+ }));
1275
+ }
1276
+ }
1277
+ function toIR(params) {
1278
+ const { schema, provider = "postgresql" } = params;
1279
+ const extensions = params.extensions ?? EMPTY_EXTENSION_REGISTRY;
1280
+ const plan = planSchema({ schema });
1281
+ const declarations = new ExtensionDeclarationCollector;
1282
+ const issues = [];
1283
+ const enums = Array.from(schema.__enums, ([name, values]) => ({
1284
+ name,
1285
+ values: [...values],
1286
+ ...schema.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
1287
+ }));
1288
+ const dialect = getSqlDialect({ dialect: providerToDialect({ provider }) });
1289
+ const models = Array.from(schema.__models.values(), (model) => {
1290
+ const built = buildModel({ model, plan, extensions, declarations, issues, dialect, enums });
1291
+ const rls = schema.__rls?.policies[model.__name];
1292
+ return rls === undefined ? built : { ...built, rls };
1293
+ });
1294
+ if (schema.__rls !== undefined) {
1295
+ for (const name of Object.keys(schema.__rls.policies)) {
1296
+ if (!schema.__models.has(name)) {
1297
+ issues.push({ rule: "rls-unknown-model", model: name, message: `RLS classification references model "${name}" absent from the composed schema` });
1298
+ }
1299
+ }
1300
+ }
1301
+ if (issues.length > 0) {
1302
+ throw new VibeError4({
1303
+ code: "VIBE_SCHEMA_INVALID",
1304
+ message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
1305
+ meta: { issues }
1306
+ });
1307
+ }
1308
+ const ir = {
1309
+ datasource: { provider },
1310
+ models,
1311
+ enums,
1312
+ extensions: declarations.toIR(),
1313
+ ...schema.__rls === undefined ? {} : { rls: { context: schema.__rls.context } }
1314
+ };
1315
+ assertValidSchemaIR({ schema: ir });
1316
+ return ir;
1317
+ }
1318
+
1319
+ // packages/dsl/src/generator/prisma.ts
1320
+ function predicateModel(params) {
1321
+ return {
1322
+ name: params.model.__name,
1323
+ fields: Array.from(params.model.__columns, ([name, column]) => buildField({ name, column }))
1324
+ };
1325
+ }
1326
+ function prismaType(params) {
1327
+ const { column } = params;
1328
+ const list = column.__config.array === true ? "[]" : "";
1329
+ switch (column.__columnType) {
1330
+ case "string":
1331
+ return `String${list}`;
1332
+ case "int":
1333
+ return `Int${list}`;
1334
+ case "bigint":
1335
+ return `BigInt${list}`;
1336
+ case "float":
1337
+ return `Float${list}`;
1338
+ case "decimal":
1339
+ return `Decimal${list}`;
1340
+ case "boolean":
1341
+ return `Boolean${list}`;
1342
+ case "datetime":
1343
+ return `DateTime${list}`;
1344
+ case "json":
1345
+ return `Json${list}`;
1346
+ case "bytes":
1347
+ return `Bytes${list}`;
1348
+ case "id":
1349
+ return column.__config.type === "autoincrement" ? "Int" : "String";
1350
+ case "enum":
1351
+ return column.__enumName === undefined ? `String${list}` : `${column.__enumName}${list}`;
1352
+ case "unsupported": {
1353
+ const extensionTypeName = column.__config["extensionType"];
1354
+ if (typeof extensionTypeName === "string") {
1355
+ const rawArgs = column.__config["extensionArgs"];
1356
+ const args = Array.isArray(rawArgs) ? rawArgs.map(String).join(", ") : "";
1357
+ return `${extensionTypeName}${args.length > 0 ? `(${args})` : ""}${list}`;
1358
+ }
1359
+ return `Unsupported(${encodePrismaString({ value: String(column.__config.dbType) })})${list}`;
1360
+ }
1361
+ }
1362
+ }
1363
+ function defaultAttribute(params) {
1364
+ const { column } = params;
1365
+ const value = fieldDefault({ column });
1366
+ if (value === undefined)
1367
+ return;
1368
+ if (value.kind === "list") {
1369
+ const values = value.values.map((entry) => column.__columnType === "enum" ? String(entry) : column.__columnType === "bigint" ? String(entry) : JSON.stringify(entry));
1370
+ return `@default([${values.join(", ")}])`;
1371
+ }
1372
+ if (value.kind === "literal")
1373
+ return `@default(${typeof value.value === "string" ? encodePrismaString({ value: value.value }) : String(value.value)})`;
1374
+ if (value.kind === "enumValue")
1375
+ return `@default(${value.value})`;
1376
+ if (value.kind === "dbgenerated")
1377
+ return `@default(dbgenerated(${encodePrismaString({ value: value.sql })}))`;
1378
+ return `@default(${value.kind}())`;
1379
+ }
1380
+ function nativeTypeAttribute(params) {
1381
+ const { column } = params;
1382
+ if (column.__columnType === "string") {
1383
+ const { maxLength, text } = column.__config;
1384
+ if (typeof maxLength === "number")
1385
+ return `@db.VarChar(${maxLength})`;
1386
+ if (text === true)
1387
+ return "@db.Text";
1388
+ return;
1389
+ }
1390
+ if (column.__columnType === "decimal") {
1391
+ const { precision, scale } = column.__config;
1392
+ if (typeof precision === "number")
1393
+ return `@db.Decimal(${precision}, ${scale ?? 0})`;
1394
+ return;
1395
+ }
1396
+ if (column.__columnType === "datetime" && column.__config.tz === true) {
1397
+ return "@db.Timestamptz";
1398
+ }
1399
+ return;
1400
+ }
1401
+ function formatColumn(params) {
1402
+ const { name, column } = params;
1403
+ const parts = [` ${name} ${prismaType({ column })}${column.__config.optional === true ? "?" : ""}`];
1404
+ if (column.__columnType === "id") {
1405
+ parts.push("@id");
1406
+ const idDefault2 = defaultAttribute({ column });
1407
+ if (idDefault2 !== undefined)
1408
+ parts.push(idDefault2);
1409
+ } else {
1410
+ const columnDefault = defaultAttribute({ column });
1411
+ if (columnDefault !== undefined)
1412
+ parts.push(columnDefault);
1413
+ }
1414
+ if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
1415
+ parts.push("@updatedAt");
1416
+ }
1417
+ if (column.__config.unique === true) {
1418
+ parts.push("@unique");
1419
+ }
1420
+ if (typeof column.__config.map === "string") {
1421
+ parts.push(`@map(${encodePrismaString({ value: column.__config.map })})`);
1422
+ }
1423
+ const nativeType2 = nativeTypeAttribute({ column });
1424
+ if (nativeType2 !== undefined)
1425
+ parts.push(nativeType2);
1426
+ if (typeof column.__config.collation === "string") {
1427
+ parts.push(`@collation(${encodePrismaString({ value: column.__config.collation })})`);
1428
+ }
1429
+ const columnLine = parts.join(" ");
1430
+ const comment = column.__config.comment;
1431
+ if (typeof comment === "string") {
1432
+ const commentLines = comment.split(`
1433
+ `).map((line) => ` /// ${line}`);
1434
+ return [...commentLines, columnLine].join(`
1435
+ `);
1436
+ }
1437
+ return columnLine;
1438
+ }
1439
+ function formatRelation(params) {
1440
+ const { relation } = params;
1441
+ const { name, targetModel, relationName } = relation;
1442
+ if (relation.kind === "list") {
766
1443
  const attribute = relationName === undefined ? "" : ` @relation(${encodePrismaString({ value: relationName })})`;
767
1444
  return ` ${name} ${targetModel}[]${attribute}`;
768
1445
  }
@@ -786,19 +1463,31 @@ function formatRelation(params) {
786
1463
  args.push(`onDelete: ${relation.onDelete}`);
787
1464
  if (relation.onUpdate !== undefined)
788
1465
  args.push(`onUpdate: ${relation.onUpdate}`);
1466
+ if (relation.constraintName !== undefined) {
1467
+ args.push(`map: ${encodePrismaString({ value: relation.constraintName })}`);
1468
+ }
789
1469
  return ` ${name} ${targetModel}${optional} @relation(${args.join(", ")})`;
790
1470
  }
791
1471
  function formatIndex(params) {
792
1472
  const { index } = params;
793
1473
  const columns = index.columns.join(", ");
794
- const whereArg = index.where === undefined ? "" : `, where: ${encodePrismaString({ value: index.where })}`;
1474
+ const predicate = compileIndexPredicate({
1475
+ where: index.where,
1476
+ wherePredicate: index.wherePredicate,
1477
+ columns: index.columns,
1478
+ model: predicateModel({ model: params.model }),
1479
+ dialect: params.dialect,
1480
+ enums: params.enums
1481
+ });
1482
+ const whereArg = predicate === undefined ? "" : `, where: ${encodePrismaString({ value: predicate.where })}`;
1483
+ const mapArg = index.map === undefined ? "" : `, map: ${encodePrismaString({ value: index.map })}`;
795
1484
  const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
796
1485
  const nameArg = index.clientName === undefined ? "" : `, name: ${encodePrismaString({ value: index.clientName })}`;
797
1486
  if (index.isUnique) {
798
- return ` @@unique([${columns}]${nameArg}${nndArg}${whereArg})`;
1487
+ return ` @@unique([${columns}]${nameArg}${mapArg}${nndArg}${whereArg})`;
799
1488
  }
800
1489
  const typeArg = index.indexType === undefined ? "" : `, type: ${index.indexType}`;
801
- return ` @@index([${columns}]${typeArg}${whereArg})`;
1490
+ return ` @@index([${columns}]${typeArg}${mapArg}${whereArg})`;
802
1491
  }
803
1492
  function formatExtensionValue(params) {
804
1493
  const { value } = params;
@@ -829,9 +1518,14 @@ function formatModel(params) {
829
1518
  lines.push(" //> Indexes");
830
1519
  lines.push("");
831
1520
  for (const index of plan.indexes.get(model.__name) ?? []) {
832
- lines.push(formatIndex({ index }));
833
- }
834
- for (const check of model.__checks ?? []) {
1521
+ lines.push(formatIndex({ index, model: params.model, dialect: params.dialect, enums: params.enums }));
1522
+ }
1523
+ for (const check of compileChecks({
1524
+ checks: model.__checks,
1525
+ model: predicateModel({ model: params.model }),
1526
+ dialect: params.dialect,
1527
+ enums: params.enums
1528
+ })) {
835
1529
  lines.push(` @@check(${encodePrismaString({ value: check.expression })}, name: ${encodePrismaString({ value: check.name })})`);
836
1530
  }
837
1531
  for (const use of model.__extensions ?? []) {
@@ -863,17 +1557,22 @@ function formatEnum(params) {
863
1557
  return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
864
1558
  `);
865
1559
  }
866
- function generatePrisma(schema, _config = {}) {
1560
+ function generatePrisma(schema, config = {}) {
1561
+ for (const model of schema.__models.values()) {
1562
+ const exclusions = compileExclusions({ exclusions: model.__exclusions, model: { name: model.__name, fields: Array.from(model.__columns, ([name, column]) => buildField({ name, column })) }, dialect: getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) }) });
1563
+ if (exclusions.length > 0)
1564
+ throw new VibeError5({ code: "VIBE_CONFIG", message: "Prisma output cannot preserve exclusion constraints; keep the DSL source", meta: { feature: "exclusionConstraints", target: "prisma" } });
1565
+ }
867
1566
  const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
868
1567
  if (stringEnums.length > 0) {
869
- throw new VibeError({
1568
+ throw new VibeError5({
870
1569
  code: "VIBE_CONFIG",
871
1570
  message: `Cannot print to Prisma: enum(s) ${stringEnums.join(", ")} use storage: "string", which .prisma cannot express losslessly. Keep the DSL as the source or switch the enum to database storage.`,
872
1571
  meta: { enums: stringEnums, target: "prisma" }
873
1572
  });
874
1573
  }
875
1574
  if (schema.__rls !== undefined) {
876
- throw new VibeError({
1575
+ throw new VibeError5({
877
1576
  code: "VIBE_CONFIG",
878
1577
  message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
879
1578
  meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
@@ -881,6 +1580,8 @@ function generatePrisma(schema, _config = {}) {
881
1580
  }
882
1581
  const plan = planSchema({ schema });
883
1582
  const sections = [];
1583
+ const dialect = getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) });
1584
+ const enums = Array.from(schema.__enums, ([name, values]) => ({ name, values: [...values] }));
884
1585
  if (schema.__enums.size > 0) {
885
1586
  sections.push("// ============================================");
886
1587
  sections.push("// Enums");
@@ -896,724 +1597,464 @@ function generatePrisma(schema, _config = {}) {
896
1597
  sections.push("// ============================================");
897
1598
  sections.push("");
898
1599
  for (const model of schema.__models.values()) {
899
- sections.push(formatModel({ model, plan }));
1600
+ sections.push(formatModel({ model, plan, dialect, enums }));
900
1601
  sections.push("");
901
1602
  }
902
1603
  return sections.join(`
903
1604
  `).trim();
904
1605
  }
905
-
906
- // packages/dsl/src/relations.ts
907
- function createColumnRef(params) {
908
- const { model, column } = params;
909
- return {
910
- __type: "columnRef",
911
- __model: model,
912
- __column: column,
913
- through(junction) {
914
- return {
915
- __type: "throughRef",
916
- __model: model,
917
- __column: column,
918
- __junction: junction.__model,
919
- __junctionColumn: junction.__column
920
- };
921
- }
922
- };
923
- }
924
- function extractRefInfo(params) {
925
- const { ref } = params;
926
- return { model: ref.__model, column: ref.__column };
927
- }
928
- function createRelationFactory(params) {
929
- const { relationType, targetModelName } = params;
930
- return (config) => {
931
- const from = extractRefInfo({ ref: config.from });
932
- const to = extractRefInfo({ ref: config.to });
933
- let throughJunction;
934
- let throughFromCol;
935
- let throughToCol;
936
- if (config.from.__type === "throughRef") {
937
- throughJunction = config.from.__junction;
938
- throughFromCol = config.from.__junctionColumn;
939
- }
940
- if (config.to.__type === "throughRef") {
941
- throughJunction = throughJunction ?? config.to.__junction;
942
- throughToCol = config.to.__junctionColumn;
943
- }
944
- return {
945
- __type: "storedRelation",
946
- __relationType: relationType,
947
- __targetModel: targetModelName,
948
- __fromModel: from.model,
949
- __fromColumn: from.column,
950
- __toModel: to.model,
951
- __toColumn: to.column,
952
- __throughJunction: throughJunction,
953
- __throughFromCol: throughFromCol,
954
- __throughToCol: throughToCol,
955
- __optional: config.optional ?? relationType === "many",
956
- __alias: config.alias,
957
- __onDelete: config.onDelete,
958
- __onUpdate: config.onUpdate
959
- };
960
- };
961
- }
962
- function createRelationsBuilder(params) {
963
- const { models } = params;
964
- const builder = {};
965
- const oneObj = {};
966
- const manyObj = {};
967
- for (const modelName of Object.keys(models)) {
968
- oneObj[modelName] = createRelationFactory({ relationType: "one", targetModelName: modelName });
969
- manyObj[modelName] = createRelationFactory({
970
- relationType: "many",
971
- targetModelName: modelName
972
- });
973
- const model = models[modelName];
974
- if (model === undefined)
975
- continue;
976
- const columnRefs = {};
977
- for (const columnName of model.__columns.keys()) {
978
- columnRefs[columnName] = createColumnRef({ model: modelName, column: columnName });
979
- }
980
- builder[modelName] = columnRefs;
981
- }
982
- builder.one = oneObj;
983
- builder.many = manyObj;
984
- return builder;
985
- }
986
- function processRelationsOutput(params) {
987
- const result = {};
988
- for (const [modelName, relations] of Object.entries(params.output)) {
989
- if (relations) {
990
- result[modelName] = relations;
991
- }
992
- }
993
- return result;
994
- }
995
- function defineRelations(models, callback) {
996
- const builder = createRelationsBuilder({ models });
997
- return processRelationsOutput({ output: callback(builder) });
998
- }
999
- function defineRelationsPart(models, callback) {
1000
- const builder = createRelationsBuilder({ models });
1001
- return processRelationsOutput({ output: callback(builder) });
1002
- }
1003
- function mergeRelations(...parts) {
1004
- const result = {};
1005
- for (const part of parts) {
1006
- for (const [modelName, relations] of Object.entries(part)) {
1007
- const existing = result[modelName] ?? {};
1008
- result[modelName] = { ...existing, ...relations };
1009
- }
1010
- }
1011
- return result;
1012
- }
1013
-
1014
- // packages/dsl/src/schema.ts
1015
- import { VibeError as VibeError2, validateRlsContextIR } from "@vibeorm/schema";
1016
- function sameValues(left, right) {
1017
- return left.length === right.length && left.every((value, index) => value === right[index]);
1018
- }
1019
- function registerEnum(params) {
1020
- const { registry, name, entry, origin } = params;
1021
- const existing = registry.get(name);
1022
- if (existing === undefined) {
1023
- registry.set(name, entry);
1024
- return;
1025
- }
1026
- const valuesDiffer = !sameValues(existing.values, entry.values);
1027
- const storageDiffers = existing.storage !== entry.storage;
1028
- if (!valuesDiffer && !storageDiffers)
1029
- return;
1030
- const detail = [
1031
- ...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
1032
- ...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
1033
- ].join("; ");
1034
- throw new VibeError2({
1035
- code: "VIBE_SCHEMA_INVALID",
1036
- message: `Enum "${name}" is defined more than once with conflicting definitions (${origin}): ${detail} \u2014 share one enum object across modules, or give the definitions different names`,
1037
- meta: {
1038
- issues: [
1039
- {
1040
- rule: "enum-conflict",
1041
- message: `Enum "${name}" reused with conflicting ${valuesDiffer ? "values" : "storage"} (${origin})`
1042
- }
1043
- ],
1044
- enum: name,
1045
- existing,
1046
- incoming: entry
1047
- }
1048
- });
1049
- }
1050
- function isModelDef(value) {
1051
- return typeof value === "object" && value !== null && "__type" in value && value.__type === "model";
1052
- }
1053
- function isEnumDef(value) {
1054
- return typeof value === "object" && value !== null && "__type" in value && value.__type === "enum";
1055
- }
1056
- function addModelTree(params) {
1057
- const { model, models, origin } = params;
1058
- const existing = models.get(model.__name);
1059
- if (existing !== undefined && existing !== model) {
1060
- throw new VibeError2({
1061
- code: "VIBE_SCHEMA_INVALID",
1062
- message: `Model "${model.__name}" is defined more than once with different definitions (${origin}) \u2014 ` + `share one model object across modules, or give the definitions different names`,
1063
- meta: {
1064
- issues: [
1065
- {
1066
- rule: "model-conflict",
1067
- message: `Model "${model.__name}" reused with a different definition (${origin})`,
1068
- model: model.__name
1069
- }
1070
- ],
1071
- model: model.__name
1072
- }
1073
- });
1074
- }
1075
- models.set(model.__name, model);
1076
- for (const childModel of model.__children.values()) {
1077
- addModelTree({ model: childModel, models, origin: `inline child of ${model.__name}` });
1078
- }
1079
- }
1080
- function extractModelsAndEnums(params) {
1081
- const { input, models, enums } = params;
1082
- for (const value of Object.values(input)) {
1083
- if (isModelDef(value)) {
1084
- addModelTree({ model: value, models, origin: "listed in dsl.schema()" });
1085
- } else if (isEnumDef(value)) {
1086
- registerEnum({
1087
- registry: enums,
1088
- name: value.__name,
1089
- entry: { values: value.__options, storage: value.__storage },
1090
- origin: "standalone enum listed in dsl.schema()"
1091
- });
1092
- } else if (typeof value === "object" && value !== null) {
1093
- extractModelsAndEnums({ input: value, models, enums });
1094
- }
1095
- }
1096
- }
1097
- function resolveEnums(params) {
1098
- const { models, enums } = params;
1099
- for (const [modelName, model] of models) {
1100
- for (const [columnName, column] of model.__columns) {
1101
- if (column.__columnType !== "enum" || column.__enumValues === undefined)
1102
- continue;
1103
- const entry = {
1104
- values: column.__enumValues,
1105
- storage: column.__enumStorage ?? "database"
1106
- };
1107
- if (column.__enumName !== undefined) {
1108
- registerEnum({
1109
- registry: enums,
1110
- name: column.__enumName,
1111
- entry,
1112
- origin: `standalone enum used by ${modelName}.${columnName}`
1113
- });
1114
- continue;
1115
- }
1116
- const name = inlineEnumName({ modelName, columnName });
1117
- registerEnum({
1118
- registry: enums,
1119
- name,
1120
- entry,
1121
- origin: `inline enum on ${modelName}.${columnName}, named ${name}`
1122
- });
1123
- column.__enumName = name;
1124
- }
1125
- }
1126
- }
1127
- var RLS_CONTEXT_TYPES = {
1606
+
1607
+ // packages/dsl/src/relations.ts
1608
+ import { VibeError as VibeError6 } from "@vibeorm/schema";
1609
+ var REF_KIND_BY_COLUMN_TYPE = {
1128
1610
  string: "String",
1129
- boolean: "Boolean",
1130
1611
  int: "Int",
1131
1612
  bigint: "BigInt",
1132
1613
  float: "Float",
1133
1614
  decimal: "Decimal",
1134
- datetime: "DateTime"
1615
+ boolean: "Boolean",
1616
+ datetime: "DateTime",
1617
+ json: "Json",
1618
+ bytes: "Bytes",
1619
+ id: "Id",
1620
+ enum: "Enum",
1621
+ unsupported: "Unsupported"
1135
1622
  };
1136
- function defineRls(params) {
1137
- if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
1138
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1139
- }
1140
- const entries = [];
1141
- const refs = [];
1142
- for (const key of Reflect.ownKeys(params.context)) {
1143
- const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
1144
- const column = descriptor?.value;
1145
- if (typeof key !== "string" || descriptor?.enumerable !== true || typeof column !== "object" || column === null || !("__columnType" in column) || !("__config" in column) || typeof column.__config !== "object" || column.__config === null || typeof column.__columnType !== "string" || !Object.hasOwn(RLS_CONTEXT_TYPES, column.__columnType) || "array" in column.__config && column.__config.array === true) {
1146
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1623
+ function createColumnRef(params) {
1624
+ const { model, column, kind } = params;
1625
+ return {
1626
+ __type: "columnRef",
1627
+ __model: model,
1628
+ __column: column,
1629
+ __kind: kind,
1630
+ through(junction) {
1631
+ return {
1632
+ __type: "throughRef",
1633
+ __model: model,
1634
+ __column: column,
1635
+ __junction: junction.__model,
1636
+ __junctionColumn: junction.__column
1637
+ };
1147
1638
  }
1148
- entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
1149
- refs.push([key, Object.freeze({ context: key })]);
1639
+ };
1640
+ }
1641
+ function extractRefInfo(params) {
1642
+ const { ref, side, target } = params;
1643
+ if (!Array.isArray(ref)) {
1644
+ const single = ref;
1645
+ return { model: single.__model, columns: [single.__column] };
1646
+ }
1647
+ const members = ref;
1648
+ if (members.length === 0) {
1649
+ throw new VibeError6({
1650
+ code: "VIBE_SCHEMA_INVALID",
1651
+ message: `Relation to ${target}: "${side}" is an empty tuple \u2014 name at least one column`,
1652
+ meta: { rule: "relation-empty-tuple", side, targetModel: target }
1653
+ });
1150
1654
  }
1151
- const context = Object.freeze(Object.fromEntries(entries));
1152
- const issues = validateRlsContextIR({ context });
1153
- if (issues.length > 0) {
1154
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1655
+ const models = [...new Set(members.map((member) => member.__model))].sort();
1656
+ if (models.length > 1) {
1657
+ throw new VibeError6({
1658
+ code: "VIBE_SCHEMA_INVALID",
1659
+ message: `Relation to ${target}: "${side}" mixes models ${models.join(", ")} \u2014 every member of a composite key must sit on one model`,
1660
+ meta: { rule: "relation-mixed-models", side, targetModel: target, models }
1661
+ });
1155
1662
  }
1156
- const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
1157
- if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
1158
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1663
+ if (members.some((member) => member.__type !== "columnRef")) {
1664
+ throw new VibeError6({
1665
+ code: "VIBE_SCHEMA_INVALID",
1666
+ message: `Relation to ${target}: "${side}" routes through a junction inside a tuple \u2014 implicit many-to-many is single-key; model the join with an explicit junction model`,
1667
+ meta: { rule: "relation-through-in-tuple", side, targetModel: target }
1668
+ });
1159
1669
  }
1160
- const named = [];
1161
- const classified = new Set;
1162
- for (const key of Reflect.ownKeys(policies)) {
1163
- const descriptor = Object.getOwnPropertyDescriptor(policies, key);
1164
- if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
1165
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1670
+ return { model: members[0].__model, columns: members.map((member) => member.__column) };
1671
+ }
1672
+ function createRelationFactory(params) {
1673
+ const { relationType, targetModelName } = params;
1674
+ return (config) => {
1675
+ const from = extractRefInfo({ ref: config.from, side: "from", target: targetModelName });
1676
+ const to = extractRefInfo({ ref: config.to, side: "to", target: targetModelName });
1677
+ if (from.columns.length !== to.columns.length) {
1678
+ throw new VibeError6({
1679
+ code: "VIBE_SCHEMA_INVALID",
1680
+ message: `Relation to ${targetModelName}: "from" names ${from.columns.length} column(s) but "to" names ${to.columns.length} \u2014 a composite key pairs member by member`,
1681
+ meta: { rule: "relation-tuple-arity", targetModel: targetModelName, from: from.columns, to: to.columns }
1682
+ });
1166
1683
  }
1167
- const name = params.models[key]?.__name ?? key;
1168
- if (classified.has(name)) {
1169
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
1684
+ let throughJunction;
1685
+ let throughFromCol;
1686
+ let throughToCol;
1687
+ const fromRef = Array.isArray(config.from) ? undefined : config.from;
1688
+ const toRef2 = Array.isArray(config.to) ? undefined : config.to;
1689
+ if (fromRef?.__type === "throughRef") {
1690
+ throughJunction = fromRef.__junction;
1691
+ throughFromCol = fromRef.__junctionColumn;
1170
1692
  }
1171
- classified.add(name);
1172
- named.push([name, descriptor.value]);
1173
- }
1174
- return { context, policies: Object.fromEntries(named) };
1175
- }
1176
- function schema(input, options) {
1177
- const models = new Map;
1178
- const enums = new Map;
1179
- extractModelsAndEnums({ input, models, enums });
1180
- resolveEnums({ models, enums });
1181
- return {
1182
- __type: "schema",
1183
- __models: models,
1184
- __enums: new Map(Array.from(enums, ([name, entry]) => [name, entry.values])),
1185
- __enumStorage: new Map(Array.from(enums, ([name, entry]) => [name, entry.storage])),
1186
- __relations: options?.relations ?? {},
1187
- ...options?.rls === undefined ? {} : { __rls: options.rls }
1693
+ if (toRef2?.__type === "throughRef") {
1694
+ throughJunction = throughJunction ?? toRef2.__junction;
1695
+ throughToCol = toRef2.__junctionColumn;
1696
+ }
1697
+ return {
1698
+ __type: "storedRelation",
1699
+ __relationType: relationType,
1700
+ __targetModel: targetModelName,
1701
+ __fromModel: from.model,
1702
+ __fromColumn: from.columns[0],
1703
+ __fromColumns: from.columns,
1704
+ __toModel: to.model,
1705
+ __toColumn: to.columns[0],
1706
+ __toColumns: to.columns,
1707
+ __throughJunction: throughJunction,
1708
+ __throughFromCol: throughFromCol,
1709
+ __throughToCol: throughToCol,
1710
+ __optional: config.optional ?? relationType === "many",
1711
+ __alias: config.alias,
1712
+ ...config.constraintName === undefined ? {} : { __constraintName: config.constraintName },
1713
+ ..."ownsForeignKey" in config ? { __ownsForeignKey: config.ownsForeignKey } : {},
1714
+ __onDelete: config.onDelete,
1715
+ __onUpdate: config.onUpdate
1716
+ };
1188
1717
  };
1189
1718
  }
1190
- function domain(params) {
1191
- const { name, models } = params;
1192
- return { name, models };
1193
- }
1194
-
1195
- // packages/dsl/src/templates.ts
1196
- var base = {
1197
- get id() {
1198
- return id({ type: "ulid" });
1199
- },
1200
- get createdAt() {
1201
- return datetime({ default: "now", tz: true, index: true });
1202
- },
1203
- get updatedAt() {
1204
- return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1205
- },
1206
- get deletedAt() {
1207
- return datetime({ optional: true, index: true });
1208
- }
1209
- };
1210
- var baseWithWorkspace = {
1211
- get id() {
1212
- return id({ type: "ulid" });
1213
- },
1214
- get createdAt() {
1215
- return datetime({ default: "now", tz: true, index: true });
1216
- },
1217
- get updatedAt() {
1218
- return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1219
- },
1220
- get deletedAt() {
1221
- return datetime({ optional: true, index: true });
1222
- },
1223
- get workspaceId() {
1224
- return string({ index: true });
1225
- }
1226
- };
1227
- var withMetadata = {
1228
- get metadata() {
1229
- return json({ optional: true });
1230
- },
1231
- get settings() {
1232
- return json({ optional: true });
1233
- }
1234
- };
1235
- var withActive = {
1236
- get isActive() {
1237
- return boolean({ default: true });
1238
- }
1239
- };
1240
- var withAudit = {
1241
- get createdById() {
1242
- return string({ optional: true, index: true });
1243
- },
1244
- get updatedById() {
1245
- return string({ optional: true, index: true });
1246
- }
1247
- };
1248
- var withExternalId = {
1249
- get externalId() {
1250
- return string({ index: true });
1251
- }
1252
- };
1253
- var withNameDescription = {
1254
- get name() {
1255
- return string();
1256
- },
1257
- get description() {
1258
- return string({ optional: true });
1259
- }
1260
- };
1261
- var fields = {
1262
- get datetime() {
1263
- return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1264
- },
1265
- get id() {
1266
- return id({ type: "ulid" });
1267
- },
1268
- get title() {
1269
- return string();
1270
- },
1271
- get description() {
1272
- return string({ optional: true });
1273
- },
1274
- get externalId() {
1275
- return string({ index: true });
1276
- },
1277
- get metadata() {
1278
- return json({ optional: true });
1279
- },
1280
- get settings() {
1281
- return json({ optional: true });
1282
- },
1283
- get isActive() {
1284
- return boolean({ default: true });
1285
- }
1286
- };
1287
- var templates = {
1288
- base,
1289
- baseWithWorkspace,
1290
- withMetadata,
1291
- withActive,
1292
- withAudit,
1293
- withExternalId,
1294
- withNameDescription,
1295
- fields
1296
- };
1297
-
1298
- // packages/dsl/src/to-ir.ts
1299
- import {
1300
- EMPTY_EXTENSION_REGISTRY,
1301
- VibeError as VibeError3,
1302
- assertValidSchemaIR,
1303
- exactIntegerLiteral,
1304
- mergeExtensionDeclarations
1305
- } from "@vibeorm/schema";
1306
- function fieldType(params) {
1307
- const { column } = params;
1308
- switch (column.__columnType) {
1309
- case "string":
1310
- return "String";
1311
- case "int":
1312
- return "Int";
1313
- case "bigint":
1314
- return "BigInt";
1315
- case "float":
1316
- return "Float";
1317
- case "decimal":
1318
- return "Decimal";
1319
- case "boolean":
1320
- return "Boolean";
1321
- case "datetime":
1322
- return "DateTime";
1323
- case "json":
1324
- return "Json";
1325
- case "bytes":
1326
- return "Bytes";
1327
- case "id":
1328
- return column.__config.type === "autoincrement" ? "Int" : "String";
1329
- case "enum":
1330
- return column.__enumName === undefined ? "String" : { enum: column.__enumName };
1331
- case "unsupported":
1332
- return "String";
1333
- }
1334
- }
1335
- function idDefault(params) {
1336
- switch (params.column.__config.type) {
1337
- case "ulid":
1338
- return { kind: "ulid" };
1339
- case "cuid":
1340
- return { kind: "cuid" };
1341
- case "uuid":
1342
- return { kind: "uuid" };
1343
- case "nanoid":
1344
- return { kind: "nanoid" };
1345
- case "autoincrement":
1346
- return { kind: "autoincrement" };
1347
- default:
1348
- return;
1719
+ function createRelationsBuilder(params) {
1720
+ const { models } = params;
1721
+ const builder = {};
1722
+ const oneObj = {};
1723
+ const manyObj = {};
1724
+ for (const modelName of Object.keys(models)) {
1725
+ oneObj[modelName] = createRelationFactory({ relationType: "one", targetModelName: modelName });
1726
+ manyObj[modelName] = createRelationFactory({
1727
+ relationType: "many",
1728
+ targetModelName: modelName
1729
+ });
1730
+ const model = models[modelName];
1731
+ if (model === undefined)
1732
+ continue;
1733
+ const columnRefs = {};
1734
+ for (const [columnName, column] of model.__columns) {
1735
+ columnRefs[columnName] = createColumnRef({
1736
+ model: modelName,
1737
+ column: columnName,
1738
+ kind: REF_KIND_BY_COLUMN_TYPE[column.__columnType]
1739
+ });
1740
+ }
1741
+ builder[modelName] = columnRefs;
1349
1742
  }
1743
+ builder.one = oneObj;
1744
+ builder.many = manyObj;
1745
+ return builder;
1350
1746
  }
1351
- function fieldDefault(params) {
1352
- const { column } = params;
1353
- if (column.__columnType === "id")
1354
- return idDefault({ column });
1355
- const { dbgenerated, default: value } = column.__config;
1356
- if (typeof dbgenerated === "string")
1357
- return { kind: "dbgenerated", sql: dbgenerated };
1358
- if (value === undefined)
1359
- return;
1360
- if (column.__columnType === "datetime" && value === "now")
1361
- return { kind: "now" };
1362
- if (column.__columnType === "enum" && typeof value === "string") {
1363
- return { kind: "enumValue", value };
1364
- }
1365
- if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
1366
- return { kind: "literal", value };
1747
+ function processRelationsOutput(params) {
1748
+ const result = {};
1749
+ for (const [modelName, relations] of Object.entries(params.output)) {
1750
+ if (relations) {
1751
+ result[modelName] = relations;
1752
+ }
1367
1753
  }
1368
- if (typeof value === "bigint")
1369
- return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
1370
- return;
1754
+ return result;
1371
1755
  }
1372
- function nativeType(params) {
1373
- const { column } = params;
1374
- if (column.__columnType === "enum") {
1375
- const { maxLength, text } = column.__config;
1376
- if (maxLength !== undefined || text !== undefined) {
1377
- const invalid = column.__enumStorage !== "string" || maxLength !== undefined && (typeof maxLength !== "number" || !Number.isSafeInteger(maxLength) || maxLength <= 0) || text !== undefined && typeof text !== "boolean" || text === true && maxLength !== undefined;
1378
- if (invalid) {
1379
- throw new VibeError3({
1380
- code: "VIBE_SCHEMA_INVALID",
1381
- message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
1382
- meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
1383
- });
1384
- }
1756
+ function defineRelations(models, callback) {
1757
+ const builder = createRelationsBuilder({ models });
1758
+ return processRelationsOutput({ output: callback(builder) });
1759
+ }
1760
+ function defineRelationsPart(models, callback) {
1761
+ const builder = createRelationsBuilder({ models });
1762
+ return processRelationsOutput({ output: callback(builder) });
1763
+ }
1764
+ function mergeRelations(...parts) {
1765
+ const result = {};
1766
+ for (const part of parts) {
1767
+ for (const [modelName, relations] of Object.entries(part)) {
1768
+ const existing = result[modelName] ?? {};
1769
+ result[modelName] = { ...existing, ...relations };
1385
1770
  }
1386
1771
  }
1387
- if (column.__columnType === "string" || column.__columnType === "enum" && column.__enumStorage === "string") {
1388
- const { maxLength, text } = column.__config;
1389
- if (typeof maxLength === "number")
1390
- return `VarChar(${maxLength})`;
1391
- if (text === true)
1392
- return "Text";
1772
+ return result;
1773
+ }
1774
+
1775
+ // packages/dsl/src/schema.ts
1776
+ import { VibeError as VibeError7, validateRlsContextIR } from "@vibeorm/schema";
1777
+ function sameValues(left, right) {
1778
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1779
+ }
1780
+ function registerEnum(params) {
1781
+ const { registry, name, entry, origin } = params;
1782
+ const existing = registry.get(name);
1783
+ if (existing === undefined) {
1784
+ registry.set(name, entry);
1393
1785
  return;
1394
1786
  }
1395
- if (column.__columnType === "decimal") {
1396
- const { precision, scale } = column.__config;
1397
- if (typeof precision === "number")
1398
- return `Decimal(${precision}, ${scale ?? 0})`;
1787
+ const valuesDiffer = !sameValues(existing.values, entry.values);
1788
+ const storageDiffers = existing.storage !== entry.storage;
1789
+ if (!valuesDiffer && !storageDiffers)
1399
1790
  return;
1400
- }
1401
- if (column.__columnType === "datetime" && column.__config.tz === true) {
1402
- return "Timestamptz";
1403
- }
1404
- if (column.__columnType === "unsupported") {
1405
- return String(column.__config.dbType);
1406
- }
1407
- return;
1791
+ const detail = [
1792
+ ...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
1793
+ ...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
1794
+ ].join("; ");
1795
+ throw new VibeError7({
1796
+ code: "VIBE_SCHEMA_INVALID",
1797
+ message: `Enum "${name}" is defined more than once with conflicting definitions (${origin}): ${detail} \u2014 share one enum object across modules, or give the definitions different names`,
1798
+ meta: {
1799
+ issues: [
1800
+ {
1801
+ rule: "enum-conflict",
1802
+ message: `Enum "${name}" reused with conflicting ${valuesDiffer ? "values" : "storage"} (${origin})`
1803
+ }
1804
+ ],
1805
+ enum: name,
1806
+ existing,
1807
+ incoming: entry
1808
+ }
1809
+ });
1408
1810
  }
1409
- function buildField(params) {
1410
- const { name, column, extensionNativeType } = params;
1411
- const config = column.__config;
1412
- const isList = column.__columnType !== "id" && config.array === true;
1413
- const dbName = typeof config.map === "string" ? config.map : undefined;
1414
- const documentation = typeof config.comment === "string" ? config.comment : undefined;
1415
- const collation = typeof config.collation === "string" ? config.collation : undefined;
1416
- const defaultValue = fieldDefault({ column });
1417
- const native = extensionNativeType ?? nativeType({ column });
1418
- return {
1419
- name,
1420
- type: fieldType({ column }),
1421
- isList,
1422
- isOptional: config.optional === true,
1423
- isId: column.__columnType === "id",
1424
- isUnique: config.unique === true,
1425
- isUpdatedAt: column.__columnType === "datetime" && config.updatedAt === true,
1426
- ...collation === undefined ? {} : { collation },
1427
- ...dbName === undefined ? {} : { dbName },
1428
- ...defaultValue === undefined ? {} : { default: defaultValue },
1429
- ...native === undefined ? {} : { nativeType: native },
1430
- ...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
1431
- ...documentation === undefined ? {} : { documentation }
1432
- };
1811
+ function isModelDef(value) {
1812
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "model";
1433
1813
  }
1434
- function buildRelation(params) {
1435
- const { relation, plan } = params;
1436
- return {
1437
- name: relation.name,
1438
- model: relation.model,
1439
- targetModel: relation.targetModel,
1440
- type: resolveRelationType({ relation, plan }),
1441
- isList: relation.isList,
1442
- isOptional: relation.isOptional,
1443
- ...relation.relationName === undefined ? {} : { relationName: relation.relationName },
1444
- ...relation.fields === undefined ? {} : { fields: [...relation.fields] },
1445
- ...relation.references === undefined ? {} : { references: [...relation.references] },
1446
- ...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
1447
- ...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
1448
- };
1814
+ function isEnumDef(value) {
1815
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "enum";
1449
1816
  }
1450
- var INDEX_KINDS = {
1451
- BTree: "btree",
1452
- Hash: "hash",
1453
- Gin: "gin",
1454
- Gist: "gist",
1455
- SpGist: "spgist",
1456
- Brin: "brin"
1457
- };
1458
- function buildIndex(params) {
1459
- const { index } = params;
1460
- const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
1461
- return {
1462
- kind,
1463
- fields: index.columns.map((name) => ({ name })),
1464
- ...index.where === undefined ? {} : { where: index.where },
1465
- ...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
1466
- ...index.clientName === undefined ? {} : { clientName: index.clientName }
1467
- };
1817
+ function addModelTree(params) {
1818
+ const { model, models, origin } = params;
1819
+ const existing = models.get(model.__name);
1820
+ if (existing !== undefined && existing !== model) {
1821
+ throw new VibeError7({
1822
+ code: "VIBE_SCHEMA_INVALID",
1823
+ message: `Model "${model.__name}" is defined more than once with different definitions (${origin}) \u2014 ` + `share one model object across modules, or give the definitions different names`,
1824
+ meta: {
1825
+ issues: [
1826
+ {
1827
+ rule: "model-conflict",
1828
+ message: `Model "${model.__name}" reused with a different definition (${origin})`,
1829
+ model: model.__name
1830
+ }
1831
+ ],
1832
+ model: model.__name
1833
+ }
1834
+ });
1835
+ }
1836
+ models.set(model.__name, model);
1837
+ for (const childModel of model.__children.values()) {
1838
+ addModelTree({ model: childModel, models, origin: `inline child of ${model.__name}` });
1839
+ }
1468
1840
  }
1469
- function buildModel(params) {
1470
- const { model, plan, extensions, declarations, issues } = params;
1471
- const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
1472
- const policy = model.__policy === undefined ? undefined : { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field };
1473
- const fields2 = [];
1474
- for (const [name, column] of model.__columns) {
1475
- const extensionTypeName = column.__config["extensionType"];
1476
- if (typeof extensionTypeName !== "string") {
1477
- fields2.push(buildField({ name, column }));
1478
- continue;
1479
- }
1480
- const registered = extensions.fieldTypes.get(extensionTypeName);
1481
- if (registered === undefined) {
1482
- issues.push({
1483
- rule: "extension-attribute",
1484
- message: `Field "${model.__name}.${name}" uses extension type "${extensionTypeName}" but no configured extension provides it`,
1485
- model: model.__name,
1486
- field: name
1841
+ function extractModelsAndEnums(params) {
1842
+ const { input, models, enums } = params;
1843
+ for (const value of Object.values(input)) {
1844
+ if (isModelDef(value)) {
1845
+ addModelTree({ model: value, models, origin: "listed in dsl.schema()" });
1846
+ } else if (isEnumDef(value)) {
1847
+ registerEnum({
1848
+ registry: enums,
1849
+ name: value.__name,
1850
+ entry: { values: value.__options, storage: value.__storage },
1851
+ origin: "standalone enum listed in dsl.schema()"
1487
1852
  });
1488
- continue;
1853
+ } else if (typeof value === "object" && value !== null) {
1854
+ extractModelsAndEnums({ input: value, models, enums });
1489
1855
  }
1490
- const rawArgs = column.__config["extensionArgs"];
1491
- const lowered = registered.fieldType.lower({
1492
- model: model.__name,
1493
- field: name,
1494
- args: Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string" || typeof entry === "number") : []
1495
- });
1496
- if ("error" in lowered) {
1497
- issues.push({
1498
- rule: "extension-attribute",
1499
- message: `Field "${model.__name}.${name}": ${lowered.error}`,
1500
- model: model.__name,
1501
- field: name
1856
+ }
1857
+ }
1858
+ function resolveEnums(params) {
1859
+ const { models, enums } = params;
1860
+ for (const [modelName, model] of models) {
1861
+ for (const [columnName, column] of model.__columns) {
1862
+ if (column.__columnType !== "enum" || column.__enumValues === undefined)
1863
+ continue;
1864
+ const entry = {
1865
+ values: column.__enumValues,
1866
+ storage: column.__enumStorage ?? "database"
1867
+ };
1868
+ if (column.__enumName !== undefined) {
1869
+ registerEnum({
1870
+ registry: enums,
1871
+ name: column.__enumName,
1872
+ entry,
1873
+ origin: `standalone enum used by ${modelName}.${columnName}`
1874
+ });
1875
+ continue;
1876
+ }
1877
+ const name = inlineEnumName({ modelName, columnName });
1878
+ registerEnum({
1879
+ registry: enums,
1880
+ name,
1881
+ entry,
1882
+ origin: `inline enum on ${modelName}.${columnName}, named ${name}`
1502
1883
  });
1503
- continue;
1884
+ column.__enumName = name;
1504
1885
  }
1505
- fields2.push(buildField({ name, column, extensionNativeType: lowered.nativeType }));
1506
- if (lowered.declaration !== undefined) {
1507
- declarations.add({
1508
- extension: registered.extension.name,
1509
- model: model.__name,
1510
- declaration: lowered.declaration
1511
- });
1886
+ }
1887
+ }
1888
+ var RLS_CONTEXT_TYPES = {
1889
+ string: "String",
1890
+ boolean: "Boolean",
1891
+ int: "Int",
1892
+ bigint: "BigInt",
1893
+ float: "Float",
1894
+ decimal: "Decimal",
1895
+ datetime: "DateTime"
1896
+ };
1897
+ function defineRls(params) {
1898
+ if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
1899
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1900
+ }
1901
+ const entries = [];
1902
+ const refs = [];
1903
+ for (const key of Reflect.ownKeys(params.context)) {
1904
+ const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
1905
+ const column = descriptor?.value;
1906
+ if (typeof key !== "string" || descriptor?.enumerable !== true || typeof column !== "object" || column === null || !("__columnType" in column) || !("__config" in column) || typeof column.__config !== "object" || column.__config === null || typeof column.__columnType !== "string" || !Object.hasOwn(RLS_CONTEXT_TYPES, column.__columnType) || "array" in column.__config && column.__config.array === true) {
1907
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1512
1908
  }
1909
+ entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
1910
+ refs.push([key, Object.freeze({ context: key })]);
1513
1911
  }
1514
- for (const use of model.__extensions ?? []) {
1515
- const registered = extensions.modelAttributes.get(use.attribute);
1516
- if (registered === undefined) {
1517
- issues.push({
1518
- rule: "unknown-attribute",
1519
- message: `Unknown block attribute "@@${use.attribute}" on model "${model.__name}" \u2014 no extension provides it`,
1520
- model: model.__name
1521
- });
1522
- continue;
1912
+ const context = Object.freeze(Object.fromEntries(entries));
1913
+ const issues = validateRlsContextIR({ context });
1914
+ if (issues.length > 0) {
1915
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1916
+ }
1917
+ const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
1918
+ if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
1919
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1920
+ }
1921
+ const named = [];
1922
+ const classified = new Set;
1923
+ for (const key of Reflect.ownKeys(policies)) {
1924
+ const descriptor = Object.getOwnPropertyDescriptor(policies, key);
1925
+ if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
1926
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1523
1927
  }
1524
- const parsed = registered.attribute.parse({
1525
- model: model.__name,
1526
- args: {
1527
- named: { ...use.args ?? {} },
1528
- ...use.fields === undefined ? {} : { fields: use.fields }
1529
- },
1530
- fieldNames: fields2.map((field) => field.name)
1531
- });
1532
- if ("error" in parsed) {
1533
- issues.push({
1534
- rule: "extension-attribute",
1535
- message: `@@${use.attribute} on "${model.__name}": ${parsed.error}`,
1536
- model: model.__name
1537
- });
1538
- continue;
1928
+ const name = params.models[key]?.__name ?? key;
1929
+ if (classified.has(name)) {
1930
+ throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
1539
1931
  }
1540
- declarations.add({
1541
- extension: registered.extension.name,
1542
- model: model.__name,
1543
- declaration: parsed.declaration
1544
- });
1932
+ classified.add(name);
1933
+ named.push([name, descriptor.value]);
1545
1934
  }
1935
+ return { context, policies: Object.fromEntries(named) };
1936
+ }
1937
+ function schema(input, options) {
1938
+ const models = new Map;
1939
+ const enums = new Map;
1940
+ extractModelsAndEnums({ input, models, enums });
1941
+ resolveEnums({ models, enums });
1546
1942
  return {
1547
- name: model.__name,
1548
- ...model.__map === undefined ? {} : { dbName: model.__map },
1549
- fields: fields2,
1550
- relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
1551
- indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index })),
1552
- ...model.__checks === undefined || model.__checks.length === 0 ? {} : { checks: model.__checks.map((check) => ({ name: check.name, expression: check.expression })) },
1553
- ...policy === undefined ? {} : { policy },
1554
- ...primaryKey === undefined ? {} : { primaryKey },
1555
- isView: false,
1556
- ...model.__comment === undefined ? {} : { documentation: model.__comment }
1943
+ __type: "schema",
1944
+ __models: models,
1945
+ __enums: new Map(Array.from(enums, ([name, entry]) => [name, entry.values])),
1946
+ __enumStorage: new Map(Array.from(enums, ([name, entry]) => [name, entry.storage])),
1947
+ __relations: options?.relations ?? {},
1948
+ ...options?.rls === undefined ? {} : { __rls: options.rls }
1557
1949
  };
1558
1950
  }
1951
+ function domain(params) {
1952
+ const { name, models } = params;
1953
+ return { name, models };
1954
+ }
1559
1955
 
1560
- class ExtensionDeclarationCollector {
1561
- byExtension = new Map;
1562
- add(params) {
1563
- const perModel = this.byExtension.get(params.extension) ?? new Map;
1564
- this.byExtension.set(params.extension, perModel);
1565
- const existing = perModel.get(params.model) ?? {};
1566
- perModel.set(params.model, mergeExtensionDeclarations({ base: existing, next: params.declaration }));
1956
+ // packages/dsl/src/templates.ts
1957
+ var base = {
1958
+ get id() {
1959
+ return id({ type: "ulid" });
1960
+ },
1961
+ get createdAt() {
1962
+ return datetime({ default: "now", tz: true, index: true });
1963
+ },
1964
+ get updatedAt() {
1965
+ return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1966
+ },
1967
+ get deletedAt() {
1968
+ return datetime({ optional: true, index: true });
1567
1969
  }
1568
- toIR() {
1569
- return [...this.byExtension.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, perModel]) => ({
1570
- name,
1571
- options: {
1572
- models: Object.fromEntries([...perModel.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
1573
- }
1574
- }));
1970
+ };
1971
+ var baseWithWorkspace = {
1972
+ get id() {
1973
+ return id({ type: "ulid" });
1974
+ },
1975
+ get createdAt() {
1976
+ return datetime({ default: "now", tz: true, index: true });
1977
+ },
1978
+ get updatedAt() {
1979
+ return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1980
+ },
1981
+ get deletedAt() {
1982
+ return datetime({ optional: true, index: true });
1983
+ },
1984
+ get workspaceId() {
1985
+ return string({ index: true });
1575
1986
  }
1576
- }
1577
- function toIR(params) {
1578
- const { schema: schema2, provider = "postgresql" } = params;
1579
- const extensions = params.extensions ?? EMPTY_EXTENSION_REGISTRY;
1580
- const plan = planSchema({ schema: schema2 });
1581
- const declarations = new ExtensionDeclarationCollector;
1582
- const issues = [];
1583
- const models = Array.from(schema2.__models.values(), (model) => {
1584
- const built = buildModel({ model, plan, extensions, declarations, issues });
1585
- const rls = schema2.__rls?.policies[model.__name];
1586
- return rls === undefined ? built : { ...built, rls };
1587
- });
1588
- if (schema2.__rls !== undefined) {
1589
- for (const name of Object.keys(schema2.__rls.policies)) {
1590
- if (!schema2.__models.has(name)) {
1591
- issues.push({ rule: "rls-unknown-model", model: name, message: `RLS classification references model "${name}" absent from the composed schema` });
1592
- }
1593
- }
1987
+ };
1988
+ var withMetadata = {
1989
+ get metadata() {
1990
+ return json({ optional: true });
1991
+ },
1992
+ get settings() {
1993
+ return json({ optional: true });
1594
1994
  }
1595
- const enums = Array.from(schema2.__enums, ([name, values]) => ({
1596
- name,
1597
- values: [...values],
1598
- ...schema2.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
1599
- }));
1600
- if (issues.length > 0) {
1601
- throw new VibeError3({
1602
- code: "VIBE_SCHEMA_INVALID",
1603
- message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
1604
- meta: { issues }
1605
- });
1995
+ };
1996
+ var withActive = {
1997
+ get isActive() {
1998
+ return boolean({ default: true });
1606
1999
  }
1607
- const ir = {
1608
- datasource: { provider },
1609
- models,
1610
- enums,
1611
- extensions: declarations.toIR(),
1612
- ...schema2.__rls === undefined ? {} : { rls: { context: schema2.__rls.context } }
1613
- };
1614
- assertValidSchemaIR({ schema: ir });
1615
- return ir;
1616
- }
2000
+ };
2001
+ var withAudit = {
2002
+ get createdById() {
2003
+ return string({ optional: true, index: true });
2004
+ },
2005
+ get updatedById() {
2006
+ return string({ optional: true, index: true });
2007
+ }
2008
+ };
2009
+ var withExternalId = {
2010
+ get externalId() {
2011
+ return string({ index: true });
2012
+ }
2013
+ };
2014
+ var withNameDescription = {
2015
+ get name() {
2016
+ return string();
2017
+ },
2018
+ get description() {
2019
+ return string({ optional: true });
2020
+ }
2021
+ };
2022
+ var fields = {
2023
+ get datetime() {
2024
+ return datetime({ default: "now", updatedAt: true, tz: true, index: true });
2025
+ },
2026
+ get id() {
2027
+ return id({ type: "ulid" });
2028
+ },
2029
+ get title() {
2030
+ return string();
2031
+ },
2032
+ get description() {
2033
+ return string({ optional: true });
2034
+ },
2035
+ get externalId() {
2036
+ return string({ index: true });
2037
+ },
2038
+ get metadata() {
2039
+ return json({ optional: true });
2040
+ },
2041
+ get settings() {
2042
+ return json({ optional: true });
2043
+ },
2044
+ get isActive() {
2045
+ return boolean({ default: true });
2046
+ }
2047
+ };
2048
+ var templates = {
2049
+ base,
2050
+ baseWithWorkspace,
2051
+ withMetadata,
2052
+ withActive,
2053
+ withAudit,
2054
+ withExternalId,
2055
+ withNameDescription,
2056
+ fields
2057
+ };
1617
2058
 
1618
2059
  // packages/dsl/src/validate.ts
1619
2060
  function validateSchema(schema2) {
@@ -1660,18 +2101,30 @@ function validateSchema(schema2) {
1660
2101
  continue;
1661
2102
  }
1662
2103
  if (relation.__relationType === "one" && relation.__fromModel === modelName) {
1663
- if (!model.__columns.has(relation.__fromColumn)) {
1664
- errors.push({
1665
- type: "error",
1666
- message: `Relation "${relationName}" references FK column "${relation.__fromColumn}" which doesn't exist on model "${modelName}"`,
1667
- model: modelName,
1668
- relation: relationName
1669
- });
2104
+ for (const column of relation.__fromColumns) {
2105
+ if (!model.__columns.has(column)) {
2106
+ errors.push({
2107
+ type: "error",
2108
+ message: `Relation "${relationName}" references FK column "${column}" which doesn't exist on model "${modelName}"`,
2109
+ model: modelName,
2110
+ relation: relationName
2111
+ });
2112
+ }
2113
+ }
2114
+ for (const column of relation.__toColumns) {
2115
+ if (!targetModel.__columns.has(column)) {
2116
+ errors.push({
2117
+ type: "error",
2118
+ message: `Relation "${relationName}" references column "${column}" which doesn't exist on target model "${relation.__targetModel}"`,
2119
+ model: modelName,
2120
+ relation: relationName
2121
+ });
2122
+ }
1670
2123
  }
1671
- if (!targetModel.__columns.has(relation.__toColumn)) {
2124
+ if (relation.__fromColumns.length !== relation.__toColumns.length) {
1672
2125
  errors.push({
1673
2126
  type: "error",
1674
- message: `Relation "${relationName}" references column "${relation.__toColumn}" which doesn't exist on target model "${relation.__targetModel}"`,
2127
+ message: `Relation "${relationName}" pairs ${relation.__fromColumns.length} FK column(s) with ${relation.__toColumns.length} referenced column(s)`,
1675
2128
  model: modelName,
1676
2129
  relation: relationName
1677
2130
  });
@@ -1758,21 +2211,26 @@ export {
1758
2211
  generatePrisma,
1759
2212
  fields,
1760
2213
  evaluateIndexes,
2214
+ evaluateChecks,
1761
2215
  dsl,
1762
2216
  domain,
1763
2217
  detectOneToOnePairs,
1764
2218
  defineRls,
1765
2219
  defineRelationsPart,
1766
2220
  defineRelations,
2221
+ createPredicateBuilder,
1767
2222
  createIndexBuilder,
1768
2223
  createEnum,
1769
2224
  createChildrenBuilder,
1770
2225
  create,
2226
+ compileIndexPredicate,
2227
+ compileChecks,
1771
2228
  cloneColumnDef,
1772
2229
  children,
1773
2230
  baseWithWorkspace,
1774
2231
  base,
1775
2232
  ChainableStringColumn,
2233
+ ChainableIntegerColumn,
1776
2234
  ChainableIdColumn,
1777
2235
  ChainableEnumColumn,
1778
2236
  ChainableDecimalColumn,
@@ -1780,4 +2238,4 @@ export {
1780
2238
  ChainableColumn
1781
2239
  };
1782
2240
 
1783
- //# debugId=EC151CEC887B44C364756E2164756E21
2241
+ //# debugId=13292A89548F16A064756E2164756E21