@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
|
@@ -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:
|
|
12
|
+
model: EntityModel,
|
|
13
13
|
action: Action,
|
|
14
14
|
when: 'before' | 'after',
|
|
15
15
|
data: { prev: Entity; input: Entity; normalizedInput: Entity; next: FullEntity },
|
package/src/models/utils.ts
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
import assert from 'assert';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
28
|
+
export const getLabel = (s: string) => startCase(camelCase(s));
|
|
38
29
|
|
|
39
|
-
export const
|
|
30
|
+
export const or =
|
|
31
|
+
<T>(...predicates: ((field: T) => boolean)[]) =>
|
|
32
|
+
(field: T) =>
|
|
33
|
+
predicates.some((predicate) => predicate(field));
|
|
40
34
|
|
|
41
|
-
export const
|
|
35
|
+
export const and =
|
|
36
|
+
<T>(...predicates: ((field: T) => boolean)[]) =>
|
|
37
|
+
(field: T) =>
|
|
38
|
+
predicates.every((predicate) => predicate(field));
|
|
42
39
|
|
|
43
|
-
export const
|
|
40
|
+
export const not =
|
|
41
|
+
<T>(predicate: (field: T) => boolean) =>
|
|
42
|
+
(field: T) =>
|
|
43
|
+
!predicate(field);
|
|
44
44
|
|
|
45
|
-
export const
|
|
45
|
+
export const isRootModel = (model: EntityModel) => model.root;
|
|
46
46
|
|
|
47
|
-
export const
|
|
47
|
+
export const isEntityModel = (model: Model): model is EntityModel => model instanceof EntityModel;
|
|
48
48
|
|
|
49
|
-
export const
|
|
49
|
+
export const isEnumModel = (model: Model): model is EnumModel => model instanceof EnumModel;
|
|
50
50
|
|
|
51
|
-
export const
|
|
51
|
+
export const isRawEnumModel = (model: Model): model is RawEnumModel => model instanceof RawEnumModel;
|
|
52
52
|
|
|
53
|
-
export const
|
|
53
|
+
export const isScalarModel = (model: Model): model is ScalarModel => model instanceof ScalarModel;
|
|
54
54
|
|
|
55
|
-
export const
|
|
55
|
+
export const isObjectModel = (model: Model): model is ObjectModel => model instanceof ObjectModel;
|
|
56
56
|
|
|
57
|
-
export const
|
|
57
|
+
export const isInputModel = (model: Model): model is InputModel => model instanceof InputModel;
|
|
58
58
|
|
|
59
|
-
export const
|
|
59
|
+
export const isInterfaceModel = (model: Model): model is InterfaceModel => model instanceof InterfaceModel;
|
|
60
60
|
|
|
61
|
-
export const
|
|
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
|
|
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
|
|
65
|
+
export const modelNeedsTable = (model: EntityModel) => model.fields.some((field) => !field.inherited);
|
|
70
66
|
|
|
71
|
-
export const
|
|
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:
|
|
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
|
|
76
|
+
export const isInherited = (field: EntityField) => field.inherited;
|
|
77
77
|
|
|
78
|
-
export const
|
|
78
|
+
export const isInTable = (field: EntityField) => field.name === 'id' || !field.inherited;
|
|
79
79
|
|
|
80
|
-
export const
|
|
80
|
+
export const isToOneRelation = (field: EntityField): field is RelationField => isRelation(field) && !!field.toOne;
|
|
81
81
|
|
|
82
|
-
export const
|
|
82
|
+
export const isQueriableField = ({ queriable }: EntityField) => queriable !== false;
|
|
83
83
|
|
|
84
|
-
export const
|
|
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 }:
|
|
90
|
+
export const isUpdatable = ({ updatable }: EntityField) => !!updatable;
|
|
89
91
|
|
|
90
|
-
export const isCreatable = ({ creatable }:
|
|
92
|
+
export const isCreatable = ({ creatable }: EntityField) => !!creatable;
|
|
91
93
|
|
|
92
|
-
export const isQueriableBy = (role: string) => (field:
|
|
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:
|
|
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:
|
|
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:
|
|
106
|
-
model.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
};
|
package/src/permissions/check.ts
CHANGED
|
@@ -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 {
|
|
5
|
-
import { get,
|
|
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:
|
|
89
|
+
model: EntityModel,
|
|
90
90
|
where: Record<string, BasicValue>,
|
|
91
91
|
action: 'UPDATE' | 'DELETE' | 'RESTORE'
|
|
92
92
|
) => {
|
|
93
|
-
const query = ctx
|
|
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:
|
|
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,
|
|
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(
|
|
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 =
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
126
|
+
const reverseRelation = model.reverseRelationsByName[relation];
|
|
127
127
|
|
|
128
|
-
if (!
|
|
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:
|
|
134
|
-
foreignKey: field.foreignKey,
|
|
133
|
+
type: reverseRelation.targetModel.name,
|
|
134
|
+
foreignKey: reverseRelation.field.foreignKey,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
if (subBlock.WHERE) {
|
package/src/resolvers/filters.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
59
|
-
apply(query, ops);
|
|
74
|
+
void apply(query, ops);
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
if (search) {
|
|
63
|
-
|
|
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 =
|
|
86
|
-
const fullKey = `${node.shortTableAlias}.${key}`;
|
|
97
|
+
const field = node.model.getField(key);
|
|
87
98
|
|
|
88
99
|
if (field.kind === 'relation') {
|
|
89
|
-
const relation =
|
|
90
|
-
const
|
|
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
|
-
|
|
94
|
-
|
|
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.
|
|
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,
|
|
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(
|
|
120
|
-
(subQuery) => subQuery.whereNull(
|
|
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(
|
|
143
|
+
ops.push((query) => query.whereNull(column));
|
|
127
144
|
continue;
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
ops.push((query) => query.whereIn(
|
|
147
|
+
ops.push((query) => query.whereIn(column, value as string[]));
|
|
131
148
|
continue;
|
|
132
149
|
}
|
|
133
150
|
|
|
134
|
-
ops.push((query) => query.where({ [
|
|
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(
|
|
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
|
-
|
|
161
|
-
query.orderBy(`${node.shortTableAlias}.${key}`, value);
|
|
177
|
+
void query.orderBy(getColumn(node, key), value);
|
|
162
178
|
}
|
|
163
179
|
};
|