@vibeorm/schema 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
@@ -16,12 +16,17 @@ function providerToDialect(params) {
16
16
  function isEnumType(fieldType) {
17
17
  return typeof fieldType === "object" && "enum" in fieldType;
18
18
  }
19
+ var ENUM_STORAGES = ["database", "string"];
20
+ function enumStorageOf(params) {
21
+ return params.enumDef.storage ?? "database";
22
+ }
19
23
  // packages/schema/src/errors.ts
20
24
  var VIBE_ERROR_CODES = [
21
25
  "VIBE_NOT_FOUND",
22
26
  "VIBE_UNIQUE_VIOLATION",
23
27
  "VIBE_FK_VIOLATION",
24
28
  "VIBE_VALIDATION",
29
+ "VIBE_DATA_INVALID",
25
30
  "VIBE_SCHEMA_INVALID",
26
31
  "VIBE_UNSUPPORTED_CAPABILITY",
27
32
  "VIBE_STALE_CLIENT",
@@ -29,6 +34,8 @@ var VIBE_ERROR_CODES = [
29
34
  "VIBE_MIGRATION",
30
35
  "VIBE_TRANSACTION",
31
36
  "VIBE_SCOPED",
37
+ "VIBE_RLS_CONTEXT",
38
+ "VIBE_PERMISSION_DENIED",
32
39
  "VIBE_ADAPTER",
33
40
  "VIBE_INTERNAL"
34
41
  ];
@@ -51,6 +58,23 @@ function unsupportedCapability(params) {
51
58
  meta: { dialect, feature }
52
59
  });
53
60
  }
61
+ var STORED_VALUE_PREVIEW_LENGTH = 80;
62
+ function describeStoredValue(params) {
63
+ const { value } = params;
64
+ if (typeof value !== "string")
65
+ return value === null ? "null" : typeof value;
66
+ return value.length > STORED_VALUE_PREVIEW_LENGTH ? `${value.slice(0, STORED_VALUE_PREVIEW_LENGTH)}\u2026` : value;
67
+ }
68
+ function invalidStoredData(params) {
69
+ const { model, field, enum: enumName, value } = params;
70
+ const preview = describeStoredValue({ value });
71
+ const invariant = enumName === undefined ? "the schema's invariant" : `enum ${enumName}`;
72
+ return new VibeError({
73
+ code: "VIBE_DATA_INVALID",
74
+ message: `Stored value on ${model}.${field} violates ${invariant}: ${JSON.stringify(preview)} \u2014 clean the rows (or stop the writer) before reading them through the ORM`,
75
+ meta: { model, field, ...enumName === undefined ? {} : { enum: enumName }, value: preview }
76
+ });
77
+ }
54
78
  function internalError(params) {
55
79
  return new VibeError({
56
80
  code: "VIBE_INTERNAL",
@@ -58,6 +82,25 @@ function internalError(params) {
58
82
  meta: params.meta
59
83
  });
60
84
  }
85
+ var RLS_CONTEXT_REASONS = Object.freeze([
86
+ "missing",
87
+ "invalid",
88
+ "expired",
89
+ "conflicting"
90
+ ]);
91
+
92
+ class RlsContextError extends VibeError {
93
+ reason;
94
+ constructor(params) {
95
+ super({
96
+ code: "VIBE_RLS_CONTEXT",
97
+ message: `Native RLS context is ${params.reason}${params.key === undefined ? "" : ` at key ${JSON.stringify(params.key)}`}`,
98
+ meta: { reason: params.reason, ...params.key === undefined ? {} : { key: params.key } }
99
+ });
100
+ this.name = "RlsContextError";
101
+ this.reason = params.reason;
102
+ }
103
+ }
61
104
  var SCOPED_REFUSAL_REASONS = [
62
105
  "policy_conflict",
63
106
  "unclassified_model",
@@ -94,6 +137,99 @@ class ScopedQueryError extends VibeError {
94
137
  this.reason = params.reason;
95
138
  }
96
139
  }
140
+ // packages/schema/src/prisma-literal.ts
141
+ var STRING_ESCAPES = {
142
+ '"': '"',
143
+ "'": "'",
144
+ "\\": "\\",
145
+ "/": "/",
146
+ n: `
147
+ `,
148
+ r: "\r",
149
+ t: "\t",
150
+ b: "\b",
151
+ f: "\f"
152
+ };
153
+ function malformed(params) {
154
+ return new VibeError({
155
+ code: "VIBE_SCHEMA_INVALID",
156
+ message: `Malformed string literal ${JSON.stringify(params.raw)}: ${params.detail}`,
157
+ meta: { issues: [{ rule: "malformed-string-literal", message: params.detail }], literal: params.raw }
158
+ });
159
+ }
160
+ function decodePrismaString(params) {
161
+ const { raw } = params;
162
+ const quoted = raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"');
163
+ const body = quoted ? raw.slice(1, -1) : raw;
164
+ if (!body.includes("\\"))
165
+ return body;
166
+ let out = "";
167
+ for (let index = 0;index < body.length; index += 1) {
168
+ const char = body[index];
169
+ if (char !== "\\") {
170
+ out += char;
171
+ continue;
172
+ }
173
+ const next = body[index + 1];
174
+ if (next === undefined)
175
+ throw malformed({ raw, detail: "it ends with a lone backslash" });
176
+ if (next === "u") {
177
+ const digits = body.slice(index + 2, index + 6);
178
+ if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
179
+ throw malformed({ raw, detail: `\\u must be followed by four hex digits, found "${digits}"` });
180
+ }
181
+ out += String.fromCharCode(Number.parseInt(digits, 16));
182
+ index += 5;
183
+ continue;
184
+ }
185
+ const decoded = STRING_ESCAPES[next];
186
+ if (decoded === undefined)
187
+ throw malformed({ raw, detail: `unknown escape "\\${next}"` });
188
+ out += decoded;
189
+ index += 1;
190
+ }
191
+ return out;
192
+ }
193
+ function unicodeEscape(params) {
194
+ return `\\u${params.char.charCodeAt(0).toString(16).padStart(4, "0")}`;
195
+ }
196
+ function encodePrismaString(params) {
197
+ const { value } = params;
198
+ let out = '"';
199
+ for (const char of value) {
200
+ if (char === "\\")
201
+ out += "\\\\";
202
+ else if (char === '"')
203
+ out += "\\\"";
204
+ else if (char === `
205
+ `)
206
+ out += "\\n";
207
+ else if (char === "\r")
208
+ out += "\\r";
209
+ else if (char === "\t")
210
+ out += "\\t";
211
+ else if (char < " " || char === "\x7F")
212
+ out += unicodeEscape({ char });
213
+ else
214
+ out += char;
215
+ }
216
+ return `${out}"`;
217
+ }
218
+ var INTEGER_LITERAL = /^-?\d+$/;
219
+ function exactIntegerLiteral(params) {
220
+ const { text } = params;
221
+ if (!INTEGER_LITERAL.test(text)) {
222
+ throw new VibeError({
223
+ code: "VIBE_SCHEMA_INVALID",
224
+ message: `Not an integer literal: ${JSON.stringify(text)}`,
225
+ meta: { issues: [{ rule: "malformed-integer-literal", message: text }], literal: text }
226
+ });
227
+ }
228
+ const asNumber = Number(text);
229
+ if (Number.isSafeInteger(asNumber))
230
+ return asNumber;
231
+ return BigInt(text).toString();
232
+ }
97
233
  // packages/schema/src/reserved.ts
