@axinom/mosaic-graphql-common 0.28.0-rc.2 → 0.28.0-rc.20
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/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.d.ts +49 -1
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.d.ts.map +1 -1
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.js +121 -26
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.js.map +1 -1
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.d.ts +8 -1
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.d.ts.map +1 -1
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.js +45 -39
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.js.map +1 -1
- package/package.json +8 -8
- package/src/plugins/bulk-edit/bulk-edit-async-plugin-factory.spec.ts +1047 -0
- package/src/plugins/bulk-edit/bulk-edit-async-plugin-factory.ts +209 -34
- package/src/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.spec.ts +360 -0
- package/src/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.ts +67 -47
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from 'graphile-build-pg';
|
|
24
24
|
import { gql as gqlExtended } from 'graphile-utils';
|
|
25
25
|
import GraphQL, {
|
|
26
|
+
GraphQLBoolean,
|
|
26
27
|
GraphQLInputFieldMap,
|
|
27
28
|
GraphQLInputObjectType,
|
|
28
29
|
GraphQLList,
|
|
@@ -95,11 +96,10 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
95
96
|
);
|
|
96
97
|
const hasForeignKeyRelations = foreignKeyRelations.length > 0;
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
-
?
|
|
99
|
+
const clearInputTypeForFKs = hasForeignKeyRelations
|
|
100
|
+
? generateClearInputTypeForFKs(
|
|
100
101
|
build,
|
|
101
102
|
pgTable,
|
|
102
|
-
'Add',
|
|
103
103
|
excludedForeignKeyRelations,
|
|
104
104
|
)
|
|
105
105
|
: undefined;
|
|
@@ -113,6 +113,15 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
113
113
|
)
|
|
114
114
|
: undefined;
|
|
115
115
|
|
|
116
|
+
const addInputTypeForFKs = hasForeignKeyRelations
|
|
117
|
+
? generateInputTypeForFKs(
|
|
118
|
+
build,
|
|
119
|
+
pgTable,
|
|
120
|
+
'Add',
|
|
121
|
+
excludedForeignKeyRelations,
|
|
122
|
+
)
|
|
123
|
+
: undefined;
|
|
124
|
+
|
|
116
125
|
const BulkEditPayloadType = getBulkEditPayloadType(build);
|
|
117
126
|
|
|
118
127
|
const args: Record<string, { type: GraphQL.GraphQLInputType }> = {
|
|
@@ -124,6 +133,12 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
124
133
|
},
|
|
125
134
|
};
|
|
126
135
|
|
|
136
|
+
if (clearInputTypeForFKs) {
|
|
137
|
+
args.relatedEntitiesToClear = {
|
|
138
|
+
type: clearInputTypeForFKs,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
if (removeInputTypeForFKs) {
|
|
128
143
|
args.relatedEntitiesToRemove = {
|
|
129
144
|
type: removeInputTypeForFKs,
|
|
@@ -144,8 +159,9 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
144
159
|
input: {
|
|
145
160
|
filter: { [key: string]: any };
|
|
146
161
|
set?: { [key: string]: any };
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
relatedEntitiesToClear?: { [key: string]: boolean };
|
|
163
|
+
relatedEntitiesToRemove?: { [key: string]: any[] };
|
|
164
|
+
relatedEntitiesToAdd?: { [key: string]: any[] };
|
|
149
165
|
},
|
|
150
166
|
context: { [str: string]: unknown },
|
|
151
167
|
resolveInfo: GraphQL.GraphQLResolveInfo,
|
|
@@ -159,6 +175,36 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
159
175
|
storeInboxMessage,
|
|
160
176
|
} = context;
|
|
161
177
|
|
|
178
|
+
if (config === undefined) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`The service config [field: config] was unable to be resolved from the GQL context for the Bulk Edit operations.`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (jwtToken === undefined) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`The JWT token [field: jwtToken] was unable to be resolved from the GQL context for the Bulk Edit operations.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (subject === undefined) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`The authenticated subject [field: subject] was unable to be resolved from the GQL context for the Bulk Edit operations.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (envOwnerPool === undefined && ownerPool === undefined) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`The DB connection pool [fields: envOwnerPool, ownerPool] was unable to be resolved from the GQL context for the Bulk Edit operations.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (storeInboxMessage === undefined) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`The storeInboxMessage function [field: storeInboxMessage] was unable to be resolved from the GQL context for the Bulk Edit operations. Ensure that the service uses the Transactional Inbox-Outbox implementation.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
162
208
|
const longLivedToken = await getLongLivedToken(
|
|
163
209
|
jwtToken as string,
|
|
164
210
|
config as BasicDBConfig,
|
|
@@ -178,12 +224,17 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
178
224
|
resolveInfo.schema,
|
|
179
225
|
);
|
|
180
226
|
|
|
181
|
-
const {
|
|
182
|
-
|
|
227
|
+
const {
|
|
228
|
+
set,
|
|
229
|
+
relatedEntitiesToClear,
|
|
230
|
+
relatedEntitiesToRemove,
|
|
231
|
+
relatedEntitiesToAdd,
|
|
232
|
+
} = input;
|
|
183
233
|
|
|
184
|
-
const { removals, additions } = getFkResolverPayload(
|
|
234
|
+
const { clears, removals, additions } = getFkResolverPayload(
|
|
185
235
|
build,
|
|
186
236
|
pgTable,
|
|
237
|
+
relatedEntitiesToClear,
|
|
187
238
|
relatedEntitiesToRemove,
|
|
188
239
|
relatedEntitiesToAdd,
|
|
189
240
|
excludedForeignKeyRelations,
|
|
@@ -192,6 +243,7 @@ export const BulkEditAsyncPluginFactory = (
|
|
|
192
243
|
await asyncResolverImplementation(
|
|
193
244
|
{
|
|
194
245
|
set: getMainResolverPayload(build, pgTable, set),
|
|
246
|
+
clears,
|
|
195
247
|
removals,
|
|
196
248
|
additions,
|
|
197
249
|
},
|
|
@@ -276,12 +328,27 @@ interface FkResolverPayload {
|
|
|
276
328
|
[fKTableName: string]: {
|
|
277
329
|
parentKeyColumnName: string;
|
|
278
330
|
fkGqlFieldNameToColumnNameMap: { [key: string]: string };
|
|
279
|
-
inputData?:
|
|
331
|
+
inputData?: { [key: string]: any }[];
|
|
280
332
|
};
|
|
281
333
|
}
|
|
282
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Metadata for clearing all entities in a foreign key relation
|
|
337
|
+
*/
|
|
338
|
+
interface FkClearPayload {
|
|
339
|
+
[fKTableName: string]: {
|
|
340
|
+
parentKeyColumnName: string;
|
|
341
|
+
shouldClear: boolean;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Complete payload for bulk item change operations
|
|
347
|
+
* Contains metadata for all possible actions: set, clear, remove, and add
|
|
348
|
+
*/
|
|
283
349
|
interface BulkItemChangePayload {
|
|
284
350
|
set: MainResolverPayload;
|
|
351
|
+
clears: FkClearPayload;
|
|
285
352
|
removals: FkResolverPayload;
|
|
286
353
|
additions: FkResolverPayload;
|
|
287
354
|
}
|
|
@@ -304,24 +371,40 @@ function getMainResolverPayload(
|
|
|
304
371
|
};
|
|
305
372
|
}
|
|
306
373
|
|
|
307
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Processes foreign key relation inputs and converts them into structured payloads
|
|
376
|
+
* for additions, removals, and clears operations.
|
|
377
|
+
*
|
|
378
|
+
* @returns Object containing metadata for all FK operations:
|
|
379
|
+
* - clears: Boolean flags for clearing all entities in a relation
|
|
380
|
+
* - removals: Specific entities to remove from relations
|
|
381
|
+
* - additions: Specific entities to add to relations
|
|
382
|
+
*
|
|
383
|
+
* TODO: Can be potentially optimized (i.e. with getAffectedForeignKeyRelations or with asyncResolverImplementation)
|
|
384
|
+
*/
|
|
308
385
|
function getFkResolverPayload(
|
|
309
386
|
build: Build,
|
|
310
387
|
pgTable: PgClass,
|
|
388
|
+
relatedEntitiesToClear:
|
|
389
|
+
| {
|
|
390
|
+
[key: string]: boolean;
|
|
391
|
+
}
|
|
392
|
+
| undefined,
|
|
311
393
|
relatedEntitiesToRemove:
|
|
312
394
|
| {
|
|
313
|
-
[key: string]: [
|
|
395
|
+
[key: string]: any[];
|
|
314
396
|
}
|
|
315
397
|
| undefined,
|
|
316
398
|
relatedEntitiesToAdd:
|
|
317
399
|
| {
|
|
318
|
-
[key: string]: [
|
|
400
|
+
[key: string]: any[];
|
|
319
401
|
}
|
|
320
402
|
| undefined,
|
|
321
403
|
excludedForeignKeyRelations?: string[],
|
|
322
404
|
): {
|
|
323
|
-
|
|
405
|
+
clears: FkClearPayload;
|
|
324
406
|
removals: FkResolverPayload;
|
|
407
|
+
additions: FkResolverPayload;
|
|
325
408
|
} {
|
|
326
409
|
const inflection: Inflection = build.inflection;
|
|
327
410
|
const foreignKeyRelations = getAffectedForeignKeyRelations(
|
|
@@ -354,35 +437,51 @@ function getFkResolverPayload(
|
|
|
354
437
|
};
|
|
355
438
|
}
|
|
356
439
|
|
|
357
|
-
const
|
|
440
|
+
const clears: FkClearPayload = {};
|
|
358
441
|
const removals: FkResolverPayload = {};
|
|
442
|
+
const additions: FkResolverPayload = {};
|
|
359
443
|
|
|
360
444
|
for (const fkProps of Object.keys(fkProcessingMetadata)) {
|
|
361
|
-
if (
|
|
362
|
-
|
|
445
|
+
if (relatedEntitiesToClear) {
|
|
446
|
+
const shouldClear =
|
|
447
|
+
relatedEntitiesToClear[fkProcessingMetadata[fkProps].fkGqlFieldName];
|
|
448
|
+
if (shouldClear) {
|
|
449
|
+
clears[fkProps] = {
|
|
450
|
+
parentKeyColumnName:
|
|
451
|
+
fkProcessingMetadata[fkProps].parentKeyColumnName,
|
|
452
|
+
shouldClear: true,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (relatedEntitiesToRemove) {
|
|
458
|
+
removals[fkProps] = {
|
|
363
459
|
parentKeyColumnName: fkProcessingMetadata[fkProps].parentKeyColumnName,
|
|
364
460
|
inputData:
|
|
365
|
-
|
|
461
|
+
relatedEntitiesToRemove[fkProcessingMetadata[fkProps].fkGqlFieldName],
|
|
366
462
|
fkGqlFieldNameToColumnNameMap:
|
|
367
463
|
fkProcessingMetadata[fkProps].fkGqlFieldNameToColumnNameMap,
|
|
368
464
|
};
|
|
369
465
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
removals[fkProps] = {
|
|
466
|
+
if (relatedEntitiesToAdd) {
|
|
467
|
+
additions[fkProps] = {
|
|
373
468
|
parentKeyColumnName: fkProcessingMetadata[fkProps].parentKeyColumnName,
|
|
374
469
|
inputData:
|
|
375
|
-
|
|
470
|
+
relatedEntitiesToAdd[fkProcessingMetadata[fkProps].fkGqlFieldName],
|
|
376
471
|
fkGqlFieldNameToColumnNameMap:
|
|
377
472
|
fkProcessingMetadata[fkProps].fkGqlFieldNameToColumnNameMap,
|
|
378
473
|
};
|
|
379
474
|
}
|
|
380
475
|
}
|
|
381
476
|
|
|
382
|
-
return {
|
|
477
|
+
return { clears, removals, additions };
|
|
383
478
|
}
|
|
384
479
|
|
|
385
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Exported for testing purposes
|
|
482
|
+
* @internal
|
|
483
|
+
*/
|
|
484
|
+
export async function asyncResolverImplementation(
|
|
386
485
|
bulkItemChangePayload: BulkItemChangePayload,
|
|
387
486
|
entityIds: string[],
|
|
388
487
|
config: MinimalConfig,
|
|
@@ -402,10 +501,14 @@ async function asyncResolverImplementation(
|
|
|
402
501
|
IsolationLevel.ReadCommitted,
|
|
403
502
|
pgSettings,
|
|
404
503
|
async (txn) => {
|
|
405
|
-
//
|
|
504
|
+
// Process each entity ID and send messages in this order:
|
|
505
|
+
// 1. SET_FIELD_VALUES (update main entity fields)
|
|
506
|
+
// 2. CLEAR_RELATED_ENTITIES (clear all FK relations)
|
|
507
|
+
// 3. REMOVE_RELATED_ENTITY (remove specific FK entities, skip if cleared)
|
|
508
|
+
// 4. ADD_RELATED_ENTITY (add new FK entities)
|
|
406
509
|
for (const entity_id of entityIds) {
|
|
510
|
+
// SET_FIELD_VALUES
|
|
407
511
|
if (bulkItemChangePayload.set.inputData) {
|
|
408
|
-
// Generate the SET_FIELD_VALUES payload
|
|
409
512
|
const actionSetPayload: PerformItemChangeCommand = {
|
|
410
513
|
table_name: bulkItemChangePayload.set.mainTableName,
|
|
411
514
|
action: 'SET_FIELD_VALUES',
|
|
@@ -429,15 +532,52 @@ async function asyncResolverImplementation(
|
|
|
429
532
|
);
|
|
430
533
|
}
|
|
431
534
|
|
|
535
|
+
// CLEAR_RELATED_ENTITIES - Process clears FIRST before any deletions
|
|
536
|
+
const clearedRelations = new Set<string>();
|
|
537
|
+
if (bulkItemChangePayload.clears) {
|
|
538
|
+
const fkTableNames = Object.keys(bulkItemChangePayload.clears);
|
|
539
|
+
|
|
540
|
+
for (const fkTableName of fkTableNames) {
|
|
541
|
+
const fkClearMetadata = bulkItemChangePayload.clears[fkTableName];
|
|
542
|
+
|
|
543
|
+
if (fkClearMetadata.shouldClear) {
|
|
544
|
+
clearedRelations.add(fkTableName); // Track cleared relations
|
|
545
|
+
|
|
546
|
+
const clearPayload: PerformItemChangeCommand = {
|
|
547
|
+
table_name: fkTableName,
|
|
548
|
+
action: 'CLEAR_RELATED_ENTITIES',
|
|
549
|
+
stringified_condition: JSON.stringify({
|
|
550
|
+
[fkClearMetadata.parentKeyColumnName]: entity_id,
|
|
551
|
+
}),
|
|
552
|
+
stringified_payload: JSON.stringify({}),
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// TODO: Add a DEBUG log for `clearPayload`
|
|
556
|
+
|
|
557
|
+
await sendMessage(
|
|
558
|
+
config,
|
|
559
|
+
storeInboxMessage,
|
|
560
|
+
clearPayload,
|
|
561
|
+
txn,
|
|
562
|
+
longLivedToken,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// REMOVE_RELATED_ENTITY - Skip if relation is being cleared
|
|
432
569
|
if (bulkItemChangePayload.removals) {
|
|
433
570
|
const fkTableNames = Object.keys(bulkItemChangePayload.removals);
|
|
434
571
|
|
|
435
|
-
// Iterate each related FK table where entities will be removed
|
|
436
572
|
for (const fkTableName of fkTableNames) {
|
|
573
|
+
// Skip if this relation is being cleared (optimization)
|
|
574
|
+
if (clearedRelations.has(fkTableName)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
437
578
|
const fkRemovalMetadata =
|
|
438
579
|
bulkItemChangePayload.removals[fkTableName];
|
|
439
580
|
if (fkRemovalMetadata.inputData) {
|
|
440
|
-
// Generate the REMOVE_RELATED_ENTITY payload per entity of the FK table
|
|
441
581
|
for (const itemToRemove of fkRemovalMetadata.inputData) {
|
|
442
582
|
const removePayload: PerformItemChangeCommand = {
|
|
443
583
|
table_name: fkTableName,
|
|
@@ -466,17 +606,17 @@ async function asyncResolverImplementation(
|
|
|
466
606
|
}
|
|
467
607
|
}
|
|
468
608
|
|
|
469
|
-
|
|
470
|
-
|
|
609
|
+
// ADD_RELATED_ENTITY
|
|
610
|
+
if (bulkItemChangePayload.additions) {
|
|
611
|
+
const fkTableNames = Object.keys(bulkItemChangePayload.additions);
|
|
471
612
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
613
|
+
for (const fkTableName of fkTableNames) {
|
|
614
|
+
const fkAdditionMetadata =
|
|
615
|
+
bulkItemChangePayload.additions[fkTableName];
|
|
475
616
|
if (fkAdditionMetadata.inputData) {
|
|
476
|
-
// Generate the ADD_RELATED_ENTITY payload per entity of the FK table
|
|
477
617
|
for (const itemToAdd of fkAdditionMetadata.inputData) {
|
|
478
618
|
const addPayload: PerformItemChangeCommand = {
|
|
479
|
-
table_name:
|
|
619
|
+
table_name: fkTableName,
|
|
480
620
|
action: 'ADD_RELATED_ENTITY',
|
|
481
621
|
stringified_condition: '',
|
|
482
622
|
stringified_payload: JSON.stringify({
|
|
@@ -549,6 +689,41 @@ function mapGqlInputObjectToSqlColumnNames(
|
|
|
549
689
|
}, {});
|
|
550
690
|
}
|
|
551
691
|
|
|
692
|
+
// Generate input type for clearing FK relations (boolean flags)
|
|
693
|
+
function generateClearInputTypeForFKs(
|
|
694
|
+
build: Build,
|
|
695
|
+
pgTable: PgClass,
|
|
696
|
+
excludedForeignKeyRelations?: string[],
|
|
697
|
+
): GraphQLInputObjectType {
|
|
698
|
+
const inflection: Inflection = build.inflection;
|
|
699
|
+
const clearInputFieldsMap: GraphQL.Thunk<GraphQL.GraphQLInputFieldConfigMap> =
|
|
700
|
+
{};
|
|
701
|
+
|
|
702
|
+
// Filter only the interesting FK relations
|
|
703
|
+
const foreignKeyRelations = getAffectedForeignKeyRelations(
|
|
704
|
+
pgTable,
|
|
705
|
+
excludedForeignKeyRelations,
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
for (const fkConstraint of foreignKeyRelations) {
|
|
709
|
+
// Add a boolean field for each FK relation
|
|
710
|
+
clearInputFieldsMap[inflection.allRows(fkConstraint.class)] = {
|
|
711
|
+
type: GraphQLBoolean,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const ClearInputType = new GraphQLInputObjectType({
|
|
716
|
+
name: `BulkEditAsync${inflection.tableType(pgTable)}ClearInput`,
|
|
717
|
+
fields: () => ({
|
|
718
|
+
...clearInputFieldsMap,
|
|
719
|
+
}),
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
build.addType(ClearInputType);
|
|
723
|
+
|
|
724
|
+
return ClearInputType;
|
|
725
|
+
}
|
|
726
|
+
|
|
552
727
|
// TODO: Handle `insertable` | `deletable` GRANTS for FK Relations
|
|
553
728
|
function generateInputTypeForFKs(
|
|
554
729
|
build: Build,
|