@smartive/graphql-magic 19.1.3-next.2 → 19.2.0-next.2

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.
@@ -1,9 +1,9 @@
1
1
  import { GraphQLResolveInfo } from 'graphql';
2
2
  import { v4 as uuid } from 'uuid';
3
- import { Context, FullContext } from '../context';
3
+ import { Context } from '../context';
4
4
  import { ForbiddenError, GraphQLError } from '../errors';
5
5
  import { EntityField, EntityModel } from '../models/models';
6
- import { Entity } from '../models/mutation-hook';
6
+ import { Entity, MutationContext, Trigger } from '../models/mutation-hook';
7
7
  import { get, isPrimitive, it, typeToField } from '../models/utils';
8
8
  import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
9
9
  import { anyDateToLuxon } from '../utils';
@@ -16,23 +16,53 @@ export const mutationResolver = async (_parent: any, args: any, partialCtx: Cont
16
16
  const ctx = { ...partialCtx, knex, info, aliases: new AliasGenerator() };
17
17
  const model = ctx.models.getModel(modelName, 'entity');
18
18
  switch (mutation) {
19
- case 'create':
20
- return await create(model, args, ctx);
21
- case 'update':
22
- return await update(model, args, ctx);
23
- case 'delete':
24
- return await del(model, args, ctx);
25
- case 'restore':
26
- return await restore(model, args, ctx);
19
+ case 'create': {
20
+ const id = await createEntity(model, args.data, ctx, 'mutation');
21
+
22
+ return await resolve(ctx, id);
23
+ }
24
+ case 'update': {
25
+ const id = args.where.id;
26
+
27
+ await updateEntity(model, id, args.data, ctx);
28
+
29
+ return await resolve(ctx, id);
30
+ }
31
+ case 'delete': {
32
+ const id = args.where.id;
33
+
34
+ await deleteEntity(model, id, args.dryRun, model.rootModel.name, id, ctx, 'mutation');
35
+
36
+ return id;
37
+ }
38
+ case 'restore': {
39
+ const id = args.where.id;
40
+
41
+ await restoreEntity(model, id, ctx, 'mutation');
42
+
43
+ return id;
44
+ }
27
45
  }
28
46
  });
29
47
  };
30
48
 