98
234
  var RESERVED_DELEGATE_NAMES = [
99
235
  "findMany",
@@ -106,6 +242,7 @@ var RESERVED_DELEGATE_NAMES = [
106
242
  "createManyAndReturn",
107
243
  "update",
108
244
  "updateMany",
245
+ "updateManyAndReturn",
109
246
  "upsert",
110
247
  "delete",
111
248
  "deleteMany",
@@ -330,7 +467,336 @@ function validateComputedConfig(params) {
330
467
  });
331
468
  }
332
469
  }
470
+ // packages/schema/src/default-values.ts
471
+ function serializeJsonDefault(params) {
472
+ const ancestors = new Set;
473
+ const visit = (value) => {
474
+ if (value === null)
475
+ return "null";
476
+ if (typeof value === "string" || typeof value === "boolean")
477
+ return JSON.stringify(value);
478
+ if (typeof value === "number" && Number.isFinite(value))
479
+ return JSON.stringify(value);
480
+ if (typeof value !== "object" || value === null || ancestors.has(value)) {
481
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "JSON defaults require finite, acyclic JSON values" });
482
+ }
483
+ ancestors.add(value);
484
+ let text;
485
+ if (Array.isArray(value)) {
486
+ text = `[${Array.from(value, visit).join(",")}]`;
487
+ } else {
488
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
489
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "JSON defaults require plain objects, not class instances" });
490
+ }
491
+ if (Reflect.ownKeys(value).some((key) => {
492
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
493
+ return typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor);
494
+ })) {
495
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "JSON defaults require enumerable data properties, without symbols or accessors" });
496
+ }
497
+ text = `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${visit(Object.getOwnPropertyDescriptor(value, key).value)}`).join(",")}}`;
498
+ }
499
+ ancestors.delete(value);
500
+ return text;
501
+ };
502
+ return visit(params.value);
503
+ }
504
+ function scalarListDefault(params) {
505
+ const { type } = params;
506
+ return Array.from(params.values, (value) => {
507
+ if (type === "BigInt" && typeof value === "bigint")
508
+ return exactIntegerLiteral({ text: value.toString() });
509
+ if (type === "BigInt" && typeof value === "string" && /^-?\d+$/.test(value))
510
+ return exactIntegerLiteral({ text: value });
511
+ if ((type === "Int" || type === "BigInt") && typeof value === "number" && Number.isSafeInteger(value) && (type !== "Int" || value >= -2147483648 && value <= 2147483647))
512
+ return value;
513
+ if (type === "Float" && typeof value === "number" && Number.isFinite(value))
514
+ return value;
515
+ if (type === "Boolean" && typeof value === "boolean")
516
+ return value;
517
+ if ((type === "String" || isEnumType(type)) && typeof value === "string")
518
+ return value;
519
+ if (type === "Decimal" && (typeof value === "string" || typeof value === "number") && /^-?\d+(\.\d+)?$/.test(String(value))) {
520
+ const text = String(value);
521
+ const [integer, fraction = ""] = text.replace(/^-/, "").split(".");
522
+ const whole = BigInt(integer).toString();
523
+ const tail = fraction.replace(/0+$/, "");
524
+ const sign = text.startsWith("-") && (whole !== "0" || tail !== "") ? "-" : "";
525
+ return `${sign}${whole}${tail === "" ? "" : `.${tail}`}`;
526
+ }
527
+ if (type === "Json" && typeof value === "string")
528
+ return value;
529
+ if (type === "DateTime") {
530
+ const text = value instanceof Date && Number.isFinite(value.getTime()) ? value.toISOString() : value;
531
+ if (typeof text === "string") {
532
+ const match = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.(\d{1,6}))?(Z|[+-]\d{2}(?::?\d{2})?)?$/.exec(text);
533
+ const timestamp = match === null ? NaN : Date.parse(match[2] === undefined ? `${text}Z` : text);
534
+ if (Number.isFinite(timestamp))
535
+ return new Date(timestamp).toISOString().replace(/\.\d{3}Z$/, `.${(match?.[1] ?? "").padEnd(6, "0")}Z`);
536
+ }
537
+ }
538
+ if (type === "Bytes" && value instanceof Uint8Array)
539
+ return `\\x${Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
540
+ if (type === "Bytes" && typeof value === "string" && /^\\x(?:[0-9a-f]{2})*$/i.test(value))
541
+ return value.toLowerCase();
542
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Invalid scalar-list default element for ${isEnumType(type) ? type.enum : type}` });
543
+ });
544
+ }
545
+
546
+ // packages/schema/src/capabilities.ts
547
+ var POSTGRES_CAPABILITIES = Object.freeze({
548
+ nativeRowLevelSecurity: true,
549
+ returning: true,
550
+ scalarArrays: true,
551
+ nativeEnums: true,
552
+ ilike: true,
553
+ lateralJoin: true,
554
+ distinctOn: true,
555
+ partialIndexes: true,
556
+ conflictTargetConstraint: true,
557
+ transactionalDdl: true,
558
+ createManyAndReturn: true,
559
+ jsonColumnType: true,
560
+ orderByNulls: true,
561
+ fullTextEngines: Object.freeze(["bm25", "tsvector"])
562
+ });
563
+ var SQLITE_CAPABILITIES = Object.freeze({
564
+ nativeRowLevelSecurity: false,
565
+ returning: true,
566
+ scalarArrays: false,
567
+ nativeEnums: false,
568
+ ilike: false,
569
+ lateralJoin: false,
570
+ distinctOn: false,
571
+ partialIndexes: true,
572
+ conflictTargetConstraint: false,
573
+ transactionalDdl: true,
574
+ createManyAndReturn: true,
575
+ jsonColumnType: false,
576
+ orderByNulls: true,
577
+ fullTextEngines: Object.freeze(["fts5"])
578
+ });
579
+ var MYSQL_CAPABILITIES = Object.freeze({
580
+ nativeRowLevelSecurity: false,
581
+ returning: false,
582
+ scalarArrays: false,
583
+ nativeEnums: true,
584
+ ilike: false,
585
+ lateralJoin: false,
586
+ distinctOn: false,
587
+ partialIndexes: false,
588
+ conflictTargetConstraint: false,
589
+ transactionalDdl: false,
590
+ createManyAndReturn: false,
591
+ jsonColumnType: true,
592
+ orderByNulls: false,
593
+ fullTextEngines: Object.freeze(["fulltext"])
594
+ });
595
+ var DIALECT_CAPABILITIES = Object.freeze({
596
+ postgres: POSTGRES_CAPABILITIES,
597
+ sqlite: SQLITE_CAPABILITIES,
598
+ mysql: MYSQL_CAPABILITIES
599
+ });
600
+
333
601
  // packages/schema/src/validate.ts
602
+ var RESERVED_ENUM_NAMES = [
603
+ "enumValidators",
604
+ "enumValidator",
605
+ "OmittedKeys",
606
+ "ExactKeys",
607
+ "PossibleKeys",
608
+ "UncertainKeys",
609
+ "HasSelectedField",
610
+ "Provided",
611
+ "ViewDelegateMap",
612
+ "extensionManifest",
613
+ "extensionMounts",
614
+ "computedManifest",
615
+ "Object",
616
+ "Set",
617
+ "JSON",
618
+ "Record",
619
+ "Readonly",
620
+ "ReadonlyArray",
621
+ "Partial",
622
+ "Omit",
623
+ "NonNullable",
624
+ "Promise",
625
+ "PromiseLike",
626
+ "Awaited",
627
+ "TemplateStringsArray",
628
+ "Date",
629
+ "Uint8Array",
630
+ "schema",
631
+ "VibeClient",
632
+ "VibeClientInstance",
633
+ "VibeClientOptions",
634
+ "VibeError",
635
+ "VibeErrorCode",
636
+ "BatchPayload",
637
+ "UpsertManyPayload",
638
+ "QueryMode",
639
+ "RelationStrategy",
640
+ "SortOrder",
641
+ "SortOrderWithNulls",
642
+ "KeysetToken",
643
+ "KeysetOptions",
644
+ "JsonValue",
645
+ "InputJsonValue",
646
+ "InputJsonArray",
647
+ "InputJsonObject",
648
+ "VibeTelemetryOptions",
649
+ "VibeTelemetryExporter",
650
+ "VibeTelemetryOperationEvent",
651
+ "VibeTelemetryStatementEvent",
652
+ "VibeTelemetryHandle",
653
+ "ScopedClientInstance",
654
+ "ScopedModelsConfig",
655
+ "ScopedNestedMode",
656
+ "ViewClient",
657
+ "ViewDefinitionInput",
658
+ "ViewRow",
659
+ "createClient",
660
+ "DatabaseAdapter",
661
+ "DbNow",
662
+ "QueryEvent",
663
+ "Skip",
664
+ "TransactionOptions",
665
+ "SchemaIR",
666
+ "VibeExtension",
667
+ "ComputedConfig",
668
+ "WriteValidators",
669
+ "String",
670
+ "Bool",
671
+ "Int",
672
+ "BigInt",
673
+ "Float",
674
+ "Decimal",
675
+ "DateTime",
676
+ "Json",
677
+ "Bytes",
678
+ ...["String", "Bool", "Int", "BigInt", "Float", "Decimal", "DateTime", "Json", "Bytes"].flatMap((prefix) => ["Filter", "NullableFilter", "ListFilter"].map((suffix) => `${prefix}${suffix}`)),
679
+ ...["Int", "Float", "BigInt", "Decimal"].flatMap((prefix) => [`${prefix}FieldUpdateOperationsInput`, `Nullable${prefix}FieldUpdateOperationsInput`])
680
+ ];
681
+ var GENERATED_TYPE_NAMES = {
682
+ enumSuffixes: ["", "Filter", "NullableFilter", "ListFilter", "Validator"],
683
+ modelSuffixes: [
684
+ "",
685
+ "ScalarFieldEnum",
686
+ "SelectableFieldEnum",
687
+ "Relations",
688
+ "CountSelect",
689
+ "Select",
690
+ "Include",
691
+ "OmitInput",
692
+ "Args",
693
+ "ListRelationFilter",
694
+ "RelationFilter",
695
+ "WhereInput",
696
+ "WhereUniqueInput",
697
+ "OrderByInput",
698
+ "CreateInput",
699
+ "UncheckedCreateInput",
700
+ "CreateManyInput",
701
+ "UpdateInput",
702
+ "UncheckedUpdateInput",
703
+ "UpdateManyInput",
704
+ "CountAggregateInput",
705
+ "AvgAggregateInput",
706
+ "SumAggregateInput",
707
+ "MinAggregateInput",
708
+ "MaxAggregateInput",
709
+ "FindManyArgs",
710
+ "FindFirstArgs",
711
+ "FindUniqueArgs",
712
+ "CreateArgs",
713
+ "ConflictTarget",
714
+ "CreateOnConflictInput",
715
+ "CreateOrSkipArgs",
716
+ "CreateManyArgs",
717
+ "CreateManyAndReturnArgs",
718
+ "UpdateArgs",
719
+ "UpdateManyArgs",
720
+ "UpdateWhereInput",
721
+ "UpdateManyAndReturnArgs",
722
+ "UpsertArgs",
723
+ "UpsertManyArgs",
724
+ "DeleteArgs",
725
+ "DeleteManyArgs",
726
+ "CountArgs",
727
+ "AggregateArgs",
728
+ "GroupByArgs",
729
+ "OmitBase",
730
+ "CountResult",
731
+ "CountPayload",
732
+ "AggregateValues",
733
+ "AggregateSelection",
734
+ "CountOutput",
735
+ "RelationPayload",
736
+ "IncludePayload",
737
+ "GetPayload",
738
+ "AggregateResult",
739
+ "GroupByResult",
740
+ "Delegate",
741
+ "ViewFields",
742
+ "ViewSelect",
743
+ "ViewInclude",
744
+ "ViewNestedArgs",
745
+ "ViewFindManyArgs",
746
+ "ViewFindUniqueArgs",
747
+ "ViewScalars",
748
+ "ViewSelectScalars",
749
+ "ViewReveal",
750
+ "ViewRelationResult",
751
+ "ViewIncludeResult",
752
+ "ViewResult",
753
+ "ViewDelegate"
754
+ ],
755
+ nestedPrefixes: [
756
+ "Create",
757
+ "UncheckedCreate",
758
+ "Update",
759
+ "UncheckedUpdate",
760
+ "ConnectOrCreate",
761
+ "CreateNestedMany",
762
+ "UpdateNestedMany",
763
+ "CreateNestedOne",
764
+ "UpdateNestedOne",
765
+ "UpdateWithWhereUnique",
766
+ "UpdateManyWithWhere",
767
+ "UpsertWithWhereUnique"
768
+ ]
769
+ };
770
+ function validateGeneratedNames(params) {
771
+ const { schema, issues } = params;
772
+ const owners = new Map(RESERVED_ENUM_NAMES.map((name) => [name, "generated client"]));
773
+ if (schema.rls !== undefined) {
774
+ owners.set("RlsContext", "native RLS client");
775
+ owners.set("BoundVibeClientInstance", "native RLS client");
776
+ }
777
+ const register = (params2) => {
778
+ const previous = owners.get(params2.name);
779
+ if (previous !== undefined && previous !== params2.owner) {
780
+ issues.push({ rule: "generated-name-collision", message: `Generated name "${params2.name}" from ${params2.owner} conflicts with ${previous}` });
781
+ } else {
782
+ owners.set(params2.name, params2.owner);
783
+ }
784
+ };
785
+ for (const model of schema.models) {
786
+ const owner = `model "${model.name}"`;
787
+ for (const suffix of GENERATED_TYPE_NAMES.modelSuffixes)
788
+ register({ name: `${model.name}${suffix}`, owner });
789
+ const withouts = ["", ...model.relations.map((relation) => `Without${relation.name.charAt(0).toUpperCase()}${relation.name.slice(1)}`)];
790
+ for (const without of withouts) {
791
+ for (const prefix of GENERATED_TYPE_NAMES.nestedPrefixes)
792
+ register({ name: `${model.name}${prefix}${without}Input`, owner });
793
+ }
794
+ }
795
+ for (const declared of schema.enums) {
796
+ for (const suffix of GENERATED_TYPE_NAMES.enumSuffixes)
797
+ register({ name: `${declared.name}${suffix}`, owner: `enum "${declared.name}"` });
798
+ }
799
+ }
334
800
  function validateSchemaIR(params) {
335
801
  const { schema } = params;
336
802
  const issues = [];
@@ -346,15 +812,15 @@ function validateSchemaIR(params) {
346
812
  modelByName.set(model.name, model);
347
813
  }
348
814
  }
349
- const enumNames = new Set;
815
+ const enumByName = new Map;
350
816
  for (const enumDef of schema.enums) {
351
- if (enumNames.has(enumDef.name)) {
817
+ if (enumByName.has(enumDef.name)) {
352
818
  issues.push({
353
819
  rule: "duplicate-enum",
354
820
  message: `Duplicate enum name "${enumDef.name}"`
355
821
  });
356
822
  } else {
357
- enumNames.add(enumDef.name);
823
+ enumByName.set(enumDef.name, enumDef);
358
824
  }
359
825
  if (enumDef.values.length === 0) {
360
826
  issues.push({
@@ -362,9 +828,52 @@ function validateSchemaIR(params) {
362
828
  message: `Enum "${enumDef.name}" has no values`
363
829
  });
364
830
  }
831
+ const seenValues = new Set;
832
+ for (const value of enumDef.values) {
833
+ if (seenValues.has(value)) {
834
+ issues.push({
835
+ rule: "duplicate-enum-value",
836
+ message: `Enum "${enumDef.name}" lists value "${value}" more than once`
837
+ });
838
+ }
839
+ seenValues.add(value);
840
+ }
841
+ if (enumDef.storage !== undefined && !ENUM_STORAGES.includes(enumDef.storage)) {
842
+ issues.push({
843
+ rule: "invalid-enum-storage",
844
+ message: `Enum "${enumDef.name}" has storage ${JSON.stringify(enumDef.storage)} \u2014 expected one of ${ENUM_STORAGES.join(", ")}`
845
+ });
846
+ }
847
+ if (modelByName.has(enumDef.name)) {
848
+ issues.push({
849
+ rule: "enum-model-name-collision",
850
+ message: `Enum "${enumDef.name}" shares its name with a model \u2014 the generated client exports one type per name`,
851
+ model: enumDef.name
852
+ });
853
+ }
854
+ if (RESERVED_ENUM_NAMES.includes(enumDef.name)) {
855
+ issues.push({
856
+ rule: "enum-reserved-name",
857
+ message: `Enum "${enumDef.name}" collides with a name the generated client already exports or imports`
858
+ });
859
+ }
365
860
  }
366
861
  for (const model of schema.models) {
367
- validateModel({ model, modelByName, enumNames, issues });
862
+ validateModel({ model, modelByName, enumByName, issues });
863
+ }
864
+ for (const model of schema.models) {
865
+ for (const relation of model.relations) {
866
+ if (relation.targetModel !== model.name)
867
+ continue;
868
+ if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
869
+ continue;
870
+ issues.push({
871
+ rule: "implicit-self-many-to-many",
872
+ message: `Relation "${model.name}.${relation.name}" is an implicit self many-to-many. Both sides of a self ` + `relation own the same join-table column, so this field and its inverse read the same rows. Replace ` + `it with an explicit junction model carrying one foreign key per direction, each under its own ` + `@relation name, and point "${relation.name}" at that model. Worked example, and the row copy for a ` + `join table that already exists: docs/queries.md, "Directed self relations".`,
873
+ model: model.name,
874
+ field: relation.name
875
+ });
876
+ }
368
877
  }
369
878
  const provider = schema.datasource.provider;
370
879
  for (const model of schema.models) {
@@ -462,10 +971,12 @@ function validateSchemaIR(params) {
462
971
  });
463
972
  }
464
973
  }
974
+ validateGeneratedNames({ schema, issues });
975
+ validateNativeRls({ schema, issues });
465
976
  return issues;
466
977
  }
467
978
  function validateModel(params) {
468
- const { model, modelByName, enumNames, issues } = params;
979
+ const { model, modelByName, enumByName, issues } = params;
469
980
  const memberNames = new Set;
470
981
  for (const field of model.fields) {
471
982
  if (memberNames.has(field.name)) {
@@ -477,13 +988,27 @@ function validateModel(params) {
477
988
  });
478
989
  }
479
990
  memberNames.add(field.name);
480
- if (isEnumType(field.type) && !enumNames.has(field.type.enum)) {
481
- issues.push({
482
- rule: "unknown-enum",
483
- message: `Field "${model.name}.${field.name}" references unknown enum "${field.type.enum}"`,
484
- model: model.name,
485
- field: field.name
486
- });
991
+ if (isEnumType(field.type)) {
992
+ const enumDef = enumByName.get(field.type.enum);
993
+ if (enumDef === undefined) {
994
+ issues.push({
995
+ rule: "unknown-enum",
996
+ message: `Field "${model.name}.${field.name}" references unknown enum "${field.type.enum}"`,
997
+ model: model.name,
998
+ field: field.name
999
+ });
1000
+ } else {
1001
+ const defaultValue = field.default;
1002
+ const candidate = defaultValue === undefined ? undefined : defaultValue.kind === "enumValue" ? defaultValue.value : defaultValue.kind === "literal" && typeof defaultValue.value === "string" ? defaultValue.value : undefined;
1003
+ if (candidate !== undefined && !enumDef.values.includes(candidate)) {
1004
+ issues.push({
1005
+ rule: "enum-default-not-member",
1006
+ message: `Default "${candidate}" on "${model.name}.${field.name}" is not a member of enum "${enumDef.name}" (${enumDef.values.join(", ")})`,
1007
+ model: model.name,
1008
+ field: field.name
1009
+ });
1010
+ }
1011
+ }
487
1012
  }
488
1013
  const columnName = field.dbName ?? field.name;
489
1014
  if (columnName.startsWith(RESERVED_COLUMN_PREFIX)) {
@@ -647,6 +1172,39 @@ function validateModel(params) {
647
1172
  }
648
1173
  }
649
1174
  function assertValidSchemaIR(params) {
1175
+ const dialect = providerToDialect({ provider: params.schema.datasource.provider });
1176
+ if (params.schema.rls !== undefined && !DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity) {
1177
+ throw unsupportedCapability({ dialect, feature: "nativeRowLevelSecurity" });
1178
+ }
1179
+ for (const model of params.schema.models) {
1180
+ const counters = model.fields.filter((field) => field.default?.kind === "autoincrement");
1181
+ if (dialect === "mysql" && counters.length > 1)
1182
+ throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
1183
+ for (const field of model.fields) {
1184
+ if (field.default?.kind === "list") {
1185
+ if (dialect !== "postgres")
1186
+ throw unsupportedCapability({ dialect, feature: "scalar-list defaults" });
1187
+ if (!field.isList)
1188
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `List default on non-list field ${model.name}.${field.name}` });
1189
+ scalarListDefault({ type: field.type, values: field.default.values });
1190
+ if (isEnumType(field.type)) {
1191
+ const name = field.type.enum;
1192
+ const members = params.schema.enums.find((entry) => entry.name === name)?.values ?? [];
1193
+ if (field.default.values.some((value) => typeof value !== "string" || !members.includes(value)))
1194
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Invalid enum-list default on ${model.name}.${field.name}` });
1195
+ }
1196
+ }
1197
+ if (field.default?.kind !== "autoincrement")
1198
+ continue;
1199
+ if (field.isList || field.isOptional || field.type !== "Int" && field.type !== "BigInt")
1200
+ throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `autoincrement requires a required scalar Int or BigInt: ${model.name}.${field.name}` });
1201
+ const pk = model.primaryKey?.fields ?? model.fields.filter((entry) => entry.isId).map((entry) => entry.name);
1202
+ if (dialect === "sqlite" && (pk.length !== 1 || pk[0] !== field.name))
1203
+ throw unsupportedCapability({ dialect, feature: `non-primary auto-increment column ${model.name}.${field.name}` });
1204
+ if (dialect === "mysql" && pk[0] !== field.name && !field.isUnique && !model.indexes.some((index) => index.fields[0]?.name === field.name))
1205
+ throw unsupportedCapability({ dialect, feature: `unindexed auto-increment column ${model.name}.${field.name}` });
1206
+ }
1207
+ }
650
1208
  const issues = validateSchemaIR(params);
