@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.
Files changed (120) hide show
  1. package/.eslintrc +2 -10
  2. package/.github/workflows/release.yml +1 -1
  3. package/.gqmrc.json +6 -0
  4. package/CHANGELOG.md +2 -2
  5. package/README.md +1 -1
  6. package/dist/bin/gqm.cjs +684 -330
  7. package/dist/cjs/index.cjs +998 -554
  8. package/dist/esm/api/execute.js +1 -1
  9. package/dist/esm/api/execute.js.map +1 -1
  10. package/dist/esm/client/mutations.d.ts +2 -2
  11. package/dist/esm/client/mutations.js +5 -4
  12. package/dist/esm/client/mutations.js.map +1 -1
  13. package/dist/esm/client/queries.d.ts +12 -17
  14. package/dist/esm/client/queries.js +30 -50
  15. package/dist/esm/client/queries.js.map +1 -1
  16. package/dist/esm/context.d.ts +1 -2
  17. package/dist/esm/db/generate.d.ts +3 -3
  18. package/dist/esm/db/generate.js +31 -29
  19. package/dist/esm/db/generate.js.map +1 -1
  20. package/dist/esm/migrations/generate.d.ts +3 -4
  21. package/dist/esm/migrations/generate.js +114 -107
  22. package/dist/esm/migrations/generate.js.map +1 -1
  23. package/dist/esm/models/index.d.ts +1 -0
  24. package/dist/esm/models/index.js +1 -0
  25. package/dist/esm/models/index.js.map +1 -1
  26. package/dist/esm/models/model-definitions.d.ts +189 -0
  27. package/dist/esm/models/model-definitions.js +2 -0
  28. package/dist/esm/models/model-definitions.js.map +1 -0
  29. package/dist/esm/models/models.d.ts +128 -174
  30. package/dist/esm/models/models.js +411 -1
  31. package/dist/esm/models/models.js.map +1 -1
  32. package/dist/esm/models/mutation-hook.d.ts +2 -2
  33. package/dist/esm/models/utils.d.ts +35 -497
  34. package/dist/esm/models/utils.js +21 -144
  35. package/dist/esm/models/utils.js.map +1 -1
  36. package/dist/esm/permissions/check.d.ts +3 -3
  37. package/dist/esm/permissions/check.js +14 -7
  38. package/dist/esm/permissions/check.js.map +1 -1
  39. package/dist/esm/permissions/generate.js +6 -6
  40. package/dist/esm/permissions/generate.js.map +1 -1
  41. package/dist/esm/resolvers/filters.d.ts +8 -0
  42. package/dist/esm/resolvers/filters.js +28 -25
  43. package/dist/esm/resolvers/filters.js.map +1 -1
  44. package/dist/esm/resolvers/index.d.ts +1 -0
  45. package/dist/esm/resolvers/index.js +1 -0
  46. package/dist/esm/resolvers/index.js.map +1 -1
  47. package/dist/esm/resolvers/mutations.js +85 -21
  48. package/dist/esm/resolvers/mutations.js.map +1 -1
  49. package/dist/esm/resolvers/node.d.ts +13 -15
  50. package/dist/esm/resolvers/node.js +41 -36
  51. package/dist/esm/resolvers/node.js.map +1 -1
  52. package/dist/esm/resolvers/resolver.js +19 -49
  53. package/dist/esm/resolvers/resolver.js.map +1 -1
  54. package/dist/esm/resolvers/resolvers.d.ts +1 -8
  55. package/dist/esm/resolvers/resolvers.js +15 -7
  56. package/dist/esm/resolvers/resolvers.js.map +1 -1
  57. package/dist/esm/resolvers/selects.d.ts +3 -0
  58. package/dist/esm/resolvers/selects.js +50 -0
  59. package/dist/esm/resolvers/selects.js.map +1 -0
  60. package/dist/esm/resolvers/utils.d.ts +12 -4
  61. package/dist/esm/resolvers/utils.js +30 -22
  62. package/dist/esm/resolvers/utils.js.map +1 -1
  63. package/dist/esm/schema/generate.d.ts +4 -4
  64. package/dist/esm/schema/generate.js +122 -131
  65. package/dist/esm/schema/generate.js.map +1 -1
  66. package/dist/esm/schema/utils.d.ts +1 -1
  67. package/dist/esm/schema/utils.js +2 -1
  68. package/dist/esm/schema/utils.js.map +1 -1
  69. package/knexfile.ts +31 -0
  70. package/migrations/20230912185644_setup.ts +127 -0
  71. package/package.json +16 -14
  72. package/src/api/execute.ts +1 -1
  73. package/src/bin/gqm/gqm.ts +25 -23
  74. package/src/bin/gqm/parse-models.ts +5 -5
  75. package/src/bin/gqm/settings.ts +13 -4
  76. package/src/bin/gqm/static-eval.ts +5 -0
  77. package/src/bin/gqm/templates.ts +23 -3
  78. package/src/client/mutations.ts +11 -5
  79. package/src/client/queries.ts +43 -80
  80. package/src/context.ts +1 -2
  81. package/src/db/generate.ts +41 -41
  82. package/src/migrations/generate.ts +165 -146
  83. package/src/models/index.ts +1 -0
  84. package/src/models/model-definitions.ts +168 -0
  85. package/src/models/models.ts +510 -166
  86. package/src/models/mutation-hook.ts +2 -2
  87. package/src/models/utils.ts +53 -187
  88. package/src/permissions/check.ts +19 -11
  89. package/src/permissions/generate.ts +6 -6
  90. package/src/resolvers/filters.ts +44 -28
  91. package/src/resolvers/index.ts +1 -0
  92. package/src/resolvers/mutations.ts +98 -36
  93. package/src/resolvers/node.ts +79 -51
  94. package/src/resolvers/resolver.ts +20 -74
  95. package/src/resolvers/resolvers.ts +18 -7
  96. package/src/resolvers/selects.ts +77 -0
  97. package/src/resolvers/utils.ts +41 -25
  98. package/src/schema/generate.ts +106 -127
  99. package/src/schema/utils.ts +2 -1
  100. package/tests/api/__snapshots__/inheritance.spec.ts.snap +83 -0
  101. package/tests/api/inheritance.spec.ts +130 -0
  102. package/tests/generated/api/index.ts +1174 -0
  103. package/tests/generated/client/index.ts +1163 -0
  104. package/tests/generated/client/mutations.ts +109 -0
  105. package/tests/generated/db/index.ts +291 -0
  106. package/tests/generated/db/knex.ts +14 -0
  107. package/tests/generated/models.json +675 -0
  108. package/tests/generated/schema.graphql +325 -0
  109. package/tests/unit/__snapshots__/resolve.spec.ts.snap +23 -0
  110. package/tests/unit/queries.spec.ts +5 -5
  111. package/tests/unit/resolve.spec.ts +8 -8
  112. package/tests/utils/database/knex.ts +5 -13
  113. package/tests/utils/database/seed.ts +57 -18
  114. package/tests/utils/models.ts +62 -7
  115. package/tests/utils/server.ts +5 -5
  116. package/tsconfig.eslint.json +1 -0
  117. package/tests/unit/__snapshots__/generate.spec.ts.snap +0 -128
  118. package/tests/unit/generate.spec.ts +0 -8
  119. package/tests/utils/database/schema.ts +0 -64
  120. 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 { Model, ModelField } from '../models/models';
