@vibeorm/dsl 2.0.0-alpha.7 → 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
@@ -8,7 +8,8 @@ function cloneColumnDef(params) {
8
8
  __name: column.__name,
9
9
  __config: { ...column.__config },
10
10
  __enumValues: column.__enumValues,
11
- __enumName: column.__enumName
11
+ __enumName: column.__enumName,
12
+ __enumStorage: column.__enumStorage
12
13
  };
13
14
  }
14
15
 
@@ -19,6 +20,7 @@ class ChainableColumn {
19
20
  __name;
20
21
  __enumValues;
21
22
  __enumName;
23
+ __enumStorage;
22
24
  constructor(columnType, config = {}) {
23
25
  this.__columnType = columnType;
24
26
  this.__config = { ...config };
@@ -69,6 +71,10 @@ class ChainableStringColumn extends ChainableColumn {
69
71
  this.__config.text = true;
70
72
  return this;
71
73
  }
74
+ collation(name) {
75
+ this.__config.collation = name;
76
+ return this;
77
+ }
72
78
  }
73
79
 
74
80
  class ChainableDecimalColumn extends ChainableColumn {
@@ -119,6 +125,9 @@ class ChainableEnumColumn extends ChainableColumn {
119
125
  if (params.enumName !== undefined) {
120
126
  this.__enumName = params.enumName;
121
127
  }
128
+ if (params.storage !== undefined) {
129
+ this.__enumStorage = params.storage;
130
+ }
122
131
  }
123
132
  default(value) {
124
133
  this.__config.default = value;
@@ -128,8 +137,15 @@ class ChainableEnumColumn extends ChainableColumn {
128
137
  function string(config = {}) {
129
138
  return new ChainableStringColumn(config);
130
139
  }
140
+
141
+ class ChainableIntegerColumn extends ChainableColumn {
142
+ autoincrement() {
143
+ this.__config.autoincrement = true;
144
+ return this;
145
+ }
146
+ }
131
147
  function int(config = {}) {
132
- return new ChainableColumn("int", config);
148
+ return new ChainableIntegerColumn("int", config);
133
149
  }
134
150
  function float(config = {}) {
135
151
  return new ChainableColumn("float", config);
@@ -164,7 +180,7 @@ function bytes(config = {}) {
164
180
  return new ChainableColumn("bytes", config);
165
181
  }
166
182
  function bigint(config = {}) {
167
- return new ChainableColumn("bigint", config);
183
+ return new ChainableIntegerColumn("bigint", config);
168
184
  }
169
185
  function unsupported(dbType) {
170
186
  const config = { dbType };
@@ -180,25 +196,49 @@ function extensionType(config) {
180
196
  });
181
197
  }
182
198
  function enumCol(config) {
183
- const { options, ...restConfig } = config;
184
- return new ChainableEnumColumn({ options, config: restConfig });
199
+ const { options, storage, ...restConfig } = config;
200
+ return new ChainableEnumColumn({
201
+ options,
202
+ config: restConfig,
203
+ ...storage === undefined ? {} : { storage }
204
+ });
185
205
  }
186
206
 
187
207
  // packages/dsl/src/enum.ts
208
+ function enumValuesOf(params) {
209
+ const entries = Object.fromEntries(params.options.map((option) => [option, option]));
210
+ return Object.freeze(entries);
211
+ }
188
212
  function createEnum(config) {
189
- const { name, options } = config;
213
+ const { name, options, storage = "database" } = config;
190
214
  return {
191
215
  __type: "enum",
192
216
  __name: name,
193
217
  __options: options,
218
+ __storage: storage,
219
+ values: enumValuesOf({ options }),
194
220
  field(fieldConfig = {}) {
195
- return new ChainableEnumColumn({ options, config: fieldConfig, enumName: name });
221
+ return new ChainableEnumColumn({ options, config: fieldConfig, enumName: name, storage });
196
222
  }
197
223
  };
198
224
  }
199
225
 
200
226
  // packages/dsl/src/generator/prisma.ts
201
- import { 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";
202
242
 
203
243
  // packages/dsl/src/naming.ts
204
244
  function capitalize(params) {
@@ -237,17 +277,18 @@ function isChildrenDef(value) {
237
277
  return typeof value === "object" && value !== null && "__type" in value && value.__type === "children";
238
278
  }
239
279
  function createIndexBuilder() {
240
- const createIndex = (columns, type, where) => ({
280
+ const createIndex = (columns, type, where, map) => ({
241
281
  __type: "index",
242
282
  __columns: [...columns],
243
283
  __indexType: type,
284
+ ...map === undefined ? {} : { __map: map },
244
285
  ...where === undefined ? {} : { __where: where }
245
286
  });
246
287
  return {
247
288
  on(...args) {
248
289
  const [first, second] = args;
249
290
  if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
250
- return createIndex(first, second.type, second.where);
291
+ return createIndex(first, second.type, second.where, second.map);
251
292
  }
252
293
  return createIndex(args);
253
294
  },
@@ -260,6 +301,7 @@ function createIndexBuilder() {
260
301
  return {
261
302
  __type: "unique",
262
303
  __columns: [...first],
304
+ ...second.map === undefined ? {} : { __map: second.map },
263
305
  ...second.where === undefined ? {} : { __where: second.where },
264
306
  ...second.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
265
307
  ...second.name === undefined ? {} : { __clientName: second.name }
@@ -320,7 +362,9 @@ function create(config) {
320
362
  __map: config.map,
321
363
  __primaryKey: config.primaryKey,
322
364
  __checks: config.checks,
323
- __extensions: config.extensions
365
+ __extensions: config.extensions,
366
+ __policy: config.policy,
367
+ __comment: config.comment
324
368
  };
325
369
  }
326
370
  function getColumnNames(params) {
@@ -341,6 +385,9 @@ function detectOneToOnePairs(params) {
341
385
  const allOnes = [];
342
386
  for (const [modelName, modelRelations] of Object.entries(relations)) {
343
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
+ }
344
391
  if (rel.__relationType === "one") {
345
392
  allOnes.push({ modelName, relName, rel });
346
393
  }
@@ -362,7 +409,7 @@ function detectOneToOnePairs(params) {
362
409
  if (paired.has(keyB))
363
410
  continue;
364
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;
365
- if (!sameColumnPair)
412
+ if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
366
413
  continue;
367
414
  const { fkOwner, backRelation } = resolveFkOwner({ a, b, models });
368
415
  const onDelete = fkOwner.rel.__onDelete ?? backRelation.rel.__onDelete;
@@ -392,21 +439,35 @@ function detectOneToOnePairs(params) {
392
439
  }
393
440
  function resolveFkOwner(params) {
394
441
  const { a, b, models } = params;
395
- const aFromIsId = models.get(a.modelName)?.__columns.get(a.rel.__fromColumn)?.__columnType === "id";
396
- 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;
397
454
  if (aFromIsId && !bFromIsId)
398
455
  return { fkOwner: b, backRelation: a };
399
456
  if (!aFromIsId && bFromIsId)
400
457
  return { fkOwner: a, backRelation: b };
401
- 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) {
402
461
  return { fkOwner: a, backRelation: b };
403
462
  }
404
- if (b.rel.__onDelete !== undefined || b.rel.__onUpdate !== undefined) {
463
+ if (bActions && !aActions) {
405
464
  return { fkOwner: b, backRelation: a };
406
465
  }
407
466
  const aLooksFk = looksLikeForeignKey({ column: a.rel.__fromColumn });
408
467
  const bLooksFk = looksLikeForeignKey({ column: b.rel.__fromColumn });
409
- 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 };
410
471
  }
411
472
  function looksLikeForeignKey(params) {
412
473
  const { column } = params;
@@ -432,7 +493,7 @@ function planExplicitRelations(params) {
432
493
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: true });
433
494
  continue;
434
495
  }
435
- const ownsFk = mergeInfo?.role === "fkOwner" || rel.__fromModel === modelName;
496
+ const ownsFk = mergeInfo?.role === "fkOwner" || rel.__ownsForeignKey !== false && rel.__fromModel === modelName;
436
497
  if (!ownsFk) {
437
498
  planned.push({ ...base, kind: "backOne", isList: false, isOptional: rel.__optional });
438
499
  continue;
@@ -549,10 +610,13 @@ function planIndexes(params) {
549
610
  isUnique,
550
611
  columns: [...index.__columns],
551
612
  indexType: index.__indexType,
613
+ ...index.__map === undefined ? {} : { map: index.__map },
552
614
  ...index.__where === undefined ? {} : { where: index.__where },
553
615
  ...index.__nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
554
616
  ...index.__clientName === undefined ? {} : { clientName: index.__clientName }
555
617
  });
618
+ } else if (index.__map !== undefined) {
619
+ byKey.set(key, { ...existing, map: index.__map, indexType: index.__indexType ?? existing.indexType });
556
620
  } else if (index.__indexType !== undefined && existing.indexType === undefined) {
557
621
  byKey.set(key, { ...existing, indexType: index.__indexType });
558
622
  }
@@ -606,815 +670,994 @@ function findCounterparts(params) {
606
670
  return (plan.relations.get(relation.targetModel) ?? []).filter((candidate) => candidate !== relation && candidate.targetModel === relation.model && (candidate.relationName ?? "") === nameKey);
607
671
  }
608
672
 
609
- // packages/dsl/src/generator/prisma.ts
610
- function prismaType(params) {
673
+ // packages/dsl/src/to-ir.ts
674
+ function fieldType(params) {
611
675
  const { column } = params;
612
- const list = column.__config.array === true ? "[]" : "";
613
676
  switch (column.__columnType) {
614
677
  case "string":
615
- return `String${list}`;
678
+ return "String";
616
679
  case "int":
617
- return `Int${list}`;
680
+ return "Int";
618
681
  case "bigint":
619
- return `BigInt${list}`;
682
+ return "BigInt";
620
683
  case "float":
621
- return `Float${list}`;
684
+ return "Float";
622
685
  case "decimal":
623
- return `Decimal${list}`;
686
+ return "Decimal";
624
687
  case "boolean":
625
- return `Boolean${list}`;
688
+ return "Boolean";
626
689
  case "datetime":
627
- return `DateTime${list}`;
690
+ return "DateTime";
628
691
  case "json":
629
- return `Json${list}`;
692
+ return "Json";
630
693
  case "bytes":
631
- return `Bytes${list}`;
694
+ return "Bytes";
632
695
  case "id":
633
696
  return column.__config.type === "autoincrement" ? "Int" : "String";
634
697
  case "enum":
635
- return column.__enumName === undefined ? `String${list}` : `${column.__enumName}${list}`;
636
- case "unsupported": {
637
- const extensionTypeName = column.__config["extensionType"];
638
- if (typeof extensionTypeName === "string") {
639
- const rawArgs = column.__config["extensionArgs"];
640
- const args = Array.isArray(rawArgs) ? rawArgs.map(String).join(", ") : "";
641
- return `${extensionTypeName}${args.length > 0 ? `(${args})` : ""}${list}`;
642
- }
643
- return `Unsupported("${String(column.__config.dbType)}")${list}`;
644
- }
698
+ return column.__enumName === undefined ? "String" : { enum: column.__enumName };
699
+ case "unsupported":
700
+ return "String";
645
701
  }
646
702
  }
647
- function idDefaultAttribute(params) {
648
- const idType = params.column.__config.type;
649
- switch (idType) {
703
+ function idDefault(params) {
704
+ switch (params.column.__config.type) {
650
705
  case "ulid":
706
+ return { kind: "ulid" };
651
707
  case "cuid":
708
+ return { kind: "cuid" };
652
709
  case "uuid":
710
+ return { kind: "uuid" };
653
711
  case "nanoid":
712
+ return { kind: "nanoid" };
654
713
  case "autoincrement":
655
- return `@default(${idType}())`;
714
+ return { kind: "autoincrement" };
656
715
  default:
657
716
  return;
658
717
  }
659
718
  }
660
- function defaultAttribute(params) {
719
+ function fieldDefault(params) {
661
720
  const { column } = params;
662
- const { dbgenerated, default: value } = column.__config;
663
- if (typeof dbgenerated === "string") {
664
- return `@default(dbgenerated("${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` });
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" };
665
735
  }
736
+ if (typeof dbgenerated === "string")
737
+ return { kind: "dbgenerated", sql: dbgenerated };
666
738
  if (value === undefined)
667
739
  return;
668
- if (column.__columnType === "datetime" && value === "now") {
669
- 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 }) };
744
+ }
745
+ if (column.__columnType === "json" && typeof value !== "string") {
746
+ return { kind: "literal", value: serializeJsonDefault({ value }) };
670
747
  }
748
+ if (column.__columnType === "datetime" && value === "now")
749
+ return { kind: "now" };
671
750
  if (column.__columnType === "enum" && typeof value === "string") {
672
- 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 };
673
758
  }
674
- if (typeof value === "string")
675
- return `@default("${value}")`;
676
- if (typeof value === "boolean" || typeof value === "number")
677
- return `@default(${value})`;
678
759
  if (typeof value === "bigint")
679
- return `@default(${value.toString()})`;
680
- return;
760
+ return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
761
+ throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
681
762
  }
682
- function nativeTypeAttribute(params) {
763
+ function nativeType(params) {
683
764
  const { column } = params;
684
- 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") {
685
779
  const { maxLength, text } = column.__config;
686
780
  if (typeof maxLength === "number")
687
- return `@db.VarChar(${maxLength})`;
781
+ return `VarChar(${maxLength})`;
688
782
  if (text === true)
689
- return "@db.Text";
783
+ return "Text";
690
784
  return;
691
785
  }
692
786
  if (column.__columnType === "decimal") {
693
787
  const { precision, scale } = column.__config;
694
788
  if (typeof precision === "number")
695
- return `@db.Decimal(${precision}, ${scale ?? 0})`;
789
+ return `Decimal(${precision}, ${scale ?? 0})`;
696
790
  return;
697
791
  }
698
792
  if (column.__columnType === "datetime" && column.__config.tz === true) {
699
- return "@db.Timestamptz";
793
+ return "Timestamptz";
794
+ }
795
+ if (column.__columnType === "unsupported") {
796
+ return String(column.__config.dbType);
700
797
  }
701
798
  return;
702
799
  }
703
- function formatColumn(params) {
704
- const { name, column } = params;
705
- const parts = [` ${name} ${prismaType({ column })}${column.__config.optional === true ? "?" : ""}`];
706
- if (column.__columnType === "id") {
707
- parts.push("@id");
708
- const idDefault = idDefaultAttribute({ column });
709
- if (idDefault !== undefined)
710
- parts.push(idDefault);
711
- } else {
712
- const columnDefault = defaultAttribute({ column });
713
- if (columnDefault !== undefined)
714
- parts.push(columnDefault);
715
- }
716
- if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
717
- parts.push("@updatedAt");
718
- }
719
- if (column.__config.unique === true) {
720
- parts.push("@unique");
721
- }
722
- if (typeof column.__config.map === "string") {
723
- parts.push(`@map("${column.__config.map}")`);
724
- }
725
- const nativeType = nativeTypeAttribute({ column });
726
- if (nativeType !== undefined)
727
- parts.push(nativeType);
728
- const columnLine = parts.join(" ");
729
- const comment = column.__config.comment;
730
- if (typeof comment === "string") {
731
- const commentLines = comment.split(`
732
- `).map((line) => ` /// ${line}`);
733
- return [...commentLines, columnLine].join(`
734
- `);
735
- }
736
- 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
+ };
737
824
  }
738
- function formatRelation(params) {
739
- const { relation } = params;
740
- const { name, targetModel, relationName } = relation;
741
- if (relation.kind === "list") {
742
- const attribute = relationName === undefined ? "" : ` @relation("${relationName}")`;
743
- return ` ${name} ${targetModel}[]${attribute}`;
744
- }
745
- const optional = relation.isOptional ? "?" : "";
746
- if (relation.kind === "backOne") {
747
- const attribute = relationName === undefined ? "" : ` @relation("${relationName}")`;
748
- return ` ${name} ${targetModel}${optional}${attribute}`;
749
- }
750
- const args = [];
751
- if (relationName !== undefined)
752
- args.push(`"${relationName}"`);
753
- if (relation.fields === undefined || relation.references === undefined) {
754
- throw internalError({
755
- message: `planned FK relation "${relation.model}.${name}" has no fields/references`,
756
- meta: { model: relation.model, relation: name }
757
- });
758
- }
759
- args.push(`fields: [${relation.fields.join(", ")}]`);
760
- args.push(`references: [${relation.references.join(", ")}]`);
761
- if (relation.onDelete !== undefined)
762
- args.push(`onDelete: ${relation.onDelete}`);
763
- if (relation.onUpdate !== undefined)
764
- args.push(`onUpdate: ${relation.onUpdate}`);
765
- 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
+ };
766
840
  }
767
- 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) {
768
850
  const { index } = params;
769
- const columns = index.columns.join(", ");
770
- const whereArg = index.where === undefined ? "" : `, where: ${JSON.stringify(index.where)}`;
771
- const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
772
- const nameArg = index.clientName === undefined ? "" : `, name: ${JSON.stringify(index.clientName)}`;
773
- if (index.isUnique) {
774
- return ` @@unique([${columns}]${nameArg}${nndArg}${whereArg})`;
775
- }
776
- const typeArg = index.indexType === undefined ? "" : `, type: ${index.indexType}`;
777
- return ` @@index([${columns}]${typeArg}${whereArg})`;
778
- }
779
- function formatExtensionValue(params) {
780
- const { value } = params;
781
- if (typeof value === "string")
782
- return `"${value}"`;
783
- if (Array.isArray(value)) {
784
- return `[${value.map((entry) => formatExtensionValue({ value: entry })).join(", ")}]`;
785
- }
786
- 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
+ };
787
860
  }
788
- function formatModel(params) {
789
- const { model, plan } = params;
790
- const lines = [`model ${model.__name} {`];
791
- 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 = [];
792
866
  for (const [name, column] of model.__columns) {
793
- lines.push(formatColumn({ name, column }));
794
- }
795
- lines.push("");
796
- lines.push(" //> Relations");
797
- for (const relation of plan.relations.get(model.__name) ?? []) {
798
- lines.push(formatRelation({ relation }));
799
- }
800
- lines.push("");
801
- lines.push(" //> Indexes");
802
- lines.push("");
803
- for (const index of plan.indexes.get(model.__name) ?? []) {
804
- lines.push(formatIndex({ index }));
805
- }
806
- for (const check of model.__checks ?? []) {
807
- lines.push(` @@check(${JSON.stringify(check.expression)}, name: ${JSON.stringify(check.name)})`);
808
- }
809
- for (const use of model.__extensions ?? []) {
810
- const args = [];
811
- if (use.fields !== undefined)
812
- args.push(`[${use.fields.join(", ")}]`);
813
- for (const [key, value] of Object.entries(use.args ?? {})) {
814
- args.push(`${key}: ${formatExtensionValue({ value })}`);
867
+ const extensionTypeName = column.__config["extensionType"];
868
+ if (typeof extensionTypeName !== "string") {
869
+ fields.push(buildField({ name, column }));
870
+ continue;
815
871
  }
816
- lines.push(` @@${use.attribute}(${args.join(", ")})`);
817
- }
818
- if (model.__primaryKey !== undefined && model.__primaryKey.length > 0) {
819
- lines.push(` @@id([${model.__primaryKey.join(", ")}])`);
820
- }
821
- if (typeof model.__map === "string") {
822
- lines.push(` @@map("${model.__map}")`);
823
- }
824
- lines.push("}");
825
- return lines.join(`
826
- `);
827
- }
828
- function formatEnum(params) {
829
- const { name, values } = params;
830
- return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
831
- `);
832
- }
833
- function generatePrisma(schema, _config = {}) {
834
- const plan = planSchema({ schema });
835
- const sections = [];
836
- if (schema.__enums.size > 0) {
837
- sections.push("// ============================================");
838
- sections.push("// Enums");
839
- sections.push("// ============================================");
840
- sections.push("");
841
- for (const [name, values] of schema.__enums) {
842
- sections.push(formatEnum({ name, values }));
843
- sections.push("");
844
- }
845
- }
846
- sections.push("// ============================================");
847
- sections.push("// Models");
848
- sections.push("// ============================================");
849
- sections.push("");
850
- for (const model of schema.__models.values()) {
851
- sections.push(formatModel({ model, plan }));
852
- sections.push("");
853
- }
854
- return sections.join(`
855
- `).trim();
856
- }
857
-
858
- // packages/dsl/src/relations.ts
859
- function createColumnRef(params) {
860
- const { model, column } = params;
861
- return {
862
- __type: "columnRef",
863
- __model: model,
864
- __column: column,
865
- through(junction) {
866
- return {
867
- __type: "throughRef",
868
- __model: model,
869
- __column: column,
870
- __junction: junction.__model,
871
- __junctionColumn: junction.__column
872
- };
873
- }
874
- };
875
- }
876
- function extractRefInfo(params) {
877
- const { ref } = params;
878
- return { model: ref.__model, column: ref.__column };
879
- }
880
- function createRelationFactory(params) {
881
- const { relationType, targetModelName } = params;
882
- return (config) => {
883
- const from = extractRefInfo({ ref: config.from });
884
- const to = extractRefInfo({ ref: config.to });
885
- let throughJunction;
886
- let throughFromCol;
887
- let throughToCol;
888
- if (config.from.__type === "throughRef") {
889
- throughJunction = config.from.__junction;
890
- throughFromCol = config.from.__junctionColumn;
891
- }
892
- if (config.to.__type === "throughRef") {
893
- throughJunction = throughJunction ?? config.to.__junction;
894
- throughToCol = config.to.__junctionColumn;
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;
895
881
  }
896
- return {
897
- __type: "storedRelation",
898
- __relationType: relationType,
899
- __targetModel: targetModelName,
900
- __fromModel: from.model,
901
- __fromColumn: from.column,
902
- __toModel: to.model,
903
- __toColumn: to.column,
904
- __throughJunction: throughJunction,
905
- __throughFromCol: throughFromCol,
906
- __throughToCol: throughToCol,
907
- __optional: config.optional ?? relationType === "many",
908
- __alias: config.alias,
909
- __onDelete: config.onDelete,
910
- __onUpdate: config.onUpdate
911
- };
912
- };
913
- }
914
- function createRelationsBuilder(params) {
915
- const { models } = params;
916
- const builder = {};
917
- const oneObj = {};
918
- const manyObj = {};
919
- for (const modelName of Object.keys(models)) {
920
- oneObj[modelName] = createRelationFactory({ relationType: "one", targetModelName: modelName });
921
- manyObj[modelName] = createRelationFactory({
922
- relationType: "many",
923
- targetModelName: modelName
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") : []
924
887
  });
925
- const model = models[modelName];
926
- if (model === undefined)
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
+ });
927
895
  continue;
928
- const columnRefs = {};
929
- for (const columnName of model.__columns.keys()) {
930
- columnRefs[columnName] = createColumnRef({ model: modelName, column: columnName });
931
896
  }
932
- builder[modelName] = columnRefs;
933
- }
934
- builder.one = oneObj;
935
- builder.many = manyObj;
936
- return builder;
937
- }
938
- function processRelationsOutput(params) {
939
- const result = {};
940
- for (const [modelName, relations] of Object.entries(params.output)) {
941
- if (relations) {
942
- result[modelName] = relations;
943
- }
944
- }
945
- return result;
946
- }
947
- function defineRelations(models, callback) {
948
- const builder = createRelationsBuilder({ models });
949
- return processRelationsOutput({ output: callback(builder) });
950
- }
951
- function defineRelationsPart(models, callback) {
952
- const builder = createRelationsBuilder({ models });
953
- return processRelationsOutput({ output: callback(builder) });
954
- }
955
- function mergeRelations(...parts) {
956
- const result = {};
957
- for (const part of parts) {
958
- for (const [modelName, relations] of Object.entries(part)) {
959
- const existing = result[modelName] ?? {};
960
- result[modelName] = { ...existing, ...relations };
961
- }
962
- }
963
- return result;
964
- }
965
-
966
- // packages/dsl/src/schema.ts
967
- function isModelDef(value) {
968
- return typeof value === "object" && value !== null && "__type" in value && value.__type === "model";
969
- }
970
- function isEnumDef(value) {
971
- return typeof value === "object" && value !== null && "__type" in value && value.__type === "enum";
972
- }
973
- function addModelTree(params) {
974
- const { model, models } = params;
975
- models.set(model.__name, model);
976
- for (const childModel of model.__children.values()) {
977
- addModelTree({ model: childModel, models });
978
- }
979
- }
980
- function extractModelsAndEnums(params) {
981
- const { input, models, standaloneEnums } = params;
982
- for (const value of Object.values(input)) {
983
- if (isModelDef(value)) {
984
- addModelTree({ model: value, models });
985
- } else if (isEnumDef(value)) {
986
- standaloneEnums.set(value.__name, value.__options);
987
- } else if (typeof value === "object" && value !== null) {
988
- extractModelsAndEnums({ input: value, models, standaloneEnums });
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
+ });
989
904
  }
990
905
  }
991
- }
992
- function resolveEnums(params) {
993
- const { models, standaloneEnums } = params;
994
- const enums = new Map(standaloneEnums);
995
- for (const [modelName, model] of models) {
996
- for (const [columnName, column] of model.__columns) {
997
- if (column.__columnType !== "enum" || column.__enumValues === undefined)
998
- continue;
999
- if (column.__enumName !== undefined) {
1000
- if (!enums.has(column.__enumName)) {
1001
- enums.set(column.__enumName, column.__enumValues);
1002
- }
1003
- continue;
1004
- }
1005
- const name = inlineEnumName({ modelName, columnName });
1006
- enums.set(name, column.__enumValues);
1007
- column.__enumName = name;
906
+ for (const use of model.__extensions ?? []) {
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;
1008
915
  }
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
+ });
1009
937
  }
1010
- return enums;
1011
- }
1012
- function schema(input, options) {
1013
- const models = new Map;
1014
- const standaloneEnums = new Map;
1015
- extractModelsAndEnums({ input, models, standaloneEnums });
1016
- return {
1017
- __type: "schema",
1018
- __models: models,
1019
- __enums: resolveEnums({ models, standaloneEnums }),
1020
- __relations: options?.relations ?? {}
1021
- };
1022
- }
1023
- function domain(params) {
1024
- const { name, models } = params;
1025
- return { name, models };
1026
- }
1027
-
1028
- // packages/dsl/src/templates.ts
1029
- var base = {
1030
- get id() {
1031
- return id({ type: "ulid" });
1032
- },
1033
- get createdAt() {
1034
- return datetime({ default: "now", tz: true, index: true });
1035
- },
1036
- get updatedAt() {
1037
- return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1038
- },
1039
- get deletedAt() {
1040
- return datetime({ optional: true, index: true });
1041
- }
1042
- };
1043
- var baseWithWorkspace = {
1044
- get id() {
1045
- return id({ type: "ulid" });
1046
- },
1047
- get createdAt() {
1048
- return datetime({ default: "now", tz: true, index: true });
1049
- },
1050
- get updatedAt() {
1051
- return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1052
- },
1053
- get deletedAt() {
1054
- return datetime({ optional: true, index: true });
1055
- },
1056
- get workspaceId() {
1057
- return string({ index: true });
1058
- }
1059
- };
1060
- var withMetadata = {
1061
- get metadata() {
1062
- return json({ optional: true });
1063
- },
1064
- get settings() {
1065
- return json({ optional: true });
1066
- }
1067
- };
1068
- var withActive = {
1069
- get isActive() {
1070
- return boolean({ default: true });
1071
- }
1072
- };
1073
- var withAudit = {
1074
- get createdById() {
1075
- return string({ optional: true, index: true });
1076
- },
1077
- get updatedById() {
1078
- return string({ optional: true, index: true });
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
+ };
950
+ }
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 }));
1079
959
  }
1080
- };
1081
- var withExternalId = {
1082
- get externalId() {
1083
- return string({ index: true });
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
+ }));
1084
967
  }
1085
- };
1086
- var withNameDescription = {
1087
- get name() {
1088
- return string();
1089
- },
1090
- get description() {
1091
- return string({ optional: true });
968
+ }
969
+ function toIR(params) {
970
+ const { schema, provider = "postgresql" } = params;
971
+ const extensions = params.extensions ?? EMPTY_EXTENSION_REGISTRY;
972
+ const plan = planSchema({ schema });
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
+ }
985
+ }
1092
986
  }
1093
- };
1094
- var fields = {
1095
- get datetime() {
1096
- return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1097
- },
1098
- get id() {
1099
- return id({ type: "ulid" });
1100
- },
1101
- get title() {
1102
- return string();
1103
- },
1104
- get description() {
1105
- return string({ optional: true });
1106
- },
1107
- get externalId() {
1108
- return string({ index: true });
1109
- },
1110
- get metadata() {
1111
- return json({ optional: true });
1112
- },
1113
- get settings() {
1114
- return json({ optional: true });
1115
- },
1116
- get isActive() {
1117
- return boolean({ default: true });
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
+ });
1118
998
  }
1119
- };
1120
- var templates = {
1121
- base,
1122
- baseWithWorkspace,
1123
- withMetadata,
1124
- withActive,
1125
- withAudit,
1126
- withExternalId,
1127
- withNameDescription,
1128
- fields
1129
- };
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;
1008
+ }
1130
1009
 
1131
- // packages/dsl/src/to-ir.ts
1132
- import {
1133
- EMPTY_EXTENSION_REGISTRY,
1134
- VibeError,
1135
- assertValidSchemaIR,
1136
- mergeExtensionDeclarations
1137
- } from "@vibeorm/schema";
1138
- function fieldType(params) {
1010
+ // packages/dsl/src/generator/prisma.ts
1011
+ function prismaType(params) {
1139
1012
  const { column } = params;
1013
+ const list = column.__config.array === true ? "[]" : "";
1140
1014
  switch (column.__columnType) {
1141
1015
  case "string":
1142
- return "String";
1016
+ return `String${list}`;
1143
1017
  case "int":
1144
- return "Int";
1018
+ return `Int${list}`;
1145
1019
  case "bigint":
1146
- return "BigInt";
1020
+ return `BigInt${list}`;
1147
1021
  case "float":
1148
- return "Float";
1022
+ return `Float${list}`;
1149
1023
  case "decimal":
1150
- return "Decimal";
1024
+ return `Decimal${list}`;
1151
1025
  case "boolean":
1152
- return "Boolean";
1026
+ return `Boolean${list}`;
1153
1027
  case "datetime":
1154
- return "DateTime";
1028
+ return `DateTime${list}`;
1155
1029
  case "json":
1156
- return "Json";
1030
+ return `Json${list}`;
1157
1031
  case "bytes":
1158
- return "Bytes";
1032
+ return `Bytes${list}`;
1159
1033
  case "id":
1160
1034
  return column.__config.type === "autoincrement" ? "Int" : "String";
1161
1035
  case "enum":
1162
- return column.__enumName === undefined ? "String" : { enum: column.__enumName };
1163
- case "unsupported":
1164
- return "String";
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}`;
1045
+ }
1165
1046
  }
1166
1047
  }
1167
- function idDefault(params) {
1168
- switch (params.column.__config.type) {
1169
- case "ulid":
1170
- return { kind: "ulid" };
1171
- case "cuid":
1172
- return { kind: "cuid" };
1173
- case "uuid":
1174
- return { kind: "uuid" };
1175
- case "nanoid":
1176
- return { kind: "nanoid" };
1177
- case "autoincrement":
1178
- return { kind: "autoincrement" };
1179
- default:
1180
- return;
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}())`;
1064
+ }
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(", ")}]`;
1181
1172
  }
1173
+ return String(value);
1182
1174
  }
