@izara_project/izara-core-library-asynchronous-flow 1.0.42 → 1.0.44

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "author": "Sven Mason <thebarbariansven@gmail.com>",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "homepage": "https://bitbucket.org/izara-core-libraries/izara-core-library-asynchronous-flow#readme",
6
- "version": "1.0.42",
6
+ "version": "1.0.44",
7
7
  "description": "Shared asynchronous flow logic",
8
8
  "type": "module",
9
9
  "main": "index.js",
@@ -16,9 +16,9 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "@izara_project/izara-core-library-core": "^1.0.32",
19
- "@izara_project/izara-core-library-dynamodb": "^1.0.20",
19
+ "@izara_project/izara-core-library-dynamodb": "^1.0.21",
20
20
  "@izara_project/izara-core-library-external-request": "^1.0.30",
21
- "@izara_project/izara-core-library-logger": "^1.0.9",
21
+ "@izara_project/izara-core-library-logger": "^1.0.11",
22
22
  "@izara_project/izara-core-library-sns": "^1.0.8",
23
23
  "@izara_project/izara-core-library-sqs": "^1.0.7",
24
24
  "@izara_project/izara-shared-core": "^1.0.13"
@@ -30,6 +30,7 @@
30
30
  "@izara_project/izara-core-library-logger": "^1.0.9",
31
31
  "@izara_project/izara-core-library-sns": "^1.0.8",
32
32
  "@izara_project/izara-core-library-sqs": "^1.0.7",
33
- "@izara_project/izara-shared-core": "^1.0.12"
33
+ "@izara_project/izara-shared-core": "^1.0.12",
34
+ "ws": "^8.16.0"
34
35
  }
35
36
  }
@@ -36,20 +36,25 @@ import { identifierUuid } from '@izara_project/izara-shared-core';
36
36
  * @returns {string}
37
37
  */
38
38
  function _joinPrefixAndId(id, prefix = '') {
39
- if (!prefix) return id;
39
+ if (!prefix) {
40
+ return id;
41
+ }
42
+
43
+ let cleanPrefix = prefix;
44
+ if (prefix.endsWith('_')) {
45
+ cleanPrefix = prefix.slice(0, -1);
46
+ }
40
47
 
41
- const cleanPrefix = prefix.endsWith('_') ? prefix.slice(0, -1) : prefix;
42
- const cleanId = id.startsWith('_') ? id.slice(1) : id;
48
+ let cleanId = id;
49
+ if (id.startsWith('_')) {
50
+ cleanId = id.slice(1);
51
+ }
43
52
 
44
53
  return `${cleanPrefix}_${cleanId}`;
45
54
  }
46
55
 
47
56
  /**
48
57
  * Create awaitingStepId by concatenating prefix and partitionKey.
49
- * If prefix ends with an underscore, it's removed before concatenation.
50
- * If partitionKey starts with an underscore, it's removed before concatenation.
51
- * This ensures a clean "PREFIX_PARTITIONKEY" format without double underscores.
52
- *
53
58
  * @param {string} partitionKey
54
59
  * @param {string} [prefix='']
55
60
  * @returns {string}
@@ -60,10 +65,6 @@ function createAwaitingStepId(partitionKey, prefix = '') {
60
65
 
61
66
  /**
62
67
  * Create pendingStepId by concatenating prefix and identifierId.
63
- * If prefix ends with an underscore, it's removed before concatenation.
64
- * If identifierId starts with an underscore, it's removed before concatenation.
65
- * This ensures a clean "PREFIX_IDENTIFIER" format without double underscores.
66
- *
67
68
  * @param {string} identifierId
68
69
  * @param {string} [prefix='']
69
70
  * @returns {string}
@@ -73,7 +74,7 @@ function createPendingStepId(identifierId, prefix = '') {
73
74
  }
74
75
 
75
76
  /**
76
- * Create a field name for storing the unique-request id, with optional prefix.
77
+ * Create field name for storing unique-request id, with optional prefix.
77
78
  * @param {string} prefix
78
79
  * @param {string} [uniqueRequestIdFieldName='UniqueRequestId']
79
80
  * @returns {string}
@@ -117,26 +118,29 @@ function createParentFlowId(prefix = '') {
117
118
  }
118
119
 
119
120
  /**
120
- * Remove the known prefix from a pendingStepId and validate it is not equal to the prefix.
121
- * If prefix is empty, the original ID is returned.
121
+ * Remove known prefix from pendingStepId and validate not equal to prefix.
122
122
  * @param {string} pendingStepId
123
123
  * @param {string} [prefix=""]
124
124
  * @returns {string}
125
- * @throws {NoRetryError} If the stripped value equals the prefix (invalid)
125
+ * @throws {NoRetryError} If stripped value equals prefix (invalid)
126
126
  */
