@izara_project/izara-core-library-asynchronous-flow 1.0.20 → 1.0.22

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.
@@ -0,0 +1,560 @@
1
+ /*
2
+ Copyright (C) 2021 Sven Mason <http://izara.io>
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { NoRetryError } from '@izara_project/izara-core-library-core';
19
+ import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
20
+
21
+ //====================================== AwaitingMultipleSteps
22
+ // AwaitingMultipleSteps is when a flow is awaiting multiple external flows before it can continue
23
+ // One awaitingStepId can have multiple pendingStepIds that are awaiting it
24
+ // One pendingStepId can have multiple awaitingStepIds it is awaiting
25
+ // logic can prepend any prefix to awaitingStepId if want to be share one table and differentiate different external step ids
26
+
27
+ /**
28
+ * Create multiple awaiting records with optional per-record additional attributes.
29
+ * Writes to both "AwaitingMultipleSteps" and "AwaitingMultipleStepByPending".
30
+ * @async
31
+ * @param {IzContext} _izContext
32
+ * @param {AwaitingMultiInputRecord[]} records
33
+ * @param {string} pendingStepId
34
+ * @returns {Promise<void>}
35
+ */
36
+ export async function createAwaitingMultipleStepsWithAdditionalAttributes(
37
+ _izContext,
38
+ records, // array of object including properties: awaitingStepId / (optional) additionalAttributes
39
+ pendingStepId
40
+ ) {
41
+ _izContext.logger.debug(
42
+ 'Lib:createAwaitingMultipleStepsWithAdditionalAttributes',
43
+ {
44
+ records: records,
45
+ pendingStepId: pendingStepId
46
+ }
47
+ );
48
+ let promiseArray = [];
49
+
50
+ for (let record of records) {
51
+ let attributesWhenCreate = {
52
+ awaitingStepId: record.awaitingStepId,
53
+ pendingStepId: pendingStepId,
54
+ timestamp: Date.now(),
55
+ ...record.additionalAttributes
56
+ };
57
+
58
+ promiseArray.push(
59
+ dynamodbSharedLib.putItem(
60
+ _izContext,
61
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
62
+ attributesWhenCreate
63
+ )
64
+ );
65
+ promiseArray.push(
66
+ dynamodbSharedLib.putItem(
67
+ _izContext,
68
+ dynamodbSharedLib.tableName(
69
+ _izContext,
70
+ 'AwaitingMultipleStepByPending'
71
+ ),
72
+ {
73
+ pendingStepId: pendingStepId,
74
+ awaitingStepId: record.awaitingStepId,
75
+ timestamp: Date.now()
76
+ }
77
+ )
78
+ );
79
+ }
80
+
81
+ await Promise.all(promiseArray);
82
+ }
83
+
84
+ /**
85
+ * Batch initializes multiple pending steps in DynamoDB.
86
+ * Records status in both the primary tracking table and the pending-relation table.
87
+ * * @async
88
+ * @param {Object} _izContext - The system context providing logger and shared utilities.
89
+ * @param {Array<Object>} records - Array of step objects to be initialized.
90
+ * @param {string} records[].awaitingStepId - The unique identifier for the specific step.
91
+ * @param {Object} [records[].additionalAttributes] - Optional metadata to store with the step.
92
+ * @param {string} pendingStepId - The parent ID linking all related steps together.
93
+ * @returns {Promise<void>}
94
+ */
95
+ export async function initAwaitingStepsWithAttrs(
96
+ _izContext,
97
+ records,
98
+ pendingStepId
99
+ ) {
100
+ _izContext.logger.debug('Lib:initAwaitingStepsWithAttrs', {
101
+ records,
102
+ pendingStepId
103
+ });
104
+
105
+ const timestamp = Date.now();
106
+ const promiseArray = [];
107
+
108
+ for (const record of records) {
109
+ promiseArray.push(
110
+ dynamodbSharedLib.putItem(
111
+ _izContext,
112
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
113
+ {
114
+ awaitingStepId: record.awaitingStepId,
115
+ pendingStepId,
116
+ timestamp
117
+ }
118
+ )
119
+ );
120
+ promiseArray.push(
121
+ dynamodbSharedLib.putItem(
122
+ _izContext,
123
+ dynamodbSharedLib.tableName(
124
+ _izContext,
125
+ 'AwaitingMultipleStepByPending'
126
+ ),
127
+ {
128
+ pendingStepId,
129
+ awaitingStepId: record.awaitingStepId,
130
+ timestamp,
131
+ ...record.additionalAttributes
132
+ }
133
+ )
134
+ );
135
+ }
136
+
137
+ await Promise.all(promiseArray);
138
+ }
139
+
140
+ /**
141
+ * Create multiple awaiting records given an array of awaitingStepIds.
142
+ * Writes to both "AwaitingMultipleSteps" and "AwaitingMultipleStepByPending".
143
+ * @async
144
+ * @param {IzContext} _izContext
145
+ * @param {string[]} records - array of awaitingStepIds
146
+ * @param {string} pendingStepId
147
+ * @returns {Promise<void>}
148
+ */
149
+ export async function createAwaitingMultipleSteps(
150
+ _izContext,
151
+ records, // array of awaitingStepIds
152
+ pendingStepId
153
+ ) {
154
+ _izContext.logger.debug('Lib:createAwaitingMultipleSteps', {
155
+ records: records,
156
+ pendingStepId: pendingStepId
157
+ });
158
+
159
+ let promiseArray = [];
160
+
161
+ for (let record of records) {
162
+ let attributesWhenCreate = {
163
+ awaitingStepId: record,
164
+ pendingStepId: pendingStepId,
165
+ timestamp: Date.now()
166
+ };
167
+ _izContext.logger.debug('putItem:AwaitingMultipleSteps +++++++', {
168
+ awaitingStepId: attributesWhenCreate.awaitingStepId
169
+ });
170
+ promiseArray.push(
171
+ dynamodbSharedLib.putItem(
172
+ _izContext,
173
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
174
+ attributesWhenCreate
175
+ )
176
+ );
177
+
178
+ _izContext.logger.debug('putItem:AwaitingMultipleStepByPending', {
179
+ awaitingStepId: attributesWhenCreate.awaitingStepId
180
+ });
181
+ promiseArray.push(
182
+ dynamodbSharedLib.putItem(
183
+ _izContext,
184
+ dynamodbSharedLib.tableName(
185
+ _izContext,
186
+ 'AwaitingMultipleStepByPending'
187
+ ),
188
+ attributesWhenCreate
189
+ )
190
+ );
191
+ }
192
+ await Promise.all(promiseArray);
193
+ }
194
+
195
+ /**
196
+ * Find all pendingStepIds that are waiting on a given awaitingStepId (multiple).
197
+ * @async
198
+ * @param {IzContext} _izContext
199
+ * @param {string} awaitingStepId
200
+ * @returns {Promise<string[]>}
201
+ */
202
+ export async function findPendingStepIdsAwaitingMultipleSteps(
203
+ _izContext,
204
+ awaitingStepId
205
+ ) {
206
+ let pendingStepIds = [];
207
+
208
+ let listPendingStepIds = await dynamodbSharedLib.query(
209
+ _izContext,
210
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
211
+ {
212
+ awaitingStepId: awaitingStepId
213
+ }
214
+ );
215
+
216
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
217
+ pendingStepIds.push(listPendingStepIds.Items[idx].pendingStepId);
218
+ }
219
+
220
+ return pendingStepIds;
221
+ }
222
+
223
+ /**
224
+ * Return the raw items waiting on a given awaitingStepId (multiple).
225
+ * @async
226
+ * @param {IzContext} _izContext
227
+ * @param {string} awaitingStepId
228
+ * @returns {Promise<AwaitingMultipleStepsItem[]>}
229
+ */
230
+ export async function findPendingStepsAwaitingMultipleSteps(
231
+ _izContext,
232
+ awaitingStepId
233
+ ) {
234
+ let listPendingSteps = await dynamodbSharedLib.query(
235
+ _izContext,
236
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
237
+ {
238
+ awaitingStepId: awaitingStepId
239
+ }
240
+ );
241
+ _izContext.logger.debug('Record of awaitingStepId', listPendingSteps);
242
+ return listPendingSteps.Items;
243
+ }
244
+
245
+ /**
246
+ * Fetch exactly one pending step for a given awaitingStepId (throws if not 1).
247
+ * @async
248
+ * @param {IzContext} _izContext
249
+ * @param {string} awaitingStepId
250
+ * @returns {Promise<AwaitingMultipleStepsItem>}
251
+ * @throws {NoRetryError} if there is not exactly one item
252
+ */
253
+ export async function findPendingStepAwaitingMultipleSteps(
254
+ _izContext,
255
+ awaitingStepId
256
+ ) {
257
+ let listPendingSteps = await dynamodbSharedLib.query(
258
+ _izContext,
259
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
260
+ {
261
+ awaitingStepId: awaitingStepId
262
+ }
263
+ );
264
+ _izContext.logger.debug('Record of awaitingStepId', listPendingSteps);
265
+
266
+ if (listPendingSteps.Items.length !== 1) {
267
+ throw new NoRetryError('listPendingSteps not equals 1');
268
+ }
269
+ return listPendingSteps.Items[0];
270
+ }
271
+
272
+ // Use case:
273
+ // Use only after checkAllAwaitingStepsFinished
274
+ // Do NOT delete records in both tables (AwaitingMultipleSteps, AwaitingMultipleStepByPending) if AwaitingMultipleSteps is not yet finished
275
+ // Delete records from both tables only when checkAllAwaitingStepsFinished = true
276
+ // Example usage: flow validateCart lambda ProcessCartOrderFromTraversal
277
+ /**
278
+ * Given a pendingStepId, return all awaiting steps (by the ByPending table).
279
+ * Use only after confirming all awaiting steps are finished, and clean up both tables accordingly.
280
+ * @async
281
+ * @param {IzContext} _izContext
282
+ * @param {string} pendingStepId
283
+ * @returns {Promise<AwaitingMultipleStepByPendingItem[]>}
284
+ */
285
+ export async function findAwaitingMultipleStepByPending(
286
+ _izContext,
287
+ pendingStepId
288
+ ) {
289
+ let listAwaitingSteps = await dynamodbSharedLib.query(
290
+ _izContext,
291
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleStepByPending'),
292
+ {
293
+ pendingStepId: pendingStepId
294
+ }
295
+ );
296
+ _izContext.logger.debug(
297
+ 'Record of AwaitingMultipleStepByPending',
298
+ listAwaitingSteps
299
+ );
300
+ return listAwaitingSteps.Items;
301
+ }
302
+
303
+ /**
304
+ * Same as checkAllAwaitingStepsFinished but writes errors/additionalAttributes to the *_ByPending row.
305
+ * @async
306
+ * @param {IzContext} _izContext
307
+ * @param {string} pendingStepId
308
+ * @param {string|null} [currentAwaitingStepId=null]
309
+ * @param {any[]} [errorsFound=[]]
310
+ * @param {Attrs} [additionalAttributes={}]
311
+ * @returns {Promise<boolean>}
312
+ */
313
+ export async function checkAllAwaitingStepsFinishedWithError(
314
+ _izContext,
315
+ pendingStepId,
316
+ currentAwaitingStepId = null,
317
+ errorsFound = [],
318
+ additionalAttributes = {}
319
+ ) {
320
+ _izContext.logger.debug('Lib:checkAllAwaitingStepsFinishedWithError', {
321
+ pendingStepId: pendingStepId,
322
+ currentAwaitingStepId: currentAwaitingStepId,
323
+ errorsFound: errorsFound
324
+ });
325
+
326
+ if (currentAwaitingStepId) {
327
+ await dynamodbSharedLib.updateItem(
328
+ _izContext,
329
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleStepByPending'),
330
+ {
331
+ pendingStepId: pendingStepId,
332
+ awaitingStepId: currentAwaitingStepId
333
+ },
334
+ {
335
+ complete: true,
336
+ errorsFound: errorsFound,
337
+ ...additionalAttributes
338
+ }
339
+ );
340
+ }
341
+ let listPendingStepIds = await dynamodbSharedLib.query(
342
+ _izContext,
343
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleStepByPending'),
344
+ {
345
+ pendingStepId: pendingStepId
346
+ },
347
+ {
348
+ limit: 50 // arbitrary limit so not pull too many results, need >2 because some others might be marked as "complete" = true
349
+ }
350
+ );
351
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
352
+ // if have error found return errorsFound
353
+ // wrap function
354
+
355
+ if (
356
+ currentAwaitingStepId &&
357
+ listPendingStepIds.Items[idx].awaitingStepId == currentAwaitingStepId
358
+ ) {
359
+ continue;
360
+ }
361
+ // if any record found not set to complete then there are steps not yet finished
362
+ if (!listPendingStepIds.Items[idx].complete) {
363
+ return false;
364
+ }
365
+ }
366
+
367
+ return true; // all passed
368
+ }
369
+
370
+ /**
371
+ * Remove all awaiting records for a given pendingStepId from both tables.
372
+ * @async
373
+ * @param {IzContext} _izContext
374
+ * @param {string} pendingStepId
375
+ * @returns {Promise<boolean>} true if delete flow processed
376
+ */
377
+ export async function clearAllAwaitingSteps(_izContext, pendingStepId) {
378
+ // ----- query items from GSI table
379
+ let listPendingStepIds = await dynamodbSharedLib.query(
380
+ _izContext,
381
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleStepByPending'),
382
+ {
383
+ pendingStepId: pendingStepId
384
+ }
385
+ );
386
+ _izContext.logger.debug('listPendingStepIds', listPendingStepIds);
387
+
388
+ let promiseArray = [];
389
+
390
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
391
+ _izContext.logger.debug(`delete item in dynamodb AwaitingMultipleSteps`);
392
+ promiseArray.push(
393
+ dynamodbSharedLib.deleteItem(
394
+ _izContext,
395
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
396
+ {
397
+ awaitingStepId: listPendingStepIds.Items[idx].awaitingStepId,
398
+ pendingStepId: listPendingStepIds.Items[idx].pendingStepId
399
+ }
400
+ )
401
+ );
402
+
403
+ _izContext.logger.debug(
404
+ `delete item in dynamodb AwaitingMultipleStepByPending`
405
+ );
406
+ promiseArray.push(
407
+ dynamodbSharedLib.deleteItem(
408
+ _izContext,
409
+ dynamodbSharedLib.tableName(
410
+ _izContext,
411
+ 'AwaitingMultipleStepByPending'
412
+ ),
413
+ {
414
+ pendingStepId: listPendingStepIds.Items[idx].pendingStepId,
415
+ awaitingStepId: listPendingStepIds.Items[idx].awaitingStepId
416
+ }
417
+ )
418
+ );
419
+ }
420
+
421
+ await Promise.all(promiseArray);
422
+
423
+ return true;
424
+ }
425
+
426
+ /**
427
+ * Remove items for AwaitingMultipleSteps and (conditionally) AwaitingMultipleStepByPending.
428
+ * If errorsFound is empty, delete from ByPending table as well.
429
+ * @async
430
+ * @param {IzContext} _izContext
431
+ * @param {string} awaitingStepId
432
+ * @param {string} pendingStepId
433
+ * @param {any[]} [errorsFound=[]]
434
+ * @returns {Promise<void>}
435
+ */
436
+ export async function removeAwaitingMultipleStep(
437
+ _izContext,
438
+ awaitingStepId,
439
+ pendingStepId,
440
+ errorsFound = []
441
+ ) {
442
+ _izContext.logger.debug('Lib:removeAwaitingMultipleStep', {
443
+ awaitingStepId: awaitingStepId,
444
+ pendingStepId: pendingStepId
445
+ });
446
+
447
+ let promiseArray = [];
448
+
449
+ let keyValues = {
450
+ awaitingStepId: awaitingStepId,
451
+ pendingStepId: pendingStepId
452
+ };
453
+
454
+ let keyValuesForByPending = {
455
+ pendingStepId: pendingStepId,
456
+ awaitingStepId: awaitingStepId
457
+ };
458
+ _izContext.logger.debug('deleting ... ', {
459
+ keyValues,
460
+ keyValuesForByPending
461
+ });
462
+
463
+ _izContext.logger.debug(
464
+ `delete item in dynamodb AwaitingMultipleSteps ==> awaitingStepId:${keyValues.awaitingStepId}`
465
+ );
466
+ promiseArray.push(
467
+ dynamodbSharedLib.deleteItem(
468
+ _izContext,
469
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
470
+ keyValues
471
+ )
472
+ );
473
+
474
+ if (errorsFound.length == 0) {
475
+ _izContext.logger.debug(
476
+ `delete item in dynamodb AwaitingMultipleStepByPending ==> awaitingStepId:${keyValues.awaitingStepId}`
477
+ );
478
+ promiseArray.push(
479
+ dynamodbSharedLib.deleteItem(
480
+ _izContext,
481
+ dynamodbSharedLib.tableName(
482
+ _izContext,
483
+ 'AwaitingMultipleStepByPending'
484
+ ),
485
+ keyValuesForByPending
486
+ )
487
+ );
488
+ }
489
+ await Promise.all(promiseArray);
490
+ }
491
+
492
+ /**
493
+ * Checks if all awaiting steps for a pending step are complete after marking the current one as complete.
494
+ * If all steps are finished, it returns `true` and an object containing the `additionalAttributes` from each of the completed steps, keyed by their `awaitingStepId`.
495
+ * If any other step is not yet complete, it returns `[false, null]`.
496
+ * @async
497
+ * @param {IzContext} _izContext
498
+ * @param {string} pendingStepId
499
+ * @param {string|null} [currentAwaitingStepId=null] - The step that has just finished.
500
+ * @param {any[]} [errorsFound=[]]
501
+ * @param {Attrs} [additionalAttributes={}]
502
+ * @returns {Promise<[boolean, Record<string, any>|null]>} A tuple indicating completion status and the collected attributes, or null if not all steps are complete.
503
+ */
504
+ export async function syncAndCheckStepsCompletion(
505
+ _izContext,
506
+ pendingStepId,
507
+ currentAwaitingStepId = null,
508
+ errorsFound = [],
509
+ additionalAttributes = {}
510
+ ) {
511
+ _izContext.logger.debug(
512
+ 'Lib:checkAllAwaitingMultipleStepsFinishedWithErrorAndReturnAdditionalAttributes',
513
+ {
514
+ pendingStepId,
515
+ currentAwaitingStepId,
516
+ errorsFound,
517
+ additionalAttributes
518
+ }
519
+ );
520
+
521
+ if (currentAwaitingStepId) {
522
+ await dynamodbSharedLib.updateItem(
523
+ _izContext,
524
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleStepByPending'),
525
+ { pendingStepId, awaitingStepId: currentAwaitingStepId },
526
+ {
527
+ complete: true,
528
+ errorsFound,
529
+ ...additionalAttributes
530
+ }
531
+ );
532
+ }
533
+
534
+ const awaitingStepItems = await dynamodbSharedLib.query(
535
+ _izContext,
536
+ dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleStepByPending'),
537
+ { pendingStepId },
538
+ {
539
+ limit: 50 // arbitrary limit so not pull too many results, need >2 because some others might be marked as "complete" = true
540
+ }
541
+ );
542
+
543
+ const collectedAttributes = {};
544
+ for (const item of awaitingStepItems.Items) {
545
+ // For any step OTHER than the one we just completed, if it's not marked as complete,
546
+ // then the overall process is not finished.
547
+ if (item.awaitingStepId !== currentAwaitingStepId && !item.complete) {
548
+ return [false, null];
549
+ }
550
+
551
+ // If the step is complete (which all should be if we reach this point),
552
+ // collect its attributes if they exist.
553
+ if (item.additionalAttributes) {
554
+ collectedAttributes[item.awaitingStepId] = item.additionalAttributes;
555
+ }
556
+ }
557
+
558
+ // If the loop completes, it means all steps were finished.
559
+ return [true, collectedAttributes];
560
+ }