@smartive/graphql-magic 19.2.0-next.1 → 19.2.0-next.3

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.
@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
3
3
  import { Context } from '../context';
4
4
  import { ForbiddenError, GraphQLError } from '../errors';
5
5
  import { EntityField, EntityModel } from '../models/models';
6
- import { Entity, MutationContext } 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';
@@ -14,34 +14,44 @@ export const mutationResolver = async (_parent: any, args: any, partialCtx: Cont
14
14
  return await partialCtx.knex.transaction(async (knex) => {
15
15
  const [, mutation, modelName] = it(info.fieldName.match(/^(create|update|delete|restore)(.+)$/));
16
16
  const ctx = { ...partialCtx, knex, info, aliases: new AliasGenerator() };
17
- const model = ctx.models.getModel(modelName, 'entity');
18
17
  switch (mutation) {
19
18
  case 'create': {
20
- const id = await createEntity(model, args.data, ctx);
19
+ const id = await createEntity(modelName, args.data, ctx, 'mutation');
21
20
 
22
21
  return await resolve(ctx, id);
23
22
  }
24
23
  case 'update': {
25
24
  const id = args.where.id;
26
25
 
27
- await updateEntity(model, id, args.data, ctx);
26
+ await updateEntity(modelName, id, args.data, ctx);
28
27
 
29
28
  return await resolve(ctx, id);
30
29
  }
31
30
  case 'delete': {
32
31
  const id = args.where.id;
33
32
 
34
- await deleteEntity(model, id, args.dryRun, model.rootModel.name, id, ctx);
33
+ await deleteEntity(modelName, id, args.dryRun, undefined, undefined, ctx, 'mutation');
35
34
 
36
- return await resolve(ctx, id);
35
+ return id;
36
+ }
37
+ case 'restore': {
38
+ const id = args.where.id;
39
+
40
+ await restoreEntity(modelName, id, ctx, 'mutation');
41
+
42
+ return id;
37
43
  }
38
- case 'restore':
39
- return await restoreEntity(model, args.where.id, ctx);
40
44
  }
41
45
  });
42
46
  };
43
47
 
