@smartive/graphql-magic 23.6.1-next.2 → 23.7.0-next.1
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/.gqmrc.json +2 -1
- package/CHANGELOG.md +3 -3
- package/dist/bin/gqm.cjs +278 -115
- package/dist/cjs/index.cjs +288 -125
- package/dist/esm/db/generate.js +5 -5
- package/dist/esm/db/generate.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +13 -1
- package/dist/esm/migrations/generate.js +197 -41
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/migrations/index.d.ts +2 -1
- package/dist/esm/migrations/index.js +2 -1
- package/dist/esm/migrations/index.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 +16 -7
- package/dist/esm/models/utils.js +16 -6
- package/dist/esm/models/utils.js.map +1 -1
- package/dist/esm/permissions/check.js +2 -2
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/resolvers/mutations.js +6 -6
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/dist/esm/resolvers/resolvers.js +3 -9
- package/dist/esm/resolvers/resolvers.js.map +1 -1
- package/dist/esm/schema/generate.js +3 -9
- package/dist/esm/schema/generate.js.map +1 -1
- package/docker-compose.yml +2 -3
- 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/db/generate.ts +5 -15
- package/src/migrations/generate.ts +257 -42
- package/src/migrations/index.ts +2 -1
- package/src/models/model-definitions.ts +20 -1
- package/src/models/models.ts +4 -1
- package/src/models/utils.ts +27 -8
- package/src/permissions/check.ts +2 -2
- package/src/resolvers/mutations.ts +6 -6
- package/src/resolvers/resolvers.ts +7 -13
- package/src/schema/generate.ts +28 -26
- package/tests/unit/constraints.spec.ts +98 -2
- package/tests/unit/generate-as.spec.ts +6 -6
- package/tests/utils/functions.ts +1 -0
package/src/schema/generate.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { DefinitionNode, DocumentNode, GraphQLSchema, buildASTSchema, print } from 'graphql';
|
|
2
2
|
import { Models } from '../models/models';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
isCreatable,
|
|
6
|
+
isQueriableField,
|
|
7
|
+
isRootModel,
|
|
8
|
+
isStoredInDatabase,
|
|
9
|
+
isUpdatable,
|
|
10
|
+
typeToField,
|
|
11
|
+
} from '../models/utils';
|
|
4
12
|
import { Field, document, enm, iface, input, object, scalar, union } from './utils';
|
|
5
13
|
|
|
6
14
|
export const generateDefinitions = ({
|
|
@@ -133,19 +141,16 @@ export const generateDefinitions = ({
|
|
|
133
141
|
types.push(
|
|
134
142
|
input(
|
|
135
143
|
`Create${model.name}`,
|
|
136
|
-
model.fields
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
nonNull: field.nonNull && field.defaultValue === undefined,
|
|
147
|
-
},
|
|
148
|
-
),
|
|
144
|
+
model.fields.filter(and(isCreatable, isStoredInDatabase)).map((field) =>
|
|
145
|
+
field.kind === 'relation'
|
|
146
|
+
? { name: `${field.name}Id`, type: 'ID', nonNull: field.nonNull }
|
|
147
|
+
: {
|
|
148
|
+
name: field.name,
|
|
149
|
+
type: field.kind === 'json' ? `Create${field.type}` : field.type,
|
|
150
|
+
list: field.list,
|
|
151
|
+
nonNull: field.nonNull && field.defaultValue === undefined,
|
|
152
|
+
},
|
|
153
|
+
),
|
|
149
154
|
),
|
|
150
155
|
);
|
|
151
156
|
}
|
|
@@ -154,18 +159,15 @@ export const generateDefinitions = ({
|
|
|
154
159
|
types.push(
|
|
155
160
|
input(
|
|
156
161
|
`Update${model.name}`,
|
|
157
|
-
model.fields
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
list: field.list,
|
|
167
|
-
},
|
|
168
|
-
),
|
|
162
|
+
model.fields.filter(and(isUpdatable, isStoredInDatabase)).map((field) =>
|
|
163
|
+
field.kind === 'relation'
|
|
164
|
+
? { name: `${field.name}Id`, type: 'ID' }
|
|
165
|
+
: {
|
|
166
|
+
name: field.name,
|
|
167
|
+
type: field.kind === 'json' ? `Update${field.type}` : field.type,
|
|
168
|
+
list: field.list,
|
|
169
|
+
},
|
|
170
|
+
),
|
|
169
171
|
),
|
|
170
172
|
);
|
|
171
173
|
}
|
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Kind } from 'graphql';
|
|
2
2
|
import { generateDBModels } from '../../src/db/generate';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
isDynamicField,
|
|
5
5
|
isStoredInDatabase,
|
|
6
6
|
} from '../../src/models/utils';
|
|
7
7
|
import { ModelDefinitions, Models } from '../../src/models';
|
|
@@ -42,10 +42,10 @@ jest.mock('code-block-writer', () => {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
describe('generateAs helpers', () => {
|
|
45
|
-
describe('
|
|
45
|
+
describe('isDynamicField', () => {
|
|
46
46
|
it('returns false when field.generateAs is undefined', () => {
|
|
47
47
|
const field: EntityField = { name: 'price', type: 'Float' };
|
|
48
|
-
expect(
|
|
48
|
+
expect(isDynamicField(field)).toBe(false);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('returns true when field has generateAs with type expression', () => {
|
|
@@ -54,7 +54,7 @@ describe('generateAs helpers', () => {
|
|
|
54
54
|
type: 'Float',
|
|
55
55
|
generateAs: { expression: 'price * quantity', type: 'expression' },
|
|
56
56
|
};
|
|
57
|
-
expect(
|
|
57
|
+
expect(isDynamicField(field)).toBe(true);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it('returns true when field has generateAs with type stored', () => {
|
|
@@ -63,7 +63,7 @@ describe('generateAs helpers', () => {
|
|
|
63
63
|
type: 'Float',
|
|
64
64
|
generateAs: { expression: 'price * quantity', type: 'stored' },
|
|
65
65
|
};
|
|
66
|
-
expect(
|
|
66
|
+
expect(isDynamicField(field)).toBe(true);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('returns true when field has generateAs with type virtual', () => {
|
|
@@ -72,7 +72,7 @@ describe('generateAs helpers', () => {
|
|
|
72
72
|
type: 'Float',
|
|
73
73
|
generateAs: { expression: 'price * quantity', type: 'virtual' },
|
|
74
74
|
};
|
|
75
|
-
expect(
|
|
75
|
+
expect(isDynamicField(field)).toBe(true);
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const functions: string[] = [];
|