@vibeorm/schema 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +401 -111
- package/dist/index.js.map +9 -8
- package/dist/ir.d.ts +5 -0
- package/dist/ir.d.ts.map +1 -1
- package/dist/migration-objects.d.ts +7 -1
- package/dist/migration-objects.d.ts.map +1 -1
- package/dist/physical-names.d.ts +119 -0
- package/dist/physical-names.d.ts.map +1 -0
- package/dist/prisma-literal.d.ts +13 -0
- package/dist/prisma-literal.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -278,7 +278,7 @@ function isSqlObjectIdentity(value) {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
function isSqlObjectDefinition(value) {
|
|
281
|
-
return record(value) && value.format === "catalog-v1" && isSqlObjectIdentity(value.identity) && record(value.facts) && Object.keys(value.facts).length > 0 && Object.values(value.facts).every((fact) => fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")) && Array.isArray(value.dependencies) && value.dependencies.every(isSqlObjectIdentity);
|
|
281
|
+
return record(value) && (value.format === "catalog-v1" || value.format === "catalog-v2") && isSqlObjectIdentity(value.identity) && record(value.facts) && Object.keys(value.facts).length > 0 && Object.values(value.facts).every((fact) => fact === null || typeof fact === "string" || typeof fact === "boolean" || typeof fact === "number" && Number.isFinite(fact) || Array.isArray(fact) && fact.every((entry) => typeof entry === "string")) && Array.isArray(value.dependencies) && value.dependencies.every(isSqlObjectIdentity);
|
|
282
282
|
}
|
|
283
283
|
function assertMigrationObjects(params) {
|
|
284
284
|
if (!Array.isArray(params.objects))
|
|
@@ -389,6 +389,21 @@ function exactIntegerLiteral(params) {
|
|
|
389
389
|
return asNumber;
|
|
390
390
|
return BigInt(text).toString();
|
|
391
391
|
}
|
|
392
|
+
var DECIMAL_LITERAL = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
393
|
+
function exactDecimalLiteral(params) {
|
|
394
|
+
const { text } = params;
|
|
395
|
+
if (!DECIMAL_LITERAL.test(text)) {
|
|
396
|
+
throw new VibeError({
|
|
397
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
398
|
+
message: `Not a numeric literal: ${JSON.stringify(text)}`,
|
|
399
|
+
meta: { issues: [{ rule: "malformed-numeric-literal", message: text }], literal: text }
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (INTEGER_LITERAL.test(text))
|
|
403
|
+
return exactIntegerLiteral({ text });
|
|
404
|
+
const mantissa = text.replace(/[eE].*$/, "").replace("-", "").replace(".", "").replace(/^0+/, "");
|
|
405
|
+
return mantissa.length <= 15 ? Number(text) : text;
|
|
406
|
+
}
|
|
392
407
|
// packages/schema/src/reserved.ts
|
|
393
408
|
var RESERVED_DELEGATE_NAMES = [
|
|
394
409
|
"findMany",
|
|
@@ -469,26 +484,26 @@ function extensionManifestEntry(params) {
|
|
|
469
484
|
}
|
|
470
485
|
var EXTENSION_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
|
|
471
486
|
function defineExtension(extension) {
|
|
472
|
-
const { name
|
|
473
|
-
if (!EXTENSION_NAME_PATTERN.test(
|
|
487
|
+
const { name } = extension;
|
|
488
|
+
if (!EXTENSION_NAME_PATTERN.test(name)) {
|
|
474
489
|
throw new VibeError({
|
|
475
490
|
code: "VIBE_CONFIG",
|
|
476
|
-
message: `extension name "${
|
|
477
|
-
meta: { name
|
|
491
|
+
message: `extension name "${name}" is invalid \u2014 must match ${EXTENSION_NAME_PATTERN} (it becomes a client identifier)`,
|
|
492
|
+
meta: { name }
|
|
478
493
|
});
|
|
479
494
|
}
|
|
480
|
-
if (RESERVED_DELEGATE_NAMES.includes(
|
|
495
|
+
if (RESERVED_DELEGATE_NAMES.includes(name)) {
|
|
481
496
|
throw new VibeError({
|
|
482
497
|
code: "VIBE_CONFIG",
|
|
483
|
-
message: `extension name "${
|
|
484
|
-
meta: { name
|
|
498
|
+
message: `extension name "${name}" collides with a core delegate method`,
|
|
499
|
+
meta: { name }
|
|
485
500
|
});
|
|
486
501
|
}
|
|
487
|
-
if (RESERVED_CLIENT_NAMES.includes(`$${
|
|
502
|
+
if (RESERVED_CLIENT_NAMES.includes(`$${name}`)) {
|
|
488
503
|
throw new VibeError({
|
|
489
504
|
code: "VIBE_CONFIG",
|
|
490
|
-
message: `extension name "${
|
|
491
|
-
meta: { name
|
|
505
|
+
message: `extension name "${name}" collides with the core client method "$${name}"`,
|
|
506
|
+
meta: { name }
|
|
492
507
|
});
|
|
493
508
|
}
|
|
494
509
|
return Object.freeze(extension);
|
|
@@ -710,6 +725,102 @@ function scalarListDefault(params) {
|
|
|
710
725
|
});
|
|
711
726
|
}
|
|
712
727
|
|
|
728
|
+
// packages/schema/src/m2m.ts
|
|
729
|
+
function implicitJoinTable(params) {
|
|
730
|
+
const sorted = [params.modelA, params.modelB].sort();
|
|
731
|
+
const first = sorted[0];
|
|
732
|
+
const second = sorted[1];
|
|
733
|
+
const key = params.relationName ?? `${first}To${second}`;
|
|
734
|
+
return {
|
|
735
|
+
tableName: `_${key}`,
|
|
736
|
+
modelA: first,
|
|
737
|
+
modelB: second,
|
|
738
|
+
columnA: "A",
|
|
739
|
+
columnB: "B"
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// packages/schema/src/physical-names.ts
|
|
744
|
+
var IDENTIFIER_LIMITS = Object.freeze({
|
|
745
|
+
postgres: { max: 63, unit: "bytes" },
|
|
746
|
+
mysql: { max: 64, unit: "characters" },
|
|
747
|
+
sqlite: undefined
|
|
748
|
+
});
|
|
749
|
+
var ENCODER = new TextEncoder;
|
|
750
|
+
function identifierLength(params) {
|
|
751
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
752
|
+
return limit?.unit === "bytes" ? ENCODER.encode(params.name).length : [...params.name].length;
|
|
753
|
+
}
|
|
754
|
+
function cutIdentifier(params) {
|
|
755
|
+
let out = "";
|
|
756
|
+
let used = 0;
|
|
757
|
+
for (const char of params.name) {
|
|
758
|
+
const size = params.unit === "bytes" ? ENCODER.encode(char).length : 1;
|
|
759
|
+
if (used + size > params.length)
|
|
760
|
+
break;
|
|
761
|
+
out += char;
|
|
762
|
+
used += size;
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
function fitDefaultName(params) {
|
|
767
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
768
|
+
if (limit === undefined)
|
|
769
|
+
return `${params.prefix}${params.suffix}`;
|
|
770
|
+
const room = limit.max - identifierLength({ name: params.suffix, dialect: params.dialect });
|
|
771
|
+
if (identifierLength({ name: params.prefix, dialect: params.dialect }) <= room)
|
|
772
|
+
return `${params.prefix}${params.suffix}`;
|
|
773
|
+
return `${cutIdentifier({ name: params.prefix, length: room, unit: limit.unit })}${params.suffix}`;
|
|
774
|
+
}
|
|
775
|
+
function assertIdentifierFits(params) {
|
|
776
|
+
const limit = IDENTIFIER_LIMITS[params.dialect];
|
|
777
|
+
if (limit === undefined)
|
|
778
|
+
return;
|
|
779
|
+
const length = identifierLength({ name: params.name, dialect: params.dialect });
|
|
780
|
+
if (length <= limit.max)
|
|
781
|
+
return;
|
|
782
|
+
throw new VibeError({
|
|
783
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
784
|
+
message: `${params.what} "${params.name}" is ${length} ${limit.unit} \u2014 ${params.dialect} keeps at most ${limit.max}; the created object would never carry this name and migrations could not converge. ${params.remedy}`,
|
|
785
|
+
meta: { name: params.name, dialect: params.dialect, length, max: limit.max }
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function defaultIndexName(params) {
|
|
789
|
+
return fitDefaultName({
|
|
790
|
+
prefix: `${params.table}_${params.columns.join("_")}`,
|
|
791
|
+
suffix: params.unique ? "_key" : "_idx",
|
|
792
|
+
dialect: params.dialect
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function defaultForeignKeyName(params) {
|
|
796
|
+
return fitDefaultName({ prefix: `${params.table}_${params.columns.join("_")}`, suffix: "_fkey", dialect: params.dialect });
|
|
797
|
+
}
|
|
798
|
+
function defaultPrimaryKeyName(params) {
|
|
799
|
+
return fitDefaultName({ prefix: params.table, suffix: "_pkey", dialect: params.dialect });
|
|
800
|
+
}
|
|
801
|
+
function implicitJoinTableObjectNames(params) {
|
|
802
|
+
return {
|
|
803
|
+
uniqueIndex: `${params.table}_AB_unique`,
|
|
804
|
+
indexB: `${params.table}_B_index`,
|
|
805
|
+
foreignKeyA: `${params.table}_A_fkey`,
|
|
806
|
+
foreignKeyB: `${params.table}_B_fkey`
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function postgresGeneratedObjectName(params) {
|
|
810
|
+
const limit = IDENTIFIER_LIMITS.postgres;
|
|
811
|
+
const overhead = params.label.length + 2;
|
|
812
|
+
let chars1 = [...params.name1];
|
|
813
|
+
let chars2 = [...params.name2];
|
|
814
|
+
const bytes = (chars) => ENCODER.encode(chars.join("")).length;
|
|
815
|
+
while (bytes(chars1) + bytes(chars2) + overhead > limit.max) {
|
|
816
|
+
if (bytes(chars1) > bytes(chars2))
|
|
817
|
+
chars1 = chars1.slice(0, -1);
|
|
818
|
+
else
|
|
819
|
+
chars2 = chars2.slice(0, -1);
|
|
820
|
+
}
|
|
821
|
+
return `${chars1.join("")}_${chars2.join("")}_${params.label}`;
|
|
822
|
+
}
|
|
823
|
+
|
|
713
824
|
// packages/schema/src/capabilities.ts
|
|
714
825
|
var POSTGRES_CAPABILITIES = Object.freeze({
|
|
715
826
|
exclusionConstraints: true,
|
|
@@ -1038,21 +1149,21 @@ var INVALID_BINDING_NAMES = new Set([
|
|
|
1038
1149
|
]);
|
|
1039
1150
|
function validateGeneratedNames(params) {
|
|
1040
1151
|
const { schema, issues } = params;
|
|
1041
|
-
const owners = new Map(RESERVED_ENUM_NAMES.map((
|
|
1152
|
+
const owners = new Map(RESERVED_ENUM_NAMES.map((name) => [name, "generated client"]));
|
|
1042
1153
|
if (schema.rls !== undefined) {
|
|
1043
1154
|
owners.set("RlsContext", "native RLS client");
|
|
1044
1155
|
owners.set("BoundVibeClientInstance", "native RLS client");
|
|
1045
1156
|
owners.set("BoundVibeTransactionClientInstance", "native RLS client");
|
|
1046
1157
|
}
|
|
1047
|
-
const register = (
|
|
1048
|
-
if (!/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(
|
|
1049
|
-
issues.push({ rule: "invalid-generated-identifier", message: `Generated name "${
|
|
1158
|
+
const register = (params) => {
|
|
1159
|
+
if (!/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u.test(params.name) || INVALID_BINDING_NAMES.has(params.name)) {
|
|
1160
|
+
issues.push({ rule: "invalid-generated-identifier", message: `Generated name "${params.name}" from ${params.owner} is not a legal TypeScript identifier` });
|
|
1050
1161
|
}
|
|
1051
|
-
const previous = owners.get(
|
|
1052
|
-
if (previous !== undefined && previous !==
|
|
1053
|
-
issues.push({ rule: "generated-name-collision", message: `Generated name "${
|
|
1162
|
+
const previous = owners.get(params.name);
|
|
1163
|
+
if (previous !== undefined && previous !== params.owner) {
|
|
1164
|
+
issues.push({ rule: "generated-name-collision", message: `Generated name "${params.name}" from ${params.owner} conflicts with ${previous}` });
|
|
1054
1165
|
} else {
|
|
1055
|
-
owners.set(
|
|
1166
|
+
owners.set(params.name, params.owner);
|
|
1056
1167
|
}
|
|
1057
1168
|
};
|
|
1058
1169
|
const aliases = new Map;
|
|
@@ -1200,8 +1311,8 @@ function validateSchemaIR(params) {
|
|
|
1200
1311
|
}
|
|
1201
1312
|
}
|
|
1202
1313
|
const seenSelectorNames = new Set;
|
|
1203
|
-
const checkSelectorName = (
|
|
1204
|
-
const { clientName, fieldCount, where, isUnique, site } =
|
|
1314
|
+
const checkSelectorName = (params) => {
|
|
1315
|
+
const { clientName, fieldCount, where, isUnique, site } = params;
|
|
1205
1316
|
if (!isUnique) {
|
|
1206
1317
|
issues.push({
|
|
1207
1318
|
rule: "client-name-non-unique",
|
|
@@ -1256,10 +1367,189 @@ function validateSchemaIR(params) {
|
|
|
1256
1367
|
});
|
|
1257
1368
|
}
|
|
1258
1369
|
}
|
|
1370
|
+
validateIdentifierLengths({ schema, issues });
|
|
1371
|
+
validatePhysicalNameUniqueness({ schema, issues });
|
|
1259
1372
|
validateGeneratedNames({ schema, issues });
|
|
1260
1373
|
validateNativeRls({ schema, issues });
|
|
1261
1374
|
return issues;
|
|
1262
1375
|
}
|
|
1376
|
+
function validatePhysicalNameUniqueness(params) {
|
|
1377
|
+
const { schema, issues } = params;
|
|
1378
|
+
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
1379
|
+
const owners = new Map;
|
|
1380
|
+
const claim = (entry) => {
|
|
1381
|
+
const key = `${entry.namespace}\x00${entry.name}`;
|
|
1382
|
+
const previous = owners.get(key);
|
|
1383
|
+
if (previous === undefined) {
|
|
1384
|
+
owners.set(key, `${entry.what} of ${entry.owner}`);
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
issues.push({
|
|
1388
|
+
rule: "duplicate-physical-name",
|
|
1389
|
+
message: `${entry.what} "${entry.name}" of ${entry.owner} collides with the ${previous} \u2014 both resolve to one physical ${entry.namespace === "relation" ? "object" : "name"}, and only one can exist`,
|
|
1390
|
+
...entry.model === undefined ? {} : { model: entry.model },
|
|
1391
|
+
...entry.field === undefined ? {} : { field: entry.field }
|
|
1392
|
+
});
|
|
1393
|
+
return false;
|
|
1394
|
+
};
|
|
1395
|
+
const indexNamespace = (table) => dialect === "mysql" ? `index:${table}` : "relation";
|
|
1396
|
+
const constraintNamespace = (table) => dialect === "mysql" ? "constraint" : `constraint:${table}`;
|
|
1397
|
+
for (const model of schema.models) {
|
|
1398
|
+
const table = model.dbName ?? model.name;
|
|
1399
|
+
if (!claim({ namespace: "relation", name: table, what: model.isView ? "view" : "table", owner: `model "${model.name}"`, model: model.name }))
|
|
1400
|
+
continue;
|
|
1401
|
+
for (const fieldIR of model.fields) {
|
|
1402
|
+
claim({ namespace: `column:${table}`, name: fieldIR.dbName ?? fieldIR.name, what: "column", owner: `field "${model.name}.${fieldIR.name}"`, model: model.name, field: fieldIR.name });
|
|
1403
|
+
}
|
|
1404
|
+
if (model.isView)
|
|
1405
|
+
continue;
|
|
1406
|
+
const columnOf = (name) => model.fields.find((candidate) => candidate.name === name)?.dbName ?? name;
|
|
1407
|
+
const seenIndexKeys = new Set;
|
|
1408
|
+
const declarations = [
|
|
1409
|
+
...model.indexes.map((index) => ({ index, site: index.kind === "unique" ? "@@unique" : "@@index" })),
|
|
1410
|
+
...model.fields.filter((fieldIR) => fieldIR.isUnique).map((fieldIR) => ({ index: { kind: "unique", fields: [{ name: fieldIR.name }] }, site: `@unique on "${fieldIR.name}"` }))
|
|
1411
|
+
];
|
|
1412
|
+
for (const { index, site } of declarations) {
|
|
1413
|
+
const unique = index.kind === "unique";
|
|
1414
|
+
const columns = index.fields.map((entry) => `${columnOf(entry.name)}${entry.sort === "desc" ? " desc" : ""}`);
|
|
1415
|
+
const key = `${unique ? "u" : "i"}${index.nullsNotDistinct === true ? "n" : ""}:${unique ? "btree" : index.kind}:${columns.join(",")}:${index.where?.trim() ?? ""}`;
|
|
1416
|
+
if (seenIndexKeys.has(key))
|
|
1417
|
+
continue;
|
|
1418
|
+
seenIndexKeys.add(key);
|
|
1419
|
+
const name = index.name ?? defaultIndexName({ table, columns: index.fields.map((entry) => columnOf(entry.name)), unique, dialect });
|
|
1420
|
+
claim({ namespace: indexNamespace(table), name, what: unique ? "unique index" : "index", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1421
|
+
if (index.constraintName !== undefined) {
|
|
1422
|
+
claim({ namespace: constraintNamespace(table), name: index.constraintName, what: "unique constraint", owner: `${site} on "${model.name}"`, model: model.name });
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (dialect === "postgres" && (model.primaryKey !== undefined || model.fields.some((fieldIR) => fieldIR.isId))) {
|
|
1426
|
+
const name = model.primaryKeyConstraintName ?? defaultPrimaryKeyName({ table, dialect });
|
|
1427
|
+
claim({ namespace: "relation", name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
|
|
1428
|
+
claim({ namespace: constraintNamespace(table), name, what: "primary key", owner: `model "${model.name}"`, model: model.name });
|
|
1429
|
+
}
|
|
1430
|
+
for (const relation of model.relations) {
|
|
1431
|
+
if (relation.fields === undefined || relation.fields.length === 0)
|
|
1432
|
+
continue;
|
|
1433
|
+
const target = schema.models.find((candidate) => candidate.name === relation.targetModel);
|
|
1434
|
+
if (target !== undefined && target.isView)
|
|
1435
|
+
continue;
|
|
1436
|
+
const name = relation.constraintName ?? defaultForeignKeyName({ table, columns: relation.fields.map((entry) => columnOf(entry)), dialect });
|
|
1437
|
+
claim({ namespace: constraintNamespace(table), name, what: "foreign key", owner: `relation "${model.name}.${relation.name}"`, model: model.name });
|
|
1438
|
+
}
|
|
1439
|
+
for (const checkIR of model.checks ?? []) {
|
|
1440
|
+
claim({ namespace: constraintNamespace(table), name: checkIR.name, what: "CHECK constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1441
|
+
}
|
|
1442
|
+
for (const exclusion of model.exclusions ?? []) {
|
|
1443
|
+
claim({ namespace: constraintNamespace(table), name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1444
|
+
if (dialect === "postgres")
|
|
1445
|
+
claim({ namespace: "relation", name: exclusion.name, what: "exclusion constraint", owner: `model "${model.name}"`, model: model.name });
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (dialect === "postgres") {
|
|
1449
|
+
for (const enumDef of schema.enums) {
|
|
1450
|
+
if (enumStorageOf({ enumDef }) !== "database")
|
|
1451
|
+
continue;
|
|
1452
|
+
claim({ namespace: "relation", name: enumDef.dbName ?? enumDef.name, what: "enum type", owner: `enum "${enumDef.name}"` });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const seenJoinTables = new Set;
|
|
1456
|
+
for (const model of schema.models) {
|
|
1457
|
+
for (const relation of model.relations) {
|
|
1458
|
+
if (relation.targetModel === model.name)
|
|
1459
|
+
continue;
|
|
1460
|
+
if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
|
|
1461
|
+
continue;
|
|
1462
|
+
const join = implicitJoinTable({ modelA: model.name, modelB: relation.targetModel, ...relation.relationName === undefined ? {} : { relationName: relation.relationName } });
|
|
1463
|
+
if (seenJoinTables.has(join.tableName))
|
|
1464
|
+
continue;
|
|
1465
|
+
seenJoinTables.add(join.tableName);
|
|
1466
|
+
const owner = `implicit relation "${model.name}.${relation.name}"`;
|
|
1467
|
+
claim({ namespace: "relation", name: join.tableName, what: "join table", owner, model: model.name });
|
|
1468
|
+
const derived = implicitJoinTableObjectNames({ table: join.tableName });
|
|
1469
|
+
claim({ namespace: indexNamespace(join.tableName), name: derived.uniqueIndex, what: "unique index", owner, model: model.name });
|
|
1470
|
+
claim({ namespace: indexNamespace(join.tableName), name: derived.indexB, what: "index", owner, model: model.name });
|
|
1471
|
+
claim({ namespace: constraintNamespace(join.tableName), name: derived.foreignKeyA, what: "foreign key", owner, model: model.name });
|
|
1472
|
+
claim({ namespace: constraintNamespace(join.tableName), name: derived.foreignKeyB, what: "foreign key", owner, model: model.name });
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function validateIdentifierLengths(params) {
|
|
1477
|
+
const { schema, issues } = params;
|
|
1478
|
+
const dialect = providerToDialect({ provider: schema.datasource.provider });
|
|
1479
|
+
const limit = IDENTIFIER_LIMITS[dialect];
|
|
1480
|
+
if (limit === undefined)
|
|
1481
|
+
return;
|
|
1482
|
+
const { max, unit } = limit;
|
|
1483
|
+
const consequence = dialect === "postgres" ? `PostgreSQL silently truncates identifiers to ${max} bytes (NAMEDATALEN - 1), so the created object never carries this name and migrations cannot converge` : `MySQL refuses identifiers longer than ${max} characters (ER_TOO_LONG_IDENT)`;
|
|
1484
|
+
const check = (entry) => {
|
|
1485
|
+
const length = identifierLength({ name: entry.name, dialect });
|
|
1486
|
+
if (length <= max)
|
|
1487
|
+
return;
|
|
1488
|
+
issues.push({
|
|
1489
|
+
rule: "identifier-too-long",
|
|
1490
|
+
message: `${entry.what} "${entry.name}" is ${length} ${unit} \u2014 ${consequence}. ${entry.remedy}`,
|
|
1491
|
+
...entry.model === undefined ? {} : { model: entry.model }
|
|
1492
|
+
});
|
|
1493
|
+
};
|
|
1494
|
+
for (const model of schema.models) {
|
|
1495
|
+
const table = model.dbName ?? model.name;
|
|
1496
|
+
check({ name: table, what: model.isView ? "View name" : "Table name", model: model.name, remedy: `Map "${model.name}" to a shorter physical name (@@map).` });
|
|
1497
|
+
for (const fieldIR of model.fields) {
|
|
1498
|
+
check({ name: fieldIR.dbName ?? fieldIR.name, what: "Column name", model: model.name, remedy: `Map "${model.name}.${fieldIR.name}" to a shorter column name (@map).` });
|
|
1499
|
+
}
|
|
1500
|
+
if (model.isView)
|
|
1501
|
+
continue;
|
|
1502
|
+
for (const index of model.indexes) {
|
|
1503
|
+
if (index.name !== undefined) {
|
|
1504
|
+
check({ name: index.name, what: index.kind === "unique" ? "Unique index name" : "Index name", model: model.name, remedy: "Shorten the index name (map:)." });
|
|
1505
|
+
}
|
|
1506
|
+
if (index.constraintName !== undefined) {
|
|
1507
|
+
check({ name: index.constraintName, what: "Unique constraint name", model: model.name, remedy: "Shorten the constraint name." });
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (model.primaryKeyConstraintName !== undefined) {
|
|
1511
|
+
check({ name: model.primaryKeyConstraintName, what: "Primary key constraint name", model: model.name, remedy: "Shorten the constraint name (map:)." });
|
|
1512
|
+
}
|
|
1513
|
+
for (const relation of model.relations) {
|
|
1514
|
+
if (relation.constraintName === undefined)
|
|
1515
|
+
continue;
|
|
1516
|
+
check({ name: relation.constraintName, what: "Foreign key constraint name", model: model.name, remedy: "Shorten the constraint name (map:)." });
|
|
1517
|
+
}
|
|
1518
|
+
for (const checkIR of model.checks ?? []) {
|
|
1519
|
+
check({ name: checkIR.name, what: "CHECK constraint name", model: model.name, remedy: "Shorten the CHECK name." });
|
|
1520
|
+
}
|
|
1521
|
+
for (const exclusion of model.exclusions ?? []) {
|
|
1522
|
+
check({ name: exclusion.name, what: "Exclusion constraint name", model: model.name, remedy: "Shorten the exclusion name." });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (dialect === "postgres") {
|
|
1526
|
+
for (const enumDef of schema.enums) {
|
|
1527
|
+
if (enumStorageOf({ enumDef }) !== "database")
|
|
1528
|
+
continue;
|
|
1529
|
+
check({ name: enumDef.dbName ?? enumDef.name, what: "Enum type name", remedy: `Map enum "${enumDef.name}" to a shorter type name (@@map).` });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const seenJoinTables = new Set;
|
|
1533
|
+
for (const model of schema.models) {
|
|
1534
|
+
for (const relation of model.relations) {
|
|
1535
|
+
if (relation.targetModel === model.name)
|
|
1536
|
+
continue;
|
|
1537
|
+
if (!isImplicitManyToManyRelation({ model, relation, models: schema.models }))
|
|
1538
|
+
continue;
|
|
1539
|
+
const join = implicitJoinTable({ modelA: model.name, modelB: relation.targetModel, ...relation.relationName === undefined ? {} : { relationName: relation.relationName } });
|
|
1540
|
+
if (seenJoinTables.has(join.tableName))
|
|
1541
|
+
continue;
|
|
1542
|
+
seenJoinTables.add(join.tableName);
|
|
1543
|
+
const remedy = `Give the relation an explicit shorter name (@relation("...")).`;
|
|
1544
|
+
check({ name: join.tableName, what: "Implicit join table name", model: model.name, remedy });
|
|
1545
|
+
const derived = implicitJoinTableObjectNames({ table: join.tableName });
|
|
1546
|
+
check({ name: derived.uniqueIndex, what: "Implicit join table index name", model: model.name, remedy });
|
|
1547
|
+
check({ name: derived.indexB, what: "Implicit join table index name", model: model.name, remedy });
|
|
1548
|
+
check({ name: derived.foreignKeyA, what: "Implicit join table foreign key name", model: model.name, remedy });
|
|
1549
|
+
check({ name: derived.foreignKeyB, what: "Implicit join table foreign key name", model: model.name, remedy });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1263
1553
|
function validateModel(params) {
|
|
1264
1554
|
const { model, modelByName, enumByName, issues } = params;
|
|
1265
1555
|
const memberNames = new Set;
|
|
@@ -1273,6 +1563,10 @@ function validateModel(params) {
|
|
|
1273
1563
|
});
|
|
1274
1564
|
}
|
|
1275
1565
|
memberNames.add(field.name);
|
|
1566
|
+
const typeIdentity = field.nativeTypeIdentity;
|
|
1567
|
+
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"))) {
|
|
1568
|
+
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 });
|
|
1569
|
+
}
|
|
1276
1570
|
const defaultValue = field.default;
|
|
1277
1571
|
if (defaultValue !== undefined && defaultValue.kind !== "dbgenerated" && defaultValue.kind !== "list") {
|
|
1278
1572
|
let valid = !field.isList;
|
|
@@ -1326,8 +1620,8 @@ function validateModel(params) {
|
|
|
1326
1620
|
field: field.name
|
|
1327
1621
|
});
|
|
1328
1622
|
} else {
|
|
1329
|
-
const
|
|
1330
|
-
const candidate =
|
|
1623
|
+
const defaultValue = field.default;
|
|
1624
|
+
const candidate = defaultValue === undefined ? undefined : defaultValue.kind === "enumValue" ? defaultValue.value : defaultValue.kind === "literal" && typeof defaultValue.value === "string" ? defaultValue.value : undefined;
|
|
1331
1625
|
if (candidate !== undefined && !enumDef.values.includes(candidate)) {
|
|
1332
1626
|
issues.push({
|
|
1333
1627
|
rule: "enum-default-not-member",
|
|
@@ -1519,22 +1813,22 @@ function validateModel(params) {
|
|
|
1519
1813
|
}
|
|
1520
1814
|
function validatePredicateFields(params) {
|
|
1521
1815
|
const { predicate, model, fieldNames, site, issues } = params;
|
|
1522
|
-
for (const
|
|
1523
|
-
if (!fieldNames.has(
|
|
1816
|
+
for (const name of predicateFieldNames({ predicate })) {
|
|
1817
|
+
if (!fieldNames.has(name)) {
|
|
1524
1818
|
issues.push({
|
|
1525
1819
|
rule: "predicate-unknown-field",
|
|
1526
|
-
message: `${site} on "${model.name}" references unknown field "${
|
|
1820
|
+
message: `${site} on "${model.name}" references unknown field "${name}"`,
|
|
1527
1821
|
model: model.name,
|
|
1528
|
-
field:
|
|
1822
|
+
field: name
|
|
1529
1823
|
});
|
|
1530
1824
|
continue;
|
|
1531
1825
|
}
|
|
1532
|
-
if (model.fields.find((field) => field.name ===
|
|
1826
|
+
if (model.fields.find((field) => field.name === name)?.isList === true) {
|
|
1533
1827
|
issues.push({
|
|
1534
1828
|
rule: "predicate-list-field",
|
|
1535
|
-
message: `${site} on "${model.name}" compares scalar list "${
|
|
1829
|
+
message: `${site} on "${model.name}" compares scalar list "${name}" \u2014 use a raw expression for array operators`,
|
|
1536
1830
|
model: model.name,
|
|
1537
|
-
field:
|
|
1831
|
+
field: name
|
|
1538
1832
|
});
|
|
1539
1833
|
}
|
|
1540
1834
|
}
|
|
@@ -1556,10 +1850,10 @@ function assertValidSchemaIR(params) {
|
|
|
1556
1850
|
const source = exclusion.source;
|
|
1557
1851
|
if (source !== undefined) {
|
|
1558
1852
|
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 })];
|
|
1559
|
-
if (references.some((
|
|
1853
|
+
if (references.some((name) => !model.fields.some((field) => field.name === name && !field.isList)))
|
|
1560
1854
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Exclusion references an unknown or list field", meta: { rule: "exclusion-field", model: model.name } });
|
|
1561
1855
|
}
|
|
1562
|
-
if (!exclusion.name || names.has(exclusion.name) || typeof exclusion.definition !== "string" || !exclusion.definition.startsWith("EXCLUDE USING ") || !Array.isArray(exclusion.requiredExtensions) || exclusion.requiredExtensions.some((
|
|
1856
|
+
if (!exclusion.name || names.has(exclusion.name) || typeof exclusion.definition !== "string" || !exclusion.definition.startsWith("EXCLUDE USING ") || !Array.isArray(exclusion.requiredExtensions) || exclusion.requiredExtensions.some((name) => name !== "btree_gist"))
|
|
1563
1857
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Invalid exclusion declaration", meta: { rule: "exclusion-definition", model: model.name } });
|
|
1564
1858
|
names.add(exclusion.name);
|
|
1565
1859
|
}
|
|
@@ -1581,8 +1875,8 @@ function assertValidSchemaIR(params) {
|
|
|
1581
1875
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `List default on non-list field ${model.name}.${field.name}` });
|
|
1582
1876
|
scalarListDefault({ type: field.type, values: field.default.values });
|
|
1583
1877
|
if (isEnumType(field.type)) {
|
|
1584
|
-
const
|
|
1585
|
-
const members = params.schema.enums.find((entry) => entry.name ===
|
|
1878
|
+
const name = field.type.enum;
|
|
1879
|
+
const members = params.schema.enums.find((entry) => entry.name === name)?.values ?? [];
|
|
1586
1880
|
if (field.default.values.some((value) => typeof value !== "string" || !members.includes(value)))
|
|
1587
1881
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: `Invalid enum-list default on ${model.name}.${field.name}` });
|
|
1588
1882
|
}
|
|
@@ -1673,8 +1967,8 @@ function validateNativeRls(params) {
|
|
|
1673
1967
|
const tables = new Set;
|
|
1674
1968
|
const throughTargets = new Map;
|
|
1675
1969
|
for (const model of schema.models) {
|
|
1676
|
-
const add = (
|
|
1677
|
-
issues.push({ ...
|
|
1970
|
+
const add = (params) => {
|
|
1971
|
+
issues.push({ ...params, model: model.name });
|
|
1678
1972
|
};
|
|
1679
1973
|
const table = model.dbName ?? model.name;
|
|
1680
1974
|
if (tables.has(table))
|
|
@@ -1682,10 +1976,10 @@ function validateNativeRls(params) {
|
|
|
1682
1976
|
tables.add(table);
|
|
1683
1977
|
const columns = new Set;
|
|
1684
1978
|
for (const field of model.fields) {
|
|
1685
|
-
const
|
|
1686
|
-
if (columns.has(
|
|
1687
|
-
add({ rule: "rls-duplicate-column", message: `RLS model ${model.name} repeats physical column ${
|
|
1688
|
-
columns.add(
|
|
1979
|
+
const name = field.dbName ?? field.name;
|
|
1980
|
+
if (columns.has(name))
|
|
1981
|
+
add({ rule: "rls-duplicate-column", message: `RLS model ${model.name} repeats physical column ${name}`, field: field.name });
|
|
1982
|
+
columns.add(name);
|
|
1689
1983
|
}
|
|
1690
1984
|
if (model.policy !== undefined)
|
|
1691
1985
|
add({ rule: "rls-policy-conflict", message: "Native RLS cannot combine with application-level ModelIR.policy" });
|
|
@@ -1738,7 +2032,7 @@ function validateNativeRls(params) {
|
|
|
1738
2032
|
add({ rule: "rls-invalid-through", message: "Native RLS through requires a to-one relation with an actual, complete foreign key on this model" });
|
|
1739
2033
|
continue;
|
|
1740
2034
|
}
|
|
1741
|
-
if (relation.isOptional || relation.fields?.some((
|
|
2035
|
+
if (relation.isOptional || relation.fields?.some((name) => model.fields.find((field) => field.name === name)?.isOptional)) {
|
|
1742
2036
|
add({ rule: "rls-nullable-through", message: "Native RLS through requires a non-nullable parent relation and foreign key; NULL parent links cannot satisfy WITH CHECK. Make the link required or use a direct ownership rule", field: relation.name });
|
|
1743
2037
|
continue;
|
|
1744
2038
|
}
|
|
@@ -1770,8 +2064,8 @@ function validRlsForeignKey(params) {
|
|
|
1770
2064
|
const refs = relation.references;
|
|
1771
2065
|
if (fields === undefined || refs === undefined || fields.length === 0 || fields.length !== refs.length || new Set(fields).size !== fields.length || new Set(refs).size !== refs.length)
|
|
1772
2066
|
return false;
|
|
1773
|
-
const validFields = fields.every((
|
|
1774
|
-
const field = model.fields.find((f) => f.name ===
|
|
2067
|
+
const validFields = fields.every((name, index) => {
|
|
2068
|
+
const field = model.fields.find((f) => f.name === name);
|
|
1775
2069
|
const ref = target.fields.find((f) => f.name === refs[index]);
|
|
1776
2070
|
return field !== undefined && ref !== undefined && !field.isList && !ref.isList && !field.isUnsupported && !ref.isUnsupported && (field.type === ref.type || isEnumType(field.type) && isEnumType(ref.type) && field.type.enum === ref.type.enum);
|
|
1777
2071
|
});
|
|
@@ -1780,76 +2074,72 @@ function validRlsForeignKey(params) {
|
|
|
1780
2074
|
...target.primaryKey === undefined ? [] : [target.primaryKey.fields],
|
|
1781
2075
|
...target.indexes.filter((index) => index.kind === "unique" && index.where === undefined).map((index) => index.fields.map((f) => f.name))
|
|
1782
2076
|
];
|
|
1783
|
-
return validFields && uniqueSets.some((set) => set.length === refs.length && set.every((
|
|
1784
|
-
}
|
|
1785
|
-
// packages/schema/src/m2m.ts
|
|
1786
|
-
function implicitJoinTable(params) {
|
|
1787
|
-
const sorted = [params.modelA, params.modelB].sort();
|
|
1788
|
-
const first = sorted[0];
|
|
1789
|
-
const second = sorted[1];
|
|
1790
|
-
const key = params.relationName ?? `${first}To${second}`;
|
|
1791
|
-
return {
|
|
1792
|
-
tableName: `_${key}`,
|
|
1793
|
-
modelA: first,
|
|
1794
|
-
modelB: second,
|
|
1795
|
-
columnA: "A",
|
|
1796
|
-
columnB: "B"
|
|
1797
|
-
};
|
|
2077
|
+
return validFields && uniqueSets.some((set) => set.length === refs.length && set.every((name) => refs.includes(name)));
|
|
1798
2078
|
}
|
|
1799
2079
|
export {
|
|
1800
|
-
|
|
1801
|
-
validateRlsContextIR,
|
|
1802
|
-
validateComputedConfig,
|
|
1803
|
-
unsupportedCapability,
|
|
1804
|
-
sqlObjectKey,
|
|
1805
|
-
serializeJsonDefault,
|
|
1806
|
-
scalarListDefault,
|
|
1807
|
-
providerToDialect,
|
|
1808
|
-
predicateKey,
|
|
1809
|
-
predicateFieldNames,
|
|
1810
|
-
mergeExtensionDeclarations,
|
|
1811
|
-
isSqlObjectIdentity,
|
|
1812
|
-
isSqlObjectDefinition,
|
|
1813
|
-
isReservedDelegateName,
|
|
1814
|
-
isReservedClientName,
|
|
1815
|
-
isPredicateFieldRef,
|
|
1816
|
-
isImplicitManyToManyRelation,
|
|
1817
|
-
isEnumType,
|
|
1818
|
-
invalidStoredData,
|
|
1819
|
-
internalError,
|
|
1820
|
-
implicitJoinTable,
|
|
1821
|
-
fieldTypeIdentity,
|
|
1822
|
-
extensionOptionsHash,
|
|
1823
|
-
extensionManifestEntry,
|
|
1824
|
-
extensionDeclarations,
|
|
1825
|
-
exactIntegerLiteral,
|
|
1826
|
-
enumStorageOf,
|
|
1827
|
-
encodePrismaString,
|
|
1828
|
-
defineExtension,
|
|
1829
|
-
decodePrismaString,
|
|
1830
|
-
buildExtensionRegistry,
|
|
1831
|
-
buildComputedManifest,
|
|
1832
|
-
assertValidSchemaIR,
|
|
1833
|
-
assertMigrationObjects,
|
|
1834
|
-
VibeError,
|
|
1835
|
-
VIBE_ERROR_CODES,
|
|
1836
|
-
ScopedQueryError,
|
|
1837
|
-
SQLITE_CAPABILITIES,
|
|
1838
|
-
SCOPED_REFUSAL_REASONS,
|
|
1839
|
-
RlsContextError,
|
|
1840
|
-
ReadOnlyClientError,
|
|
1841
|
-
RLS_CONTEXT_REASONS,
|
|
1842
|
-
RESERVED_ENUM_NAMES,
|
|
1843
|
-
RESERVED_DELEGATE_NAMES,
|
|
1844
|
-
RESERVED_CLIENT_NAMES,
|
|
1845
|
-
READ_ONLY_REFUSAL_REASONS,
|
|
1846
|
-
PREDICATE_COMPARISON_OPS,
|
|
1847
|
-
POSTGRES_CAPABILITIES,
|
|
1848
|
-
MYSQL_CAPABILITIES,
|
|
1849
|
-
ENUM_STORAGES,
|
|
1850
|
-
EMPTY_EXTENSION_REGISTRY,
|
|
2080
|
+
DIALECTS,
|
|
1851
2081
|
DIALECT_CAPABILITIES,
|
|
1852
|
-
|
|
2082
|
+
EMPTY_EXTENSION_REGISTRY,
|
|
2083
|
+
ENUM_STORAGES,
|
|
2084
|
+
IDENTIFIER_LIMITS,
|
|
2085
|
+
MYSQL_CAPABILITIES,
|
|
2086
|
+
POSTGRES_CAPABILITIES,
|
|
2087
|
+
PREDICATE_COMPARISON_OPS,
|
|
2088
|
+
READ_ONLY_REFUSAL_REASONS,
|
|
2089
|
+
RESERVED_CLIENT_NAMES,
|
|
2090
|
+
RESERVED_DELEGATE_NAMES,
|
|
2091
|
+
RESERVED_ENUM_NAMES,
|
|
2092
|
+
RLS_CONTEXT_REASONS,
|
|
2093
|
+
ReadOnlyClientError,
|
|
2094
|
+
RlsContextError,
|
|
2095
|
+
SCOPED_REFUSAL_REASONS,
|
|
2096
|
+
SQLITE_CAPABILITIES,
|
|
2097
|
+
ScopedQueryError,
|
|
2098
|
+
VIBE_ERROR_CODES,
|
|
2099
|
+
VibeError,
|
|
2100
|
+
assertIdentifierFits,
|
|
2101
|
+
assertMigrationObjects,
|
|
2102
|
+
assertValidSchemaIR,
|
|
2103
|
+
buildComputedManifest,
|
|
2104
|
+
buildExtensionRegistry,
|
|
2105
|
+
decodePrismaString,
|
|
2106
|
+
defaultForeignKeyName,
|
|
2107
|
+
defaultIndexName,
|
|
2108
|
+
defaultPrimaryKeyName,
|
|
2109
|
+
defineExtension,
|
|
2110
|
+
encodePrismaString,
|
|
2111
|
+
enumStorageOf,
|
|
2112
|
+
exactDecimalLiteral,
|
|
2113
|
+
exactIntegerLiteral,
|
|
2114
|
+
extensionDeclarations,
|
|
2115
|
+
extensionManifestEntry,
|
|
2116
|
+
extensionOptionsHash,
|
|
2117
|
+
fieldTypeIdentity,
|
|
2118
|
+
fitDefaultName,
|
|
2119
|
+
identifierLength,
|
|
2120
|
+
implicitJoinTable,
|
|
2121
|
+
implicitJoinTableObjectNames,
|
|
2122
|
+
internalError,
|
|
2123
|
+
invalidStoredData,
|
|
2124
|
+
isEnumType,
|
|
2125
|
+
isImplicitManyToManyRelation,
|
|
2126
|
+
isPredicateFieldRef,
|
|
2127
|
+
isReservedClientName,
|
|
2128
|
+
isReservedDelegateName,
|
|
2129
|
+
isSqlObjectDefinition,
|
|
2130
|
+
isSqlObjectIdentity,
|
|
2131
|
+
mergeExtensionDeclarations,
|
|
2132
|
+
postgresGeneratedObjectName,
|
|
2133
|
+
predicateFieldNames,
|
|
2134
|
+
predicateKey,
|
|
2135
|
+
providerToDialect,
|
|
2136
|
+
scalarListDefault,
|
|
2137
|
+
serializeJsonDefault,
|
|
2138
|
+
sqlObjectKey,
|
|
2139
|
+
unsupportedCapability,
|
|
2140
|
+
validateComputedConfig,
|
|
2141
|
+
validateRlsContextIR,
|
|
2142
|
+
validateSchemaIR
|
|
1853
2143
|
};
|
|
1854
2144
|
|
|
1855
|
-
//# debugId=
|
|
2145
|
+
//# debugId=47DE6CAFB1D4DC2864756E2164756E21
|