651
1209
  if (issues.length > 0) {
652
1210
  throw new VibeError({
@@ -656,6 +1214,181 @@ function assertValidSchemaIR(params) {
656
1214
  });
657
1215
  }
658
1216
  }
1217
+ function rlsRecord(value) {
1218
+ return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) && Reflect.ownKeys(value).every((key) => {
1219
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1220
+ return typeof key === "string" && descriptor?.enumerable === true && "value" in descriptor;
1221
+ });
1222
+ }
1223
+ function rlsKeys(params) {
1224
+ const own = Reflect.ownKeys(params.value);
1225
+ return own.length === params.keys.length && params.keys.every((key) => Object.hasOwn(params.value, key));
1226
+ }
1227
+ function validateRlsContextIR(params) {
1228
+ const { context } = params;
1229
+ const issues = [];
1230
+ if (!rlsRecord(context))
1231
+ return [{ rule: "rls-invalid-context", message: "Native RLS context must be a named scalar-slot map" }];
1232
+ const seen = new Set;
1233
+ for (const key of Reflect.ownKeys(context)) {
1234
+ const slot = typeof key === "string" ? context[key] : undefined;
1235
+ if (typeof key !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || seen.has(key.toLowerCase()) || !rlsRecord(slot) || !rlsKeys({ value: slot, keys: ["type", "nullable"] }) || typeof slot.nullable !== "boolean" || (typeof slot.type !== "string" || !["String", "Boolean", "Int", "BigInt", "Float", "Decimal", "DateTime"].includes(slot.type))) {
1236
+ issues.push({ rule: "rls-invalid-context", message: `Invalid native RLS context slot ${String(key)}; use distinct setting keys and supported scalar slots with explicit nullable` });
1237
+ }
1238
+ if (typeof key === "string")
1239
+ seen.add(key.toLowerCase());
1240
+ }
1241
+ return issues;
1242
+ }
1243
+ function isImplicitManyToManyRelation(params) {
1244
+ const { model, relation, models } = params;
1245
+ if (model.isView || !relation.isList || (relation.fields?.length ?? 0) !== 0)
1246
+ return false;
1247
+ const target = models.find((candidate) => candidate.name === relation.targetModel);
1248
+ if (target === undefined || target.isView)
1249
+ return false;
1250
+ const counterpart = soleRelationCounterpart({ relation, ownModel: model.name, target });
1251
+ if (counterpart === undefined || !counterpart.isList || (counterpart.fields?.length ?? 0) !== 0)
1252
+ return false;
1253
+ return soleRelationCounterpart({ relation: counterpart, ownModel: target.name, target: model }) === relation;
1254
+ }
1255
+ function soleRelationCounterpart(params) {
1256
+ const { relation, ownModel, target } = params;
1257
+ const candidates = target.relations.filter((candidate) => candidate !== relation && candidate.targetModel === ownModel && candidate.relationName === relation.relationName);
1258
+ return candidates.length === 1 ? candidates[0] : undefined;
1259
+ }
1260
+ function validateNativeRls(params) {
1261
+ const { schema, issues } = params;
1262
+ if (schema.rls === undefined) {
1263
+ if (schema.models.some((model) => model.rls !== undefined)) {
1264
+ issues.push({ rule: "rls-missing-context", message: "Model RLS rules require a schema-level rls.context declaration" });
1265
+ }
1266
+ return;
1267
+ }
1268
+ const dialect = providerToDialect({ provider: schema.datasource.provider });
1269
+ if (!DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity) {
1270
+ issues.push({ rule: "rls-unsupported-capability", message: `nativeRowLevelSecurity is not supported on ${dialect}` });
1271
+ }
1272
+ if (!rlsRecord(schema.rls) || !rlsKeys({ value: schema.rls, keys: ["context"] })) {
1273
+ issues.push({ rule: "rls-invalid-context", message: "Native RLS supports only a schema-level context declaration" });
1274
+ return;
1275
+ }
1276
+ const contextIssues = validateRlsContextIR({ context: schema.rls.context });
1277
+ issues.push(...contextIssues);
1278
+ const context = contextIssues.length === 0 ? schema.rls.context : undefined;
1279
+ const byName = new Map(schema.models.map((model) => [model.name, model]));
1280
+ const tables = new Set;
1281
+ const throughTargets = new Map;
1282
+ for (const model of schema.models) {
1283
+ const add = (params2) => {
1284
+ issues.push({ ...params2, model: model.name });
1285
+ };
1286
+ const table = model.dbName ?? model.name;
1287
+ if (tables.has(table))
1288
+ add({ rule: "rls-duplicate-table", message: `RLS models share physical table ${table}` });
1289
+ tables.add(table);
1290
+ const columns = new Set;
1291
+ for (const field of model.fields) {
1292
+ const name = field.dbName ?? field.name;
1293
+ if (columns.has(name))
1294
+ add({ rule: "rls-duplicate-column", message: `RLS model ${model.name} repeats physical column ${name}`, field: field.name });
1295
+ columns.add(name);
1296
+ }
1297
+ if (model.policy !== undefined)
1298
+ add({ rule: "rls-policy-conflict", message: "Native RLS cannot combine with application-level ModelIR.policy" });
1299
+ if (model.isView)
1300
+ add({ rule: "rls-view-unsupported", message: "Database views are not supported on native RLS clients; remove the view declaration" });
1301
+ if (model.rls === undefined) {
1302
+ add({ rule: "rls-unclassified-model", message: `Classify ${model.name} as where, through, unprotected or deny` });
1303
+ continue;
1304
+ }
1305
+ if (model.rls !== "unprotected") {
1306
+ for (const relation of model.relations) {
1307
+ if (isImplicitManyToManyRelation({ model, relation, models: schema.models })) {
1308
+ add({ rule: "rls-implicit-many-to-many", message: "Protected implicit many-to-many relations need an explicit junction model with its own rule", field: relation.name });
1309
+ }
1310
+ }
1311
+ }
1312
+ const rule = model.rls;
1313
+ if (rule === "unprotected" || rule === "deny")
1314
+ continue;
1315
+ if (!rlsRecord(rule)) {
1316
+ add({ rule: "rls-invalid-rule", message: "Native RLS accepts where, through, unprotected or deny only" });
1317
+ continue;
1318
+ }
1319
+ if (rlsKeys({ value: rule, keys: ["where"] }) && rlsRecord(rule.where) && Reflect.ownKeys(rule.where).length > 0) {
1320
+ for (const fieldName of Reflect.ownKeys(rule.where)) {
1321
+ const ref = typeof fieldName === "string" ? rule.where[fieldName] : undefined;
1322
+ if (!rlsRecord(ref) || !rlsKeys({ value: ref, keys: ["context"] }) || typeof ref.context !== "string") {
1323
+ add({ rule: "rls-invalid-rule", message: "Ownership equality accepts a context reference only" });
1324
+ continue;
1325
+ }
1326
+ const field = model.fields.find((f) => f.name === fieldName);
1327
+ if (field === undefined) {
1328
+ add({ rule: "rls-unknown-field", message: `Native RLS requires a direct scalar field, not path ${String(fieldName)}` });
1329
+ } else if (field.isList || field.isUnsupported || isEnumType(field.type) || field.type === "Json" || field.type === "Bytes") {
1330
+ add({ rule: "rls-unsupported-field", message: "Native RLS ownership requires a supported non-list scalar", field: field.name });
1331
+ } else if (context !== undefined) {
1332
+ const slot = Object.hasOwn(context, ref.context) ? context[ref.context] : undefined;
1333
+ if (slot === undefined)
1334
+ add({ rule: "rls-unknown-context", message: `Unknown native RLS context key ${ref.context}`, field: field.name });
1335
+ else if (slot.type !== field.type)
1336
+ add({ rule: "rls-context-type-mismatch", message: `Context ${ref.context} does not match ${model.name}.${field.name}`, field: field.name });
1337
+ }
1338
+ }
1339
+ continue;
1340
+ }
1341
+ if (rlsKeys({ value: rule, keys: ["through"] }) && typeof rule.through === "string") {
1342
+ const relation = model.relations.find((r) => r.name === rule.through);
1343
+ const target = relation === undefined ? undefined : byName.get(relation.targetModel);
1344
+ if (relation === undefined || target === undefined || relation.model !== model.name || relation.isList || relation.type !== "oneToOne" && relation.type !== "manyToOne" || !validRlsForeignKey({ model, target, relation })) {
1345
+ add({ rule: "rls-invalid-through", message: "Native RLS through requires a to-one relation with an actual, complete foreign key on this model" });
1346
+ continue;
1347
+ }
1348
+ if (relation.isOptional || relation.fields?.some((name) => model.fields.find((field) => field.name === name)?.isOptional)) {
1349
+ add({ rule: "rls-nullable-through", message: "Native RLS through requires a non-nullable parent relation and foreign key; NULL parent links cannot satisfy WITH CHECK. Make the link required or use a direct ownership rule", field: relation.name });
1350
+ continue;
1351
+ }
1352
+ if (table === "parent")
1353
+ add({ rule: "rls-through-alias-conflict", message: "A through table mapped to parent would shadow the fixed policy parent alias; use another table name" });
1354
+ throughTargets.set(model.name, target.name);
1355
+ continue;
1356
+ }
1357
+ add({ rule: "rls-invalid-rule", message: "Native RLS accepts a nonempty equality map or a single through relation; extra properties and operation-specific rules refuse" });
1358
+ }
1359
+ for (const [origin, first] of throughTargets) {
1360
+ const seen = new Set([origin]);
1361
+ let current = first;
1362
+ while (current !== undefined && !seen.has(current)) {
1363
+ seen.add(current);
1364
+ const rule = byName.get(current)?.rls;
1365
+ if (rlsRecord(rule) && rlsKeys({ value: rule, keys: ["where"] }))
1366
+ break;
1367
+ current = throughTargets.get(current);
1368
+ }
1369
+ if (current === undefined || current === origin || throughTargets.has(current) && seen.has(current)) {
1370
+ issues.push({ rule: "rls-through-chain-invalid", message: `Through chain from ${origin} must be acyclic and end in a direct where rule`, model: origin });
1371
+ }
1372
+ }
1373
+ }
1374
+ function validRlsForeignKey(params) {
1375
+ const { model, target, relation } = params;
1376
+ const fields = relation.fields;
1377
+ const refs = relation.references;
1378
+ if (fields === undefined || refs === undefined || fields.length === 0 || fields.length !== refs.length || new Set(fields).size !== fields.length || new Set(refs).size !== refs.length)
1379
+ return false;
1380
+ const validFields = fields.every((name, index) => {
1381
+ const field = model.fields.find((f) => f.name === name);
1382
+ const ref = target.fields.find((f) => f.name === refs[index]);
1383
+ return field !== undefined && ref !== undefined && !field.isList && !ref.isList && !field.isUnsupported && !ref.isUnsupported && (field.type === ref.type || isEnumType(field.type) && isEnumType(ref.type) && field.type.enum === ref.type.enum);
1384
+ });
1385
+ const uniqueSets = [
1386
+ ...target.fields.filter((f) => f.isId || f.isUnique).map((f) => [f.name]),
1387
+ ...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
1388
+ ...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined).map((index) => index.fields.map((f) => f.name))
1389
+ ];
1390
+ return validFields && uniqueSets.some((set) => set.length === refs.length && set.every((name) => refs.includes(name)));
1391
+ }
659
1392
  // packages/schema/src/m2m.ts
