@smartive/graphql-magic 9.1.2 → 10.0.0
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/.eslintrc +2 -10
- package/.github/workflows/release.yml +1 -1
- package/.gqmrc.json +6 -0
- package/CHANGELOG.md +2 -2
- package/README.md +1 -1
- package/dist/bin/gqm.cjs +684 -330
- package/dist/cjs/index.cjs +998 -554
- package/dist/esm/api/execute.js +1 -1
- package/dist/esm/api/execute.js.map +1 -1
- package/dist/esm/client/mutations.d.ts +2 -2
- package/dist/esm/client/mutations.js +5 -4
- package/dist/esm/client/mutations.js.map +1 -1
- package/dist/esm/client/queries.d.ts +12 -17
- package/dist/esm/client/queries.js +30 -50
- package/dist/esm/client/queries.js.map +1 -1
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/db/generate.d.ts +3 -3
- package/dist/esm/db/generate.js +31 -29
- package/dist/esm/db/generate.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +3 -4
- package/dist/esm/migrations/generate.js +114 -107
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/index.d.ts +1 -0
- package/dist/esm/models/index.js +1 -0
- package/dist/esm/models/index.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +189 -0
- package/dist/esm/models/model-definitions.js +2 -0
- package/dist/esm/models/model-definitions.js.map +1 -0
- package/dist/esm/models/models.d.ts +128 -174
- package/dist/esm/models/models.js +411 -1
- package/dist/esm/models/models.js.map +1 -1
- package/dist/esm/models/mutation-hook.d.ts +2 -2
- package/dist/esm/models/utils.d.ts +35 -497
- package/dist/esm/models/utils.js +21 -144
- package/dist/esm/models/utils.js.map +1 -1
- package/dist/esm/permissions/check.d.ts +3 -3
- package/dist/esm/permissions/check.js +14 -7
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/permissions/generate.js +6 -6
- package/dist/esm/permissions/generate.js.map +1 -1
- package/dist/esm/resolvers/filters.d.ts +8 -0
- package/dist/esm/resolvers/filters.js +28 -25
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/index.d.ts +1 -0
- package/dist/esm/resolvers/index.js +1 -0
- package/dist/esm/resolvers/index.js.map +1 -1
- package/dist/esm/resolvers/mutations.js +85 -21
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/dist/esm/resolvers/node.d.ts +13 -15
- package/dist/esm/resolvers/node.js +41 -36
- package/dist/esm/resolvers/node.js.map +1 -1
- package/dist/esm/resolvers/resolver.js +19 -49
- package/dist/esm/resolvers/resolver.js.map +1 -1
- package/dist/esm/resolvers/resolvers.d.ts +1 -8
- package/dist/esm/resolvers/resolvers.js +15 -7
- package/dist/esm/resolvers/resolvers.js.map +1 -1
- package/dist/esm/resolvers/selects.d.ts +3 -0
- package/dist/esm/resolvers/selects.js +50 -0
- package/dist/esm/resolvers/selects.js.map +1 -0
- package/dist/esm/resolvers/utils.d.ts +12 -4
- package/dist/esm/resolvers/utils.js +30 -22
- package/dist/esm/resolvers/utils.js.map +1 -1
- package/dist/esm/schema/generate.d.ts +4 -4
- package/dist/esm/schema/generate.js +122 -131
- package/dist/esm/schema/generate.js.map +1 -1
- package/dist/esm/schema/utils.d.ts +1 -1
- package/dist/esm/schema/utils.js +2 -1
- package/dist/esm/schema/utils.js.map +1 -1
- package/knexfile.ts +31 -0
- package/migrations/20230912185644_setup.ts +127 -0
- package/package.json +16 -14
- package/src/api/execute.ts +1 -1
- package/src/bin/gqm/gqm.ts +25 -23
- package/src/bin/gqm/parse-models.ts +5 -5
- package/src/bin/gqm/settings.ts +13 -4
- package/src/bin/gqm/static-eval.ts +5 -0
- package/src/bin/gqm/templates.ts +23 -3
- package/src/client/mutations.ts +11 -5
- package/src/client/queries.ts +43 -80
- package/src/context.ts +1 -2
- package/src/db/generate.ts +41 -41
- package/src/migrations/generate.ts +165 -146
- package/src/models/index.ts +1 -0
- package/src/models/model-definitions.ts +168 -0
- package/src/models/models.ts +510 -166
- package/src/models/mutation-hook.ts +2 -2
- package/src/models/utils.ts +53 -187
- package/src/permissions/check.ts +19 -11
- package/src/permissions/generate.ts +6 -6
- package/src/resolvers/filters.ts +44 -28
- package/src/resolvers/index.ts +1 -0
- package/src/resolvers/mutations.ts +98 -36
- package/src/resolvers/node.ts +79 -51
- package/src/resolvers/resolver.ts +20 -74
- package/src/resolvers/resolvers.ts +18 -7
- package/src/resolvers/selects.ts +77 -0
- package/src/resolvers/utils.ts +41 -25
- package/src/schema/generate.ts +106 -127
- package/src/schema/utils.ts +2 -1
- package/tests/api/__snapshots__/inheritance.spec.ts.snap +83 -0
- package/tests/api/inheritance.spec.ts +130 -0
- package/tests/generated/api/index.ts +1174 -0
- package/tests/generated/client/index.ts +1163 -0
- package/tests/generated/client/mutations.ts +109 -0
- package/tests/generated/db/index.ts +291 -0
- package/tests/generated/db/knex.ts +14 -0
- package/tests/generated/models.json +675 -0
- package/tests/generated/schema.graphql +325 -0
- package/tests/unit/__snapshots__/resolve.spec.ts.snap +23 -0
- package/tests/unit/queries.spec.ts +5 -5
- package/tests/unit/resolve.spec.ts +8 -8
- package/tests/utils/database/knex.ts +5 -13
- package/tests/utils/database/seed.ts +57 -18
- package/tests/utils/models.ts +62 -7
- package/tests/utils/server.ts +5 -5
- package/tsconfig.eslint.json +1 -0
- package/tests/unit/__snapshots__/generate.spec.ts.snap +0 -128
- package/tests/unit/generate.spec.ts +0 -8
- package/tests/utils/database/schema.ts +0 -64
- package/tests/utils/generate-migration.ts +0 -24
|
@@ -3,9 +3,9 @@ import { DateTime } from 'luxon';
|
|
|
3
3
|
import { v4 as uuid } from 'uuid';
|
|
4
4
|
import { Context, FullContext } from '../context';
|
|
5
5
|
import { ForbiddenError, GraphQLError } from '../errors';
|
|
6
|
-
import {
|
|
6
|
+
import { EntityField, EntityModel } from '../models/models';
|
|
7
7
|
import { Entity, FullEntity } from '../models/mutation-hook';
|
|
8
|
-
import { get,
|
|
8
|
+
import { get, isPrimitive, it, typeToField } from '../models/utils';
|
|
9
9
|
import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
|
|
10
10
|
import { resolve } from './resolver';
|
|
11
11
|
import { AliasGenerator } from './utils';
|
|
@@ -14,7 +14,7 @@ export const mutationResolver = async (_parent: any, args: any, partialCtx: Cont
|
|
|
14
14
|
return await partialCtx.knex.transaction(async (knex) => {
|
|
15
15
|
const [, mutation, modelName] = it(info.fieldName.match(/^(create|update|delete|restore)(.+)$/));
|
|
16
16
|
const ctx = { ...partialCtx, knex, info, aliases: new AliasGenerator() };
|
|
17
|
-
const model =
|
|
17
|
+
const model = ctx.models.getModel(modelName, 'entity');
|
|
18
18
|
switch (mutation) {
|
|
19
19
|
case 'create':
|
|
20
20
|
return await create(model, args, ctx);
|
|
@@ -28,11 +28,14 @@ export const mutationResolver = async (_parent: any, args: any, partialCtx: Cont
|
|
|
28
28
|
});
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const create = async (model:
|
|
31
|
+
const create = async (model: EntityModel, { data: input }: { data: any }, ctx: FullContext) => {
|
|
32
32
|
const normalizedInput = { ...input };
|
|
33
33
|
normalizedInput.id = uuid();
|
|
34
34
|
normalizedInput.createdAt = ctx.now;
|
|
35
35
|
normalizedInput.createdById = ctx.user.id;
|
|
36
|
+
if (model.parent) {
|
|
37
|
+
normalizedInput.type = model.name;
|
|
38
|
+
}
|
|
36
39
|
sanitize(ctx, model, normalizedInput);
|
|
37
40
|
|
|
38
41
|
await checkCanWrite(ctx, model, normalizedInput, 'CREATE');
|
|
@@ -40,14 +43,31 @@ const create = async (model: Model, { data: input }: { data: any }, ctx: FullCon
|
|
|
40
43
|
|
|
41
44
|
const data = { prev: {}, input, normalizedInput, next: normalizedInput };
|
|
42
45
|
await ctx.mutationHook?.(model, 'create', 'before', data, ctx);
|
|
43
|
-
|
|
46
|
+
if (model.parent) {
|
|
47
|
+
const rootInput = {};
|
|
48
|
+
const childInput = { id: normalizedInput.id };
|
|
49
|
+
for (const field of model.fields) {
|
|
50
|
+
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
|
|
51
|
+
if (columnName in normalizedInput) {
|
|
52
|
+
if (field.inherited) {
|
|
53
|
+
rootInput[columnName] = normalizedInput[columnName];
|
|
54
|
+
} else {
|
|
55
|
+
childInput[columnName] = normalizedInput[columnName];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
await ctx.knex(model.parent).insert(rootInput);
|
|
60
|
+
await ctx.knex(model.name).insert(childInput);
|
|
61
|
+
} else {
|
|
62
|
+
await ctx.knex(model.name).insert(normalizedInput);
|
|
63
|
+
}
|
|
44
64
|
await createRevision(model, normalizedInput, ctx);
|
|
45
65
|
await ctx.mutationHook?.(model, 'create', 'after', data, ctx);
|
|
46
66
|
|
|
47
67
|
return await resolve(ctx, normalizedInput.id);
|
|
48
68
|
};
|
|
49
69
|
|
|
50
|
-
const update = async (model:
|
|
70
|
+
const update = async (model: EntityModel, { where, data: input }: { where: any; data: any }, ctx: FullContext) => {
|
|
51
71
|
if (Object.keys(where).length === 0) {
|
|
52
72
|
throw new Error(`No ${model.name} specified.`);
|
|
53
73
|
}
|
|
@@ -72,7 +92,30 @@ const update = async (model: Model, { where, data: input }: { where: any; data:
|
|
|
72
92
|
const next = { ...prev, ...normalizedInput };
|
|
73
93
|
const data = { prev, input, normalizedInput, next };
|
|
74
94
|
await ctx.mutationHook?.(model, 'update', 'before', data, ctx);
|
|
75
|
-
|
|
95
|
+
|
|
96
|
+
if (model.parent) {
|
|
97
|
+
const rootInput = {};
|
|
98
|
+
const childInput = {};
|
|
99
|
+
for (const field of model.fields) {
|
|
100
|
+
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
|
|
101
|
+
if (columnName in normalizedInput) {
|
|
102
|
+
if (field.inherited) {
|
|
103
|
+
rootInput[columnName] = normalizedInput[columnName];
|
|
104
|
+
} else {
|
|
105
|
+
childInput[columnName] = normalizedInput[columnName];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (Object.keys(rootInput).length) {
|
|
110
|
+
await ctx.knex(model.parent).where({ id: prev.id }).update(rootInput);
|
|
111
|
+
}
|
|
112
|
+
if (Object.keys(childInput).length) {
|
|
113
|
+
await ctx.knex(model.name).where({ id: prev.id }).update(childInput);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
await ctx.knex(model.name).where({ id: prev.id }).update(normalizedInput);
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
await createRevision(model, next, ctx);
|
|
77
120
|
await ctx.mutationHook?.(model, 'update', 'after', data, ctx);
|
|
78
121
|
}
|
|
@@ -82,12 +125,13 @@ const update = async (model: Model, { where, data: input }: { where: any; data:
|
|
|
82
125
|
|
|
83
126
|
type Callbacks = (() => Promise<void>)[];
|
|
84
127
|
|
|
85
|
-
const del = async (model:
|
|
128
|
+
const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun: boolean }, ctx: FullContext) => {
|
|
86
129
|
if (Object.keys(where).length === 0) {
|
|
87
130
|
throw new Error(`No ${model.name} specified.`);
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
const
|
|
133
|
+
const rootModel = model.rootModel;
|
|
134
|
+
const entity = await getEntityToMutate(ctx, rootModel, where, 'DELETE');
|
|
91
135
|
|
|
92
136
|
if (entity.deleted) {
|
|
93
137
|
throw new ForbiddenError('Entity is already deleted.');
|
|
@@ -107,7 +151,7 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
107
151
|
const mutations: Callbacks = [];
|
|
108
152
|
const afterHooks: Callbacks = [];
|
|
109
153
|
|
|
110
|
-
const deleteCascade = async (currentModel:
|
|
154
|
+
const deleteCascade = async (currentModel: EntityModel, entity: FullEntity) => {
|
|
111
155
|
if (entity.deleted) {
|
|
112
156
|
return;
|
|
113
157
|
}
|
|
@@ -140,10 +184,9 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
for (const {
|
|
143
|
-
|
|
144
|
-
foreignKey,
|
|
145
|
-
|
|
146
|
-
} of currentModel.reverseRelations) {
|
|
187
|
+
targetModel: descendantModel,
|
|
188
|
+
field: { name, foreignKey, onDelete },
|
|
189
|
+
} of currentModel.reverseRelations.filter((reverseRelation) => !reverseRelation.field.inherited)) {
|
|
147
190
|
const query = ctx.knex(descendantModel.name).where({ [foreignKey]: entity.id });
|
|
148
191
|
switch (onDelete) {
|
|
149
192
|
case 'set-null': {
|
|
@@ -189,7 +232,7 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
189
232
|
}
|
|
190
233
|
};
|
|
191
234
|
|
|
192
|
-
await deleteCascade(
|
|
235
|
+
await deleteCascade(rootModel, entity);
|
|
193
236
|
|
|
194
237
|
for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
|
|
195
238
|
await callback();
|
|
@@ -206,12 +249,14 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
206
249
|
return entity.id;
|
|
207
250
|
};
|
|
208
251
|
|
|
209
|
-
const restore = async (model:
|
|
252
|
+
const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullContext) => {
|
|
210
253
|
if (Object.keys(where).length === 0) {
|
|
211
254
|
throw new Error(`No ${model.name} specified.`);
|
|
212
255
|
}
|
|
213
256
|
|
|
214
|
-
const
|
|
257
|
+
const rootModel = model.rootModel;
|
|
258
|
+
|
|
259
|
+
const entity = await getEntityToMutate(ctx, rootModel, where, 'RESTORE');
|
|
215
260
|
|
|
216
261
|
if (!entity.deleted) {
|
|
217
262
|
throw new ForbiddenError('Entity is not deleted.');
|
|
@@ -221,7 +266,7 @@ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext
|
|
|
221
266
|
const mutations: Callbacks = [];
|
|
222
267
|
const afterHooks: Callbacks = [];
|
|
223
268
|
|
|
224
|
-
const restoreCascade = async (currentModel:
|
|
269
|
+
const restoreCascade = async (currentModel: EntityModel, relatedEntity: FullEntity) => {
|
|
225
270
|
if (!relatedEntity.deleted || !relatedEntity.deletedAt || !relatedEntity.deletedAt.equals(entity.deletedAt)) {
|
|
226
271
|
return;
|
|
227
272
|
}
|
|
@@ -243,9 +288,12 @@ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext
|
|
|
243
288
|
});
|
|
244
289
|
}
|
|
245
290
|
|
|
246
|
-
for (const {
|
|
247
|
-
|
|
248
|
-
|
|
291
|
+
for (const {
|
|
292
|
+
targetModel: descendantModel,
|
|
293
|
+
field: { foreignKey },
|
|
294
|
+
} of currentModel.reverseRelations
|
|
295
|
+
.filter((reverseRelation) => !reverseRelation.field.inherited)
|
|
296
|
+
.filter(({ targetModel: { deletable } }) => deletable)) {
|
|
249
297
|
const query = ctx.knex(descendantModel.name).where({ [foreignKey]: relatedEntity.id });
|
|
250
298
|
applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'RESTORE');
|
|
251
299
|
const descendants = await query;
|
|
@@ -255,7 +303,7 @@ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext
|
|
|
255
303
|
}
|
|
256
304
|
};
|
|
257
305
|
|
|
258
|
-
await restoreCascade(
|
|
306
|
+
await restoreCascade(rootModel, entity);
|
|
259
307
|
|
|
260
308
|
for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
|
|
261
309
|
await callback();
|
|
@@ -264,32 +312,46 @@ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext
|
|
|
264
312
|
return entity.id;
|
|
265
313
|
};
|
|
266
314
|
|
|
267
|
-
const createRevision = async (model:
|
|
315
|
+
const createRevision = async (model: EntityModel, data: Entity, ctx: Context) => {
|
|
268
316
|
if (model.updatable) {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
317
|
+
const revisionId = uuid();
|
|
318
|
+
const rootRevisionData: Entity = {
|
|
319
|
+
id: revisionId,
|
|
320
|
+
[`${typeToField(model.parent || model.name)}Id`]: data.id,
|
|
272
321
|
createdAt: ctx.now,
|
|
273
322
|
createdById: ctx.user.id,
|
|
274
323
|
};
|
|
275
324
|
|
|
276
325
|
if (model.deletable) {
|
|
277
|
-
|
|
326
|
+
rootRevisionData.deleted = data.deleted || false;
|
|
278
327
|
}
|
|
328
|
+
const childRevisionData = { id: revisionId };
|
|
279
329
|
|
|
280
|
-
for (const
|
|
281
|
-
const col =
|
|
282
|
-
|
|
283
|
-
|
|
330
|
+
for (const field of model.fields.filter(({ updatable }) => updatable)) {
|
|
331
|
+
const col = field.kind === 'relation' ? `${field.name}Id` : field.name;
|
|
332
|
+
let value;
|
|
333
|
+
if (field.nonNull && (!(col in data) || col === undefined || col === null)) {
|
|
334
|
+
value = get(field, 'defaultValue');
|
|
284
335
|
} else {
|
|
285
|
-
|
|
336
|
+
value = data[col];
|
|
286
337
|
}
|
|
338
|
+
if (!model.parent || field.inherited) {
|
|
339
|
+
rootRevisionData[col] = value;
|
|
340
|
+
} else {
|
|
341
|
+
childRevisionData[col] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (model.parent) {
|
|
346
|
+
await ctx.knex(`${model.parent}Revision`).insert(rootRevisionData);
|
|
347
|
+
await ctx.knex(`${model.name}Revision`).insert(childRevisionData);
|
|
348
|
+
} else {
|
|
349
|
+
await ctx.knex(`${model.name}Revision`).insert(rootRevisionData);
|
|
287
350
|
}
|
|
288
|
-
await ctx.knex(`${model.name}Revision`).insert(revisionData);
|
|
289
351
|
}
|
|
290
352
|
};
|
|
291
353
|
|
|
292
|
-
const sanitize = (ctx: FullContext, model:
|
|
354
|
+
const sanitize = (ctx: FullContext, model: EntityModel, data: Entity) => {
|
|
293
355
|
if (model.updatable) {
|
|
294
356
|
data.updatedAt = ctx.now;
|
|
295
357
|
data.updatedById = ctx.user.id;
|
|
@@ -307,12 +369,12 @@ const sanitize = (ctx: FullContext, model: Model, data: Entity) => {
|
|
|
307
369
|
continue;
|
|
308
370
|
}
|
|
309
371
|
|
|
310
|
-
if (
|
|
372
|
+
if (field.list && field.kind === 'enum' && Array.isArray(data[key])) {
|
|
311
373
|
data[key] = `{${(data[key] as string[]).join(',')}}`;
|
|
312
374
|
continue;
|
|
313
375
|
}
|
|
314
376
|
}
|
|
315
377
|
};
|
|
316
378
|
|
|
317
|
-
const isEndOfDay = (field?:
|
|
379
|
+
const isEndOfDay = (field?: EntityField) =>
|
|
318
380
|
isPrimitive(field) && field.type === 'DateTime' && field?.endOfDay === true && field?.dateTimeType === 'date';
|
package/src/resolvers/node.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type {
|
|
|
8
8
|
} from 'graphql';
|
|
9
9
|
|
|
10
10
|
import { FullContext } from '../context';
|
|
11
|
-
import {
|
|
12
|
-
import { get, isObjectModel, summonByKey
|
|
11
|
+
import { EntityModel, Relation } from '../models/models';
|
|
12
|
+
import { get, isObjectModel, summonByKey } from '../models/utils';
|
|
13
13
|
import {
|
|
14
14
|
getFragmentTypeName,
|
|
15
15
|
getNameOrAlias,
|
|
@@ -24,15 +24,18 @@ import {
|
|
|
24
24
|
export type ResolverNode = {
|
|
25
25
|
ctx: FullContext;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
rootModel: EntityModel;
|
|
28
|
+
rootTableAlias: string;
|
|
29
|
+
|
|
30
|
+
model: EntityModel;
|
|
28
31
|
tableAlias: string;
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
resultAlias: string;
|
|
30
34
|
|
|
31
35
|
baseTypeDefinition: ObjectTypeDefinitionNode;
|
|
32
|
-
baseModel?:
|
|
36
|
+
baseModel?: EntityModel;
|
|
33
37
|
|
|
34
38
|
typeDefinition: ObjectTypeDefinitionNode;
|
|
35
|
-
model: Model;
|
|
36
39
|
|
|
37
40
|
selectionSet: readonly SelectionNode[];
|
|
38
41
|
};
|
|
@@ -40,24 +43,20 @@ export type ResolverNode = {
|
|
|
40
43
|
export type FieldResolverNode = ResolverNode & {
|
|
41
44
|
field: FieldNode;
|
|
42
45
|
fieldDefinition: FieldDefinitionNode;
|
|
43
|
-
foreignKey?: string;
|
|
44
46
|
isList: boolean;
|
|
45
47
|
};
|
|
46
48
|
|
|
47
|
-
export type
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
tableAlias: string;
|
|
51
|
-
shortTableAlias: string;
|
|
52
|
-
model: Model;
|
|
53
|
-
|
|
54
|
-
foreignKey?: string;
|
|
49
|
+
export type RelationResolverNode = FieldResolverNode & {
|
|
50
|
+
relation: Relation;
|
|
51
|
+
foreignKey: string;
|
|
55
52
|
};
|
|
56
53
|
|
|
57
54
|
export const getResolverNode = ({
|
|
58
55
|
ctx,
|
|
59
56
|
node,
|
|
60
57
|
tableAlias,
|
|
58
|
+
rootTableAlias,
|
|
59
|
+
resultAlias,
|
|
61
60
|
baseTypeDefinition,
|
|
62
61
|
typeName,
|
|
63
62
|
}: {
|
|
@@ -65,18 +64,30 @@ export const getResolverNode = ({
|
|
|
65
64
|
node: FieldNode | InlineFragmentNode | FragmentDefinitionNode;
|
|
66
65
|
baseTypeDefinition: ObjectTypeDefinitionNode;
|
|
67
66
|
tableAlias: string;
|
|
67
|
+
rootTableAlias: string;
|
|
68
|
+
resultAlias: string;
|
|
68
69
|
typeName: string;
|
|
69
|
-
}): ResolverNode =>
|
|
70
|
-
ctx,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
}): ResolverNode => {
|
|
71
|
+
const model = ctx.models.getModel(typeName, 'entity');
|
|
72
|
+
const rootModel = model.parent ? ctx.models.getModel(model.parent, 'entity') : model;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
ctx,
|
|
76
|
+
|
|
77
|
+
rootModel,
|
|
78
|
+
rootTableAlias,
|
|
79
|
+
|
|
80
|
+
model,
|
|
81
|
+
tableAlias,
|
|
82
|
+
|
|
83
|
+
resultAlias,
|
|
84
|
+
|
|
85
|
+
baseTypeDefinition,
|
|
86
|
+
baseModel: ctx.models.entities.find((model) => model.name === baseTypeDefinition.name.value),
|
|
87
|
+
typeDefinition: getType(ctx.info.schema, typeName),
|
|
88
|
+
selectionSet: get(node.selectionSet, 'selections'),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
80
91
|
|
|
81
92
|
export const getRootFieldNode = ({
|
|
82
93
|
ctx,
|
|
@@ -91,15 +102,22 @@ export const getRootFieldNode = ({
|
|
|
91
102
|
const fieldDefinition = summonByKey(baseTypeDefinition.fields || [], 'name.value', fieldName);
|
|
92
103
|
|
|
93
104
|
const typeName = getTypeName(fieldDefinition.type);
|
|
105
|
+
const model = ctx.models.getModel(typeName, 'entity');
|
|
106
|
+
const rootModel = model.parent ? ctx.models.getModel(model.parent, 'entity') : model;
|
|
94
107
|
|
|
95
108
|
return {
|
|
96
109
|
ctx,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
|
|
111
|
+
rootModel,
|
|
112
|
+
rootTableAlias: rootModel.name,
|
|
113
|
+
|
|
114
|
+
model,
|
|
115
|
+
tableAlias: model.name,
|
|
116
|
+
|
|
117
|
+
resultAlias: rootModel.name,
|
|
118
|
+
|
|
100
119
|
baseTypeDefinition,
|
|
101
120
|
typeDefinition: getType(ctx.info.schema, typeName),
|
|
102
|
-
model: summonByName(ctx.models, typeName),
|
|
103
121
|
selectionSet: get(node.selectionSet, 'selections'),
|
|
104
122
|
field: node,
|
|
105
123
|
fieldDefinition,
|
|
@@ -122,7 +140,13 @@ export const getInlineFragments = (node: ResolverNode) =>
|
|
|
122
140
|
getResolverNode({
|
|
123
141
|
ctx: node.ctx,
|
|
124
142
|
node: subNode,
|
|
143
|
+
|
|
144
|
+
rootTableAlias: node.rootTableAlias,
|
|
145
|
+
|
|
125
146
|
tableAlias: node.tableAlias + '__' + getFragmentTypeName(subNode),
|
|
147
|
+
|
|
148
|
+
resultAlias: node.resultAlias,
|
|
149
|
+
|
|
126
150
|
baseTypeDefinition: node.baseTypeDefinition,
|
|
127
151
|
typeName: getFragmentTypeName(subNode),
|
|
128
152
|
})
|
|
@@ -133,14 +157,20 @@ export const getFragmentSpreads = (node: ResolverNode) =>
|
|
|
133
157
|
getResolverNode({
|
|
134
158
|
ctx: node.ctx,
|
|
135
159
|
node: node.ctx.info.fragments[subNode.name.value],
|
|
160
|
+
|
|
161
|
+
rootTableAlias: node.rootTableAlias,
|
|
162
|
+
|
|
136
163
|
tableAlias: node.tableAlias,
|
|
164
|
+
|
|
165
|
+
resultAlias: node.resultAlias,
|
|
166
|
+
|
|
137
167
|
baseTypeDefinition: node.baseTypeDefinition,
|
|
138
168
|
typeName: node.model.name,
|
|
139
169
|
})
|
|
140
170
|
);
|
|
141
171
|
|
|
142
172
|
export const getJoins = (node: ResolverNode, toMany: boolean) => {
|
|
143
|
-
const nodes:
|
|
173
|
+
const nodes: RelationResolverNode[] = [];
|
|
144
174
|
for (const subNode of node.selectionSet.filter(isFieldNode).filter(({ selectionSet }) => selectionSet)) {
|
|
145
175
|
const ctx = node.ctx;
|
|
146
176
|
const baseTypeDefinition = node.typeDefinition;
|
|
@@ -150,42 +180,40 @@ export const getJoins = (node: ResolverNode, toMany: boolean) => {
|
|
|
150
180
|
|
|
151
181
|
const typeName = getTypeName(fieldDefinition.type);
|
|
152
182
|
|
|
153
|
-
if (isObjectModel(
|
|
183
|
+
if (isObjectModel(ctx.models.getModel(typeName))) {
|
|
154
184
|
continue;
|
|
155
185
|
}
|
|
156
186
|
|
|
157
|
-
const baseModel =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
if (!reverseRelation) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
foreignKey = reverseRelation.foreignKey;
|
|
166
|
-
} else {
|
|
167
|
-
const modelField = baseModel.fieldsByName[fieldName];
|
|
168
|
-
if (modelField?.kind !== 'relation') {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
foreignKey = modelField.foreignKey;
|
|
187
|
+
const baseModel = ctx.models.getModel(baseTypeDefinition.name.value, 'entity');
|
|
188
|
+
|
|
189
|
+
const relation = (toMany ? baseModel.reverseRelationsByName : baseModel.relationsByName)[fieldName];
|
|
190
|
+
if (!relation) {
|
|
191
|
+
continue;
|
|
172
192
|
}
|
|
173
193
|
|
|
174
194
|
const tableAlias = node.tableAlias + '__' + fieldNameOrAlias;
|
|
195
|
+
const model = ctx.models.getModel(typeName, 'entity');
|
|
196
|
+
const rootModel = model;
|
|
175
197
|
|
|
176
198
|
nodes.push({
|
|
177
199
|
ctx,
|
|
178
|
-
|
|
200
|
+
|
|
201
|
+
rootModel,
|
|
202
|
+
rootTableAlias: tableAlias,
|
|
203
|
+
|
|
204
|
+
model,
|
|
179
205
|
tableAlias,
|
|
180
|
-
|
|
206
|
+
|
|
207
|
+
resultAlias: tableAlias,
|
|
208
|
+
|
|
181
209
|
baseTypeDefinition,
|
|
182
210
|
baseModel,
|
|
183
211
|
typeDefinition: getType(ctx.info.schema, typeName),
|
|
184
|
-
model: summonByName(ctx.models, typeName),
|
|
185
212
|
selectionSet: get(subNode.selectionSet, 'selections'),
|
|
186
213
|
field: subNode,
|
|
187
214
|
fieldDefinition,
|
|
188
|
-
|
|
215
|
+
relation,
|
|
216
|
+
foreignKey: relation.field.foreignKey,
|
|
189
217
|
isList: isListType(fieldDefinition.type),
|
|
190
218
|
});
|
|
191
219
|
}
|
|
@@ -3,21 +3,14 @@ import { Knex } from 'knex';
|
|
|
3
3
|
import cloneDeep from 'lodash/cloneDeep';
|
|
4
4
|
import flatMap from 'lodash/flatMap';
|
|
5
5
|
import { Context, FullContext } from '../context';
|
|
6
|
-
import { NotFoundError
|
|
6
|
+
import { NotFoundError } from '../errors';
|
|
7
7
|
import { get, summonByKey } from '../models/utils';
|
|
8
8
|
import { applyPermissions } from '../permissions/check';
|
|
9
9
|
import { PermissionStack } from '../permissions/generate';
|
|
10
10
|
import { applyFilters } from './filters';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
getFragmentSpreads,
|
|
15
|
-
getInlineFragments,
|
|
16
|
-
getJoins,
|
|
17
|
-
getRootFieldNode,
|
|
18
|
-
getSimpleFields,
|
|
19
|
-
} from './node';
|
|
20
|
-
import { AliasGenerator, Entry, ID_ALIAS, Joins, addJoin, applyJoins, getNameOrAlias, hydrate, isListType } from './utils';
|
|
11
|
+
import { FieldResolverNode, ResolverNode, getFragmentSpreads, getInlineFragments, getJoins, getRootFieldNode } from './node';
|
|
12
|
+
import { applySelects } from './selects';
|
|
13
|
+
import { AliasGenerator, Entry, ID_ALIAS, Joins, applyJoins, getColumn, getNameOrAlias, hydrate, isListType } from './utils';
|
|
21
14
|
|
|
22
15
|
export const queryResolver = (_parent: any, _args: any, ctx: Context, info: GraphQLResolveInfo) =>
|
|
23
16
|
resolve({ ...ctx, info, aliases: new AliasGenerator() });
|
|
@@ -36,18 +29,15 @@ export const resolve = async (ctx: FullContext, id?: string) => {
|
|
|
36
29
|
const { query, verifiedPermissionStacks } = await buildQuery(node);
|
|
37
30
|
|
|
38
31
|
if (ctx.info.fieldName === 'me') {
|
|
39
|
-
|
|
40
|
-
query.where({ [`${node.shortTableAlias}.id`]: node.ctx.user.id });
|
|
32
|
+
void query.where({ [getColumn(node, 'id')]: node.ctx.user.id });
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
query.limit(1);
|
|
35
|
+
if (id) {
|
|
36
|
+
void query.where({ [getColumn(node, 'id')]: id });
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
query.where({ id });
|
|
39
|
+
if (!node.isList) {
|
|
40
|
+
void query.limit(1);
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
const raw = await query;
|
|
@@ -74,22 +64,22 @@ const buildQuery = async (
|
|
|
74
64
|
node: FieldResolverNode,
|
|
75
65
|
parentVerifiedPermissionStacks?: VerifiedPermissionStacks
|
|
76
66
|
): Promise<{ query: Knex.QueryBuilder; verifiedPermissionStacks: VerifiedPermissionStacks }> => {
|
|
77
|
-
const
|
|
78
|
-
const query = ctx.knex.fromRaw(`"${tableName}" as "${shortTableAlias}"`);
|
|
67
|
+
const query = node.ctx.knex.fromRaw(`"${node.rootModel.name}" as "${node.ctx.aliases.getShort(node.resultAlias)}"`);
|
|
79
68
|
|
|
80
|
-
const joins: Joins =
|
|
69
|
+
const joins: Joins = [];
|
|
81
70
|
applyFilters(node, query, joins);
|
|
82
71
|
applySelects(node, query, joins);
|
|
83
72
|
applyJoins(node.ctx.aliases, query, joins);
|
|
84
73
|
|
|
85
74
|
const tables = [
|
|
86
|
-
[
|
|
87
|
-
...
|
|
75
|
+
[node.rootModel.name, node.rootTableAlias] satisfies [string, string],
|
|
76
|
+
...joins.map(({ table2Name, table2Alias }) => [table2Name, table2Alias] satisfies [string, string]),
|
|
88
77
|
];
|
|
78
|
+
|
|
89
79
|
const verifiedPermissionStacks: VerifiedPermissionStacks = {};
|
|
90
80
|
for (const [table, alias] of tables) {
|
|
91
81
|
const verifiedPermissionStack = applyPermissions(
|
|
92
|
-
ctx,
|
|
82
|
+
node.ctx,
|
|
93
83
|
table,
|
|
94
84
|
node.ctx.aliases.getShort(alias),
|
|
95
85
|
query,
|
|
@@ -105,52 +95,6 @@ const buildQuery = async (
|
|
|
105
95
|
return { query, verifiedPermissionStacks };
|
|
106
96
|
};
|
|
107
97
|
|
|
108
|
-
const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins: Joins) => {
|
|
109
|
-
// Simple field selects
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
111
|
-
query.select(
|
|
112
|
-
...[
|
|
113
|
-
{ field: 'id', alias: ID_ALIAS },
|
|
114
|
-
...getSimpleFields(node)
|
|
115
|
-
.filter((n) => {
|
|
116
|
-
const field = node.model.fields.find(({ name }) => name === n.name.value);
|
|
117
|
-
|
|
118
|
-
if (!field || field.kind === 'relation' || field.kind === 'custom') {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (typeof field.queriable === 'object' && !field.queriable.roles?.includes(node.ctx.user.role)) {
|
|
123
|
-
throw new PermissionError(
|
|
124
|
-
'READ',
|
|
125
|
-
`${node.model.name}'s field "${field.name}"`,
|
|
126
|
-
'field permission not available'
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return true;
|
|
131
|
-
})
|
|
132
|
-
.map((n) => ({ field: n.name.value, alias: getNameOrAlias(n) })),
|
|
133
|
-
].map(
|
|
134
|
-
({ field, alias }: { field: string; alias: string }) =>
|
|
135
|
-
`${node.shortTableAlias}.${field} as ${node.shortTableAlias}__${alias}`
|
|
136
|
-
)
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
for (const subNode of getInlineFragments(node)) {
|
|
140
|
-
applySelects(subNode, query, joins);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
for (const subNode of getFragmentSpreads(node)) {
|
|
144
|
-
applySelects(subNode, query, joins);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const subNode of getJoins(node, false)) {
|
|
148
|
-
addJoin(joins, node.tableAlias, subNode.tableName, subNode.tableAlias, get(subNode, 'foreignKey'), 'id');
|
|
149
|
-
|
|
150
|
-
applySelects(subNode, query, joins);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
98
|
const applySubQueries = async (
|
|
155
99
|
node: ResolverNode,
|
|
156
100
|
entries: Entry[],
|
|
@@ -174,13 +118,15 @@ const applySubQueries = async (
|
|
|
174
118
|
const fieldName = getNameOrAlias(subNode.field);
|
|
175
119
|
const isList = isListType(subNode.fieldDefinition.type);
|
|
176
120
|
entries.forEach((entry) => (entry[fieldName] = isList ? [] : null));
|
|
177
|
-
const foreignKey =
|
|
121
|
+
const foreignKey = subNode.foreignKey;
|
|
178
122
|
const { query, verifiedPermissionStacks } = await buildQuery(subNode, parentVerifiedPermissionStacks);
|
|
123
|
+
const shortTableAlias = subNode.ctx.aliases.getShort(subNode.tableAlias);
|
|
124
|
+
const shortResultAlias = subNode.ctx.aliases.getShort(subNode.resultAlias);
|
|
179
125
|
const queries = ids.map((id) =>
|
|
180
126
|
query
|
|
181
127
|
.clone()
|
|
182
|
-
.select(`${
|
|
183
|
-
.where({ [`${
|
|
128
|
+
.select(`${shortTableAlias}.${foreignKey} as ${shortResultAlias}__${foreignKey}`)
|
|
129
|
+
.where({ [`${shortTableAlias}.${foreignKey}`]: id })
|
|
184
130
|
);
|
|
185
131
|
|
|
186
132
|
// TODO: make unionAll faster then promise.all...
|