@izara_project/izara-core-library-asynchronous-flow 1.0.1 → 1.0.2

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/index.js ADDED
@@ -0,0 +1,58 @@
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
+ 'use strict';
19
+
20
+ // Import the Asynchronous Flow shared library functions
21
+ const asyncFlowSharedLib = require('./src/AsyncFlowSharedLib');
22
+
23
+ // Export all the functions
24
+ module.exports = {
25
+ // Identifier creation
26
+ createFieldNameUniqueRequestId: asyncFlowSharedLib.createFieldNameUniqueRequestId,
27
+ createConcatenatedAwaitingStepId: asyncFlowSharedLib.createConcatenatedAwaitingStepId,
28
+ createConcatenatedPendingStepId: asyncFlowSharedLib.createConcatenatedPendingStepId,
29
+ createAwaitingStepId: asyncFlowSharedLib.createAwaitingStepId,
30
+ createPendingStepId: asyncFlowSharedLib.createPendingStepId,
31
+ explodePendingStepId: asyncFlowSharedLib.explodePendingStepId,
32
+
33
+ // Awaiting Multiple Steps
34
+ createAwaitingMultipleStepsWithAdditionalAttributes: asyncFlowSharedLib.createAwaitingMultipleStepsWithAdditionalAttributes,
35
+ createAwaitingMultipleSteps: asyncFlowSharedLib.createAwaitingMultipleSteps,
36
+
37
+ // Awaiting Step
38
+ createAwaitingStep: asyncFlowSharedLib.createAwaitingStep,
39
+ findPendingStepIdsAwaitingStep: asyncFlowSharedLib.findPendingStepIdsAwaitingStep,
40
+ findPendingStepsAwaitingStep: asyncFlowSharedLib.findPendingStepsAwaitingStep,
41
+
42
+ // Checking and clearing steps
43
+ checkAllAwaitingStepsFinished: asyncFlowSharedLib.checkAllAwaitingStepsFinished,
44
+ clearAllAwaitingSteps: asyncFlowSharedLib.clearAllAwaitingSteps,
45
+ removeAwaitingStep: asyncFlowSharedLib.removeAwaitingStep,
46
+ removeAwaitingMultipleStep: asyncFlowSharedLib.removeAwaitingMultipleStep,
47
+ removeAwaitingStepWithCheckUniqueRequestId: asyncFlowSharedLib.removeAwaitingStepWithCheckUniqueRequestId,
48
+
49
+ // Request and cache processing
50
+ checkUniqueRequestProcessing: asyncFlowSharedLib.checkUniqueRequestProcessing,
51
+ checkTimeCacheComplete: asyncFlowSharedLib.checkTimeCacheComplete,
52
+ checkAndGetTimeCacheComplete: asyncFlowSharedLib.checkAndGetTimeCacheComplete,
53
+ checkCacheUniqueRequestId: asyncFlowSharedLib.checkCacheUniqueRequestId,
54
+
55
+ // Pagination and invocation helpers
56
+ validateStartKeyParam: asyncFlowSharedLib.validateStartKeyParam,
57
+ validateMultipleInvocations: asyncFlowSharedLib.validateMultipleInvocations
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-library-asynchronous-flow",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Shared asynchronous flow logic",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,4 +25,4 @@
25
25
  "@izara_project/izara-shared": "^1.0.121",
26
26
  "aws-jwt-verify": "^4.0.1"
27
27
  }
