@vibeorm/dsl 2.0.0-alpha.8 → 2.0.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,21 @@ 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 VibeError3, encodePrismaString, internalError } from "@vibeorm/schema";
228
+
229
+ // packages/dsl/src/to-ir.ts
230
+ import {
231
+ EMPTY_EXTENSION_REGISTRY,
232
+ VibeError as VibeError2,
233
+ assertValidSchemaIR,
234
+ exactIntegerLiteral,
235
+ scalarListDefault,
236
+ serializeJsonDefault,
237
+ mergeExtensionDeclarations
238
+ } from "@vibeorm/schema";
239
+
240
+ // packages/dsl/src/relation-plan.ts
241
+ import { VibeError } from "@vibeorm/schema";
221
242
 
222
243
  // packages/dsl/src/naming.ts
223
244
  function capitalize(params) {
@@ -256,17 +277,18 @@ function isChildrenDef(value) {
256
277
  return typeof value === "object" && value !== null && "__type" in value && value.__type === "children";
257
278
  }
258
279
  function createIndexBuilder() {
259
- const createIndex = (columns, type, where) => ({
280
+ const createIndex = (columns, type, where, map) => ({
260
281
  __type: "index",
261
282
  __columns: [...columns],
262
283
  __indexType: type,
284
+ ...map === undefined ? {} : { __map: map },
263
285
  ...where === undefined ? {} : { __where: where }
264
286
  });
265
287
  return {
266
288
  on(...args) {
267
289
  const [first, second] = args;
268
290
  if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
269
- return createIndex(first, second.type, second.where);
291
+ return createIndex(first, second.type, second.where, second.map);
270
292
  }
271
293
  return createIndex(args);
272
294
  },
@@ -279,6 +301,7 @@ function createIndexBuilder() {
279
301
  return {
280
302
  __type: "unique",
281
303
  __columns: [...first],
304
+ ...second.map === undefined ? {} : { __map: second.map },
282
305
  ...second.where === undefined ? {} : { __where: second.where },
283
306
  ...second.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
284
307
  ...second.name === undefined ? {} : { __clientName: second.name }
@@ -362,6 +385,9 @@ function detectOneToOnePairs(params) {
362
385
  const allOnes = [];
363
386
  for (const [modelName, modelRelations] of Object.entries(relations)) {
364
387
  for (const [relName, rel] of Object.entries(modelRelations)) {
388
+ if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
389
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
390
+ }
365
391
  if (rel.__relationType === "one") {
366
392
  allOnes.push({ modelName, relName, rel });
367
393
  }
@@ -383,7 +409,7 @@ function detectOneToOnePairs(params) {
383
409
  if (paired.has(keyB))
384
410
  continue;
385
411
  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)
412
+ if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
387
413
  continue;
388
414
  const { fkOwner, backRelation } = resolveFkOwner({ a, b, models });
389
415
  const onDelete = fkOwner.rel.__onDelete ?? backRelation.rel.__onDelete;
@@ -413,21 +439,35 @@ function detectOneToOnePairs(params) {
413
439
  }
414
440
  function resolveFkOwner(params) {
415
441
  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";
442
+ const ownershipError = () => {
443
+ throw new VibeError({ 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" } });
444
+ };
445
+ const aOwns = a.rel.__ownsForeignKey;
446
+ const bOwns = b.rel.__ownsForeignKey;
447
+ if (aOwns !== undefined || bOwns !== undefined) {
448
+ if (aOwns === bOwns)
449
+ return ownershipError();
450
+ return aOwns === true || bOwns === false ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
451
+ }
452
+ const aFromIsId = models.get(a.modelName)?.__columns.get(a.rel.__fromColumn)?.__columnType === "id" || models.get(a.modelName)?.__primaryKey?.includes(a.rel.__fromColumn) === true;
453
+ const bFromIsId = models.get(b.modelName)?.__columns.get(b.rel.__fromColumn)?.__columnType === "id" || models.get(b.modelName)?.__primaryKey?.includes(b.rel.__fromColumn) === true;
418
454
  if (aFromIsId && !bFromIsId)
419
455
  return { fkOwner: b, backRelation: a };
420
456
  if (!aFromIsId && bFromIsId)
421
457
  return { fkOwner: a, backRelation: b };
422
- if (a.rel.__onDelete !== undefined || a.rel.__onUpdate !== undefined) {
458
+ const aActions = a.rel.__onDelete !== undefined || a.rel.__onUpdate !== undefined;
459
+ const bActions = b.rel.__onDelete !== undefined || b.rel.__onUpdate !== undefined;
460
+ if (aActions && !bActions) {
423
461
  return { fkOwner: a, backRelation: b };
424
462
  }
425
- if (b.rel.__onDelete !== undefined || b.rel.__onUpdate !== undefined) {
463
+ if (bActions && !aActions) {
426
464
  return { fkOwner: b, backRelation: a };
427
465
  }
428
466
  const aLooksFk = looksLikeForeignKey({ column: a.rel.__fromColumn });
429
467
  const bLooksFk = looksLikeForeignKey({ column: b.rel.__fromColumn });
430
- return aLooksFk && !bLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
468
+ if (aLooksFk === bLooksFk)
469
+ return ownershipError();
470
+ return aLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
431
471
  }
432
472
  function looksLikeForeignKey(params) {
433
473
  const { column } = params;
@@ -453,7 +493,7 @@ function planExplicitRelations(params) {
453
493
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: true });
454
494
  continue;
455
495
  }
456
- const ownsFk = mergeInfo?.role === "fkOwner" || rel.__fromModel === modelName;
496
+ const ownsFk = mergeInfo?.role === "fkOwner" || rel.__ownsForeignKey !== false && rel.__fromModel === modelName;
457
497
  if (!ownsFk) {
458
498
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: rel.__optional });
459
499
  continue;
@@ -570,10 +610,13 @@ function planIndexes(params) {
570
610
  isUnique,
571
611
  columns: [...index.__columns],
572
612
  indexType: index.__indexType,
613
+ ...index.__map === undefined ? {} : { map: index.__map },
573
614
  ...index.__where === undefined ? {} : { where: index.__where },
574
615
  ...index.__nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
575
616
  ...index.__clientName === undefined ? {} : { clientName: index.__clientName }
576
617
  });
618
+ } else if (index.__map !== undefined) {
619
+ byKey.set(key, { ...existing, map: index.__map, indexType: index.__indexType ?? existing.indexType });
577
620
  } else if (index.__indexType !== undefined && existing.indexType === undefined) {
578
621
  byKey.set(key, { ...existing, indexType: index.__indexType });
579
622
  }
@@ -627,319 +670,639 @@ function findCounterparts(params) {
627
670
  return (plan.relations.get(relation.targetModel) ?? []).filter((candidate) => candidate !== relation && candidate.targetModel === relation.model && (candidate.relationName ?? "") === nameKey);
628
671
  }
629
672
 
630
- // packages/dsl/src/generator/prisma.ts
631
- function prismaType(params) {
673
+ // packages/dsl/src/to-ir.ts
674
+ function fieldType(params) {
632
675
  const { column } = params;
633
- const list = column.__config.array === true ? "[]" : "";
634
676
  switch (column.__columnType) {
635
677
  case "string":
636
- return `String${list}`;
678
+ return "String";
637
679
  case "int":
638
- return `Int${list}`;
680
+ return "Int";
639
681
  case "bigint":
640
- return `BigInt${list}`;
682
+ return "BigInt";
641
683
  case "float":
642
- return `Float${list}`;
684
+ return "Float";
643
685
  case "decimal":
644
- return `Decimal${list}`;
686
+ return "Decimal";
645
687
  case "boolean":
646
- return `Boolean${list}`;
688
+ return "Boolean";
647
689
  case "datetime":
648
- return `DateTime${list}`;
690
+ return "DateTime";
649
691
  case "json":
650
- return `Json${list}`;
692
+ return "Json";
651
693
  case "bytes":
652
- return `Bytes${list}`;
694
+ return "Bytes";
653
695
  case "id":
654
696
  return column.__config.type === "autoincrement" ? "Int" : "String";
655
697
  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
- }
698
+ return column.__enumName === undefined ? "String" : { enum: column.__enumName };
699
+ case "unsupported":
700
+ return "String";
666
701
  }
667
702
  }
668
- function idDefaultAttribute(params) {
669
- const idType = params.column.__config.type;
670
- switch (idType) {
703
+ function idDefault(params) {
704
+ switch (params.column.__config.type) {
671
705
  case "ulid":
706
+ return { kind: "ulid" };
672
707
  case "cuid":
708
+ return { kind: "cuid" };
673
709
  case "uuid":
710
+ return { kind: "uuid" };
674
711
  case "nanoid":
712
+ return { kind: "nanoid" };
675
713
  case "autoincrement":
676
- return `@default(${idType}())`;
714
+ return { kind: "autoincrement" };
677
715
  default:
678
716
  return;
679
717
  }
680
718
  }
681
- function defaultAttribute(params) {
719
+ function fieldDefault(params) {
682
720
  const { column } = params;
683
- const { dbgenerated, default: value } = column.__config;
684
- if (typeof dbgenerated === "string") {
685
- return `@default(dbgenerated(${encodePrismaString({ value: dbgenerated })}))`;
721
+ if (column.__columnType === "id") {
722
+ if (column.__config.default !== undefined || column.__config.dbgenerated !== undefined || column.__config.autoincrement !== undefined)
723
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
724
+ return idDefault({ column });
725
+ }
726
+ const { dbgenerated, default: value, autoincrement } = column.__config;
727
+ if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
728
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
686
729
  }
730
+ if (autoincrement === true) {
731
+ if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
732
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
733
+ }
734
+ return { kind: "autoincrement" };
735
+ }
736
+ if (typeof dbgenerated === "string")
737
+ return { kind: "dbgenerated", sql: dbgenerated };
687
738
  if (value === undefined)
688
739
  return;
689
- if (column.__columnType === "datetime" && value === "now") {
690
- return "@default(now())";
740
+ if (column.__config.array === true) {
741
+ if (!Array.isArray(value))
742
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
743
+ return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
691
744
  }
745
+ if (column.__columnType === "json" && typeof value !== "string") {
746
+ return { kind: "literal", value: serializeJsonDefault({ value }) };
747
+ }
748
+ if (column.__columnType === "datetime" && value === "now")
749
+ return { kind: "now" };
692
750
  if (column.__columnType === "enum" && typeof value === "string") {
693
- return `@default(${value})`;
751
+ return { kind: "enumValue", value };
752
+ }
753
+ if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
754
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
755
+ }
756
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
757
+ return { kind: "literal", value };
694
758
  }
695
- if (typeof value === "string")
696
- return `@default(${encodePrismaString({ value })})`;
697
- if (typeof value === "boolean" || typeof value === "number")
698
- return `@default(${value})`;
699
759
  if (typeof value === "bigint")
700
- return `@default(${value.toString()})`;
701
- return;
760
+ return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
761
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
702
762
  }
703
- function nativeTypeAttribute(params) {
763
+ function nativeType(params) {
704
764
  const { column } = params;
705
- if (column.__columnType === "string") {
765
+ if (column.__columnType === "enum") {
766
+ const { maxLength, text } = column.__config;
767
+ if (maxLength !== undefined || text !== undefined) {
768
+ const invalid = column.__enumStorage !== "string" || maxLength !== undefined && (typeof maxLength !== "number" || !Number.isSafeInteger(maxLength) || maxLength <= 0) || text !== undefined && typeof text !== "boolean" || text === true && maxLength !== undefined;
769
+ if (invalid) {
770
+ throw new VibeError2({
771
+ code: "VIBE_SCHEMA_INVALID",
772
+ message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
773
+ meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
774
+ });
775
+ }
776
+ }
777
+ }
778
+ if (column.__columnType === "string" || column.__columnType === "enum" && column.__enumStorage === "string") {
706
779
  const { maxLength, text } = column.__config;
707
780
  if (typeof maxLength === "number")
708
- return `@db.VarChar(${maxLength})`;
781
+ return `VarChar(${maxLength})`;
709
782
  if (text === true)
710
- return "@db.Text";
783
+ return "Text";
711
784
  return;
712
785
  }
713
786
  if (column.__columnType === "decimal") {
714
787
  const { precision, scale } = column.__config;
715
788
  if (typeof precision === "number")
716
- return `@db.Decimal(${precision}, ${scale ?? 0})`;
789
+ return `Decimal(${precision}, ${scale ?? 0})`;
717
790
  return;
718
791
  }
719
792
  if (column.__columnType === "datetime" && column.__config.tz === true) {
720
- return "@db.Timestamptz";
793
+ return "Timestamptz";
794
+ }
795
+ if (column.__columnType === "unsupported") {
796
+ return String(column.__config.dbType);
721
797
  }
722
798
  return;
723
799
  }
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 })})`);
751
- }
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
- `);
759
- }
760
- return columnLine;
800
+ function buildField(params) {
801
+ const { name, column, extensionNativeType } = params;
802
+ const config = column.__config;
803
+ const isList = column.__columnType !== "id" && config.array === true;
804
+ const dbName = typeof config.map === "string" ? config.map : undefined;
805
+ const documentation = typeof config.comment === "string" ? config.comment : undefined;
806
+ const collation = typeof config.collation === "string" ? config.collation : undefined;
807
+ const defaultValue = fieldDefault({ column });
808
+ const native = extensionNativeType ?? nativeType({ column });
809
+ return {
810
+ name,
811
+ type: fieldType({ column }),
812
+ isList,
813
+ isOptional: config.optional === true,
814
+ isId: column.__columnType === "id",
815
+ isUnique: config.unique === true,
816
+ isUpdatedAt: column.__columnType === "datetime" && config.updatedAt === true,
817
+ ...collation === undefined ? {} : { collation },
818
+ ...dbName === undefined ? {} : { dbName },
819
+ ...defaultValue === undefined ? {} : { default: defaultValue },
820
+ ...native === undefined ? {} : { nativeType: native },
821
+ ...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
822
+ ...documentation === undefined ? {} : { documentation }
823
+ };
761
824
  }
762
- function formatRelation(params) {
763
- const { relation } = params;
764
- const { name, targetModel, relationName } = relation;
765
- if (relation.kind === "list") {
766
- const attribute = relationName === undefined ? "" : ` @relation(${encodePrismaString({ value: relationName })})`;
767
- return ` ${name} ${targetModel}[]${attribute}`;
768
- }
769
- const optional = relation.isOptional ? "?" : "";
770
- if (relation.kind === "backOne") {
771
- const attribute = relationName === undefined ? "" : ` @relation(${encodePrismaString({ value: relationName })})`;
772
- return ` ${name} ${targetModel}${optional}${attribute}`;
773
- }
774
- const args = [];
775
- if (relationName !== undefined)
776
- args.push(encodePrismaString({ value: relationName }));
777
- if (relation.fields === undefined || relation.references === undefined) {
778
- throw internalError({
779
- message: `planned FK relation "${relation.model}.${name}" has no fields/references`,
780
- meta: { model: relation.model, relation: name }
781
- });
782
- }
783
- args.push(`fields: [${relation.fields.join(", ")}]`);
784
- args.push(`references: [${relation.references.join(", ")}]`);
785
- if (relation.onDelete !== undefined)
786
- args.push(`onDelete: ${relation.onDelete}`);
787
- if (relation.onUpdate !== undefined)
788
- args.push(`onUpdate: ${relation.onUpdate}`);
789
- return ` ${name} ${targetModel}${optional} @relation(${args.join(", ")})`;
825
+ function buildRelation(params) {
826
+ const { relation, plan } = params;
827
+ return {
828
+ name: relation.name,
829
+ model: relation.model,
830
+ targetModel: relation.targetModel,
831
+ type: resolveRelationType({ relation, plan }),
832
+ isList: relation.isList,
833
+ isOptional: relation.isOptional,
834
+ ...relation.relationName === undefined ? {} : { relationName: relation.relationName },
835
+ ...relation.fields === undefined ? {} : { fields: [...relation.fields] },
836
+ ...relation.references === undefined ? {} : { references: [...relation.references] },
837
+ ...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
838
+ ...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
839
+ };
790
840
  }
791
- function formatIndex(params) {
841
+ var INDEX_KINDS = {
842
+ BTree: "btree",
843
+ Hash: "hash",
844
+ Gin: "gin",
845
+ Gist: "gist",
846
+ SpGist: "spgist",
847
+ Brin: "brin"
848
+ };
849
+ function buildIndex(params) {
792
850
  const { index } = params;
793
- const columns = index.columns.join(", ");
794
- const whereArg = index.where === undefined ? "" : `, where: ${encodePrismaString({ value: index.where })}`;
795
- const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
796
- const nameArg = index.clientName === undefined ? "" : `, name: ${encodePrismaString({ value: index.clientName })}`;
797
- if (index.isUnique) {
798
- return ` @@unique([${columns}]${nameArg}${nndArg}${whereArg})`;
799
- }
800
- const typeArg = index.indexType === undefined ? "" : `, type: ${index.indexType}`;
801
- return ` @@index([${columns}]${typeArg}${whereArg})`;
802
- }
803
- function formatExtensionValue(params) {
804
- const { value } = params;
805
- if (typeof value === "string")
806
- return encodePrismaString({ value });
807
- if (Array.isArray(value)) {
808
- return `[${value.map((entry) => formatExtensionValue({ value: entry })).join(", ")}]`;
809
- }
810
- return String(value);
851
+ const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
852
+ return {
853
+ kind,
854
+ fields: index.columns.map((name) => ({ name })),
855
+ ...index.map === undefined ? {} : { name: index.map },
856
+ ...index.where === undefined ? {} : { where: index.where },
857
+ ...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
858
+ ...index.clientName === undefined ? {} : { clientName: index.clientName }
859
+ };
811
860
  }
812
- function formatModel(params) {
813
- const { model, plan } = params;
814
- const lines = [
815
- ...model.__comment === undefined ? [] : model.__comment.split(`
816
- `).map((line) => `/// ${line}`),
817
- `model ${model.__name} {`
818
- ];
819
- lines.push(" //> Columns");
861
+ function buildModel(params) {
862
+ const { model, plan, extensions, declarations, issues } = params;
863
+ const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
864
+ const policy = model.__policy === undefined ? undefined : { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field };
865
+ const fields = [];
820
866
  for (const [name, column] of model.__columns) {
821
- lines.push(formatColumn({ name, column }));
822
- }
823
- lines.push("");
824
- lines.push(" //> Relations");
825
- for (const relation of plan.relations.get(model.__name) ?? []) {
826
- lines.push(formatRelation({ relation }));
827
- }
828
- lines.push("");
829
- lines.push(" //> Indexes");
830
- lines.push("");
831
- for (const index of plan.indexes.get(model.__name) ?? []) {
832
- lines.push(formatIndex({ index }));
833
- }
834
- for (const check of model.__checks ?? []) {
835
- lines.push(` @@check(${encodePrismaString({ value: check.expression })}, name: ${encodePrismaString({ value: check.name })})`);
867
+ const extensionTypeName = column.__config["extensionType"];
868
+ if (typeof extensionTypeName !== "string") {
869
+ fields.push(buildField({ name, column }));
870
+ continue;
871
+ }
872
+ const registered = extensions.fieldTypes.get(extensionTypeName);
873
+ if (registered === undefined) {
874
+ issues.push({
875
+ rule: "extension-attribute",
876
+ message: `Field "${model.__name}.${name}" uses extension type "${extensionTypeName}" but no configured extension provides it`,
877
+ model: model.__name,
878
+ field: name
879
+ });
880
+ continue;
881
+ }
882
+ const rawArgs = column.__config["extensionArgs"];
883
+ const lowered = registered.fieldType.lower({
884
+ model: model.__name,
885
+ field: name,
886
+ args: Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string" || typeof entry === "number") : []
887
+ });
888
+ if ("error" in lowered) {
889
+ issues.push({
890
+ rule: "extension-attribute",
891
+ message: `Field "${model.__name}.${name}": ${lowered.error}`,
892
+ model: model.__name,
893
+ field: name
894
+ });
895
+ continue;
896
+ }
897
+ fields.push(buildField({ name, column, extensionNativeType: lowered.nativeType }));
898
+ if (lowered.declaration !== undefined) {
899
+ declarations.add({
900
+ extension: registered.extension.name,
901
+ model: model.__name,
902
+ declaration: lowered.declaration
903
+ });
904
+ }
836
905
  }
837
906
  for (const use of model.__extensions ?? []) {
838
- const args = [];
839
- if (use.fields !== undefined)
840
- args.push(`[${use.fields.join(", ")}]`);
841
- for (const [key, value] of Object.entries(use.args ?? {})) {
842
- args.push(`${key}: ${formatExtensionValue({ value })}`);
907
+ const registered = extensions.modelAttributes.get(use.attribute);
908
+ if (registered === undefined) {
909
+ issues.push({
910
+ rule: "unknown-attribute",
911
+ message: `Unknown block attribute "@@${use.attribute}" on model "${model.__name}" \u2014 no extension provides it`,
912
+ model: model.__name
913
+ });
914
+ continue;
843
915
  }
844
- lines.push(` @@${use.attribute}(${args.join(", ")})`);
845
- }
846
- if (model.__primaryKey !== undefined && model.__primaryKey.length > 0) {
847
- lines.push(` @@id([${model.__primaryKey.join(", ")}])`);
848
- }
849
- if (model.__policy !== undefined) {
850
- const { field, context } = model.__policy;
851
- const contextArg = context === undefined || context === field ? "" : `, context: ${encodePrismaString({ value: context })}`;
852
- lines.push(` @@policy([${field}]${contextArg})`);
853
- }
854
- if (typeof model.__map === "string") {
855
- lines.push(` @@map(${encodePrismaString({ value: model.__map })})`);
916
+ const parsed = registered.attribute.parse({
917
+ model: model.__name,
918
+ args: {
919
+ named: { ...use.args ?? {} },
920
+ ...use.fields === undefined ? {} : { fields: use.fields }
921
+ },
922
+ fieldNames: fields.map((field) => field.name)
923
+ });
924
+ if ("error" in parsed) {
925
+ issues.push({
926
+ rule: "extension-attribute",
927
+ message: `@@${use.attribute} on "${model.__name}": ${parsed.error}`,
928
+ model: model.__name
929
+ });
930
+ continue;
931
+ }
932
+ declarations.add({
933
+ extension: registered.extension.name,
934
+ model: model.__name,
935
+ declaration: parsed.declaration
936
+ });
856
937
  }
857
- lines.push("}");
858
- return lines.join(`
859
- `);
860
- }
861
- function formatEnum(params) {
862
- const { name, values } = params;
863
- return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
864
- `);
938
+ return {
939
+ name: model.__name,
940
+ ...model.__map === undefined ? {} : { dbName: model.__map },
941
+ fields,
942
+ relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
943
+ indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index })),
944
+ ...model.__checks === undefined || model.__checks.length === 0 ? {} : { checks: model.__checks.map((check) => ({ name: check.name, expression: check.expression })) },
945
+ ...policy === undefined ? {} : { policy },
946
+ ...primaryKey === undefined ? {} : { primaryKey },
947
+ isView: false,
948
+ ...model.__comment === undefined ? {} : { documentation: model.__comment }
949
+ };
865
950
  }
866
- function generatePrisma(schema, _config = {}) {
867
- const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
868
- if (stringEnums.length > 0) {
869
- throw new VibeError({
870
- code: "VIBE_CONFIG",
871
- 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
- meta: { enums: stringEnums, target: "prisma" }
873
- });
951
+
952
+ class ExtensionDeclarationCollector {
953
+ byExtension = new Map;
954
+ add(params) {
955
+ const perModel = this.byExtension.get(params.extension) ?? new Map;
956
+ this.byExtension.set(params.extension, perModel);
957
+ const existing = perModel.get(params.model) ?? {};
958
+ perModel.set(params.model, mergeExtensionDeclarations({ base: existing, next: params.declaration }));
874
959
  }
875
- if (schema.__rls !== undefined) {
876
- throw new VibeError({
877
- code: "VIBE_CONFIG",
878
- message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
879
- meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
880
- });
960
+ toIR() {
961
+ return [...this.byExtension.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, perModel]) => ({
962
+ name,
963
+ options: {
964
+ models: Object.fromEntries([...perModel.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
965
+ }
966
+ }));
881
967
  }
968
+ }
969
+ function toIR(params) {
970
+ const { schema, provider = "postgresql" } = params;
971
+ const extensions = params.extensions ?? EMPTY_EXTENSION_REGISTRY;
882
972
  const plan = planSchema({ schema });
883
- const sections = [];
884
- if (schema.__enums.size > 0) {
885
- sections.push("// ============================================");
886
- sections.push("// Enums");
887
- sections.push("// ============================================");
888
- sections.push("");
889
- for (const [name, values] of schema.__enums) {
890
- sections.push(formatEnum({ name, values }));
891
- sections.push("");
973
+ const declarations = new ExtensionDeclarationCollector;
974
+ const issues = [];
975
+ const models = Array.from(schema.__models.values(), (model) => {
976
+ const built = buildModel({ model, plan, extensions, declarations, issues });
977
+ const rls = schema.__rls?.policies[model.__name];
978
+ return rls === undefined ? built : { ...built, rls };
979
+ });
980
+ if (schema.__rls !== undefined) {
981
+ for (const name of Object.keys(schema.__rls.policies)) {
982
+ if (!schema.__models.has(name)) {
983
+ issues.push({ rule: "rls-unknown-model", model: name, message: `RLS classification references model "${name}" absent from the composed schema` });
984
+ }
892
985
  }
893
986
  }
894
- sections.push("// ============================================");
895
- sections.push("// Models");
896
- sections.push("// ============================================");
897
- sections.push("");
898
- for (const model of schema.__models.values()) {
899
- sections.push(formatModel({ model, plan }));
900
- sections.push("");
987
+ const enums = Array.from(schema.__enums, ([name, values]) => ({
988
+ name,
989
+ values: [...values],
990
+ ...schema.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
991
+ }));
992
+ if (issues.length > 0) {
993
+ throw new VibeError2({
994
+ code: "VIBE_SCHEMA_INVALID",
995
+ message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
996
+ meta: { issues }
997
+ });
901
998
  }
902
- return sections.join(`
903
- `).trim();
999
+ const ir = {
1000
+ datasource: { provider },
1001
+ models,
1002
+ enums,
1003
+ extensions: declarations.toIR(),
1004
+ ...schema.__rls === undefined ? {} : { rls: { context: schema.__rls.context } }
1005
+ };
1006
+ assertValidSchemaIR({ schema: ir });
1007
+ return ir;
904
1008
  }
905
1009
 
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
- };
1010
+ // packages/dsl/src/generator/prisma.ts
1011
+ function prismaType(params) {
1012
+ const { column } = params;
1013
+ const list = column.__config.array === true ? "[]" : "";
1014
+ switch (column.__columnType) {
1015
+ case "string":
1016
+ return `String${list}`;
1017
+ case "int":
1018
+ return `Int${list}`;
1019
+ case "bigint":
1020
+ return `BigInt${list}`;
1021
+ case "float":
1022
+ return `Float${list}`;
1023
+ case "decimal":
1024
+ return `Decimal${list}`;
1025
+ case "boolean":
1026
+ return `Boolean${list}`;
1027
+ case "datetime":
1028
+ return `DateTime${list}`;
1029
+ case "json":
1030
+ return `Json${list}`;
1031
+ case "bytes":
1032
+ return `Bytes${list}`;
1033
+ case "id":
1034
+ return column.__config.type === "autoincrement" ? "Int" : "String";
1035
+ case "enum":
1036
+ return column.__enumName === undefined ? `String${list}` : `${column.__enumName}${list}`;
1037
+ case "unsupported": {
1038
+ const extensionTypeName = column.__config["extensionType"];
1039
+ if (typeof extensionTypeName === "string") {
1040
+ const rawArgs = column.__config["extensionArgs"];
1041
+ const args = Array.isArray(rawArgs) ? rawArgs.map(String).join(", ") : "";
1042
+ return `${extensionTypeName}${args.length > 0 ? `(${args})` : ""}${list}`;
1043
+ }
1044
+ return `Unsupported(${encodePrismaString({ value: String(column.__config.dbType) })})${list}`;
921
1045
  }
922
- };
1046
+ }
923
1047
  }
924
- function extractRefInfo(params) {
925
- const { ref } = params;
926
- return { model: ref.__model, column: ref.__column };
1048
+ function defaultAttribute(params) {
1049
+ const { column } = params;
1050
+ const value = fieldDefault({ column });
1051
+ if (value === undefined)
1052
+ return;
1053
+ if (value.kind === "list") {
1054
+ const values = value.values.map((entry) => column.__columnType === "enum" ? String(entry) : column.__columnType === "bigint" ? String(entry) : JSON.stringify(entry));
1055
+ return `@default([${values.join(", ")}])`;
1056
+ }
1057
+ if (value.kind === "literal")
1058
+ return `@default(${typeof value.value === "string" ? encodePrismaString({ value: value.value }) : String(value.value)})`;
1059
+ if (value.kind === "enumValue")
1060
+ return `@default(${value.value})`;
1061
+ if (value.kind === "dbgenerated")
1062
+ return `@default(dbgenerated(${encodePrismaString({ value: value.sql })}))`;
1063
+ return `@default(${value.kind}())`;
927
1064
  }
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;
1065
+ function nativeTypeAttribute(params) {
1066
+ const { column } = params;
1067
+ if (column.__columnType === "string") {
1068
+ const { maxLength, text } = column.__config;
1069
+ if (typeof maxLength === "number")
1070
+ return `@db.VarChar(${maxLength})`;
1071
+ if (text === true)
1072
+ return "@db.Text";
1073
+ return;
1074
+ }
1075
+ if (column.__columnType === "decimal") {
1076
+ const { precision, scale } = column.__config;
1077
+ if (typeof precision === "number")
1078
+ return `@db.Decimal(${precision}, ${scale ?? 0})`;
1079
+ return;
1080
+ }
1081
+ if (column.__columnType === "datetime" && column.__config.tz === true) {
1082
+ return "@db.Timestamptz";
1083
+ }
1084
+ return;
1085
+ }
1086
+ function formatColumn(params) {
1087
+ const { name, column } = params;
1088
+ const parts = [` ${name} ${prismaType({ column })}${column.__config.optional === true ? "?" : ""}`];
1089
+ if (column.__columnType === "id") {
1090
+ parts.push("@id");
1091
+ const idDefault2 = defaultAttribute({ column });
1092
+ if (idDefault2 !== undefined)
1093
+ parts.push(idDefault2);
1094
+ } else {
1095
+ const columnDefault = defaultAttribute({ column });
1096
+ if (columnDefault !== undefined)
1097
+ parts.push(columnDefault);
1098
+ }
1099
+ if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
1100
+ parts.push("@updatedAt");
1101
+ }
1102
+ if (column.__config.unique === true) {
1103
+ parts.push("@unique");
1104
+ }
1105
+ if (typeof column.__config.map === "string") {
1106
+ parts.push(`@map(${encodePrismaString({ value: column.__config.map })})`);
1107
+ }
1108
+ const nativeType2 = nativeTypeAttribute({ column });
1109
+ if (nativeType2 !== undefined)
1110
+ parts.push(nativeType2);
1111
+ if (typeof column.__config.collation === "string") {
1112
+ parts.push(`@collation(${encodePrismaString({ value: column.__config.collation })})`);
1113
+ }
1114
+ const columnLine = parts.join(" ");
1115
+ const comment = column.__config.comment;
1116
+ if (typeof comment === "string") {
1117
+ const commentLines = comment.split(`
1118
+ `).map((line) => ` /// ${line}`);
1119
+ return [...commentLines, columnLine].join(`
1120
+ `);
1121
+ }
1122
+ return columnLine;
1123
+ }
1124
+ function formatRelation(params) {
1125
+ const { relation } = params;
1126
+ const { name, targetModel, relationName } = relation;
1127
+ if (relation.kind === "list") {
1128
+ const attribute = relationName === undefined ? "" : ` @relation(${encodePrismaString({ value: relationName })})`;
1129
+ return ` ${name} ${targetModel}[]${attribute}`;
1130
+ }
1131
+ const optional = relation.isOptional ? "?" : "";
1132
+ if (relation.kind === "backOne") {
1133
+ const attribute = relationName === undefined ? "" : ` @relation(${encodePrismaString({ value: relationName })})`;
1134
+ return ` ${name} ${targetModel}${optional}${attribute}`;
1135
+ }
1136
+ const args = [];
1137
+ if (relationName !== undefined)
1138
+ args.push(encodePrismaString({ value: relationName }));
1139
+ if (relation.fields === undefined || relation.references === undefined) {
1140
+ throw internalError({
1141
+ message: `planned FK relation "${relation.model}.${name}" has no fields/references`,
1142
+ meta: { model: relation.model, relation: name }
1143
+ });
1144
+ }
1145
+ args.push(`fields: [${relation.fields.join(", ")}]`);
1146
+ args.push(`references: [${relation.references.join(", ")}]`);
1147
+ if (relation.onDelete !== undefined)
1148
+ args.push(`onDelete: ${relation.onDelete}`);
1149
+ if (relation.onUpdate !== undefined)
1150
+ args.push(`onUpdate: ${relation.onUpdate}`);
1151
+ return ` ${name} ${targetModel}${optional} @relation(${args.join(", ")})`;
1152
+ }
1153
+ function formatIndex(params) {
1154
+ const { index } = params;
1155
+ const columns = index.columns.join(", ");
1156
+ const whereArg = index.where === undefined ? "" : `, where: ${encodePrismaString({ value: index.where })}`;
1157
+ const mapArg = index.map === undefined ? "" : `, map: ${encodePrismaString({ value: index.map })}`;
1158
+ const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
1159
+ const nameArg = index.clientName === undefined ? "" : `, name: ${encodePrismaString({ value: index.clientName })}`;
1160
+ if (index.isUnique) {
1161
+ return ` @@unique([${columns}]${nameArg}${mapArg}${nndArg}${whereArg})`;
1162
+ }
1163
+ const typeArg = index.indexType === undefined ? "" : `, type: ${index.indexType}`;
1164
+ return ` @@index([${columns}]${typeArg}${mapArg}${whereArg})`;
1165
+ }
1166
+ function formatExtensionValue(params) {
1167
+ const { value } = params;
1168
+ if (typeof value === "string")
1169
+ return encodePrismaString({ value });
1170
+ if (Array.isArray(value)) {
1171
+ return `[${value.map((entry) => formatExtensionValue({ value: entry })).join(", ")}]`;
1172
+ }
1173
+ return String(value);
1174
+ }
1175
+ function formatModel(params) {
1176
+ const { model, plan } = params;
1177
+ const lines = [
1178
+ ...model.__comment === undefined ? [] : model.__comment.split(`
1179
+ `).map((line) => `/// ${line}`),
1180
+ `model ${model.__name} {`
1181
+ ];
1182
+ lines.push(" //> Columns");
1183
+ for (const [name, column] of model.__columns) {
1184
+ lines.push(formatColumn({ name, column }));
1185
+ }
1186
+ lines.push("");
1187
+ lines.push(" //> Relations");
1188
+ for (const relation of plan.relations.get(model.__name) ?? []) {
1189
+ lines.push(formatRelation({ relation }));
1190
+ }
1191
+ lines.push("");
1192
+ lines.push(" //> Indexes");
1193
+ lines.push("");
1194
+ for (const index of plan.indexes.get(model.__name) ?? []) {
1195
+ lines.push(formatIndex({ index }));
1196
+ }
1197
+ for (const check of model.__checks ?? []) {
1198
+ lines.push(` @@check(${encodePrismaString({ value: check.expression })}, name: ${encodePrismaString({ value: check.name })})`);
1199
+ }
1200
+ for (const use of model.__extensions ?? []) {
1201
+ const args = [];
1202
+ if (use.fields !== undefined)
1203
+ args.push(`[${use.fields.join(", ")}]`);
1204
+ for (const [key, value] of Object.entries(use.args ?? {})) {
1205
+ args.push(`${key}: ${formatExtensionValue({ value })}`);
1206
+ }
1207
+ lines.push(` @@${use.attribute}(${args.join(", ")})`);
1208
+ }
1209
+ if (model.__primaryKey !== undefined && model.__primaryKey.length > 0) {
1210
+ lines.push(` @@id([${model.__primaryKey.join(", ")}])`);
1211
+ }
1212
+ if (model.__policy !== undefined) {
1213
+ const { field, context } = model.__policy;
1214
+ const contextArg = context === undefined || context === field ? "" : `, context: ${encodePrismaString({ value: context })}`;
1215
+ lines.push(` @@policy([${field}]${contextArg})`);
1216
+ }
1217
+ if (typeof model.__map === "string") {
1218
+ lines.push(` @@map(${encodePrismaString({ value: model.__map })})`);
1219
+ }
1220
+ lines.push("}");
1221
+ return lines.join(`
1222
+ `);
1223
+ }
1224
+ function formatEnum(params) {
1225
+ const { name, values } = params;
1226
+ return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
1227
+ `);
1228
+ }
1229
+ function generatePrisma(schema, _config = {}) {
1230
+ const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
1231
+ if (stringEnums.length > 0) {
1232
+ throw new VibeError3({
1233
+ code: "VIBE_CONFIG",
1234
+ 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.`,
1235
+ meta: { enums: stringEnums, target: "prisma" }
1236
+ });
1237
+ }
1238
+ if (schema.__rls !== undefined) {
1239
+ throw new VibeError3({
1240
+ code: "VIBE_CONFIG",
1241
+ message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
1242
+ meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
1243
+ });
1244
+ }
1245
+ const plan = planSchema({ schema });
1246
+ const sections = [];
1247
+ if (schema.__enums.size > 0) {
1248
+ sections.push("// ============================================");
1249
+ sections.push("// Enums");
1250
+ sections.push("// ============================================");
1251
+ sections.push("");
1252
+ for (const [name, values] of schema.__enums) {
1253
+ sections.push(formatEnum({ name, values }));
1254
+ sections.push("");
1255
+ }
1256
+ }
1257
+ sections.push("// ============================================");
1258
+ sections.push("// Models");
1259
+ sections.push("// ============================================");
1260
+ sections.push("");
1261
+ for (const model of schema.__models.values()) {
1262
+ sections.push(formatModel({ model, plan }));
1263
+ sections.push("");
1264
+ }
1265
+ return sections.join(`
1266
+ `).trim();
1267
+ }
1268
+
1269
+ // packages/dsl/src/relations.ts
1270
+ function createColumnRef(params) {
1271
+ const { model, column } = params;
1272
+ return {
1273
+ __type: "columnRef",
1274
+ __model: model,
1275
+ __column: column,
1276
+ through(junction) {
1277
+ return {
1278
+ __type: "throughRef",
1279
+ __model: model,
1280
+ __column: column,
1281
+ __junction: junction.__model,
1282
+ __junctionColumn: junction.__column
1283
+ };
1284
+ }
1285
+ };
1286
+ }
1287
+ function extractRefInfo(params) {
1288
+ const { ref } = params;
1289
+ return { model: ref.__model, column: ref.__column };
1290
+ }
1291
+ function createRelationFactory(params) {
1292
+ const { relationType, targetModelName } = params;
1293
+ return (config) => {
1294
+ const from = extractRefInfo({ ref: config.from });
1295
+ const to = extractRefInfo({ ref: config.to });
1296
+ let throughJunction;
1297
+ let throughFromCol;
1298
+ let throughToCol;
1299
+ if (config.from.__type === "throughRef") {
1300
+ throughJunction = config.from.__junction;
1301
+ throughFromCol = config.from.__junctionColumn;
1302
+ }
1303
+ if (config.to.__type === "throughRef") {
1304
+ throughJunction = throughJunction ?? config.to.__junction;
1305
+ throughToCol = config.to.__junctionColumn;
943
1306
  }
944
1307
  return {
945
1308
  __type: "storedRelation",
@@ -954,6 +1317,7 @@ function createRelationFactory(params) {
954
1317
  __throughToCol: throughToCol,
955
1318
  __optional: config.optional ?? relationType === "many",
956
1319
  __alias: config.alias,
1320
+ ..."ownsForeignKey" in config ? { __ownsForeignKey: config.ownsForeignKey } : {},
957
1321
  __onDelete: config.onDelete,
958
1322
  __onUpdate: config.onUpdate
959
1323
  };
@@ -1012,7 +1376,7 @@ function mergeRelations(...parts) {
1012
1376
  }
1013
1377
 
1014
1378
  // packages/dsl/src/schema.ts
1015
- import { VibeError as VibeError2, validateRlsContextIR } from "@vibeorm/schema";
1379
+ import { VibeError as VibeError4, validateRlsContextIR } from "@vibeorm/schema";
1016
1380
  function sameValues(left, right) {
1017
1381
  return left.length === right.length && left.every((value, index) => value === right[index]);
1018
1382
  }
@@ -1031,7 +1395,7 @@ function registerEnum(params) {
1031
1395
  ...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
1032
1396
  ...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
1033
1397
  ].join("; ");
1034
- throw new VibeError2({
1398
+ throw new VibeError4({
1035
1399
  code: "VIBE_SCHEMA_INVALID",
1036
1400
  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
1401
  meta: {
@@ -1057,7 +1421,7 @@ function addModelTree(params) {
1057
1421
  const { model, models, origin } = params;
1058
1422
  const existing = models.get(model.__name);
1059
1423
  if (existing !== undefined && existing !== model) {
1060
- throw new VibeError2({
1424
+ throw new VibeError4({
1061
1425
  code: "VIBE_SCHEMA_INVALID",
1062
1426
  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
1427
  meta: {
@@ -1135,7 +1499,7 @@ var RLS_CONTEXT_TYPES = {
1135
1499
  };
1136
1500
  function defineRls(params) {
1137
1501
  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" }] } });
1502
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1139
1503
  }
1140
1504
  const entries = [];
1141
1505
  const refs = [];
@@ -1143,7 +1507,7 @@ function defineRls(params) {
1143
1507
  const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
1144
1508
  const column = descriptor?.value;
1145
1509
  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" }] } });
1510
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1147
1511
  }
1148
1512
  entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
1149
1513
  refs.push([key, Object.freeze({ context: key })]);
@@ -1151,22 +1515,22 @@ function defineRls(params) {
1151
1515
  const context = Object.freeze(Object.fromEntries(entries));
1152
1516
  const issues = validateRlsContextIR({ context });
1153
1517
  if (issues.length > 0) {
1154
- throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1518
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1155
1519
  }
1156
1520
  const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
1157
1521
  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" }] } });
1522
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1159
1523
  }
1160
1524
  const named = [];
1161
1525
  const classified = new Set;
1162
1526
  for (const key of Reflect.ownKeys(policies)) {
1163
1527
  const descriptor = Object.getOwnPropertyDescriptor(policies, key);
1164
1528
  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" }] } });
1529
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1166
1530
  }
1167
1531
  const name = params.models[key]?.__name ?? key;
1168
1532
  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 }] } });
1533
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
1170
1534
  }
1171
1535
  classified.add(name);
1172
1536
  named.push([name, descriptor.value]);
@@ -1295,326 +1659,6 @@ var templates = {
1295
1659
  fields
1296
1660
  };
1297
1661
 
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;
1349
- }
1350
- }
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 };
1367
- }
1368
- if (typeof value === "bigint")
1369
- return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
1370
- return;
1371
- }
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
- }
1385
- }
1386
- }
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";
1393
- return;
1394
- }
1395
- if (column.__columnType === "decimal") {
1396
- const { precision, scale } = column.__config;
1397
- if (typeof precision === "number")
1398
- return `Decimal(${precision}, ${scale ?? 0})`;
1399
- 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;
1408
- }
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
- };
1433
- }
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
- };
1449
- }
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
- };
1468
- }
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
1487
- });
1488
- continue;
1489
- }
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
1502
- });
1503
- continue;
1504
- }
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
- });
1512
- }
1513
- }
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;
1523
- }
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;
1539
- }
1540
- declarations.add({
1541
- extension: registered.extension.name,
1542
- model: model.__name,
1543
- declaration: parsed.declaration
1544
- });
1545
- }
1546
- 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 }
1557
- };
1558
- }
1559
-
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 }));
1567
- }
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
- }));
1575
- }
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
- }
1594
- }
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
- });
1606
- }
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
- }
1617
-
1618
1662
  // packages/dsl/src/validate.ts
1619
1663
  function validateSchema(schema2) {
1620
1664
  const errors = [];
@@ -1773,6 +1817,7 @@ export {
1773
1817
  baseWithWorkspace,
1774
1818
  base,
1775
1819
  ChainableStringColumn,
1820
+ ChainableIntegerColumn,
1776
1821
  ChainableIdColumn,
1777
1822
  ChainableEnumColumn,
1778
1823
  ChainableDecimalColumn,
@@ -1780,4 +1825,4 @@ export {
1780
1825
  ChainableColumn
1781
1826
  };
1782
1827
 
1783
- //# debugId=EC151CEC887B44C364756E2164756E21
1828
+ //# debugId=88D4E8D0B695042E64756E2164756E21