@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
@@ -1,6 +1,6 @@
1
1
  import { DateTime } from 'luxon';
2
- import { Model } from '.';
3
2
  import { Context } from '..';
3
+ import { EntityModel } from './models';
4
4
 
5
5
  export type Entity = Record<string, unknown> & { createdAt?: DateTime; deletedAt?: DateTime };
6
6
 
@@ -9,7 +9,7 @@ export type FullEntity = Entity & { id: string };
9
9
  export type Action = 'create' | 'update' | 'delete' | 'restore';
10
10
 
11
11
  export type MutationHook = (
12
- model: Model,
12
+ model: EntityModel,
13
13
  action: Action,
14
14
  when: 'before' | 'after',
15
15
  data: { prev: Entity; input: Entity; normalizedInput: Entity; next: FullEntity },
@@ -1,28 +1,19 @@
1
1
  import assert from 'assert';
2
- import { pluralize } from 'inflection';
3
- import camelCase from 'lodash/camelCase';
2
+ import { camelCase, startCase } from 'lodash';
4
3
  import lodashGet from 'lodash/get';
5
- import kebabCase from 'lodash/kebabCase';
6
- import startCase from 'lodash/startCase';
7
4
  import {
8
- BooleanField,
9
5
  CustomField,
10
- DateTimeField,
6
+ EntityField,
11
7
  EntityModel,
12
8
  EnumField,
13
9
  EnumModel,
14
10
  InputModel,
11
+ InterfaceModel,
15
12
  Model,
16
- ModelField,
17
- Models,
18
13
  ObjectModel,
19
14
  PrimitiveField,
20
15
  RawEnumModel,
21
- RawModel,
22
- RawModels,
23
- Relation,
24
16
  RelationField,
25
- ReverseRelation,
26
17
  ScalarModel,
27
18
  } from './models';
28
19
 
@@ -34,216 +25,90 @@ export const merge = <T>(objects: ({ [name: string]: T } | undefined | false)[]
34
25
  // Target -> target
35
26
  export const typeToField = (type: string) => type.substr(0, 1).toLowerCase() + type.substr(1);
36
27
 
37
- export const getModelPlural = (model: EntityModel | Model) => model.plural || pluralize(model.name);
28
+ export const getLabel = (s: string) => startCase(camelCase(s));
38
29
 
39
- export const getModelPluralField = (model: Model) => typeToField(getModelPlural(model));
30
+ export const or =
31
+ <T>(...predicates: ((field: T) => boolean)[]) =>
32
+ (field: T) =>
33
+ predicates.some((predicate) => predicate(field));
40
34
 
41
- export const getModelSlug = (model: Model) => kebabCase(getModelPlural(model));
35
+ export const and =
36
+ <T>(...predicates: ((field: T) => boolean)[]) =>
37
+ (field: T) =>
38
+ predicates.every((predicate) => predicate(field));
42
39
 
43
- export const getModelLabelPlural = (model: Model) => getLabel(getModelPlural(model));
40
+ export const not =
41
+ <T>(predicate: (field: T) => boolean) =>
42
+ (field: T) =>
43
+ !predicate(field);
44
44
 
45
- export const getModelLabel = (model: Model) => getLabel(model.name);
45
+ export const isRootModel = (model: EntityModel) => model.root;
46
46
 
47
- export const getLabel = (s: string) => startCase(camelCase(s));
47
+ export const isEntityModel = (model: Model): model is EntityModel => model instanceof EntityModel;
48
48
 
49
- export const isEntityModel = (model: RawModel): model is EntityModel => model.kind === 'entity';
49
+ export const isEnumModel = (model: Model): model is EnumModel => model instanceof EnumModel;
50
50
 
51
- export const isEnumModel = (model: RawModel): model is EnumModel => model.kind === 'enum';
51
+ export const isRawEnumModel = (model: Model): model is RawEnumModel => model instanceof RawEnumModel;
52
52
 
53
- export const isRawEnumModel = (model: RawModel): model is RawEnumModel => model.kind === 'raw-enum';
53
+ export const isScalarModel = (model: Model): model is ScalarModel => model instanceof ScalarModel;
54
54
 
55
- export const isScalarModel = (model: RawModel): model is ScalarModel => model.kind === 'scalar';
55
+ export const isObjectModel = (model: Model): model is ObjectModel => model instanceof ObjectModel;
56
56
 
57
- export const isObjectModel = (model: RawModel): model is ObjectModel => model.kind === 'object';
57
+ export const isInputModel = (model: Model): model is InputModel => model instanceof InputModel;
58
58
 
59
- export const isInputModel = (model: RawModel): model is InputModel => model.kind === 'input';
59
+ export const isInterfaceModel = (model: Model): model is InterfaceModel => model instanceof InterfaceModel;
60
60
 
61
- export const isEnumList = (models: RawModels, field: ModelField) =>
62
- field?.list === true && models.find(({ name }) => name === field.kind)?.kind === 'enum';
61
+ export const isUpdatableModel = (model: EntityModel) => model.updatable && model.fields.some(isUpdatableField);
63
62
 
64
- export const and =
65
- (...predicates: ((field: ModelField) => boolean)[]) =>
66
- (field: ModelField) =>
67
- predicates.every((predicate) => predicate(field));
63
+ export const isUpdatableField = (field: EntityField) => !field.inherited && !!field.updatable;
68
64
 
69
- export const not = (predicate: (field: ModelField) => boolean) => (field: ModelField) => !predicate(field);
65
+ export const modelNeedsTable = (model: EntityModel) => model.fields.some((field) => !field.inherited);
70
66
 
71
- export const isPrimitive = (field: ModelField): field is PrimitiveField =>
67
+ export const hasName = (name: string) => (field: EntityField) => field.name == name;
68
+
69
+ export const isPrimitive = (field: EntityField): field is PrimitiveField =>
72
70
  field.kind === undefined || field.kind === 'primitive';
73
71
 
74
- export const isEnum = (field: ModelField): field is EnumField => field.kind === 'enum';
72
+ export const isEnum = (field: EntityField): field is EnumField => field.kind === 'enum';
73
+
74
+ export const isRelation = (field: EntityField): field is RelationField => field.kind === 'relation';
75
75
 
76
- export const isRelation = (field: ModelField): field is RelationField => field.kind === 'relation';
76
+ export const isInherited = (field: EntityField) => field.inherited;
77
77
 
78
- export const isToOneRelation = (field: ModelField): field is RelationField => isRelation(field) && !!field.toOne;
78
+ export const isInTable = (field: EntityField) => field.name === 'id' || !field.inherited;
79
79
 
80
- export const isQueriableField = ({ queriable }: ModelField) => queriable !== false;
80
+ export const isToOneRelation = (field: EntityField): field is RelationField => isRelation(field) && !!field.toOne;
81
81
 
82
- export const isCustomField = (field: ModelField): field is CustomField => field.kind === 'custom';
82
+ export const isQueriableField = ({ queriable }: EntityField) => queriable !== false;
83
83
 
84
- export const isVisible = ({ hidden }: ModelField) => hidden !== true;
84
+ export const isCustomField = (field: EntityField): field is CustomField => field.kind === 'custom';
85
+
86
+ export const isVisible = ({ hidden }: EntityField) => hidden !== true;
85
87
 
86
88
  export const isSimpleField = and(not(isRelation), not(isCustomField));
87
89
 
88
- export const isUpdatable = ({ updatable }: ModelField) => !!updatable;
90
+ export const isUpdatable = ({ updatable }: EntityField) => !!updatable;
89
91
 
90
- export const isCreatable = ({ creatable }: ModelField) => !!creatable;
92
+ export const isCreatable = ({ creatable }: EntityField) => !!creatable;
91
93
 
92
- export const isQueriableBy = (role: string) => (field: ModelField) =>
94
+ export const isQueriableBy = (role: string) => (field: EntityField) =>
93
95
  field.queriable !== false &&
94
96
  (field.queriable === undefined ||
95
97
  field.queriable === true ||
96
98
  !field.queriable.roles ||
97
99
  field.queriable.roles.includes(role));
98
100
 
99
- export const isUpdatableBy = (role: string) => (field: ModelField) =>
101
+ export const isUpdatableBy = (role: string) => (field: EntityField) =>
100
102
  field.updatable && (field.updatable === true || !field.updatable.roles || field.updatable.roles.includes(role));
101
103
 
102
- export const isCreatableBy = (role: string) => (field: ModelField) =>
104
+ export const isCreatableBy = (role: string) => (field: EntityField) =>
103
105
  field.creatable && (field.creatable === true || !field.creatable.roles || field.creatable.roles.includes(role));
104
106
 
105
- export const actionableRelations = (model: Model, action: 'create' | 'update' | 'filter') =>
106
- model.fields
107
- .filter(isRelation)
108
- .filter(
109
- (field) =>
110
- field[`${action === 'filter' ? action : action.slice(0, -1)}able` as 'filterable' | 'creatable' | 'updatable']
111
- );
112
-
113
- export const getModels = (rawModels: RawModels): Models => {
114
- const models: Models = rawModels.filter(isEntityModel).map((model) => {
115
- const objectModel: Model = {
116
- ...model,
117
- fieldsByName: {},
118
- relations: [],
119
- relationsByName: {},
120
- reverseRelations: [],
121
- reverseRelationsByName: {},
122
- fields: (
123
- [
124
- { name: 'id', type: 'ID', nonNull: true, unique: true, primary: true, generated: true },
125
- ...model.fields,
126
- ...(model.creatable
127
- ? [
128
- {
129
- name: 'createdAt',
130
- type: 'DateTime',
131
- nonNull: true,
132
- orderable: true,
133
- generated: true,
134
- ...(typeof model.creatable === 'object' && model.creatable.createdAt),
135
- } satisfies DateTimeField,
136
- {
137
- name: 'createdBy',
138
- kind: 'relation',
139
- type: 'User',
140
- nonNull: true,
141
- reverse: `created${getModelPlural(model)}`,
142
- generated: true,
143
- ...(typeof model.creatable === 'object' && model.creatable.createdBy),
144
- } satisfies RelationField,
145
- ]
146
- : []),
147
- ...(model.updatable
148
- ? [
149
- {
150
- name: 'updatedAt',
151
- type: 'DateTime',
152
- nonNull: true,
153
- orderable: true,
154
- generated: true,
155
- ...(typeof model.updatable === 'object' && model.updatable.updatedAt),
156
- } satisfies DateTimeField,
157
- {
158
- name: 'updatedBy',
159
- kind: 'relation',
160
- type: 'User',
161
- nonNull: true,
162
- reverse: `updated${getModelPlural(model)}`,
163
- generated: true,
164
- ...(typeof model.updatable === 'object' && model.updatable.updatedBy),
165
- } satisfies RelationField,
166
- ]
167
- : []),
168
- ...(model.deletable
169
- ? [
170
- {
171
- name: 'deleted',
172
- type: 'Boolean',
173
- nonNull: true,
174
- defaultValue: false,
175
- filterable: { default: false },
176
- generated: true,
177
- ...(typeof model.deletable === 'object' && model.deletable.deleted),
178
- } satisfies BooleanField,
179
- {
180
- name: 'deletedAt',
181
- type: 'DateTime',
182
- orderable: true,
183
- generated: true,
184
- ...(typeof model.deletable === 'object' && model.deletable.deletedAt),
185
- } satisfies DateTimeField,
186
- {
187
- name: 'deletedBy',
188
- kind: 'relation',
189
- type: 'User',
190
- reverse: `deleted${getModelPlural(model)}`,
191
- generated: true,
192
- ...(typeof model.deletable === 'object' && model.deletable.deletedBy),
193
- } satisfies RelationField,
194
- ]
195
- : []),
196
- ] satisfies ModelField[]
197
- ).map((field: ModelField) => ({
198
- ...field,
199
- ...(field.kind === 'relation' && {
200
- foreignKey: field.foreignKey || `${field.name}Id`,
201
- }),
202
- })),
203
- };
204
-
205
- for (const field of objectModel.fields) {
206
- objectModel.fieldsByName[field.name] = field;
207
- }
208
-
209
- return objectModel;
210
- });
211
-
212
- for (const model of models) {
213
- for (const field of model.fields) {
214
- if (field.kind !== 'relation') {
215
- continue;
216
- }
217
-
218
- const fieldModel = summonByName(models, field.type);
219
-
220
- const reverseRelation: ReverseRelation = {
221
- kind: 'relation',
222
- name: field.reverse || (field.toOne ? typeToField(model.name) : getModelPluralField(model)),
223
- foreignKey: get(field, 'foreignKey'),
224
- type: model.name,
225
- toOne: !!field.toOne,
226
- fieldModel,
227
- field,
228
- model,
229
- };
230
-
231
- const relation: Relation = {
232
- field,
233
- model: fieldModel,
234
- reverseRelation,
235
- };
236
- model.relations.push(relation);
237
- model.relationsByName[relation.field.name] = relation;
238
-
239
- fieldModel.reverseRelations.push(reverseRelation);
240
-
241
- fieldModel.reverseRelationsByName[reverseRelation.name] = reverseRelation;
242
- }
243
- }
244
-
245
- return models;
246
- };
107
+ export const actionableRelations = (model: EntityModel, action: 'create' | 'update' | 'filter') =>
108
+ model.relations.filter(
109
+ (relation) =>
110
+ relation.field[`${action === 'filter' ? action : action.slice(0, -1)}able` as 'filterable' | 'creatable' | 'updatable']
111
+ );
247
112
 
248
113
  export const summonByName = <T extends { name: string }>(array: T[], value: string) => summonByKey(array, 'name', value);
249
114
 
@@ -277,8 +142,9 @@ export const it = <T>(object: T | null | undefined): ForSure<T> => {
277
142
  export const get = <T, U extends keyof ForSure<T>>(object: T | null | undefined, key: U): ForSure<ForSure<T>[U]> => {
278
143
  const value = it(object)[key];
279
144
  if (value === undefined || value === null) {
280
- console.trace();
281
- throw new Error(`Object doesn't have ${String(key)}`);
145
+ const error = new Error(`Object doesn't have ${String(key)}`);
146
+ console.warn(error);
147
+ throw error;
282
148
  }
283
149
  return value as ForSure<ForSure<T>[U]>;
284
150
  };
@@ -1,8 +1,8 @@
1
1
  import { Knex } from 'knex';
2
2
  import { FullContext } from '../context';
3
3
  import { NotFoundError, PermissionError } from '../errors';
4
- import { Model } from '../models/models';
5
- import { get, getModelPlural, isRelation, summonByName } from '../models/utils';
4
+ import { EntityModel } from '../models/models';
5
+ import { get, isRelation } from '../models/utils';
6
6
  import { AliasGenerator, hash, ors } from '../resolvers/utils';
7
7
  import { BasicValue } from '../values';
8
8
  import { PermissionAction, PermissionLink, PermissionStack } from './generate';
@@ -86,11 +86,14 @@ export const applyPermissions = (
86
86
  */
87
87
  export const getEntityToMutate = async (
88
88
  ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
89
- model: Model,
89
+ model: EntityModel,
90
90
  where: Record<string, BasicValue>,
91
91
  action: 'UPDATE' | 'DELETE' | 'RESTORE'
92
92
  ) => {
93
- const query = ctx.knex(model.name).where(where).first();
93
+ const query = ctx
94
+ .knex(model.parent || model.name)
95
+ .where(where)
96
+ .first();
94
97
  let entity = await query.clone();
95
98
 
96
99
  if (!entity) {
@@ -113,6 +116,11 @@ export const getEntityToMutate = async (
113
116
  throw new PermissionError(action, `this ${model.name}`, 'no available permissions applied');
114
117
  }
115
118
 
119
+ if (model.parent) {
120
+ const subEntity = await ctx.knex(model.name).where({ id: entity.id }).first();
121
+ Object.assign(entity, subEntity);
122
+ }
123
+
116
124
  return entity;
117
125
  };
118
126
 
@@ -121,7 +129,7 @@ export const getEntityToMutate = async (
121
129
  */
122
130
  export const checkCanWrite = async (
123
131
  ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
124
- model: Model,
132
+ model: EntityModel,
125
133
  data: Record<string, BasicValue>,
126
134
  action: 'CREATE' | 'UPDATE'
127
135
  ) => {
@@ -131,7 +139,7 @@ export const checkCanWrite = async (
131
139
  return;
132
140
  }
133
141
  if (permissionStack === false) {
134
- throw new PermissionError(action, getModelPlural(model), 'no applicable permissions');
142
+ throw new PermissionError(action, model.plural, 'no applicable permissions');
135
143
  }
136
144
 
137
145
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- using `select(1 as any)` to instantiate an "empty" query builder
@@ -202,11 +210,11 @@ const permissionLinkQuery = (
202
210
  subQuery.where({ [`${alias}.id`]: ctx.user.id });
203
211
  }
204
212
  if (where) {
205
- applyWhere(summonByName(ctx.models, type), subQuery, alias, where, aliases);
213
+ applyWhere(ctx.models.getModel(type, 'entity'), subQuery, alias, where, aliases);
206
214
  }
207
215
 
208
216
  for (const { type, foreignKey, reverse, where } of links) {
209
- const model = summonByName(ctx.models, type);
217
+ const model = ctx.models.getModel(type, 'entity');
210
218
  const subAlias = aliases.getShort();
211
219
  if (reverse) {
212
220
  // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
@@ -226,7 +234,7 @@ const permissionLinkQuery = (
226
234
  subQuery.whereRaw(`"${alias}".id = ?`, id);
227
235
  };
228
236
 
229
- const applyWhere = (model: Model, query: Knex.QueryBuilder, alias: string, where: any, aliases: AliasGenerator) => {
237
+ const applyWhere = (model: EntityModel, query: Knex.QueryBuilder, alias: string, where: any, aliases: AliasGenerator) => {
230
238
  for (const [key, value] of Object.entries(where)) {
231
239
  const relation = model.relationsByName[key];
232
240
 
@@ -234,11 +242,11 @@ const applyWhere = (model: Model, query: Knex.QueryBuilder, alias: string, where
234
242
  const subAlias = aliases.getShort();
235
243
  // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
236
244
  query.leftJoin(
237
- `${relation.model.name} as ${subAlias}`,
245
+ `${relation.targetModel.name} as ${subAlias}`,
238
246
  `${alias}.${relation.field.foreignKey || `${relation.field.name}Id`}`,
239
247
  `${subAlias}.id`
240
248
  );
241
- applyWhere(relation.model, query, subAlias, value, aliases);
249
+ applyWhere(relation.targetModel, query, subAlias, value, aliases);
242
250
  } else if (Array.isArray(value)) {
243
251
  // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
244
252
  query.whereIn(`${alias}.${key}`, value);
@@ -1,5 +1,5 @@
1
1
  import { Models } from '../models/models';
2
- import { isRelation, summonByName } from '../models/utils';
2
+ import { isRelation } from '../models/utils';
3
3
 
4
4
  export type PermissionAction = 'READ' | 'CREATE' | 'UPDATE' | 'DELETE' | 'RESTORE' | 'LINK';
5
5
 
@@ -96,7 +96,7 @@ export const generatePermissions = (models: Models, config: PermissionsConfig) =
96
96
 
97
97
  const addPermissions = (models: Models, permissions: RolePermissions, links: PermissionLink[], block: PermissionsBlock) => {
98
98
  const { type } = links[links.length - 1];
99
- const model = summonByName(models, type);
99
+ const model = models.getModel(type, 'entity');
100
100
 
101
101
  for (const action of ACTIONS) {
102
102
  if (action === 'READ' || action in block) {
@@ -123,15 +123,15 @@ const addPermissions = (models: Models, permissions: RolePermissions, links: Per
123
123
  reverse: true,
124
124
  };
125
125
  } else {
126
- const field = model.reverseRelationsByName[relation];
126
+ const reverseRelation = model.reverseRelationsByName[relation];
127
127
 
128
- if (!field) {
128
+ if (!reverseRelation) {
129
129
  throw new Error(`Relation ${relation} in model ${model.name} does not exist.`);
130
130
  }
131
131
 
132
132
  link = {
133
- type: field.model.name,
134
- foreignKey: field.foreignKey,
133
+ type: reverseRelation.targetModel.name,
134
+ foreignKey: reverseRelation.field.foreignKey,
135
135
  };
136
136
  }
137
137
  if (subBlock.WHERE) {
@@ -1,9 +1,9 @@
1
1
  import { Knex } from 'knex';
2
+ import { EntityModel, FullContext } from '..';
2
3
  import { ForbiddenError, UserInputError } from '../errors';
3
- import { get, summonByName } from '../models/utils';
4
4
  import { OrderBy, Where, normalizeArguments } from './arguments';
5
- import { FieldResolverNode, WhereNode } from './node';
6
- import { Joins, Ops, addJoin, apply, ors } from './utils';
5
+ import { FieldResolverNode } from './node';
6
+ import { Joins, Ops, addJoin, apply, getColumn, ors } from './utils';
7
7
 
8
8
  export const SPECIAL_FILTERS: Record<string, string> = {
9
9
  GT: '?? > ?',
@@ -12,6 +12,16 @@ export const SPECIAL_FILTERS: Record<string, string> = {
12
12
  LTE: '?? <= ?',
13
13
  };
14
14
 
15
+ export type WhereNode = {
16
+ ctx: FullContext;
17
+
18
+ rootModel: EntityModel;
19
+ rootTableAlias: string;
20
+
21
+ model: EntityModel;
22
+ tableAlias: string;
23
+ };
24
+
15
25
  export const applyFilters = (node: FieldResolverNode, query: Knex.QueryBuilder, joins: Joins) => {
16
26
  const normalizedArguments = normalizeArguments(node);
17
27
  if (!normalizedArguments.orderBy) {
@@ -52,16 +62,20 @@ export const applyFilters = (node: FieldResolverNode, query: Knex.QueryBuilder,
52
62
  applyOrderBy(node, orderBy, query);
53
63
  }
54
64
 
65
+ if (node.model.parent) {
66
+ void query.where({
67
+ [getColumn(node, 'type')]: node.model.name,
68
+ });
69
+ }
70
+
55
71
  if (where) {
56
72
  const ops: Ops<Knex.QueryBuilder> = [];
57
73
  applyWhere(node, where, ops, joins);
58
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
59
- apply(query, ops);
74
+ void apply(query, ops);
60
75
  }
61
76
 
62
77
  if (search) {
63
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
64
- applySearch(node, search, query);
78
+ void applySearch(node, search, query);
65
79
  }
66
80
  };
67
81
 
@@ -76,37 +90,40 @@ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>,
76
90
  // Should not happen
77
91
  throw new Error(`Invalid filter ${key}.`);
78
92
  }
79
- ops.push((query) =>
80
- query.whereRaw(SPECIAL_FILTERS[filter], [`${node.shortTableAlias}.${actualKey}`, value as string])
81
- );
93
+ ops.push((query) => query.whereRaw(SPECIAL_FILTERS[filter], [getColumn(node, actualKey), value as string]));
82
94
  continue;
83
95
  }
84
96
 
85
- const field = summonByName(node.model.fields, key);
86
- const fullKey = `${node.shortTableAlias}.${key}`;
97
+ const field = node.model.getField(key);
87
98
 
88
99
  if (field.kind === 'relation') {
89
- const relation = get(node.model.relationsByName, field.name);
90
- const tableAlias = `${node.model.name}__W__${key}`;
100
+ const relation = node.model.getRelation(field.name);
101
+ const targetModel = relation.targetModel;
102
+ const rootModel = targetModel.parentModel || targetModel;
103
+ const rootTableAlias = `${node.model.name}__W__${key}`;
104
+ const tableAlias = targetModel === rootModel ? rootTableAlias : `${node.model.name}__WS__${key}`;
91
105
  const subNode: WhereNode = {
92
106
  ctx: node.ctx,
93
- model: relation.model,
94
- tableName: relation.model.name,
107
+
108
+ rootModel,
109
+ rootTableAlias,
110
+
111
+ model: targetModel,
95
112
  tableAlias,
96
- shortTableAlias: node.ctx.aliases.getShort(tableAlias),
97
- foreignKey: relation.field.foreignKey,
98
113
  };
99
- addJoin(joins, node.tableAlias, subNode.tableName, subNode.tableAlias, get(subNode, 'foreignKey'), 'id');
114
+ addJoin(joins, node.tableAlias, subNode.model.name, subNode.tableAlias, relation.field.foreignKey, 'id');
100
115
  applyWhere(subNode, value as Where, ops, joins);
101
116
  continue;
102
117
  }
103
118
 
119
+ const column = getColumn(node, key);
120
+
104
121
  if (Array.isArray(value)) {
105
122
  if (field && field.list) {
106
123
  ops.push((query) =>
107
124
  ors(
108
125
  query,
109
- value.map((v) => (subQuery) => subQuery.whereRaw('? = ANY(??)', [v, fullKey] as string[]))
126
+ value.map((v) => (subQuery) => subQuery.whereRaw('? = ANY(??)', [v, column] as string[]))
110
127
  )
111
128
  );
112
129
  continue;
@@ -116,22 +133,22 @@ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>,
116
133
  if (value.some((v) => v !== null)) {
117
134
  ops.push((query) =>
118
135
  ors(query, [
119
- (subQuery) => subQuery.whereIn(fullKey, value.filter((v) => v !== null) as string[]),
120
- (subQuery) => subQuery.whereNull(fullKey),
136
+ (subQuery) => subQuery.whereIn(column, value.filter((v) => v !== null) as string[]),
137
+ (subQuery) => subQuery.whereNull(column),
121
138
  ])
122
139
  );
123
140
  continue;
124
141
  }
125
142
 
126
- ops.push((query) => query.whereNull(fullKey));
143
+ ops.push((query) => query.whereNull(column));
127
144
  continue;
128
145
  }
129
146
 
130
- ops.push((query) => query.whereIn(fullKey, value as string[]));
147
+ ops.push((query) => query.whereIn(column, value as string[]));
131
148
  continue;
132
149
  }
133
150
 
134
- ops.push((query) => query.where({ [fullKey]: value }));
151
+ ops.push((query) => query.where({ [column]: value }));
135
152
  }
136
153
  };
137
154
 
@@ -143,7 +160,7 @@ const applySearch = (node: FieldResolverNode, search: string, query: Knex.QueryB
143
160
  .map(
144
161
  ({ name }) =>
145
162
  (query) =>
146
- query.whereILike(`${node.shortTableAlias}.${name}`, `%${search}%`)
163
+ query.whereILike(getColumn(node, name), `%${search}%`)
147
164
  )
148
165
  );
149
166
 
@@ -157,7 +174,6 @@ const applyOrderBy = (node: FieldResolverNode, orderBy: OrderBy, query: Knex.Que
157
174
  const value = vals[key];
158
175
 
159
176
  // Simple field
160
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
161
- query.orderBy(`${node.shortTableAlias}.${key}`, value);
177
+ void query.orderBy(getColumn(node, key), value);
162
178
  }
163
179
  };
@@ -6,4 +6,5 @@ export * from './mutations';
6
6
  export * from './node';
7
7
  export * from './resolver';
8
8
  export * from './resolvers';
9
+ export * from './selects';
9
10
  export * from './utils';