@vibeorm/schema 3.0.0 → 4.1.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/errors.d.ts +8 -0
- package/dist/extension.d.ts +19 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.js +520 -204
- package/dist/index.js.map +13 -10
- package/dist/ir.d.ts +7 -0
- package/dist/json-schema-ref.d.ts +30 -0
- package/dist/model-sql-namespace.d.ts +8 -0
- package/dist/native-rls-admission.d.ts +7 -0
- package/dist/predicate.d.ts +29 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ var VIBE_ERROR_CODES = [
|
|
|
18
18
|
"VIBE_READ_ONLY",
|
|
19
19
|
"VIBE_RLS_CONTEXT",
|
|
20
20
|
"VIBE_PERMISSION_DENIED",
|
|
21
|
+
"VIBE_CANCELLED",
|
|
21
22
|
"VIBE_ADAPTER",
|
|
22
23
|
"VIBE_ADAPTER_CLOSED",
|
|
23
24
|
"VIBE_INTERNAL"
|
|
@@ -260,6 +261,7 @@ var POSTGRES_CAPABILITIES = Object.freeze({
|
|
|
260
261
|
exclusionConstraints: true,
|
|
261
262
|
exclusionExtensions: Object.freeze(["btree_gist"]),
|
|
262
263
|
checkNormalization: "rollback",
|
|
264
|
+
schemaCheckExpressions: true,
|
|
263
265
|
nativeRowLevelSecurity: true,
|
|
264
266
|
returning: true,
|
|
265
267
|
scalarArrays: true,
|
|
@@ -286,6 +288,7 @@ var SQLITE_CAPABILITIES = Object.freeze({
|
|
|
286
288
|
exclusionConstraints: false,
|
|
287
289
|
exclusionExtensions: Object.freeze([]),
|
|
288
290
|
checkNormalization: "sourceText",
|
|
291
|
+
schemaCheckExpressions: false,
|
|
289
292
|
nativeRowLevelSecurity: false,
|
|
290
293
|
returning: true,
|
|
291
294
|
scalarArrays: false,
|
|
@@ -312,6 +315,7 @@ var MYSQL_CAPABILITIES = Object.freeze({
|
|
|
312
315
|
exclusionConstraints: false,
|
|
313
316
|
exclusionExtensions: Object.freeze([]),
|
|
314
317
|
checkNormalization: "temporary",
|
|
318
|
+
schemaCheckExpressions: false,
|
|
315
319
|
nativeRowLevelSecurity: false,
|
|
316
320
|
returning: false,
|
|
317
321
|
scalarArrays: false,
|
|
@@ -337,6 +341,261 @@ var DIALECT_CAPABILITIES = Object.freeze({
|
|
|
337
341
|
mysql: MYSQL_CAPABILITIES
|
|
338
342
|
});
|
|
339
343
|
|
|
344
|
+
// packages/schema/src/ir.ts
|
|
345
|
+
var DIALECTS = ["postgres", "sqlite", "mysql"];
|
|
346
|
+
function providerToDialect(params) {
|
|
347
|
+
const { provider } = params;
|
|
348
|
+
switch (provider) {
|
|
349
|
+
case "postgresql":
|
|
350
|
+
case "pglite":
|
|
351
|
+
return "postgres";
|
|
352
|
+
case "sqlite":
|
|
353
|
+
return "sqlite";
|
|
354
|
+
case "mysql":
|
|
355
|
+
return "mysql";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function isEnumType(fieldType) {
|
|
359
|
+
return typeof fieldType === "object" && "enum" in fieldType;
|
|
360
|
+
}
|
|
361
|
+
function fieldTypeIdentity(params) {
|
|
362
|
+
const { type } = params;
|
|
363
|
+
return isEnumType(type) ? type.namespace === undefined || type.namespace === "public" ? `enum:${type.enum}` : JSON.stringify(["enum", type.namespace, type.enum]) : type;
|
|
364
|
+
}
|
|
365
|
+
var ENUM_STORAGES = ["database", "string"];
|
|
366
|
+
function enumStorageOf(params) {
|
|
367
|
+
return params.enumDef.storage ?? "database";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// packages/schema/src/native-rls-admission.ts
|
|
371
|
+
function assertNativeRlsAdmission(params) {
|
|
372
|
+
const { schema } = params;
|
|
373
|
+
if (schema.rls === undefined && !schema.models.some((model) => model.rls !== undefined))
|
|
374
|
+
return;
|
|
375
|
+
const dialect = params.dialect ?? providerToDialect({ provider: schema.datasource.provider });
|
|
376
|
+
if (!DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity) {
|
|
377
|
+
throw unsupportedCapability({ dialect, feature: "nativeRowLevelSecurity" });
|
|
378
|
+
}
|
|
379
|
+
for (const model of schema.models) {
|
|
380
|
+
const rule = model.rls;
|
|
381
|
+
if (rule === undefined || rule === "unprotected")
|
|
382
|
+
continue;
|
|
383
|
+
const parentName = rule !== null && typeof rule === "object" && "through" in rule ? model.relations.find((relation) => relation.name === rule.through)?.targetModel : undefined;
|
|
384
|
+
const parent = parentName === undefined ? undefined : schema.models.find((entry) => entry.name === parentName);
|
|
385
|
+
if (model.namespace !== undefined && model.namespace !== "public" || parent?.namespace !== undefined && parent.namespace !== "public") {
|
|
386
|
+
throw unsupportedCapability({
|
|
387
|
+
dialect,
|
|
388
|
+
feature: "fixedNamespaceNativeRls",
|
|
389
|
+
hint: "Native RLS protects public tables only, including deny-protected models and through-policy parents"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// packages/schema/src/reserved.ts
|
|
395
|
+
var RESERVED_DELEGATE_NAMES = [
|
|
396
|
+
"findMany",
|
|
397
|
+
"findPage",
|
|
398
|
+
"findFirst",
|
|
399
|
+
"findUnique",
|
|
400
|
+
"findFirstOrThrow",
|
|
401
|
+
"findUniqueOrThrow",
|
|
402
|
+
"create",
|
|
403
|
+
"createMany",
|
|
404
|
+
"createManyAndReturn",
|
|
405
|
+
"update",
|
|
406
|
+
"updateMany",
|
|
407
|
+
"updateManyAndReturn",
|
|
408
|
+
"upsert",
|
|
409
|
+
"upsertMany",
|
|
410
|
+
"delete",
|
|
411
|
+
"deleteMany",
|
|
412
|
+
"count",
|
|
413
|
+
"aggregate",
|
|
414
|
+
"groupBy",
|
|
415
|
+
"fields"
|
|
416
|
+
];
|
|
417
|
+
var RESERVED_CLIENT_NAMES = [
|
|
418
|
+
"$connect",
|
|
419
|
+
"$disconnect",
|
|
420
|
+
"$transaction",
|
|
421
|
+
"$querySql",
|
|
422
|
+
"$querySqlResult",
|
|
423
|
+
"$executeSql",
|
|
424
|
+
"$queryRaw",
|
|
425
|
+
"$queryRawUnsafe",
|
|
426
|
+
"$executeRaw",
|
|
427
|
+
"$executeRawUnsafe",
|
|
428
|
+
"$on",
|
|
429
|
+
"$extends",
|
|
430
|
+
"$defineView",
|
|
431
|
+
"$withPolicy",
|
|
432
|
+
"$bypassPolicy",
|
|
433
|
+
"$scoped",
|
|
434
|
+
"$readOnly",
|
|
435
|
+
"$models",
|
|
436
|
+
"$withContext",
|
|
437
|
+
"$advisoryLock",
|
|
438
|
+
"$tryAdvisoryLock"
|
|
439
|
+
];
|
|
440
|
+
var RESERVED_COLUMN_PREFIX = "__vibe_";
|
|
441
|
+
function isReservedDelegateName(params) {
|
|
442
|
+
return RESERVED_DELEGATE_NAMES.includes(params.name);
|
|
443
|
+
}
|
|
444
|
+
function isReservedClientName(params) {
|
|
445
|
+
const withPrefix = params.name.startsWith("$") ? params.name : `$${params.name}`;
|
|
446
|
+
return RESERVED_CLIENT_NAMES.includes(withPrefix);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// packages/schema/src/extension.ts
|
|
450
|
+
function stableSerialize(value) {
|
|
451
|
+
if (value === null || typeof value !== "object")
|
|
452
|
+
return JSON.stringify(value) ?? "undefined";
|
|
453
|
+
if (Array.isArray(value))
|
|
454
|
+
return `[${value.map(stableSerialize).join(",")}]`;
|
|
455
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableSerialize(entryValue)}`);
|
|
456
|
+
return `{${entries.join(",")}}`;
|
|
457
|
+
}
|
|
458
|
+
function extensionOptionsHash(params) {
|
|
459
|
+
const text = stableSerialize(params.options);
|
|
460
|
+
let hash = 2166136261;
|
|
461
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
462
|
+
hash ^= text.charCodeAt(index);
|
|
463
|
+
hash = Math.imul(hash, 16777619);
|
|
464
|
+
}
|
|
465
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
466
|
+
}
|
|
467
|
+
function extensionManifestEntry(params) {
|
|
468
|
+
const { extension } = params;
|
|
469
|
+
return {
|
|
470
|
+
name: extension.name,
|
|
471
|
+
version: extension.version,
|
|
472
|
+
optionsHash: extensionOptionsHash({ options: extension.options })
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
var EXTENSION_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
|
|
476
|
+
function defineExtension(extension) {
|
|
477
|
+
const { name } = extension;
|
|
478
|
+
if (!EXTENSION_NAME_PATTERN.test(name)) {
|
|
479
|
+
throw new VibeError({
|
|
480
|
+
code: "VIBE_CONFIG",
|
|
481
|
+
message: `extension name "${name}" is invalid \u2014 must match ${EXTENSION_NAME_PATTERN} (it becomes a client identifier)`,
|
|
482
|
+
meta: { name }
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (RESERVED_DELEGATE_NAMES.includes(name)) {
|
|
486
|
+
throw new VibeError({
|
|
487
|
+
code: "VIBE_CONFIG",
|
|
488
|
+
message: `extension name "${name}" collides with a core delegate method`,
|
|
489
|
+
meta: { name }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (RESERVED_CLIENT_NAMES.includes(`$${name}`)) {
|
|
493
|
+
throw new VibeError({
|
|
494
|
+
code: "VIBE_CONFIG",
|
|
495
|
+
message: `extension name "${name}" collides with the core client method "$${name}"`,
|
|
496
|
+
meta: { name }
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return Object.freeze(extension);
|
|
500
|
+
}
|
|
501
|
+
function buildExtensionRegistry(params) {
|
|
502
|
+
const byName = new Map;
|
|
503
|
+
const modelAttributes = new Map;
|
|
504
|
+
const fieldTypes = new Map;
|
|
505
|
+
for (const extension of params.extensions) {
|
|
506
|
+
if (byName.has(extension.name)) {
|
|
507
|
+
throw new VibeError({
|
|
508
|
+
code: "VIBE_CONFIG",
|
|
509
|
+
message: `duplicate extension name "${extension.name}" in the configured extension list`,
|
|
510
|
+
meta: { name: extension.name }
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
byName.set(extension.name, extension);
|
|
514
|
+
for (const attribute of extension.schema?.modelAttributes ?? []) {
|
|
515
|
+
const existing = modelAttributes.get(attribute.name);
|
|
516
|
+
if (existing !== undefined) {
|
|
517
|
+
throw new VibeError({
|
|
518
|
+
code: "VIBE_CONFIG",
|
|
519
|
+
message: `model attribute "@@${attribute.name}" is claimed by both "${existing.extension.name}" and "${extension.name}"`,
|
|
520
|
+
meta: { attribute: attribute.name }
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
modelAttributes.set(attribute.name, { extension, attribute });
|
|
524
|
+
}
|
|
525
|
+
for (const fieldType of extension.schema?.fieldTypes ?? []) {
|
|
526
|
+
const existing = fieldTypes.get(fieldType.name);
|
|
527
|
+
if (existing !== undefined) {
|
|
528
|
+
throw new VibeError({
|
|
529
|
+
code: "VIBE_CONFIG",
|
|
530
|
+
message: `field type "${fieldType.name}" is claimed by both "${existing.extension.name}" and "${extension.name}"`,
|
|
531
|
+
meta: { fieldType: fieldType.name }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
fieldTypes.set(fieldType.name, { extension, fieldType });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { extensions: params.extensions, byName, modelAttributes, fieldTypes };
|
|
538
|
+
}
|
|
539
|
+
var EMPTY_EXTENSION_REGISTRY = buildExtensionRegistry({
|
|
540
|
+
extensions: []
|
|
541
|
+
});
|
|
542
|
+
var PUBLIC_ONLY_EXTENSIONS = ["search", "vector", "trgm"];
|
|
543
|
+
function hasPublicExtensionDeclaration(params) {
|
|
544
|
+
return PUBLIC_ONLY_EXTENSIONS.some((name) => Object.hasOwn(extensionDeclarations({ schema: params.schema, name }), params.model));
|
|
545
|
+
}
|
|
546
|
+
function assertExtensionModelPlacement(params) {
|
|
547
|
+
if (params.namespace !== undefined && params.namespace !== "public") {
|
|
548
|
+
throw unsupportedCapability({ dialect: params.dialect ?? "postgres", feature: "fixedNamespaceExtensions", hint: "Search, vector and trigram extensions currently require public table placement" });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function assertExtensionPlacement(params) {
|
|
552
|
+
const dialect = params.dialect ?? providerToDialect({ provider: params.schema.datasource.provider });
|
|
553
|
+
for (const extension of params.schema.extensions) {
|
|
554
|
+
if (!PUBLIC_ONLY_EXTENSIONS.includes(extension.name))
|
|
555
|
+
continue;
|
|
556
|
+
const declarations = extensionDeclarations({ schema: params.schema, name: extension.name });
|
|
557
|
+
for (const model of params.schema.models) {
|
|
558
|
+
if (Object.hasOwn(declarations, model.name))
|
|
559
|
+
assertExtensionModelPlacement({ namespace: model.namespace, dialect });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function isPlainObject(value) {
|
|
564
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
565
|
+
}
|
|
566
|
+
function mergeExtensionDeclarations(params) {
|
|
567
|
+
if (params.extension !== undefined && PUBLIC_ONLY_EXTENSIONS.includes(params.extension) && Array.isArray(params.base.fields) && Array.isArray(params.next.fields)) {
|
|
568
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Repeated index-bearing ${params.extension} declarations are unsupported; use one common-configuration multi-field declaration`, meta: { rule: "repeated-extension-declaration", extension: params.extension } });
|
|
569
|
+
}
|
|
570
|
+
const out = { ...params.base };
|
|
571
|
+
for (const [key, value] of Object.entries(params.next)) {
|
|
572
|
+
const existing = out[key];
|
|
573
|
+
out[key] = isPlainObject(existing) && isPlainObject(value) ? { ...existing, ...value } : value;
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
function extensionDeclarations(params) {
|
|
578
|
+
const decl = params.schema.extensions.find((entry) => entry.name === params.name);
|
|
579
|
+
if (decl === undefined)
|
|
580
|
+
return {};
|
|
581
|
+
const models = decl.options["models"];
|
|
582
|
+
if (models === null || typeof models !== "object" || Array.isArray(models))
|
|
583
|
+
return {};
|
|
584
|
+
return models;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// packages/schema/src/model-sql-namespace.ts
|
|
588
|
+
function modelSqlNamespace(params) {
|
|
589
|
+
const { schema, model } = params;
|
|
590
|
+
if (model.namespace !== undefined || (params.dialect ?? providerToDialect({ provider: schema.datasource.provider })) !== "postgres")
|
|
591
|
+
return model.namespace;
|
|
592
|
+
const protectedModel = model.rls !== undefined && model.rls !== "unprotected";
|
|
593
|
+
const throughParent = schema.models.some((child) => {
|
|
594
|
+
const rule = child.rls;
|
|
595
|
+
return rule !== null && typeof rule === "object" && "through" in rule && child.relations.some((relation) => relation.name === rule.through && relation.targetModel === model.name);
|
|
596
|
+
});
|
|
597
|
+
return protectedModel || throughParent || hasPublicExtensionDeclaration({ schema, model: model.name }) ? SQL_NAMESPACES.postgres : undefined;
|
|
598
|
+
}
|
|
340
599
|
// packages/schema/src/migration-objects.ts
|
|
341
600
|
function sqlObjectKey(params) {
|
|
342
601
|
const object = params.identity;
|
|
@@ -536,32 +795,6 @@ function assertIndexOperatorClasses(params) {
|
|
|
536
795
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Index operator class must be a qualified SQL name { namespace, name }", meta: { rule: "index-operator-class-identity", model: model.name, field: field.name } });
|
|
537
796
|
}
|
|
538
797
|
}
|
|
539
|
-
// packages/schema/src/ir.ts
|
|
540
|
-
var DIALECTS = ["postgres", "sqlite", "mysql"];
|
|
541
|
-
function providerToDialect(params) {
|
|
542
|
-
const { provider } = params;
|
|
543
|
-
switch (provider) {
|
|
544
|
-
case "postgresql":
|
|
545
|
-
case "pglite":
|
|
546
|
-
return "postgres";
|
|
547
|
-
case "sqlite":
|
|
548
|
-
return "sqlite";
|
|
549
|
-
case "mysql":
|
|
550
|
-
return "mysql";
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
function isEnumType(fieldType) {
|
|
554
|
-
return typeof fieldType === "object" && "enum" in fieldType;
|
|
555
|
-
}
|
|
556
|
-
function fieldTypeIdentity(params) {
|
|
557
|
-
const { type } = params;
|
|
558
|
-
return isEnumType(type) ? type.namespace === undefined || type.namespace === "public" ? `enum:${type.enum}` : JSON.stringify(["enum", type.namespace, type.enum]) : type;
|
|
559
|
-
}
|
|
560
|
-
var ENUM_STORAGES = ["database", "string"];
|
|
561
|
-
function enumStorageOf(params) {
|
|
562
|
-
return params.enumDef.storage ?? "database";
|
|
563
|
-
}
|
|
564
|
-
|
|
565
798
|
// packages/schema/src/module-schema.ts
|
|
566
799
|
function prepareDatabaseModuleSchema(params) {
|
|
567
800
|
const { schema, module } = params;
|
|
@@ -610,36 +843,121 @@ function isPredicateFieldRef(operand) {
|
|
|
610
843
|
return operand.kind === "field";
|
|
611
844
|
}
|
|
612
845
|
function predicateFieldNames(params) {
|
|
613
|
-
|
|
846
|
+
return [...new Set(inspectPredicate(params).expressions.flatMap((expression) => expression.kind === "field" ? [expression.name] : []))];
|
|
847
|
+
}
|
|
848
|
+
function inspectPredicate(params) {
|
|
849
|
+
const expressions = [];
|
|
850
|
+
let checkOnly = false;
|
|
851
|
+
const expression = (node) => {
|
|
852
|
+
if (typeof node !== "object" || node === null)
|
|
853
|
+
throw invalidOperand();
|
|
854
|
+
switch (node.kind) {
|
|
855
|
+
case "field":
|
|
856
|
+
expressions.push(node);
|
|
857
|
+
return;
|
|
858
|
+
case "jsonText":
|
|
859
|
+
if (typeof node.field !== "object" || node.field === null || node.field.kind !== "field")
|
|
860
|
+
throw invalidOperand();
|
|
861
|
+
expressions.push(node);
|
|
862
|
+
checkOnly = true;
|
|
863
|
+
expression(node.field);
|
|
864
|
+
return;
|
|
865
|
+
case "length":
|
|
866
|
+
expressions.push(node);
|
|
867
|
+
checkOnly = true;
|
|
868
|
+
expression(node.expression);
|
|
869
|
+
return;
|
|
870
|
+
default:
|
|
871
|
+
throw invalidOperand();
|
|
872
|
+
}
|
|
873
|
+
};
|
|
614
874
|
const walk = (node) => {
|
|
615
875
|
switch (node.kind) {
|
|
616
876
|
case "compare":
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
|
|
877
|
+
expression(node.left);
|
|
878
|
+
if (typeof node.right !== "object" || node.right === null || node.right.kind !== "value")
|
|
879
|
+
expression(node.right);
|
|
620
880
|
return;
|
|
621
881
|
case "inList":
|
|
622
882
|
case "isNull":
|
|
623
|
-
|
|
883
|
+
expression(node.field);
|
|
624
884
|
return;
|
|
625
885
|
case "and":
|
|
626
886
|
case "or":
|
|
627
887
|
for (const operand of node.operands)
|
|
628
888
|
walk(operand);
|
|
629
889
|
return;
|
|
890
|
+
case "isTrue":
|
|
891
|
+
checkOnly = true;
|
|
892
|
+
walk(node.operand);
|
|
893
|
+
return;
|
|
630
894
|
case "not":
|
|
631
895
|
walk(node.operand);
|
|
632
896
|
return;
|
|
633
897
|
}
|
|
634
898
|
};
|
|
635
899
|
walk(params.predicate);
|
|
636
|
-
return
|
|
900
|
+
return { expressions, checkOnly };
|
|
901
|
+
}
|
|
902
|
+
function invalidOperand() {
|
|
903
|
+
return new VibeError({
|
|
904
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
905
|
+
message: "A schema predicate operand must be a field, jsonText or length expression, or a literal on the right of a comparison",
|
|
906
|
+
meta: { rule: "predicate-operand-invalid" }
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function assertPredicateCapabilities(params) {
|
|
910
|
+
if (!inspectPredicate(params).checkOnly)
|
|
911
|
+
return;
|
|
912
|
+
if (params.usage !== "check")
|
|
913
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "schema CHECK expressions outside CHECK constraints" });
|
|
914
|
+
if (!DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)
|
|
915
|
+
throw unsupportedCapability({ dialect: params.dialect, feature: "schemaCheckExpressions" });
|
|
637
916
|
}
|
|
638
917
|
function predicateKey(params) {
|
|
918
|
+
if (inspectPredicate(params).checkOnly) {
|
|
919
|
+
const scalar = (node) => {
|
|
920
|
+
switch (node.kind) {
|
|
921
|
+
case "field":
|
|
922
|
+
return ["field", node.name];
|
|
923
|
+
case "jsonText":
|
|
924
|
+
return ["jsonText", node.field.name, node.path];
|
|
925
|
+
case "length":
|
|
926
|
+
return ["length", scalar(node.expression)];
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const key = (node) => {
|
|
930
|
+
switch (node.kind) {
|
|
931
|
+
case "compare":
|
|
932
|
+
return [node.kind, node.op, scalar(node.left), node.right.kind === "value" ? ["value", node.right.value] : scalar(node.right)];
|
|
933
|
+
case "inList":
|
|
934
|
+
return [node.kind, node.field.name, node.negate === true, node.values.map((value) => value.value)];
|
|
935
|
+
case "isNull":
|
|
936
|
+
return [node.kind, node.field.name, node.negate === true];
|
|
937
|
+
case "and":
|
|
938
|
+
case "or":
|
|
939
|
+
return [node.kind, node.operands.map(key)];
|
|
940
|
+
case "not":
|
|
941
|
+
case "isTrue":
|
|
942
|
+
return [node.kind, key(node.operand)];
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
return `check:${JSON.stringify(key(params.predicate))}`;
|
|
946
|
+
}
|
|
947
|
+
const expression = (node) => {
|
|
948
|
+
switch (node.kind) {
|
|
949
|
+
case "field":
|
|
950
|
+
return `f:${node.name}`;
|
|
951
|
+
case "jsonText":
|
|
952
|
+
return `jsonText(${JSON.stringify([node.field.name, node.path])})`;
|
|
953
|
+
case "length":
|
|
954
|
+
return `length(${expression(node.expression)})`;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
639
957
|
const encode = (node) => {
|
|
640
958
|
switch (node.kind) {
|
|
641
959
|
case "compare":
|
|
642
|
-
return `cmp(${node.op},${node.left.name
|
|
960
|
+
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
961
|
case "inList":
|
|
644
962
|
return `in${node.negate === true ? "!" : ""}(${node.field.name},[${node.values.map((entry) => `${typeof entry.value}:${String(entry.value)}`).join("|")}])`;
|
|
645
963
|
case "isNull":
|
|
@@ -649,6 +967,8 @@ function predicateKey(params) {
|
|
|
649
967
|
return `${node.kind}(${node.operands.map(encode).join(",")})`;
|
|
650
968
|
case "not":
|
|
651
969
|
return `not(${encode(node.operand)})`;
|
|
970
|
+
case "isTrue":
|
|
971
|
+
return `isTrue(${encode(node.operand)})`;
|
|
652
972
|
}
|
|
653
973
|
};
|
|
654
974
|
return encode(params.predicate);
|
|
@@ -775,172 +1095,6 @@ function exactDecimalLiteral(params) {
|
|
|
775
1095
|
const mantissa = text.replace(/[eE].*$/, "").replace("-", "").replace(".", "").replace(/^0+/, "");
|
|
776
1096
|
return mantissa.length <= 15 ? Number(text) : text;
|
|
777
1097
|
}
|
|
778
|
-
// packages/schema/src/reserved.ts
|
|
779
|
-
var RESERVED_DELEGATE_NAMES = [
|
|
780
|
-
"findMany",
|
|
781
|
-
"findPage",
|
|
782
|
-
"findFirst",
|
|
783
|
-
"findUnique",
|
|
784
|
-
"findFirstOrThrow",
|
|
785
|
-
"findUniqueOrThrow",
|
|
786
|
-
"create",
|
|
787
|
-
"createMany",
|
|
788
|
-
"createManyAndReturn",
|
|
789
|
-
"update",
|
|
790
|
-
"updateMany",
|
|
791
|
-
"updateManyAndReturn",
|
|
792
|
-
"upsert",
|
|
793
|
-
"upsertMany",
|
|
794
|
-
"delete",
|
|
795
|
-
"deleteMany",
|
|
796
|
-
"count",
|
|
797
|
-
"aggregate",
|
|
798
|
-
"groupBy",
|
|
799
|
-
"fields"
|
|
800
|
-
];
|
|
801
|
-
var RESERVED_CLIENT_NAMES = [
|
|
802
|
-
"$connect",
|
|
803
|
-
"$disconnect",
|
|
804
|
-
"$transaction",
|
|
805
|
-
"$querySql",
|
|
806
|
-
"$executeSql",
|
|
807
|
-
"$queryRaw",
|
|
808
|
-
"$queryRawUnsafe",
|
|
809
|
-
"$executeRaw",
|
|
810
|
-
"$executeRawUnsafe",
|
|
811
|
-
"$on",
|
|
812
|
-
"$extends",
|
|
813
|
-
"$defineView",
|
|
814
|
-
"$withPolicy",
|
|
815
|
-
"$bypassPolicy",
|
|
816
|
-
"$scoped",
|
|
817
|
-
"$readOnly",
|
|
818
|
-
"$models",
|
|
819
|
-
"$withContext",
|
|
820
|
-
"$advisoryLock",
|
|
821
|
-
"$tryAdvisoryLock"
|
|
822
|
-
];
|
|
823
|
-
var RESERVED_COLUMN_PREFIX = "__vibe_";
|
|
824
|
-
function isReservedDelegateName(params) {
|
|
825
|
-
return RESERVED_DELEGATE_NAMES.includes(params.name);
|
|
826
|
-
}
|
|
827
|
-
function isReservedClientName(params) {
|
|
828
|
-
const withPrefix = params.name.startsWith("$") ? params.name : `$${params.name}`;
|
|
829
|
-
return RESERVED_CLIENT_NAMES.includes(withPrefix);
|
|
830
|
-
}
|
|
831
|
-
// packages/schema/src/extension.ts
|
|
832
|
-
function stableSerialize(value) {
|
|
833
|
-
if (value === null || typeof value !== "object")
|
|
834
|
-
return JSON.stringify(value) ?? "undefined";
|
|
835
|
-
if (Array.isArray(value))
|
|
836
|
-
return `[${value.map(stableSerialize).join(",")}]`;
|
|
837
|
-
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableSerialize(entryValue)}`);
|
|
838
|
-
return `{${entries.join(",")}}`;
|
|
839
|
-
}
|
|
840
|
-
function extensionOptionsHash(params) {
|
|
841
|
-
const text = stableSerialize(params.options);
|
|
842
|
-
let hash = 2166136261;
|
|
843
|
-
for (let index = 0;index < text.length; index += 1) {
|
|
844
|
-
hash ^= text.charCodeAt(index);
|
|
845
|
-
hash = Math.imul(hash, 16777619);
|
|
846
|
-
}
|
|
847
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
848
|
-
}
|
|
849
|
-
function extensionManifestEntry(params) {
|
|
850
|
-
const { extension } = params;
|
|
851
|
-
return {
|
|
852
|
-
name: extension.name,
|
|
853
|
-
version: extension.version,
|
|
854
|
-
optionsHash: extensionOptionsHash({ options: extension.options })
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
var EXTENSION_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
|
|
858
|
-
function defineExtension(extension) {
|
|
859
|
-
const { name } = extension;
|
|
860
|
-
if (!EXTENSION_NAME_PATTERN.test(name)) {
|
|
861
|
-
throw new VibeError({
|
|
862
|
-
code: "VIBE_CONFIG",
|
|
863
|
-
message: `extension name "${name}" is invalid \u2014 must match ${EXTENSION_NAME_PATTERN} (it becomes a client identifier)`,
|
|
864
|
-
meta: { name }
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
if (RESERVED_DELEGATE_NAMES.includes(name)) {
|
|
868
|
-
throw new VibeError({
|
|
869
|
-
code: "VIBE_CONFIG",
|
|
870
|
-
message: `extension name "${name}" collides with a core delegate method`,
|
|
871
|
-
meta: { name }
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
if (RESERVED_CLIENT_NAMES.includes(`$${name}`)) {
|
|
875
|
-
throw new VibeError({
|
|
876
|
-
code: "VIBE_CONFIG",
|
|
877
|
-
message: `extension name "${name}" collides with the core client method "$${name}"`,
|
|
878
|
-
meta: { name }
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
return Object.freeze(extension);
|
|
882
|
-
}
|
|
883
|
-
function buildExtensionRegistry(params) {
|
|
884
|
-
const byName = new Map;
|
|
885
|
-
const modelAttributes = new Map;
|
|
886
|
-
const fieldTypes = new Map;
|
|
887
|
-
for (const extension of params.extensions) {
|
|
888
|
-
if (byName.has(extension.name)) {
|
|
889
|
-
throw new VibeError({
|
|
890
|
-
code: "VIBE_CONFIG",
|
|
891
|
-
message: `duplicate extension name "${extension.name}" in the configured extension list`,
|
|
892
|
-
meta: { name: extension.name }
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
byName.set(extension.name, extension);
|
|
896
|
-
for (const attribute of extension.schema?.modelAttributes ?? []) {
|
|
897
|
-
const existing = modelAttributes.get(attribute.name);
|
|
898
|
-
if (existing !== undefined) {
|
|
899
|
-
throw new VibeError({
|
|
900
|
-
code: "VIBE_CONFIG",
|
|
901
|
-
message: `model attribute "@@${attribute.name}" is claimed by both "${existing.extension.name}" and "${extension.name}"`,
|
|
902
|
-
meta: { attribute: attribute.name }
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
modelAttributes.set(attribute.name, { extension, attribute });
|
|
906
|
-
}
|
|
907
|
-
for (const fieldType of extension.schema?.fieldTypes ?? []) {
|
|
908
|
-
const existing = fieldTypes.get(fieldType.name);
|
|
909
|
-
if (existing !== undefined) {
|
|
910
|
-
throw new VibeError({
|
|
911
|
-
code: "VIBE_CONFIG",
|
|
912
|
-
message: `field type "${fieldType.name}" is claimed by both "${existing.extension.name}" and "${extension.name}"`,
|
|
913
|
-
meta: { fieldType: fieldType.name }
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
fieldTypes.set(fieldType.name, { extension, fieldType });
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return { extensions: params.extensions, byName, modelAttributes, fieldTypes };
|
|
920
|
-
}
|
|
921
|
-
var EMPTY_EXTENSION_REGISTRY = buildExtensionRegistry({
|
|
922
|
-
extensions: []
|
|
923
|
-
});
|
|
924
|
-
function isPlainObject(value) {
|
|
925
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
926
|
-
}
|
|
927
|
-
function mergeExtensionDeclarations(params) {
|
|
928
|
-
const out = { ...params.base };
|
|
929
|
-
for (const [key, value] of Object.entries(params.next)) {
|
|
930
|
-
const existing = out[key];
|
|
931
|
-
out[key] = isPlainObject(existing) && isPlainObject(value) ? { ...existing, ...value } : value;
|
|
932
|
-
}
|
|
933
|
-
return out;
|
|
934
|
-
}
|
|
935
|
-
function extensionDeclarations(params) {
|
|
936
|
-
const decl = params.schema.extensions.find((entry) => entry.name === params.name);
|
|
937
|
-
if (decl === undefined)
|
|
938
|
-
return {};
|
|
939
|
-
const models = decl.options["models"];
|
|
940
|
-
if (models === null || typeof models !== "object" || Array.isArray(models))
|
|
941
|
-
return {};
|
|
942
|
-
return models;
|
|
943
|
-
}
|
|
944
1098
|
// packages/schema/src/computed.ts
|
|
945
1099
|
function buildComputedManifest(params) {
|
|
946
1100
|
const { computed } = params;
|
|
@@ -1097,6 +1251,80 @@ function scalarListDefault(params) {
|
|
|
1097
1251
|
});
|
|
1098
1252
|
}
|
|
1099
1253
|
|
|
1254
|
+
// packages/schema/src/json-schema-ref.ts
|
|
1255
|
+
var EXPORT_NAME = /^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u;
|
|
1256
|
+
var REFERENCE_ANNOTATION = /@(schema|type)\(\s*([$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*)\s+from\s+("(?:[^"\\\r\n]|\\.)*")\s*\)/gu;
|
|
1257
|
+
function annotations(params) {
|
|
1258
|
+
const matches = [...params.documentation.matchAll(REFERENCE_ANNOTATION)].filter((match) => match[1] === params.kind);
|
|
1259
|
+
const remaining = params.documentation.replace(REFERENCE_ANNOTATION, "");
|
|
1260
|
+
const malformed = remaining.match(params.kind === "schema" ? /@schema(?=\s*\(|\s|$)/gu : /@type(?=\s*\(|\s|$)/gu) ?? [];
|
|
1261
|
+
return { matches, count: matches.length + malformed.length };
|
|
1262
|
+
}
|
|
1263
|
+
function hasJsonSchemaAnnotation(params) {
|
|
1264
|
+
return annotations({ documentation: params.documentation ?? "", kind: "schema" }).count > 0;
|
|
1265
|
+
}
|
|
1266
|
+
function jsonTypeAnnotationIssue(params) {
|
|
1267
|
+
const refs = annotations({ documentation: params.documentation ?? "", kind: "type" });
|
|
1268
|
+
if (refs.count === 0)
|
|
1269
|
+
return;
|
|
1270
|
+
if (refs.count > 1)
|
|
1271
|
+
return "A field may declare only one assertion-only JSON type";
|
|
1272
|
+
const match = refs.matches[0];
|
|
1273
|
+
if (match === undefined)
|
|
1274
|
+
return 'Expected @type(Name from "specifier")';
|
|
1275
|
+
return jsonSchemaRefIssue({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
|
|
1276
|
+
}
|
|
1277
|
+
function jsonTypeAnnotationRef(params) {
|
|
1278
|
+
const issue = jsonTypeAnnotationIssue(params);
|
|
1279
|
+
if (issue !== undefined)
|
|
1280
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-json-type-ref", message: issue }] } });
|
|
1281
|
+
const match = annotations({ documentation: params.documentation ?? "", kind: "type" }).matches[0];
|
|
1282
|
+
return match === undefined ? undefined : assertJsonSchemaRef({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
|
|
1283
|
+
}
|
|
1284
|
+
function jsonSchemaRefIssue(params) {
|
|
1285
|
+
const { value } = params;
|
|
1286
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1287
|
+
return "JSON schema reference must be { name, from }; validators belong in the referenced export";
|
|
1288
|
+
const keys = Reflect.ownKeys(value);
|
|
1289
|
+
if (keys.length !== 2 || !keys.includes("name") || !keys.includes("from"))
|
|
1290
|
+
return "JSON schema reference accepts only name and from";
|
|
1291
|
+
const name = Object.getOwnPropertyDescriptor(value, "name");
|
|
1292
|
+
const from = Object.getOwnPropertyDescriptor(value, "from");
|
|
1293
|
+
if (name?.enumerable !== true || !("value" in name) || typeof name.value !== "string" || !EXPORT_NAME.test(name.value) || name.value === "default")
|
|
1294
|
+
return "JSON schema name must be a valid named export, not default";
|
|
1295
|
+
if (from?.enumerable !== true || !("value" in from) || typeof from.value !== "string" || from.value.trim().length === 0 || /[\u0000-\u001f\u007f]/u.test(from.value))
|
|
1296
|
+
return "JSON schema from must be a non-empty import specifier without control characters";
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
function assertJsonSchemaRef(params) {
|
|
1300
|
+
const issue = jsonSchemaRefIssue(params);
|
|
1301
|
+
if (issue !== undefined)
|
|
1302
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: issue, meta: { issues: [{ rule: "invalid-json-schema-ref", message: issue }] } });
|
|
1303
|
+
const value = params.value;
|
|
1304
|
+
return Object.freeze({ name: value.name, from: value.from });
|
|
1305
|
+
}
|
|
1306
|
+
function normalizeJsonSchemaRef(params) {
|
|
1307
|
+
const { documentation, reference } = params;
|
|
1308
|
+
const jsonType = jsonTypeAnnotationRef({ documentation });
|
|
1309
|
+
const refs = annotations({ documentation: documentation ?? "", kind: "schema" });
|
|
1310
|
+
let jsonSchema = reference === undefined ? undefined : assertJsonSchemaRef({ value: reference });
|
|
1311
|
+
let remaining = documentation;
|
|
1312
|
+
if (refs.count > 1 || refs.count > 0 && jsonSchema !== undefined)
|
|
1313
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare only one JSON schema reference", meta: { issues: [{ rule: "duplicate-json-schema-ref" }] } });
|
|
1314
|
+
if (refs.count === 1) {
|
|
1315
|
+
const match = refs.matches[0];
|
|
1316
|
+
if (match === undefined)
|
|
1317
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: 'Expected @schema(Name from "specifier")', meta: { issues: [{ rule: "invalid-json-schema-ref" }] } });
|
|
1318
|
+
jsonSchema = assertJsonSchemaRef({ value: { name: match[2], from: decodePrismaString({ raw: match[3] }) } });
|
|
1319
|
+
remaining = documentation?.replace(match[0], "").trim();
|
|
1320
|
+
if (remaining === "")
|
|
1321
|
+
remaining = undefined;
|
|
1322
|
+
}
|
|
1323
|
+
if (jsonSchema !== undefined && jsonType !== undefined)
|
|
1324
|
+
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" }] } });
|
|
1325
|
+
return { ...remaining === undefined ? {} : { documentation: remaining }, ...jsonSchema === undefined ? {} : { jsonSchema } };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1100
1328
|
// packages/schema/src/m2m.ts
|
|
1101
1329
|
function implicitJoinTable(params) {
|
|
1102
1330
|
const sorted = [params.modelA, params.modelB].sort();
|
|
@@ -1122,6 +1350,8 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1122
1350
|
"SqlColumn",
|
|
1123
1351
|
"SqlFragment",
|
|
1124
1352
|
"ResultDecoder",
|
|
1353
|
+
"ResultReadDecoder",
|
|
1354
|
+
"SqlQueryResult",
|
|
1125
1355
|
"ExtendedDateTime",
|
|
1126
1356
|
"enumValidators",
|
|
1127
1357
|
"enumValidator",
|
|
@@ -1178,6 +1408,9 @@ var RESERVED_ENUM_NAMES = [
|
|
|
1178
1408
|
"InputJsonValue",
|
|
1179
1409
|
"InputJsonArray",
|
|
1180
1410
|
"InputJsonObject",
|
|
1411
|
+
"BoundJsonFilter",
|
|
1412
|
+
"BoundJsonFilterCommon",
|
|
1413
|
+
"BoundJsonNullableFilter",
|
|
1181
1414
|
"VibeTelemetryOptions",
|
|
1182
1415
|
"VibeTelemetryExporter",
|
|
1183
1416
|
"VibeTelemetryOperationEvent",
|
|
@@ -1874,6 +2107,23 @@ function validateModel(params) {
|
|
|
1874
2107
|
});
|
|
1875
2108
|
}
|
|
1876
2109
|
memberNames.add(field.name);
|
|
2110
|
+
const typedIssue = jsonTypeAnnotationIssue({ documentation: field.documentation });
|
|
2111
|
+
if (typedIssue !== undefined)
|
|
2112
|
+
issues.push({ rule: "invalid-json-type-ref", message: typedIssue, model: model.name, field: field.name });
|
|
2113
|
+
const hasType = typedIssue === undefined && jsonTypeAnnotationRef({ documentation: field.documentation }) !== undefined;
|
|
2114
|
+
if (hasType && field.type !== "Json")
|
|
2115
|
+
issues.push({ rule: "json-type-non-json", message: "JSON type assertions are only valid on Json fields", model: model.name, field: field.name });
|
|
2116
|
+
if (field.jsonSchema !== undefined) {
|
|
2117
|
+
const issue = jsonSchemaRefIssue({ value: field.jsonSchema });
|
|
2118
|
+
if (issue !== undefined)
|
|
2119
|
+
issues.push({ rule: "invalid-json-schema-ref", message: issue, model: model.name, field: field.name });
|
|
2120
|
+
if (field.type !== "Json" || field.isUnsupported === true)
|
|
2121
|
+
issues.push({ rule: "json-schema-non-json", message: "JSON schemas are only valid on queryable Json fields", model: model.name, field: field.name });
|
|
2122
|
+
if (hasType)
|
|
2123
|
+
issues.push({ rule: "conflicting-json-schema-ref", message: "A field cannot declare both @type and a JSON schema", model: model.name, field: field.name });
|
|
2124
|
+
}
|
|
2125
|
+
if (hasJsonSchemaAnnotation({ documentation: field.documentation }))
|
|
2126
|
+
issues.push({ rule: "unnormalized-json-schema-ref", message: "Normalize @schema documentation into FieldIR.jsonSchema before consuming SchemaIR", model: model.name, field: field.name });
|
|
1877
2127
|
const typeIdentity = field.nativeTypeIdentity;
|
|
1878
2128
|
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
2129
|
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 +2367,8 @@ function validateModel(params) {
|
|
|
2117
2367
|
model,
|
|
2118
2368
|
fieldNames,
|
|
2119
2369
|
site: `CHECK "${check.name}"`,
|
|
2370
|
+
usage: "check",
|
|
2371
|
+
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2120
2372
|
issues
|
|
2121
2373
|
});
|
|
2122
2374
|
}
|
|
@@ -2138,13 +2390,55 @@ function validateModel(params) {
|
|
|
2138
2390
|
model,
|
|
2139
2391
|
fieldNames,
|
|
2140
2392
|
site: `partial index on [${index.fields.map((entry) => entry.name).join(", ")}]`,
|
|
2393
|
+
usage: "indexPredicate",
|
|
2394
|
+
dialect: providerToDialect({ provider: params.schema.datasource.provider }),
|
|
2141
2395
|
issues
|
|
2142
2396
|
});
|
|
2143
2397
|
}
|
|
2144
2398
|
}
|
|
2399
|
+
for (const exclusion of model.exclusions ?? []) {
|
|
2400
|
+
const predicate = exclusion.source?.where;
|
|
2401
|
+
if (predicate !== undefined)
|
|
2402
|
+
validatePredicateFields({ predicate, model, fieldNames, site: `exclusion "${exclusion.name}"`, usage: "exclusionPredicate", dialect: providerToDialect({ provider: params.schema.datasource.provider }), issues });
|
|
2403
|
+
}
|
|
2145
2404
|
}
|
|
2146
2405
|
function validatePredicateFields(params) {
|
|
2147
2406
|
const { predicate, model, fieldNames, site, issues } = params;
|
|
2407
|
+
const inspection = inspectPredicate({ predicate });
|
|
2408
|
+
if (inspection.checkOnly && (params.usage !== "check" || !DIALECT_CAPABILITIES[params.dialect].schemaCheckExpressions)) {
|
|
2409
|
+
issues.push({ rule: "predicate-check-capability", message: `${site}: computed schema predicates require a PostgreSQL CHECK`, model: model.name });
|
|
2410
|
+
}
|
|
2411
|
+
const expressionType = (expression) => {
|
|
2412
|
+
if (expression.kind === "field")
|
|
2413
|
+
return model.fields.find((field) => field.name === expression.name)?.type;
|
|
2414
|
+
if (expression.kind === "jsonText") {
|
|
2415
|
+
const field = model.fields.find((candidate) => candidate.name === expression.field.name);
|
|
2416
|
+
if (field !== undefined && (field.type !== "Json" || field.isUnsupported === true))
|
|
2417
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: jsonText requires a Json field`, model: model.name, field: field.name });
|
|
2418
|
+
if (!Array.isArray(expression.path) || Array.from(expression.path).some((segment) => typeof segment !== "string" || segment.includes("\x00")))
|
|
2419
|
+
issues.push({ rule: "predicate-json-path", message: `${site}: jsonText requires a string-only path without NUL bytes`, model: model.name });
|
|
2420
|
+
return "String";
|
|
2421
|
+
}
|
|
2422
|
+
if (expressionType(expression.expression) !== "String")
|
|
2423
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: length requires a text expression`, model: model.name });
|
|
2424
|
+
return "Int";
|
|
2425
|
+
};
|
|
2426
|
+
const validateComputedComparisons = (node) => {
|
|
2427
|
+
if (node.kind === "compare") {
|
|
2428
|
+
if (node.left.kind !== "field" || node.right.kind !== "value" && node.right.kind !== "field") {
|
|
2429
|
+
const left = expressionType(node.left);
|
|
2430
|
+
const right = node.right.kind === "value" ? undefined : expressionType(node.right);
|
|
2431
|
+
if (left !== undefined && right !== undefined && left !== right)
|
|
2432
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: computed comparison operands must have the same scalar type`, model: model.name });
|
|
2433
|
+
if (node.right.kind === "value" && left === "String" && typeof node.right.value !== "string")
|
|
2434
|
+
issues.push({ rule: "predicate-type-mismatch", message: `${site}: a text expression requires a string literal`, model: model.name });
|
|
2435
|
+
}
|
|
2436
|
+
} else if (node.kind === "and" || node.kind === "or")
|
|
2437
|
+
node.operands.forEach(validateComputedComparisons);
|
|
2438
|
+
else if (node.kind === "not" || node.kind === "isTrue")
|
|
2439
|
+
validateComputedComparisons(node.operand);
|
|
2440
|
+
};
|
|
2441
|
+
validateComputedComparisons(predicate);
|
|
2148
2442
|
for (const name of predicateFieldNames({ predicate })) {
|
|
2149
2443
|
if (!fieldNames.has(name)) {
|
|
2150
2444
|
issues.push({
|
|
@@ -2155,7 +2449,10 @@ function validatePredicateFields(params) {
|
|
|
2155
2449
|
});
|
|
2156
2450
|
continue;
|
|
2157
2451
|
}
|
|
2158
|
-
|
|
2452
|
+
const referenced = model.fields.find((field) => field.name === name);
|
|
2453
|
+
if (inspection.checkOnly && referenced?.isUnsupported === true)
|
|
2454
|
+
issues.push({ rule: "predicate-unsupported-type", message: `${site}: computed predicates cannot reference unsupported fields`, model: model.name, field: name });
|
|
2455
|
+
if (referenced?.isList === true) {
|
|
2159
2456
|
issues.push({
|
|
2160
2457
|
rule: "predicate-list-field",
|
|
2161
2458
|
message: `${site} on "${model.name}" compares scalar list "${name}" \u2014 use a raw expression for array operators`,
|
|
@@ -2167,6 +2464,8 @@ function validatePredicateFields(params) {
|
|
|
2167
2464
|
}
|
|
2168
2465
|
function assertValidSchemaIR(params) {
|
|
2169
2466
|
const dialect = providerToDialect({ provider: params.schema.datasource.provider });
|
|
2467
|
+
assertNativeRlsAdmission({ schema: params.schema, dialect });
|
|
2468
|
+
assertExtensionPlacement({ schema: params.schema, dialect });
|
|
2170
2469
|
assertIndexOperatorClasses({ schema: params.schema, dialect });
|
|
2171
2470
|
for (const entry of [...params.schema.models, ...params.schema.enums])
|
|
2172
2471
|
if (entry.namespace !== undefined)
|
|
@@ -2182,6 +2481,12 @@ function assertValidSchemaIR(params) {
|
|
|
2182
2481
|
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
2482
|
}
|
|
2184
2483
|
for (const model of params.schema.models) {
|
|
2484
|
+
for (const check of model.checks ?? [])
|
|
2485
|
+
if (check.predicate !== undefined)
|
|
2486
|
+
assertPredicateCapabilities({ predicate: check.predicate, dialect, usage: "check" });
|
|
2487
|
+
for (const index of model.indexes)
|
|
2488
|
+
if (index.wherePredicate !== undefined)
|
|
2489
|
+
assertPredicateCapabilities({ predicate: index.wherePredicate, dialect, usage: "indexPredicate" });
|
|
2185
2490
|
if (model.exclusions !== undefined && !Array.isArray(model.exclusions))
|
|
2186
2491
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusions must be an array", meta: { rule: "exclusion-definition" } });
|
|
2187
2492
|
if ((model.exclusions?.length ?? 0) === 0)
|
|
@@ -2195,6 +2500,8 @@ function assertValidSchemaIR(params) {
|
|
|
2195
2500
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid exclusion declaration", meta: { rule: "exclusion-definition" } });
|
|
2196
2501
|
const source = exclusion.source;
|
|
2197
2502
|
if (source !== undefined) {
|
|
2503
|
+
if (source.where !== undefined)
|
|
2504
|
+
assertPredicateCapabilities({ predicate: source.where, dialect, usage: "exclusionPredicate" });
|
|
2198
2505
|
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
2506
|
if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
|
|
2200
2507
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
|
|
@@ -2206,9 +2513,6 @@ function assertValidSchemaIR(params) {
|
|
|
2206
2513
|
}
|
|
2207
2514
|
if (params.schema.migrationObjects !== undefined)
|
|
2208
2515
|
assertMigrationObjects({ objects: params.schema.migrationObjects });
|
|
2209
|
-
if (params.schema.rls !== undefined && !DIALECT_CAPABILITIES[dialect].nativeRowLevelSecurity) {
|
|
2210
|
-
throw unsupportedCapability({ dialect, feature: "nativeRowLevelSecurity" });
|
|
2211
|
-
}
|
|
2212
2516
|
for (const model of params.schema.models) {
|
|
2213
2517
|
const counters = model.fields.filter((field) => field.default?.kind === "autoincrement");
|
|
2214
2518
|
if (dialect === "mysql" && counters.length > 1)
|
|
@@ -2454,9 +2758,14 @@ export {
|
|
|
2454
2758
|
ScopedQueryError,
|
|
2455
2759
|
VIBE_ERROR_CODES,
|
|
2456
2760
|
VibeError,
|
|
2761
|
+
assertExtensionModelPlacement,
|
|
2762
|
+
assertExtensionPlacement,
|
|
2457
2763
|
assertIdentifierFits,
|
|
2458
2764
|
assertIndexOperatorClasses,
|
|
2765
|
+
assertJsonSchemaRef,
|
|
2459
2766
|
assertMigrationObjects,
|
|
2767
|
+
assertNativeRlsAdmission,
|
|
2768
|
+
assertPredicateCapabilities,
|
|
2460
2769
|
assertSqlNamespace,
|
|
2461
2770
|
assertValidSchemaIR,
|
|
2462
2771
|
buildComputedManifest,
|
|
@@ -2475,9 +2784,11 @@ export {
|
|
|
2475
2784
|
extensionOptionsHash,
|
|
2476
2785
|
fieldTypeIdentity,
|
|
2477
2786
|
fitDefaultName,
|
|
2787
|
+
hasJsonSchemaAnnotation,
|
|
2478
2788
|
identifierLength,
|
|
2479
2789
|
implicitJoinTable,
|
|
2480
2790
|
implicitJoinTableObjectNames,
|
|
2791
|
+
inspectPredicate,
|
|
2481
2792
|
internalError,
|
|
2482
2793
|
invalidStoredData,
|
|
2483
2794
|
isEnumType,
|
|
@@ -2487,7 +2798,12 @@ export {
|
|
|
2487
2798
|
isReservedDelegateName,
|
|
2488
2799
|
isSqlObjectDefinition,
|
|
2489
2800
|
isSqlObjectIdentity,
|
|
2801
|
+
jsonSchemaRefIssue,
|
|
2802
|
+
jsonTypeAnnotationIssue,
|
|
2803
|
+
jsonTypeAnnotationRef,
|
|
2490
2804
|
mergeExtensionDeclarations,
|
|
2805
|
+
modelSqlNamespace,
|
|
2806
|
+
normalizeJsonSchemaRef,
|
|
2491
2807
|
postgresGeneratedObjectName,
|
|
2492
2808
|
predicateFieldNames,
|
|
2493
2809
|
predicateKey,
|
|
@@ -2506,4 +2822,4 @@ export {
|
|
|
2506
2822
|
validateSchemaIR
|
|
2507
2823
|
};
|
|
2508
2824
|
|
|
2509
|
-
//# debugId=
|
|
2825
|
+
//# debugId=721E026706902A1F64756E2164756E21
|