660
1393
  function implicitJoinTable(params) {
661
1394
  const sorted = [params.modelA, params.modelB].sort();
@@ -672,30 +1405,47 @@ function implicitJoinTable(params) {
672
1405
  }
673
1406
  export {
674
1407
  validateSchemaIR,
1408
+ validateRlsContextIR,
675
1409
  validateComputedConfig,
676
1410
  unsupportedCapability,
1411
+ serializeJsonDefault,
1412
+ scalarListDefault,
677
1413
  providerToDialect,
678
1414
  mergeExtensionDeclarations,
679
1415
  isReservedDelegateName,
680
1416
  isReservedClientName,
1417
+ isImplicitManyToManyRelation,
681
1418
  isEnumType,
1419
+ invalidStoredData,
682
1420
  internalError,
683
1421
  implicitJoinTable,
684
1422
  extensionOptionsHash,
685
1423
  extensionManifestEntry,
686
1424
  extensionDeclarations,
1425
+ exactIntegerLiteral,
1426
+ enumStorageOf,
1427
+ encodePrismaString,
687
1428
  defineExtension,
1429
+ decodePrismaString,
688
1430
  buildExtensionRegistry,
689
1431
  buildComputedManifest,
690
1432
  assertValidSchemaIR,
691
1433
  VibeError,
692
1434
  VIBE_ERROR_CODES,
693
1435
  ScopedQueryError,
1436
+ SQLITE_CAPABILITIES,
694
1437
  SCOPED_REFUSAL_REASONS,
1438
+ RlsContextError,
1439
+ RLS_CONTEXT_REASONS,
1440
+ RESERVED_ENUM_NAMES,
695
1441
  RESERVED_DELEGATE_NAMES,
696
1442
  RESERVED_CLIENT_NAMES,
1443
+ POSTGRES_CAPABILITIES,
1444
+ MYSQL_CAPABILITIES,
1445
+ ENUM_STORAGES,
697
1446
  EMPTY_EXTENSION_REGISTRY,
1447
+ DIALECT_CAPABILITIES,
698
1448
  DIALECTS
699
1449
  };
700
1450
 
701
- //# debugId=9AF13DF5C6791C7664756E2164756E21
1451
+ //# debugId=E40659BCEBFDD98E64756E2164756E21