@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.
@@ -165,7 +165,26 @@ export type ModelDefinition = {
165
165
  */
166
166
  manyToManyRelation?: boolean;
167
167
 
168
- constraints?: { kind: 'check'; name: string; expression: string }[];
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;
@@ -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?: { kind: 'check'; name: string; expression: string }[];
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
  }
@@ -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 { extractColumnReferencesFromCheckExpression, validateCheckConstraint } from '../../src/models/utils';
2
+ import {
3
+ extractColumnReferencesFromCheckExpression,
4
+ validateCheckConstraint,
5
+ validateExcludeConstraint,
6
+ } from '../../src/models/utils';
3
7
 
4
- describe('check constraints', () => {
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
  });