127
127
  function explodePendingStepId(pendingStepId, prefix = '') {
128
- if (!pendingStepId) throw new NoRetryError('pendingStepId is required');
129
- if (!prefix) return pendingStepId;
128
+ if (!pendingStepId) {
129
+ throw new NoRetryError('pendingStepId is required');
130
+ }
131
+ if (!prefix) {
132
+ return pendingStepId;
133
+ }
130
134
 
131
- const identifierId = pendingStepId.slice(prefix.length);
132
- if (identifierId === prefix)
135
+ const identifierId = pendingStepId.slice(prefix.length + 1);
136
+ if (identifierId === prefix) {
133
137
  throw new NoRetryError('IdentifierId should not be like prefix.');
138
+ }
134
139
 
135
- Logger.debug('return explode:', identifierId);
140
+ Logger.debug('return explode:', { identifierId });
136
141
  return identifierId;
137
142
  }
138
143
 
139
- // copy from izara-core-library-trigger-cache
140
144
  /**
141
145
  * Checks if record has uniqueRequestId set, if not set to this requests uniqueRequestId and continue to process
142
146
  * if already set check if matches this request uniqueRequestId, if yes continue to process, if not then stop processing
@@ -161,11 +165,11 @@ function explodePendingStepId(pendingStepId, prefix = '') {
161
165
  *
162
166
  * @async
163
167
  * @param {IzContext} _izContext
164
- * @param {string} tableName - Logical table name known by dynamodbSharedLib.tableName
165
- * @param {DynamoKey} primaryKey - Primary key for the target record
168
+ * @param {string} tableName - Logical table name
169
+ * @param {DynamoKey} primaryKey - Primary key for target record
166
170
  * @param {string} [prefix=''] - Optional field-name prefix
167
- * @param {string|null} [overwriteUniqueRequestId=null] - If provided, use this as the "current" unique request id
168
- * @param {string} [uniqueRequestIdFieldName="UniqueRequestId"] - Base field name before prefix
171
+ * @param {string|null} [overwriteUniqueRequestId=null] - Overwrite ID
172
+ * @param {string} [uniqueRequestIdFieldName="UniqueRequestId"] - Base name
169
173
  * @returns {Promise<[UniqueRequestStatus, Attrs]>}
170
174
  * @throws {NoRetryError} If unable to set uniqueRequestId after 2 tries
171
175
  */