6
+ import { EntityField, EntityModel } from '../models/models';
7
7
  import { Entity, FullEntity } from '../models/mutation-hook';
8
- import { get, isEnumList, isPrimitive, it, summonByName, typeToField } from '../models/utils';
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 = summonByName(ctx.models, modelName);
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: Model, { data: input }: { data: any }, ctx: FullContext) => {
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
- await ctx.knex(model.name).insert(normalizedInput);
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: Model, { where, data: input }: { where: any; data: any }, ctx: FullContext) => {
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
- await ctx.knex(model.name).where(where).update(normalizedInput);
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: Model, { where, dryRun }: { where: any; dryRun: boolean }, ctx: FullContext) => {
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 entity = await getEntityToMutate(ctx, model, where, 'DELETE');
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: Model, entity: FullEntity) => {
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
- model: descendantModel,
144
- foreignKey,
145
- field: { name, onDelete },
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(model, entity);
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: Model, { where }: { where: any }, ctx: FullContext) => {
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 entity = await getEntityToMutate(ctx, model, where, 'RESTORE');
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: Model, relatedEntity: FullEntity) => {
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 { model: descendantModel, foreignKey } of currentModel.reverseRelations.filter(
247
- ({ model: { deletable } }) => deletable
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(model, entity);
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: Model, data: Entity, ctx: Context) => {
315
+ const createRevision = async (model: EntityModel, data: Entity, ctx: Context) => {
268
316
  if (model.updatable) {
269
- const revisionData: Entity = {
270
- id: uuid(),
271
- [`${typeToField(model.name)}Id`]: data.id,
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
- revisionData.deleted = data.deleted || false;
326
+ rootRevisionData.deleted = data.deleted || false;
278
327
  }
328
+ const childRevisionData = { id: revisionId };
279
329
 
280
- for (const { kind: type, name, nonNull, ...field } of model.fields.filter(({ updatable }) => updatable)) {
281
- const col = type === 'relation' ? `${name}Id` : name;
282
- if (nonNull && (!(col in data) || col === undefined || col === null)) {
283
- revisionData[col] = get(field, 'defaultValue');
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
- revisionData[col] = data[col];
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: Model, data: Entity) => {
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 (isEnumList(ctx.rawModels, field) && Array.isArray(data[key])) {
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?: ModelField) =>
379
+ const isEndOfDay = (field?: EntityField) =>
318
380
  isPrimitive(field) && field.type === 'DateTime' && field?.endOfDay === true && field?.dateTimeType === 'date';
@@ -8,8 +8,8 @@ import type {
8
8
  } from 'graphql';
9
9
 
10
10
  import { FullContext } from '../context';
11
- import { Model } from '../models/models';
12
- import { get, isObjectModel, summonByKey, summonByName } from '../models/utils';
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
- tableName: string;
27
+ rootModel: EntityModel;
28
+ rootTableAlias: string;
29
+
30
+ model: EntityModel;
28
31
  tableAlias: string;
29
- shortTableAlias: string;
32
+
33
+ resultAlias: string;
30
34
 
31
35
  baseTypeDefinition: ObjectTypeDefinitionNode;
32
- baseModel?: Model;
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 WhereNode = {
48
- ctx: FullContext;
49
- tableName: string;
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
- tableName: typeName,
72
- tableAlias,
73
- shortTableAlias: ctx.aliases.getShort(tableAlias),
74
- baseTypeDefinition,
75
- baseModel: ctx.models.find((model) => model.name === baseTypeDefinition.name.value),
76
- typeDefinition: getType(ctx.info.schema, typeName),
77
- model: summonByName(ctx.models, typeName),
78
- selectionSet: get(node.selectionSet, 'selections'),
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
- tableName: typeName,
98
- tableAlias: typeName,
99
- shortTableAlias: ctx.aliases.getShort(typeName),
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: FieldResolverNode[] = [];
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(summonByName(ctx.rawModels, typeName))) {
183
+ if (isObjectModel(ctx.models.getModel(typeName))) {
154
184
  continue;
155
185
  }
156
186
 
157
- const baseModel = summonByName(ctx.models, baseTypeDefinition.name.value);
158
-
159
- let foreignKey: string | undefined;
160
- if (toMany) {
161
- const reverseRelation = baseModel.reverseRelationsByName[fieldName];
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
- tableName: typeName,
200
+
201
+ rootModel,
202
+ rootTableAlias: tableAlias,
203
+
204
+ model,
179
205
  tableAlias,
180
- shortTableAlias: ctx.aliases.getShort(tableAlias),
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
- foreignKey,
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, PermissionError } from '../errors';
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
- FieldResolverNode,
13
- ResolverNode,
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
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
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 (!node.isList) {
44
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
45
- query.limit(1);
35
+ if (id) {
36
+ void query.where({ [getColumn(node, 'id')]: id });
46
37
  }
47
38
 
48
- if (id) {
49
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
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 { tableAlias, shortTableAlias, tableName, model, ctx } = node;
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
- [model.name, tableAlias] as const,
87
- ...Object.keys(joins).map((tableName) => tableName.split(':') as [string, string]),
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 = get(subNode, '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(`${subNode.shortTableAlias}.${foreignKey} as ${subNode.shortTableAlias}__${foreignKey}`)
183
- .where({ [`${subNode.shortTableAlias}.${foreignKey}`]: id })
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...