1183
- function fieldDefault(params) {
1184
- const { column } = params;
1185
- if (column.__columnType === "id")
1186
- return idDefault({ column });
1187
- const { dbgenerated, default: value } = column.__config;
1188
- if (typeof dbgenerated === "string")
1189
- return { kind: "dbgenerated", sql: dbgenerated };
1190
- if (value === undefined)
1191
- return;
1192
- if (column.__columnType === "datetime" && value === "now")
1193
- return { kind: "now" };
1194
- if (column.__columnType === "enum" && typeof value === "string") {
1195
- return { kind: "enumValue", value };
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 }));
1196
1185
  }
1197
- if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
1198
- return { kind: "literal", value };
1186
+ lines.push("");
1187
+ lines.push(" //> Relations");
1188
+ for (const relation of plan.relations.get(model.__name) ?? []) {
1189
+ lines.push(formatRelation({ relation }));
1199
1190
  }
1200
- if (typeof value === "bigint")
1201
- return { kind: "literal", value: Number(value) };
1202
- return;
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
+ `);
1203
1223
  }
1204
- function nativeType(params) {
1205
- const { column } = params;
1206
- if (column.__columnType === "string") {
1207
- const { maxLength, text } = column.__config;
1208
- if (typeof maxLength === "number")
1209
- return `VarChar(${maxLength})`;
1210
- if (text === true)
1211
- return "Text";
1212
- return;
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
+ });
1213
1237
  }
1214
- if (column.__columnType === "decimal") {
1215
- const { precision, scale } = column.__config;
1216
- if (typeof precision === "number")
1217
- return `Decimal(${precision}, ${scale ?? 0})`;
1218
- return;
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
+ });
1219
1244
  }
1220
- if (column.__columnType === "datetime" && column.__config.tz === true) {
1221
- return "Timestamptz";
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
+ }
1222
1256
  }
1223
- if (column.__columnType === "unsupported") {
1224
- return String(column.__config.dbType);
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("");
1225
1264
  }
1226
- return;
1265
+ return sections.join(`
1266
+ `).trim();
1227
1267
  }
1228
- function buildField(params) {
1229
- const { name, column, extensionNativeType } = params;
1230
- const config = column.__config;
1231
- const isList = column.__columnType !== "id" && config.array === true;
1232
- const dbName = typeof config.map === "string" ? config.map : undefined;
1233
- const documentation = typeof config.comment === "string" ? config.comment : undefined;
1234
- const defaultValue = fieldDefault({ column });
1235
- const native = extensionNativeType ?? nativeType({ column });
1268
+
1269
+ // packages/dsl/src/relations.ts
1270
+ function createColumnRef(params) {
1271
+ const { model, column } = params;
1236
1272
  return {
1237
- name,
1238
- type: fieldType({ column }),
1239
- isList,
1240
- isOptional: config.optional === true,
1241
- isId: column.__columnType === "id",
1242
- isUnique: config.unique === true,
1243
- isUpdatedAt: column.__columnType === "datetime" && config.updatedAt === true,
1244
- ...dbName === undefined ? {} : { dbName },
1245
- ...defaultValue === undefined ? {} : { default: defaultValue },
1246
- ...native === undefined ? {} : { nativeType: native },
1247
- ...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
1248
- ...documentation === undefined ? {} : { documentation }
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
+ }
1249
1285
  };
1250
1286
  }
1251
- function buildRelation(params) {
1252
- const { relation, plan } = params;
1253
- return {
1254
- name: relation.name,
1255
- model: relation.model,
1256
- targetModel: relation.targetModel,
1257
- type: resolveRelationType({ relation, plan }),
1258
- isList: relation.isList,
1259
- isOptional: relation.isOptional,
1260
- ...relation.relationName === undefined ? {} : { relationName: relation.relationName },
1261
- ...relation.fields === undefined ? {} : { fields: [...relation.fields] },
1262
- ...relation.references === undefined ? {} : { references: [...relation.references] },
1263
- ...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
1264
- ...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
1265
- };
1287
+ function extractRefInfo(params) {
1288
+ const { ref } = params;
1289
+ return { model: ref.__model, column: ref.__column };
1266
1290
  }
1267
- var INDEX_KINDS = {
1268
- BTree: "btree",
1269
- Hash: "hash",
1270
- Gin: "gin",
1271
- Gist: "gist",
1272
- SpGist: "spgist",
1273
- Brin: "brin"
1274
- };
1275
- function buildIndex(params) {
1276
- const { index } = params;
1277
- const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
1278
- return {
1279
- kind,
1280
- fields: index.columns.map((name) => ({ name })),
1281
- ...index.where === undefined ? {} : { where: index.where },
1282
- ...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
1283
- ...index.clientName === undefined ? {} : { clientName: index.clientName }
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;
1306
+ }
1307
+ return {
1308
+ __type: "storedRelation",
1309
+ __relationType: relationType,
1310
+ __targetModel: targetModelName,
1311
+ __fromModel: from.model,
1312
+ __fromColumn: from.column,
1313
+ __toModel: to.model,
1314
+ __toColumn: to.column,
1315
+ __throughJunction: throughJunction,
1316
+ __throughFromCol: throughFromCol,
1317
+ __throughToCol: throughToCol,
1318
+ __optional: config.optional ?? relationType === "many",
1319
+ __alias: config.alias,
1320
+ ..."ownsForeignKey" in config ? { __ownsForeignKey: config.ownsForeignKey } : {},
1321
+ __onDelete: config.onDelete,
1322
+ __onUpdate: config.onUpdate
1323
+ };
1284
1324
  };
1285
1325
  }
1286
- function buildModel(params) {
1287
- const { model, plan, extensions, declarations, issues } = params;
1288
- const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
1289
- const fields2 = [];
1290
- for (const [name, column] of model.__columns) {
1291
- const extensionTypeName = column.__config["extensionType"];
1292
- if (typeof extensionTypeName !== "string") {
1293
- fields2.push(buildField({ name, column }));
1326
+ function createRelationsBuilder(params) {
1327
+ const { models } = params;
1328
+ const builder = {};
1329
+ const oneObj = {};
1330
+ const manyObj = {};
1331
+ for (const modelName of Object.keys(models)) {
1332
+ oneObj[modelName] = createRelationFactory({ relationType: "one", targetModelName: modelName });
1333
+ manyObj[modelName] = createRelationFactory({
1334
+ relationType: "many",
1335
+ targetModelName: modelName
1336
+ });
1337
+ const model = models[modelName];
1338
+ if (model === undefined)
1294
1339
  continue;
1340
+ const columnRefs = {};
1341
+ for (const columnName of model.__columns.keys()) {
1342
+ columnRefs[columnName] = createColumnRef({ model: modelName, column: columnName });
1343
+ }
1344
+ builder[modelName] = columnRefs;
1345
+ }
1346
+ builder.one = oneObj;
1347
+ builder.many = manyObj;
1348
+ return builder;
1349
+ }
1350
+ function processRelationsOutput(params) {
1351
+ const result = {};
1352
+ for (const [modelName, relations] of Object.entries(params.output)) {
1353
+ if (relations) {
1354
+ result[modelName] = relations;
1355
+ }
1356
+ }
1357
+ return result;
1358
+ }
1359
+ function defineRelations(models, callback) {
1360
+ const builder = createRelationsBuilder({ models });
1361
+ return processRelationsOutput({ output: callback(builder) });
1362
+ }
1363
+ function defineRelationsPart(models, callback) {
1364
+ const builder = createRelationsBuilder({ models });
1365
+ return processRelationsOutput({ output: callback(builder) });
1366
+ }
1367
+ function mergeRelations(...parts) {
1368
+ const result = {};
1369
+ for (const part of parts) {
1370
+ for (const [modelName, relations] of Object.entries(part)) {
1371
+ const existing = result[modelName] ?? {};
1372
+ result[modelName] = { ...existing, ...relations };
1295
1373
  }
1296
- const registered = extensions.fieldTypes.get(extensionTypeName);
1297
- if (registered === undefined) {
1298
- issues.push({
1299
- rule: "extension-attribute",
1300
- message: `Field "${model.__name}.${name}" uses extension type "${extensionTypeName}" but no configured extension provides it`,
1301
- model: model.__name,
1302
- field: name
1303
- });
1304
- continue;
1374
+ }
1375
+ return result;
1376
+ }
1377
+
1378
+ // packages/dsl/src/schema.ts
1379
+ import { VibeError as VibeError4, validateRlsContextIR } from "@vibeorm/schema";
1380
+ function sameValues(left, right) {
1381
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1382
+ }
1383
+ function registerEnum(params) {
1384
+ const { registry, name, entry, origin } = params;
1385
+ const existing = registry.get(name);
1386
+ if (existing === undefined) {
1387
+ registry.set(name, entry);
1388
+ return;
1389
+ }
1390
+ const valuesDiffer = !sameValues(existing.values, entry.values);
1391
+ const storageDiffers = existing.storage !== entry.storage;
1392
+ if (!valuesDiffer && !storageDiffers)
1393
+ return;
1394
+ const detail = [
1395
+ ...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
1396
+ ...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
1397
+ ].join("; ");
1398
+ throw new VibeError4({
1399
+ code: "VIBE_SCHEMA_INVALID",
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`,
1401
+ meta: {
1402
+ issues: [
1403
+ {
1404
+ rule: "enum-conflict",
1405
+ message: `Enum "${name}" reused with conflicting ${valuesDiffer ? "values" : "storage"} (${origin})`
1406
+ }
1407
+ ],
1408
+ enum: name,
1409
+ existing,
1410
+ incoming: entry
1305
1411
  }
