@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.
- package/index.js +9 -5
- package/package.json +1 -1
- package/src/asyncFlowSharedLib.js +507 -0
- package/src/awaitingMultipleSteps.js +568 -0
- package/src/awaitingStep.js +244 -0
- package/src/AsyncFlowSharedLib.js +0 -1311
package/index.js
CHANGED
|
@@ -15,10 +15,14 @@ You should have received a copy of the GNU Affero General Public License
|
|
|
15
15
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
'use strict';
|
|
19
|
-
|
|
20
18
|
// Import the Asynchronous Flow shared library functions
|
|
21
|
-
import asyncFlowSharedLib from './src/
|
|
19
|
+
import * as asyncFlowSharedLib from './src/asyncFlowSharedLib.js';
|
|
20
|
+
import * as awaitingStep from './src/awaitingStep.js';
|
|
21
|
+
import * as awaitingMultipleSteps from './src/awaitingMultipleSteps.js';
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
export default
|
|
23
|
+
// Combine all functions from the imported modules into a single export object.
|
|
24
|
+
export default {
|
|
25
|
+
...asyncFlowSharedLib,
|
|
26
|
+
...awaitingStep,
|
|
27
|
+
...awaitingMultipleSteps
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,507 @@
|
|
|
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
|
+
// Core libraries
|
|
19
|
+
import { NoRetryError } from '@izara_project/izara-core-library-core';
|
|
20
|
+
import Logger from '@izara_project/izara-core-library-logger';
|
|
21
|
+
|
|
22
|
+
// Data and service interaction libraries
|
|
23
|
+
import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
|
|
24
|
+
import sqsSharedLib from '@izara_project/izara-core-library-sqs';
|
|
25
|
+
|
|
26
|
+
// External service interactions
|
|
27
|
+
import { sqs } from '@izara_project/izara-core-library-external-request';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a field name for storing the unique-request id, with optional prefix.
|
|
31
|
+
* @param {string} prefix
|
|
32
|
+
* @param {string} [uniqueRequestIdFieldName='UniqueRequestId']
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function createFieldNameUniqueRequestId(
|
|
36
|
+
prefix,
|
|
37
|
+
uniqueRequestIdFieldName = 'UniqueRequestId'
|
|
38
|
+
) {
|
|
39
|
+
return prefix + uniqueRequestIdFieldName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a concatenated awaitingStepId from composite parts.
|
|
44
|
+
* @param {string} partitionKey
|
|
45
|
+
* @param {string|number} sortId
|
|
46
|
+
* @param {string} [prefix=""]
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function createConcatenatedAwaitingStepId(
|
|
50
|
+
partitionKey,
|
|
51
|
+
sortId,
|
|
52
|
+
prefix = ''
|
|
53
|
+
) {
|
|
54
|
+
return prefix + partitionKey + '_' + sortId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a concatenated pendingStepId from parent/child ids.
|
|
59
|
+
* @param {string} parentId
|
|
60
|
+
* @param {string} childId
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function createConcatenatedPendingStepId(parentId, childId) {
|
|
64
|
+
return parentId + '_' + childId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a simple awaitingStepId (prefix + partitionKey).
|
|
69
|
+
* @param {string} partitionKey
|
|
70
|
+
* @param {string} [prefix=""]
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function createAwaitingStepId(partitionKey, prefix = '') {
|
|
74
|
+
return prefix + partitionKey;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a simple pendingStepId (prefix + identifierId).
|
|
79
|
+
* @param {string} identifierId
|
|
80
|
+
* @param {string} [prefix=""]
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
export function createPendingStepId(identifierId, prefix = '') {
|
|
84
|
+
return prefix + identifierId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove the known prefix from a pendingStepId and validate it is not equal to the prefix.
|
|
89
|
+
* If prefix is empty, the original ID is returned.
|
|
90
|
+
* @param {string} pendingStepId
|
|
91
|
+
* @param {string} [prefix=""]
|
|
92
|
+
* @returns {string}
|
|
93
|
+
* @throws {NoRetryError} If the stripped value equals the prefix (invalid)
|
|
94
|
+
*/
|
|
95
|
+
export function explodePendingStepId(pendingStepId, prefix = '') {
|
|
96
|
+
if (prefix === '') {
|
|
97
|
+
return pendingStepId;
|
|
98
|
+
} else {
|
|
99
|
+
let identifierId = pendingStepId.slice(prefix.length);
|
|
100
|
+
// ** validate identifierId == prefix
|
|
101
|
+
if (identifierId == prefix) {
|
|
102
|
+
throw new NoRetryError('IdentifierId should not be like prefix.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Logger.debug('return explode:', identifierId);
|
|
106
|
+
return identifierId;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// copy from izara-core-library-trigger-cache
|
|
111
|
+
/**
|
|
112
|
+
* Build the field name that stores "cache complete" marker.
|
|
113
|
+
* @param {string} [prefix='cache']
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
export function createFieldNameCacheComplete(prefix = 'cache') {
|
|
117
|
+
return prefix + 'CacheComplete';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the field name that stores a cache "status".
|
|
122
|
+
* @param {string} [prefix='cache']
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
export function createFieldNameStatus(prefix = 'cache') {
|
|
126
|
+
return prefix + 'Status';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Checks if record has uniqueRequestId set, if not set to this requests uniqueRequestId and continue to process
|
|
131
|
+
* if already set check if matches this request uniqueRequestId, if yes continue to process, if not then stop processing
|
|
132
|
+
* status: "process"|"stop"|"recordNotFound"
|
|
133
|
+
|
|
134
|
+
* if unable to set uniqueRequestId (because other thread updated it) throw NoRetryError
|
|
135
|
+
* conditional update existingRecord set uniqueRequestId = _izContext.uniqueRequestId
|
|
136
|
+
* conditional check uniqueRequestId still not exist in case another thread added
|
|
137
|
+
* if conditional not pass "continue" and try again
|
|
138
|
+
|
|
139
|
+
* @returns [string] [returnStatus, existingRecord]
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Coordinate unique-request processing over a record:
|
|
144
|
+
* - If no record: returns ["recordNotFound", {}]
|
|
145
|
+
* - If record exists but no uniqueRequestId: tries to set it via conditional update
|
|
146
|
+
* - If record has a different uniqueRequestId: returns ["stop", existingRecord]
|
|
147
|
+
* - If record has the same uniqueRequestId: returns ["process", existingRecord]
|
|
148
|
+
*
|
|
149
|
+
* Will loop up to 2 attempts to avoid races; throws on 3rd iteration.
|
|
150
|
+
*
|
|
151
|
+
* @async
|
|
152
|
+
* @param {IzContext} _izContext
|
|
153
|
+
* @param {string} tableName - Logical table name known by dynamodbSharedLib.tableName
|
|
154
|
+
* @param {DynamoKey} primaryKey - Primary key for the target record
|
|
155
|
+
* @param {string} [prefix=''] - Optional field-name prefix
|
|
156
|
+
* @param {string|null} [overwriteUniqueRequestId=null] - If provided, use this as the "current" unique request id
|
|
157
|
+
* @param {string} [uniqueRequestIdFieldName="UniqueRequestId"] - Base field name before prefix
|
|
158
|
+
* @returns {Promise<[UniqueRequestStatus, Attrs]>}
|
|
159
|
+
* @throws {NoRetryError} If unable to set uniqueRequestId after 2 tries
|
|
160
|
+
*/
|
|
161
|
+
export async function checkUniqueRequestProcessing(
|
|
162
|
+
_izContext,
|
|
163
|
+
tableName,
|
|
164
|
+
primaryKey,
|
|
165
|
+
prefix = '',
|
|
166
|
+
overwriteUniqueRequestId = null,
|
|
167
|
+
uniqueRequestIdFieldName = 'UniqueRequestId' // if set will check/add this fieldname with _izContext.uniqueRequestId value
|
|
168
|
+
) {
|
|
169
|
+
try {
|
|
170
|
+
_izContext.logger.debug(
|
|
171
|
+
'current uniqueRequestId:',
|
|
172
|
+
_izContext.uniqueRequestId
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
let uniqueRequestId = _izContext.uniqueRequestId;
|
|
176
|
+
if (overwriteUniqueRequestId) {
|
|
177
|
+
uniqueRequestId = overwriteUniqueRequestId;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// add prefix
|
|
181
|
+
uniqueRequestIdFieldName = createFieldNameUniqueRequestId(
|
|
182
|
+
prefix,
|
|
183
|
+
uniqueRequestIdFieldName
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
//loop 3 times incase status changes, on 3rd iteration throw error (so checks 2 times)
|
|
187
|
+
for (let i = 1; i <= 3; i++) {
|
|
188
|
+
if (i == 3) {
|
|
189
|
+
throw new NoRetryError(
|
|
190
|
+
`unable to set uniqueRequestId in table ${tableName}, record`,
|
|
191
|
+
primaryKey
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
//* get record from dynamo
|
|
195
|
+
let existingRecord = await dynamodbSharedLib.getItem(
|
|
196
|
+
_izContext,
|
|
197
|
+
await dynamodbSharedLib.tableName(_izContext, tableName),
|
|
198
|
+
primaryKey
|
|
199
|
+
);
|
|
200
|
+
_izContext.logger.debug('existingRecord:', existingRecord);
|
|
201
|
+
|
|
202
|
+
let returnStatus = 'process';
|
|
203
|
+
|
|
204
|
+
if (!existingRecord) {
|
|
205
|
+
// return ["recordNotFound", null]; // cannot return null, js cannot found object and throw error and dont know where it is?
|
|
206
|
+
return ['recordNotFound', {}];
|
|
207
|
+
} else if (!existingRecord[uniqueRequestIdFieldName]) {
|
|
208
|
+
// uniqueRequestId not yet exist
|
|
209
|
+
|
|
210
|
+
let updateAttributes = [
|
|
211
|
+
{
|
|
212
|
+
attributeName: uniqueRequestIdFieldName,
|
|
213
|
+
action: 'set',
|
|
214
|
+
value: _izContext.uniqueRequestId
|
|
215
|
+
}
|
|
216
|
+
];
|
|
217
|
+
//* conditional check uniqueRequestId still not exist in case another thread added
|
|
218
|
+
const queryElementsWhenUpdate = {
|
|
219
|
+
logicalElements: [
|
|
220
|
+
{
|
|
221
|
+
type: 'function',
|
|
222
|
+
function: 'attribute_not_exists',
|
|
223
|
+
attribute: uniqueRequestIdFieldName
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
returnValues: 'ALL_NEW'
|
|
227
|
+
};
|
|
228
|
+
// // ----- main query ---------
|
|
229
|
+
let updateRecord = null;
|
|
230
|
+
try {
|
|
231
|
+
updateRecord = await dynamodbSharedLib.updateItem(
|
|
232
|
+
_izContext,
|
|
233
|
+
await dynamodbSharedLib.tableName(_izContext, tableName),
|
|
234
|
+
primaryKey,
|
|
235
|
+
updateAttributes,
|
|
236
|
+
queryElementsWhenUpdate,
|
|
237
|
+
{ complexAttributes: true } // force to complex
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return [returnStatus, updateRecord];
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// catch error when put and handle errors.
|
|
243
|
+
_izContext.logger.debug('updateItem storedCache expired err', err);
|
|
244
|
+
if (err.name == 'ConditionalCheckFailedException') {
|
|
245
|
+
continue;
|
|
246
|
+
} else {
|
|
247
|
+
// e.g. ProvisionedThroughputExceededException
|
|
248
|
+
// throw new Error('Unhandled Error when putItem', err) // TT, if throw will stop
|
|
249
|
+
throw err; // TT, if throw will stop
|
|
250
|
+
}
|
|
251
|
+
} //end catch err
|
|
252
|
+
} else if (existingRecord[uniqueRequestIdFieldName] !== uniqueRequestId) {
|
|
253
|
+
// when another request/uniqueRequestId need to stop process
|
|
254
|
+
return ['stop', existingRecord];
|
|
255
|
+
} else if (existingRecord[uniqueRequestIdFieldName] == uniqueRequestId) {
|
|
256
|
+
// if existingRecord[uniqueRequestIdFieldName] == uniqueRequestId then return "process
|
|
257
|
+
return [returnStatus, existingRecord];
|
|
258
|
+
}
|
|
259
|
+
} // end loop 2 times
|
|
260
|
+
//should never get here
|
|
261
|
+
throw new NoRetryError(
|
|
262
|
+
'unexpected logic flow in checkUniqueRequestProcessing'
|
|
263
|
+
);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
_izContext.logger.error('ERROR checkUniqueRequestProcessing:', err);
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ----- Helper funtion, common use case in triggerFlow ---------
|
|
271
|
+
/**
|
|
272
|
+
* Convenience wrapper around checkAndGetTimeCacheComplete.
|
|
273
|
+
* Returns [isSameOrUnset, uniqueRequestIdWhenComplete].
|
|
274
|
+
* @async
|
|
275
|
+
* @param {IzContext} _izContext
|
|
276
|
+
* @param {string} fullMainTableName - Fully resolved table name (not logical)
|
|
277
|
+
* @param {DynamoKey} keyValues
|
|
278
|
+
* @param {number|string} timeCacheComplete - Caller’s expected cache mark
|
|
279
|
+
* @param {string} prefix - Field prefix used in cache fields
|
|
280
|
+
* @returns {Promise<[boolean, string|undefined]>}
|
|
281
|
+
*/
|
|
282
|
+
export async function checkTimeCacheComplete(
|
|
283
|
+
_izContext,
|
|
284
|
+
fullMainTableName,
|
|
285
|
+
keyValues,
|
|
286
|
+
timeCacheComplete,
|
|
287
|
+
prefix
|
|
288
|
+
) {
|
|
289
|
+
let [returnValue, newTimeCacheComplete, uniqueRequestId] =
|
|
290
|
+
await checkAndGetTimeCacheComplete(
|
|
291
|
+
_izContext,
|
|
292
|
+
fullMainTableName,
|
|
293
|
+
keyValues,
|
|
294
|
+
timeCacheComplete,
|
|
295
|
+
prefix
|
|
296
|
+
);
|
|
297
|
+
return [returnValue, uniqueRequestId];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Read cache fields from a record and compare cacheComplete value to caller’s `timeCacheComplete`.
|
|
302
|
+
* If stored differs (and is present), returns [false, storedValue].
|
|
303
|
+
* Otherwise returns [true, storedValue, uniqueRequestId].
|
|
304
|
+
* @async
|
|
305
|
+
* @param {IzContext} _izContext
|
|
306
|
+
* @param {string} fullMainTableName - Fully resolved table name (not logical)
|
|
307
|
+
* @param {DynamoKey} keyValues
|
|
308
|
+
* @param {number|string} timeCacheComplete
|
|
309
|
+
* @param {string} prefix
|
|
310
|
+
* @returns {Promise<[boolean, any, string|undefined]>}
|
|
311
|
+
* @throws {NoRetryError} If record cannot be fetched
|
|
312
|
+
*/
|
|
313
|
+
export async function checkAndGetTimeCacheComplete(
|
|
314
|
+
_izContext,
|
|
315
|
+
fullMainTableName,
|
|
316
|
+
keyValues,
|
|
317
|
+
timeCacheComplete,
|
|
318
|
+
prefix
|
|
319
|
+
) {
|
|
320
|
+
_izContext.logger.debug('event checkTimeCacheComplete', {
|
|
321
|
+
fullMainTableName: fullMainTableName,
|
|
322
|
+
keyValues: keyValues,
|
|
323
|
+
timeCacheComplete: timeCacheComplete,
|
|
324
|
+
prefix: prefix
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
let uniqueRequestIdFieldName = createFieldNameUniqueRequestId('cache');
|
|
328
|
+
let cacheCompleteFieldName = createFieldNameCacheComplete(prefix);
|
|
329
|
+
let statusFieldName = createFieldNameStatus(prefix);
|
|
330
|
+
|
|
331
|
+
let getTimeCacheComplete = await dynamodbSharedLib.getItem(
|
|
332
|
+
_izContext,
|
|
333
|
+
fullMainTableName,
|
|
334
|
+
keyValues
|
|
335
|
+
);
|
|
336
|
+
_izContext.logger.debug(
|
|
337
|
+
`getTimeCacheComplete from ${fullMainTableName}: `,
|
|
338
|
+
getTimeCacheComplete
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (!getTimeCacheComplete) {
|
|
342
|
+
throw new NoRetryError(
|
|
343
|
+
`cannot getTimeCacheComplete from ${fullMainTableName}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// check if exist and match with initail create not changing in flow.
|
|
348
|
+
// if (!getTimeCacheComplete[uniqueRequestIdFieldName]
|
|
349
|
+
// // || !getTimeCacheComplete[cacheCompleteFieldName]
|
|
350
|
+
// || (getTimeCacheComplete[cacheCompleteFieldName]
|
|
351
|
+
// && getTimeCacheComplete[cacheCompleteFieldName] !== timeCacheComplete
|
|
352
|
+
// && getTimeCacheComplete[statusFieldName] != "complete")
|
|
353
|
+
// ) {
|
|
354
|
+
// return [false, getTimeCacheComplete[cacheCompleteFieldName]];
|
|
355
|
+
// } else {
|
|
356
|
+
// return [true, getTimeCacheComplete[cacheCompleteFieldName], getTimeCacheComplete[uniqueRequestIdFieldName]];
|
|
357
|
+
// }
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
getTimeCacheComplete[cacheCompleteFieldName] &&
|
|
361
|
+
getTimeCacheComplete[cacheCompleteFieldName] !== timeCacheComplete
|
|
362
|
+
) {
|
|
363
|
+
return [false, getTimeCacheComplete[cacheCompleteFieldName]];
|
|
364
|
+
} else {
|
|
365
|
+
return [
|
|
366
|
+
true,
|
|
367
|
+
getTimeCacheComplete[cacheCompleteFieldName],
|
|
368
|
+
getTimeCacheComplete[uniqueRequestIdFieldName]
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ----- shared both stored cache and triggered cache, check uniqueRequestId changed ---------
|
|
374
|
+
/**
|
|
375
|
+
* Ensure the cache object’s "uniqueRequestIdCompleteFieldName" equals the given checkUniqueRequestId.
|
|
376
|
+
* @async
|
|
377
|
+
* @param {IzContext} _izContext
|
|
378
|
+
* @param {string} fullMainTableName
|
|
379
|
+
* @param {DynamoKey} keyValues
|
|
380
|
+
* @param {string} checkUniqueRequestId
|
|
381
|
+
* @param {string} uniqueRequestIdCompleteFieldName
|
|
382
|
+
* @returns {Promise<boolean>}
|
|
383
|
+
*/
|
|
384
|
+
export async function checkCacheUniqueRequestId(
|
|
385
|
+
_izContext,
|
|
386
|
+
fullMainTableName,
|
|
387
|
+
keyValues,
|
|
388
|
+
checkUniqueRequestId,
|
|
389
|
+
uniqueRequestIdCompleteFieldName
|
|
390
|
+
) {
|
|
391
|
+
let cacheObject = await dynamodbSharedLib.getItem(
|
|
392
|
+
_izContext,
|
|
393
|
+
fullMainTableName,
|
|
394
|
+
keyValues
|
|
395
|
+
);
|
|
396
|
+
_izContext.logger.debug('cacheObject', cacheObject);
|
|
397
|
+
|
|
398
|
+
if (
|
|
399
|
+
!cacheObject[uniqueRequestIdCompleteFieldName] ||
|
|
400
|
+
cacheObject[uniqueRequestIdCompleteFieldName] !== checkUniqueRequestId
|
|
401
|
+
) {
|
|
402
|
+
return false;
|
|
403
|
+
} else {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
//====================================== end Multiple Lambda Invocations (Logic pagination of handling results)
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Validate and normalize a DynamoDB pagination startKey object.
|
|
412
|
+
* Ensures partitionKeyFieldName/sortKeyFieldName are alphanumeric/underscore/hyphen.
|
|
413
|
+
* If startKey is empty object, returns `null` for easier processing downstream.
|
|
414
|
+
* @param {IzContext} _izContext
|
|
415
|
+
* @param {Attrs|null|undefined} startKey
|
|
416
|
+
* @param {string} partitionKeyFieldName
|
|
417
|
+
* @param {string} sortKeyFieldName
|
|
418
|
+
* @returns {Attrs|null}
|
|
419
|
+
* @throws {NoRetryError} On invalid field names or malformed startKey
|
|
420
|
+
*/
|
|
421
|
+
export function validateStartKeyParam(
|
|
422
|
+
_izContext,
|
|
423
|
+
startKey,
|
|
424
|
+
partitionKeyFieldName,
|
|
425
|
+
sortKeyFieldName
|
|
426
|
+
) {
|
|
427
|
+
const stringNotEmptyRegex = /^[A-Za-z0-9_-]+$/;
|
|
428
|
+
|
|
429
|
+
if (
|
|
430
|
+
!partitionKeyFieldName ||
|
|
431
|
+
!sortKeyFieldName ||
|
|
432
|
+
!stringNotEmptyRegex.test(partitionKeyFieldName) ||
|
|
433
|
+
!stringNotEmptyRegex.test(sortKeyFieldName)
|
|
434
|
+
) {
|
|
435
|
+
throw new NoRetryError(
|
|
436
|
+
'validateStartKeyParam: Invalid partitionKeyFieldName or sortKeyFieldName'
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (startKey && Object.keys(startKey).length != 0) {
|
|
441
|
+
if (!startKey[partitionKeyFieldName] || !startKey[sortKeyFieldName]) {
|
|
442
|
+
throw new NoRetryError(
|
|
443
|
+
'validateStartKeyParam: Invalid startKey, missing partitionKeyFieldName or sortKeyFieldName'
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
startKey = null; // if empty set to null so lib functions process correctly
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return startKey;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* For paginated multi-invocation flows: validates the current invocation count,
|
|
455
|
+
* attaches the (optional) next startKey to the message, increments the count,
|
|
456
|
+
* and re-enqueues the message to the specified SQS queue.
|
|
457
|
+
* @async
|
|
458
|
+
* @param {IzContext} _izContext
|
|
459
|
+
* @param {Attrs} messageProperty - The message body object to re-dispatch
|
|
460
|
+
* @param {Attrs} [passOnStartKey={}] - Optional startKey to pass along
|
|
461
|
+
* @param {number} numberInvocation - Current invocation count
|
|
462
|
+
* @param {string} queueName - Logical queue name resolvable via sqsSharedLib.sqsQueueUrl
|
|
463
|
+
* @returns {Promise<void>}
|
|
464
|
+
* @throws {NoRetryError} If `numberInvocation` exceeds internal safety limit
|
|
465
|
+
*/
|
|
466
|
+
export async function validateMultipleInvocations(
|
|
467
|
+
_izContext,
|
|
468
|
+
messageProperty,
|
|
469
|
+
passOnStartKey = {},
|
|
470
|
+
numberInvocation,
|
|
471
|
+
queueName
|
|
472
|
+
) {
|
|
473
|
+
_izContext.logger.debug('Lib:validateMultipleInvocations', {
|
|
474
|
+
messageProperty: messageProperty,
|
|
475
|
+
passOnStartKey: passOnStartKey,
|
|
476
|
+
numberInvocation: numberInvocation,
|
|
477
|
+
queueName: queueName
|
|
478
|
+
});
|
|
479
|
+
try {
|
|
480
|
+
const maxProcessInvocationCountImport = 20;
|
|
481
|
+
if (numberInvocation >= maxProcessInvocationCountImport) {
|
|
482
|
+
_izContext.logger.error(
|
|
483
|
+
'numberInvocation exceeds maxProcessInvocationCountImport limit'
|
|
484
|
+
);
|
|
485
|
+
throw new NoRetryError(
|
|
486
|
+
'numberInvocation exceeds maxProcessInvocationCountImport limit'
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (passOnStartKey) {
|
|
491
|
+
messageProperty['startKey'] = passOnStartKey;
|
|
492
|
+
}
|
|
493
|
+
messageProperty['numberInvocation'] = numberInvocation;
|
|
494
|
+
|
|
495
|
+
let messageReInvokeFunction = {
|
|
496
|
+
MessageBody: JSON.stringify(messageProperty),
|
|
497
|
+
QueueUrl: await sqsSharedLib.sqsQueueUrl(_izContext, queueName)
|
|
498
|
+
};
|
|
499
|
+
_izContext.logger.debug(
|
|
500
|
+
`Send mesage to Dsq:${queueName} `,
|
|
501
|
+
messageReInvokeFunction
|
|
502
|
+
);
|
|
503
|
+
await sqs.sendMessage(_izContext, messageReInvokeFunction);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
}
|