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

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,568 @@
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
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
62
+ attributesWhenCreate
63
+ )
64
+ );
65
+ promiseArray.push(
66
+ dynamodbSharedLib.putItem(
67
+ _izContext,
68
+ await 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
+ export async function createAwaitingMultipleStepsWithAdditionalAttributesV2(
85
+ _izContext,
86
+ records, // array of object including properties: awaitingStepId / (optional) additionalAttributes
87
+ pendingStepId
88
+ ) {
89
+ _izContext.logger.debug(
90
+ 'Lib:createAwaitingMultipleStepsWithAdditionalAttributesV2',
91
+ {
92
+ records: records,
93
+ pendingStepId: pendingStepId
94
+ }
95
+ );
96
+ const promiseArray = [];
97
+
98
+ for (let record of records) {
99
+ promiseArray.push(
100
+ dynamodbSharedLib.putItem(
101
+ _izContext,
102
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
103
+ {
104
+ awaitingStepId: record.awaitingStepId,
105
+ pendingStepId: pendingStepId,
106
+ timestamp: Date.now()
107
+ }
108
+ )
109
+ );
110
+ promiseArray.push(
111
+ dynamodbSharedLib.putItem(
112
+ _izContext,
113
+ await dynamodbSharedLib.tableName(
114
+ _izContext,
115
+ 'AwaitingMultipleStepByPending'
116
+ ),
117
+ {
118
+ pendingStepId: pendingStepId,
119
+ awaitingStepId: record.awaitingStepId,
120
+ timestamp: Date.now(),
121
+ ...record.additionalAttributes
122
+ }
123
+ )
124
+ );
125
+ }
126
+
127
+ await Promise.all(promiseArray);
128
+ }
129
+
130
+ /**
131
+ * Create multiple awaiting records given an array of awaitingStepIds.
132
+ * Writes to both "AwaitingMultipleSteps" and "AwaitingMultipleStepByPending".
133
+ * @async
134
+ * @param {IzContext} _izContext
135
+ * @param {string[]} records - array of awaitingStepIds
136
+ * @param {string} pendingStepId
137
+ * @returns {Promise<void>}
138
+ */
139
+ export async function createAwaitingMultipleSteps(
140
+ _izContext,
141
+ records, // array of awaitingStepIds
142
+ pendingStepId
143
+ ) {
144
+ _izContext.logger.debug('Lib:createAwaitingMultipleSteps', {
145
+ records: records,
146
+ pendingStepId: pendingStepId
147
+ });
148
+
149
+ let promiseArray = [];
150
+
151
+ for (let record of records) {
152
+ let attributesWhenCreate = {
153
+ awaitingStepId: record,
154
+ pendingStepId: pendingStepId,
155
+ timestamp: Date.now()
156
+ };
157
+ _izContext.logger.debug('putItem:AwaitingMultipleSteps +++++++', {
158
+ awaitingStepId: attributesWhenCreate.awaitingStepId
159
+ });
160
+ promiseArray.push(
161
+ dynamodbSharedLib.putItem(
162
+ _izContext,
163
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
164
+ attributesWhenCreate
165
+ )
166
+ );
167
+
168
+ _izContext.logger.debug('putItem:AwaitingMultipleStepByPending', {
169
+ awaitingStepId: attributesWhenCreate.awaitingStepId
170
+ });
171
+ promiseArray.push(
172
+ dynamodbSharedLib.putItem(
173
+ _izContext,
174
+ await dynamodbSharedLib.tableName(
175
+ _izContext,
176
+ 'AwaitingMultipleStepByPending'
177
+ ),
178
+ attributesWhenCreate
179
+ )
180
+ );
181
+ }
182
+ await Promise.all(promiseArray);
183
+ }
184
+
185
+ /**
186
+ * Find all pendingStepIds that are waiting on a given awaitingStepId (multiple).
187
+ * @async
188
+ * @param {IzContext} _izContext
189
+ * @param {string} awaitingStepId
190
+ * @returns {Promise<string[]>}
191
+ */
192
+ export async function findPendingStepIdsAwaitingMultipleSteps(
193
+ _izContext,
194
+ awaitingStepId
195
+ ) {
196
+ let pendingStepIds = [];
197
+
198
+ let listPendingStepIds = await dynamodbSharedLib.query(
199
+ _izContext,
200
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
201
+ {
202
+ awaitingStepId: awaitingStepId
203
+ }
204
+ );
205
+
206
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
207
+ pendingStepIds.push(listPendingStepIds.Items[idx].pendingStepId);
208
+ }
209
+
210
+ return pendingStepIds;
211
+ }
212
+
213
+ /**
214
+ * Return the raw items waiting on a given awaitingStepId (multiple).
215
+ * @async
216
+ * @param {IzContext} _izContext
217
+ * @param {string} awaitingStepId
218
+ * @returns {Promise<AwaitingMultipleStepsItem[]>}
219
+ */
220
+ export async function findPendingStepsAwaitingMultipleSteps(
221
+ _izContext,
222
+ awaitingStepId
223
+ ) {
224
+ let listPendingSteps = await dynamodbSharedLib.query(
225
+ _izContext,
226
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
227
+ {
228
+ awaitingStepId: awaitingStepId
229
+ }
230
+ );
231
+ _izContext.logger.debug('Record of awaitingStepId', listPendingSteps);
232
+ return listPendingSteps.Items;
233
+ }
234
+
235
+ /**
236
+ * Fetch exactly one pending step for a given awaitingStepId (throws if not 1).
237
+ * @async
238
+ * @param {IzContext} _izContext
239
+ * @param {string} awaitingStepId
240
+ * @returns {Promise<AwaitingMultipleStepsItem>}
241
+ * @throws {NoRetryError} if there is not exactly one item
242
+ */
243
+ export async function findPendingStepAwaitingMultipleSteps(
244
+ _izContext,
245
+ awaitingStepId
246
+ ) {
247
+ let listPendingSteps = await dynamodbSharedLib.query(
248
+ _izContext,
249
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
250
+ {
251
+ awaitingStepId: awaitingStepId
252
+ }
253
+ );
254
+ _izContext.logger.debug('Record of awaitingStepId', listPendingSteps);
255
+
256
+ if (listPendingSteps.Items.length !== 1) {
257
+ throw new NoRetryError('listPendingSteps not equals 1');
258
+ }
259
+ return listPendingSteps.Items[0];
260
+ }
261
+
262
+ // Use case:
263
+ // Use only after checkAllAwaitingStepsFinished
264
+ // Do NOT delete records in both tables (AwaitingMultipleSteps, AwaitingMultipleStepByPending) if AwaitingMultipleSteps is not yet finished
265
+ // Delete records from both tables only when checkAllAwaitingStepsFinished = true
266
+ // Example usage: flow validateCart lambda ProcessCartOrderFromTraversal
267
+ /**
268
+ * Given a pendingStepId, return all awaiting steps (by the ByPending table).
269
+ * Use only after confirming all awaiting steps are finished, and clean up both tables accordingly.
270
+ * @async
271
+ * @param {IzContext} _izContext
272
+ * @param {string} pendingStepId
273
+ * @returns {Promise<AwaitingMultipleStepByPendingItem[]>}
274
+ */
275
+ export async function findAwaitingMultipleStepByPending(
276
+ _izContext,
277
+ pendingStepId
278
+ ) {
279
+ let listAwaitingSteps = await dynamodbSharedLib.query(
280
+ _izContext,
281
+ await dynamodbSharedLib.tableName(
282
+ _izContext,
283
+ 'AwaitingMultipleStepByPending'
284
+ ),
285
+ {
286
+ pendingStepId: pendingStepId
287
+ }
288
+ );
289
+ _izContext.logger.debug(
290
+ 'Record of AwaitingMultipleStepByPending',
291
+ listAwaitingSteps
292
+ );
293
+ return listAwaitingSteps.Items;
294
+ }
295
+
296
+ /**
297
+ * Same as checkAllAwaitingStepsFinished but writes errors/additionalAttributes to the *_ByPending row.
298
+ * @async
299
+ * @param {IzContext} _izContext
300
+ * @param {string} pendingStepId
301
+ * @param {string|null} [currentAwaitingStepId=null]
302
+ * @param {any[]} [errorsFound=[]]
303
+ * @param {Attrs} [additionalAttributes={}]
304
+ * @returns {Promise<boolean>}
305
+ */
306
+ export async function checkAllAwaitingStepsFinishedWithError(
307
+ _izContext,
308
+ pendingStepId,
309
+ currentAwaitingStepId = null,
310
+ errorsFound = [],
311
+ additionalAttributes = {}
312
+ ) {
313
+ _izContext.logger.debug('Lib:checkAllAwaitingStepsFinishedWithError', {
314
+ pendingStepId: pendingStepId,
315
+ currentAwaitingStepId: currentAwaitingStepId,
316
+ errorsFound: errorsFound
317
+ });
318
+
319
+ if (currentAwaitingStepId) {
320
+ await dynamodbSharedLib.updateItem(
321
+ _izContext,
322
+ await dynamodbSharedLib.tableName(
323
+ _izContext,
324
+ 'AwaitingMultipleStepByPending'
325
+ ),
326
+ {
327
+ pendingStepId: pendingStepId,
328
+ awaitingStepId: currentAwaitingStepId
329
+ },
330
+ {
331
+ complete: true,
332
+ errorsFound: errorsFound,
333
+ ...additionalAttributes
334
+ }
335
+ );
336
+ }
337
+ let listPendingStepIds = await dynamodbSharedLib.query(
338
+ _izContext,
339
+ await dynamodbSharedLib.tableName(
340
+ _izContext,
341
+ 'AwaitingMultipleStepByPending'
342
+ ),
343
+ {
344
+ pendingStepId: pendingStepId
345
+ },
346
+ {
347
+ limit: 50 // arbitrary limit so not pull too many results, need >2 because some others might be marked as "complete" = true
348
+ }
349
+ );
350
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
351
+ // if have error found return errorsFound
352
+ // wrap function
353
+
354
+ if (
355
+ currentAwaitingStepId &&
356
+ listPendingStepIds.Items[idx].awaitingStepId == currentAwaitingStepId
357
+ ) {
358
+ continue;
359
+ }
360
+ // if any record found not set to complete then there are steps not yet finished
361
+ if (!listPendingStepIds.Items[idx].complete) {
362
+ return false;
363
+ }
364
+ }
365
+
366
+ return true; // all passed
367
+ }
368
+
369
+ /**
370
+ * Remove all awaiting records for a given pendingStepId from both tables.
371
+ * @async
372
+ * @param {IzContext} _izContext
373
+ * @param {string} pendingStepId
374
+ * @returns {Promise<boolean>} true if delete flow processed
375
+ */
376
+ export async function clearAllAwaitingSteps(_izContext, pendingStepId) {
377
+ // ----- query items from GSI table
378
+ let listPendingStepIds = await dynamodbSharedLib.query(
379
+ _izContext,
380
+ await dynamodbSharedLib.tableName(
381
+ _izContext,
382
+ 'AwaitingMultipleStepByPending'
383
+ ),
384
+ {
385
+ pendingStepId: pendingStepId
386
+ }
387
+ );
388
+ _izContext.logger.debug('listPendingStepIds', listPendingStepIds);
389
+
390
+ let promiseArray = [];
391
+
392
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
393
+ _izContext.logger.debug(`delete item in dynamodb AwaitingMultipleSteps`);
394
+ promiseArray.push(
395
+ dynamodbSharedLib.deleteItem(
396
+ _izContext,
397
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
398
+ {
399
+ awaitingStepId: listPendingStepIds.Items[idx].awaitingStepId,
400
+ pendingStepId: listPendingStepIds.Items[idx].pendingStepId
401
+ }
402
+ )
403
+ );
404
+
405
+ _izContext.logger.debug(
406
+ `delete item in dynamodb AwaitingMultipleStepByPending`
407
+ );
408
+ promiseArray.push(
409
+ dynamodbSharedLib.deleteItem(
410
+ _izContext,
411
+ await dynamodbSharedLib.tableName(
412
+ _izContext,
413
+ 'AwaitingMultipleStepByPending'
414
+ ),
415
+ {
416
+ pendingStepId: listPendingStepIds.Items[idx].pendingStepId,
417
+ awaitingStepId: listPendingStepIds.Items[idx].awaitingStepId
418
+ }
419
+ )
420
+ );
421
+ }
422
+
423
+ await Promise.all(promiseArray);
424
+
425
+ return true;
426
+ }
427
+
428
+ /**
429
+ * Remove items for AwaitingMultipleSteps and (conditionally) AwaitingMultipleStepByPending.
430
+ * If errorsFound is empty, delete from ByPending table as well.
431
+ * @async
432
+ * @param {IzContext} _izContext
433
+ * @param {string} awaitingStepId
434
+ * @param {string} pendingStepId
435
+ * @param {any[]} [errorsFound=[]]
436
+ * @returns {Promise<void>}
437
+ */
438
+ export async function removeAwaitingMultipleStep(
439
+ _izContext,
440
+ awaitingStepId,
441
+ pendingStepId,
442
+ errorsFound = []
443
+ ) {
444
+ _izContext.logger.debug('Lib:removeAwaitingMultipleStep', {
445
+ awaitingStepId: awaitingStepId,
446
+ pendingStepId: pendingStepId
447
+ });
448
+
449
+ let promiseArray = [];
450
+
451
+ let keyValues = {
452
+ awaitingStepId: awaitingStepId,
453
+ pendingStepId: pendingStepId
454
+ };
455
+
456
+ let keyValuesForByPending = {
457
+ pendingStepId: pendingStepId,
458
+ awaitingStepId: awaitingStepId
459
+ };
460
+ _izContext.logger.debug('deleting ... ', {
461
+ keyValues,
462
+ keyValuesForByPending
463
+ });
464
+
465
+ _izContext.logger.debug(
466
+ `delete item in dynamodb AwaitingMultipleSteps ==> awaitingStepId:${keyValues.awaitingStepId}`
467
+ );
468
+ promiseArray.push(
469
+ dynamodbSharedLib.deleteItem(
470
+ _izContext,
471
+ await dynamodbSharedLib.tableName(_izContext, 'AwaitingMultipleSteps'),
472
+ keyValues
473
+ )
474
+ );
475
+
476
+ if (errorsFound.length == 0) {
477
+ _izContext.logger.debug(
478
+ `delete item in dynamodb AwaitingMultipleStepByPending ==> awaitingStepId:${keyValues.awaitingStepId}`
479
+ );
480
+ promiseArray.push(
481
+ dynamodbSharedLib.deleteItem(
482
+ _izContext,
483
+ await dynamodbSharedLib.tableName(
484
+ _izContext,
485
+ 'AwaitingMultipleStepByPending'
486
+ ),
487
+ keyValuesForByPending
488
+ )
489
+ );
490
+ }
491
+ await Promise.all(promiseArray);
492
+ }
493
+
494
+ /**
495
+ * Checks if all awaiting steps for a pending step are complete after marking the current one as complete.
496
+ * If all steps are finished, it returns `true` and an object containing the `additionalAttributes` from each of the completed steps, keyed by their `awaitingStepId`.
497
+ * If any other step is not yet complete, it returns `[false, null]`.
498
+ * @async
499
+ * @param {IzContext} _izContext
500
+ * @param {string} pendingStepId
501
+ * @param {string|null} [currentAwaitingStepId=null] - The step that has just finished.
502
+ * @param {any[]} [errorsFound=[]]
503
+ * @param {Attrs} [additionalAttributes={}]
504
+ * @returns {Promise<[boolean, Record<string, any>|null]>} A tuple indicating completion status and the collected attributes, or null if not all steps are complete.
505
+ */
506
+ export async function checkAllAwaitingMultipleStepsFinishedWithErrorAndReturnAdditionalAttributes(
507
+ _izContext,
508
+ pendingStepId,
509
+ currentAwaitingStepId = null,
510
+ errorsFound = [],
511
+ additionalAttributes = {}
512
+ ) {
513
+ _izContext.logger.debug(
514
+ 'Lib:checkAllAwaitingMultipleStepsFinishedWithErrorAndReturnAdditionalAttributes',
515
+ {
516
+ pendingStepId,
517
+ currentAwaitingStepId,
518
+ errorsFound,
519
+ additionalAttributes
520
+ }
521
+ );
522
+
523
+ if (currentAwaitingStepId) {
524
+ await dynamodbSharedLib.updateItem(
525
+ _izContext,
526
+ await dynamodbSharedLib.tableName(
527
+ _izContext,
528
+ 'AwaitingMultipleStepByPending'
529
+ ),
530
+ { pendingStepId, awaitingStepId: currentAwaitingStepId },
531
+ {
532
+ complete: true,
533
+ errorsFound,
534
+ ...additionalAttributes
535
+ }
536
+ );
537
+ }
538
+
539
+ const awaitingStepItems = await dynamodbSharedLib.query(
540
+ _izContext,
541
+ await dynamodbSharedLib.tableName(
542
+ _izContext,
543
+ 'AwaitingMultipleStepByPending'
544
+ ),
545
+ { pendingStepId },
546
+ {
547
+ limit: 50 // arbitrary limit so not pull too many results, need >2 because some others might be marked as "complete" = true
548
+ }
549
+ );
550
+
551
+ const collectedAttributes = {};
552
+ for (const item of awaitingStepItems.Items) {
553
+ // For any step OTHER than the one we just completed, if it's not marked as complete,
554
+ // then the overall process is not finished.
555
+ if (item.awaitingStepId !== currentAwaitingStepId && !item.complete) {
556
+ return [false, null];
557
+ }
558
+
559
+ // If the step is complete (which all should be if we reach this point),
560
+ // collect its attributes if they exist.
561
+ if (item.additionalAttributes) {
562
+ collectedAttributes[item.awaitingStepId] = item.additionalAttributes;
563
+ }
564
+ }
565
+
566
+ // If the loop completes, it means all steps were finished.
567
+ return [true, collectedAttributes];
568
+ }