28
- }
28
+ }
@@ -0,0 +1,864 @@
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
+ 'use strict';
19
+
20
+ // Core libraries
21
+ const { NoRetryError } = require('@izara_project/izara-core-library-core');
22
+ const Logger = require('@izara_project/izara-core-library-logger');
23
+ const dynamodbSharedLib = require('@izara_project/izara-core-library-dynamodb');
24
+ const sqsSharedLib = require('@izara_project/izara-core-library-sqs');
25
+ const triggeredCacheSharedLib = require('@izara_project/izara-core-library-triggeredCache');
26
+
27
+ // External services
28
+ const { sqs } = require('@izara_project/izara-core-library-external-request');
29
+
30
+ module.exports.createFieldNameUniqueRequestId = (
31
+ prefix,
32
+ uniqueRequestIdFieldName = 'UniqueRequestId'
33
+ ) => {
34
+ return prefix + uniqueRequestIdFieldName;
35
+ }
36
+
37
+ module.exports.createConcatenatedAwaitingStepId = (
38
+ partitionKey,
39
+ sortId,
40
+ prefix = ""
41
+ ) => {
42
+ return prefix + partitionKey + "_" + sortId
43
+ }
44
+
45
+ module.exports.createConcatenatedPendingStepId = (
46
+ parentId,
47
+ childId
48
+ ) => {
49
+ return parentId + "_" + childId
50
+ }
51
+
52
+ module.exports.createAwaitingStepId = (
53
+ partitionKey,
54
+ prefix = ""
55
+ ) => {
56
+ return prefix + partitionKey
57
+ }
58
+
59
+ module.exports.createPendingStepId = (
60
+ identifierId,
61
+ prefix = ""
62
+ ) => {
63
+ return prefix + identifierId
64
+ }
65
+
66
+ module.exports.explodePendingStepId = (
67
+ pendingStepId,
68
+ prefix = ""
69
+ ) => {
70
+ if (prefix === "") {
71
+ return pendingStepId;
72
+ } else {
73
+ let identifierId = pendingStepId.slice(prefix.length)
74
+ // ** validate identifierId == prefix
75
+ if (identifierId == prefix) {
76
+ throw new NoRetryError("IdentifierId should not be like prefix.");
77
+ }
78
+
79
+ Logger.debug("return explode:", identifierId);
80
+ return identifierId;
81
+ }
82
+
83
+ }
84
+
85
+ //====================================== AwaitingMultipleSteps
86
+ // AwaitingMultipleSteps is when a flow is awaiting multiple external flows before it can continue
87
+ // One awaitingStepId can have multiple pendingStepIds that are awaiting it
88
+ // One pendingStepId can have multiple awaitingStepIds it is awaiting
89
+ // logic can prepend any prefix to awaitingStepId if want to be share one table and differentiate different external step ids
90
+
91
+ //not yet used ?
92
+ module.exports.createAwaitingMultipleStepsWithAdditionalAttributes = async (
93
+ _izContext,
94
+ records, // array of object including properties: awaitingStepId / (optional) additionalAttributes
95
+ pendingStepId,
96
+ tableName = "AwaitingMultipleSteps",
97
+ byPendingTableName = "AwaitingMultipleStepByPending"
98
+ ) => {
99
+ _izContext.logger.debug("Lib:createAwaitingMultipleStepsWithAdditionalAttributes", {
100
+ records: records,
101
+ pendingStepId: pendingStepId,
102
+ tableName: tableName,
103
+ byPendingTableName: byPendingTableName
104
+ });
105
+ let promiseArray = []
106
+
107
+ for (let record of records) {
108
+
109
+ let attributesWhenCreate = {
110
+ awaitingStepId: record.awaitingStepId,
111
+ pendingStepId: pendingStepId,
112
+ timestamp: Date.now(),
113
+ ...record.additionalAttributes
114
+ }
115
+
116
+ promiseArray.push(
117
+ dynamodbSharedLib.putItem(
118
+ _izContext,
119
+ await dynamodbSharedLib.tableName(_izContext, tableName),
120
+ attributesWhenCreate
121
+ )
122
+ );
123
+ promiseArray.push(
124
+ dynamodbSharedLib.putItem(
125
+ _izContext,
126
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
127
+ {
128
+ pendingStepId: pendingStepId,
129
+ awaitingStepId: record.awaitingStepId,
130
+ timestamp: Date.now(),
131
+ }
132
+ ));
133
+
134
+ }
135
+
136
+ await Promise.all(promiseArray);
137
+
138
+ }
139
+
140
+ module.exports.createAwaitingMultipleSteps = async (
141
+ _izContext,
142
+ records, // array of awaitingStepIds
143
+ pendingStepId,
144
+ tableName = "AwaitingMultipleSteps",
145
+ byPendingTableName = "AwaitingMultipleStepByPending"
146
+ ) => {
147
+ _izContext.logger.debug("Lib:createAwaitingMultipleSteps", {
148
+ records: records,
149
+ pendingStepId: pendingStepId,
150
+ tableName: tableName,
151
+ byPendingTableName: byPendingTableName
152
+ });
153
+
154
+ // // 0001 ORG.
155
+ let promiseArray = []
156
+
157
+ for (let record of records) {
158
+
159
+ let attributesWhenCreate = {
160
+ awaitingStepId: record,
161
+ pendingStepId: pendingStepId,
162
+ timestamp: Date.now()
163
+ }
164
+ _izContext.logger.debug("putItem:AwaitingMultipleSteps +++++++", { awaitingStepId: attributesWhenCreate.awaitingStepId })
165
+ promiseArray.push(
166
+ dynamodbSharedLib.putItem(
167
+ _izContext,
168
+ await dynamodbSharedLib.tableName(_izContext, tableName),
169
+ attributesWhenCreate
170
+ )
171
+ );
172
+
173
+
174
+ _izContext.logger.debug("putItem:AwaitingMultipleStepByPending", { awaitingStepId: attributesWhenCreate.awaitingStepId })
175
+ promiseArray.push(
176
+ dynamodbSharedLib.putItem(
177
+ _izContext,
178
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
179
+ attributesWhenCreate
180
+ )
181
+ );
182
+
183
+ _izContext.logger.debug("check 0")
184
+
185
+ }
186
+ await Promise.all(promiseArray);
187
+
188
+ }
189
+
190
+ //====================================== AwaitingStep //======================================
191
+ // AwaitingStep is when a flow is awaiting one external flow, and will continue it's flow once that one external flow is complete
192
+ // One awaitingStepId can have multiple pendingStepIds that are awaiting it
193
+ // logic can prepend any prefix to awaitingStepId if want to be share one table and differentiate different external step ids
194
+
195
+ module.exports.createAwaitingStep = async (
196
+ _izContext,
197
+ awaitingStepId,
198
+ pendingStepId,
199
+ tableName = "AwaitingStep",
200
+ additionalAttributes = {} // save in top level
201
+ ) => {
202
+
203
+ let attributesWhenCreate = {
204
+ awaitingStepId: awaitingStepId,
205
+ pendingStepId: pendingStepId,
206
+ timestamp: Date.now(),
207
+ uniqueRequestId: _izContext.uniqueRequestId,
208
+ }
209
+ // loop if not empty
210
+ for (let additionalAttribute in additionalAttributes) {
211
+ attributesWhenCreate[additionalAttribute] = additionalAttributes[additionalAttribute]
212
+ }
213
+
214
+ await dynamodbSharedLib.putItem(
215
+ _izContext,
216
+ await dynamodbSharedLib.tableName(_izContext, tableName),
217
+ attributesWhenCreate
218
+ );
219
+ }
220
+
221
+
222
+
223
+
224
+ module.exports.findPendingStepIdsAwaitingStep = async (
225
+ _izContext,
226
+ awaitingStepId,
227
+ tableName = "AwaitingStep"
228
+ ) => {
229
+
230
+ let pendingStepIds = [];
231
+
232
+ let listPendingStepIds = await dynamodbSharedLib.query(
233
+ _izContext,
234
+ await dynamodbSharedLib.tableName(_izContext, tableName),
235
+ {
236
+ awaitingStepId: awaitingStepId
237
+ }
238
+ );
239
+
240
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
241
+ pendingStepIds.push(listPendingStepIds.Items[idx].pendingStepId)
242
+ }
243
+
244
+ return pendingStepIds
245
+ }
246
+
247
+ module.exports.findPendingStepsAwaitingStep = async (
248
+ _izContext,
249
+ awaitingStepId,
250
+ tableName = "AwaitingStep"
251
+ ) => {
252
+
253
+ let listPendingSteps = await dynamodbSharedLib.query(
254
+ _izContext,
255
+ await dynamodbSharedLib.tableName(_izContext, tableName),
256
+ {
257
+ awaitingStepId: awaitingStepId
258
+ }
259
+ );
260
+ _izContext.logger.debug("Record of awaitingStepId", listPendingSteps)
261
+ return listPendingSteps.Items;
262
+ }
263
+
264
+
265
+
266
+ module.exports.checkAllAwaitingStepsFinished = async (
267
+ _izContext,
268
+ pendingStepId,
269
+ currentAwaitingStepId = null,
270
+ tableName = 'AwaitingMultipleSteps',
271
+ byPendingTableName = "AwaitingMultipleStepByPending"
272
+ // indexName = ''
273
+ ) => {
274
+ _izContext.logger.debug("Lib:checkAllAwaitingStepsFinished", {
275
+ pendingStepId: pendingStepId,
276
+ currentAwaitingStepId: currentAwaitingStepId,
277
+ tableName: tableName,
278
+ byPendingTableName: byPendingTableName
279
+ });
280
+
281
+ if (currentAwaitingStepId) {
282
+
283
+ //* not need to save complete in AwaitingMultipleSteps Table but save in AwaitingMultipleStepByPending
284
+ // await dynamodbSharedLib.updateItem(
285
+ // _izContext,
286
+ // dynamodbSharedLib.tableName(tableName),
287
+ // {
288
+ // awaitingStepId: currentAwaitingStepId,
289
+ // pendingStepId: pendingStepId
290
+ // },
291
+ // {
292
+ // complete: true
293
+ // },
294
+ // )
295
+
296
+ _izContext.logger.debug(`---- update ${byPendingTableName} table ----`);
297
+ await dynamodbSharedLib.updateItem(
298
+ _izContext,
299
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
300
+ {
301
+ pendingStepId: pendingStepId,
302
+ awaitingStepId: currentAwaitingStepId
303
+ },
304
+ {
305
+ complete: true
306
+ },
307
+ );
308
+
309
+ }
310
+ let listPendingStepIds = await dynamodbSharedLib.query(
311
+ _izContext,
312
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
313
+ {
314
+ pendingStepId: pendingStepId
315
+ },
316
+ {
317
+ limit: 50 // arbritrary limit so not pull too many results, need >2 because some others might be marked as "complete" = true
318
+ }
319
+ );
320
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
321
+
322
+ if (currentAwaitingStepId && listPendingStepIds.Items[idx].awaitingStepId == currentAwaitingStepId) {
323
+ continue;
324
+ }
325
+ // if any record found not set to complete then there are steps not yet finished
326
+ if (!listPendingStepIds.Items[idx].complete) {
327
+ return false;
328
+ }
329
+ }
330
+
331
+ return true;
332
+
333
+
334
+ // // mark currentAwaitingStepId as complete, this is done to protect against race condition in between checking all complete
335
+ // // and then removing the record, we need to let other requests know this record is complete and need to do this before
336
+ // // querying all remaining records, otherwise if have only two remaining requests they might both show as records remaining
337
+ // // (seeing each other) and not continue flow, then each deletes their record and no records remain to trigger next step.
338
+ // // can result in race condition where multiple requests see all records finished, code has to account for this, but should already do so for idempotence
339
+ // // add query that updates the currentAwaitingStepId to have attribute "complete" = true
340
+ // if (currentAwaitingStepId) {
341
+ // await dynamodbSharedLib.updateItem(
342
+ // _izContext,
343
+ // dynamodbSharedLib.tableName(tableName),
344
+ // {
345
+ // awaitingStepId: currentAwaitingStepId,
346
+ // pendingStepId: pendingStepId
347
+ // },
348
+ // {
349
+ // complete: true
350
+ // },
351
+ // )
352
+ // }
353
+ // let listPendingStepIds = await dynamodbSharedLib.query(
354
+ // _izContext,
355
+ // dynamodbSharedLib.tableName(tableName),
356
+ // {
357
+ // pendingStepId: pendingStepId
358
+ // },
359
+ // {
360
+ // indexName: dynamodbSharedLib.tableName(tableName + 'Index'),
361
+ // limit: 50 // arbritrary limit so not pull too many results, need >2 because some others might be marked as "complete" = true
362
+ // }
363
+ // );
364
+ // for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
365
+
366
+ // if (currentAwaitingStepId && listPendingStepIds.Items[idx].awaitingStepId == currentAwaitingStepId) {
367
+ // continue;
368
+ // }
369
+ // // if any record found not set to complete then there are steps not yet finished
370
+ // if (!listPendingStepIds.Items[idx].complete) {
371
+ // return false;
372
+ // }
373
+ // }
374
+
375
+ // return true;
376
+ }
377
+
378
+ module.exports.clearAllAwaitingSteps = async (
379
+ _izContext,
380
+ pendingStepId,
381
+ tableName = 'AwaitingMultipleSteps',
382
+ byPendingTableName = "AwaitingMultipleStepByPending"
383
+ ) => {
384
+ _izContext.logger.debug("Lib:clearAllAwaitingSteps", {
385
+ pendingStepId: pendingStepId,
386
+ tableName: tableName,
387
+ byPendingTableName: byPendingTableName
388
+ })
389
+
390
+ tableName = await dynamodbSharedLib.tableName(_izContext, tableName)
391
+
392
+ // ----- query items from GSI table
393
+ let listPendingStepIds = await dynamodbSharedLib.query(
394
+ _izContext,
395
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
396
+ {
397
+ pendingStepId: pendingStepId
398
+ },
399
+ );
400
+ _izContext.logger.debug('listPendingStepIds', listPendingStepIds);
401
+
402
+
403
+ let promiseArray = []
404
+
405
+ for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
406
+
407
+ let keyValues = {
408
+ awaitingStepId: listPendingStepIds.Items[idx].awaitingStepId,
409
+ pendingStepId: listPendingStepIds.Items[idx].pendingStepId
410
+ }
411
+ _izContext.logger.debug('deleting ... ', keyValues);
412
+
413
+ _izContext.logger.debug(`delete item in dynamodb AwaitingMultipleSteps ==> awaitingStepId:${keyValues.awaitingStepId}`)
414
+ promiseArray.push(
415
+ dynamodbSharedLib.deleteItem(
416
+ _izContext,
417
+ tableName,
418
+ keyValues
419
+ ));
420
+ _izContext.logger.debug(`delete item in dynamodb AwaitingMultipleStepByPending ==> awaitingStepId:${keyValues.awaitingStepId}`)
421
+ promiseArray.push(
422
+ dynamodbSharedLib.deleteItem(
423
+ _izContext,
424
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
425
+ keyValues
426
+ ));
427
+
428
+ }
429
+
430
+ await Promise.all(promiseArray);
431
+
432
+ return true;
433
+ }
434
+
435
+
436
+ module.exports.removeAwaitingStep = async (
437
+ _izContext,
438
+ awaitingStepId,
439
+ pendingStepId,
440
+ tableName = "AwaitingStep"
441
+ ) => {
442
+
443
+ await dynamodbSharedLib.deleteItem(
444
+ _izContext,
445
+ await dynamodbSharedLib.tableName(_izContext, tableName),
446
+ {
447
+ awaitingStepId: awaitingStepId,
448
+ pendingStepId: pendingStepId
449
+ }
450
+ );
451
+ }
452
+
453
+ module.exports.removeAwaitingMultipleStep = async (
454
+ _izContext,
455
+ awaitingStepId,
456
+ pendingStepId,
457
+ tableName = "AwaitingMultipleSteps",
458
+ byPendingTableName = "AwaitingMultipleStepByPending"
459
+ ) => {
460
+ _izContext.logger.debug("Lib:removeAwaitingMultipleStep", {
461
+ awaitingStepId: awaitingStepId,
462
+ pendingStepId: pendingStepId,
463
+ tableName: tableName,
464
+ byPendingTableName: byPendingTableName
465
+ })
466
+
467
+ let promiseArray = []
468
+
469
+ let keyValues = {
470
+ awaitingStepId: awaitingStepId,
471
+ pendingStepId: pendingStepId
472
+ }
473
+ _izContext.logger.debug('deleting ... ', keyValues);
474
+
475
+ _izContext.logger.debug(`delete item in dynamodb AwaitingMultipleSteps ==> awaitingStepId:${keyValues.awaitingStepId}`)
476
+ promiseArray.push(
477
+ dynamodbSharedLib.deleteItem(
478
+ _izContext,
479
+ await dynamodbSharedLib.tableName(_izContext, tableName),
480
+ keyValues
481
+ ));
482
+
483
+ _izContext.logger.debug(`delete item in dynamodb AwaitingMultipleStepByPending ==> awaitingStepId:${keyValues.awaitingStepId}`)
484
+ promiseArray.push(
485
+ dynamodbSharedLib.deleteItem(
486
+ _izContext,
487
+ await dynamodbSharedLib.tableName(_izContext, byPendingTableName),
488
+ keyValues
489
+ ));
490
+
491
+ await Promise.all(promiseArray);
492
+
493
+ }
494
+
495
+ // only works with AwaitingStep, does not work with AwaitingMultipleSteps, need to make new function that checks conditional pass? If pass delete from byPending table too
496
+ module.exports.removeAwaitingStepWithCheckUniqueRequestId = async (
497
+ _izContext,
498
+ awaitingStepId,
499
+ pendingStepId,
500
+ checkUniqueRequestId,
501
+ prefix = "",
502
+ tableName = "AwaitingStep"
503
+ ) => {
504
+
505
+ // if logicalElements add queryElements.logicalElements
506
+ // set condition uniqueRequestId match with exists
507
+ const queryElementsCondition = {
508
+ additionalAttributes: {
509
+ checkUniqueRequestId: checkUniqueRequestId,
510
+ },
511
+ logicalElements: [
512
+ {
513
+ type: "logical",
514
+ comparison: "equals",
515
+ lhsAttribute: this.createFieldNameUniqueRequestId(prefix),
516
+ rhsReference: "checkUniqueRequestId"
517
+ },
518
+ ]
519
+ }
520
+
521
+ await dynamodbSharedLib.deleteItem(
522
+ _izContext,
523
+ await dynamodbSharedLib.tableName(_izContext, tableName),
524
+ {
525
+ awaitingStepId: awaitingStepId,
526
+ pendingStepId: pendingStepId
527
+ },
528
+ queryElementsCondition,
529
+ {
530
+ errorOnConditionalExpNotPass: false
531
+ }
532
+ );
533
+ }
534
+
535
+
536
+
537
+ //====================================== end AwaitingStep
538
+
539
+
540
+
541
+
542
+
543
+ /**
544
+ * Checks if record has uniqueRequestId set, if not set to this requests uniqueRequestId and continue to process
545
+ * if already set check if matches this request uniqueRequestId, if yes continue to process, if not then stop processing
546
+ * status: "process"|"stop"|"recordNotFound"
547
+
548
+ * if unable to set uniqueRequestId (because other thread updated it) throw NoRetryError
549
+ * conditional update existingRecord set uniqueRequestId = _izContext.uniqueRequestId
550
+ * conditional check uniqueRequestId still not exist in case another thread added
551
+ * if conditional not pass "continue" and try again
552
+
553
+ * @returns [string] [returnStatus, existingRecord]
554
+ */
555
+ module.exports.checkUniqueRequestProcessing = async (
556
+ _izContext,
557
+ tableName,
558
+ primaryKey,
559
+ prefix = '',
560
+ overwriteUniqueRequestId = null,
561
+ uniqueRequestIdFieldName = "UniqueRequestId" // if set will check/add this fieldname with _izContext.uniqueRequestId value
562
+ ) => {
563
+ try {
564
+
565
+ _izContext.logger.debug('current uniqueRequestId:', _izContext.uniqueRequestId)
566
+
567
+ let uniqueRequestId = _izContext.uniqueRequestId;
568
+ if (overwriteUniqueRequestId) {
569
+ uniqueRequestId = overwriteUniqueRequestId;
570
+ }
571
+
572
+ // add prefix
573
+ uniqueRequestIdFieldName = this.createFieldNameUniqueRequestId(
574
+ prefix,
575
+ uniqueRequestIdFieldName
576
+ );
577
+
578
+ //loop 3 times incase status changes, on 3rd iteration throw error (so checks 2 times)
579
+ for (let i = 1; i <= 3; i++) {
580
+ if (i == 3) {
581
+ throw new NoRetryError(`unable to set uniqueRequestId in table ${tableName}, record`, primaryKey);
582
+ }
583
+ //* get record from dynamo
584
+ let existingRecord = await dynamodbSharedLib.getItem(
585
+ _izContext,
586
+ await dynamodbSharedLib.tableName(_izContext, tableName),
587
+ primaryKey,
588
+ )
589
+ _izContext.logger.debug('existingRecord:', existingRecord);
590
+
591
+ let returnStatus = "process";
592
+
593
+ if (!existingRecord) {
594
+ // return ["recordNotFound", null]; // cannot return null, js cannot found object and throw error and dont know where it is?
595
+ return ["recordNotFound", {}];
596
+
597
+ } else if (!existingRecord[uniqueRequestIdFieldName]) {
598
+ // uniqueRequestId not yet exist
599
+
600
+ let updateAttributes = [
601
+ {
602
+ attributeName: uniqueRequestIdFieldName,
603
+ action: "set",
604
+ value: _izContext.uniqueRequestId,
605
+ },
606
+ ]
607
+ //* conditional check uniqueRequestId still not exist in case another thread added
608
+ const queryElementsWhenUpdate = {
609
+ logicalElements: [
610
+ {
611
+ type: "function",
612
+ function: "attribute_not_exists",
613
+ attribute: uniqueRequestIdFieldName,
614
+ }
615
+ ],
616
+ returnValues: 'ALL_NEW'
617
+ }
618
+ // // ----- main query ---------
619
+ let updateRecord = null;
620
+ try {
621
+ updateRecord = await dynamodbSharedLib.updateItem(
622
+ _izContext,
623
+ await dynamodbSharedLib.tableName(_izContext, tableName),
624
+ primaryKey,
625
+ updateAttributes,
626
+ queryElementsWhenUpdate,
627
+ { complexAttributes: true }, // force to complex
628
+ )
629
+
630
+ } catch (err) { // catch error when put and handle errors.
631
+ _izContext.logger.debug('updateItem storedCache expired err', err);
632
+ if (err.name == 'ConditionalCheckFailedException') {
633
+ continue;
634
+ } else { // e.g. ProvisionedThroughputExceededException
635
+ // throw new Error('Unhandled Error when putItem', err) // TT, if throw will stop
636
+ throw (err) // TT, if throw will stop
637
+ }
638
+ } //end catch err
639
+
640
+ } else if (existingRecord[uniqueRequestIdFieldName] !== uniqueRequestId) {
641
+
642
+ // when another request/uniqueRequestId need to stop process
643
+ returnStatus = "stop";
644
+ }
645
+ // if existingRecord[uniqueRequestIdFieldName] == uniqueRequestId then return "process"
646
+
647
+ return [returnStatus, existingRecord];
648
+
649
+ } // end loop 2 times
650
+ //should never get here
651
+ throw new NoRetryError('unexpected logic flow in checkUniqueRequestProcessing');
652
+
653
+ } catch (err) {
654
+ _izContext.logger.error('ERROR checkUniqueRequestProcessing:', err)
655
+ throw (err)
656
+ }
657
+ };
658
+
659
+ // ----- Helper funtion, common use case in triggerFlow ---------
660
+ module.exports.checkTimeCacheComplete = async (
661
+ _izContext,
662
+ fullMainTableName,
663
+ keyValues,
664
+ timeCacheComplete,
665
+ prefix
666
+ ) => {
667
+ let [returnValue, newTimeCacheComplete, uniqueRequestId] = await this.checkAndGetTimeCacheComplete(
668
+ _izContext,
669
+ fullMainTableName,
670
+ keyValues,
671
+ timeCacheComplete,
672
+ prefix
673
+ );
674
+ return [returnValue, uniqueRequestId];
675
+ }
676
+ module.exports.checkAndGetTimeCacheComplete = async (
677
+ _izContext,
678
+ fullMainTableName,
679
+ keyValues,
680
+ timeCacheComplete,
681
+ prefix
682
+ ) => {
683
+ _izContext.logger.debug('event checkTimeCacheComplete', {
684
+ fullMainTableName: fullMainTableName,
685
+ keyValues: keyValues,
686
+ timeCacheComplete: timeCacheComplete,
687
+ prefix: prefix
688
+ });
689
+
690
+ let uniqueRequestIdFieldName = triggeredCacheSharedLib.createFieldNameUniqueRequestId(prefix);
691
+ let cacheCompleteFieldName = triggeredCacheSharedLib.createFieldNameCacheComplete(prefix);
692
+ let statusFieldName = triggeredCacheSharedLib.createFieldNameStatus(prefix);
693
+
694
+ let getTimeCacheComplete = await dynamodbSharedLib.getItem(
695
+ _izContext,
696
+ fullMainTableName,
697
+ keyValues
698
+ )
699
+ _izContext.logger.debug(`getTimeCacheComplete from ${fullMainTableName}: `, getTimeCacheComplete);
700
+
701
+ if (!getTimeCacheComplete) {
702
+ throw new NoRetryError(`cannot getTimeCacheComplete from ${fullMainTableName}`);
703
+ }
704
+
705
+ // check if exist and match with initail create not changing in flow.
706
+ if (!getTimeCacheComplete[uniqueRequestIdFieldName]
707
+ // || !getTimeCacheComplete[cacheCompleteFieldName]
708
+ || (getTimeCacheComplete[cacheCompleteFieldName]
709
+ && getTimeCacheComplete[cacheCompleteFieldName] !== timeCacheComplete
710
+ && getTimeCacheComplete[statusFieldName] != "complete")
711
+ ) {
712
+ return [false, getTimeCacheComplete[cacheCompleteFieldName]];
713
+ } else {
714
+ return [true, getTimeCacheComplete[cacheCompleteFieldName], getTimeCacheComplete[uniqueRequestIdFieldName]];
715
+ }
716
+
717
+
718
+ }
719
+
720
+
721
+ // ----- shared both stored cache and triggered cache, check uniqueRequestId changed ---------
722
+ module.exports.checkCacheUniqueRequestId = async (
723
+ _izContext,
724
+ fullMainTableName,
725
+ keyValues,
726
+ checkUniqueRequestId,
727
+ uniqueRequestIdCompleteFieldName
728
+ ) => {
729
+
730
+ let cacheObject = await dynamodbSharedLib.getItem(
731
+ _izContext,
732
+ fullMainTableName,
733
+ keyValues
734
+ )
735
+ _izContext.logger.debug('cacheObject', cacheObject);
736
+
737
+ if (!cacheObject[uniqueRequestIdCompleteFieldName] || cacheObject[uniqueRequestIdCompleteFieldName] !== checkUniqueRequestId) {
738
+ return false
739
+ } else {
740
+ return true
741
+ }
742
+ }
743
+
744
+
745
+
746
+
747
+
748
+
749
+ // use shared findPendingStepIdsAwaitingStep
750
+ // module.exports.findPendingStepIdsAwaitingStepM = async (
751
+ // _izContext,
752
+ // awaitingStepId,
753
+ // tableName = "AwaitingMultipleSteps"
754
+ // ) => {
755
+
756
+ // let pendingStepIds = [];
757
+
758
+ // let listPendingStepIds = await dynamodbSharedLib.query(
759
+ // _izContext,
760
+ // dynamodbSharedLib.tableName(tableName),
761
+ // {
762
+ // awaitingStepId: awaitingStepId
763
+ // }
764
+ // );
765
+
766
+ // for (let idx = 0; idx < listPendingStepIds.Items.length; idx++) {
767
+ // pendingStepIds.push(listPendingStepIds.Items[idx].pendingStepId)
768
+ // }
769
+
770
+ // return pendingStepIds
771
+ // }
772
+
773
+
774
+
775
+
776
+ // use shared removeAwaitingStep
777
+ // module.exports.removeAwaitingStep = async (
778
+ // _izContext,
779
+ // awaitingStepId,
780
+ // pendingStepId,
781
+ // tableName = "AwaitingMultipleSteps"
782
+ // ) => {
783
+
784
+ // await dynamodbSharedLib.deleteItem(
785
+ // _izContext,
786
+ // dynamodbSharedLib.tableName(tableName),
787
+ // {
788
+ // awaitingStepId: awaitingStepId,
789
+ // pendingStepId: pendingStepId
790
+ // }
791
+ // );
792
+ // }
793
+
794
+
795
+ //====================================== end AwaitingMultipleSteps
796
+
797
+
798
+
799
+ //====================================== end Multiple Lambda Invocations (Logic pagination of handling results)
800
+
801
+ module.exports.validateStartKeyParam = (
802
+ _izContext,
803
+ startKey,
804
+ partitionKeyFieldName,
805
+ sortKeyFieldName = null
806
+ ) => {
807
+
808
+ if (startKey && Object.keys(startKey).length != 0) {
809
+ if (
810
+ startKey.constructor !== Object ||
811
+ !startKey[partitionKeyFieldName]
812
+ ) {
813
+ startKey = false; // we can type check for false to see if invalid
814
+ }
815
+ if (sortKeyFieldName !== null && sortKeyFieldName !== undefined && !startKey[sortKeyFieldName]) {
816
+ startKey = false;
817
+ }
818
+ } else {
819
+ startKey = null; // if empty set to null so lib functions process correctly (not want empty object)
820
+ }
821
+
822
+ return startKey;
823
+ }
824
+
825
+
826
+ module.exports.validateMultipleInvocations = async (
827
+ _izContext,
828
+ messageProperty,
829
+ passOnStartKey = {},
830
+ numberInvocation,
831
+ queueName
832
+ ) => {
833
+ _izContext.logger.debug("Lib:validateMultipleInvocations", {
834
+ messageProperty: messageProperty,
835
+ passOnStartKey: passOnStartKey,
836
+ numberInvocation: numberInvocation,
837
+ queueName: queueName
838
+ });
839
+ try {
840
+ const maxProcessInvocationCountImport = 20
841
+ if (numberInvocation >= maxProcessInvocationCountImport) {
842
+ _izContext.logger.error("numberInvocation exceeds maxProcessInvocationCountImport limit");
843
+ throw new NoRetryError("numberInvocation exceeds maxProcessInvocationCountImport limit");
844
+ };
845
+
846
+ if (passOnStartKey) {
847
+ messageProperty["startKey"] = passOnStartKey
848
+ };
849
+ messageProperty["numberInvocation"] = numberInvocation;
850
+
851
+ let messageReInvokeFunction = {
852
+ MessageBody: JSON.stringify(
853
+ messageProperty
854
+ ),
855
+ QueueUrl: await sqsSharedLib.sqsQueueUrl(_izContext, queueName)
856
+ };
857
+ _izContext.logger.debug(`Send mesage to Dsq:${queueName} `, messageReInvokeFunction);
858
+ await sqs.sendMessage(_izContext, messageReInvokeFunction);
859
+
860
+
861
+ } catch (err) {
862
+ throw (err)
863
+ }
864
+ }