@vibeorm/schema 3.0.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/dist/capabilities.d.ts +2 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +259 -9
- package/dist/index.js.map +9 -8
- package/dist/ir.d.ts +7 -0
- package/dist/json-schema-ref.d.ts +30 -0
- package/dist/predicate.d.ts +29 -3
- package/package.json +1 -1
package/dist/capabilities.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export type DialectCapabilities = {
|
|
|
51
51
|
readonly exclusionConstraints: boolean;
|
|
52
52
|
readonly exclusionExtensions: readonly string[];
|
|
53
53
|
readonly checkNormalization: "rollback" | "temporary" | "sourceText";
|
|
54
|
+
/** Typed JSON text, text length and fail-closed boolean expressions in CHECKs. */
|
|
55
|
+
readonly schemaCheckExpressions: boolean;
|
|
54
56
|
/** Native database row policies; PGlite additionally needs a verified restricted role. */
|
|
55
57
|
readonly nativeRowLevelSecurity: boolean;
|
|
56
58
|
/** INSERT/UPDATE/DELETE ... RETURNING. False ⇒ insertId + re-select fallback. */
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export type { DatabaseModuleOptions, DatabaseModuleManifest, DatabaseModuleArtif
|
|
|
8
8
|
export { prepareDatabaseModuleSchema } from "./module-schema.ts";
|
|
9
9
|
export type { ExclusionIR, ExclusionSpec, ExclusionKey, ExclusionEndpoint } from "./exclusion.ts";
|
|
10
10
|
export { DIALECTS, ENUM_STORAGES, enumStorageOf, fieldTypeIdentity, isEnumType, providerToDialect } from "./ir.ts";
|
|
11
|
-
export type { CascadeAction, DatasourceIR, DefaultValueIR, Dialect, EnumIR, EnumStorage, ExtensionDeclIR, FieldIR, FieldType, IndexFieldIR, CheckIR, IndexIR, IndexKind, ModelIR, Provider, RelationIR, RelationType, ScalarType, SchemaIR, } from "./ir.ts";
|
|
12
|
-
export { PREDICATE_COMPARISON_OPS, isPredicateFieldRef, predicateFieldNames, predicateKey } from "./predicate.ts";
|
|
13
|
-
export type { PredicateComparisonOp, PredicateFieldRef, PredicateIR, PredicateOperand, PredicateValue } from "./predicate.ts";
|
|
11
|
+
export type { CascadeAction, DatasourceIR, DefaultValueIR, Dialect, EnumIR, EnumStorage, ExtensionDeclIR, FieldIR, FieldType, JsonSchemaRefIR, IndexFieldIR, CheckIR, IndexIR, IndexKind, ModelIR, Provider, RelationIR, RelationType, ScalarType, SchemaIR, } from "./ir.ts";
|
|
12
|
+
export { PREDICATE_COMPARISON_OPS, assertPredicateCapabilities, inspectPredicate, isPredicateFieldRef, predicateFieldNames, predicateKey } from "./predicate.ts";
|
|
13
|
+
export type { PredicateComparisonOp, PredicateExpression, PredicateFieldRef, PredicateIR, PredicateOperand, PredicateValue } from "./predicate.ts";
|
|
14
14
|
export { assertMigrationObjects, isSqlObjectDefinition, isSqlObjectIdentity, sqlObjectKey, sqlObjectNamespace } from "./migration-objects.ts";
|
|
15
15
|
export type { MigrationObjectIR, QualifiedSqlName, SqlObjectDefinition, SqlObjectDefinitionFormat, SqlObjectFacts, SqlObjectIdentity } from "./migration-objects.ts";
|
|
16
16
|
export type { AdvisoryLockMode, DialectCapabilities, FullTextEngine, RowLockStrength, RowLockWaitPolicy, TimeOfDayType, } from "./capabilities.ts";
|
|
@@ -35,4 +35,5 @@ export { RLS_CONTEXT_REASONS, RlsContextError } from "./errors.ts";
|
|
|
35
35
|
export type { RlsContextReason } from "./errors.ts";
|
|
36
36
|
export { isImplicitManyToManyRelation, validateRlsContextIR } from "./validate.ts";
|
|
37
37
|
export { scalarListDefault, serializeJsonDefault } from "./default-values.ts";
|
|
38
|
+
export { assertJsonSchemaRef, hasJsonSchemaAnnotation, jsonSchemaRefIssue, jsonTypeAnnotationIssue, jsonTypeAnnotationRef, normalizeJsonSchemaRef } from "./json-schema-ref.ts";
|
|
38
39
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -260,6 +260,7 @@ var POSTGRES_CAPABILITIES = Object.freeze({
|
|
|
260
260
|
exclusionConstraints: true,
|
|
261
261
|
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
262
262
|
checkNormalization: "rollback",
|
|
263
|
+
schemaCheckExpressions: true,
|
|
263
264
|
nativeRowLevelSecurity: true,
|
|
264
265
|
returning: true,
|
|
265
266
|
scalarArrays: true,
|
|
@@ -286,6 +287,7 @@ var SQLITE_CAPABILITIES = Object.freeze({
|
|
|
286
287
|
exclusionConstraints: false,
|
|
287
288
|
exclusionExtensions: Object.freeze([]),
|
|
288
289
|
checkNormalization: "sourceText",
|
|
290
|
+
schemaCheckExpressions: false,
|
|
289
291
|
nativeRowLevelSecurity: false,
|
|
290
292
|
returning: true,
|
|
291
293
|
scalarArrays: false,
|
|
@@ -312,6 +314,7 @@ var MYSQL_CAPABILITIES = Object.freeze({
|
|
|
312
314
|
exclusionConstraints: false,
|
|
313
315
|
exclusionExtensions: Object.freeze([]),
|
|
314
316
|
checkNormalization: "temporary",
|
|
317
|
+
schemaCheckExpressions: false,
|
|
315
318
|
nativeRowLevelSecurity: false,
|
|
316
319
|
returning: false,
|
|
317
320
|
scalarArrays: false,
|
|
@@ -610,36 +613,121 @@ function isPredicateFieldRef(operand) {
|
|
|
610
613
|
return operand.kind === "field";
|
|
611
614
|
}
|
|
612
615
|
function predicateFieldNames(params) {
|
|
613
|
-
|
|
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
|
+
};
|
|
614
644
|
const walk = (node) => {
|
|
615
645
|
switch (node.kind) {
|
|
616
646
|
case "compare":
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
|
|
647
|
+
expression(node.left);
|
|
648
|
+
if (typeof node.right !== "object" || node.right === null || node.right.kind !== "value")
|
|
649
|
+
expression(node.right);
|
|
620
650
|
return;
|
|
621
651
|
case "inList":
|
|
622
652
|
case "isNull":
|
|
623
|
-
|
|
653
|
+
expression(node.field);
|
|
624
654
|
return;
|
|
625
655
|
case "and":
|
|
626
656
|
case "or":
|
|
627
657
|
for (const operand of node.operands)
|
|
628
658
|
walk(operand);
|
|
629
659
|
return;
|
|
660
|
+
case "isTrue":
|
|
661
|
+
checkOnly = true;
|
|
662
|
+
walk(node.operand);
|
|
663
|
+
return;
|
|
630
664
|
case "not":
|
|
631
665
|
walk(node.operand);
|
|
632
666
|
return;
|
|
633
667
|
}
|
|
634
668
|
};
|
|
635
669
|
walk(params.predicate);
|
|
636
|
-
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" });
|
|
637
686
|
}
|
|
638
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
|
+
};
|
|
639
727
|
const encode = (node) => {
|
|
640
728
|
switch (node.kind) {
|
|
641
729
|
case "compare":
|
|
642
|
-
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)})`;
|
|
643
731
|
case "inList":
|
|
644
732
|
return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
|
|
645
733
|
case "isNull":
|
|
@@ -649,6 +737,8 @@ function predicateKey(params) {
|
|
|
649
737
|
return `${node.kind}(${node.operands.map(encode).join(",")})`;
|
|
650
738
|
case "not":
|
|
651
739
|
return `not(${encode(node.operand)})`;
|
|
740
|
+
case "isTrue":
|
|
741
|
+
return `isTrue(${encode(node.operand)})`;
|
|
652
742
|
}
|
|
653
743
|
};
|
|
654
744
|
return encode(params.predicate);
|
|
@@ -803,6 +893,7 @@ var RESERVED_CLIENT_NAMES = [
|
|
|
803
893
|
"$disconnect",
|
|
804
894
|
"$transaction",
|
|
805
895
|
"$querySql",
|
|
896
|
+
"$querySqlResult",
|
|
806
897
|
"$executeSql",
|
|
807
898
|
"$queryRaw",
|
|
808
899
|
"$queryRawUnsafe",
|
|
@@ -1097,6 +1188,80 @@ function scalarListDefault(params) {
|
|
|
1097
1188
|
});
|
|
1098
1189
|
}
|
|
1099
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
|
+
|
|
1100
1265
|
// packages/schema/src/m2m.ts
|
|
1101
1266
|
function implicitJoinTable(params) {
|
|
1102
1267
|
const sorted = [params.modelA, params.modelB].sort();
|
|
@@ -1122,6 +1287,8 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1122
1287
|
"SqlColumn",
|
|
1123
1288
|
"SqlFragment",
|
|
1124
1289
|
"ResultDecoder",
|
|
1290
|
+
"ResultReadDecoder",
|
|
1291
|
+
"SqlQueryResult",
|
|
1125
1292
|
"ExtendedDateTime",
|
|
1126
1293
|
"enumValidators",
|
|
1127
1294
|
"enumValidator",
|
|
@@ -1178,6 +1345,9 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1178
1345
|
"InputJsonValue",
|
|
1179
1346
|
"InputJsonArray",
|
|
1180
1347
|
"InputJsonObject",
|
|
1348
|
+
"BoundJsonFilter",
|
|
1349
|
+
"BoundJsonFilterCommon",
|
|
1350
|
+
"BoundJsonNullableFilter",
|
|
1181
1351
|
"VibeTelemetryOptions",
|
|
1182
1352
|
"VibeTelemetryExporter",
|
|
1183
1353
|
"VibeTelemetryOperationEvent",
|
|
@@ -1874,6 +2044,23 @@ function validateModel(params) {
|
|
|
1874
2044
|
});
|
|
1875
2045
|
}
|
|
1876
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 });
|
|
1877
2064
|
const typeIdentity = field.nativeTypeIdentity;
|
|
1878
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"))) {
|
|
1879
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 });
|
|
@@ -2117,6 +2304,8 @@ function validateModel(params) {
|
|
|
2117
2304
|
model,
|
|
2118
2305
|
fieldNames,
|
|
2119
2306
|
site: `CHECK "${check.name}"`,
|
|
2307
|
+
usage: "check",
|
|
2308
|
+
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2120
2309
|
issues
|
|
2121
2310
|
});
|
|
2122
2311
|
}
|
|
@@ -2138,13 +2327,55 @@ function validateModel(params) {
|
|
|
2138
2327
|
model,
|
|
2139
2328
|
fieldNames,
|
|
2140
2329
|
site: `partial index on [${index.fields.map((entry) => entry.name).join(", ")}]`,
|
|
2330
|
+
usage: "indexPredicate",
|
|
2331
|
+
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2141
2332
|
issues
|
|
2142
2333
|
});
|
|
2143
2334
|
}
|
|
2144
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
|
+
}
|
|
2145
2341
|
}
|
|
2146
2342
|
function validatePredicateFields(params) {
|
|
2147
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);
|
|
2148
2379
|
for (const name of predicateFieldNames({ predicate })) {
|
|
2149
2380
|
if (!fieldNames.has(name)) {
|
|
2150
2381
|
issues.push({
|
|
@@ -2155,7 +2386,10 @@ function validatePredicateFields(params) {
|
|
|
2155
2386
|
});
|
|
2156
2387
|
continue;
|
|
2157
2388
|
}
|
|
2158
|
-
|
|
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) {
|
|
2159
2393
|
issues.push({
|
|
2160
2394
|
rule: "predicate-list-field",
|
|
2161
2395
|
message: `${site} on "${model.name}" compares scalar list "${name}" \u2014 use a raw expression for array operators`,
|
|
@@ -2182,6 +2416,12 @@ function assertValidSchemaIR(params) {
|
|
|
2182
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 } });
|
|
2183
2417
|
}
|
|
2184
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" });
|
|
2185
2425
|
if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
|
|
2186
2426
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
|
|
2187
2427
|
if ((model.exclusions?.length ?? 0) === 0)
|
|
@@ -2195,6 +2435,8 @@ function assertValidSchemaIR(params) {
|
|
|
2195
2435
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid exclusion declaration", meta: { rule: "exclusion-definition" } });
|
|
2196
2436
|
const source = exclusion.source;
|
|
2197
2437
|
if (source !== undefined) {
|
|
2438
|
+
if (source.where !== undefined)
|
|
2439
|
+
assertPredicateCapabilities({ predicate: source.where, dialect, usage: "exclusionPredicate" });
|
|
2198
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 })];
|
|
2199
2441
|
if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
|
|
2200
2442
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
|
|
@@ -2456,7 +2698,9 @@ export {
|
|
|
2456
2698
|
VibeError,
|
|
2457
2699
|
assertIdentifierFits,
|
|
2458
2700
|
assertIndexOperatorClasses,
|
|
2701
|
+
assertJsonSchemaRef,
|
|
2459
2702
|
assertMigrationObjects,
|
|
2703
|
+
assertPredicateCapabilities,
|
|
2460
2704
|
assertSqlNamespace,
|
|
2461
2705
|
assertValidSchemaIR,
|
|
2462
2706
|
buildComputedManifest,
|
|
@@ -2475,9 +2719,11 @@ export {
|
|
|
2475
2719
|
extensionOptionsHash,
|
|
2476
2720
|
fieldTypeIdentity,
|
|
2477
2721
|
fitDefaultName,
|
|
2722
|
+
hasJsonSchemaAnnotation,
|
|
2478
2723
|
identifierLength,
|
|
2479
2724
|
implicitJoinTable,
|
|
2480
2725
|
implicitJoinTableObjectNames,
|
|
2726
|
+
inspectPredicate,
|
|
2481
2727
|
internalError,
|
|
2482
2728
|
invalidStoredData,
|
|
2483
2729
|
isEnumType,
|
|
@@ -2487,7 +2733,11 @@ export {
|
|
|
2487
2733
|
isReservedDelegateName,
|
|
2488
2734
|
isSqlObjectDefinition,
|
|
2489
2735
|
isSqlObjectIdentity,
|
|
2736
|
+
jsonSchemaRefIssue,
|
|
2737
|
+
jsonTypeAnnotationIssue,
|
|
2738
|
+
jsonTypeAnnotationRef,
|
|
2490
2739
|
mergeExtensionDeclarations,
|
|
2740
|
+
normalizeJsonSchemaRef,
|
|
2491
2741
|
postgresGeneratedObjectName,
|
|
2492
2742
|
predicateFieldNames,
|
|
2493
2743
|
predicateKey,
|
|
@@ -2506,4 +2756,4 @@ export {
|
|
|
2506
2756
|
validateSchemaIR
|
|
2507
2757
|
};
|
|
2508
2758
|
|
|
2509
|
-
//# debugId=
|
|
2759
|
+
//# debugId=6D288D6F1DD93E5864756E2164756E21
|