1306
- const rawArgs = column.__config["extensionArgs"];
1307
- const lowered = registered.fieldType.lower({
1308
- model: model.__name,
1309
- field: name,
1310
- args: Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string" || typeof entry === "number") : []
1412
+ });
1413
+ }
1414
+ function isModelDef(value) {
1415
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "model";
1416
+ }
1417
+ function isEnumDef(value) {
1418
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "enum";
1419
+ }
1420
+ function addModelTree(params) {
1421
+ const { model, models, origin } = params;
1422
+ const existing = models.get(model.__name);
1423
+ if (existing !== undefined && existing !== model) {
1424
+ throw new VibeError4({
1425
+ code: "VIBE_SCHEMA_INVALID",
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`,
1427
+ meta: {
1428
+ issues: [
1429
+ {
1430
+ rule: "model-conflict",
1431
+ message: `Model "${model.__name}" reused with a different definition (${origin})`,
1432
+ model: model.__name
1433
+ }
1434
+ ],
1435
+ model: model.__name
1436
+ }
1311
1437
  });
1312
- if ("error" in lowered) {
1313
- issues.push({
1314
- rule: "extension-attribute",
1315
- message: `Field "${model.__name}.${name}": ${lowered.error}`,
1316
- model: model.__name,
1317
- field: name
1438
+ }
1439
+ models.set(model.__name, model);
1440
+ for (const childModel of model.__children.values()) {
1441
+ addModelTree({ model: childModel, models, origin: `inline child of ${model.__name}` });
1442
+ }
1443
+ }
1444
+ function extractModelsAndEnums(params) {
1445
+ const { input, models, enums } = params;
1446
+ for (const value of Object.values(input)) {
1447
+ if (isModelDef(value)) {
1448
+ addModelTree({ model: value, models, origin: "listed in dsl.schema()" });
1449
+ } else if (isEnumDef(value)) {
1450
+ registerEnum({
1451
+ registry: enums,
1452
+ name: value.__name,
1453
+ entry: { values: value.__options, storage: value.__storage },
1454
+ origin: "standalone enum listed in dsl.schema()"
1318
1455
  });
1319
- continue;
1456
+ } else if (typeof value === "object" && value !== null) {
1457
+ extractModelsAndEnums({ input: value, models, enums });
1320
1458
  }
1321
- fields2.push(buildField({ name, column, extensionNativeType: lowered.nativeType }));
1322
- if (lowered.declaration !== undefined) {
1323
- declarations.add({
1324
- extension: registered.extension.name,
1325
- model: model.__name,
1326
- declaration: lowered.declaration
1459
+ }
1460
+ }
1461
+ function resolveEnums(params) {
1462
+ const { models, enums } = params;
1463
+ for (const [modelName, model] of models) {
1464
+ for (const [columnName, column] of model.__columns) {
1465
+ if (column.__columnType !== "enum" || column.__enumValues === undefined)
1466
+ continue;
1467
+ const entry = {
1468
+ values: column.__enumValues,
1469
+ storage: column.__enumStorage ?? "database"
1470
+ };
1471
+ if (column.__enumName !== undefined) {
1472
+ registerEnum({
1473
+ registry: enums,
1474
+ name: column.__enumName,
1475
+ entry,
1476
+ origin: `standalone enum used by ${modelName}.${columnName}`
1477
+ });
1478
+ continue;
1479
+ }
1480
+ const name = inlineEnumName({ modelName, columnName });
1481
+ registerEnum({
1482
+ registry: enums,
1483
+ name,
1484
+ entry,
1485
+ origin: `inline enum on ${modelName}.${columnName}, named ${name}`
1327
1486
  });
1487
+ column.__enumName = name;
1328
1488
  }
1329
1489
  }
1330
- for (const use of model.__extensions ?? []) {
1331
- const registered = extensions.modelAttributes.get(use.attribute);
1332
- if (registered === undefined) {
1333
- issues.push({
1334
- rule: "unknown-attribute",
1335
- message: `Unknown block attribute "@@${use.attribute}" on model "${model.__name}" \u2014 no extension provides it`,
1336
- model: model.__name
1337
- });
1338
- continue;
1490
+ }
1491
+ var RLS_CONTEXT_TYPES = {
1492
+ string: "String",
1493
+ boolean: "Boolean",
1494
+ int: "Int",
1495
+ bigint: "BigInt",
1496
+ float: "Float",
1497
+ decimal: "Decimal",
1498
+ datetime: "DateTime"
1499
+ };
1500
+ function defineRls(params) {
1501
+ if (typeof params.context !== "object" || params.context === null || Array.isArray(params.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" }] } });
1503
+ }
1504
+ const entries = [];
1505
+ const refs = [];
1506
+ for (const key of Reflect.ownKeys(params.context)) {
1507
+ const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
1508
+ const column = descriptor?.value;
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) {
1510
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
1339
1511
  }
1340
- const parsed = registered.attribute.parse({
1341
- model: model.__name,
1342
- args: {
1343
- named: { ...use.args ?? {} },
1344
- ...use.fields === undefined ? {} : { fields: use.fields }
1345
- },
1346
- fieldNames: fields2.map((field) => field.name)
1347
- });
1348
- if ("error" in parsed) {
1349
- issues.push({
1350
- rule: "extension-attribute",
1351
- message: `@@${use.attribute} on "${model.__name}": ${parsed.error}`,
1352
- model: model.__name
1353
- });
1354
- continue;
1512
+ entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
1513
+ refs.push([key, Object.freeze({ context: key })]);
1514
+ }
1515
+ const context = Object.freeze(Object.fromEntries(entries));
1516
+ const issues = validateRlsContextIR({ context });
1517
+ if (issues.length > 0) {
1518
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
1519
+ }
1520
+ const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
1521
+ if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
1522
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1523
+ }
1524
+ const named = [];
1525
+ const classified = new Set;
1526
+ for (const key of Reflect.ownKeys(policies)) {
1527
+ const descriptor = Object.getOwnPropertyDescriptor(policies, key);
1528
+ if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
1529
+ throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
1355
1530
  }
1356
- declarations.add({
1357
- extension: registered.extension.name,
1358
- model: model.__name,
1359
- declaration: parsed.declaration
1360
- });
1531
+ const name = params.models[key]?.__name ?? key;
1532
+ if (classified.has(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 }] } });
1534
+ }
1535
+ classified.add(name);
1536
+ named.push([name, descriptor.value]);
1361
1537
  }
1538
+ return { context, policies: Object.fromEntries(named) };
1539
+ }
1540
+ function schema(input, options) {
1541
+ const models = new Map;
1542
+ const enums = new Map;
1543
+ extractModelsAndEnums({ input, models, enums });
1544
+ resolveEnums({ models, enums });
1362
1545
  return {
1363
- name: model.__name,
1364
- ...model.__map === undefined ? {} : { dbName: model.__map },
1365
- fields: fields2,
1366
- relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
1367
- indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index })),
1368
- ...model.__checks === undefined || model.__checks.length === 0 ? {} : { checks: model.__checks.map((check) => ({ name: check.name, expression: check.expression })) },
1369
- ...primaryKey === undefined ? {} : { primaryKey },
1370
- isView: false
1546
+ __type: "schema",
1547
+ __models: models,
1548
+ __enums: new Map(Array.from(enums, ([name, entry]) => [name, entry.values])),
1549
+ __enumStorage: new Map(Array.from(enums, ([name, entry]) => [name, entry.storage])),
1550
+ __relations: options?.relations ?? {},
1551
+ ...options?.rls === undefined ? {} : { __rls: options.rls }
1371
1552
  };
1372
1553
  }
1554
+ function domain(params) {
1555
+ const { name, models } = params;
1556
+ return { name, models };
1557
+ }
1373
1558
 
1374
- class ExtensionDeclarationCollector {
1375
- byExtension = new Map;
1376
- add(params) {
1377
- const perModel = this.byExtension.get(params.extension) ?? new Map;
1378
- this.byExtension.set(params.extension, perModel);
1379
- const existing = perModel.get(params.model) ?? {};
1380
- perModel.set(params.model, mergeExtensionDeclarations({ base: existing, next: params.declaration }));
1559
+ // packages/dsl/src/templates.ts
1560
+ var base = {
1561
+ get id() {
1562
+ return id({ type: "ulid" });
1563
+ },
1564
+ get createdAt() {
1565
+ return datetime({ default: "now", tz: true, index: true });
1566
+ },
1567
+ get updatedAt() {
1568
+ return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1569
+ },
1570
+ get deletedAt() {
1571
+ return datetime({ optional: true, index: true });
1381
1572
  }
1382
- toIR() {
1383
- return [...this.byExtension.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, perModel]) => ({
1384
- name,
1385
- options: {
1386
- models: Object.fromEntries([...perModel.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
1387
- }
1388
- }));
1573
+ };
1574
+ var baseWithWorkspace = {
1575
+ get id() {
1576
+ return id({ type: "ulid" });
1577
+ },
1578
+ get createdAt() {
1579
+ return datetime({ default: "now", tz: true, index: true });
1580
+ },
1581
+ get updatedAt() {
1582
+ return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1583
+ },
1584
+ get deletedAt() {
1585
+ return datetime({ optional: true, index: true });
1586
+ },
1587
+ get workspaceId() {
1588
+ return string({ index: true });
1389
1589
  }
1390
- }
1391
- function toIR(params) {
1392
- const { schema: schema2, provider = "postgresql" } = params;
1393
- const extensions = params.extensions ?? EMPTY_EXTENSION_REGISTRY;
1394
- const plan = planSchema({ schema: schema2 });
1395
- const declarations = new ExtensionDeclarationCollector;
1396
- const issues = [];
1397
- const models = Array.from(schema2.__models.values(), (model) => buildModel({ model, plan, extensions, declarations, issues }));
1398
- const enums = Array.from(schema2.__enums, ([name, values]) => ({
1399
- name,
1400
- values: [...values]
1401
- }));
1402
- if (issues.length > 0) {
1403
- throw new VibeError({
1404
- code: "VIBE_SCHEMA_INVALID",
1405
- message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
1406
- meta: { issues }
1407
- });
1590
+ };
1591
+ var withMetadata = {
1592
+ get metadata() {
1593
+ return json({ optional: true });
1594
+ },
1595
+ get settings() {
1596
+ return json({ optional: true });
1408
1597
  }
1409
- const ir = {
1410
- datasource: { provider },
1411
- models,
1412
- enums,
1413
- extensions: declarations.toIR()
1414
- };
1415
- assertValidSchemaIR({ schema: ir });
1416
- return ir;
1417
- }
1598
+ };
1599
+ var withActive = {
1600
+ get isActive() {
1601
+ return boolean({ default: true });
1602
+ }
1603
+ };
1604
+ var withAudit = {
1605
+ get createdById() {
1606
+ return string({ optional: true, index: true });
1607
+ },
1608
+ get updatedById() {
1609
+ return string({ optional: true, index: true });
1610
+ }
1611
+ };
1612
+ var withExternalId = {
1613
+ get externalId() {
1614
+ return string({ index: true });
1615
+ }
1616
+ };
1617
+ var withNameDescription = {
1618
+ get name() {
1619
+ return string();
1620
+ },
1621
+ get description() {
1622
+ return string({ optional: true });
1623
+ }
1624
+ };
1625
+ var fields = {
1626
+ get datetime() {
1627
+ return datetime({ default: "now", updatedAt: true, tz: true, index: true });
1628
+ },
1629
+ get id() {
1630
+ return id({ type: "ulid" });
1631
+ },
1632
+ get title() {
1633
+ return string();
1634
+ },
1635
+ get description() {
1636
+ return string({ optional: true });
1637
+ },
1638
+ get externalId() {
1639
+ return string({ index: true });
1640
+ },
1641
+ get metadata() {
1642
+ return json({ optional: true });
1643
+ },
1644
+ get settings() {
1645
+ return json({ optional: true });
1646
+ },
1647
+ get isActive() {
1648
+ return boolean({ default: true });
1649
+ }
1650
+ };
1651
+ var templates = {
1652
+ base,
1653
+ baseWithWorkspace,
1654
+ withMetadata,
1655
+ withActive,
1656
+ withAudit,
1657
+ withExternalId,
1658
+ withNameDescription,
1659
+ fields
1660
+ };
1418
1661
 
1419
1662
  // packages/dsl/src/validate.ts
1420
1663
  function validateSchema(schema2) {
@@ -1534,6 +1777,7 @@ var dsl = {
1534
1777
  defineRelations,
1535
1778
  defineRelationsPart,
1536
1779
  mergeRelations,
1780
+ defineRls,
1537
1781
  schema,
1538
1782
  domain,
1539
1783
  toIR,
@@ -1561,6 +1805,7 @@ export {
1561
1805
  dsl,
1562
1806
  domain,
1563
1807
  detectOneToOnePairs,
1808
+ defineRls,
1564
1809
  defineRelationsPart,
1565
1810
  defineRelations,
1566
1811
  createIndexBuilder,
@@ -1572,6 +1817,7 @@ export {
1572
1817
  baseWithWorkspace,
1573
1818
  base,
1574
1819
  ChainableStringColumn,
1820
+ ChainableIntegerColumn,
1575
1821
  ChainableIdColumn,
1576
1822
  ChainableEnumColumn,
1577
1823
  ChainableDecimalColumn,
@@ -1579,4 +1825,4 @@ export {
1579
1825
  ChainableColumn
1580
1826
  };
1581
1827
 
1582
- //# debugId=EB3F790CB1F4BD1C64756E2164756E21
1828
+ //# debugId=88D4E8D0B695042E64756E2164756E21