@smartive/graphql-magic 19.3.0 → 19.3.1-next.1
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 +3 -3
- package/dist/cjs/index.cjs +72 -72
- package/dist/esm/resolvers/mutations.d.ts +6 -6
- package/dist/esm/resolvers/mutations.js +15 -15
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/package.json +3 -3
- package/src/resolvers/mutations.ts +395 -391
|
@@ -10,10 +10,13 @@ import { anyDateToLuxon } from '../utils';
|
|
|
10
10
|
import { resolve } from './resolver';
|
|
11
11
|
import { AliasGenerator, fetchDisplay, getTechnicalDisplay } from './utils';
|
|
12
12
|
|
|
13
|
+
const withTransaction = async <T extends MutationContext>(ctx: T, fn: (ctx: T) => Promise<any>) =>
|
|
14
|
+
await ctx.knex.transaction(async (knex) => fn({ ...ctx, knex }));
|
|
15
|
+
|
|
13
16
|
export const mutationResolver = async (_parent: any, args: any, partialCtx: Context, info: GraphQLResolveInfo) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const [, mutation, modelName] = it(info.fieldName.match(/^(create|update|delete|restore)(.+)$/));
|
|
18
|
+
|
|
19
|
+
return await withTransaction({ ...partialCtx, info, aliases: new AliasGenerator() }, async (ctx) => {
|
|
17
20
|
switch (mutation) {
|
|
18
21
|
case 'create': {
|
|
19
22
|
const id = await createEntity(modelName, args.data, ctx, 'mutation');
|
|
@@ -53,78 +56,80 @@ export const createEntity = async (
|
|
|
53
56
|
input: Entity,
|
|
54
57
|
ctx: MutationContext,
|
|
55
58
|
trigger: Trigger = 'direct-call',
|
|
56
|
-
) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
normalizedInput.id
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
normalizedInput.createdAt
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
normalizedInput.createdById
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await checkCanWrite(ctx, model, normalizedInput, 'CREATE');
|
|
75
|
-
await ctx.handleUploads?.(normalizedInput);
|
|
76
|
-
|
|
77
|
-
await ctx.mutationHook?.({
|
|
78
|
-
model,
|
|
79
|
-
action: 'create',
|
|
80
|
-
trigger,
|
|
81
|
-
when: 'before',
|
|
82
|
-
data: { prev: {}, input, normalizedInput, next: normalizedInput },
|
|
83
|
-
ctx,
|
|
84
|
-
});
|
|
59
|
+
) =>
|
|
60
|
+
withTransaction(ctx, async (ctx) => {
|
|
61
|
+
const model = ctx.models.getModel(modelName, 'entity');
|
|
62
|
+
const normalizedInput = { ...input };
|
|
63
|
+
if (!normalizedInput.id) {
|
|
64
|
+
normalizedInput.id = uuid();
|
|
65
|
+
}
|
|
66
|
+
const id = normalizedInput.id as string;
|
|
67
|
+
if (!normalizedInput.createdAt) {
|
|
68
|
+
normalizedInput.createdAt = ctx.now;
|
|
69
|
+
}
|
|
70
|
+
if (!normalizedInput.createdById) {
|
|
71
|
+
normalizedInput.createdById = ctx.user?.id;
|
|
72
|
+
}
|
|
73
|
+
if (model.parent) {
|
|
74
|
+
normalizedInput.type = model.name;
|
|
75
|
+
}
|
|
76
|
+
sanitize(ctx, model, normalizedInput);
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
await checkCanWrite(ctx, model, normalizedInput, 'CREATE');
|
|
79
|
+
await ctx.handleUploads?.(normalizedInput);
|
|
80
|
+
|
|
81
|
+
await ctx.mutationHook?.({
|
|
82
|
+
model,
|
|
83
|
+
action: 'create',
|
|
84
|
+
trigger,
|
|
85
|
+
when: 'before',
|
|
86
|
+
data: { prev: {}, input, normalizedInput, next: normalizedInput },
|
|
87
|
+
ctx,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (model.parent) {
|
|
91
|
+
const rootInput = {};
|
|
92
|
+
const childInput = { id };
|
|
93
|
+
for (const field of model.fields) {
|
|
94
|
+
const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
|
|
95
|
+
if (columnName in normalizedInput) {
|
|
96
|
+
if (field.inherited) {
|
|
97
|
+
rootInput[columnName] = normalizedInput[columnName];
|
|
98
|
+
} else {
|
|
99
|
+
childInput[columnName] = normalizedInput[columnName];
|
|
100
|
+
}
|
|
96
101
|
}
|
|
97
102
|
}
|
|
103
|
+
await ctx.knex(model.parent).insert(rootInput);
|
|
104
|
+
await ctx.knex(model.name).insert(childInput);
|
|
105
|
+
} else {
|
|
106
|
+
await ctx.knex(model.name).insert(normalizedInput);
|
|
98
107
|
}
|
|
99
|
-
await
|
|
100
|
-
await ctx.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
trigger,
|
|
109
|
-
when: 'after',
|
|
110
|
-
data: { prev: {}, input, normalizedInput, next: normalizedInput },
|
|
111
|
-
ctx,
|
|
112
|
-
});
|
|
108
|
+
await createRevision(model, normalizedInput, ctx);
|
|
109
|
+
await ctx.mutationHook?.({
|
|
110
|
+
model,
|
|
111
|
+
action: 'create',
|
|
112
|
+
trigger,
|
|
113
|
+
when: 'after',
|
|
114
|
+
data: { prev: {}, input, normalizedInput, next: normalizedInput },
|
|
115
|
+
ctx,
|
|
116
|
+
});
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
};
|
|
118
|
+
return normalizedInput.id as string;
|
|
119
|
+
});
|
|
116
120
|
|
|
117
121
|
export const updateEntities = async (
|
|
118
122
|
modelName: string,
|
|
119
123
|
where: Record<string, unknown>,
|
|
120
124
|
updateFields: Entity,
|
|
121
125
|
ctx: MutationContext,
|
|
122
|
-
) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
126
|
+
) =>
|
|
127
|
+
withTransaction(ctx, async (ctx) => {
|
|
128
|
+
const entities = await ctx.knex(modelName).where(where).select('id');
|
|
129
|
+
for (const entity of entities) {
|
|
130
|
+
await updateEntity(modelName, entity.id, updateFields, ctx);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
128
133
|
|
|
129
134
|
export const updateEntity = async (
|
|
130
135
|
modelName: string,
|
|
@@ -132,55 +137,57 @@ export const updateEntity = async (
|
|
|
132
137
|
input: Entity,
|
|
133
138
|
ctx: MutationContext,
|
|
134
139
|
trigger: Trigger = 'direct-call',
|
|
135
|
-
) =>
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
) =>
|
|
141
|
+
withTransaction(ctx, async (ctx) => {
|
|
142
|
+
const model = ctx.models.getModel(modelName, 'entity');
|
|
143
|
+
const normalizedInput = { ...input };
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
sanitize(ctx, model, normalizedInput);
|
|
140
146
|
|
|
141
|
-
|
|
147
|
+
const currentEntity = await getEntityToMutate(ctx, model, { id }, 'UPDATE');
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
// Remove data that wouldn't mutate given that it's irrelevant for permissions
|
|
150
|
+
for (const key of Object.keys(normalizedInput)) {
|
|
151
|
+
if (normalizedInput[key] === currentEntity[key]) {
|
|
152
|
+
delete normalizedInput[key];
|
|
153
|
+
}
|
|
147
154
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (Object.keys(normalizedInput).length > 0) {
|
|
151
|
-
await checkCanWrite(ctx, model, normalizedInput, 'UPDATE');
|
|
152
|
-
await ctx.handleUploads?.(normalizedInput);
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
model,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
156
|
+
if (Object.keys(normalizedInput).length > 0) {
|
|
157
|
+
await checkCanWrite(ctx, model, normalizedInput, 'UPDATE');
|
|
158
|
+
await ctx.handleUploads?.(normalizedInput);
|
|
159
|
+
|
|
160
|
+
await ctx.mutationHook?.({
|
|
161
|
+
model,
|
|
162
|
+
action: 'update',
|
|
163
|
+
trigger,
|
|
164
|
+
when: 'before',
|
|
165
|
+
data: { prev: currentEntity, input, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
166
|
+
ctx,
|
|
167
|
+
});
|
|
168
|
+
await doUpdate(model, currentEntity, normalizedInput, ctx);
|
|
169
|
+
await ctx.mutationHook?.({
|
|
170
|
+
model,
|
|
171
|
+
action: 'update',
|
|
172
|
+
trigger,
|
|
173
|
+
when: 'after',
|
|
174
|
+
data: { prev: currentEntity, input, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
175
|
+
ctx,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
173
179
|
|
|
174
180
|
type Callbacks = (() => Promise<void>)[];
|
|
175
181
|
|
|
176
|
-
export const deleteEntities = async (modelName: string, where: Record<string, unknown>, ctx: MutationContext) =>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
182
|
+
export const deleteEntities = async (modelName: string, where: Record<string, unknown>, ctx: MutationContext) =>
|
|
183
|
+
withTransaction(ctx, async (ctx) => {
|
|
184
|
+
const entities = await ctx.knex(modelName).where(where).select('id');
|
|
185
|
+
for (const entity of entities) {
|
|
186
|
+
await deleteEntity(modelName, entity.id, ctx, {
|
|
187
|
+
trigger: 'direct-call',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
184
191
|
|
|
185
192
|
export const deleteEntity = async (
|
|
186
193
|
modelName: string,
|
|
@@ -193,338 +200,335 @@ export const deleteEntity = async (
|
|
|
193
200
|
dryRun?: boolean;
|
|
194
201
|
trigger?: Trigger;
|
|
195
202
|
} = {},
|
|
196
|
-
) =>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
) =>
|
|
204
|
+
withTransaction(ctx, async (ctx) => {
|
|
205
|
+
const model = ctx.models.getModel(modelName, 'entity');
|
|
206
|
+
const rootModel = model.rootModel;
|
|
207
|
+
const entity = await getEntityToMutate(ctx, rootModel, { id }, 'DELETE');
|
|
208
|
+
|
|
209
|
+
if (entity.deleted) {
|
|
210
|
+
throw new ForbiddenError(`${getTechnicalDisplay(model, entity)} is already deleted.`);
|
|
211
|
+
}
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
string,
|
|
208
|
-
Record<
|
|
213
|
+
const toDelete: Record<string, Record<string, string>> = {};
|
|
214
|
+
const toUnlink: Record<
|
|
209
215
|
string,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
Record<
|
|
216
|
+
Record<
|
|
217
|
+
string,
|
|
218
|
+
{
|
|
219
|
+
display: string;
|
|
220
|
+
fields: string[];
|
|
221
|
+
}
|
|
222
|
+
>
|
|
223
|
+
> = {};
|
|
224
|
+
const restricted: Record<
|
|
219
225
|
string,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
Record<
|
|
227
|
+
string,
|
|
228
|
+
{
|
|
229
|
+
display: string;
|
|
230
|
+
fields: string[];
|
|
231
|
+
}
|
|
232
|
+
>
|
|
233
|
+
> = {};
|
|
226
234
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
const beforeHooks: Callbacks = [];
|
|
236
|
+
const mutations: Callbacks = [];
|
|
237
|
+
const afterHooks: Callbacks = [];
|
|
230
238
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
if ((currentEntity.id as string) in toDelete[currentModel.name]) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
toDelete[currentModel.name][currentEntity.id as string] = await fetchDisplay(ctx.knex, currentModel, currentEntity);
|
|
240
|
-
|
|
241
|
-
if (!dryRun) {
|
|
242
|
-
const normalizedInput = {
|
|
243
|
-
deleted: true,
|
|
244
|
-
deletedAt: ctx.now,
|
|
245
|
-
deletedById: ctx.user?.id,
|
|
246
|
-
deleteRootType: rootModel.name,
|
|
247
|
-
deleteRootId: entity.id,
|
|
248
|
-
};
|
|
249
|
-
if (mutationHook) {
|
|
250
|
-
beforeHooks.push(async () => {
|
|
251
|
-
await mutationHook({
|
|
252
|
-
model: currentModel,
|
|
253
|
-
action: 'delete',
|
|
254
|
-
trigger: currentTrigger,
|
|
255
|
-
when: 'before',
|
|
256
|
-
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
257
|
-
ctx,
|
|
258
|
-
});
|
|
259
|
-
});
|
|
239
|
+
const mutationHook = ctx.mutationHook;
|
|
240
|
+
const deleteCascade = async (currentModel: EntityModel, currentEntity: Entity, currentTrigger: Trigger) => {
|
|
241
|
+
if (!(currentModel.name in toDelete)) {
|
|
242
|
+
toDelete[currentModel.name] = {};
|
|
260
243
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
244
|
+
if ((currentEntity.id as string) in toDelete[currentModel.name]) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
toDelete[currentModel.name][currentEntity.id as string] = await fetchDisplay(ctx.knex, currentModel, currentEntity);
|
|
248
|
+
|
|
249
|
+
if (!dryRun) {
|
|
250
|
+
const normalizedInput = {
|
|
251
|
+
deleted: true,
|
|
252
|
+
deletedAt: ctx.now,
|
|
253
|
+
deletedById: ctx.user?.id,
|
|
254
|
+
deleteRootType: rootModel.name,
|
|
255
|
+
deleteRootId: entity.id,
|
|
256
|
+
};
|
|
257
|
+
if (mutationHook) {
|
|
258
|
+
beforeHooks.push(async () => {
|
|
259
|
+
await mutationHook({
|
|
260
|
+
model: currentModel,
|
|
261
|
+
action: 'delete',
|
|
262
|
+
trigger: currentTrigger,
|
|
263
|
+
when: 'before',
|
|
264
|
+
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
265
|
+
ctx,
|
|
266
|
+
});
|
|
273
267
|
});
|
|
268
|
+
}
|
|
269
|
+
mutations.push(async () => {
|
|
270
|
+
await doUpdate(currentModel, currentEntity, normalizedInput, ctx);
|
|
274
271
|
});
|
|
272
|
+
if (mutationHook) {
|
|
273
|
+
afterHooks.push(async () => {
|
|
274
|
+
await mutationHook({
|
|
275
|
+
model: currentModel,
|
|
276
|
+
action: 'delete',
|
|
277
|
+
trigger: currentTrigger,
|
|
278
|
+
when: 'after',
|
|
279
|
+
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
280
|
+
ctx,
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
275
284
|
}
|
|
276
|
-
}
|
|
277
285
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
286
|
+
for (const {
|
|
287
|
+
targetModel: descendantModel,
|
|
288
|
+
field: { name, foreignKey, onDelete },
|
|
289
|
+
} of currentModel.reverseRelations.filter((reverseRelation) => !reverseRelation.field.inherited)) {
|
|
290
|
+
const query = ctx.knex(descendantModel.name).where({ [foreignKey]: currentEntity.id, deleted: false });
|
|
291
|
+
const descendants = await query;
|
|
292
|
+
if (descendants.length) {
|
|
293
|
+
switch (onDelete) {
|
|
294
|
+
case 'set-null':
|
|
295
|
+
for (const descendant of descendants) {
|
|
296
|
+
if (dryRun) {
|
|
297
|
+
if (!toUnlink[descendantModel.name]) {
|
|
298
|
+
toUnlink[descendantModel.name] = {};
|
|
299
|
+
}
|
|
300
|
+
if (!toUnlink[descendantModel.name][descendant.id]) {
|
|
301
|
+
toUnlink[descendantModel.name][descendant.id] = {
|
|
302
|
+
display: await fetchDisplay(ctx.knex, descendantModel, descendant),
|
|
303
|
+
fields: [],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
toUnlink[descendantModel.name][descendant.id].fields.push(name);
|
|
307
|
+
} else {
|
|
308
|
+
const normalizedInput = { [`${name}Id`]: null };
|
|
309
|
+
if (mutationHook) {
|
|
310
|
+
beforeHooks.push(async () => {
|
|
311
|
+
await mutationHook({
|
|
312
|
+
model: descendantModel,
|
|
313
|
+
action: 'update',
|
|
314
|
+
trigger: 'set-null',
|
|
315
|
+
when: 'before',
|
|
316
|
+
data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
|
|
317
|
+
ctx,
|
|
318
|
+
});
|
|
310
319
|
});
|
|
320
|
+
}
|
|
321
|
+
mutations.push(async () => {
|
|
322
|
+
await doUpdate(descendantModel, descendant, normalizedInput, ctx);
|
|
311
323
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
when: 'after',
|
|
323
|
-
data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
|
|
324
|
-
ctx,
|
|
324
|
+
if (mutationHook) {
|
|
325
|
+
afterHooks.push(async () => {
|
|
326
|
+
await mutationHook({
|
|
327
|
+
model: descendantModel,
|
|
328
|
+
action: 'update',
|
|
329
|
+
trigger: 'set-null',
|
|
330
|
+
when: 'after',
|
|
331
|
+
data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
|
|
332
|
+
ctx,
|
|
333
|
+
});
|
|
325
334
|
});
|
|
326
|
-
}
|
|
335
|
+
}
|
|
327
336
|
}
|
|
328
337
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
338
|
+
break;
|
|
339
|
+
case 'restrict':
|
|
340
|
+
if (dryRun) {
|
|
341
|
+
if (!restricted[descendantModel.name]) {
|
|
342
|
+
restricted[descendantModel.name] = {};
|
|
343
|
+
}
|
|
344
|
+
for (const descendant of descendants) {
|
|
345
|
+
if (!restricted[descendantModel.name][descendant.id]) {
|
|
346
|
+
restricted[descendantModel.name][descendant.id] = {
|
|
347
|
+
display: await fetchDisplay(ctx.knex, descendantModel, descendant),
|
|
348
|
+
fields: [name],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
restricted[descendantModel.name][descendant.id].fields.push(name);
|
|
342
352
|
}
|
|
343
|
-
|
|
353
|
+
} else {
|
|
354
|
+
throw new ForbiddenError(
|
|
355
|
+
`${getTechnicalDisplay(model, entity)} cannot be deleted because it has ${getTechnicalDisplay(descendantModel, descendants[0])}${descendants.length > 1 ? ` (among others)` : ''}.`,
|
|
356
|
+
);
|
|
344
357
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'DELETE');
|
|
359
|
-
const deletableDescendants = await query;
|
|
360
|
-
const notDeletableDescendants = descendants.filter(
|
|
361
|
-
(descendant) => !deletableDescendants.some((d) => d.id === descendant.id),
|
|
362
|
-
);
|
|
363
|
-
if (notDeletableDescendants.length) {
|
|
364
|
-
throw new ForbiddenError(
|
|
365
|
-
`${getTechnicalDisplay(model, entity)} depends on ${descendantModel.labelPlural} which you have no permissions to delete.`,
|
|
358
|
+
break;
|
|
359
|
+
case 'cascade':
|
|
360
|
+
default: {
|
|
361
|
+
if (!descendantModel.deletable) {
|
|
362
|
+
throw new ForbiddenError(
|
|
363
|
+
`${getTechnicalDisplay(model, entity)} depends on ${getTechnicalDisplay(descendantModel, descendants[0])}${descendants.length > 1 ? ` (among others)` : ''} which cannot be deleted.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'DELETE');
|
|
367
|
+
const deletableDescendants = await query;
|
|
368
|
+
const notDeletableDescendants = descendants.filter(
|
|
369
|
+
(descendant) => !deletableDescendants.some((d) => d.id === descendant.id),
|
|
366
370
|
);
|
|
371
|
+
if (notDeletableDescendants.length) {
|
|
372
|
+
throw new ForbiddenError(
|
|
373
|
+
`${getTechnicalDisplay(model, entity)} depends on ${descendantModel.labelPlural} which you have no permissions to delete.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
for (const descendant of descendants) {
|
|
377
|
+
await deleteCascade(descendantModel, descendant, 'cascade');
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
367
380
|
}
|
|
368
|
-
for (const descendant of descendants) {
|
|
369
|
-
await deleteCascade(descendantModel, descendant, 'cascade');
|
|
370
|
-
}
|
|
371
|
-
break;
|
|
372
381
|
}
|
|
373
382
|
}
|
|
374
383
|
}
|
|
375
|
-
}
|
|
376
|
-
};
|
|
384
|
+
};
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
await deleteCascade(rootModel, entity, trigger);
|
|
379
387
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
388
|
+
for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
|
|
389
|
+
await callback();
|
|
390
|
+
}
|
|
383
391
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
392
|
+
if (dryRun) {
|
|
393
|
+
throw new GraphQLError(`Delete dry run:`, {
|
|
394
|
+
code: 'DELETE_DRY_RUN',
|
|
395
|
+
toDelete,
|
|
396
|
+
toUnlink,
|
|
397
|
+
restricted,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
393
401
|
|
|
394
|
-
export const restoreEntity = async (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
trigger: Trigger = 'direct-call',
|
|
399
|
-
) => {
|
|
400
|
-
const model = ctx.models.getModel(modelName, 'entity');
|
|
401
|
-
const rootModel = model.rootModel;
|
|
402
|
+
export const restoreEntity = async (modelName: string, id: string, ctx: MutationContext, trigger: Trigger = 'direct-call') =>
|
|
403
|
+
withTransaction(ctx, async (ctx) => {
|
|
404
|
+
const model = ctx.models.getModel(modelName, 'entity');
|
|
405
|
+
const rootModel = model.rootModel;
|
|
402
406
|
|
|
403
|
-
|
|
407
|
+
const entity = await getEntityToMutate(ctx, rootModel, { id }, 'RESTORE');
|
|
404
408
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
409
|
+
if (!entity.deleted) {
|
|
410
|
+
throw new ForbiddenError(`${getTechnicalDisplay(model, entity)} is not deleted.`);
|
|
411
|
+
}
|
|
408
412
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
if (entity.deleteRootId) {
|
|
414
|
+
if (!(entity.deleteRootType === rootModel.name && entity.deleteRootId === entity.id)) {
|
|
415
|
+
throw new ForbiddenError(
|
|
416
|
+
`Can't restore ${getTechnicalDisplay(model, entity)} directly. To restore it, restore ${entity.deleteRootType} ${entity.deleteRootId}.`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
414
419
|
}
|
|
415
|
-
}
|
|
416
420
|
|
|
417
|
-
|
|
421
|
+
const toRestore: Record<string, Set<string>> = {};
|
|
418
422
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
const beforeHooks: Callbacks = [];
|
|
424
|
+
const mutations: Callbacks = [];
|
|
425
|
+
const afterHooks: Callbacks = [];
|
|
422
426
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
427
|
+
const restoreCascade = async (currentModel: EntityModel, currentEntity: Entity, currentTrigger: Trigger) => {
|
|
428
|
+
if (entity.deleteRootId || currentEntity.deleteRootId) {
|
|
429
|
+
if (!(currentEntity.deleteRootType === model.name && currentEntity.deleteRootId === entity.id)) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Legacy heuristic
|
|
434
|
+
} else if (
|
|
435
|
+
!anyDateToLuxon(currentEntity.deletedAt, ctx.timeZone)!.equals(anyDateToLuxon(entity.deletedAt, ctx.timeZone)!)
|
|
436
|
+
) {
|
|
426
437
|
return;
|
|
427
438
|
}
|
|
428
439
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
model: currentModel,
|
|
452
|
-
action: 'restore',
|
|
453
|
-
trigger: currentTrigger,
|
|
454
|
-
when: 'before',
|
|
455
|
-
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
456
|
-
ctx,
|
|
440
|
+
if (!(currentModel.name in toRestore)) {
|
|
441
|
+
toRestore[currentModel.name] = new Set();
|
|
442
|
+
}
|
|
443
|
+
toRestore[currentModel.name].add(currentEntity.id as string);
|
|
444
|
+
|
|
445
|
+
const normalizedInput: Entity = {
|
|
446
|
+
deleted: false,
|
|
447
|
+
deletedAt: null,
|
|
448
|
+
deletedById: null,
|
|
449
|
+
deleteRootType: null,
|
|
450
|
+
deleteRootId: null,
|
|
451
|
+
};
|
|
452
|
+
if (ctx.mutationHook) {
|
|
453
|
+
beforeHooks.push(async () => {
|
|
454
|
+
await ctx.mutationHook!({
|
|
455
|
+
model: currentModel,
|
|
456
|
+
action: 'restore',
|
|
457
|
+
trigger: currentTrigger,
|
|
458
|
+
when: 'before',
|
|
459
|
+
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
460
|
+
ctx,
|
|
461
|
+
});
|
|
457
462
|
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
}
|
|
464
|
+
mutations.push(async () => {
|
|
465
|
+
for (const relation of currentModel.relations) {
|
|
466
|
+
const parentId = currentEntity[relation.field.foreignKey] as string | undefined;
|
|
467
|
+
if (!parentId) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (toRestore[relation.targetModel.name]?.has(parentId)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const parent = await ctx.knex(relation.targetModel.name).where({ id: parentId }).first();
|
|
474
|
+
if (parent?.deleted) {
|
|
475
|
+
throw new ForbiddenError(
|
|
476
|
+
`Can't restore ${getTechnicalDisplay(model, entity)} because it depends on deleted ${relation.targetModel.name} ${parentId}.`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
465
479
|
}
|
|
466
|
-
|
|
467
|
-
|
|
480
|
+
|
|
481
|
+
await doUpdate(currentModel, currentEntity, normalizedInput, ctx);
|
|
482
|
+
});
|
|
483
|
+
if (ctx.mutationHook) {
|
|
484
|
+
afterHooks.push(async () => {
|
|
485
|
+
await ctx.mutationHook!({
|
|
486
|
+
model: currentModel,
|
|
487
|
+
action: 'restore',
|
|
488
|
+
trigger: currentTrigger,
|
|
489
|
+
when: 'after',
|
|
490
|
+
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
491
|
+
ctx,
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for (const {
|
|
497
|
+
targetModel: descendantModel,
|
|
498
|
+
field: { foreignKey },
|
|
499
|
+
} of currentModel.reverseRelations
|
|
500
|
+
.filter((reverseRelation) => !reverseRelation.field.inherited)
|
|
501
|
+
.filter(({ targetModel: { deletable } }) => deletable)) {
|
|
502
|
+
const query = ctx.knex(descendantModel.name).where({ [foreignKey]: currentEntity.id, deleted: true });
|
|
503
|
+
if (currentEntity.deleteRootId) {
|
|
504
|
+
query.where({ deleteRootType: currentEntity.deleteRootType, deleteRootId: currentEntity.deleteRootId });
|
|
505
|
+
} else {
|
|
506
|
+
// Legacy heuristic
|
|
507
|
+
query.where({ deletedAt: currentEntity.deletedAt });
|
|
468
508
|
}
|
|
469
|
-
const
|
|
470
|
-
|
|
509
|
+
const descendantsToRestore = await query;
|
|
510
|
+
applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'RESTORE');
|
|
511
|
+
const restorableDescendants = await query;
|
|
512
|
+
const notRestorableDescendants = descendantsToRestore.filter(
|
|
513
|
+
(descendant) => !restorableDescendants.some((d) => d.id === descendant.id),
|
|
514
|
+
);
|
|
515
|
+
if (notRestorableDescendants.length) {
|
|
471
516
|
throw new ForbiddenError(
|
|
472
|
-
|
|
517
|
+
`${getTechnicalDisplay(currentModel, currentEntity)} depends on ${descendantModel.labelPlural} which you have no permissions to restore.`,
|
|
473
518
|
);
|
|
474
519
|
}
|
|
520
|
+
for (const descendant of descendantsToRestore) {
|
|
521
|
+
await restoreCascade(descendantModel, descendant, 'cascade');
|
|
522
|
+
}
|
|
475
523
|
}
|
|
524
|
+
};
|
|
476
525
|
|
|
477
|
-
|
|
478
|
-
});
|
|
479
|
-
if (ctx.mutationHook) {
|
|
480
|
-
afterHooks.push(async () => {
|
|
481
|
-
await ctx.mutationHook!({
|
|
482
|
-
model: currentModel,
|
|
483
|
-
action: 'restore',
|
|
484
|
-
trigger: currentTrigger,
|
|
485
|
-
when: 'after',
|
|
486
|
-
data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
|
|
487
|
-
ctx,
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
}
|
|
526
|
+
await restoreCascade(rootModel, entity, trigger);
|
|
491
527
|
|
|
492
|
-
for (const {
|
|
493
|
-
|
|
494
|
-
field: { foreignKey },
|
|
495
|
-
} of currentModel.reverseRelations
|
|
496
|
-
.filter((reverseRelation) => !reverseRelation.field.inherited)
|
|
497
|
-
.filter(({ targetModel: { deletable } }) => deletable)) {
|
|
498
|
-
const query = ctx.knex(descendantModel.name).where({ [foreignKey]: currentEntity.id, deleted: true });
|
|
499
|
-
if (currentEntity.deleteRootId) {
|
|
500
|
-
query.where({ deleteRootType: currentEntity.deleteRootType, deleteRootId: currentEntity.deleteRootId });
|
|
501
|
-
} else {
|
|
502
|
-
// Legacy heuristic
|
|
503
|
-
query.where({ deletedAt: currentEntity.deletedAt });
|
|
504
|
-
}
|
|
505
|
-
const descendantsToRestore = await query;
|
|
506
|
-
applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'RESTORE');
|
|
507
|
-
const restorableDescendants = await query;
|
|
508
|
-
const notRestorableDescendants = descendantsToRestore.filter(
|
|
509
|
-
(descendant) => !restorableDescendants.some((d) => d.id === descendant.id),
|
|
510
|
-
);
|
|
511
|
-
if (notRestorableDescendants.length) {
|
|
512
|
-
throw new ForbiddenError(
|
|
513
|
-
`${getTechnicalDisplay(currentModel, currentEntity)} depends on ${descendantModel.labelPlural} which you have no permissions to restore.`,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
for (const descendant of descendantsToRestore) {
|
|
517
|
-
await restoreCascade(descendantModel, descendant, 'cascade');
|
|
518
|
-
}
|
|
528
|
+
for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
|
|
529
|
+
await callback();
|
|
519
530
|
}
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
await restoreCascade(rootModel, entity, trigger);
|
|
523
|
-
|
|
524
|
-
for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
|
|
525
|
-
await callback();
|
|
526
|
-
}
|
|
527
|
-
};
|
|
531
|
+
});
|
|
528
532
|
|
|
529
533
|
export const createRevision = async (model: EntityModel, data: Entity, ctx: MutationContext) => {
|
|
530
534
|
if (model.updatable) {
|