@@ -178,7 +182,7 @@ async function checkUniqueRequestProcessing(
178
182
  uniqueRequestIdFieldName = 'UniqueRequestId' // if set will check/add this fieldname with _izContext.uniqueRequestId value
179
183
  ) {
180
184
  _izContext.logger.debug(
181
- '[Lib:AsyncFlow:checkUniqueRequestProcessing] Input: ',
185
+ '[Lib:AsyncFlow:checkUniqueRequestProcessing] Input:',
182
186
  {
183
187
  _izContext,
184
188
  tableName,
@@ -194,13 +198,15 @@ async function checkUniqueRequestProcessing(
194
198
  }
195
199
 
196
200
  try {
197
- _izContext.logger.debug(
198
- 'current uniqueRequestId:',
199
- _izContext.uniqueRequestId
200
- );
201
+ _izContext.logger.debug('[Lib:checkUniqueRequestProcessing] reqId', {
202
+ uniqueRequestId: _izContext.uniqueRequestId
203
+ });
204
+
205
+ let uniqueRequestId = overwriteUniqueRequestId;
206
+ if (!uniqueRequestId) {
207
+ uniqueRequestId = _izContext.uniqueRequestId;
208
+ }
201
209
 
202
- const uniqueRequestId =
203
- overwriteUniqueRequestId || _izContext.uniqueRequestId;
204
210
  uniqueRequestIdFieldName = createFieldNameUniqueRequestId(
205
211
  prefix,
206
212
  uniqueRequestIdFieldName
@@ -213,11 +219,17 @@ async function checkUniqueRequestProcessing(
213
219
  primaryKey
214
220
  );
215
221
 
216
- if (!existingRecord) return ['recordNotFound', {}];
222
+ if (!existingRecord) {
223
+ return ['recordNotFound', {}];
224
+ }
217
225
 
218
226
  const currentReqId = existingRecord[uniqueRequestIdFieldName];
219
- if (currentReqId === uniqueRequestId) return ['process', existingRecord];
220
- if (currentReqId) return ['stop', existingRecord];
227
+ if (currentReqId === uniqueRequestId) {
228
+ return ['process', existingRecord];
229
+ }
230
+ if (currentReqId) {
231
+ return ['stop', existingRecord];
232
+ }
221
233
 
222
234
  try {
223
235
  const updateRecord = await dynamodbSharedLib.updateItem(
@@ -245,17 +257,22 @@ async function checkUniqueRequestProcessing(
245
257
  );
246
258
  return ['process', updateRecord];
247
259
  } catch (err) {
248
- if (err.name === 'ConditionalCheckFailedException') continue;
260
+ if (err.name === 'ConditionalCheckFailedException') {
261
+ continue;
262
+ }
249
263
  throw err;
250
264
  }
251
265
  }
252
266
 
253
267
  throw new NoRetryError(
254
- `unable to set uniqueRequestId in table ${tableName}, record`,
255
- primaryKey
268
+ `unable to set uniqueRequestId in table ${tableName}`,
269
+ { primaryKey }
256
270
  );
257
271
  } catch (err) {
258
- _izContext.logger.error('ERROR checkUniqueRequestProcessing:', err);
272
+ _izContext.logger.error(
273
+ '[Lib:checkUniqueRequestProcessing] Error',
274
+ { err }
275
+ );
259
276
  throw err;
260
277
  }
261
278
  }
@@ -266,9 +283,9 @@ async function checkUniqueRequestProcessing(
266
283
  * Returns [isSameOrUnset, uniqueRequestIdWhenComplete].
267
284
  * @async
268
285
  * @param {IzContext} _izContext
269
- * @param {string} fullMainTableName - Fully resolved table name (not logical)
286
+ * @param {string} fullMainTableName - Fully resolved table name
270
287
  * @param {DynamoKey} keyValues
271
- * @param {number|string} timeCacheComplete - Caller’s expected cache mark
288
+ * @param {number|string} timeCacheComplete - Expected cache mark
272
289
  * @param {string} prefix - Field prefix used in cache fields
273
290
  * @returns {Promise<[boolean, string|undefined]>}
274
291
  */
@@ -279,7 +296,7 @@ async function checkTimeCacheComplete(
279
296
  timeCacheComplete,
280
297
  prefix
281
298
  ) {
282
- _izContext.logger.debug('[Lib:AsyncFlow:checkTimeCacheComplete] Input: ', {
299
+ _izContext.logger.debug('[Lib:AsyncFlow:checkTimeCacheComplete] Input:', {
283
300
  _izContext,
284
301
  fullMainTableName,
285
302
  keyValues,
@@ -289,11 +306,12 @@ async function checkTimeCacheComplete(
289
306
 
290
307
  if (!fullMainTableName || !keyValues || !timeCacheComplete || !prefix) {
291
308
  throw new NoRetryError(
292
- 'fullMainTableName, keyValues, timeCacheComplete and prefix are required'
309
+ 'fullMainTableName, keyValues, timeCacheComplete' +
310
+ ' and prefix are required'
293
311
  );
294
312
  }
295
313
 
296
- let [returnValue, newTimeCacheComplete, uniqueRequestId] =
314
+ const [returnValue, , uniqueRequestId] =
297
315
  await checkAndGetTimeCacheComplete(
298
316
  _izContext,
299
317
  fullMainTableName,
@@ -310,7 +328,7 @@ async function checkTimeCacheComplete(
310
328
  * Otherwise returns [true, storedValue, uniqueRequestId].
311
329
  * @async
312
330
  * @param {IzContext} _izContext
313
- * @param {string} fullMainTableName - Fully resolved table name (not logical)
331
+ * @param {string} fullMainTableName - Fully resolved table name
314
332
  * @param {DynamoKey} keyValues
315
333
  * @param {number|string} timeCacheComplete
316
334
  * @param {string} prefix
@@ -325,27 +343,26 @@ async function checkAndGetTimeCacheComplete(
325
343
  prefix
326
344
  ) {
327
345
  _izContext.logger.debug(
328
- '[Lib:AsyncFlow:checkAndGetTimeCacheComplete] Input: ',
346
+ '[Lib:AsyncFlow:checkAndGetTimeCacheComplete] Input:',
329
347
  {
330
- fullMainTableName: fullMainTableName,
331
- keyValues: keyValues,
332
- timeCacheComplete: timeCacheComplete,
333
- prefix: prefix
348
+ fullMainTableName,
349
+ keyValues,
350
+ timeCacheComplete,
351
+ prefix
334
352
  }
335
353
  );
336
354
 
337
355
  const uniqueRequestIdFieldName = createFieldNameUniqueRequestId('cache');
338
356
  const cacheCompleteFieldName = prefix + 'CacheComplete';
339
- const statusFieldName = prefix + 'Status';
340
357
 
341
- let getTimeCacheComplete = await dynamodbSharedLib.getItem(
358
+ const getTimeCacheComplete = await dynamodbSharedLib.getItem(
342
359
  _izContext,
343
360
  fullMainTableName,
344
361
  keyValues
345
362
  );
346
363
  _izContext.logger.debug(
347
- `getTimeCacheComplete from ${fullMainTableName}: `,
348
- getTimeCacheComplete
364
+ `[Lib:checkAndGetTimeCacheComplete] ${fullMainTableName}`,
365
+ { getTimeCacheComplete }
349
366
  );
350
367
 
351
368
  if (!getTimeCacheComplete) {
@@ -371,13 +388,13 @@ async function checkAndGetTimeCacheComplete(
371
388
  getTimeCacheComplete[cacheCompleteFieldName] !== timeCacheComplete
372
389
  ) {
373
390
  return [false, getTimeCacheComplete[cacheCompleteFieldName]];
374
- } else {
375
- return [
376
- true,
377
- getTimeCacheComplete[cacheCompleteFieldName],
378
- getTimeCacheComplete[uniqueRequestIdFieldName]
379
- ];
380
391
  }
392
+
393
+ return [
394
+ true,
395
+ getTimeCacheComplete[cacheCompleteFieldName],
396
+ getTimeCacheComplete[uniqueRequestIdFieldName]
397
+ ];
381
398
  }
382
399
 
383
400
  // ----- shared both stored cache and triggered cache, check uniqueRequestId changed ---------
@@ -403,9 +420,13 @@ async function checkCacheUniqueRequestId(
403
420
  fullMainTableName,
404
421
  keyValues
405
422
  );
406
- _izContext.logger.debug('cacheObject', cacheObject);
423
+ _izContext.logger.debug('[Lib:checkCacheUniqueRequestId] Output', {
424
+ cacheObject
425
+ });
407
426
 
408
- return cacheObject[uniqueRequestIdCompleteFieldName] === checkUniqueRequestId;
427
+ return (
428
+ cacheObject[uniqueRequestIdCompleteFieldName] === checkUniqueRequestId
429
+ );
409
430
  }
410
431
 
411
432
  //====================================== end Multiple Lambda Invocations (Logic pagination of handling results)
@@ -427,7 +448,7 @@ function validateStartKeyParam(
427
448
  partitionKeyFieldName,
428
449
  sortKeyFieldName
429
450
  ) {
430
- _izContext.logger.debug('[Lib:AsyncFlow:validateStartKeyParam] Input: ', {
451
+ _izContext.logger.debug('[Lib:AsyncFlow:validateStartKeyParam] Input:', {
431
452
  startKey,
432
453
  partitionKeyFieldName,
433
454
  sortKeyFieldName
@@ -442,14 +463,16 @@ function validateStartKeyParam(
442
463
  !stringNotEmptyRegex.test(sortKeyFieldName)
443
464
  ) {
444
465
  throw new NoRetryError(
445
- 'validateStartKeyParam: Invalid partitionKeyFieldName or sortKeyFieldName'
466
+ 'validateStartKeyParam:' +
467
+ ' Invalid partitionKeyFieldName or sortKeyFieldName'
446
468
  );
447
469
  }
448
470
 
449
471
  if (startKey && Object.keys(startKey).length !== 0) {
450
472
  if (!startKey[partitionKeyFieldName] || !startKey[sortKeyFieldName]) {
451
473
  throw new NoRetryError(
452
- 'validateStartKeyParam: Invalid startKey, missing partitionKeyFieldName or sortKeyFieldName'
474
+ 'validateStartKeyParam: Invalid startKey,' +
475
+ ' missing partitionKeyFieldName or sortKeyFieldName'
453
476
  );
454
477
  }
455
478
  return startKey;
@@ -464,12 +487,12 @@ function validateStartKeyParam(
464
487
  * and re-enqueues the message to the specified SQS queue.
465
488
  * @async
466
489
  * @param {IzContext} _izContext
467
- * @param {Attrs} messageProperty - The message body object to re-dispatch
490
+ * @param {Attrs} messageProperty - Message body object to re-dispatch
468
491
  * @param {Attrs} [passOnStartKey={}] - Optional startKey to pass along
469
492
  * @param {number} numberInvocation - Current invocation count
470
- * @param {string} queueName - Logical queue name resolvable via sqsSharedLib.sqsQueueUrl
493
+ * @param {string} queueName - Logical queue name
471
494
  * @returns {Promise<void>}
472
- * @throws {NoRetryError} If `numberInvocation` exceeds internal safety limit
495
+ * @throws {NoRetryError} If numberInvocation exceeds safety limit
473
496
  */
474
497
  async function validateMultipleInvocations(
475
498
  _izContext,
@@ -479,12 +502,12 @@ async function validateMultipleInvocations(
479
502
  queueName
480
503
  ) {
481
504
  _izContext.logger.debug(
482
- '[Lib:AsyncFlow:validateMultipleInvocations] Input: ',
505
+ '[Lib:AsyncFlow:validateMultipleInvocations] Input:',
483
506
  {
484
- messageProperty: messageProperty,
485
- passOnStartKey: passOnStartKey,
486
- numberInvocation: numberInvocation,
487
- queueName: queueName
507
+ messageProperty,
508
+ passOnStartKey,
509
+ numberInvocation,
510
+ queueName
488
511
  }
489
512
  );
490
513
 
@@ -509,11 +532,15 @@ async function validateMultipleInvocations(
509
532
  QueueUrl: await sqsSharedLib.sqsQueueUrl(_izContext, queueName)
510
533
  };
511
534
  _izContext.logger.debug(
512
- `Send message to Dsq:${queueName} `,
513
- messageReInvokeFunction
535
+ `[Lib:validateMultipleInvocations] Send message to Dsq:${queueName}`,
536
+ { messageReInvokeFunction }
514
537
  );
515
538
  await sqs.sendMessage(_izContext, messageReInvokeFunction);
516
539
  } catch (err) {
540
+ _izContext.logger.error(
541
+ '[Lib:validateMultipleInvocations] Error',
542
+ { err }
543
+ );
517
544
  throw err;
518
545
  }
519
546
  }
@@ -21,7 +21,8 @@ import snsSharedLib from '@izara_project/izara-core-library-sns';
21
21
 
22
22
  import {
23
23
  createParentFlowId,
24
- createPendingStepId
24
+ createPendingStepId,
25
+ createAwaitingStepId
25
26
  } from './asyncFlowSharedLib.js';
26
27
 
27
28
  function _assertPendingStepId(pendingStepId) {
@@ -49,13 +50,17 @@ async function _putAwaitingMultipleStepRecords(
49
50
  records.flatMap(({ awaitingStepId, additionalAttributes }) => {
50
51
  const keys = { awaitingStepId, pendingStepId };
51
52
 
53
+ const itemPending = {
54
+ ...keys,
55
+ ...sharedAttributes
56
+ };
57
+ if (additionalAttributes) {
58
+ itemPending.additionalAttributes = additionalAttributes;
59
+ }
60
+
52
61
  return [
53
62
  dynamodbSharedLib.putItem(_izContext, tableSteps, keys),
54
- dynamodbSharedLib.putItem(_izContext, tablePending, {
55
- ...keys,
56
- ...sharedAttributes,
57
- ...(additionalAttributes && { additionalAttributes })
58
- })
63
+ dynamodbSharedLib.putItem(_izContext, tablePending, itemPending)
59
64
  ];
60
65
  })
61
66
  );
@@ -96,23 +101,24 @@ async function _queryAwaitingStepsByPendingStepId(_izContext, pendingStepId) {
96
101
  // One pendingStepId can have multiple awaitingStepIds awaiting it.
97
102
 
98
103
  /**
99
- * Create awaiting multiple step records to block a flow until multiple steps complete.
104
+ * Create awaiting multiple step records to block a flow
105
+ * until multiple steps complete.
100
106
  * @async
101
107
  * @param {IzContext} _izContext - The Izara context.
102
- * @param {string} pendingStepId - The ID of the pending step that is waiting.
108
+ * @param {string} pendingStepId - The ID of pending step.
103
109
  * @param {Array<object>} [records=[]] - Array of step items.
104
- * @param {string} [records[].awaitingStepId] - Optional ID of the awaiting step. If not provided, a random UUID will be generated.
105
- * @param {object} [records[].message] - Optional message payload to publish if flowType is provided.
106
- * @param {object} [records[].flowType] - Optional flowType override for this specific step record.
107
- * @param {string} records[].flowType.flowTag - Tag identifying the flow.
108
- * @param {string} records[].flowType.serviceTag - Tag identifying the service.
110
+ * @param {string} [records[].awaitingStepId] - Awaiting step ID.
111
+ * @param {object} [records[].message] - Message payload to publish.
112
+ * @param {object} [records[].flowType] - FlowType override.
113
+ * @param {string} records[].flowType.flowTag - Tag identifying flow.
114
+ * @param {string} records[].flowType.serviceTag - Tag identifying service.
109
115
  * @param {object} [options={}] - Optional configurations.
110
- * @param {string} [options.prefix=''] - Prefix to prepend to pendingStepId.
111
- * @param {object} [options.additionalAttributes={}] - Default additional attributes to associate with each pending step record.
112
- * @param {object|null} [options.flowType=null] - Default flow schema/service metadata if starting external tasks.
113
- * @param {string} [options.flowType.flowTag] - Tag identifying the default flow.
114
- * @param {string} [options.flowType.serviceTag] - Tag identifying the default service.
115
- * @returns {Promise<string|undefined>} The awaitingStepId of the first mapped record.
116
+ * @param {string} [options.prefix=''] - Prefix for pendingStepId.
117
+ * @param {object} [options.additionalAttributes={}] - Default attributes.
118
+ * @param {object|null} [options.flowType=null] - Default flow schema.
119
+ * @param {string} [options.flowType.flowTag] - Default flow tag.
120
+ * @param {string} [options.flowType.serviceTag] - Default service tag.
121
+ * @returns {Promise<string|null>} The awaitingStepId of first record.
116
122
  */
117
123
  export async function createAwaitingMultipleSteps(
118
124
  _izContext,
@@ -157,7 +163,9 @@ export async function createAwaitingMultipleSteps(
157
163
  }
158
164
 
159
165
  const awaitingStepId =
160
- record.awaitingStepId || createParentFlowId(prefix);
166
+ createAwaitingStepId(record.awaitingStepId, prefix) ||
167
+ createParentFlowId(prefix);
168
+
161
169
  return {
162
170
  awaitingStepId,
163
171
  additionalAttributes,
@@ -174,21 +182,27 @@ export async function createAwaitingMultipleSteps(
174
182
  });
175
183
  }
176
184
 
177
- await _putAwaitingMultipleStepRecords(
178
- _izContext,
179
- finalRecords.map(({ awaitingStepId, additionalAttributes: itemAttrs }) => {
185
+ const mappedStepRecords = finalRecords.map(
186
+ ({ awaitingStepId, additionalAttributes: itemAttrs }) => {
180
187
  const hasItemAttrs = itemAttrs && Object.keys(itemAttrs).length > 0;
181
188
  const hasOptAttrs =
182
189
  additionalAttributes && Object.keys(additionalAttributes).length > 0;
183
190
 
191
+ let mergedAttributes = null;
192
+ if (hasItemAttrs || hasOptAttrs) {
193
+ mergedAttributes = { ...additionalAttributes, ...itemAttrs };
194
+ }
195
+
184
196
  return {
185
197
  awaitingStepId,
186
- additionalAttributes:
187
- hasItemAttrs || hasOptAttrs
188
- ? { ...additionalAttributes, ...itemAttrs }
189
- : undefined
198
+ additionalAttributes: mergedAttributes
190
199
  };
191
- }),
200
+ }
201
+ );
202
+
203
+ await _putAwaitingMultipleStepRecords(
204
+ _izContext,
205
+ mappedStepRecords,
192
206
  finalPendingStepId,
193
207
  {
194
208
  complete: false,
@@ -198,16 +212,19 @@ export async function createAwaitingMultipleSteps(
198
212
 
199
213
  if (hasFlowPublish) {
200
214
  await Promise.all(
201
- finalRecords.map(({ message, topicArn }) =>
202
- sns.publishAsync(_izContext, {
215
+ finalRecords.map(async ({ message, topicArn }) => {
216
+ await sns.publishAsync(_izContext, {
203
217
  TopicArn: `${topicArn}_In`,
204
218
  Message: JSON.stringify(message)
205
- })
206
- )
219
+ });
220
+ })
207
221
  );
208
222
  }
209
223
 
210
- return finalRecords[0]?.awaitingStepId;
224
+ if (finalRecords[0] && finalRecords[0].awaitingStepId) {
225
+ return finalRecords[0].awaitingStepId;
226
+ }
227
+ return null;
211
228
  }
212
229
 
213
230
  /**
@@ -246,11 +263,11 @@ export async function updateAwaitingMultipleStep(
246
263
  }
247
264
 
248
265
  /**
249
- * Query and return the first pending step item that matches the given awaitingStepId.
266
+ * Query and return the first pending step item that matches awaitingStepId.
250
267
  * @async
251
268
  * @param {IzContext} _izContext - The Izara context.
252
269
  * @param {string} awaitingStepId - The ID of the awaiting step.
253
- * @returns {Promise<object|undefined>} The found item or undefined.
270
+ * @returns {Promise<object|null>} The found item or null.
254
271
  */
255
272
  export async function findPendingStepAwaitingMultipleSteps(
256
273
  _izContext,
@@ -265,11 +282,14 @@ export async function findPendingStepAwaitingMultipleSteps(
265
282
  awaitingStepId
266
283
  );
267
284
 
268
- return items[0];
285
+ if (items && items.length > 0) {
286
+ return items[0];
287
+ }
288
+ return null;
269
289
  }
270
290
 
271
291
  /**
272
- * Query and return all pending step items matching the given awaitingStepId.
292
+ * Query and return all pending step items matching awaitingStepId.
273
293
  * @async
274
294
  * @param {IzContext} _izContext - The Izara context.
275
295
  * @param {string} awaitingStepId - The ID of the awaiting step.
@@ -283,11 +303,11 @@ export async function findPendingStepsAwaitingMultipleSteps(
283
303
  }
284
304
 
285
305
  /**
286
- * Retrieve the pendingStepId of the first step matching the given awaitingStepId.
306
+ * Retrieve pendingStepId of first step matching awaitingStepId.
287
307
  * @async
288
308
  * @param {IzContext} _izContext - The Izara context.
289
309
  * @param {string} awaitingStepId - The ID of the awaiting step.
290
- * @returns {Promise<string|undefined>} The pendingStepId of the first item found.
310
+ * @returns {Promise<string|null>} pendingStepId of first item found or null.
291
311
  */
292
312
  export async function findPendingStepIdAwaitingMultipleSteps(
293
313
  _izContext,
@@ -297,11 +317,14 @@ export async function findPendingStepIdAwaitingMultipleSteps(
297
317
  _izContext,
298
318
  awaitingStepId
299
319
  );
300
- return items[0]?.pendingStepId;
320
+ if (items && items[0] && items[0].pendingStepId) {
321
+ return items[0].pendingStepId;
322
+ }
323
+ return null;
301
324
  }
302
325
 
303
326
  /**
304
- * Query and return all awaiting steps matching the given pendingStepId from byPending index table.
327
+ * Query and return all awaiting steps matching pendingStepId.
305
328
  * @async
306
329
  * @param {IzContext} _izContext - The Izara context.
307
330
  * @param {string} pendingStepId - The ID of the pending step.
@@ -315,22 +338,21 @@ export async function findAwaitingMultipleStepByPending(
315
338
  }
316
339
 
317
340
  /**
318
- * Check if all awaiting steps for a pendingStepId are completed. If a current awaitingStepId
319
- * is provided, it updates its status to complete and returns values/errors.
341
+ * Check if all awaiting steps for a pendingStepId are completed.
320
342
  * @async
321
343
  * @param {IzContext} _izContext - The Izara context.
322
344
  * @param {string} pendingStepId - The ID of the pending step.
323
- * @param {string|null} [currentAwaitingStepId=null] - The ID of the current awaiting step that completed.
324
- * @param {Array<any>} [errorsFound=[]] - Errors collected from the current step.
325
- * @param {object} [returnValues={}] - Values returned from the current step.
326
- * @param {object} [settings={ checkIsAllError: false }] - Execution settings.
327
- * @param {boolean} [settings.checkIsAllError=false] - Whether to verify if all steps failed.
328
- * @returns {Promise<object>} Status object containing isComplete, collectedAttributes, collectedErrors, returnValues, additionalAttributes, etc.
345
+ * @param {string|null} [currentAwaitingStepId=null] - Completed step ID.
346
+ * @param {Array<any>} [errorsFound=[]] - Errors collected from current step.
347
+ * @param {object} [returnValues={}] - Values returned from current step.
348
+ * @param {object} [settings={ checkIsAllError: false }] - Settings.
349
+ * @param {boolean} [settings.checkIsAllError=false] - Verify all failed.
350
+ * @returns {Promise<object>} Status object containing completion details.
329
351
  */
330
352
  export async function checkAllAwaitingStepsFinishedWithReturnParams(
331
353
  _izContext,
332
354
  pendingStepId,
333
- currentAwaitingStepId = null,
355
+ currentAwaitingStepId,
334
356
  errorsFound = [],
335
357
  returnValues = {},
336
358
  settings = { checkIsAllError: false }
@@ -370,24 +392,29 @@ export async function checkAllAwaitingStepsFinishedWithReturnParams(
370
392
 
371
393
  _izContext.logger.debug(
372
394
  '[Lib:checkAllAwaitingStepsFinishedWithReturnParams] awaitingStepItems',
373
- awaitingStepItems
395
+ { awaitingStepItems }
374
396
  );
375
397
 
376
398
  const hasIncomplete = awaitingStepItems.some(
377
399
  item => item.awaitingStepId !== currentAwaitingStepId && !item.complete
378
400
  );
379
401
 
402
+ let defaultAttrs = null;
403
+ if (awaitingStepItems[0] && awaitingStepItems[0].additionalAttributes) {
404
+ defaultAttrs = awaitingStepItems[0].additionalAttributes;
405
+ }
406
+
380
407
  if (hasIncomplete) {
381
408
  return {
382
409
  isComplete: false,
383
- collectedAttributes: null,
410
+ collectedReturnValueByAwaitingStepId: null,
384
411
  collectedErrors: [],
385
412
  returnValues: null,
386
- additionalAttributes: awaitingStepItems[0]?.additionalAttributes || null
413
+ additionalAttributes: defaultAttrs
387
414
  };
388
415
  }
389
416
 
390
- const collectedAttributes = {};
417
+ const collectedReturnValueByAwaitingStepId = {};
391
418
  let sharedReturnValues = null;
392
419
 
393
420
  for (const item of awaitingStepItems) {
@@ -395,30 +422,42 @@ export async function checkAllAwaitingStepsFinishedWithReturnParams(
395
422
  continue;
396
423
  }
397
424
 
398
- collectedAttributes[item.awaitingStepId] = item.returnValues;
399
- sharedReturnValues ??= item.returnValues;
425
+ collectedReturnValueByAwaitingStepId[item.awaitingStepId] =
426
+ item.returnValues;
427
+ if (sharedReturnValues === null) {
428
+ sharedReturnValues = item.returnValues;
429
+ }
400
430
  }
401
431
 
402
- const collectedErrors = awaitingStepItems.flatMap(item =>
403
- item.errorsFound?.length > 0
404
- ? [`Error found in step ${item.awaitingStepId}`, ...item.errorsFound]
405
- : []
406
- );
432
+ const collectedErrors = [];
433
+ for (const item of awaitingStepItems) {
434
+ if (item.errorsFound && item.errorsFound.length > 0) {
435
+ collectedErrors.push(
436
+ `Error found in step ${item.awaitingStepId}`,
437
+ ...item.errorsFound
438
+ );
439
+ }
440
+ }
407
441
 
408
- return {
442
+ const result = {
409
443
  isComplete: true,
410
- collectedAttributes,
444
+ collectedReturnValueByAwaitingStepId,
411
445
  collectedErrors,
412
446
  returnValues: sharedReturnValues,
413
- additionalAttributes: awaitingStepItems[0]?.additionalAttributes || null,
414
- ...(settings.checkIsAllError && {
415
- isAllError: awaitingStepItems.every(item => item.errorsFound?.length > 0)
416
- })
447
+ additionalAttributes: defaultAttrs
417
448
  };
449
+
450
+ if (settings.checkIsAllError) {
451
+ result.isAllError = awaitingStepItems.every(
452
+ item => item.errorsFound && item.errorsFound.length > 0
453
+ );
454
+ }
455
+
456
+ return result;
418
457
  }
419
458
 
420
459
  /**
421
- * Clean up/delete all awaiting multiple steps records associated with the pendingStepId from both tables.
460
+ * Clean up/delete all awaiting multiple steps records for pendingStepId.
422
461
  * @async
423
462
  * @param {IzContext} _izContext - The Izara context.
424
463
  * @param {string} pendingStepId - The ID of the pending step.
@@ -429,7 +468,7 @@ export async function clearAllAwaitingSteps(_izContext, pendingStepId) {
429
468
  pendingStepId
430
469
  });
431
470
 
432
- const listPendingStepIds = await _queryAwaitingStepsByPendingStepId(
471
+ const awaitingStepItems = await _queryAwaitingStepsByPendingStepId(
433
472
  _izContext,
434
473
  pendingStepId
435
474
  );
@@ -443,7 +482,7 @@ export async function clearAllAwaitingSteps(_izContext, pendingStepId) {
443
482
  );
444
483
 
445
484
  await Promise.all(
446
- listPendingStepIds.flatMap(
485
+ awaitingStepItems.flatMap(
447
486
  ({ awaitingStepId, pendingStepId: itemPendingStepId }) => [
448
487
  dynamodbSharedLib.deleteItem(_izContext, tableSteps, {
449
488
  awaitingStepId,
@@ -461,12 +500,12 @@ export async function clearAllAwaitingSteps(_izContext, pendingStepId) {
461
500
  }
462
501
 
463
502
  /**
464
- * Delete a specific awaiting step from Steps table (and optionally from byPending table if no errors found).
503
+ * Delete a specific awaiting step from Steps table (and byPending table).
465
504
  * @async
466
505
  * @param {IzContext} _izContext - The Izara context.
467
506
  * @param {string} awaitingStepId - The ID of the awaiting step.
468
507
  * @param {string} pendingStepId - The ID of the pending step.
469
- * @param {Array<any>} [errorsFound=[]] - List of errors. If empty, the record is removed from byPending too.
508
+ * @param {Array<any>} [errorsFound=[]] - List of errors.
470
509
  * @returns {Promise<void>}
471
510
  */
472
511
  export async function removeAwaitingMultipleStep(
@@ -483,7 +522,8 @@ export async function removeAwaitingMultipleStep(
483
522
 
484
523
  if (!awaitingStepId || !pendingStepId) {
485
524
  throw new NoRetryError(
486
- '[Lib:removeAwaitingMultipleStep] awaitingStepId or pendingStepId required'
525
+ '[Lib:removeAwaitingMultipleStep]' +
526
+ ' awaitingStepId or pendingStepId required'
487
527
  );
488
528
  }
489
529
 
@@ -44,16 +44,18 @@ async function _findSinglePendingStep(_izContext, awaitingStepId) {
44
44
  //======================================
45
45
  // AwaitingStep
46
46
  //======================================
47
- // AwaitingStep flow awaiting one external flow, will continue its flow once one external flow completes.
47
+ // AwaitingStep flow awaiting one external flow, will continue its flow once
48
+ // one external flow completes.
48
49
  // One awaitingStepId can have multiple pendingStepIds awaiting it.
49
50
 
50
51
  /**
51
- * Create a single awaiting step record to block a flow until a specific external flow step completes.
52
+ * Create a single awaiting step record to block a flow until a specific
53
+ * external flow step completes.
52
54
  * @async
53
55
  * @param {IzContext} _izContext - The Izara context.
54
56
  * @param {string} awaitingStepId - The ID of the awaiting step.
55
- * @param {string} pendingStepId - The ID of the pending step that is waiting.
56
- * @param {object} [additionalAttributes={}] - Additional attributes to save with the record.
57
+ * @param {string} pendingStepId - The ID of pending step that is waiting.
58
+ * @param {object} [additionalAttributes={}] - Attributes to save.
57
59
  * @param {object} [callingFlowConfig={}] - Calling flow configuration.
58
60
  * @returns {Promise<void>}
59
61
  */
@@ -91,15 +93,15 @@ export async function createAwaitingStep(
91
93
  }
92
94
 
93
95
  /**
94
- * Query and return the single pending step item that matches the given awaitingStepId.
96
+ * Query and return single pending step item matching given awaitingStepId.
95
97
  * @async
96
98
  * @param {IzContext} _izContext - The Izara context.
97
99
  * @param {string} awaitingStepId - The ID of the awaiting step.
98
100
  * @returns {Promise<object>} The found pending step record.
99
- * @throws {NoRetryError} If awaitingStepId is missing or if not exactly one record is found.
101
+ * @throws {NoRetryError} If missing or if not exactly one record found.
100
102
  */
101
103
  export async function findPendingStep(_izContext, awaitingStepId) {
102
- return _findSinglePendingStep(_izContext, awaitingStepId);
104
+ return _findSinglePendingStep(_izContext, awaitingStepId);
103
105
  }
104
106
 
105
107
  /**
@@ -122,7 +124,7 @@ export async function removeAwaitingStep(
122
124
  });
123
125
 
124
126
  if (!awaitingStepId || !pendingStepId) {
125
- throw new NoRetryError('awaitingStepId pendingStepId required');
127
+ throw new NoRetryError('awaitingStepId, pendingStepId required');
126
128
  }
127
129
 
128
130
  await dynamodbSharedLib.deleteItem(
@@ -133,7 +135,7 @@ export async function removeAwaitingStep(
133
135
  }
134
136
 
135
137
  /**
136
- * Delete an awaiting step record from AwaitingStep table, conditional on uniqueRequestId.
138
+ * Delete awaiting step record from AwaitingStep table by uniqueRequestId.
137
139
  * @async
138
140
  * @param {IzContext} _izContext - The Izara context.
139
141
  * @param {string} awaitingStepId - The ID of the awaiting step.
@@ -157,7 +159,7 @@ export async function removeAwaitingStepWithCheckUniqueRequestId(
157
159
 
158
160
  if (!awaitingStepId || !pendingStepId || !checkUniqueRequestId) {
159
161
  throw new NoRetryError(
160
- 'awaitingStepId, pendingStepId checkUniqueRequestId required'
162
+ 'awaitingStepId, pendingStepId, checkUniqueRequestId required'
161
163
  );
162
164
  }
163
165