@smartive/graphql-magic 23.6.1 → 23.7.0-next.2
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/CHANGELOG.md +2 -2
- package/dist/bin/gqm.cjs +191 -30
- package/dist/cjs/index.cjs +193 -30
- package/dist/esm/migrations/generate.d.ts +13 -1
- package/dist/esm/migrations/generate.js +191 -31
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +27 -2
- package/dist/esm/models/models.d.ts +1 -5
- package/dist/esm/models/models.js +4 -1
- package/dist/esm/models/models.js.map +1 -1
- package/dist/esm/models/utils.d.ts +10 -0
- package/dist/esm/models/utils.js +11 -0
- package/dist/esm/models/utils.js.map +1 -1
- package/docs/docs/2-models.md +18 -4
- package/docs/docs/5-migrations.md +11 -5
- package/package.json +3 -3
- package/src/bin/gqm/parse-knexfile.ts +1 -0
- package/src/bin/gqm/settings.ts +4 -0
- package/src/migrations/generate.ts +251 -32
- package/src/models/model-definitions.ts +20 -1
- package/src/models/models.ts +4 -1
- package/src/models/utils.ts +20 -0
- package/tests/unit/constraints.spec.ts +98 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
## [23.
|
|
1
|
+
## [23.7.0-next.2](https://github.com/smartive/graphql-magic/compare/v23.7.0-next.1...v23.7.0-next.2) (2026-03-04)
|
|
2
2
|
|
|
3
3
|
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
*
|
|
5
|
+
* gen ([4cf0f15](https://github.com/smartive/graphql-magic/commit/4cf0f159cacc23134bd9f0760b581ac007dc24c8))
|
package/dist/bin/gqm.cjs
CHANGED
|
@@ -389,6 +389,8 @@ var EntityModel = class extends Model {
|
|
|
389
389
|
for (const constraint of this.constraints) {
|
|
390
390
|
if (constraint.kind === "check") {
|
|
391
391
|
validateCheckConstraint(this, constraint);
|
|
392
|
+
} else if (constraint.kind === "exclude") {
|
|
393
|
+
validateExcludeConstraint(this, constraint);
|
|
392
394
|
}
|
|
393
395
|
}
|
|
394
396
|
}
|
|
@@ -644,6 +646,19 @@ var validateCheckConstraint = (model, constraint) => {
|
|
|
644
646
|
}
|
|
645
647
|
}
|
|
646
648
|
};
|
|
649
|
+
var validateExcludeConstraint = (model, constraint) => {
|
|
650
|
+
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
651
|
+
for (const el of constraint.elements) {
|
|
652
|
+
if ("column" in el) {
|
|
653
|
+
if (!validColumnNames.has(el.column)) {
|
|
654
|
+
const validList = [...validColumnNames].sort().join(", ");
|
|
655
|
+
throw new Error(
|
|
656
|
+
`Exclude constraint "${constraint.name}" references column "${el.column}" which does not exist on model ${model.name}. Valid columns: ${validList}`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
};
|
|
647
662
|
|
|
648
663
|
// src/db/generate.ts
|
|
649
664
|
var import_code_block_writer = __toESM(require("code-block-writer"), 1);
|
|
@@ -1084,6 +1099,10 @@ var MigrationGenerator = class {
|
|
|
1084
1099
|
columns = {};
|
|
1085
1100
|
/** table name -> constraint name -> check clause expression */
|
|
1086
1101
|
existingCheckConstraints = {};
|
|
1102
|
+
/** table name -> constraint name -> exclude definition (normalized) */
|
|
1103
|
+
existingExcludeConstraints = {};
|
|
1104
|
+
/** table name -> constraint name -> trigger definition (normalized) */
|
|
1105
|
+
existingConstraintTriggers = {};
|
|
1087
1106
|
uuidUsed;
|
|
1088
1107
|
nowUsed;
|
|
1089
1108
|
needsMigration = false;
|
|
@@ -1111,8 +1130,46 @@ var MigrationGenerator = class {
|
|
|
1111
1130
|
}
|
|
1112
1131
|
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
1113
1132
|
}
|
|
1133
|
+
const excludeResult = await schema.knex.raw(
|
|
1134
|
+
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_constraintdef(c.oid) as constraint_def
|
|
1135
|
+
FROM pg_constraint c
|
|
1136
|
+
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
1137
|
+
WHERE n.nspname = 'public' AND c.contype = 'x'`
|
|
1138
|
+
);
|
|
1139
|
+
const excludeRows = "rows" in excludeResult && Array.isArray(excludeResult.rows) ? excludeResult.rows : [];
|
|
1140
|
+
for (const row of excludeRows) {
|
|
1141
|
+
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
1142
|
+
if (!this.existingExcludeConstraints[tableName]) {
|
|
1143
|
+
this.existingExcludeConstraints[tableName] = /* @__PURE__ */ new Map();
|
|
1144
|
+
}
|
|
1145
|
+
this.existingExcludeConstraints[tableName].set(row.constraint_name, this.normalizeExcludeDef(row.constraint_def));
|
|
1146
|
+
}
|
|
1147
|
+
const triggerResult = await schema.knex.raw(
|
|
1148
|
+
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
|
|
1149
|
+
FROM pg_constraint c
|
|
1150
|
+
JOIN pg_trigger t ON t.tgconstraint = c.oid
|
|
1151
|
+
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
1152
|
+
WHERE n.nspname = 'public' AND c.contype = 't'`
|
|
1153
|
+
);
|
|
1154
|
+
const triggerRows = "rows" in triggerResult && Array.isArray(triggerResult.rows) ? triggerResult.rows : [];
|
|
1155
|
+
for (const row of triggerRows) {
|
|
1156
|
+
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
1157
|
+
if (!this.existingConstraintTriggers[tableName]) {
|
|
1158
|
+
this.existingConstraintTriggers[tableName] = /* @__PURE__ */ new Map();
|
|
1159
|
+
}
|
|
1160
|
+
this.existingConstraintTriggers[tableName].set(row.constraint_name, this.normalizeTriggerDef(row.trigger_def));
|
|
1161
|
+
}
|
|
1114
1162
|
const up = [];
|
|
1115
1163
|
const down = [];
|
|
1164
|
+
const needsBtreeGist = models.entities.some(
|
|
1165
|
+
(model) => model.constraints?.some((c) => c.kind === "exclude" && c.elements.some((el) => "column" in el && el.operator === "="))
|
|
1166
|
+
);
|
|
1167
|
+
if (needsBtreeGist) {
|
|
1168
|
+
up.unshift(() => {
|
|
1169
|
+
this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
|
|
1170
|
+
this.writer.blankLine();
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1116
1173
|
this.createEnums(
|
|
1117
1174
|
this.models.enums.filter((enm2) => !enums.includes((0, import_lowerFirst.default)(enm2.name))),
|
|
1118
1175
|
up,
|
|
@@ -1191,10 +1248,22 @@ var MigrationGenerator = class {
|
|
|
1191
1248
|
if (entry.kind === "check") {
|
|
1192
1249
|
validateCheckConstraint(model, entry);
|
|
1193
1250
|
const table = model.name;
|
|
1194
|
-
const constraintName = this.
|
|
1195
|
-
|
|
1251
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
1252
|
+
up.push(() => {
|
|
1253
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1254
|
+
});
|
|
1255
|
+
} else if (entry.kind === "exclude") {
|
|
1256
|
+
validateExcludeConstraint(model, entry);
|
|
1257
|
+
const table = model.name;
|
|
1258
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
1259
|
+
up.push(() => {
|
|
1260
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
1261
|
+
});
|
|
1262
|
+
} else if (entry.kind === "constraint_trigger") {
|
|
1263
|
+
const table = model.name;
|
|
1264
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
1196
1265
|
up.push(() => {
|
|
1197
|
-
this.
|
|
1266
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
1198
1267
|
});
|
|
1199
1268
|
}
|
|
1200
1269
|
}
|
|
@@ -1234,33 +1303,81 @@ var MigrationGenerator = class {
|
|
|
1234
1303
|
);
|
|
1235
1304
|
this.updateFields(model, existingFields, up, down);
|
|
1236
1305
|
if (model.constraints?.length) {
|
|
1237
|
-
const
|
|
1306
|
+
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
1307
|
+
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
1308
|
+
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
1238
1309
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
1239
1310
|
const entry = model.constraints[i];
|
|
1240
|
-
if (entry.kind !== "check") {
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
validateCheckConstraint(model, entry);
|
|
1244
1311
|
const table = model.name;
|
|
1245
|
-
const constraintName = this.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1312
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
1313
|
+
if (entry.kind === "check") {
|
|
1314
|
+
validateCheckConstraint(model, entry);
|
|
1315
|
+
const newExpression = entry.expression;
|
|
1316
|
+
const existingExpression = existingCheckMap?.get(constraintName);
|
|
1317
|
+
if (existingExpression === void 0) {
|
|
1318
|
+
up.push(() => {
|
|
1319
|
+
this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
|
|
1320
|
+
});
|
|
1321
|
+
down.push(() => {
|
|
1322
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1323
|
+
});
|
|
1324
|
+
} else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
|
|
1325
|
+
up.push(() => {
|
|
1326
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1327
|
+
this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
|
|
1328
|
+
});
|
|
1329
|
+
down.push(() => {
|
|
1330
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1331
|
+
this.addCheckConstraint(table, constraintName, existingExpression);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
} else if (entry.kind === "exclude") {
|
|
1335
|
+
validateExcludeConstraint(model, entry);
|
|
1336
|
+
const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
|
|
1337
|
+
const existingDef = existingExcludeMap?.get(constraintName);
|
|
1338
|
+
if (existingDef === void 0) {
|
|
1339
|
+
up.push(() => {
|
|
1340
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
1341
|
+
});
|
|
1342
|
+
down.push(() => {
|
|
1343
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
1344
|
+
});
|
|
1345
|
+
} else if (existingDef !== newDef) {
|
|
1346
|
+
up.push(() => {
|
|
1347
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
1348
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
1349
|
+
});
|
|
1350
|
+
down.push(() => {
|
|
1351
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
1352
|
+
const escaped = this.escapeExpressionForRaw(existingDef);
|
|
1353
|
+
this.writer.writeLine(
|
|
1354
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`
|
|
1355
|
+
);
|
|
1356
|
+
this.writer.blankLine();
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
} else if (entry.kind === "constraint_trigger") {
|
|
1360
|
+
const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
|
|
1361
|
+
const existingDef = existingTriggerMap?.get(constraintName);
|
|
1362
|
+
if (existingDef === void 0) {
|
|
1363
|
+
up.push(() => {
|
|
1364
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
1365
|
+
});
|
|
1366
|
+
down.push(() => {
|
|
1367
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
1368
|
+
});
|
|
1369
|
+
} else if (existingDef !== newDef) {
|
|
1370
|
+
up.push(() => {
|
|
1371
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
1372
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
1373
|
+
});
|
|
1374
|
+
down.push(() => {
|
|
1375
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
1376
|
+
const escaped = existingDef.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1377
|
+
this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
|
|
1378
|
+
this.writer.blankLine();
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1264
1381
|
}
|
|
1265
1382
|
}
|
|
1266
1383
|
}
|
|
@@ -1635,20 +1752,27 @@ var MigrationGenerator = class {
|
|
|
1635
1752
|
renameColumn(from, to) {
|
|
1636
1753
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
1637
1754
|
}
|
|
1638
|
-
|
|
1755
|
+
getConstraintName(model, entry, index) {
|
|
1639
1756
|
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
1640
1757
|
}
|
|
1641
1758
|
normalizeCheckExpression(expr) {
|
|
1642
1759
|
return expr.replace(/\s+/g, " ").trim();
|
|
1643
1760
|
}
|
|
1761
|
+
normalizeExcludeDef(def) {
|
|
1762
|
+
return def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
|
|
1763
|
+
}
|
|
1764
|
+
normalizeTriggerDef(def) {
|
|
1765
|
+
return def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
|
|
1766
|
+
}
|
|
1644
1767
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
1645
1768
|
escapeExpressionForRaw(expr) {
|
|
1646
1769
|
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1647
1770
|
}
|
|
1648
|
-
addCheckConstraint(table, constraintName, expression) {
|
|
1771
|
+
addCheckConstraint(table, constraintName, expression, deferrable) {
|
|
1649
1772
|
const escaped = this.escapeExpressionForRaw(expression);
|
|
1773
|
+
const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : "";
|
|
1650
1774
|
this.writer.writeLine(
|
|
1651
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
1775
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`
|
|
1652
1776
|
);
|
|
1653
1777
|
this.writer.blankLine();
|
|
1654
1778
|
}
|
|
@@ -1656,6 +1780,43 @@ var MigrationGenerator = class {
|
|
|
1656
1780
|
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1657
1781
|
this.writer.blankLine();
|
|
1658
1782
|
}
|
|
1783
|
+
buildExcludeDef(entry) {
|
|
1784
|
+
const elementsStr = entry.elements.map((el) => "column" in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`).join(", ");
|
|
1785
|
+
const whereClause = entry.where ? ` WHERE (${entry.where})` : "";
|
|
1786
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1787
|
+
return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
|
|
1788
|
+
}
|
|
1789
|
+
addExcludeConstraint(table, constraintName, entry) {
|
|
1790
|
+
const def = this.buildExcludeDef(entry);
|
|
1791
|
+
const escaped = this.escapeExpressionForRaw(def);
|
|
1792
|
+
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
1793
|
+
this.writer.blankLine();
|
|
1794
|
+
}
|
|
1795
|
+
dropExcludeConstraint(table, constraintName) {
|
|
1796
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1797
|
+
this.writer.blankLine();
|
|
1798
|
+
}
|
|
1799
|
+
buildConstraintTriggerDef(table, constraintName, entry) {
|
|
1800
|
+
const eventsStr = entry.events.join(" OR ");
|
|
1801
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1802
|
+
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
1803
|
+
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
1804
|
+
return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
|
|
1805
|
+
}
|
|
1806
|
+
addConstraintTrigger(table, constraintName, entry) {
|
|
1807
|
+
const eventsStr = entry.events.join(" OR ");
|
|
1808
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
1809
|
+
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
1810
|
+
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
1811
|
+
this.writer.writeLine(
|
|
1812
|
+
`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`
|
|
1813
|
+
);
|
|
1814
|
+
this.writer.blankLine();
|
|
1815
|
+
}
|
|
1816
|
+
dropConstraintTrigger(table, constraintName) {
|
|
1817
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1818
|
+
this.writer.blankLine();
|
|
1819
|
+
}
|
|
1659
1820
|
value(value2) {
|
|
1660
1821
|
if (typeof value2 === "string") {
|
|
1661
1822
|
return `'${value2}'`;
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -198,6 +198,7 @@ __export(index_exports, {
|
|
|
198
198
|
updateEntity: () => updateEntity,
|
|
199
199
|
updateFunctions: () => updateFunctions,
|
|
200
200
|
validateCheckConstraint: () => validateCheckConstraint,
|
|
201
|
+
validateExcludeConstraint: () => validateExcludeConstraint,
|
|
201
202
|
value: () => value
|
|
202
203
|
});
|
|
203
204
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -654,6 +655,8 @@ var EntityModel = class extends Model {
|
|
|
654
655
|
for (const constraint of this.constraints) {
|
|
655
656
|
if (constraint.kind === "check") {
|
|
656
657
|
validateCheckConstraint(this, constraint);
|
|
658
|
+
} else if (constraint.kind === "exclude") {
|
|
659
|
+
validateExcludeConstraint(this, constraint);
|
|
657
660
|
}
|
|
658
661
|
}
|
|
659
662
|
}
|
|
@@ -952,6 +955,19 @@ var validateCheckConstraint = (model, constraint) => {
|
|
|
952
955
|
}
|
|
953
956
|
}
|
|
954
957
|
};
|
|
958
|
+
var validateExcludeConstraint = (model, constraint) => {
|
|
959
|
+
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
960
|
+
for (const el of constraint.elements) {
|
|
961
|
+
if ("column" in el) {
|
|
962
|
+
if (!validColumnNames.has(el.column)) {
|
|
963
|
+
const validList = [...validColumnNames].sort().join(", ");
|
|
964
|
+
throw new Error(
|
|
965
|
+
`Exclude constraint "${constraint.name}" references column "${el.column}" which does not exist on model ${model.name}. Valid columns: ${validList}`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
955
971
|
|
|
956
972
|
// src/client/queries.ts
|
|
957
973
|
var fieldIsSearchable = (model, fieldName) => {
|
|
@@ -3098,6 +3114,10 @@ var MigrationGenerator = class {
|
|
|
3098
3114
|
columns = {};
|
|
3099
3115
|
/** table name -> constraint name -> check clause expression */
|
|
3100
3116
|
existingCheckConstraints = {};
|
|
3117
|
+
/** table name -> constraint name -> exclude definition (normalized) */
|
|
3118
|
+
existingExcludeConstraints = {};
|
|
3119
|
+
/** table name -> constraint name -> trigger definition (normalized) */
|
|
3120
|
+
existingConstraintTriggers = {};
|
|
3101
3121
|
uuidUsed;
|
|
3102
3122
|
nowUsed;
|
|
3103
3123
|
needsMigration = false;
|
|
@@ -3125,8 +3145,46 @@ var MigrationGenerator = class {
|
|
|
3125
3145
|
}
|
|
3126
3146
|
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
3127
3147
|
}
|
|
3148
|
+
const excludeResult = await schema.knex.raw(
|
|
3149
|
+
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_constraintdef(c.oid) as constraint_def
|
|
3150
|
+
FROM pg_constraint c
|
|
3151
|
+
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
3152
|
+
WHERE n.nspname = 'public' AND c.contype = 'x'`
|
|
3153
|
+
);
|
|
3154
|
+
const excludeRows = "rows" in excludeResult && Array.isArray(excludeResult.rows) ? excludeResult.rows : [];
|
|
3155
|
+
for (const row of excludeRows) {
|
|
3156
|
+
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
3157
|
+
if (!this.existingExcludeConstraints[tableName]) {
|
|
3158
|
+
this.existingExcludeConstraints[tableName] = /* @__PURE__ */ new Map();
|
|
3159
|
+
}
|
|
3160
|
+
this.existingExcludeConstraints[tableName].set(row.constraint_name, this.normalizeExcludeDef(row.constraint_def));
|
|
3161
|
+
}
|
|
3162
|
+
const triggerResult = await schema.knex.raw(
|
|
3163
|
+
`SELECT c.conrelid::regclass::text as table_name, c.conname as constraint_name, pg_get_triggerdef(t.oid) as trigger_def
|
|
3164
|
+
FROM pg_constraint c
|
|
3165
|
+
JOIN pg_trigger t ON t.tgconstraint = c.oid
|
|
3166
|
+
JOIN pg_namespace n ON c.connamespace = n.oid
|
|
3167
|
+
WHERE n.nspname = 'public' AND c.contype = 't'`
|
|
3168
|
+
);
|
|
3169
|
+
const triggerRows = "rows" in triggerResult && Array.isArray(triggerResult.rows) ? triggerResult.rows : [];
|
|
3170
|
+
for (const row of triggerRows) {
|
|
3171
|
+
const tableName = row.table_name.split(".").pop()?.replace(/^"|"$/g, "") ?? row.table_name;
|
|
3172
|
+
if (!this.existingConstraintTriggers[tableName]) {
|
|
3173
|
+
this.existingConstraintTriggers[tableName] = /* @__PURE__ */ new Map();
|
|
3174
|
+
}
|
|
3175
|
+
this.existingConstraintTriggers[tableName].set(row.constraint_name, this.normalizeTriggerDef(row.trigger_def));
|
|
3176
|
+
}
|
|
3128
3177
|
const up = [];
|
|
3129
3178
|
const down = [];
|
|
3179
|
+
const needsBtreeGist = models.entities.some(
|
|
3180
|
+
(model) => model.constraints?.some((c) => c.kind === "exclude" && c.elements.some((el) => "column" in el && el.operator === "="))
|
|
3181
|
+
);
|
|
3182
|
+
if (needsBtreeGist) {
|
|
3183
|
+
up.unshift(() => {
|
|
3184
|
+
this.writer.writeLine(`await knex.raw('CREATE EXTENSION IF NOT EXISTS btree_gist');`);
|
|
3185
|
+
this.writer.blankLine();
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3130
3188
|
this.createEnums(
|
|
3131
3189
|
this.models.enums.filter((enm2) => !enums.includes((0, import_lowerFirst.default)(enm2.name))),
|
|
3132
3190
|
up,
|
|
@@ -3205,10 +3263,22 @@ var MigrationGenerator = class {
|
|
|
3205
3263
|
if (entry.kind === "check") {
|
|
3206
3264
|
validateCheckConstraint(model, entry);
|
|
3207
3265
|
const table = model.name;
|
|
3208
|
-
const constraintName = this.
|
|
3209
|
-
const expression = entry.expression;
|
|
3266
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
3210
3267
|
up.push(() => {
|
|
3211
|
-
this.addCheckConstraint(table, constraintName, expression);
|
|
3268
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
3269
|
+
});
|
|
3270
|
+
} else if (entry.kind === "exclude") {
|
|
3271
|
+
validateExcludeConstraint(model, entry);
|
|
3272
|
+
const table = model.name;
|
|
3273
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
3274
|
+
up.push(() => {
|
|
3275
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
3276
|
+
});
|
|
3277
|
+
} else if (entry.kind === "constraint_trigger") {
|
|
3278
|
+
const table = model.name;
|
|
3279
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
3280
|
+
up.push(() => {
|
|
3281
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
3212
3282
|
});
|
|
3213
3283
|
}
|
|
3214
3284
|
}
|
|
@@ -3248,33 +3318,81 @@ var MigrationGenerator = class {
|
|
|
3248
3318
|
);
|
|
3249
3319
|
this.updateFields(model, existingFields, up, down);
|
|
3250
3320
|
if (model.constraints?.length) {
|
|
3251
|
-
const
|
|
3321
|
+
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
3322
|
+
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
3323
|
+
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
3252
3324
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
3253
3325
|
const entry = model.constraints[i];
|
|
3254
|
-
if (entry.kind !== "check") {
|
|
3255
|
-
continue;
|
|
3256
|
-
}
|
|
3257
|
-
validateCheckConstraint(model, entry);
|
|
3258
3326
|
const table = model.name;
|
|
3259
|
-
const constraintName = this.
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3327
|
+
const constraintName = this.getConstraintName(model, entry, i);
|
|
3328
|
+
if (entry.kind === "check") {
|
|
3329
|
+
validateCheckConstraint(model, entry);
|
|
3330
|
+
const newExpression = entry.expression;
|
|
3331
|
+
const existingExpression = existingCheckMap?.get(constraintName);
|
|
3332
|
+
if (existingExpression === void 0) {
|
|
3333
|
+
up.push(() => {
|
|
3334
|
+
this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
|
|
3335
|
+
});
|
|
3336
|
+
down.push(() => {
|
|
3337
|
+
this.dropCheckConstraint(table, constraintName);
|
|
3338
|
+
});
|
|
3339
|
+
} else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
|
|
3340
|
+
up.push(() => {
|
|
3341
|
+
this.dropCheckConstraint(table, constraintName);
|
|
3342
|
+
this.addCheckConstraint(table, constraintName, newExpression, entry.deferrable);
|
|
3343
|
+
});
|
|
3344
|
+
down.push(() => {
|
|
3345
|
+
this.dropCheckConstraint(table, constraintName);
|
|
3346
|
+
this.addCheckConstraint(table, constraintName, existingExpression);
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
} else if (entry.kind === "exclude") {
|
|
3350
|
+
validateExcludeConstraint(model, entry);
|
|
3351
|
+
const newDef = this.normalizeExcludeDef(this.buildExcludeDef(entry));
|
|
3352
|
+
const existingDef = existingExcludeMap?.get(constraintName);
|
|
3353
|
+
if (existingDef === void 0) {
|
|
3354
|
+
up.push(() => {
|
|
3355
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
3356
|
+
});
|
|
3357
|
+
down.push(() => {
|
|
3358
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
3359
|
+
});
|
|
3360
|
+
} else if (existingDef !== newDef) {
|
|
3361
|
+
up.push(() => {
|
|
3362
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
3363
|
+
this.addExcludeConstraint(table, constraintName, entry);
|
|
3364
|
+
});
|
|
3365
|
+
down.push(() => {
|
|
3366
|
+
this.dropExcludeConstraint(table, constraintName);
|
|
3367
|
+
const escaped = this.escapeExpressionForRaw(existingDef);
|
|
3368
|
+
this.writer.writeLine(
|
|
3369
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`
|
|
3370
|
+
);
|
|
3371
|
+
this.writer.blankLine();
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
} else if (entry.kind === "constraint_trigger") {
|
|
3375
|
+
const newDef = this.normalizeTriggerDef(this.buildConstraintTriggerDef(table, constraintName, entry));
|
|
3376
|
+
const existingDef = existingTriggerMap?.get(constraintName);
|
|
3377
|
+
if (existingDef === void 0) {
|
|
3378
|
+
up.push(() => {
|
|
3379
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
3380
|
+
});
|
|
3381
|
+
down.push(() => {
|
|
3382
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
3383
|
+
});
|
|
3384
|
+
} else if (existingDef !== newDef) {
|
|
3385
|
+
up.push(() => {
|
|
3386
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
3387
|
+
this.addConstraintTrigger(table, constraintName, entry);
|
|
3388
|
+
});
|
|
3389
|
+
down.push(() => {
|
|
3390
|
+
this.dropConstraintTrigger(table, constraintName);
|
|
3391
|
+
const escaped = existingDef.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
3392
|
+
this.writer.writeLine(`await knex.raw(\`${escaped}\`);`);
|
|
3393
|
+
this.writer.blankLine();
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3278
3396
|
}
|
|
3279
3397
|
}
|
|
3280
3398
|
}
|
|
@@ -3649,20 +3767,27 @@ var MigrationGenerator = class {
|
|
|
3649
3767
|
renameColumn(from, to) {
|
|
3650
3768
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
3651
3769
|
}
|
|
3652
|
-
|
|
3770
|
+
getConstraintName(model, entry, index) {
|
|
3653
3771
|
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
3654
3772
|
}
|
|
3655
3773
|
normalizeCheckExpression(expr) {
|
|
3656
3774
|
return expr.replace(/\s+/g, " ").trim();
|
|
3657
3775
|
}
|
|
3776
|
+
normalizeExcludeDef(def) {
|
|
3777
|
+
return def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
|
|
3778
|
+
}
|
|
3779
|
+
normalizeTriggerDef(def) {
|
|
3780
|
+
return def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
|
|
3781
|
+
}
|
|
3658
3782
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
3659
3783
|
escapeExpressionForRaw(expr) {
|
|
3660
3784
|
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
3661
3785
|
}
|
|
3662
|
-
addCheckConstraint(table, constraintName, expression) {
|
|
3786
|
+
addCheckConstraint(table, constraintName, expression, deferrable) {
|
|
3663
3787
|
const escaped = this.escapeExpressionForRaw(expression);
|
|
3788
|
+
const deferrableClause = deferrable ? ` DEFERRABLE ${deferrable}` : "";
|
|
3664
3789
|
this.writer.writeLine(
|
|
3665
|
-
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
3790
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})${deferrableClause}\`);`
|
|
3666
3791
|
);
|
|
3667
3792
|
this.writer.blankLine();
|
|
3668
3793
|
}
|
|
@@ -3670,6 +3795,43 @@ var MigrationGenerator = class {
|
|
|
3670
3795
|
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
3671
3796
|
this.writer.blankLine();
|
|
3672
3797
|
}
|
|
3798
|
+
buildExcludeDef(entry) {
|
|
3799
|
+
const elementsStr = entry.elements.map((el) => "column" in el ? `"${el.column}" WITH ${el.operator}` : `${el.expression} WITH ${el.operator}`).join(", ");
|
|
3800
|
+
const whereClause = entry.where ? ` WHERE (${entry.where})` : "";
|
|
3801
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
3802
|
+
return `EXCLUDE USING ${entry.using} (${elementsStr})${whereClause}${deferrableClause}`;
|
|
3803
|
+
}
|
|
3804
|
+
addExcludeConstraint(table, constraintName, entry) {
|
|
3805
|
+
const def = this.buildExcludeDef(entry);
|
|
3806
|
+
const escaped = this.escapeExpressionForRaw(def);
|
|
3807
|
+
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" ${escaped}\`);`);
|
|
3808
|
+
this.writer.blankLine();
|
|
3809
|
+
}
|
|
3810
|
+
dropExcludeConstraint(table, constraintName) {
|
|
3811
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
3812
|
+
this.writer.blankLine();
|
|
3813
|
+
}
|
|
3814
|
+
buildConstraintTriggerDef(table, constraintName, entry) {
|
|
3815
|
+
const eventsStr = entry.events.join(" OR ");
|
|
3816
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
3817
|
+
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
3818
|
+
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
3819
|
+
return `CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}`;
|
|
3820
|
+
}
|
|
3821
|
+
addConstraintTrigger(table, constraintName, entry) {
|
|
3822
|
+
const eventsStr = entry.events.join(" OR ");
|
|
3823
|
+
const deferrableClause = entry.deferrable ? ` DEFERRABLE ${entry.deferrable}` : "";
|
|
3824
|
+
const argsStr = entry.function.args?.length ? entry.function.args.map((a) => `"${a}"`).join(", ") : "";
|
|
3825
|
+
const executeClause = argsStr ? `EXECUTE FUNCTION ${entry.function.name}(${argsStr})` : `EXECUTE FUNCTION ${entry.function.name}()`;
|
|
3826
|
+
this.writer.writeLine(
|
|
3827
|
+
`await knex.raw(\`CREATE CONSTRAINT TRIGGER "${constraintName}" ${entry.when} ${eventsStr} ON "${table}"${deferrableClause} FOR EACH ${entry.forEach} ${executeClause}\`);`
|
|
3828
|
+
);
|
|
3829
|
+
this.writer.blankLine();
|
|
3830
|
+
}
|
|
3831
|
+
dropConstraintTrigger(table, constraintName) {
|
|
3832
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
3833
|
+
this.writer.blankLine();
|
|
3834
|
+
}
|
|
3673
3835
|
value(value2) {
|
|
3674
3836
|
if (typeof value2 === "string") {
|
|
3675
3837
|
return `'${value2}'`;
|
|
@@ -4696,5 +4858,6 @@ var printSchemaFromModels = (models) => printSchema((0, import_graphql6.buildAST
|
|
|
4696
4858
|
updateEntity,
|
|
4697
4859
|
updateFunctions,
|
|
4698
4860
|
validateCheckConstraint,
|
|
4861
|
+
validateExcludeConstraint,
|
|
4699
4862
|
value
|
|
4700
4863
|
});
|
|
@@ -9,6 +9,10 @@ export declare class MigrationGenerator {
|
|
|
9
9
|
private columns;
|
|
10
10
|
/** table name -> constraint name -> check clause expression */
|
|
11
11
|
private existingCheckConstraints;
|
|
12
|
+
/** table name -> constraint name -> exclude definition (normalized) */
|
|
13
|
+
private existingExcludeConstraints;
|
|
14
|
+
/** table name -> constraint name -> trigger definition (normalized) */
|
|
15
|
+
private existingConstraintTriggers;
|
|
12
16
|
private uuidUsed?;
|
|
13
17
|
private nowUsed?;
|
|
14
18
|
needsMigration: boolean;
|
|
@@ -30,12 +34,20 @@ export declare class MigrationGenerator {
|
|
|
30
34
|
private dropTable;
|
|
31
35
|
private renameTable;
|
|
32
36
|
private renameColumn;
|
|
33
|
-
private
|
|
37
|
+
private getConstraintName;
|
|
34
38
|
private normalizeCheckExpression;
|
|
39
|
+
private normalizeExcludeDef;
|
|
40
|
+
private normalizeTriggerDef;
|
|
35
41
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
36
42
|
private escapeExpressionForRaw;
|
|
37
43
|
private addCheckConstraint;
|
|
38
44
|
private dropCheckConstraint;
|
|
45
|
+
private buildExcludeDef;
|
|
46
|
+
private addExcludeConstraint;
|
|
47
|
+
private dropExcludeConstraint;
|
|
48
|
+
private buildConstraintTriggerDef;
|
|
49
|
+
private addConstraintTrigger;
|
|
50
|
+
private dropConstraintTrigger;
|
|
39
51
|
private value;
|
|
40
52
|
private columnRaw;
|
|
41
53
|
private column;
|