@smartive/graphql-magic 3.1.0 → 5.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/CHANGELOG.md +1 -6
- package/README.md +46 -0
- package/dist/cjs/index.cjs +260 -196
- package/dist/esm/api/execute.d.ts +10 -0
- package/dist/esm/api/execute.js +32 -0
- package/dist/esm/api/execute.js.map +1 -0
- package/dist/esm/api/index.d.ts +1 -0
- package/dist/esm/api/index.js +3 -0
- package/dist/esm/api/index.js.map +1 -0
- package/dist/esm/client/queries.d.ts +4 -1
- package/dist/esm/client/queries.js +7 -4
- package/dist/esm/client/queries.js.map +1 -1
- package/dist/esm/context.d.ts +1 -4
- package/dist/esm/db/generate.js +21 -13
- package/dist/esm/db/generate.js.map +1 -1
- package/dist/esm/generate/generate.d.ts +1 -1
- package/dist/esm/generate/generate.js +17 -22
- package/dist/esm/generate/generate.js.map +1 -1
- package/dist/esm/generate/mutations.d.ts +1 -1
- package/dist/esm/generate/utils.d.ts +10 -1
- package/dist/esm/generate/utils.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +1 -1
- package/dist/esm/migrations/generate.js +82 -89
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/index.d.ts +2 -0
- package/dist/esm/models/index.js +4 -0
- package/dist/esm/models/index.js.map +1 -0
- package/dist/esm/models/models.d.ts +192 -0
- package/dist/esm/models/models.js +2 -0
- package/dist/esm/models/models.js.map +1 -0
- package/dist/esm/models/utils.d.ts +205 -0
- package/dist/esm/{utils.js → models/utils.js} +69 -21
- package/dist/esm/models/utils.js.map +1 -0
- package/dist/esm/permissions/check.d.ts +1 -1
- package/dist/esm/permissions/check.js +2 -2
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/permissions/generate.d.ts +1 -1
- package/dist/esm/permissions/generate.js +2 -2
- package/dist/esm/permissions/generate.js.map +1 -1
- package/dist/esm/resolvers/arguments.js +1 -1
- package/dist/esm/resolvers/arguments.js.map +1 -1
- package/dist/esm/resolvers/filters.js +2 -2
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/mutations.js +5 -6
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/dist/esm/resolvers/node.d.ts +1 -1
- package/dist/esm/resolvers/node.js +4 -5
- package/dist/esm/resolvers/node.js.map +1 -1
- package/dist/esm/resolvers/resolver.js +3 -3
- package/dist/esm/resolvers/resolver.js.map +1 -1
- package/dist/esm/resolvers/resolvers.d.ts +1 -1
- package/dist/esm/resolvers/resolvers.js +1 -1
- package/dist/esm/resolvers/resolvers.js.map +1 -1
- package/dist/esm/resolvers/utils.js +1 -1
- package/dist/esm/resolvers/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/api/execute.ts +45 -0
- package/src/api/index.ts +3 -0
- package/src/client/queries.ts +16 -10
- package/src/context.ts +1 -3
- package/src/db/generate.ts +22 -15
- package/src/generate/generate.ts +26 -34
- package/src/generate/mutations.ts +1 -1
- package/src/generate/utils.ts +11 -1
- package/src/index.ts +2 -2
- package/src/migrations/generate.ts +84 -82
- package/src/models/index.ts +4 -0
- package/src/models/models.ts +184 -0
- package/src/models/utils.ts +288 -0
- package/src/permissions/check.ts +3 -3
- package/src/permissions/generate.ts +3 -3
- package/src/resolvers/arguments.ts +1 -1
- package/src/resolvers/filters.ts +2 -2
- package/src/resolvers/mutations.ts +10 -9
- package/src/resolvers/node.ts +6 -6
- package/src/resolvers/resolver.ts +3 -3
- package/src/resolvers/resolvers.ts +2 -2
- package/src/resolvers/utils.ts +1 -1
- package/tests/unit/resolve.spec.ts +4 -19
- package/tests/utils/models.ts +8 -7
- package/tests/utils/server.ts +13 -33
- package/dist/esm/models.d.ts +0 -170
- package/dist/esm/models.js +0 -27
- package/dist/esm/models.js.map +0 -1
- package/dist/esm/utils.d.ts +0 -25
- package/dist/esm/utils.js.map +0 -1
- package/src/models.ts +0 -228
- package/src/utils.ts +0 -187
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { pluralize } from 'inflection';
|
|
3
|
+
import camelCase from 'lodash/camelCase';
|
|
4
|
+
import lodashGet from 'lodash/get';
|
|
5
|
+
import kebabCase from 'lodash/kebabCase';
|
|
6
|
+
import startCase from 'lodash/startCase';
|
|
7
|
+
import {
|
|
8
|
+
BooleanField,
|
|
9
|
+
DateTimeField,
|
|
10
|
+
EnumModel,
|
|
11
|
+
Model,
|
|
12
|
+
ModelField,
|
|
13
|
+
Models,
|
|
14
|
+
ObjectModel,
|
|
15
|
+
RawEnumModel,
|
|
16
|
+
RawField,
|
|
17
|
+
RawModel,
|
|
18
|
+
RawModels,
|
|
19
|
+
RawObjectModel,
|
|
20
|
+
Relation,
|
|
21
|
+
RelationField,
|
|
22
|
+
ReverseRelation,
|
|
23
|
+
ScalarModel,
|
|
24
|
+
} from './models';
|
|
25
|
+
|
|
26
|
+
const isNotFalsy = <T>(v: T | null | undefined | false): v is T => typeof v !== 'undefined' && v !== null && v !== false;
|
|
27
|
+
|
|
28
|
+
export const merge = <T>(objects: ({ [name: string]: T } | undefined | false)[] | undefined): { [name: string]: T } =>
|
|
29
|
+
(objects || []).filter(isNotFalsy).reduce((i, acc) => ({ ...acc, ...i }), {});
|
|
30
|
+
|
|
31
|
+
// Target -> target
|
|
32
|
+
export const typeToField = (type: string) => type.substr(0, 1).toLowerCase() + type.substr(1);
|
|
33
|
+
|
|
34
|
+
export const getModelPlural = (model: ObjectModel | Model) => model.plural || pluralize(model.name);
|
|
35
|
+
|
|
36
|
+
export const getModelPluralField = (model: Model) => typeToField(getModelPlural(model));
|
|
37
|
+
|
|
38
|
+
export const getModelSlug = (model: Model) => kebabCase(getModelPlural(model));
|
|
39
|
+
|
|
40
|
+
export const getModelLabelPlural = (model: Model) => getLabel(getModelPlural(model));
|
|
41
|
+
|
|
42
|
+
export const getModelLabel = (model: Model) => getLabel(model.name);
|
|
43
|
+
|
|
44
|
+
export const getLabel = (s: string) => startCase(camelCase(s));
|
|
45
|
+
|
|
46
|
+
export const isObjectModel = (model: RawModel): model is ObjectModel => model.type === 'object';
|
|
47
|
+
|
|
48
|
+
export const isEnumModel = (model: RawModel): model is EnumModel => model.type === 'enum';
|
|
49
|
+
|
|
50
|
+
export const isRawEnumModel = (model: RawModel): model is RawEnumModel => model.type === 'raw-enum';
|
|
51
|
+
|
|
52
|
+
export const isScalarModel = (model: RawModel): model is ScalarModel => model.type === 'scalar';
|
|
53
|
+
|
|
54
|
+
export const isRawObjectModel = (model: RawModel): model is RawObjectModel => model.type === 'raw';
|
|
55
|
+
|
|
56
|
+
export const isEnumList = (models: RawModels, field: ModelField) =>
|
|
57
|
+
field?.list === true && models.find(({ name }) => name === field.type)?.type === 'enum';
|
|
58
|
+
|
|
59
|
+
export const and =
|
|
60
|
+
(...predicates: ((field: ModelField) => boolean)[]) =>
|
|
61
|
+
(field: ModelField) =>
|
|
62
|
+
predicates.every((predicate) => predicate(field));
|
|
63
|
+
|
|
64
|
+
export const not = (predicate: (field: ModelField) => boolean) => (field: ModelField) => !predicate(field);
|
|
65
|
+
|
|
66
|
+
export const isRelation = (field: ModelField): field is RelationField => field.type === 'relation';
|
|
67
|
+
|
|
68
|
+
export const isToOneRelation = (field: ModelField): field is RelationField => isRelation(field) && !!field.toOne;
|
|
69
|
+
|
|
70
|
+
export const isQueriableField = ({ queriable }: ModelField) => queriable !== false;
|
|
71
|
+
|
|
72
|
+
export const isRaw = (field: ModelField): field is RawField => field.type === 'raw';
|
|
73
|
+
|
|
74
|
+
export const isVisible = ({ hidden }: ModelField) => hidden !== true;
|
|
75
|
+
|
|
76
|
+
export const isSimpleField = and(not(isRelation), not(isRaw));
|
|
77
|
+
|
|
78
|
+
export const isUpdatable = ({ updatable }: ModelField) => !!updatable;
|
|
79
|
+
|
|
80
|
+
export const isCreatable = ({ creatable }: ModelField) => !!creatable;
|
|
81
|
+
|
|
82
|
+
export const isQueriableBy = (role: string) => (field: ModelField) =>
|
|
83
|
+
field.queriable !== false && (field.queriable == true || !field.queriable.roles || field.queriable.roles.includes(role));
|
|
84
|
+
|
|
85
|
+
export const isUpdatableBy = (role: string) => (field: ModelField) =>
|
|
86
|
+
field.updatable && (field.updatable === true || !field.updatable.roles || field.updatable.roles.includes(role));
|
|
87
|
+
|
|
88
|
+
export const isCreatableBy = (role: string) => (field: ModelField) =>
|
|
89
|
+
field.creatable && (field.creatable === true || !field.creatable.roles || field.creatable.roles.includes(role));
|
|
90
|
+
|
|
91
|
+
export const actionableRelations = (model: Model, action: 'create' | 'update' | 'filter') =>
|
|
92
|
+
model.fields
|
|
93
|
+
.filter(isRelation)
|
|
94
|
+
.filter(
|
|
95
|
+
(field) =>
|
|
96
|
+
field[`${action === 'filter' ? action : action.slice(0, -1)}able` as 'filterable' | 'creatable' | 'updatable']
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const getModels = (rawModels: RawModels): Models => {
|
|
100
|
+
const models: Models = rawModels.filter(isObjectModel).map((model) => {
|
|
101
|
+
const objectModel: Model = {
|
|
102
|
+
...model,
|
|
103
|
+
fieldsByName: {},
|
|
104
|
+
relations: [],
|
|
105
|
+
relationsByName: {},
|
|
106
|
+
reverseRelations: [],
|
|
107
|
+
reverseRelationsByName: {},
|
|
108
|
+
fields: (
|
|
109
|
+
[
|
|
110
|
+
{ name: 'id', type: 'ID', nonNull: true, unique: true, primary: true, generated: true },
|
|
111
|
+
...model.fields,
|
|
112
|
+
...(model.creatable
|
|
113
|
+
? [
|
|
114
|
+
{
|
|
115
|
+
name: 'createdAt',
|
|
116
|
+
type: 'DateTime',
|
|
117
|
+
|
|
118
|
+
nonNull: true,
|
|
119
|
+
orderable: true,
|
|
120
|
+
generated: true,
|
|
121
|
+
...(typeof model.creatable === 'object' && model.creatable.createdAt),
|
|
122
|
+
} satisfies DateTimeField,
|
|
123
|
+
{
|
|
124
|
+
name: 'createdBy',
|
|
125
|
+
type: 'relation',
|
|
126
|
+
typeName: 'User',
|
|
127
|
+
nonNull: true,
|
|
128
|
+
reverse: `created${getModelPlural(model)}`,
|
|
129
|
+
generated: true,
|
|
130
|
+
...(typeof model.creatable === 'object' && model.creatable.createdBy),
|
|
131
|
+
} satisfies RelationField,
|
|
132
|
+
]
|
|
133
|
+
: []),
|
|
134
|
+
...(model.updatable
|
|
135
|
+
? [
|
|
136
|
+
{
|
|
137
|
+
name: 'updatedAt',
|
|
138
|
+
type: 'DateTime',
|
|
139
|
+
nonNull: true,
|
|
140
|
+
orderable: true,
|
|
141
|
+
generated: true,
|
|
142
|
+
...(typeof model.updatable === 'object' && model.updatable.updatedAt),
|
|
143
|
+
} satisfies DateTimeField,
|
|
144
|
+
{
|
|
145
|
+
name: 'updatedBy',
|
|
146
|
+
type: 'relation',
|
|
147
|
+
typeName: 'User',
|
|
148
|
+
nonNull: true,
|
|
149
|
+
reverse: `updated${getModelPlural(model)}`,
|
|
150
|
+
generated: true,
|
|
151
|
+
...(typeof model.updatable === 'object' && model.updatable.updatedBy),
|
|
152
|
+
} satisfies RelationField,
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
...(model.deletable
|
|
156
|
+
? [
|
|
157
|
+
{
|
|
158
|
+
name: 'deleted',
|
|
159
|
+
type: 'Boolean',
|
|
160
|
+
nonNull: true,
|
|
161
|
+
default: false,
|
|
162
|
+
filterable: { default: false },
|
|
163
|
+
generated: true,
|
|
164
|
+
...(typeof model.deletable === 'object' && model.deletable.deleted),
|
|
165
|
+
} satisfies BooleanField,
|
|
166
|
+
{
|
|
167
|
+
name: 'deletedAt',
|
|
168
|
+
type: 'DateTime',
|
|
169
|
+
orderable: true,
|
|
170
|
+
generated: true,
|
|
171
|
+
...(typeof model.deletable === 'object' && model.deletable.deletedAt),
|
|
172
|
+
} satisfies DateTimeField,
|
|
173
|
+
{
|
|
174
|
+
name: 'deletedBy',
|
|
175
|
+
type: 'relation',
|
|
176
|
+
typeName: 'User',
|
|
177
|
+
reverse: `deleted${getModelPlural(model)}`,
|
|
178
|
+
generated: true,
|
|
179
|
+
...(typeof model.deletable === 'object' && model.deletable.deletedBy),
|
|
180
|
+
} satisfies RelationField,
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
] satisfies ModelField[]
|
|
184
|
+
).map((field: ModelField) => ({
|
|
185
|
+
...field,
|
|
186
|
+
...(field.type === 'relation' && {
|
|
187
|
+
foreignKey: field.foreignKey || `${field.name}Id`,
|
|
188
|
+
}),
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
for (const field of objectModel.fields) {
|
|
193
|
+
objectModel.fieldsByName[field.name] = field;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return objectModel;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
for (const model of models) {
|
|
200
|
+
for (const field of model.fields) {
|
|
201
|
+
if (field.type !== 'relation') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const fieldModel = summonByName(models, field.typeName);
|
|
206
|
+
|
|
207
|
+
const reverseRelation: ReverseRelation = {
|
|
208
|
+
type: 'relation',
|
|
209
|
+
name: field.reverse || (field.toOne ? typeToField(model.name) : getModelPluralField(model)),
|
|
210
|
+
foreignKey: get(field, 'foreignKey'),
|
|
211
|
+
typeName: model.name,
|
|
212
|
+
toOne: !!field.toOne,
|
|
213
|
+
fieldModel,
|
|
214
|
+
field,
|
|
215
|
+
model,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const relation: Relation = {
|
|
219
|
+
field,
|
|
220
|
+
model: fieldModel,
|
|
221
|
+
reverseRelation,
|
|
222
|
+
};
|
|
223
|
+
model.relations.push(relation);
|
|
224
|
+
model.relationsByName[relation.field.name] = relation;
|
|
225
|
+
|
|
226
|
+
fieldModel.reverseRelations.push(reverseRelation);
|
|
227
|
+
|
|
228
|
+
fieldModel.reverseRelationsByName[reverseRelation.name] = reverseRelation;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return models;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const summonByName = <T extends { name: string }>(array: T[], value: string) => summonByKey(array, 'name', value);
|
|
236
|
+
|
|
237
|
+
export const summonByKey = <T>(array: readonly T[] | undefined, key: string, value: unknown) =>
|
|
238
|
+
summon(array, (element: T) => lodashGet(element, key) === value, `No element found with ${key} ${value}`);
|
|
239
|
+
|
|
240
|
+
export const summon = <T>(array: readonly T[] | undefined, cb: Parameters<T[]['find']>[1], errorMessage?: string) => {
|
|
241
|
+
if (array === undefined) {
|
|
242
|
+
console.trace();
|
|
243
|
+
throw new Error('Base array is not defined.');
|
|
244
|
+
}
|
|
245
|
+
const result = array.find(cb);
|
|
246
|
+
if (result === undefined) {
|
|
247
|
+
console.trace();
|
|
248
|
+
throw new Error(errorMessage || 'Element not found.');
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
type ForSure<T> = T extends undefined | null ? never : T;
|
|
254
|
+
|
|
255
|
+
export const it = <T>(object: T | null | undefined): ForSure<T> => {
|
|
256
|
+
if (object === undefined || object === null) {
|
|
257
|
+
console.trace();
|
|
258
|
+
throw new Error('Base object is not defined.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return object as ForSure<T>;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const get = <T, U extends keyof ForSure<T>>(object: T | null | undefined, key: U): ForSure<ForSure<T>[U]> => {
|
|
265
|
+
const value = it(object)[key];
|
|
266
|
+
if (value === undefined || value === null) {
|
|
267
|
+
console.trace();
|
|
268
|
+
throw new Error(`Object doesn't have ${String(key)}`);
|
|
269
|
+
}
|
|
270
|
+
return value as ForSure<ForSure<T>[U]>;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export const getString = (v: unknown) => {
|
|
274
|
+
assert(typeof v === 'string');
|
|
275
|
+
return v;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const retry = async <T>(cb: () => Promise<T>, condition: (e: any) => boolean) => {
|
|
279
|
+
try {
|
|
280
|
+
return await cb();
|
|
281
|
+
} catch (e) {
|
|
282
|
+
if (condition(e)) {
|
|
283
|
+
return await cb();
|
|
284
|
+
} else {
|
|
285
|
+
throw e;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
package/src/permissions/check.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Knex } from 'knex';
|
|
2
2
|
import { FullContext } from '../context';
|
|
3
3
|
import { NotFoundError, PermissionError } from '../errors';
|
|
4
|
-
import { Model } from '../models';
|
|
4
|
+
import { Model } from '../models/models';
|
|
5
|
+
import { get, getModelPlural, isRelation, summonByName } from '../models/utils';
|
|
5
6
|
import { AliasGenerator, hash, ors } from '../resolvers/utils';
|
|
6
|
-
import { get, getModelPlural, summonByName } from '../utils';
|
|
7
7
|
import { BasicValue } from '../values';
|
|
8
8
|
import { PermissionAction, PermissionLink, PermissionStack } from './generate';
|
|
9
9
|
|
|
@@ -139,7 +139,7 @@ export const checkCanWrite = async (
|
|
|
139
139
|
let linked = false;
|
|
140
140
|
|
|
141
141
|
for (const field of model.fields
|
|
142
|
-
.filter(
|
|
142
|
+
.filter(isRelation)
|
|
143
143
|
.filter((field) => field.generated || (action === 'CREATE' ? field.creatable : field.updatable))) {
|
|
144
144
|
const foreignKey = field.foreignKey || `${field.name}Id`;
|
|
145
145
|
const foreignId = data[foreignKey] as string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Models } from '../models';
|
|
2
|
-
import { summonByName } from '../utils';
|
|
1
|
+
import { Models } from '../models/models';
|
|
2
|
+
import { isRelation, summonByName } from '../models/utils';
|
|
3
3
|
|
|
4
4
|
export type PermissionAction = 'READ' | 'CREATE' | 'UPDATE' | 'DELETE' | 'RESTORE' | 'LINK';
|
|
5
5
|
|
|
@@ -114,7 +114,7 @@ const addPermissions = (models: Models, permissions: RolePermissions, links: Per
|
|
|
114
114
|
|
|
115
115
|
if (block.RELATIONS) {
|
|
116
116
|
for (const [relation, subBlock] of Object.entries(block.RELATIONS)) {
|
|
117
|
-
const field = model.fields.find((field) => field.
|
|
117
|
+
const field = model.fields.filter(isRelation).find((field) => field.name === relation);
|
|
118
118
|
let link: PermissionLink;
|
|
119
119
|
if (field) {
|
|
120
120
|
link = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GraphQLObjectType, GraphQLSchema, TypeDefinitionNode, TypeNode, ValueNode } from 'graphql';
|
|
2
2
|
import { Kind } from 'graphql';
|
|
3
|
-
import { summonByKey } from '../utils';
|
|
3
|
+
import { summonByKey } from '../models/utils';
|
|
4
4
|
import { Value } from '../values';
|
|
5
5
|
import { FieldResolverNode } from './node';
|
|
6
6
|
import { Maybe, VariableValues } from './utils';
|
package/src/resolvers/filters.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Knex } from 'knex';
|
|
2
2
|
import { ForbiddenError, UserInputError } from '../errors';
|
|
3
|
-
import { get, summonByName } from '../utils';
|
|
3
|
+
import { get, summonByName } from '../models/utils';
|
|
4
4
|
import { OrderBy, Where, normalizeArguments } from './arguments';
|
|
5
5
|
import { FieldResolverNode, WhereNode } from './node';
|
|
6
6
|
import { Joins, Ops, addJoin, apply, ors } from './utils';
|
|
@@ -85,7 +85,7 @@ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>,
|
|
|
85
85
|
const field = summonByName(node.model.fields, key);
|
|
86
86
|
const fullKey = `${node.shortTableAlias}.${key}`;
|
|
87
87
|
|
|
88
|
-
if (field.relation) {
|
|
88
|
+
if (field.type === 'relation') {
|
|
89
89
|
const relation = get(node.model.relationsByName, field.name);
|
|
90
90
|
const tableAlias = `${node.model.name}__W__${key}`;
|
|
91
91
|
const subNode: WhereNode = {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { GraphQLResolveInfo } from 'graphql';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
2
3
|
import { v4 as uuid } from 'uuid';
|
|
3
4
|
import { Context, FullContext } from '../context';
|
|
4
5
|
import { ForbiddenError, GraphQLError } from '../errors';
|
|
5
|
-
import { Entity, Model, ModelField
|
|
6
|
+
import { Entity, Model, ModelField } from '../models/models';
|
|
7
|
+
import { get, isEnumList, it, summonByName, typeToField } from '../models/utils';
|
|
6
8
|
import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
|
|
7
|
-
import { get, it, summonByName, typeToField } from '../utils';
|
|
8
9
|
import { resolve } from './resolver';
|
|
9
10
|
import { AliasGenerator } from './utils';
|
|
10
11
|
|
|
@@ -113,10 +114,10 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
|
|
|
113
114
|
if (!(currentModel.name in toDelete)) {
|
|
114
115
|
toDelete[currentModel.name] = {};
|
|
115
116
|
}
|
|
116
|
-
if (entity.id in toDelete[currentModel.name]) {
|
|
117
|
+
if ((entity.id as string) in toDelete[currentModel.name]) {
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
|
-
toDelete[currentModel.name][entity.id] = entity[currentModel.displayField || 'id'] || entity.id;
|
|
120
|
+
toDelete[currentModel.name][entity.id as string] = (entity[currentModel.displayField || 'id'] || entity.id) as string;
|
|
120
121
|
|
|
121
122
|
if (!dryRun) {
|
|
122
123
|
const normalizedInput = { deleted: true, deletedAt: ctx.now, deletedById: ctx.user.id };
|
|
@@ -275,8 +276,8 @@ const createRevision = async (model: Model, data: Entity, ctx: Context) => {
|
|
|
275
276
|
revisionData.deleted = data.deleted || false;
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
for (const {
|
|
279
|
-
const col = relation ? `${name}Id` : name;
|
|
279
|
+
for (const { type, name, nonNull, ...field } of model.fields.filter(({ updatable }) => updatable)) {
|
|
280
|
+
const col = type === 'relation' ? `${name}Id` : name;
|
|
280
281
|
if (nonNull && (!(col in data) || col === undefined || col === null)) {
|
|
281
282
|
revisionData[col] = get(field, 'default');
|
|
282
283
|
} else {
|
|
@@ -301,16 +302,16 @@ const sanitize = (ctx: FullContext, model: Model, data: Entity) => {
|
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
if (isEndOfDay(field) && data[key]) {
|
|
304
|
-
data[key] = data[key].endOf('day');
|
|
305
|
+
data[key] = (data[key] as DateTime).endOf('day');
|
|
305
306
|
continue;
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
if (isEnumList(ctx.rawModels, field) && Array.isArray(data[key])) {
|
|
309
|
-
data[key] = `{${data[key].join(',')}}`;
|
|
310
|
+
data[key] = `{${(data[key] as string[]).join(',')}}`;
|
|
310
311
|
continue;
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
314
|
};
|
|
314
315
|
|
|
315
316
|
const isEndOfDay = (field?: ModelField) =>
|
|
316
|
-
field?.endOfDay === true && field?.dateTimeType === 'date' && field?.type === 'DateTime';
|
|
317
|
+
field.type === 'DateTime' && field?.endOfDay === true && field?.dateTimeType === 'date' && field?.type === 'DateTime';
|
package/src/resolvers/node.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type {
|
|
|
8
8
|
} from 'graphql';
|
|
9
9
|
|
|
10
10
|
import { FullContext } from '../context';
|
|
11
|
-
import {
|
|
12
|
-
import { get, summonByKey, summonByName } from '../utils';
|
|
11
|
+
import { Model } from '../models/models';
|
|
12
|
+
import { get, isRawObjectModel, summonByKey, summonByName } from '../models/utils';
|
|
13
13
|
import {
|
|
14
14
|
getFragmentTypeName,
|
|
15
15
|
getNameOrAlias,
|
|
@@ -113,7 +113,7 @@ export const getSimpleFields = (node: ResolverNode) => {
|
|
|
113
113
|
return true;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
return node.model.fields.some(({
|
|
116
|
+
return node.model.fields.some(({ type, name }) => type === 'json' && name === selection.name.value);
|
|
117
117
|
});
|
|
118
118
|
};
|
|
119
119
|
|
|
@@ -150,13 +150,13 @@ export const getJoins = (node: ResolverNode, toMany: boolean) => {
|
|
|
150
150
|
|
|
151
151
|
const typeName = getTypeName(fieldDefinition.type);
|
|
152
152
|
|
|
153
|
-
if (
|
|
153
|
+
if (isRawObjectModel(summonByName(ctx.rawModels, typeName))) {
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
const baseModel = summonByName(ctx.models, baseTypeDefinition.name.value);
|
|
158
158
|
|
|
159
|
-
let foreignKey;
|
|
159
|
+
let foreignKey: string | undefined;
|
|
160
160
|
if (toMany) {
|
|
161
161
|
const reverseRelation = baseModel.reverseRelationsByName[fieldName];
|
|
162
162
|
if (!reverseRelation) {
|
|
@@ -165,7 +165,7 @@ export const getJoins = (node: ResolverNode, toMany: boolean) => {
|
|
|
165
165
|
foreignKey = reverseRelation.foreignKey;
|
|
166
166
|
} else {
|
|
167
167
|
const modelField = baseModel.fieldsByName[fieldName];
|
|
168
|
-
if (
|
|
168
|
+
if (modelField?.type !== 'relation') {
|
|
169
169
|
continue;
|
|
170
170
|
}
|
|
171
171
|
foreignKey = modelField.foreignKey;
|
|
@@ -4,9 +4,9 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
4
4
|
import flatMap from 'lodash/flatMap';
|
|
5
5
|
import { Context, FullContext } from '../context';
|
|
6
6
|
import { NotFoundError, PermissionError } from '../errors';
|
|
7
|
+
import { get, summonByKey } from '../models/utils';
|
|
7
8
|
import { applyPermissions } from '../permissions/check';
|
|
8
9
|
import { PermissionStack } from '../permissions/generate';
|
|
9
|
-
import { get, summonByKey } from '../utils';
|
|
10
10
|
import { applyFilters } from './filters';
|
|
11
11
|
import {
|
|
12
12
|
FieldResolverNode,
|
|
@@ -115,11 +115,11 @@ const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins: Joins
|
|
|
115
115
|
.filter((n) => {
|
|
116
116
|
const field = node.model.fields.find(({ name }) => name === n.name.value);
|
|
117
117
|
|
|
118
|
-
if (!field || field.relation || field.raw) {
|
|
118
|
+
if (!field || field.type === 'relation' || field.type === 'raw') {
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
if (field.
|
|
122
|
+
if (typeof field.queriable === 'object' && !field.queriable.roles?.includes(node.ctx.user.role)) {
|
|
123
123
|
throw new PermissionError(
|
|
124
124
|
'READ',
|
|
125
125
|
`${node.model.name}'s field "${field.name}"`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Models } from '../models';
|
|
2
|
-
import { getModelPluralField, merge, typeToField } from '../utils';
|
|
1
|
+
import { Models } from '../models/models';
|
|
2
|
+
import { getModelPluralField, merge, typeToField } from '../models/utils';
|
|
3
3
|
import { mutationResolver } from './mutations';
|
|
4
4
|
import { queryResolver } from './resolver';
|
|
5
5
|
|
package/src/resolvers/utils.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
import { Kind } from 'graphql';
|
|
13
13
|
import { Knex } from 'knex';
|
|
14
14
|
import { UserInputError } from '../errors';
|
|
15
|
-
import { get, it } from '../utils';
|
|
15
|
+
import { get, it } from '../models/utils';
|
|
16
16
|
import { Value } from '../values';
|
|
17
17
|
import { FieldResolverNode } from './node';
|
|
18
18
|
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { makeExecutableSchema } from '@graphql-tools/schema';
|
|
2
|
-
import { execute, parse, Source } from 'graphql';
|
|
3
1
|
import knex from 'knex';
|
|
4
2
|
import { DateTime } from 'luxon';
|
|
3
|
+
import { execute } from '../../src';
|
|
5
4
|
import { gql } from '../../src/client/gql';
|
|
6
|
-
import { Context } from '../../src/context';
|
|
7
|
-
import { generate } from '../../src/generate';
|
|
8
5
|
import { getResolvers } from '../../src/resolvers';
|
|
9
6
|
import { models, permissions, rawModels } from '../utils/models';
|
|
10
7
|
|
|
11
|
-
const test = async (operationName: string, query: string,
|
|
8
|
+
const test = async (operationName: string, query: string, variables: object, responses: unknown[]) => {
|
|
12
9
|
const knexInstance = knex({
|
|
13
10
|
client: 'postgresql',
|
|
14
11
|
});
|
|
@@ -25,11 +22,8 @@ const test = async (operationName: string, query: string, variableValues: object
|
|
|
25
22
|
});
|
|
26
23
|
|
|
27
24
|
const user = await knexInstance('User').where({ id: 1 }).first();
|
|
28
|
-
const
|
|
29
|
-
const contextValue: Context = {
|
|
30
|
-
req: null as any,
|
|
25
|
+
const result = await execute({
|
|
31
26
|
knex: knexInstance,
|
|
32
|
-
document: typeDefs,
|
|
33
27
|
locale: 'en',
|
|
34
28
|
locales: ['en'],
|
|
35
29
|
user,
|
|
@@ -37,16 +31,7 @@ const test = async (operationName: string, query: string, variableValues: object
|
|
|
37
31
|
models,
|
|
38
32
|
permissions,
|
|
39
33
|
now: DateTime.fromISO('2020-01-01T00:00:00.000Z'),
|
|
40
|
-
|
|
41
|
-
const result = await execute({
|
|
42
|
-
schema: makeExecutableSchema({
|
|
43
|
-
typeDefs,
|
|
44
|
-
resolvers: getResolvers(models),
|
|
45
|
-
}),
|
|
46
|
-
document: parse(new Source(query, 'GraphQL request')),
|
|
47
|
-
contextValue,
|
|
48
|
-
variableValues,
|
|
49
|
-
operationName,
|
|
34
|
+
body: { operationName, query, variables }
|
|
50
35
|
});
|
|
51
36
|
|
|
52
37
|
expect(result).toMatchSnapshot();
|
package/tests/utils/models.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RawModels } from '../../src/models';
|
|
2
|
+
import { getModels } from '../../src/models/utils';
|
|
2
3
|
import { generatePermissions, PermissionsConfig } from '../../src/permissions/generate';
|
|
3
|
-
import { getModels } from '../../src/utils';
|
|
4
4
|
|
|
5
5
|
export const rawModels: RawModels = [
|
|
6
6
|
{
|
|
@@ -16,7 +16,7 @@ export const rawModels: RawModels = [
|
|
|
16
16
|
|
|
17
17
|
{
|
|
18
18
|
name: 'SomeRawObject',
|
|
19
|
-
type: 'raw
|
|
19
|
+
type: 'raw',
|
|
20
20
|
fields: [{ name: 'field', type: 'String' }],
|
|
21
21
|
},
|
|
22
22
|
|
|
@@ -30,7 +30,8 @@ export const rawModels: RawModels = [
|
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
name: 'role',
|
|
33
|
-
type: '
|
|
33
|
+
type: 'enum',
|
|
34
|
+
typeName: 'Role',
|
|
34
35
|
},
|
|
35
36
|
],
|
|
36
37
|
},
|
|
@@ -47,11 +48,11 @@ export const rawModels: RawModels = [
|
|
|
47
48
|
orderable: true,
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
|
-
type: '
|
|
51
|
+
type: 'relation',
|
|
52
|
+
typeName: 'AnotherObject',
|
|
51
53
|
name: 'myself',
|
|
52
54
|
toOne: true,
|
|
53
55
|
reverse: 'self',
|
|
54
|
-
relation: true
|
|
55
56
|
}
|
|
56
57
|
],
|
|
57
58
|
},
|
|
@@ -73,8 +74,8 @@ export const rawModels: RawModels = [
|
|
|
73
74
|
},
|
|
74
75
|
{
|
|
75
76
|
name: 'another',
|
|
76
|
-
type: '
|
|
77
|
-
|
|
77
|
+
type: 'relation',
|
|
78
|
+
typeName: 'AnotherObject',
|
|
78
79
|
filterable: true,
|
|
79
80
|
updatable: true,
|
|
80
81
|
nonNull: true,
|
package/tests/utils/server.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { execute, parse, Source, TypedQueryDocumentNode } from 'graphql';
|
|
1
|
+
import { TypedQueryDocumentNode } from 'graphql';
|
|
3
2
|
import graphqlRequest, { RequestDocument, Variables } from 'graphql-request';
|
|
4
|
-
import {
|
|
3
|
+
import { RequestListener, createServer } from 'http';
|
|
5
4
|
import { Knex } from 'knex';
|
|
6
5
|
import { DateTime } from 'luxon';
|
|
7
|
-
import {
|
|
8
|
-
import { generate } from '../../src/generate';
|
|
9
|
-
import { getResolvers } from '../../src/resolvers';
|
|
6
|
+
import { execute } from '../../src';
|
|
10
7
|
import { getKnex } from './database/knex';
|
|
11
8
|
import { setupSchema } from './database/schema';
|
|
12
9
|
import { ADMIN_ID, setupSeed } from './database/seed';
|
|
@@ -50,25 +47,7 @@ export const withServer = async (
|
|
|
50
47
|
|
|
51
48
|
handler = async (req, res) => {
|
|
52
49
|
const user = await knex('User').where({ id: ADMIN_ID }).first();
|
|
53
|
-
|
|
54
|
-
const typeDefs = generate(rawModels);
|
|
55
|
-
const contextValue: Context = {
|
|
56
|
-
req,
|
|
57
|
-
knex,
|
|
58
|
-
document: typeDefs,
|
|
59
|
-
locale: 'en',
|
|
60
|
-
locales: ['en'],
|
|
61
|
-
user,
|
|
62
|
-
rawModels,
|
|
63
|
-
models,
|
|
64
|
-
permissions,
|
|
65
|
-
now: DateTime.fromISO('2020-01-01T00:00:00.000Z'),
|
|
66
|
-
};
|
|
67
|
-
const {
|
|
68
|
-
query,
|
|
69
|
-
variables: variableValues,
|
|
70
|
-
operationName,
|
|
71
|
-
} = await new Promise<any>((res) => {
|
|
50
|
+
const body = await new Promise<any>((res) => {
|
|
72
51
|
const chunks: any = [];
|
|
73
52
|
req
|
|
74
53
|
.on('data', (chunk) => {
|
|
@@ -79,14 +58,15 @@ export const withServer = async (
|
|
|
79
58
|
});
|
|
80
59
|
});
|
|
81
60
|
const result = await execute({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
knex,
|
|
62
|
+
locale: 'en',
|
|
63
|
+
locales: ['en'],
|
|
64
|
+
user,
|
|
65
|
+
rawModels,
|
|
66
|
+
models,
|
|
67
|
+
permissions,
|
|
68
|
+
now: DateTime.fromISO('2020-01-01T00:00:00.000Z'),
|
|
69
|
+
body,
|
|
90
70
|
});
|
|
91
71
|
|
|
92
72
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|