44
- export const createEntity = async (model: EntityModel, input: Entity, ctx: MutationContext) => {
48
+ export const createEntity = async (
49
+ modelName: string,
50
+ input: Entity,
51
+ ctx: MutationContext,
52
+ trigger: Trigger = 'direct-call',
53
+ ) => {
54
+ const model = ctx.models.getModel(modelName, 'entity');
45
55
  const normalizedInput = { ...input };
46
56
  if (!normalizedInput.id) {
47
57
  normalizedInput.id = uuid();
@@ -62,56 +72,61 @@ export const createEntity = async (model: EntityModel, input: Entity, ctx: Mutat
62
72
  await ctx.handleUploads?.(normalizedInput);
63
73
 
64
74
  const data = { prev: {}, input, normalizedInput, next: normalizedInput };
65
- await ctx.mutationHook?.({ model, action: 'create', trigger: 'mutation', when: 'before', data, ctx });
66
-
67
- await createEntity(model, normalizedInput, ctx);
75
+ await ctx.mutationHook?.({ model, action: 'create', trigger, when: 'before', data, ctx });
68
76
 
69
77
  if (model.parent) {
70
78
  const rootInput = {};
71
79
  const childInput = { id };
72
80
  for (const field of model.fields) {
73
81
  const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
74
- if (columnName in data) {
82
+ if (columnName in normalizedInput) {
75
83
  if (field.inherited) {
76
- rootInput[columnName] = data[columnName];
84
+ rootInput[columnName] = normalizedInput[columnName];
77
85
  } else {
78
- childInput[columnName] = data[columnName];
86
+ childInput[columnName] = normalizedInput[columnName];
79
87
  }
80
88
  }
81
89
  }
82
90
  await ctx.knex(model.parent).insert(rootInput);
83
91
  await ctx.knex(model.name).insert(childInput);
84
92
  } else {
85
- await ctx.knex(model.name).insert(data);
93
+ await ctx.knex(model.name).insert(normalizedInput);
86
94
  }
87
- await createRevision(model, data, ctx);
88
- await ctx.mutationHook?.({ model, action: 'create', trigger: 'mutation', when: 'after', data, ctx });
95
+ await createRevision(model, normalizedInput, ctx);
96
+ await ctx.mutationHook?.({ model, action: 'create', trigger, when: 'after', data, ctx });
89
97
 
90
98
  return normalizedInput.id as string;
91
99
  };
92
100
 
93
101
  export const updateEntities = async (
94
- model: EntityModel,
102
+ modelName: string,
95
103
  where: Record<string, unknown>,
96
104
  updateFields: Entity,
97
105
  ctx: MutationContext,
98
106
  ) => {
99
- const entities = await ctx.knex(model.name).where(where).select('id');
107
+ const entities = await ctx.knex(modelName).where(where).select('id');
100
108
  for (const entity of entities) {
101
- await updateEntity(model, entity.id, updateFields, ctx);
109
+ await updateEntity(modelName, entity.id, updateFields, ctx);
102
110
  }
103
111
  };
104
112
 
105
- export const updateEntity = async (model: EntityModel, id: string, input: Entity, ctx: MutationContext) => {
113
+ export const updateEntity = async (
114
+ modelName: string,
115
+ id: string,
116
+ input: Entity,
117
+ ctx: MutationContext,
118
+ trigger: Trigger = 'direct-call',
119
+ ) => {
120
+ const model = ctx.models.getModel(modelName, 'entity');
106
121
  const normalizedInput = { ...input };
107
122
 
108
123
  sanitize(ctx, model, normalizedInput);
109
124
 
110
- const prev = await getEntityToMutate(ctx, model, { id }, 'UPDATE');
125
+ const currentEntity = await getEntityToMutate(ctx, model, { id }, 'UPDATE');
111
126
 
112
127
  // Remove data that wouldn't mutate given that it's irrelevant for permissions
113
128
  for (const key of Object.keys(normalizedInput)) {
114
- if (normalizedInput[key] === prev[key]) {
129
+ if (normalizedInput[key] === currentEntity[key]) {
115
130
  delete normalizedInput[key];
116
131
  }
117
132
  }
@@ -120,39 +135,55 @@ export const updateEntity = async (model: EntityModel, id: string, input: Entity
120
135
  await checkCanWrite(ctx, model, normalizedInput, 'UPDATE');
121
136
  await ctx.handleUploads?.(normalizedInput);
122
137
 
123
- const next = { ...prev, ...normalizedInput };
124
- const data = { prev, input, normalizedInput, next };
125
- await ctx.mutationHook?.({ model, action: 'update', trigger: 'mutation', when: 'before', data, ctx });
126
-
127
- await doUpdate(model, normalizedInput, next, ctx);
128
- await ctx.mutationHook?.({ model, action: 'update', trigger: 'mutation', when: 'after', data, ctx });
138
+ await ctx.mutationHook?.({
139
+ model,
140
+ action: 'update',
141
+ trigger,
142
+ when: 'before',
143
+ data: { prev: currentEntity, input, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
144
+ ctx,
145
+ });
146
+ await doUpdate(model, currentEntity, normalizedInput, ctx);
147
+ await ctx.mutationHook?.({
148
+ model,
149
+ action: 'update',
150
+ trigger,
151
+ when: 'after',
152
+ data: { prev: currentEntity, input, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
153
+ ctx,
154
+ });
129
155
  }
130
156
  };
131
157
 
132
158
  type Callbacks = (() => Promise<void>)[];
133
159
 
134
160
  export const deleteEntities = async (
135
- model: EntityModel,
161
+ modelName: string,
136
162
  where: Record<string, unknown>,
137
- deleteRootType: string,
138
- deleteRootId: string,
163
+ deleteRootType: string | undefined,
164
+ deleteRootId: string | undefined,
139
165
  ctx: MutationContext,
140
166
  ) => {
141
- const entities = await ctx.knex(model.name).where(where).select('id');
167
+ const entities = await ctx.knex(modelName).where(where).select('id');
142
168
  for (const entity of entities) {
143
- await deleteEntity(model, entity.id, false, deleteRootType, deleteRootId, ctx);
169
+ await deleteEntity(modelName, entity.id, false, deleteRootType, deleteRootId, ctx);
144
170
  }
145
171
  };
146
172
 
147
173
  export const deleteEntity = async (
148
- model: EntityModel,
174
+ modelName: string,
149
175
  id: string,
150
176
  dryRun: boolean,
151
- deleteRootType: string,
152
- deleteRootId: string,
177
+ deleteRootType: string | undefined,
178
+ deleteRootId: string | undefined = id,
153
179
  ctx: MutationContext,
180
+ trigger: Trigger = 'direct-call',
154
181
  ) => {
182
+ const model = ctx.models.getModel(modelName, 'entity');
155
183
  const rootModel = model.rootModel;
184
+ if (!deleteRootType) {
185
+ deleteRootType = rootModel.name;
186
+ }
156
187
  const entity = await getEntityToMutate(ctx, rootModel, { id }, 'DELETE');
157
188
 
158
189
  if (entity.deleted) {
@@ -186,7 +217,7 @@ export const deleteEntity = async (
186
217
  const afterHooks: Callbacks = [];
187
218
 
188
219
  const mutationHook = ctx.mutationHook;
189
- const deleteCascade = async (currentModel: EntityModel, currentEntity: Entity) => {
220
+ const deleteCascade = async (currentModel: EntityModel, currentEntity: Entity, currentTrigger: Trigger) => {
190
221
  if (!(currentModel.name in toDelete)) {
191
222
  toDelete[currentModel.name] = {};
192
223
  }
@@ -194,7 +225,6 @@ export const deleteEntity = async (
194
225
  return;
195
226
  }
196
227
  toDelete[currentModel.name][currentEntity.id as string] = await fetchDisplay(ctx.knex, currentModel, currentEntity);
197
- const trigger = currentModel.name === rootModel.name && currentEntity.id === entity.id ? 'mutation' : 'cascade';
198
228
 
199
229
  if (!dryRun) {
200
230
  const normalizedInput = {
@@ -204,31 +234,29 @@ export const deleteEntity = async (
204
234
  deleteRootType,
205
235
  deleteRootId,
206
236
  };
207
- const next = { ...currentEntity, ...normalizedInput };
208
- const data = { prev: currentEntity, input: {}, normalizedInput, next };
209
237
  if (mutationHook) {
210
238
  beforeHooks.push(async () => {
211
239
  await mutationHook({
212
240
  model: currentModel,
213
241
  action: 'delete',
214
- trigger,
242
+ trigger: currentTrigger,
215
243
  when: 'before',
216
- data,
244
+ data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
217
245
  ctx,
218
246
  });
219
247
  });
220
248
  }
221
249
  mutations.push(async () => {
222
- await doUpdate(currentModel, normalizedInput, next, ctx);
250
+ await doUpdate(currentModel, currentEntity, normalizedInput, ctx);
223
251
  });
224
252
  if (mutationHook) {
225
253
  afterHooks.push(async () => {
226
254
  await mutationHook({
227
255
  model: currentModel,
228
256
  action: 'delete',
229
- trigger,
257
+ trigger: currentTrigger,
230
258
  when: 'after',
231
- data,
259
+ data: { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } },
232
260
  ctx,
233
261
  });
234
262
  });
@@ -258,8 +286,6 @@ export const deleteEntity = async (
258
286
  toUnlink[descendantModel.name][descendant.id].fields.push(name);
259
287
  } else {
260
288
  const normalizedInput = { [`${name}Id`]: null };
261
- const next = { ...descendant, ...normalizedInput };
262
- const data = { prev: descendant, input: {}, normalizedInput, next };
263
289
  if (mutationHook) {
264
290
  beforeHooks.push(async () => {
265
291
  await mutationHook({
@@ -267,13 +293,13 @@ export const deleteEntity = async (
267
293
  action: 'update',
268
294
  trigger: 'set-null',
269
295
  when: 'before',
270
- data,
296
+ data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
271
297
  ctx,
272
298
  });
273
299
  });
274
300
  }
275
301
  mutations.push(async () => {
276
- await doUpdate(descendantModel, normalizedInput, next, ctx);
302
+ await doUpdate(descendantModel, descendant, normalizedInput, ctx);
277
303
  });
278
304
  if (mutationHook) {
279
305
  afterHooks.push(async () => {
@@ -282,7 +308,7 @@ export const deleteEntity = async (
282
308
  action: 'update',
283
309
  trigger: 'set-null',
284
310
  when: 'after',
285
- data,
311
+ data: { prev: descendant, input: {}, normalizedInput, next: { ...descendant, ...normalizedInput } },
286
312
  ctx,
287
313
  });
288
314
  });
@@ -328,7 +354,7 @@ export const deleteEntity = async (
328
354
  );
329
355
  }
330
356
  for (const descendant of descendants) {
331
- await deleteCascade(descendantModel, descendant);
357
+ await deleteCascade(descendantModel, descendant, 'cascade');
332
358
  }
333
359
  break;
334
360
  }
@@ -337,7 +363,7 @@ export const deleteEntity = async (
337
363
  }
338
364
  };
339
365
 
340
- await deleteCascade(rootModel, entity);
366
+ await deleteCascade(rootModel, entity, trigger);
341
367
 
342
368
  for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
343
369
  await callback();
@@ -351,11 +377,15 @@ export const deleteEntity = async (
351
377
  restricted,
352
378
  });
353
379
  }
354
-
355
- return entity.id;
356
380
  };
357
381
 
358
- const restoreEntity = async (model: EntityModel, id: string, ctx: MutationContext) => {
382
+ export const restoreEntity = async (
383
+ modelName: string,
384
+ id: string,
385
+ ctx: MutationContext,
386
+ trigger: Trigger = 'direct-call',
387
+ ) => {
388
+ const model = ctx.models.getModel(modelName, 'entity');
359
389
  const rootModel = model.rootModel;
360
390
 
361
391
  const entity = await getEntityToMutate(ctx, rootModel, { id }, 'RESTORE');
@@ -378,7 +408,7 @@ const restoreEntity = async (model: EntityModel, id: string, ctx: MutationContex
378
408
  const mutations: Callbacks = [];
379
409
  const afterHooks: Callbacks = [];
380
410
 
381
- const restoreCascade = async (currentModel: EntityModel, currentEntity: Entity) => {
411
+ const restoreCascade = async (currentModel: EntityModel, currentEntity: Entity, currentTrigger: Trigger) => {
382
412
  if (entity.deleteRootId) {
383
413
  if (!(currentEntity.deleteRootType === model.name && currentEntity.deleteRootId === entity.id)) {
384
414
  return;
@@ -422,15 +452,29 @@ const restoreEntity = async (model: EntityModel, id: string, ctx: MutationContex
422
452
  const data = { prev: currentEntity, input: {}, normalizedInput, next: { ...currentEntity, ...normalizedInput } };
423
453
  if (ctx.mutationHook) {
424
454
  beforeHooks.push(async () => {
425
- await ctx.mutationHook!({ model: currentModel, action: 'restore', trigger: 'mutation', when: 'before', data, ctx });
455
+ await ctx.mutationHook!({
456
+ model: currentModel,
457
+ action: 'restore',
458
+ trigger: currentTrigger,
459
+ when: 'before',
460
+ data,
461
+ ctx,
462
+ });
426
463
  });
427
464
  }
428
465
  mutations.push(async () => {
429
- await doUpdate(currentModel, normalizedInput, data.next, ctx);
466
+ await doUpdate(currentModel, currentEntity, normalizedInput, ctx);
430
467
  });
431
468
  if (ctx.mutationHook) {
432
469
  afterHooks.push(async () => {
433
- await ctx.mutationHook!({ model: currentModel, action: 'restore', trigger: 'mutation', when: 'after', data, ctx });
470
+ await ctx.mutationHook!({
471
+ model: currentModel,
472
+ action: 'restore',
473
+ trigger: currentTrigger,
474
+ when: 'after',
475
+ data,
476
+ ctx,
477
+ });
434
478
  });
435
479
  }
436
480
 
@@ -453,18 +497,16 @@ const restoreEntity = async (model: EntityModel, id: string, ctx: MutationContex
453
497
  );
454
498
  }
455
499
  for (const descendant of deletedDescendants) {
456
- await restoreCascade(descendantModel, descendant);
500
+ await restoreCascade(descendantModel, descendant, 'cascade');
457
501
  }
458
502
  }
459
503
  };
460
504
 
461
- await restoreCascade(rootModel, entity);
505
+ await restoreCascade(rootModel, entity, trigger);
462
506
 
463
507
  for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
464
508
  await callback();
465
509
  }
466
-
467
- return id;
468
510
  };
469
511
 
470
512
  export const createRevision = async (model: EntityModel, data: Entity, ctx: MutationContext) => {
@@ -546,13 +588,13 @@ const isEndOfDay = (field: EntityField) =>
546
588
  const isEndOfMonth = (field: EntityField) =>
547
589
  isPrimitive(field) && field.type === 'DateTime' && field?.endOfMonth === true && field?.dateTimeType === 'year_and_month';
548
590
 
549
- const doUpdate = async (model: EntityModel, updateFields: Entity, allFields: Entity, ctx: MutationContext) => {
591
+ const doUpdate = async (model: EntityModel, currentEntity: Entity, update: Entity, ctx: MutationContext) => {
550
592
  if (model.updatable) {
551
- if (!updateFields.updatedAt) {
552
- updateFields.updatedAt = ctx.now;
593
+ if (!update.updatedAt) {
594
+ update.updatedAt = ctx.now;
553
595
  }
554
- if (!updateFields.updatedById) {
555
- updateFields.updatedById = ctx.user?.id;
596
+ if (!update.updatedById) {
597
+ update.updatedById = ctx.user?.id;
556
598
  }
557
599
  }
558
600
  if (model.parent) {
@@ -560,22 +602,22 @@ const doUpdate = async (model: EntityModel, updateFields: Entity, allFields: Ent
560
602
  const childInput = {};
561
603
  for (const field of model.fields) {
562
604
  const columnName = field.kind === 'relation' ? `${field.name}Id` : field.name;
563
- if (columnName in updateFields) {
605
+ if (columnName in update) {
564
606
  if (field.inherited) {
565
- rootInput[columnName] = updateFields[columnName];
607
+ rootInput[columnName] = update[columnName];
566
608
  } else {
567
- childInput[columnName] = updateFields[columnName];
609
+ childInput[columnName] = update[columnName];
568
610
  }
569
611
  }
570
612
  }
571
613
  if (Object.keys(rootInput).length) {
572
- await ctx.knex(model.parent).where({ id: allFields.id }).update(rootInput);
614
+ await ctx.knex(model.parent).where({ id: currentEntity.id }).update(rootInput);
573
615
  }
574
616
  if (Object.keys(childInput).length) {
575
- await ctx.knex(model.name).where({ id: allFields.id }).update(childInput);
617
+ await ctx.knex(model.name).where({ id: currentEntity.id }).update(childInput);
576
618
  }
577
619
  } else {
578
- await ctx.knex(model.name).where({ id: allFields.id }).update(updateFields);
620
+ await ctx.knex(model.name).where({ id: currentEntity.id }).update(update);
579
621
  }
580
- await createRevision(model, allFields, ctx);
622
+ await createRevision(model, { ...currentEntity, ...update }, ctx);
581
623
  };