@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
|
@@ -165,7 +165,26 @@ export type ModelDefinition = {
|
|
|
165
165
|
*/
|
|
166
166
|
manyToManyRelation?: boolean;
|
|
167
167
|
|
|
168
|
-
constraints?:
|
|
168
|
+
constraints?: (
|
|
169
|
+
| { kind: 'check'; name: string; expression: string; deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE' }
|
|
170
|
+
| {
|
|
171
|
+
kind: 'exclude';
|
|
172
|
+
name: string;
|
|
173
|
+
using: 'gist';
|
|
174
|
+
elements: ({ column: string; operator: '=' } | { expression: string; operator: '&&' })[];
|
|
175
|
+
where?: string;
|
|
176
|
+
deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
|
|
177
|
+
}
|
|
178
|
+
| {
|
|
179
|
+
kind: 'constraint_trigger';
|
|
180
|
+
name: string;
|
|
181
|
+
when: 'AFTER' | 'BEFORE';
|
|
182
|
+
events: ('INSERT' | 'UPDATE')[];
|
|
183
|
+
forEach: 'ROW' | 'STATEMENT';
|
|
184
|
+
deferrable?: 'INITIALLY DEFERRED' | 'INITIALLY IMMEDIATE';
|
|
185
|
+
function: { name: string; args?: string[] };
|
|
186
|
+
}
|
|
187
|
+
)[];
|
|
169
188
|
|
|
170
189
|
// temporary fields for the generation of migrations
|
|
171
190
|
deleted?: true;
|
package/src/models/models.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
summonByName,
|
|
41
41
|
typeToField,
|
|
42
42
|
validateCheckConstraint,
|
|
43
|
+
validateExcludeConstraint,
|
|
43
44
|
} from './utils';
|
|
44
45
|
|
|
45
46
|
// These might one day become classes
|
|
@@ -357,7 +358,7 @@ export class EntityModel extends Model {
|
|
|
357
358
|
defaultOrderBy?: OrderBy[];
|
|
358
359
|
fields: EntityField[];
|
|
359
360
|
|
|
360
|
-
constraints?:
|
|
361
|
+
constraints?: EntityModelDefinition['constraints'];
|
|
361
362
|
|
|
362
363
|
// temporary fields for the generation of migrations
|
|
363
364
|
deleted?: true;
|
|
@@ -392,6 +393,8 @@ export class EntityModel extends Model {
|
|
|
392
393
|
for (const constraint of this.constraints) {
|
|
393
394
|
if (constraint.kind === 'check') {
|
|
394
395
|
validateCheckConstraint(this, constraint);
|
|
396
|
+
} else if (constraint.kind === 'exclude') {
|
|
397
|
+
validateExcludeConstraint(this, constraint);
|
|
395
398
|
}
|
|
396
399
|
}
|
|
397
400
|
}
|
package/src/models/utils.ts
CHANGED
|
@@ -259,3 +259,23 @@ export const validateCheckConstraint = (model: EntityModel, constraint: { name:
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
};
|
|
262
|
+
|
|
263
|
+
export const validateExcludeConstraint = (
|
|
264
|
+
model: EntityModel,
|
|
265
|
+
constraint: {
|
|
266
|
+
name: string;
|
|
267
|
+
elements: ({ column: string; operator: string } | { expression: string; operator: string })[];
|
|
268
|
+
},
|
|
269
|
+
): void => {
|
|
270
|
+
const validColumnNames = new Set(model.fields.map((f) => getColumnName(f)));
|
|
271
|
+
for (const el of constraint.elements) {
|
|
272
|
+
if ('column' in el) {
|
|
273
|
+
if (!validColumnNames.has(el.column)) {
|
|
274
|
+
const validList = [...validColumnNames].sort().join(', ');
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Exclude constraint "${constraint.name}" references column "${el.column}" which does not exist on model ${model.name}. Valid columns: ${validList}`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { ModelDefinitions, Models } from '../../src/models';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
extractColumnReferencesFromCheckExpression,
|
|
4
|
+
validateCheckConstraint,
|
|
5
|
+
validateExcludeConstraint,
|
|
6
|
+
} from '../../src/models/utils';
|
|
3
7
|
|
|
4
|
-
describe('
|
|
8
|
+
describe('constraints', () => {
|
|
5
9
|
const modelDefinitions: ModelDefinitions = [
|
|
6
10
|
{
|
|
7
11
|
kind: 'entity',
|
|
@@ -80,4 +84,96 @@ describe('check constraints', () => {
|
|
|
80
84
|
).toThrow(/Valid columns:.*\bscore\b.*\bstatus\b/);
|
|
81
85
|
});
|
|
82
86
|
});
|
|
87
|
+
|
|
88
|
+
describe('validateExcludeConstraint', () => {
|
|
89
|
+
it('does not throw when column elements reference valid columns', () => {
|
|
90
|
+
expect(() =>
|
|
91
|
+
validateExcludeConstraint(productModel, {
|
|
92
|
+
name: 'valid',
|
|
93
|
+
elements: [
|
|
94
|
+
{ column: 'score', operator: '=' },
|
|
95
|
+
{ expression: 'tsrange("startDate", "endDate")', operator: '&&' },
|
|
96
|
+
],
|
|
97
|
+
}),
|
|
98
|
+
).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not throw when only expression elements', () => {
|
|
102
|
+
expect(() =>
|
|
103
|
+
validateExcludeConstraint(productModel, {
|
|
104
|
+
name: 'valid',
|
|
105
|
+
elements: [{ expression: 'tsrange(now(), now())', operator: '&&' }],
|
|
106
|
+
}),
|
|
107
|
+
).not.toThrow();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('uses relation column name when validating', () => {
|
|
111
|
+
expect(() =>
|
|
112
|
+
validateExcludeConstraint(productModel, {
|
|
113
|
+
name: 'valid',
|
|
114
|
+
elements: [{ column: 'parentId', operator: '=' }],
|
|
115
|
+
}),
|
|
116
|
+
).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('throws when column element references missing column', () => {
|
|
120
|
+
expect(() =>
|
|
121
|
+
validateExcludeConstraint(productModel, {
|
|
122
|
+
name: 'bad',
|
|
123
|
+
elements: [{ column: 'unknown_column', operator: '=' }],
|
|
124
|
+
}),
|
|
125
|
+
).toThrow(
|
|
126
|
+
/Exclude constraint "bad" references column "unknown_column" which does not exist on model Product/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('exclude and constraint_trigger constraints', () => {
|
|
132
|
+
const allocationDefinitions: ModelDefinitions = [
|
|
133
|
+
{
|
|
134
|
+
kind: 'entity',
|
|
135
|
+
name: 'PortfolioAllocation',
|
|
136
|
+
fields: [
|
|
137
|
+
{ name: 'portfolio', kind: 'relation', type: 'Portfolio', reverse: 'allocations' },
|
|
138
|
+
{ name: 'startDate', type: 'DateTime' },
|
|
139
|
+
{ name: 'endDate', type: 'DateTime' },
|
|
140
|
+
{ name: 'deleted', type: 'Boolean', nonNull: true, defaultValue: false },
|
|
141
|
+
],
|
|
142
|
+
constraints: [
|
|
143
|
+
{
|
|
144
|
+
kind: 'exclude',
|
|
145
|
+
name: 'no_overlap_per_portfolio',
|
|
146
|
+
using: 'gist',
|
|
147
|
+
elements: [
|
|
148
|
+
{ column: 'portfolioId', operator: '=' },
|
|
149
|
+
{
|
|
150
|
+
expression: 'tsrange("startDate", COALESCE("endDate", \'infinity\'::timestamptz))',
|
|
151
|
+
operator: '&&',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
where: '"deleted" = false',
|
|
155
|
+
deferrable: 'INITIALLY DEFERRED',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
kind: 'constraint_trigger',
|
|
159
|
+
name: 'contiguous_periods',
|
|
160
|
+
when: 'AFTER',
|
|
161
|
+
events: ['INSERT', 'UPDATE'],
|
|
162
|
+
forEach: 'ROW',
|
|
163
|
+
deferrable: 'INITIALLY DEFERRED',
|
|
164
|
+
function: { name: 'contiguous_periods_check' },
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
kind: 'entity',
|
|
170
|
+
name: 'Portfolio',
|
|
171
|
+
fields: [{ name: 'name', type: 'String' }],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
it('constructs models with exclude and constraint_trigger without throwing', () => {
|
|
176
|
+
expect(() => new Models(allocationDefinitions)).not.toThrow();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
83
179
|
});
|