@smartive/graphql-magic 23.5.0 → 23.6.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/.github/workflows/docs.yml +3 -3
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/testing.yml +1 -1
- package/CHANGELOG.md +3 -3
- package/dist/bin/gqm.cjs +117 -0
- package/dist/cjs/index.cjs +122 -1
- package/dist/esm/migrations/generate.d.ts +8 -0
- package/dist/esm/migrations/generate.js +83 -1
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +5 -0
- package/dist/esm/models/models.d.ts +5 -0
- package/dist/esm/models/models.js +9 -2
- package/dist/esm/models/models.js.map +1 -1
- package/dist/esm/models/utils.d.ts +14 -0
- package/dist/esm/models/utils.js +33 -0
- package/dist/esm/models/utils.js.map +1 -1
- package/dist/esm/resolvers/arguments.js +1 -1
- package/dist/esm/resolvers/arguments.js.map +1 -1
- package/docker-compose.yml +1 -1
- package/docs/docs/2-models.md +28 -0
- package/docs/docs/5-migrations.md +9 -0
- package/eslint.config.mjs +4 -1
- package/package.json +2 -1
- package/renovate.json +1 -29
- package/src/migrations/generate.ts +98 -0
- package/src/models/model-definitions.ts +2 -0
- package/src/models/models.ts +18 -2
- package/src/models/utils.ts +38 -1
- package/src/resolvers/arguments.ts +1 -1
- package/tests/unit/constraints.spec.ts +83 -0
|
@@ -8,12 +8,12 @@ jobs:
|
|
|
8
8
|
docs:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@v6
|
|
11
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
12
12
|
with:
|
|
13
13
|
persist-credentials: false
|
|
14
14
|
|
|
15
15
|
- name: Setup Node
|
|
16
|
-
uses: actions/setup-node@v6
|
|
16
|
+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
|
17
17
|
with:
|
|
18
18
|
node-version: '24'
|
|
19
19
|
- name: Install and Build
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
npm install
|
|
23
23
|
npm run build
|
|
24
24
|
- name: Deploy
|
|
25
|
-
uses: JamesIves/github-pages-deploy-action@v4
|
|
25
|
+
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4
|
|
26
26
|
with:
|
|
27
27
|
branch: gh-pages
|
|
28
28
|
folder: docs/build
|
|
@@ -19,10 +19,10 @@ jobs:
|
|
|
19
19
|
name: build and release
|
|
20
20
|
runs-on: ubuntu-latest
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@v6
|
|
22
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
23
23
|
with:
|
|
24
24
|
submodules: true
|
|
25
|
-
- uses: actions/setup-node@v6
|
|
25
|
+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
|
26
26
|
with:
|
|
27
27
|
node-version: 24
|
|
28
28
|
registry-url: 'https://registry.npmjs.org'
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
## [23.
|
|
1
|
+
## [23.6.0-next.2](https://github.com/smartive/graphql-magic/compare/v23.6.0-next.1...v23.6.0-next.2) (2026-02-23)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
*
|
|
5
|
+
* Simplify type checking in normalizeValueByTypeDefinition function ([0a06610](https://github.com/smartive/graphql-magic/commit/0a066109c83114b4b0c06afd8531addb4fbbac17))
|
package/dist/bin/gqm.cjs
CHANGED
|
@@ -357,6 +357,7 @@ var EntityModel = class extends Model {
|
|
|
357
357
|
displayField;
|
|
358
358
|
defaultOrderBy;
|
|
359
359
|
fields;
|
|
360
|
+
constraints;
|
|
360
361
|
// temporary fields for the generation of migrations
|
|
361
362
|
deleted;
|
|
362
363
|
oldName;
|
|
@@ -384,6 +385,13 @@ var EntityModel = class extends Model {
|
|
|
384
385
|
for (const field of definition.fields) {
|
|
385
386
|
this.fieldsByName[field.name] = field;
|
|
386
387
|
}
|
|
388
|
+
if (this.constraints?.length) {
|
|
389
|
+
for (const constraint of this.constraints) {
|
|
390
|
+
if (constraint.kind === "check") {
|
|
391
|
+
validateCheckConstraint(this, constraint);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
387
395
|
}
|
|
388
396
|
getField(name2) {
|
|
389
397
|
return get(this.fieldsByName, name2);
|
|
@@ -607,6 +615,31 @@ var isManyToManyRelationEntityModel = ({
|
|
|
607
615
|
}
|
|
608
616
|
return manyToManyRelation;
|
|
609
617
|
};
|
|
618
|
+
var extractColumnReferencesFromCheckExpression = (expression) => {
|
|
619
|
+
const normalized = expression.replace(/\s+/g, " ").trim();
|
|
620
|
+
const quotedIdentifiers = [];
|
|
621
|
+
const re = /"([^"]+)"/g;
|
|
622
|
+
let match;
|
|
623
|
+
while ((match = re.exec(normalized)) !== null) {
|
|
624
|
+
quotedIdentifiers.push(match[1]);
|
|
625
|
+
}
|
|
626
|
+
return [...new Set(quotedIdentifiers)];
|
|
627
|
+
};
|
|
628
|
+
var validateCheckConstraint = (model, constraint) => {
|
|
629
|
+
const identifiers = extractColumnReferencesFromCheckExpression(constraint.expression);
|
|
630
|
+
if (identifiers.length === 0) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
634
|
+
for (const identifier of identifiers) {
|
|
635
|
+
if (!validColumnNames.has(identifier)) {
|
|
636
|
+
const validList = [...validColumnNames].sort().join(", ");
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Constraint "${constraint.name}" references column "${identifier}" which does not exist on model ${model.name}. Valid columns: ${validList}`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
610
643
|
|
|
611
644
|
// src/db/generate.ts
|
|
612
645
|
var import_code_block_writer = __toESM(require("code-block-writer"), 1);
|
|
@@ -976,6 +1009,8 @@ var MigrationGenerator = class {
|
|
|
976
1009
|
});
|
|
977
1010
|
schema;
|
|
978
1011
|
columns = {};
|
|
1012
|
+
/** table name -> constraint name -> check clause expression */
|
|
1013
|
+
existingCheckConstraints = {};
|
|
979
1014
|
uuidUsed;
|
|
980
1015
|
nowUsed;
|
|
981
1016
|
needsMigration = false;
|
|
@@ -987,6 +1022,22 @@ var MigrationGenerator = class {
|
|
|
987
1022
|
for (const table of tables) {
|
|
988
1023
|
this.columns[table] = await schema.columnInfo(table);
|
|
989
1024
|
}
|
|
1025
|
+
const checkResult = await schema.knex.raw(
|
|
1026
|
+
`SELECT tc.table_name, tc.constraint_name, cc.check_clause
|
|
1027
|
+
FROM information_schema.table_constraints tc
|
|
1028
|
+
JOIN information_schema.check_constraints cc
|
|
1029
|
+
ON tc.constraint_schema = cc.constraint_schema AND tc.constraint_name = cc.constraint_name
|
|
1030
|
+
WHERE tc.table_schema = ? AND tc.constraint_type = ?`,
|
|
1031
|
+
["public", "CHECK"]
|
|
1032
|
+
);
|
|
1033
|
+
const rows = "rows" in checkResult && Array.isArray(checkResult.rows) ? checkResult.rows : [];
|
|
1034
|
+
for (const row of rows) {
|
|
1035
|
+
const tableName = row.table_name;
|
|
1036
|
+
if (!this.existingCheckConstraints[tableName]) {
|
|
1037
|
+
this.existingCheckConstraints[tableName] = /* @__PURE__ */ new Map();
|
|
1038
|
+
}
|
|
1039
|
+
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
1040
|
+
}
|
|
990
1041
|
const up = [];
|
|
991
1042
|
const down = [];
|
|
992
1043
|
this.createEnums(
|
|
@@ -1061,6 +1112,20 @@ var MigrationGenerator = class {
|
|
|
1061
1112
|
}
|
|
1062
1113
|
});
|
|
1063
1114
|
});
|
|
1115
|
+
if (model.constraints?.length) {
|
|
1116
|
+
for (let i = 0; i < model.constraints.length; i++) {
|
|
1117
|
+
const entry = model.constraints[i];
|
|
1118
|
+
if (entry.kind === "check") {
|
|
1119
|
+
validateCheckConstraint(model, entry);
|
|
1120
|
+
const table = model.name;
|
|
1121
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
1122
|
+
const expression = entry.expression;
|
|
1123
|
+
up.push(() => {
|
|
1124
|
+
this.addCheckConstraint(table, constraintName, expression);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1064
1129
|
down.push(() => {
|
|
1065
1130
|
this.dropTable(model.name);
|
|
1066
1131
|
});
|
|
@@ -1095,6 +1160,37 @@ var MigrationGenerator = class {
|
|
|
1095
1160
|
(field) => (!field.generateAs || field.generateAs.type === "expression") && this.hasChanged(model, field)
|
|
1096
1161
|
);
|
|
1097
1162
|
this.updateFields(model, existingFields, up, down);
|
|
1163
|
+
if (model.constraints?.length) {
|
|
1164
|
+
const existingMap = this.existingCheckConstraints[model.name];
|
|
1165
|
+
for (let i = 0; i < model.constraints.length; i++) {
|
|
1166
|
+
const entry = model.constraints[i];
|
|
1167
|
+
if (entry.kind !== "check") {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
validateCheckConstraint(model, entry);
|
|
1171
|
+
const table = model.name;
|
|
1172
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
1173
|
+
const newExpression = entry.expression;
|
|
1174
|
+
const existingExpression = existingMap?.get(constraintName);
|
|
1175
|
+
if (existingExpression === void 0) {
|
|
1176
|
+
up.push(() => {
|
|
1177
|
+
this.addCheckConstraint(table, constraintName, newExpression);
|
|
1178
|
+
});
|
|
1179
|
+
down.push(() => {
|
|
1180
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1181
|
+
});
|
|
1182
|
+
} else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
|
|
1183
|
+
up.push(() => {
|
|
1184
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1185
|
+
this.addCheckConstraint(table, constraintName, newExpression);
|
|
1186
|
+
});
|
|
1187
|
+
down.push(() => {
|
|
1188
|
+
this.dropCheckConstraint(table, constraintName);
|
|
1189
|
+
this.addCheckConstraint(table, constraintName, existingExpression);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1098
1194
|
}
|
|
1099
1195
|
if (isUpdatableModel(model)) {
|
|
1100
1196
|
if (!tables.includes(`${model.name}Revision`)) {
|
|
@@ -1466,6 +1562,27 @@ var MigrationGenerator = class {
|
|
|
1466
1562
|
renameColumn(from, to) {
|
|
1467
1563
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
1468
1564
|
}
|
|
1565
|
+
getCheckConstraintName(model, entry, index) {
|
|
1566
|
+
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
1567
|
+
}
|
|
1568
|
+
normalizeCheckExpression(expr) {
|
|
1569
|
+
return expr.replace(/\s+/g, " ").trim();
|
|
1570
|
+
}
|
|
1571
|
+
/** Escape expression for embedding inside a template literal in generated code */
|
|
1572
|
+
escapeExpressionForRaw(expr) {
|
|
1573
|
+
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
1574
|
+
}
|
|
1575
|
+
addCheckConstraint(table, constraintName, expression) {
|
|
1576
|
+
const escaped = this.escapeExpressionForRaw(expression);
|
|
1577
|
+
this.writer.writeLine(
|
|
1578
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
1579
|
+
);
|
|
1580
|
+
this.writer.blankLine();
|
|
1581
|
+
}
|
|
1582
|
+
dropCheckConstraint(table, constraintName) {
|
|
1583
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
1584
|
+
this.writer.blankLine();
|
|
1585
|
+
}
|
|
1469
1586
|
value(value2) {
|
|
1470
1587
|
if (typeof value2 === "string") {
|
|
1471
1588
|
return `'${value2}'`;
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -79,6 +79,7 @@ __export(index_exports, {
|
|
|
79
79
|
document: () => document,
|
|
80
80
|
enm: () => enm,
|
|
81
81
|
execute: () => execute,
|
|
82
|
+
extractColumnReferencesFromCheckExpression: () => extractColumnReferencesFromCheckExpression,
|
|
82
83
|
extractFunctionBody: () => extractFunctionBody,
|
|
83
84
|
fetchDisplay: () => fetchDisplay,
|
|
84
85
|
fetchManyToManyDisplay: () => fetchManyToManyDisplay,
|
|
@@ -194,6 +195,7 @@ __export(index_exports, {
|
|
|
194
195
|
updateEntities: () => updateEntities,
|
|
195
196
|
updateEntity: () => updateEntity,
|
|
196
197
|
updateFunctions: () => updateFunctions,
|
|
198
|
+
validateCheckConstraint: () => validateCheckConstraint,
|
|
197
199
|
value: () => value
|
|
198
200
|
});
|
|
199
201
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -618,6 +620,7 @@ var EntityModel = class extends Model {
|
|
|
618
620
|
displayField;
|
|
619
621
|
defaultOrderBy;
|
|
620
622
|
fields;
|
|
623
|
+
constraints;
|
|
621
624
|
// temporary fields for the generation of migrations
|
|
622
625
|
deleted;
|
|
623
626
|
oldName;
|
|
@@ -645,6 +648,13 @@ var EntityModel = class extends Model {
|
|
|
645
648
|
for (const field of definition.fields) {
|
|
646
649
|
this.fieldsByName[field.name] = field;
|
|
647
650
|
}
|
|
651
|
+
if (this.constraints?.length) {
|
|
652
|
+
for (const constraint of this.constraints) {
|
|
653
|
+
if (constraint.kind === "check") {
|
|
654
|
+
validateCheckConstraint(this, constraint);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
648
658
|
}
|
|
649
659
|
getField(name2) {
|
|
650
660
|
return get(this.fieldsByName, name2);
|
|
@@ -913,6 +923,31 @@ var isManyToManyRelationEntityModel = ({
|
|
|
913
923
|
}
|
|
914
924
|
return manyToManyRelation;
|
|
915
925
|
};
|
|
926
|
+
var extractColumnReferencesFromCheckExpression = (expression) => {
|
|
927
|
+
const normalized = expression.replace(/\s+/g, " ").trim();
|
|
928
|
+
const quotedIdentifiers = [];
|
|
929
|
+
const re = /"([^"]+)"/g;
|
|
930
|
+
let match;
|
|
931
|
+
while ((match = re.exec(normalized)) !== null) {
|
|
932
|
+
quotedIdentifiers.push(match[1]);
|
|
933
|
+
}
|
|
934
|
+
return [...new Set(quotedIdentifiers)];
|
|
935
|
+
};
|
|
936
|
+
var validateCheckConstraint = (model, constraint) => {
|
|
937
|
+
const identifiers = extractColumnReferencesFromCheckExpression(constraint.expression);
|
|
938
|
+
if (identifiers.length === 0) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
942
|
+
for (const identifier of identifiers) {
|
|
943
|
+
if (!validColumnNames.has(identifier)) {
|
|
944
|
+
const validList = [...validColumnNames].sort().join(", ");
|
|
945
|
+
throw new Error(
|
|
946
|
+
`Constraint "${constraint.name}" references column "${identifier}" which does not exist on model ${model.name}. Valid columns: ${validList}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
};
|
|
916
951
|
|
|
917
952
|
// src/client/queries.ts
|
|
918
953
|
var fieldIsSearchable = (model, fieldName) => {
|
|
@@ -1201,7 +1236,7 @@ function normalizeValue(value2, type, schema) {
|
|
|
1201
1236
|
}
|
|
1202
1237
|
}
|
|
1203
1238
|
var normalizeValueByTypeDefinition = (value2, type, schema) => {
|
|
1204
|
-
if (
|
|
1239
|
+
if (type?.kind !== import_graphql3.Kind.INPUT_OBJECT_TYPE_DEFINITION) {
|
|
1205
1240
|
return value2;
|
|
1206
1241
|
}
|
|
1207
1242
|
if (value2 === null || value2 === void 0) {
|
|
@@ -2980,6 +3015,8 @@ var MigrationGenerator = class {
|
|
|
2980
3015
|
});
|
|
2981
3016
|
schema;
|
|
2982
3017
|
columns = {};
|
|
3018
|
+
/** table name -> constraint name -> check clause expression */
|
|
3019
|
+
existingCheckConstraints = {};
|
|
2983
3020
|
uuidUsed;
|
|
2984
3021
|
nowUsed;
|
|
2985
3022
|
needsMigration = false;
|
|
@@ -2991,6 +3028,22 @@ var MigrationGenerator = class {
|
|
|
2991
3028
|
for (const table of tables) {
|
|
2992
3029
|
this.columns[table] = await schema.columnInfo(table);
|
|
2993
3030
|
}
|
|
3031
|
+
const checkResult = await schema.knex.raw(
|
|
3032
|
+
`SELECT tc.table_name, tc.constraint_name, cc.check_clause
|
|
3033
|
+
FROM information_schema.table_constraints tc
|
|
3034
|
+
JOIN information_schema.check_constraints cc
|
|
3035
|
+
ON tc.constraint_schema = cc.constraint_schema AND tc.constraint_name = cc.constraint_name
|
|
3036
|
+
WHERE tc.table_schema = ? AND tc.constraint_type = ?`,
|
|
3037
|
+
["public", "CHECK"]
|
|
3038
|
+
);
|
|
3039
|
+
const rows = "rows" in checkResult && Array.isArray(checkResult.rows) ? checkResult.rows : [];
|
|
3040
|
+
for (const row of rows) {
|
|
3041
|
+
const tableName = row.table_name;
|
|
3042
|
+
if (!this.existingCheckConstraints[tableName]) {
|
|
3043
|
+
this.existingCheckConstraints[tableName] = /* @__PURE__ */ new Map();
|
|
3044
|
+
}
|
|
3045
|
+
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
3046
|
+
}
|
|
2994
3047
|
const up = [];
|
|
2995
3048
|
const down = [];
|
|
2996
3049
|
this.createEnums(
|
|
@@ -3065,6 +3118,20 @@ var MigrationGenerator = class {
|
|
|
3065
3118
|
}
|
|
3066
3119
|
});
|
|
3067
3120
|
});
|
|
3121
|
+
if (model.constraints?.length) {
|
|
3122
|
+
for (let i = 0; i < model.constraints.length; i++) {
|
|
3123
|
+
const entry = model.constraints[i];
|
|
3124
|
+
if (entry.kind === "check") {
|
|
3125
|
+
validateCheckConstraint(model, entry);
|
|
3126
|
+
const table = model.name;
|
|
3127
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
3128
|
+
const expression = entry.expression;
|
|
3129
|
+
up.push(() => {
|
|
3130
|
+
this.addCheckConstraint(table, constraintName, expression);
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3068
3135
|
down.push(() => {
|
|
3069
3136
|
this.dropTable(model.name);
|
|
3070
3137
|
});
|
|
@@ -3099,6 +3166,37 @@ var MigrationGenerator = class {
|
|
|
3099
3166
|
(field) => (!field.generateAs || field.generateAs.type === "expression") && this.hasChanged(model, field)
|
|
3100
3167
|
);
|
|
3101
3168
|
this.updateFields(model, existingFields, up, down);
|
|
3169
|
+
if (model.constraints?.length) {
|
|
3170
|
+
const existingMap = this.existingCheckConstraints[model.name];
|
|
3171
|
+
for (let i = 0; i < model.constraints.length; i++) {
|
|
3172
|
+
const entry = model.constraints[i];
|
|
3173
|
+
if (entry.kind !== "check") {
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
validateCheckConstraint(model, entry);
|
|
3177
|
+
const table = model.name;
|
|
3178
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
3179
|
+
const newExpression = entry.expression;
|
|
3180
|
+
const existingExpression = existingMap?.get(constraintName);
|
|
3181
|
+
if (existingExpression === void 0) {
|
|
3182
|
+
up.push(() => {
|
|
3183
|
+
this.addCheckConstraint(table, constraintName, newExpression);
|
|
3184
|
+
});
|
|
3185
|
+
down.push(() => {
|
|
3186
|
+
this.dropCheckConstraint(table, constraintName);
|
|
3187
|
+
});
|
|
3188
|
+
} else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
|
|
3189
|
+
up.push(() => {
|
|
3190
|
+
this.dropCheckConstraint(table, constraintName);
|
|
3191
|
+
this.addCheckConstraint(table, constraintName, newExpression);
|
|
3192
|
+
});
|
|
3193
|
+
down.push(() => {
|
|
3194
|
+
this.dropCheckConstraint(table, constraintName);
|
|
3195
|
+
this.addCheckConstraint(table, constraintName, existingExpression);
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3102
3200
|
}
|
|
3103
3201
|
if (isUpdatableModel(model)) {
|
|
3104
3202
|
if (!tables.includes(`${model.name}Revision`)) {
|
|
@@ -3470,6 +3568,27 @@ var MigrationGenerator = class {
|
|
|
3470
3568
|
renameColumn(from, to) {
|
|
3471
3569
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
3472
3570
|
}
|
|
3571
|
+
getCheckConstraintName(model, entry, index) {
|
|
3572
|
+
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
3573
|
+
}
|
|
3574
|
+
normalizeCheckExpression(expr) {
|
|
3575
|
+
return expr.replace(/\s+/g, " ").trim();
|
|
3576
|
+
}
|
|
3577
|
+
/** Escape expression for embedding inside a template literal in generated code */
|
|
3578
|
+
escapeExpressionForRaw(expr) {
|
|
3579
|
+
return expr.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
3580
|
+
}
|
|
3581
|
+
addCheckConstraint(table, constraintName, expression) {
|
|
3582
|
+
const escaped = this.escapeExpressionForRaw(expression);
|
|
3583
|
+
this.writer.writeLine(
|
|
3584
|
+
`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`
|
|
3585
|
+
);
|
|
3586
|
+
this.writer.blankLine();
|
|
3587
|
+
}
|
|
3588
|
+
dropCheckConstraint(table, constraintName) {
|
|
3589
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
3590
|
+
this.writer.blankLine();
|
|
3591
|
+
}
|
|
3473
3592
|
value(value2) {
|
|
3474
3593
|
if (typeof value2 === "string") {
|
|
3475
3594
|
return `'${value2}'`;
|
|
@@ -4446,6 +4565,7 @@ var printSchemaFromModels = (models) => printSchema((0, import_graphql6.buildAST
|
|
|
4446
4565
|
document,
|
|
4447
4566
|
enm,
|
|
4448
4567
|
execute,
|
|
4568
|
+
extractColumnReferencesFromCheckExpression,
|
|
4449
4569
|
extractFunctionBody,
|
|
4450
4570
|
fetchDisplay,
|
|
4451
4571
|
fetchManyToManyDisplay,
|
|
@@ -4561,5 +4681,6 @@ var printSchemaFromModels = (models) => printSchema((0, import_graphql6.buildAST
|
|
|
4561
4681
|
updateEntities,
|
|
4562
4682
|
updateEntity,
|
|
4563
4683
|
updateFunctions,
|
|
4684
|
+
validateCheckConstraint,
|
|
4564
4685
|
value
|
|
4565
4686
|
});
|
|
@@ -7,6 +7,8 @@ export declare class MigrationGenerator {
|
|
|
7
7
|
private writer;
|
|
8
8
|
private schema;
|
|
9
9
|
private columns;
|
|
10
|
+
/** table name -> constraint name -> check clause expression */
|
|
11
|
+
private existingCheckConstraints;
|
|
10
12
|
private uuidUsed?;
|
|
11
13
|
private nowUsed?;
|
|
12
14
|
needsMigration: boolean;
|
|
@@ -28,6 +30,12 @@ export declare class MigrationGenerator {
|
|
|
28
30
|
private dropTable;
|
|
29
31
|
private renameTable;
|
|
30
32
|
private renameColumn;
|
|
33
|
+
private getCheckConstraintName;
|
|
34
|
+
private normalizeCheckExpression;
|
|
35
|
+
/** Escape expression for embedding inside a template literal in generated code */
|
|
36
|
+
private escapeExpressionForRaw;
|
|
37
|
+
private addCheckConstraint;
|
|
38
|
+
private dropCheckConstraint;
|
|
31
39
|
private value;
|
|
32
40
|
private columnRaw;
|
|
33
41
|
private column;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import CodeBlockWriter from 'code-block-writer';
|
|
2
2
|
import { SchemaInspector } from 'knex-schema-inspector';
|
|
3
3
|
import lowerFirst from 'lodash/lowerFirst';
|
|
4
|
-
import { and, get, isCreatableModel, isInherited, isUpdatableField, isUpdatableModel, modelNeedsTable, not, summonByName, typeToField, } from '../models/utils';
|
|
4
|
+
import { and, get, isCreatableModel, isInherited, isUpdatableField, isUpdatableModel, modelNeedsTable, not, summonByName, typeToField, validateCheckConstraint, } from '../models/utils';
|
|
5
5
|
import { getColumnName } from '../resolvers';
|
|
6
6
|
import { getDatabaseFunctions, normalizeAggregateDefinition, normalizeFunctionBody, } from './update-functions';
|
|
7
7
|
export class MigrationGenerator {
|
|
@@ -14,6 +14,8 @@ export class MigrationGenerator {
|
|
|
14
14
|
});
|
|
15
15
|
schema;
|
|
16
16
|
columns = {};
|
|
17
|
+
/** table name -> constraint name -> check clause expression */
|
|
18
|
+
existingCheckConstraints = {};
|
|
17
19
|
uuidUsed;
|
|
18
20
|
nowUsed;
|
|
19
21
|
needsMigration = false;
|
|
@@ -31,6 +33,21 @@ export class MigrationGenerator {
|
|
|
31
33
|
for (const table of tables) {
|
|
32
34
|
this.columns[table] = await schema.columnInfo(table);
|
|
33
35
|
}
|
|
36
|
+
const checkResult = await schema.knex.raw(`SELECT tc.table_name, tc.constraint_name, cc.check_clause
|
|
37
|
+
FROM information_schema.table_constraints tc
|
|
38
|
+
JOIN information_schema.check_constraints cc
|
|
39
|
+
ON tc.constraint_schema = cc.constraint_schema AND tc.constraint_name = cc.constraint_name
|
|
40
|
+
WHERE tc.table_schema = ? AND tc.constraint_type = ?`, ['public', 'CHECK']);
|
|
41
|
+
const rows = 'rows' in checkResult && Array.isArray(checkResult.rows)
|
|
42
|
+
? checkResult.rows
|
|
43
|
+
: [];
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
const tableName = row.table_name;
|
|
46
|
+
if (!this.existingCheckConstraints[tableName]) {
|
|
47
|
+
this.existingCheckConstraints[tableName] = new Map();
|
|
48
|
+
}
|
|
49
|
+
this.existingCheckConstraints[tableName].set(row.constraint_name, row.check_clause);
|
|
50
|
+
}
|
|
34
51
|
const up = [];
|
|
35
52
|
const down = [];
|
|
36
53
|
this.createEnums(this.models.enums.filter((enm) => !enums.includes(lowerFirst(enm.name))), up, down);
|
|
@@ -106,6 +123,20 @@ export class MigrationGenerator {
|
|
|
106
123
|
}
|
|
107
124
|
});
|
|
108
125
|
});
|
|
126
|
+
if (model.constraints?.length) {
|
|
127
|
+
for (let i = 0; i < model.constraints.length; i++) {
|
|
128
|
+
const entry = model.constraints[i];
|
|
129
|
+
if (entry.kind === 'check') {
|
|
130
|
+
validateCheckConstraint(model, entry);
|
|
131
|
+
const table = model.name;
|
|
132
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
133
|
+
const expression = entry.expression;
|
|
134
|
+
up.push(() => {
|
|
135
|
+
this.addCheckConstraint(table, constraintName, expression);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
109
140
|
down.push(() => {
|
|
110
141
|
this.dropTable(model.name);
|
|
111
142
|
});
|
|
@@ -139,6 +170,38 @@ export class MigrationGenerator {
|
|
|
139
170
|
}
|
|
140
171
|
const existingFields = model.fields.filter((field) => (!field.generateAs || field.generateAs.type === 'expression') && this.hasChanged(model, field));
|
|
141
172
|
this.updateFields(model, existingFields, up, down);
|
|
173
|
+
if (model.constraints?.length) {
|
|
174
|
+
const existingMap = this.existingCheckConstraints[model.name];
|
|
175
|
+
for (let i = 0; i < model.constraints.length; i++) {
|
|
176
|
+
const entry = model.constraints[i];
|
|
177
|
+
if (entry.kind !== 'check') {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
validateCheckConstraint(model, entry);
|
|
181
|
+
const table = model.name;
|
|
182
|
+
const constraintName = this.getCheckConstraintName(model, entry, i);
|
|
183
|
+
const newExpression = entry.expression;
|
|
184
|
+
const existingExpression = existingMap?.get(constraintName);
|
|
185
|
+
if (existingExpression === undefined) {
|
|
186
|
+
up.push(() => {
|
|
187
|
+
this.addCheckConstraint(table, constraintName, newExpression);
|
|
188
|
+
});
|
|
189
|
+
down.push(() => {
|
|
190
|
+
this.dropCheckConstraint(table, constraintName);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else if (this.normalizeCheckExpression(existingExpression) !== this.normalizeCheckExpression(newExpression)) {
|
|
194
|
+
up.push(() => {
|
|
195
|
+
this.dropCheckConstraint(table, constraintName);
|
|
196
|
+
this.addCheckConstraint(table, constraintName, newExpression);
|
|
197
|
+
});
|
|
198
|
+
down.push(() => {
|
|
199
|
+
this.dropCheckConstraint(table, constraintName);
|
|
200
|
+
this.addCheckConstraint(table, constraintName, existingExpression);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
142
205
|
}
|
|
143
206
|
if (isUpdatableModel(model)) {
|
|
144
207
|
if (!tables.includes(`${model.name}Revision`)) {
|
|
@@ -524,6 +587,25 @@ export class MigrationGenerator {
|
|
|
524
587
|
renameColumn(from, to) {
|
|
525
588
|
this.writer.writeLine(`table.renameColumn('${from}', '${to}');`);
|
|
526
589
|
}
|
|
590
|
+
getCheckConstraintName(model, entry, index) {
|
|
591
|
+
return `${model.name}_${entry.name}_${entry.kind}_${index}`;
|
|
592
|
+
}
|
|
593
|
+
normalizeCheckExpression(expr) {
|
|
594
|
+
return expr.replace(/\s+/g, ' ').trim();
|
|
595
|
+
}
|
|
596
|
+
/** Escape expression for embedding inside a template literal in generated code */
|
|
597
|
+
escapeExpressionForRaw(expr) {
|
|
598
|
+
return expr.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
599
|
+
}
|
|
600
|
+
addCheckConstraint(table, constraintName, expression) {
|
|
601
|
+
const escaped = this.escapeExpressionForRaw(expression);
|
|
602
|
+
this.writer.writeLine(`await knex.raw(\`ALTER TABLE "${table}" ADD CONSTRAINT "${constraintName}" CHECK (${escaped})\`);`);
|
|
603
|
+
this.writer.blankLine();
|
|
604
|
+
}
|
|
605
|
+
dropCheckConstraint(table, constraintName) {
|
|
606
|
+
this.writer.writeLine(`await knex.raw('ALTER TABLE "${table}" DROP CONSTRAINT "${constraintName}"');`);
|
|
607
|
+
this.writer.blankLine();
|
|
608
|
+
}
|
|
527
609
|
value(value) {
|
|
528
610
|
if (typeof value === 'string') {
|
|
529
611
|
return `'${value}'`;
|