@smartive/graphql-magic 3.1.0 → 4.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/dist/cjs/index.cjs +196 -170
- 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/db/generate.js +21 -13
- package/dist/esm/db/generate.js.map +1 -1
- package/dist/esm/generate/generate.js +17 -22
- package/dist/esm/generate/generate.js.map +1 -1
- package/dist/esm/generate/utils.d.ts +10 -1
- package/dist/esm/generate/utils.js.map +1 -1
- package/dist/esm/migrations/generate.js +82 -89
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models.d.ts +115 -93
- package/dist/esm/models.js +1 -26
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/permissions/check.js +2 -2
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/permissions/generate.js +2 -2
- package/dist/esm/permissions/generate.js.map +1 -1
- package/dist/esm/resolvers/filters.js +1 -1
- 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.js +4 -5
- package/dist/esm/resolvers/node.js.map +1 -1
- package/dist/esm/resolvers/resolver.js +2 -2
- package/dist/esm/resolvers/resolver.js.map +1 -1
- package/dist/esm/utils.d.ts +181 -1
- package/dist/esm/utils.js +69 -21
- package/dist/esm/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/client/queries.ts +16 -10
- package/src/db/generate.ts +22 -15
- package/src/generate/generate.ts +26 -34
- package/src/generate/utils.ts +11 -1
- package/src/migrations/generate.ts +84 -82
- package/src/models.ts +113 -157
- package/src/permissions/check.ts +2 -2
- package/src/permissions/generate.ts +2 -2
- package/src/resolvers/filters.ts +1 -1
- package/src/resolvers/mutations.ts +10 -9
- package/src/resolvers/node.ts +6 -6
- package/src/resolvers/resolver.ts +2 -2
- package/src/utils.ts +158 -57
- package/tests/utils/models.ts +7 -6
|
@@ -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';
|
|
6
7
|
import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
|
|
7
|
-
import { get, it, summonByName, typeToField } from '../utils';
|
|
8
|
+
import { get, isEnumList, 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';
|
|
12
|
+
import { get, isRawObjectModel, summonByKey, summonByName } from '../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;
|
|
@@ -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}"`,
|
package/src/utils.ts
CHANGED
|
@@ -4,7 +4,24 @@ import camelCase from 'lodash/camelCase';
|
|
|
4
4
|
import lodashGet from 'lodash/get';
|
|
5
5
|
import kebabCase from 'lodash/kebabCase';
|
|
6
6
|
import startCase from 'lodash/startCase';
|
|
7
|
-
import {
|
|
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';
|
|
8
25
|
|
|
9
26
|
const isNotFalsy = <T>(v: T | null | undefined | false): v is T => typeof v !== 'undefined' && v !== null && v !== false;
|
|
10
27
|
|
|
@@ -26,6 +43,59 @@ export const getModelLabel = (model: Model) => getLabel(model.name);
|
|
|
26
43
|
|
|
27
44
|
export const getLabel = (s: string) => startCase(camelCase(s));
|
|
28
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
|
+
|
|
29
99
|
export const getModels = (rawModels: RawModels): Models => {
|
|
30
100
|
const models: Models = rawModels.filter(isObjectModel).map((model) => {
|
|
31
101
|
const objectModel: Model = {
|
|
@@ -35,60 +105,86 @@ export const getModels = (rawModels: RawModels): Models => {
|
|
|
35
105
|
relationsByName: {},
|
|
36
106
|
reverseRelations: [],
|
|
37
107
|
reverseRelationsByName: {},
|
|
38
|
-
fields:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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) => ({
|
|
89
185
|
...field,
|
|
90
|
-
...(field.relation && {
|
|
91
|
-
foreignKey: foreignKey || `${field.name}Id`,
|
|
186
|
+
...(field.type === 'relation' && {
|
|
187
|
+
foreignKey: field.foreignKey || `${field.name}Id`,
|
|
92
188
|
}),
|
|
93
189
|
})),
|
|
94
190
|
};
|
|
@@ -101,13 +197,18 @@ export const getModels = (rawModels: RawModels): Models => {
|
|
|
101
197
|
});
|
|
102
198
|
|
|
103
199
|
for (const model of models) {
|
|
104
|
-
for (const field of model.fields
|
|
105
|
-
|
|
200
|
+
for (const field of model.fields) {
|
|
201
|
+
if (field.type !== 'relation') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const fieldModel = summonByName(models, field.typeName);
|
|
106
206
|
|
|
107
207
|
const reverseRelation: ReverseRelation = {
|
|
208
|
+
type: 'relation',
|
|
108
209
|
name: field.reverse || (field.toOne ? typeToField(model.name) : getModelPluralField(model)),
|
|
109
210
|
foreignKey: get(field, 'foreignKey'),
|
|
110
|
-
|
|
211
|
+
typeName: model.name,
|
|
111
212
|
toOne: !!field.toOne,
|
|
112
213
|
fieldModel,
|
|
113
214
|
field,
|
package/tests/utils/models.ts
CHANGED
|
@@ -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,
|