@vibeorm/schema 2.5.0 → 4.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/LICENSE +21 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/errors.d.ts +10 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +419 -45
- package/dist/index.js.map +11 -9
- package/dist/ir.d.ts +7 -0
- package/dist/json-schema-ref.d.ts +30 -0
- package/dist/predicate.d.ts +29 -3
- package/dist/time-of-day.d.ts +19 -0
- package/package.json +4 -2
- package/dist/capabilities.d.ts.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/database-module.d.ts.map +0 -1
- package/dist/default-values.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/exclusion.d.ts.map +0 -1
- package/dist/extension.d.ts.map +0 -1
- package/dist/index-operator-classes.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/ir.d.ts.map +0 -1
- package/dist/m2m.d.ts.map +0 -1
- package/dist/migration-objects.d.ts.map +0 -1
- package/dist/module-schema.d.ts.map +0 -1
- package/dist/namespaces.d.ts.map +0 -1
- package/dist/physical-names.d.ts.map +0 -1
- package/dist/predicate.d.ts.map +0 -1
- package/dist/prisma-literal.d.ts.map +0 -1
- package/dist/reserved.d.ts.map +0 -1
- package/dist/validate.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ var VIBE_ERROR_CODES = [
|
|
|
19
19
|
"VIBE_RLS_CONTEXT",
|
|
20
20
|
"VIBE_PERMISSION_DENIED",
|
|
21
21
|
"VIBE_ADAPTER",
|
|
22
|
+
"VIBE_ADAPTER_CLOSED",
|
|
22
23
|
"VIBE_INTERNAL"
|
|
23
24
|
];
|
|
24
25
|
|
|
@@ -259,6 +260,7 @@ var POSTGRES_CAPABILITIES = Object.freeze({
|
|
|
259
260
|
exclusionConstraints: true,
|
|
260
261
|
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
261
262
|
checkNormalization: "rollback",
|
|
263
|
+
schemaCheckExpressions: true,
|
|
262
264
|
nativeRowLevelSecurity: true,
|
|
263
265
|
returning: true,
|
|
264
266
|
scalarArrays: true,
|
|
@@ -275,7 +277,8 @@ var POSTGRES_CAPABILITIES = Object.freeze({
|
|
|
275
277
|
fullTextEngines: Object.freeze(["bm25", "tsvector"]),
|
|
276
278
|
rowLockStrengths: Object.freeze(["update", "noKeyUpdate", "share", "keyShare"]),
|
|
277
279
|
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
278
|
-
advisoryLocks: true
|
|
280
|
+
advisoryLocks: true,
|
|
281
|
+
timeOfDayTypes: Object.freeze(["time", "timetz"])
|
|
279
282
|
});
|
|
280
283
|
var SQLITE_CAPABILITIES = Object.freeze({
|
|
281
284
|
fixedNamespaces: false,
|
|
@@ -284,6 +287,7 @@ var SQLITE_CAPABILITIES = Object.freeze({
|
|
|
284
287
|
exclusionConstraints: false,
|
|
285
288
|
exclusionExtensions: Object.freeze([]),
|
|
286
289
|
checkNormalization: "sourceText",
|
|
290
|
+
schemaCheckExpressions: false,
|
|
287
291
|
nativeRowLevelSecurity: false,
|
|
288
292
|
returning: true,
|
|
289
293
|
scalarArrays: false,
|
|
@@ -300,7 +304,8 @@ var SQLITE_CAPABILITIES = Object.freeze({
|
|
|
300
304
|
fullTextEngines: Object.freeze(["fts5"]),
|
|
301
305
|
rowLockStrengths: Object.freeze([]),
|
|
302
306
|
rowLockWaitPolicies: Object.freeze([]),
|
|
303
|
-
advisoryLocks: false
|
|
307
|
+
advisoryLocks: false,
|
|
308
|
+
timeOfDayTypes: Object.freeze([])
|
|
304
309
|
});
|
|
305
310
|
var MYSQL_CAPABILITIES = Object.freeze({
|
|
306
311
|
fixedNamespaces: false,
|
|
@@ -309,6 +314,7 @@ var MYSQL_CAPABILITIES = Object.freeze({
|
|
|
309
314
|
exclusionConstraints: false,
|
|
310
315
|
exclusionExtensions: Object.freeze([]),
|
|
311
316
|
checkNormalization: "temporary",
|
|
317
|
+
schemaCheckExpressions: false,
|
|
312
318
|
nativeRowLevelSecurity: false,
|
|
313
319
|
returning: false,
|
|
314
320
|
scalarArrays: false,
|
|
@@ -325,7 +331,8 @@ var MYSQL_CAPABILITIES = Object.freeze({
|
|
|
325
331
|
fullTextEngines: Object.freeze(["fulltext"]),
|
|
326
332
|
rowLockStrengths: Object.freeze(["update", "share"]),
|
|
327
333
|
rowLockWaitPolicies: Object.freeze(["wait", "nowait", "skipLocked"]),
|
|
328
|
-
advisoryLocks: false
|
|
334
|
+
advisoryLocks: false,
|
|
335
|
+
timeOfDayTypes: Object.freeze(["time"])
|
|
329
336
|
});
|
|
330
337
|
var DIALECT_CAPABILITIES = Object.freeze({
|
|
331
338
|
postgres: POSTGRES_CAPABILITIES,
|
|
@@ -606,36 +613,121 @@ function isPredicateFieldRef(operand) {
|
|
|
606
613
|
return operand.kind === "field";
|
|
607
614
|
}
|
|
608
615
|
function predicateFieldNames(params) {
|
|
609
|
-
|
|
616
|
+
return [...new Set(inspectPredicate(params).expressions.flatMap((expression) => expression.kind === "field" ? [expression.name] : []))];
|
|
617
|
+
}
|
|
618
|
+
function inspectPredicate(params) {
|
|
619
|
+
const expressions = [];
|
|
620
|
+
let checkOnly = false;
|
|
621
|
+
const expression = (node) => {
|
|
622
|
+
if (typeof node !== "object" || node === null)
|
|
623
|
+
throw invalidOperand();
|
|
624
|
+
switch (node.kind) {
|
|
625
|
+
case "field":
|
|
626
|
+
expressions.push(node);
|
|
627
|
+
return;
|
|
628
|
+
case "jsonText":
|
|
629
|
+
if (typeof node.field !== "object" || node.field === null || node.field.kind !== "field")
|
|
630
|
+
throw invalidOperand();
|
|
631
|
+
expressions.push(node);
|
|
632
|
+
checkOnly = true;
|
|
633
|
+
expression(node.field);
|
|
634
|
+
return;
|
|
635
|
+
case "length":
|
|
636
|
+
expressions.push(node);
|
|
637
|
+
checkOnly = true;
|
|
638
|
+
expression(node.expression);
|
|
639
|
+
return;
|
|
640
|
+
default:
|
|
641
|
+
throw invalidOperand();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
610
644
|
const walk = (node) => {
|
|
611
645
|
switch (node.kind) {
|
|
612
646
|
case "compare":
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
647
|
+
expression(node.left);
|
|
648
|
+
if (typeof node.right !== "object" || node.right === null || node.right.kind !== "value")
|
|
649
|
+
expression(node.right);
|
|
616
650
|
return;
|
|
617
651
|
case "inList":
|
|
618
652
|
case "isNull":
|
|
619
|
-
|
|
653
|
+
expression(node.field);
|
|
620
654
|
return;
|
|
621
655
|
case "and":
|
|
622
656
|
case "or":
|
|
623
657
|
for (const operand of node.operands)
|
|
624
658
|
walk(operand);
|
|
625
659
|
return;
|
|
660
|
+
case "isTrue":
|
|
661
|
+
checkOnly = true;
|
|
662
|
+
walk(node.operand);
|
|
663
|
+
return;
|
|
626
664
|
case "not":
|
|
627
665
|
walk(node.operand);
|
|
628
666
|
return;
|
|
629
667
|
}
|
|
630
668
|
};
|
|
631
669
|
walk(params.predicate);
|
|
632
|
-
return
|
|
670
|
+
return { expressions, checkOnly };
|
|
671
|
+
}
|
|
672
|
+
function invalidOperand() {
|
|
673
|
+
return new VibeError({
|
|
674
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
675
|
+
message: "A schema predicate operand must be a field, jsonText or length expression, or a literal on the right of a comparison",
|
|
676
|
+
meta: { rule: "predicate-operand-invalid" }
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
function assertPredicateCapabilities(params) {
|
|
680
|
+
if (!inspectPredicate(params).checkOnly)
|
|
681
|
+
return;
|
|
682
|
+
if (params.usage !== "check")
|
|
683
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "schema CHECK expressions outside CHECK constraints" });
|
|
684
|
+
if (!DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)
|
|
685
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "schemaCheckExpressions" });
|
|
633
686
|
}
|
|
634
687
|
function predicateKey(params) {
|
|
688
|
+
if (inspectPredicate(params).checkOnly) {
|
|
689
|
+
const scalar = (node) => {
|
|
690
|
+
switch (node.kind) {
|
|
691
|
+
case "field":
|
|
692
|
+
return ["field", node.name];
|
|
693
|
+
case "jsonText":
|
|
694
|
+
return ["jsonText", node.field.name, node.path];
|
|
695
|
+
case "length":
|
|
696
|
+
return ["length", scalar(node.expression)];
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
const key = (node) => {
|
|
700
|
+
switch (node.kind) {
|
|
701
|
+
case "compare":
|
|
702
|
+
return [node.kind, node.op, scalar(node.left), node.right.kind === "value" ? ["value", node.right.value] : scalar(node.right)];
|
|
703
|
+
case "inList":
|
|
704
|
+
return [node.kind, node.field.name, node.negate === true, node.values.map((value) => value.value)];
|
|
705
|
+
case "isNull":
|
|
706
|
+
return [node.kind, node.field.name, node.negate === true];
|
|
707
|
+
case "and":
|
|
708
|
+
case "or":
|
|
709
|
+
return [node.kind, node.operands.map(key)];
|
|
710
|
+
case "not":
|
|
711
|
+
case "isTrue":
|
|
712
|
+
return [node.kind, key(node.operand)];
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
return `check:${JSON.stringify(key(params.predicate))}`;
|
|
716
|
+
}
|
|
717
|
+
const expression = (node) => {
|
|
718
|
+
switch (node.kind) {
|
|
719
|
+
case "field":
|
|
720
|
+
return `f:${node.name}`;
|
|
721
|
+
case "jsonText":
|
|
722
|
+
return `jsonText(${JSON.stringify([node.field.name, node.path])})`;
|
|
723
|
+
case "length":
|
|
724
|
+
return `length(${expression(node.expression)})`;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
635
727
|
const encode = (node) => {
|
|
636
728
|
switch (node.kind) {
|
|
637
729
|
case "compare":
|
|
638
|
-
return `cmp(${node.op},${node.left.name
|
|
730
|
+
return `cmp(${node.op},${node.left.kind === "field" ? node.left.name : expression(node.left)},${node.right.kind === "value" ? `v:${typeof node.right.value}:${String(node.right.value)}` : expression(node.right)})`;
|
|
639
731
|
case "inList":
|
|
640
732
|
return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
|
|
641
733
|
case "isNull":
|
|
@@ -645,10 +737,26 @@ function predicateKey(params) {
|
|
|
645
737
|
return `${node.kind}(${node.operands.map(encode).join(",")})`;
|
|
646
738
|
case "not":
|
|
647
739
|
return `not(${encode(node.operand)})`;
|
|
740
|
+
case "isTrue":
|
|
741
|
+
return `isTrue(${encode(node.operand)})`;
|
|
648
742
|
}
|
|
649
743
|
};
|
|
650
744
|
return encode(params.predicate);
|
|
651
745
|
}
|
|
746
|
+
// packages/schema/src/time-of-day.ts
|
|
747
|
+
var TIME_OF_DAY_NATIVE_TYPE = /^time(tz)?(?:\(\d+\))?$|^time(?:\(\d+\))?with(out)?timezone$/;
|
|
748
|
+
function timeOfDayType(params) {
|
|
749
|
+
if (params.nativeType === undefined)
|
|
750
|
+
return;
|
|
751
|
+
const match = TIME_OF_DAY_NATIVE_TYPE.exec(params.nativeType.toLowerCase().replace(/\s+/g, ""));
|
|
752
|
+
if (match === null)
|
|
753
|
+
return;
|
|
754
|
+
if (match[1] !== undefined)
|
|
755
|
+
return "timetz";
|
|
756
|
+
if (match[0].endsWith("withtimezone") && match[2] === undefined)
|
|
757
|
+
return "timetz";
|
|
758
|
+
return "time";
|
|
759
|
+
}
|
|
652
760
|
// packages/schema/src/prisma-literal.ts
|
|
653
761
|
var STRING_ESCAPES = {
|
|
654
762
|
'"': '"',
|
|
@@ -785,6 +893,7 @@ var RESERVED_CLIENT_NAMES = [
|
|
|
785
893
|
"$disconnect",
|
|
786
894
|
"$transaction",
|
|
787
895
|
"$querySql",
|
|
896
|
+
"$querySqlResult",
|
|
788
897
|
"$executeSql",
|
|
789
898
|
"$queryRaw",
|
|
790
899
|
"$queryRawUnsafe",
|
|
@@ -1079,6 +1188,80 @@ function scalarListDefault(params) {
|
|
|
1079
1188
|
});
|
|
1080
1189
|
}
|
|
1081
1190
|
|
|
1191
|
+
// packages/schema/src/json-schema-ref.ts
|
|
1192
|
+
var EXPORT_NAME = /^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u;
|
|
1193
|
+
var REFERENCE_ANNOTATION = /@(schema|type)\(\s*([$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*)\s+from\s+("(?:[^"\\\r\n]|\\.)*")\s*\)/gu;
|
|
1194
|
+
function annotations(params) {
|
|
1195
|
+
const matches = [...params.documentation.matchAll(REFERENCE_ANNOTATION)].filter((match) => match[1] === params.kind);
|
|
1196
|
+
const remaining = params.documentation.replace(REFERENCE_ANNOTATION, "");
|
|
1197
|
+
const malformed = remaining.match(params.kind === "schema" ? /@schema(?=\s*\(|\s|$)/gu : /@type(?=\s*\(|\s|$)/gu) ?? [];
|
|
1198
|
+
return { matches, count: matches.length + malformed.length };
|
|
1199
|
+
}
|
|
1200
|
+
function hasJsonSchemaAnnotation(params) {
|
|
1201
|
+
return annotations({ documentation: params.documentation ?? "", kind: "schema" }).count > 0;
|
|
1202
|
+
}
|
|
1203
|
+
function jsonTypeAnnotationIssue(params) {
|
|
1204
|
+
const refs = annotations({ documentation: params.documentation ?? "", kind: "type" });
|
|
1205
|
+
if (refs.count === 0)
|
|
1206
|
+
return;
|
|
1207
|
+
if (refs.count > 1)
|
|
1208
|
+
return "A field may declare only one assertion-only JSON type";
|
|
1209
|
+
const match = refs.matches[0];
|
|
1210
|
+
if (match === undefined)
|
|
1211
|
+
return 'Expected @type(Name from "specifier")';
|
|
1212
|
+
return jsonSchemaRefIssue({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
|
|
1213
|
+
}
|
|
1214
|
+
function jsonTypeAnnotationRef(params) {
|
|
1215
|
+
const issue = jsonTypeAnnotationIssue(params);
|
|
1216
|
+
if (issue !== undefined)
|
|
1217
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-json-type-ref", message: issue }] } });
|
|
1218
|
+
const match = annotations({ documentation: params.documentation ?? "", kind: "type" }).matches[0];
|
|
1219
|
+
return match === undefined ? undefined : assertJsonSchemaRef({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
|
|
1220
|
+
}
|
|
1221
|
+
function jsonSchemaRefIssue(params) {
|
|
1222
|
+
const { value } = params;
|
|
1223
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1224
|
+
return "JSON schema reference must be { name, from }; validators belong in the referenced export";
|
|
1225
|
+
const keys = Reflect.ownKeys(value);
|
|
1226
|
+
if (keys.length !== 2 || !keys.includes("name") || !keys.includes("from"))
|
|
1227
|
+
return "JSON schema reference accepts only name and from";
|
|
1228
|
+
const name = Object.getOwnPropertyDescriptor(value, "name");
|
|
1229
|
+
const from = Object.getOwnPropertyDescriptor(value, "from");
|
|
1230
|
+
if (name?.enumerable !== true || !("value" in name) || typeof name.value !== "string" || !EXPORT_NAME.test(name.value) || name.value === "default")
|
|
1231
|
+
return "JSON schema name must be a valid named export, not default";
|
|
1232
|
+
if (from?.enumerable !== true || !("value" in from) || typeof from.value !== "string" || from.value.trim().length === 0 || /[\u0000-\u001f\u007f]/u.test(from.value))
|
|
1233
|
+
return "JSON schema from must be a non-empty import specifier without control characters";
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
function assertJsonSchemaRef(params) {
|
|
1237
|
+
const issue = jsonSchemaRefIssue(params);
|
|
1238
|
+
if (issue !== undefined)
|
|
1239
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-json-schema-ref", message: issue }] } });
|
|
1240
|
+
const value = params.value;
|
|
1241
|
+
return Object.freeze({ name: value.name, from: value.from });
|
|
1242
|
+
}
|
|
1243
|
+
function normalizeJsonSchemaRef(params) {
|
|
1244
|
+
const { documentation, reference } = params;
|
|
1245
|
+
const jsonType = jsonTypeAnnotationRef({ documentation });
|
|
1246
|
+
const refs = annotations({ documentation: documentation ?? "", kind: "schema" });
|
|
1247
|
+
let jsonSchema = reference === undefined ? undefined : assertJsonSchemaRef({ value: reference });
|
|
1248
|
+
let remaining = documentation;
|
|
1249
|
+
if (refs.count > 1 || refs.count > 0 && jsonSchema !== undefined)
|
|
1250
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare only one JSON schema reference", meta: { issues: [{ rule: "duplicate-json-schema-ref" }] } });
|
|
1251
|
+
if (refs.count === 1) {
|
|
1252
|
+
const match = refs.matches[0];
|
|
1253
|
+
if (match === undefined)
|
|
1254
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: 'Expected @schema(Name from "specifier")', meta: { issues: [{ rule: "invalid-json-schema-ref" }] } });
|
|
1255
|
+
jsonSchema = assertJsonSchemaRef({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
|
|
1256
|
+
remaining = documentation?.replace(match[0], "").trim();
|
|
1257
|
+
if (remaining === "")
|
|
1258
|
+
remaining = undefined;
|
|
1259
|
+
}
|
|
1260
|
+
if (jsonSchema !== undefined && jsonType !== undefined)
|
|
1261
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "JSON schema validation and assertion-only @type cannot be combined on one field", meta: { issues: [{ rule: "conflicting-json-schema-ref" }] } });
|
|
1262
|
+
return { ...remaining === undefined ? {} : { documentation: remaining }, ...jsonSchema === undefined ? {} : { jsonSchema } };
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1082
1265
|
// packages/schema/src/m2m.ts
|
|
1083
1266
|
function implicitJoinTable(params) {
|
|
1084
1267
|
const sorted = [params.modelA, params.modelB].sort();
|
|
@@ -1104,6 +1287,8 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1104
1287
|
"SqlColumn",
|
|
1105
1288
|
"SqlFragment",
|
|
1106
1289
|
"ResultDecoder",
|
|
1290
|
+
"ResultReadDecoder",
|
|
1291
|
+
"SqlQueryResult",
|
|
1107
1292
|
"ExtendedDateTime",
|
|
1108
1293
|
"enumValidators",
|
|
1109
1294
|
"enumValidator",
|
|
@@ -1113,6 +1298,11 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1113
1298
|
"UncertainKeys",
|
|
1114
1299
|
"HasSelectedField",
|
|
1115
1300
|
"Provided",
|
|
1301
|
+
"SelectionMember",
|
|
1302
|
+
"SelectionValue",
|
|
1303
|
+
"BagKeys",
|
|
1304
|
+
"IncludedRelation",
|
|
1305
|
+
"IncludedCount",
|
|
1116
1306
|
"ViewDelegateMap",
|
|
1117
1307
|
"extensionManifest",
|
|
1118
1308
|
"extensionMounts",
|
|
@@ -1155,6 +1345,9 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1155
1345
|
"InputJsonValue",
|
|
1156
1346
|
"InputJsonArray",
|
|
1157
1347
|
"InputJsonObject",
|
|
1348
|
+
"BoundJsonFilter",
|
|
1349
|
+
"BoundJsonFilterCommon",
|
|
1350
|
+
"BoundJsonNullableFilter",
|
|
1158
1351
|
"VibeTelemetryOptions",
|
|
1159
1352
|
"VibeTelemetryExporter",
|
|
1160
1353
|
"VibeTelemetryOperationEvent",
|
|
@@ -1226,6 +1419,18 @@ var GENERATED_TYPE_NAMES = {
|
|
|
1226
1419
|
"SumAggregateInput",
|
|
1227
1420
|
"MinAggregateInput",
|
|
1228
1421
|
"MaxAggregateInput",
|
|
1422
|
+
"CountAggregateHavingInput",
|
|
1423
|
+
"AvgAggregateHavingInput",
|
|
1424
|
+
"SumAggregateHavingInput",
|
|
1425
|
+
"MinAggregateHavingInput",
|
|
1426
|
+
"MaxAggregateHavingInput",
|
|
1427
|
+
"CountAggregateOrderByInput",
|
|
1428
|
+
"AvgAggregateOrderByInput",
|
|
1429
|
+
"SumAggregateOrderByInput",
|
|
1430
|
+
"MinAggregateOrderByInput",
|
|
1431
|
+
"MaxAggregateOrderByInput",
|
|
1432
|
+
"HavingInput",
|
|
1433
|
+
"GroupByOrderByInput",
|
|
1229
1434
|
"FindManyArgs",
|
|
1230
1435
|
"FindPageArgs",
|
|
1231
1436
|
"FindFirstArgs",
|
|
@@ -1254,6 +1459,7 @@ var GENERATED_TYPE_NAMES = {
|
|
|
1254
1459
|
"AggregateSelection",
|
|
1255
1460
|
"CountOutput",
|
|
1256
1461
|
"RelationPayload",
|
|
1462
|
+
"ResolvedSelection",
|
|
1257
1463
|
"IncludePayload",
|
|
1258
1464
|
"GetPayload",
|
|
1259
1465
|
"AggregateResult",
|
|
@@ -1395,6 +1601,57 @@ function validateGeneratedNames(params) {
|
|
|
1395
1601
|
register({ name: `${declared.name}${suffix}`, owner: `enum "${declared.name}"` });
|
|
1396
1602
|
}
|
|
1397
1603
|
}
|
|
1604
|
+
function foreignKeyKeys(params) {
|
|
1605
|
+
const distinct = new Set;
|
|
1606
|
+
const repeated = [];
|
|
1607
|
+
for (const fields of params.keys) {
|
|
1608
|
+
if (new Set(fields).size === fields.length)
|
|
1609
|
+
distinct.add(JSON.stringify([...fields].sort()));
|
|
1610
|
+
else
|
|
1611
|
+
repeated.push(fields);
|
|
1612
|
+
}
|
|
1613
|
+
return { distinct, repeated };
|
|
1614
|
+
}
|
|
1615
|
+
function matchesForeignKey(params) {
|
|
1616
|
+
if (params.keys.distinct.has(JSON.stringify([...params.references].sort())))
|
|
1617
|
+
return true;
|
|
1618
|
+
return params.keys.repeated.some((fields) => fields.length === params.references.length && fields.every((name) => params.references.includes(name)));
|
|
1619
|
+
}
|
|
1620
|
+
function referencesPartialOnly(params) {
|
|
1621
|
+
const { target, references, schema, lookups } = params;
|
|
1622
|
+
let partial = lookups.partialKeys.get(target);
|
|
1623
|
+
if (partial === undefined) {
|
|
1624
|
+
partial = foreignKeyKeys({ keys: target.indexes.filter((index) => index.kind === "unique" && (index.where !== undefined || index.wherePredicate !== undefined)).map((index) => index.fields.map((field) => field.name)) });
|
|
1625
|
+
lookups.partialKeys.set(target, partial);
|
|
1626
|
+
}
|
|
1627
|
+
if (partial.distinct.size === 0 && partial.repeated.length === 0)
|
|
1628
|
+
return false;
|
|
1629
|
+
if (!matchesForeignKey({ keys: partial, references }))
|
|
1630
|
+
return false;
|
|
1631
|
+
let total = lookups.totalKeys.get(target);
|
|
1632
|
+
if (total === undefined) {
|
|
1633
|
+
total = foreignKeyKeys({ keys: [
|
|
1634
|
+
...target.fields.filter((field) => field.isId || field.isUnique).map((field) => [field.name]),
|
|
1635
|
+
...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
|
|
1636
|
+
...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined && index.wherePredicate === undefined).map((index) => index.fields.map((field) => field.name))
|
|
1637
|
+
] });
|
|
1638
|
+
lookups.totalKeys.set(target, total);
|
|
1639
|
+
}
|
|
1640
|
+
if (matchesForeignKey({ keys: total, references }))
|
|
1641
|
+
return false;
|
|
1642
|
+
if (lookups.independentTables === undefined) {
|
|
1643
|
+
const tables = new Set;
|
|
1644
|
+
for (const object of schema.migrationObjects ?? []) {
|
|
1645
|
+
const identity = object.identity;
|
|
1646
|
+
if (identity.kind === "index" || identity.kind === "constraint" && (identity.constraintType === "unique" || identity.constraintType === "primaryKey")) {
|
|
1647
|
+
tables.add(JSON.stringify([identity.table.namespace, identity.table.name]));
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
lookups.independentTables = tables;
|
|
1651
|
+
}
|
|
1652
|
+
const placement = qualifiedSqlName({ name: target.dbName ?? target.name, namespace: target.namespace });
|
|
1653
|
+
return !lookups.independentTables.has(JSON.stringify([placement.namespace, placement.name]));
|
|
1654
|
+
}
|
|
1398
1655
|
function validateSchemaIR(params) {
|
|
1399
1656
|
const { schema } = params;
|
|
1400
1657
|
const issues = [];
|
|
@@ -1465,8 +1722,18 @@ function validateSchemaIR(params) {
|
|
|
1465
1722
|
});
|
|
1466
1723
|
}
|
|
1467
1724
|
}
|
|
1725
|
+
const fieldsByModel = new Map;
|
|
1468
1726
|
for (const model of schema.models) {
|
|
1469
|
-
|
|
1727
|
+
const fields = new Map;
|
|
1728
|
+
for (const field of model.fields)
|
|
1729
|
+
if (!fields.has(field.name))
|
|
1730
|
+
fields.set(field.name, field);
|
|
1731
|
+
fieldsByModel.set(model, fields);
|
|
1732
|
+
}
|
|
1733
|
+
const lookups = { fieldsByModel, partialKeys: new Map, totalKeys: new Map };
|
|
1734
|
+
const postgres = providerToDialect({ provider: schema.datasource.provider }) === "postgres";
|
|
1735
|
+
for (const model of schema.models) {
|
|
1736
|
+
validateModel({ model, modelByName, enumByName, issues, schema, lookups, postgres });
|
|
1470
1737
|
}
|
|
1471
1738
|
for (const model of schema.models) {
|
|
1472
1739
|
for (const relation of model.relations) {
|
|
@@ -1511,19 +1778,11 @@ function validateSchemaIR(params) {
|
|
|
1511
1778
|
}
|
|
1512
1779
|
}
|
|
1513
1780
|
for (const model of schema.models) {
|
|
1514
|
-
const fieldNames =
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
defaultKeys.add(model.primaryKey.fields.join("_"));
|
|
1518
|
-
}
|
|
1519
|
-
for (const index of model.indexes) {
|
|
1520
|
-
if (index.kind === "unique" && index.where === undefined && index.fields.length > 1 && index.clientName === undefined) {
|
|
1521
|
-
defaultKeys.add(index.fields.map((field) => field.name).join("_"));
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
const seenSelectorNames = new Set;
|
|
1781
|
+
const fieldNames = fieldsByModel.get(model);
|
|
1782
|
+
const seenSelectorNames = new Map;
|
|
1783
|
+
const addressable = (names) => names.every((name) => fieldNames.get(name)?.isUnsupported !== true);
|
|
1525
1784
|
const checkSelectorName = (params) => {
|
|
1526
|
-
const { clientName,
|
|
1785
|
+
const { clientName, fields, where, isUnique, site } = params;
|
|
1527
1786
|
if (!isUnique) {
|
|
1528
1787
|
issues.push({
|
|
1529
1788
|
rule: "client-name-non-unique",
|
|
@@ -1540,7 +1799,7 @@ function validateSchemaIR(params) {
|
|
|
1540
1799
|
});
|
|
1541
1800
|
return;
|
|
1542
1801
|
}
|
|
1543
|
-
if (
|
|
1802
|
+
if (fields.length < 2) {
|
|
1544
1803
|
issues.push({
|
|
1545
1804
|
rule: "client-name-single-field",
|
|
1546
1805
|
message: `name: ("${clientName}") on a single-field ${site} of "${model.name}" \u2014 the field itself is the selector; name: applies to compound groups only`,
|
|
@@ -1548,7 +1807,9 @@ function validateSchemaIR(params) {
|
|
|
1548
1807
|
});
|
|
1549
1808
|
return;
|
|
1550
1809
|
}
|
|
1551
|
-
|
|
1810
|
+
const tuple = JSON.stringify(fields);
|
|
1811
|
+
const previous = seenSelectorNames.get(clientName);
|
|
1812
|
+
if (fieldNames.has(clientName) || previous !== undefined && previous !== tuple) {
|
|
1552
1813
|
issues.push({
|
|
1553
1814
|
rule: "client-name-collision",
|
|
1554
1815
|
message: `name: ("${clientName}") on "${model.name}" collides with a field or another unique selector \u2014 WhereUniqueInput keys must be unambiguous`,
|
|
@@ -1556,22 +1817,22 @@ function validateSchemaIR(params) {
|
|
|
1556
1817
|
});
|
|
1557
1818
|
return;
|
|
1558
1819
|
}
|
|
1559
|
-
seenSelectorNames.
|
|
1820
|
+
seenSelectorNames.set(clientName, tuple);
|
|
1560
1821
|
};
|
|
1561
|
-
if (model.primaryKey !== undefined && model.primaryKey.clientName !== undefined) {
|
|
1822
|
+
if (model.primaryKey !== undefined && (model.primaryKey.clientName !== undefined || model.primaryKey.fields.length > 1 && addressable(model.primaryKey.fields))) {
|
|
1562
1823
|
checkSelectorName({
|
|
1563
|
-
clientName: model.primaryKey.clientName,
|
|
1564
|
-
|
|
1824
|
+
clientName: model.primaryKey.clientName ?? model.primaryKey.fields.join("_"),
|
|
1825
|
+
fields: model.primaryKey.fields,
|
|
1565
1826
|
isUnique: true,
|
|
1566
1827
|
site: "@@id"
|
|
1567
1828
|
});
|
|
1568
1829
|
}
|
|
1569
1830
|
for (const index of model.indexes) {
|
|
1570
|
-
if (index.clientName === undefined)
|
|
1831
|
+
if (index.clientName === undefined && (index.kind !== "unique" || index.where !== undefined || index.fields.length < 2 || !addressable(index.fields.map((field) => field.name))))
|
|
1571
1832
|
continue;
|
|
1572
1833
|
checkSelectorName({
|
|
1573
|
-
clientName: index.clientName,
|
|
1574
|
-
|
|
1834
|
+
clientName: index.clientName ?? index.fields.map((field) => field.name).join("_"),
|
|
1835
|
+
fields: index.fields.map((field) => field.name),
|
|
1575
1836
|
...index.where === undefined ? {} : { where: index.where },
|
|
1576
1837
|
isUnique: index.kind === "unique",
|
|
1577
1838
|
site: "@@unique"
|
|
@@ -1579,13 +1840,13 @@ function validateSchemaIR(params) {
|
|
|
1579
1840
|
}
|
|
1580
1841
|
}
|
|
1581
1842
|
validateIdentifierLengths({ schema, issues });
|
|
1582
|
-
validatePhysicalNameUniqueness({ schema, issues });
|
|
1843
|
+
validatePhysicalNameUniqueness({ schema, issues, modelByName, fieldsByModel });
|
|
1583
1844
|
validateGeneratedNames({ schema, issues });
|
|
1584
1845
|
validateNativeRls({ schema, issues });
|
|
1585
1846
|
return issues;
|
|
1586
1847
|
}
|
|
1587
1848
|
function validatePhysicalNameUniqueness(params) {
|
|
1588
|
-
const { schema, issues } = params;
|
|
1849
|
+
const { schema, issues, modelByName, fieldsByModel } = params;
|
|
1589
1850
|
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
1590
1851
|
const owners = new Map;
|
|
1591
1852
|
const claim = (entry) => {
|
|
@@ -1615,7 +1876,8 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1615
1876
|
}
|
|
1616
1877
|
if (model.isView)
|
|
1617
1878
|
continue;
|
|
1618
|
-
const
|
|
1879
|
+
const fields = fieldsByModel.get(model);
|
|
1880
|
+
const columnOf = (name) => fields.get(name)?.dbName ?? name;
|
|
1619
1881
|
const seenIndexKeys = new Set;
|
|
1620
1882
|
const declarations = [
|
|
1621
1883
|
...model.indexes.map((index) => ({ index, site: index.kind === "unique" ? "@@unique" : "@@index" })),
|
|
@@ -1623,8 +1885,8 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1623
1885
|
];
|
|
1624
1886
|
for (const { index, site } of declarations) {
|
|
1625
1887
|
const unique = index.kind === "unique";
|
|
1626
|
-
const columns = index.fields.map((entry) =>
|
|
1627
|
-
const key =
|
|
1888
|
+
const columns = index.fields.map((entry) => [columnOf(entry.name), entry.sort === "desc", entry.operatorClass === undefined ? null : [entry.operatorClass.namespace, entry.operatorClass.name]]);
|
|
1889
|
+
const key = JSON.stringify([unique, index.nullsNotDistinct === true, unique ? "btree" : index.kind, columns, index.where?.trim() ?? ""]);
|
|
1628
1890
|
if (seenIndexKeys.has(key))
|
|
1629
1891
|
continue;
|
|
1630
1892
|
seenIndexKeys.add(key);
|
|
@@ -1642,7 +1904,7 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1642
1904
|
for (const relation of model.relations) {
|
|
1643
1905
|
if (relation.fields === undefined || relation.fields.length === 0)
|
|
1644
1906
|
continue;
|
|
1645
|
-
const target =
|
|
1907
|
+
const target = modelByName.get(relation.targetModel);
|
|
1646
1908
|
if (target !== undefined && target.isView)
|
|
1647
1909
|
continue;
|
|
1648
1910
|
const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
|
|
@@ -1671,7 +1933,7 @@ function validatePhysicalNameUniqueness(params) {
|
|
|
1671
1933
|
continue;
|
|
1672
1934
|
if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
|
|
1673
1935
|
continue;
|
|
1674
|
-
const target =
|
|
1936
|
+
const target = modelByName.get(relation.targetModel);
|
|
1675
1937
|
const physicalNamespace = model.namespace ?? "public";
|
|
1676
1938
|
if ((target?.namespace ?? "public") !== physicalNamespace) {
|
|
1677
1939
|
issues.push({ rule: "implicit-join-namespace", message: "Implicit many-to-many endpoints must share one fixed namespace", model: model.name, field: relation.name });
|
|
@@ -1770,7 +2032,7 @@ function validateIdentifierLengths(params) {
|
|
|
1770
2032
|
}
|
|
1771
2033
|
}
|
|
1772
2034
|
function validateModel(params) {
|
|
1773
|
-
const { model, modelByName, enumByName, issues } = params;
|
|
2035
|
+
const { model, modelByName, enumByName, issues, lookups, postgres } = params;
|
|
1774
2036
|
const memberNames = new Set;
|
|
1775
2037
|
for (const field of model.fields) {
|
|
1776
2038
|
if (memberNames.has(field.name)) {
|
|
@@ -1782,6 +2044,23 @@ function validateModel(params) {
|
|
|
1782
2044
|
});
|
|
1783
2045
|
}
|
|
1784
2046
|
memberNames.add(field.name);
|
|
2047
|
+
const typedIssue = jsonTypeAnnotationIssue({ documentation: field.documentation });
|
|
2048
|
+
if (typedIssue !== undefined)
|
|
2049
|
+
issues.push({ rule: "invalid-json-type-ref", message: typedIssue, model: model.name, field: field.name });
|
|
2050
|
+
const hasType = typedIssue === undefined && jsonTypeAnnotationRef({ documentation: field.documentation }) !== undefined;
|
|
2051
|
+
if (hasType && field.type !== "Json")
|
|
2052
|
+
issues.push({ rule: "json-type-non-json", message: "JSON type assertions are only valid on Json fields", model: model.name, field: field.name });
|
|
2053
|
+
if (field.jsonSchema !== undefined) {
|
|
2054
|
+
const issue = jsonSchemaRefIssue({ value: field.jsonSchema });
|
|
2055
|
+
if (issue !== undefined)
|
|
2056
|
+
issues.push({ rule: "invalid-json-schema-ref", message: issue, model: model.name, field: field.name });
|
|
2057
|
+
if (field.type !== "Json" || field.isUnsupported === true)
|
|
2058
|
+
issues.push({ rule: "json-schema-non-json", message: "JSON schemas are only valid on queryable Json fields", model: model.name, field: field.name });
|
|
2059
|
+
if (hasType)
|
|
2060
|
+
issues.push({ rule: "conflicting-json-schema-ref", message: "A field cannot declare both @type and a JSON schema", model: model.name, field: field.name });
|
|
2061
|
+
}
|
|
2062
|
+
if (hasJsonSchemaAnnotation({ documentation: field.documentation }))
|
|
2063
|
+
issues.push({ rule: "unnormalized-json-schema-ref", message: "Normalize @schema documentation into FieldIR.jsonSchema before consuming SchemaIR", model: model.name, field: field.name });
|
|
1785
2064
|
const typeIdentity = field.nativeTypeIdentity;
|
|
1786
2065
|
if (typeIdentity !== undefined && (typeIdentity === null || typeof typeIdentity !== "object" || field.isUnsupported !== true || typeof typeIdentity.source !== "string" || typeIdentity.source !== field.nativeType || typeof typeIdentity.namespace !== "string" || typeIdentity.namespace.length === 0 || typeIdentity.namespace.includes("\x00"))) {
|
|
1787
2066
|
issues.push({ rule: "invalid-native-type-identity", message: `PostgreSQL type identity on "${model.name}.${field.name}" must be bound to its unsupported declaration`, model: model.name, field: field.name });
|
|
@@ -1872,8 +2151,23 @@ function validateModel(params) {
|
|
|
1872
2151
|
}
|
|
1873
2152
|
memberNames.add(relation.name);
|
|
1874
2153
|
}
|
|
1875
|
-
const fieldNames = new Set(
|
|
1876
|
-
const
|
|
2154
|
+
const fieldNames = new Set(lookups.fieldsByModel.get(model).keys());
|
|
2155
|
+
const idFields = model.fields.filter((f) => f.isId);
|
|
2156
|
+
const hasIdField = idFields.length > 0;
|
|
2157
|
+
if (idFields.length > 1) {
|
|
2158
|
+
issues.push({
|
|
2159
|
+
rule: "multiple-field-ids",
|
|
2160
|
+
message: `Model "${model.name}" has multiple @id fields (${idFields.map((field) => field.name).join(", ")}); declare one compound primary key instead`,
|
|
2161
|
+
model: model.name
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
if (idFields.length === 1 && model.primaryKey !== undefined && (model.primaryKey.fields.length !== 1 || model.primaryKey.fields[0] !== idFields[0]?.name)) {
|
|
2165
|
+
issues.push({
|
|
2166
|
+
rule: "conflicting-primary-key",
|
|
2167
|
+
message: `Model "${model.name}" declares @id on "${idFields[0]?.name}" and a different block primary key; both declarations must name the same single logical field`,
|
|
2168
|
+
model: model.name
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
1877
2171
|
if (!model.isView && !hasIdField && model.primaryKey === undefined) {
|
|
1878
2172
|
issues.push({
|
|
1879
2173
|
rule: "missing-id",
|
|
@@ -1932,8 +2226,14 @@ function validateModel(params) {
|
|
|
1932
2226
|
});
|
|
1933
2227
|
}
|
|
1934
2228
|
}
|
|
2229
|
+
if (postgres && new Set(relation.references).size !== relation.references.length) {
|
|
2230
|
+
issues.push({ rule: "relation-duplicate-column", message: `Relation "${model.name}.${relation.name}" repeats a referenced column`, model: model.name, field: relation.name });
|
|
2231
|
+
}
|
|
1935
2232
|
if (target !== undefined) {
|
|
1936
|
-
|
|
2233
|
+
if (postgres && referencesPartialOnly({ target, references: relation.references, schema: params.schema, lookups })) {
|
|
2234
|
+
issues.push({ rule: "relation-partial-only-target", message: `Relation "${model.name}.${relation.name}" references only a partial unique key`, model: model.name, field: relation.name });
|
|
2235
|
+
}
|
|
2236
|
+
const targetFieldNames = lookups.fieldsByModel.get(target);
|
|
1937
2237
|
for (const refField of relation.references) {
|
|
1938
2238
|
if (!targetFieldNames.has(refField)) {
|
|
1939
2239
|
issues.push({
|
|
@@ -2004,6 +2304,8 @@ function validateModel(params) {
|
|
|
2004
2304
|
model,
|
|
2005
2305
|
fieldNames,
|
|
2006
2306
|
site: `CHECK "${check.name}"`,
|
|
2307
|
+
usage: "check",
|
|
2308
|
+
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2007
2309
|
issues
|
|
2008
2310
|
});
|
|
2009
2311
|
}
|
|
@@ -2025,13 +2327,55 @@ function validateModel(params) {
|
|
|
2025
2327
|
model,
|
|
2026
2328
|
fieldNames,
|
|
2027
2329
|
site: `partial index on [${index.fields.map((entry) => entry.name).join(", ")}]`,
|
|
2330
|
+
usage: "indexPredicate",
|
|
2331
|
+
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2028
2332
|
issues
|
|
2029
2333
|
});
|
|
2030
2334
|
}
|
|
2031
2335
|
}
|
|
2336
|
+
for (const exclusion of model.exclusions ?? []) {
|
|
2337
|
+
const predicate = exclusion.source?.where;
|
|
2338
|
+
if (predicate !== undefined)
|
|
2339
|
+
validatePredicateFields({ predicate, model, fieldNames, site: `exclusion "${exclusion.name}"`, usage: "exclusionPredicate", dialect: providerToDialect({ provider: params.schema.datasource.provider }), issues });
|
|
2340
|
+
}
|
|
2032
2341
|
}
|
|
2033
2342
|
function validatePredicateFields(params) {
|
|
2034
2343
|
const { predicate, model, fieldNames, site, issues } = params;
|
|
2344
|
+
const inspection = inspectPredicate({ predicate });
|
|
2345
|
+
if (inspection.checkOnly && (params.usage !== "check" || !DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)) {
|
|
2346
|
+
issues.push({ rule: "predicate-check-capability", message: `${site}: computed schema predicates require a PostgreSQL CHECK`, model: model.name });
|
|
2347
|
+
}
|
|
2348
|
+
const expressionType = (expression) => {
|
|
2349
|
+
if (expression.kind === "field")
|
|
2350
|
+
return model.fields.find((field) => field.name === expression.name)?.type;
|
|
2351
|
+
if (expression.kind === "jsonText") {
|
|
2352
|
+
const field = model.fields.find((candidate) => candidate.name === expression.field.name);
|
|
2353
|
+
if (field !== undefined && (field.type !== "Json" || field.isUnsupported === true))
|
|
2354
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: jsonText requires a Json field`, model: model.name, field: field.name });
|
|
2355
|
+
if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00")))
|
|
2356
|
+
issues.push({ rule: "predicate-json-path", message: `${site}: jsonText requires a string-only path without NUL bytes`, model: model.name });
|
|
2357
|
+
return "String";
|
|
2358
|
+
}
|
|
2359
|
+
if (expressionType(expression.expression) !== "String")
|
|
2360
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: length requires a text expression`, model: model.name });
|
|
2361
|
+
return "Int";
|
|
2362
|
+
};
|
|
2363
|
+
const validateComputedComparisons = (node) => {
|
|
2364
|
+
if (node.kind === "compare") {
|
|
2365
|
+
if (node.left.kind !== "field" || node.right.kind !== "value" && node.right.kind !== "field") {
|
|
2366
|
+
const left = expressionType(node.left);
|
|
2367
|
+
const right = node.right.kind === "value" ? undefined : expressionType(node.right);
|
|
2368
|
+
if (left !== undefined && right !== undefined && left !== right)
|
|
2369
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: computed comparison operands must have the same scalar type`, model: model.name });
|
|
2370
|
+
if (node.right.kind === "value" && left === "String" && typeof node.right.value !== "string")
|
|
2371
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: a text expression requires a string literal`, model: model.name });
|
|
2372
|
+
}
|
|
2373
|
+
} else if (node.kind === "and" || node.kind === "or")
|
|
2374
|
+
node.operands.forEach(validateComputedComparisons);
|
|
2375
|
+
else if (node.kind === "not" || node.kind === "isTrue")
|
|
2376
|
+
validateComputedComparisons(node.operand);
|
|
2377
|
+
};
|
|
2378
|
+
validateComputedComparisons(predicate);
|
|
2035
2379
|
for (const name of predicateFieldNames({ predicate })) {
|
|
2036
2380
|
if (!fieldNames.has(name)) {
|
|
2037
2381
|
issues.push({
|
|
@@ -2042,7 +2386,10 @@ function validatePredicateFields(params) {
|
|
|
2042
2386
|
});
|
|
2043
2387
|
continue;
|
|
2044
2388
|
}
|
|
2045
|
-
|
|
2389
|
+
const referenced = model.fields.find((field) => field.name === name);
|
|
2390
|
+
if (inspection.checkOnly && referenced?.isUnsupported === true)
|
|
2391
|
+
issues.push({ rule: "predicate-unsupported-type", message: `${site}: computed predicates cannot reference unsupported fields`, model: model.name, field: name });
|
|
2392
|
+
if (referenced?.isList === true) {
|
|
2046
2393
|
issues.push({
|
|
2047
2394
|
rule: "predicate-list-field",
|
|
2048
2395
|
message: `${site} on "${model.name}" compares scalar list "${name}" \u2014 use a raw expression for array operators`,
|
|
@@ -2069,6 +2416,12 @@ function assertValidSchemaIR(params) {
|
|
|
2069
2416
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Enum reference namespace differs from its declared native type", meta: { rule: "enum-namespace", model: model.name, field: field.name } });
|
|
2070
2417
|
}
|
|
2071
2418
|
for (const model of params.schema.models) {
|
|
2419
|
+
for (const check of model.checks ?? [])
|
|
2420
|
+
if (check.predicate !== undefined)
|
|
2421
|
+
assertPredicateCapabilities({ predicate: check.predicate, dialect, usage: "check" });
|
|
2422
|
+
for (const index of model.indexes)
|
|
2423
|
+
if (index.wherePredicate !== undefined)
|
|
2424
|
+
assertPredicateCapabilities({ predicate: index.wherePredicate, dialect, usage: "indexPredicate" });
|
|
2072
2425
|
if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
|
|
2073
2426
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
|
|
2074
2427
|
if ((model.exclusions?.length ?? 0) === 0)
|
|
@@ -2082,6 +2435,8 @@ function assertValidSchemaIR(params) {
|
|
|
2082
2435
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid exclusion declaration", meta: { rule: "exclusion-definition" } });
|
|
2083
2436
|
const source = exclusion.source;
|
|
2084
2437
|
if (source !== undefined) {
|
|
2438
|
+
if (source.where !== undefined)
|
|
2439
|
+
assertPredicateCapabilities({ predicate: source.where, dialect, usage: "exclusionPredicate" });
|
|
2085
2440
|
const references = [...source.keys.flatMap((key) => ("field" in key) ? [key.field] : key.fields), ...[source.range.lower, source.range.upper].flatMap((endpoint) => ("field" in endpoint) ? [endpoint.field] : []), ...source.where === undefined ? [] : predicateFieldNames({ predicate: source.where })];
|
|
2086
2441
|
if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
|
|
2087
2442
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
|
|
@@ -2101,6 +2456,16 @@ function assertValidSchemaIR(params) {
|
|
|
2101
2456
|
if (dialect === "mysql" && counters.length > 1)
|
|
2102
2457
|
throw unsupportedCapability({ dialect, feature: "multiple auto-increment columns" });
|
|
2103
2458
|
for (const field of model.fields) {
|
|
2459
|
+
const timeOfDay = field.isUnsupported === true || field.type !== "DateTime" ? undefined : timeOfDayType({ nativeType: field.nativeType });
|
|
2460
|
+
if (timeOfDay !== undefined && !DIALECT_CAPABILITIES[dialect].timeOfDayTypes.includes(timeOfDay)) {
|
|
2461
|
+
const instead = dialect === "sqlite" ? "sqlite has no time-of-day column type; store a plain DateTime or a String" : "there is no time-with-offset type here; use @db.Time(n), which stores the UTC time of day";
|
|
2462
|
+
const error = unsupportedCapability({ dialect, feature: "timeOfDayTypes", hint: `${model.name}.${field.name} declares @db.${field.nativeType ?? ""} (${timeOfDay}) \u2014 ${instead}` });
|
|
2463
|
+
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
|
|
2464
|
+
}
|
|
2465
|
+
if (timeOfDay !== undefined && dialect === "mysql" && field.default?.kind === "now") {
|
|
2466
|
+
const error = unsupportedCapability({ dialect, feature: "@default(now()) on a TIME column", hint: `${model.name}.${field.name} \u2014 MySQL has no current-time default for TIME; drop the default and write the value (or use @updatedAt)` });
|
|
2467
|
+
throw new VibeError({ code: error.code, message: error.message, meta: { ...error.meta, model: model.name, field: field.name, nativeType: field.nativeType } });
|
|
2468
|
+
}
|
|
2104
2469
|
if (field.default?.kind === "list") {
|
|
2105
2470
|
if (dialect !== "postgres")
|
|
2106
2471
|
throw unsupportedCapability({ dialect, feature: "scalar-list defaults" });
|
|
@@ -2333,7 +2698,9 @@ export {
|
|
|
2333
2698
|
VibeError,
|
|
2334
2699
|
assertIdentifierFits,
|
|
2335
2700
|
assertIndexOperatorClasses,
|
|
2701
|
+
assertJsonSchemaRef,
|
|
2336
2702
|
assertMigrationObjects,
|
|
2703
|
+
assertPredicateCapabilities,
|
|
2337
2704
|
assertSqlNamespace,
|
|
2338
2705
|
assertValidSchemaIR,
|
|
2339
2706
|
buildComputedManifest,
|
|
@@ -2352,9 +2719,11 @@ export {
|
|
|
2352
2719
|
extensionOptionsHash,
|
|
2353
2720
|
fieldTypeIdentity,
|
|
2354
2721
|
fitDefaultName,
|
|
2722
|
+
hasJsonSchemaAnnotation,
|
|
2355
2723
|
identifierLength,
|
|
2356
2724
|
implicitJoinTable,
|
|
2357
2725
|
implicitJoinTableObjectNames,
|
|
2726
|
+
inspectPredicate,
|
|
2358
2727
|
internalError,
|
|
2359
2728
|
invalidStoredData,
|
|
2360
2729
|
isEnumType,
|
|
@@ -2364,7 +2733,11 @@ export {
|
|
|
2364
2733
|
isReservedDelegateName,
|
|
2365
2734
|
isSqlObjectDefinition,
|
|
2366
2735
|
isSqlObjectIdentity,
|
|
2736
|
+
jsonSchemaRefIssue,
|
|
2737
|
+
jsonTypeAnnotationIssue,
|
|
2738
|
+
jsonTypeAnnotationRef,
|
|
2367
2739
|
mergeExtensionDeclarations,
|
|
2740
|
+
normalizeJsonSchemaRef,
|
|
2368
2741
|
postgresGeneratedObjectName,
|
|
2369
2742
|
predicateFieldNames,
|
|
2370
2743
|
predicateKey,
|
|
@@ -2376,10 +2749,11 @@ export {
|
|
|
2376
2749
|
sqlNamespaceIssue,
|
|
2377
2750
|
sqlObjectKey,
|
|
2378
2751
|
sqlObjectNamespace,
|
|
2752
|
+
timeOfDayType,
|
|
2379
2753
|
unsupportedCapability,
|
|
2380
2754
|
validateComputedConfig,
|
|
2381
2755
|
validateRlsContextIR,
|
|
2382
2756
|
validateSchemaIR
|
|
2383
2757
|
};
|
|
2384
2758
|
|
|
2385
|
-
//# debugId=
|
|
2759
|
+
//# debugId=6D288D6F1DD93E5864756E2164756E21
|