31
- const create = async (model: EntityModel, { data: input }: { data: any }, ctx: FullContext) => {
49
+ export const createEntity = async (
50
+ model: EntityModel,
51
+ input: Entity,
52
+ ctx: MutationContext,
53
+ trigger: Trigger = 'direct-call',
54
+ ) => {
32
55
  const normalizedInput = { ...input };
33
- normalizedInput.id = uuid();
34
- normalizedInput.createdAt = ctx.now;
35
- normalizedInput.createdById = ctx.user?.id;
56
+ if (!normalizedInput.id) {
57
+ normalizedInput.id = uuid();
58
+ }
59
+ const id = normalizedInput.id as string;
60
+ if (!normalizedInput.createdAt) {
61
+ normalizedInput.createdAt = ctx.now;
62
+ }
63
+ if (!normalizedInput.createdById) {
64
+ normalizedInput.createdById = ctx.user?.id;
65
+ }
36
66
  if (model.parent) {
37
67
  normalizedInput.type = model.name;
38
68
  }
@@ -42,10 +72,11 @@ const create = async (model: EntityModel, { data: input }: { data: any }, ctx: F
42
72
  await ctx.handleUploads?.(normalizedInput);
43
73
 
44
74
  const data = { prev: {}, input, normalizedInput, next: normalizedInput };
45
- await ctx.mutationHook?.({ model, action: 'create', trigger: 'mutation', when: 'before', data, ctx });
75
+ await ctx.mutationHook?.({ model, action: 'create', trigger, when: 'before', data, ctx });
76
+
46
77
  if (model.parent) {
47
78
  const rootInput = {};
48
- const childInput = { id: normalizedInput.id };
79
+ const childInput = { id };
49
80
  for (const field of model.fields) {
50
81
  const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
51
82
  if (columnName in normalizedInput) {
@@ -62,25 +93,39 @@ const create = async (model: EntityModel, { data: input }: { data: any }, ctx: F
62
93
  await ctx.knex(model.name).insert(normalizedInput);
63
94
  }
64
95
  await createRevision(model, normalizedInput, ctx);
65
- await ctx.mutationHook?.({ model, action: 'create', trigger: 'mutation', when: 'after', data, ctx });
96
+ await ctx.mutationHook?.({ model, action: 'create', trigger, when: 'after', data, ctx });
66
97
 
67
- return await resolve(ctx, normalizedInput.id);
98
+ return normalizedInput.id as string;
68
99
  };
69
100
 
70
- const update = async (model: EntityModel, { where, data: input }: { where: any; data: any }, ctx: FullContext) => {
71
- if (Object.keys(where).length === 0) {
72
- throw new Error(`No ${model.name} specified.`);
101
+ export const updateEntities = async (
102
+ model: EntityModel,
103
+ where: Record<string, unknown>,
104
+ updateFields: Entity,
105
+ ctx: MutationContext,
106
+ ) => {
107
+ const entities = await ctx.knex(model.name).where(where).select('id');
108
+ for (const entity of entities) {
109
+ await updateEntity(model, entity.id, updateFields, ctx);
73
110
  }
111
+ };
74
112
 
113
+ export const updateEntity = async (
114
+ model: EntityModel,
115
+ id: string,
116
+ input: Entity,
117
+ ctx: MutationContext,
118
+ trigger: Trigger = 'direct-call',
119
+ ) => {
75
120
  const normalizedInput = { ...input };
76
121
 
77
122
  sanitize(ctx, model, normalizedInput);
78
123
 
79
- const prev = await getEntityToMutate(ctx, model, where, 'UPDATE');
124
+ const currentEntity = await getEntityToMutate(ctx, model, { id }, 'UPDATE');
80
125
 
81
126
  // Remove data that wouldn't mutate given that it's irrelevant for permissions
82
127
  for (const key of Object.keys(normalizedInput)) {
83
- if (normalizedInput[key] === prev[key]) {
128
+ if (normalizedInput[key] === currentEntity[key]) {
84
129
  delete normalizedInput[key];
85
130
  }
86
131
  }
@@ -89,49 +134,52 @@ const update = async (model: EntityModel, { where, data: input }: { where: any;
89
134
  await checkCanWrite(ctx, model, normalizedInput, 'UPDATE');
90
135
  await ctx.handleUploads?.(normalizedInput);
91
136
 
92
- const next = { ...prev, ...normalizedInput };
93
- const data = { prev, input, normalizedInput, next };
94
- await ctx.mutationHook?.({ model, action: 'update', trigger: 'mutation', when: 'before', data, ctx });
95
-
96
- if (model.parent) {
97
- const rootInput = {};
98
- const childInput = {};
99
- for (const field of model.fields) {
100
- const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
101
- if (columnName in normalizedInput) {
102
- if (field.inherited) {
103
- rootInput[columnName] = normalizedInput[columnName];
104
- } else {
105
- childInput[columnName] = normalizedInput[columnName];
106
- }
107
- }
108
- }
109
- if (Object.keys(rootInput).length) {
110
- await ctx.knex(model.parent).where({ id: prev.id }).update(rootInput);
111
- }
112
- if (Object.keys(childInput).length) {
113
- await ctx.knex(model.name).where({ id: prev.id }).update(childInput);
114
- }
115
- } else {
116
- await ctx.knex(model.name).where({ id: prev.id }).update(normalizedInput);
117
- }
118
-
119
- await createRevision(model, next, ctx);
120
- await ctx.mutationHook?.({ model, action: 'update', trigger: 'mutation', when: 'after', data, ctx });
137
+ await ctx.mutationHook?.({
138
+ model,
139
+ action: 'update',
140
+ trigger,
141
+ when: 'before',
142
+ data: { prev: currentEntity, input, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
143
+ ctx,
144
+ });
145
+ await doUpdate(model, currentEntity, normalizedInput, ctx);
146
+ await ctx.mutationHook?.({
147
+ model,
148
+ action: 'update',
149
+ trigger,
150
+ when: 'after',
151
+ data: { prev: currentEntity, input, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
152
+ ctx,
153
+ });
121
154
  }
122
-
123
- return await resolve(ctx);
124
155
  };
125
156
 
126
157
  type Callbacks = (() => Promise<void>)[];
127
158
 
128
- const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun: boolean }, ctx: FullContext) => {
129
- if (Object.keys(where).length === 0) {
130
- throw new Error(`No ${model.name} specified.`);
159
+ export const deleteEntities = async (
160
+ model: EntityModel,
161
+ where: Record<string, unknown>,
162
+ deleteRootType: string | undefined,
163
+ deleteRootId: string | undefined,
164
+ ctx: MutationContext,
165
+ ) => {
166
+ const entities = await ctx.knex(model.name).where(where).select('id');
167
+ for (const entity of entities) {
168
+ await deleteEntity(model, entity.id, false, deleteRootType, deleteRootId, ctx);
131
169
  }
170
+ };
132
171
 
172
+ export const deleteEntity = async (
173
+ model: EntityModel,
174
+ id: string,
175
+ dryRun: boolean,
176
+ deleteRootType: string | undefined = model.rootModel.name,
177
+ deleteRootId: string | undefined = id,
178
+ ctx: MutationContext,
179
+ trigger: Trigger = 'direct-call',
180
+ ) => {
133
181
  const rootModel = model.rootModel;
134
- const entity = await getEntityToMutate(ctx, rootModel, where, 'DELETE');
182
+ const entity = await getEntityToMutate(ctx, rootModel, { id }, 'DELETE');
135
183
 
136
184
  if (entity.deleted) {
137
185
  throw new ForbiddenError(`${getTechnicalDisplay(model, entity)} is already deleted.`);
@@ -164,7 +212,7 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
164
212
  const afterHooks: Callbacks = [];
165
213
 
166
214
  const mutationHook = ctx.mutationHook;
167
- const deleteCascade = async (currentModel: EntityModel, currentEntity: Entity) => {
215
+ const deleteCascade = async (currentModel: EntityModel, currentEntity: Entity, currentTrigger: Trigger) => {
168
216
  if (!(currentModel.name in toDelete)) {
169
217
  toDelete[currentModel.name] = {};
170
218
  }
@@ -172,42 +220,38 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
172
220
  return;
173
221
  }
174
222
  toDelete[currentModel.name][currentEntity.id as string] = await fetchDisplay(ctx.knex, currentModel, currentEntity);
175
- const trigger = currentModel.name === rootModel.name && currentEntity.id === entity.id ? 'mutation' : 'cascade';
176
223
 
177
224
  if (!dryRun) {
178
225
  const normalizedInput = {
179
226
  deleted: true,
180
227
  deletedAt: ctx.now,
181
228
  deletedById: ctx.user?.id,
182
- deleteRootType: rootModel.name,
183
- deleteRootId: entity.id,
229
+ deleteRootType,
230
+ deleteRootId,
184
231
  };
185
- const next = { ...currentEntity, ...normalizedInput };
186
- const data = { prev: currentEntity, input: {}, normalizedInput, next };
187
232
  if (mutationHook) {
188
233
  beforeHooks.push(async () => {
189
234
  await mutationHook({
190
235
  model: currentModel,
191
236
  action: 'delete',
192
- trigger,
237
+ trigger: currentTrigger,
193
238
  when: 'before',
194
- data,
239
+ data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
195
240
  ctx,
196
241
  });
197
242
  });
198
243
  }
199
244
  mutations.push(async () => {
200
- await ctx.knex(currentModel.name).where({ id: currentEntity.id }).update(normalizedInput);
201
- await createRevision(currentModel, next, ctx);
245
+ await doUpdate(currentModel, currentEntity, normalizedInput, ctx);
202
246
  });
203
247
  if (mutationHook) {
204
248
  afterHooks.push(async () => {
205
249
  await mutationHook({
206
250
  model: currentModel,
207
251
  action: 'delete',
208
- trigger,
252
+ trigger: currentTrigger,
209
253
  when: 'after',
210
- data,
254
+ data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
211
255
  ctx,
212
256
  });
213
257
  });
@@ -237,8 +281,6 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
237
281
  toUnlink[descendantModel.name][descendant.id].fields.push(name);
238
282
  } else {
239
283
  const normalizedInput = { [`${name}Id`]: null };
240
- const next = { ...descendant, ...normalizedInput };
241
- const data = { prev: descendant, input: {}, normalizedInput, next };
242
284
  if (mutationHook) {
243
285
  beforeHooks.push(async () => {
244
286
  await mutationHook({
@@ -246,14 +288,13 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
246
288
  action: 'update',
247
289
  trigger: 'set-null',
248
290
  when: 'before',
249
- data,
291
+ data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
250
292
  ctx,
251
293
  });
252
294
  });
253
295
  }
254
296
  mutations.push(async () => {
255
- await ctx.knex(descendantModel.name).where({ id: descendant.id }).update(normalizedInput);
256
- await createRevision(descendantModel, next, ctx);
297
+ await doUpdate(descendantModel, descendant, normalizedInput, ctx);
257
298
  });
258
299
  if (mutationHook) {
259
300
  afterHooks.push(async () => {
@@ -262,7 +303,7 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
262
303
  action: 'update',
263
304
  trigger: 'set-null',
264
305
  when: 'after',
265
- data,
306
+ data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
266
307
  ctx,
267
308
  });
268
309
  });
@@ -308,7 +349,7 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
308
349
  );
309
350
  }
310
351
  for (const descendant of descendants) {
311
- await deleteCascade(descendantModel, descendant);
352
+ await deleteCascade(descendantModel, descendant, 'cascade');
312
353
  }
313
354
  break;
314
355
  }
@@ -317,7 +358,7 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
317
358
  }
318
359
  };
319
360
 
320
- await deleteCascade(rootModel, entity);
361
+ await deleteCascade(rootModel, entity, trigger);
321
362
 
322
363
  for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
323
364
  await callback();
@@ -331,18 +372,17 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
331
372
  restricted,
332
373
  });
333
374
  }
334
-
335
- return entity.id;
336
375
  };
337
376
 
338
- const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullContext) => {
339
- if (Object.keys(where).length === 0) {
340
- throw new Error(`No ${model.name} specified.`);
341
- }
342
-
377
+ export const restoreEntity = async (
378
+ model: EntityModel,
379
+ id: string,
380
+ ctx: MutationContext,
381
+ trigger: Trigger = 'direct-call',
382
+ ) => {
343
383
  const rootModel = model.rootModel;
344
384
 
345
- const entity = await getEntityToMutate(ctx, rootModel, where, 'RESTORE');
385
+ const entity = await getEntityToMutate(ctx, rootModel, { id }, 'RESTORE');
346
386
 
347
387
  if (!entity.deleted) {
348
388
  throw new ForbiddenError(`${getTechnicalDisplay(model, entity)} is not deleted.`);
@@ -362,9 +402,9 @@ const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullC
362
402
  const mutations: Callbacks = [];
363
403
  const afterHooks: Callbacks = [];
364
404
 
365
- const restoreCascade = async (currentModel: EntityModel, currentEntity: Entity) => {
405
+ const restoreCascade = async (currentModel: EntityModel, currentEntity: Entity, currentTrigger: Trigger) => {
366
406
  if (entity.deleteRootId) {
367
- if (!(currentEntity.deleteRootType === currentModel.name && currentEntity.deleteRootId === entity.id)) {
407
+ if (!(currentEntity.deleteRootType === model.name && currentEntity.deleteRootId === entity.id)) {
368
408
  return;
369
409
  }
370
410
 
@@ -381,7 +421,7 @@ const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullC
381
421
  toRestore[currentModel.name].add(currentEntity.id as string);
382
422
 
383
423
  for (const relation of currentModel.relations) {
384
- const parentId = entity[relation.field.foreignKey];
424
+ const parentId = currentEntity[relation.field.foreignKey] as string | undefined;
385
425
  if (!parentId) {
386
426
  continue;
387
427
  }
@@ -406,16 +446,29 @@ const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullC
406
446
  const data = { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } };
407
447
  if (ctx.mutationHook) {
408
448
  beforeHooks.push(async () => {
409
- await ctx.mutationHook!({ model: currentModel, action: 'restore', trigger: 'mutation', when: 'before', data, ctx });
449
+ await ctx.mutationHook!({
450
+ model: currentModel,
451
+ action: 'restore',
452
+ trigger: currentTrigger,
453
+ when: 'before',
454
+ data,
455
+ ctx,
456
+ });
410
457
  });
411
458
  }
412
459
  mutations.push(async () => {
413
- await ctx.knex(currentModel.name).where({ id: currentEntity.id }).update(normalizedInput);
414
- await createRevision(currentModel, { ...currentEntity, deleted: false }, ctx);
460
+ await doUpdate(currentModel, currentEntity, normalizedInput, ctx);
415
461
  });
416
462
  if (ctx.mutationHook) {
417
463
  afterHooks.push(async () => {
418
- await ctx.mutationHook!({ model: currentModel, action: 'restore', trigger: 'mutation', when: 'after', data, ctx });
464
+ await ctx.mutationHook!({
465
+ model: currentModel,
466
+ action: 'restore',
467
+ trigger: currentTrigger,
468
+ when: 'after',
469
+ data,
470
+ ctx,
471
+ });
419
472
  });
420
473
  }
421
474
 
@@ -438,21 +491,19 @@ const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullC
438
491
  );
439
492
  }
440
493
  for (const descendant of deletedDescendants) {
441
- await restoreCascade(descendantModel, descendant);
494
+ await restoreCascade(descendantModel, descendant, 'cascade');
442
495
  }
443
496
  }
444
497
  };
445
498
 
446
- await restoreCascade(rootModel, entity);
499
+ await restoreCascade(rootModel, entity, trigger);
447
500
 
448
501
  for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
449
502
  await callback();
450
503
  }
451
-
452
- return entity.id;
453
504
  };
454
505
 
455
- export const createRevision = async (model: EntityModel, data: Entity, ctx: Pick<Context, 'knex' | 'now' | 'user'>) => {
506
+ export const createRevision = async (model: EntityModel, data: Entity, ctx: MutationContext) => {
456
507
  if (model.updatable) {
457
508
  const revisionId = uuid();
458
509
  const rootRevisionData: Entity = {
@@ -491,10 +542,14 @@ export const createRevision = async (model: EntityModel, data: Entity, ctx: Pick
491
542
  }
492
543
  };
493
544
 
494
- const sanitize = (ctx: FullContext, model: EntityModel, data: Entity) => {
545
+ const sanitize = (ctx: MutationContext, model: EntityModel, data: Entity) => {
495
546
  if (model.updatable) {
496
- data.updatedAt = ctx.now;
497
- data.updatedById = ctx.user?.id;
547
+ if (!data.updatedAt) {
548
+ data.updatedAt = ctx.now;
549
+ }
550
+ if (!data.updatedById) {
551
+ data.updatedById = ctx.user?.id;
552
+ }
498
553
  }
499
554
 
500
555
  for (const key of Object.keys(data)) {
@@ -526,3 +581,37 @@ const isEndOfDay = (field: EntityField) =>
526
581
 
527
582
  const isEndOfMonth = (field: EntityField) =>
528
583
  isPrimitive(field) && field.type === 'DateTime' && field?.endOfMonth === true && field?.dateTimeType === 'year_and_month';
584
+
585
+ const doUpdate = async (model: EntityModel, currentEntity: Entity, update: Entity, ctx: MutationContext) => {
586
+ if (model.updatable) {
587
+ if (!update.updatedAt) {
588
+ update.updatedAt = ctx.now;
589
+ }
590
+ if (!update.updatedById) {
591
+ update.updatedById = ctx.user?.id;
592
+ }
593
+ }
594
+ if (model.parent) {
595
+ const rootInput = {};
596
+ const childInput = {};
597
+ for (const field of model.fields) {
598
+ const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
599
+ if (columnName in update) {
600
+ if (field.inherited) {
601
+ rootInput[columnName] = update[columnName];
602
+ } else {
603
+ childInput[columnName] = update[columnName];
604
+ }
605
+ }
606
+ }
607
+ if (Object.keys(rootInput).length) {
608
+ await ctx.knex(model.parent).where({ id: currentEntity.id }).update(rootInput);
609
+ }
610
+ if (Object.keys(childInput).length) {
611
+ await ctx.knex(model.name).where({ id: currentEntity.id }).update(childInput);
612
+ }
613
+ } else {
614
+ await ctx.knex(model.name).where({ id: currentEntity.id }).update(update);
615
+ }
616
+ await createRevision(model, { ...currentEntity, ...update }, ctx);
617
+ };