@izara_project/izara-core-library-lock-update 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.
@@ -0,0 +1,1078 @@
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 { NoRetryError, validator } from '@izara_project/izara-core-library-core';
21
+ import dynamodbSharedLib from '@izara_project/izara-core-library-dynamodb';
22
+ import errorHandlingSharedLib from '@izara_project/izara-core-library-error-handling';
23
+ import { sns } from '@izara_project/izara-core-library-external-request';
24
+ import { identifiersObject } from '@izara_project/izara-core-library-service-schemas';
25
+ import snsSharedLib from '@izara_project/izara-core-library-sns';
26
+ import { getObjectSchema } from '@izara_project/izara-core-library-service-schemas';
27
+ import graphSharedLib from '@izara_project/izara-core-library-graph-service';
28
+
29
+ import lockUpdateConstants from './constants/lockUpdateConstants.js';
30
+
31
+
32
+
33
+ function _createAjvValidationSchema(objectSchema) {
34
+ const properties = {};
35
+ const identifierFieldNames = new Set();
36
+
37
+ // Collect identifier field names
38
+ for (const identifier of objectSchema.identifiers) {
39
+ if (identifier.fieldName) {
40
+ identifierFieldNames.add(identifier.fieldName);
41
+ }
42
+
43
+ if (Array.isArray(identifier.fieldNames)) {
44
+ for (const fieldName of identifier.fieldNames) {
45
+ identifierFieldNames.add(fieldName);
46
+ }
47
+ }
48
+ }
49
+
50
+ // Create properties
51
+ for (const [fieldName, fieldSchema] of Object.entries(
52
+ objectSchema.fieldNames
53
+ )) {
54
+ if (identifierFieldNames.has(fieldName)) {
55
+ continue;
56
+ }
57
+
58
+ properties[fieldName] = {
59
+ type: fieldSchema.type
60
+ };
61
+ }
62
+
63
+ // Add identifier properties
64
+ for (const identifier of objectSchema.identifiers) {
65
+ if (identifier.name) {
66
+ properties[identifier.name] = {
67
+ type: 'string'
68
+ };
69
+ }
70
+
71
+ if (identifier.fieldName) {
72
+ properties[identifier.fieldName] = {
73
+ type: objectSchema.fieldNames[identifier.fieldName].type
74
+ };
75
+ }
76
+ }
77
+
78
+ return {
79
+ type: 'object',
80
+ properties,
81
+ additionalProperties: false
82
+ };
83
+ }
84
+
85
+ async function _handleConflictAndAddToQueue(
86
+ _izContext,
87
+ queueObjType,
88
+ queueIdentifiers,
89
+ flowType,
90
+ requestMessage,
91
+ lockTableName,
92
+ identifiersBase,
93
+ uniqueRequestId,
94
+ lockTag
95
+ ) {
96
+
97
+ _izContext.logger.debug(
98
+ 'Function: _handleConflictAndAddToQueue',
99
+ {
100
+ queueObjType,
101
+ queueIdentifiers,
102
+ flowType,
103
+ requestMessage,
104
+ lockTableName,
105
+ identifiersBase,
106
+ uniqueRequestId,
107
+ lockTag
108
+ }
109
+ );
110
+
111
+ const queueSchemaResult =
112
+ await getObjectSchema.getObjSchemaS3WithCache(
113
+ _izContext,
114
+ queueObjType
115
+ );
116
+
117
+ if (
118
+ queueSchemaResult.errorsFound.length > 0 ||
119
+ !queueSchemaResult.result
120
+ ) {
121
+ throw new NoRetryError(
122
+ JSON.stringify(queueSchemaResult.errorsFound)
123
+ );
124
+ }
125
+
126
+ const queueObjectSchema = queueSchemaResult.result;
127
+
128
+ const queueStorageResource = getDynamoDbStorageResource(
129
+ _izContext,
130
+ queueObjectSchema
131
+ );
132
+
133
+ const queueTableName = dynamodbSharedLib.tableName(
134
+ _izContext,
135
+ queueStorageResource.tableName,
136
+ queueStorageResource.serviceTag
137
+ );
138
+
139
+ const queueDynamoDbIdentifiers =
140
+ await identifiersObject.dynamoDbIdentifiersByStorageResource(
141
+ _izContext,
142
+ queueObjectSchema,
143
+ queueStorageResource,
144
+ queueIdentifiers
145
+ );
146
+
147
+ // Save the queue record only when overwriteUniqueRequestId is not provided.
148
+ // If overwriteUniqueRequestId is provided, the request is already being processed
149
+ // through the existing record, so skip creating a new queue record.
150
+ if (
151
+ !requestMessage.hasOwnProperty('overwriteUniqueRequestId') ||
152
+ requestMessage.overwriteUniqueRequestId === ''
153
+ ) {
154
+
155
+ const queueRecordObjInstance = {
156
+ ...queueDynamoDbIdentifiers,
157
+ flowType,
158
+ messageBody: requestMessage
159
+ };
160
+
161
+ const queueValidationSchema = _createAjvValidationSchema(
162
+ queueObjectSchema
163
+ );
164
+
165
+ const queueValidationResult = validator.validateObject(
166
+ queueValidationSchema,
167
+ queueRecordObjInstance
168
+ );
169
+
170
+ if (queueValidationResult.pass !== true) {
171
+ throw new NoRetryError(queueValidationResult.error);
172
+ }
173
+
174
+ const conditionCheckElements = {
175
+ logicalElements: dynamodbSharedLib.generateNotExistLogicalElements(
176
+ queueDynamoDbIdentifiers,
177
+ ),
178
+ returnValues: 'ALL_NEW'
179
+ };
180
+
181
+ await dynamodbSharedLib.putItem(
182
+ _izContext,
183
+ queueTableName,
184
+ queueRecordObjInstance,
185
+ conditionCheckElements
186
+ );
187
+ }
188
+
189
+ const {
190
+ fieldNameUniqueRequestId,
191
+ fieldNameLockStatus
192
+ } = createLockFieldNames(lockTag)
193
+
194
+ const queryElementsWhenAcquireLock = {
195
+ logicalElements: [
196
+ {
197
+ type: 'function',
198
+ function: 'attribute_not_exists',
199
+ attribute: fieldNameUniqueRequestId
200
+ }
201
+ ],
202
+ returnValues: 'ALL_NEW'
203
+ };
204
+
205
+ try {
206
+ const reCheckLock = await dynamodbSharedLib.updateItem(
207
+ _izContext,
208
+ lockTableName,
209
+ identifiersBase,
210
+ {
211
+ [fieldNameUniqueRequestId]: uniqueRequestId,
212
+ [fieldNameLockStatus]: true
213
+ },
214
+ queryElementsWhenAcquireLock,
215
+ {
216
+ errorOnConditionalExpNotPass: true
217
+ }
218
+ );
219
+
220
+ return ['process', reCheckLock];
221
+ } catch (error) {
222
+ // Another request still owns the lock.
223
+ return ['stop', null];
224
+ }
225
+ }
226
+
227
+
228
+ function getDynamoDbStorageResource(
229
+ _izContext,
230
+ objectSchema,
231
+ ) {
232
+ for (const storageResource of Object.values(
233
+ objectSchema.storageResources
234
+ )) {
235
+ if (storageResource.storageType !== 'dynamoDB') {
236
+ continue;
237
+ }
238
+
239
+ if (!storageResource.tableName) {
240
+ continue;
241
+ }
242
+
243
+ return storageResource;
244
+ }
245
+
246
+ throw new NoRetryError(
247
+ `DynamoDB storage resource with tableName not found for objType: `
248
+ + `${JSON.stringify(objectSchema.objectType)}`
249
+ );
250
+ }
251
+
252
+ function createLockFieldNames(lockTag) {
253
+ const fieldNameUniqueRequestId =
254
+ lockUpdateConstants.createUniqueRequestIdFieldName(lockTag);
255
+
256
+ const fieldNameLockStatus =
257
+ lockUpdateConstants.createLockStatusFieldName(lockTag);
258
+
259
+ return {
260
+ fieldNameUniqueRequestId,
261
+ fieldNameLockStatus
262
+ };
263
+ }
264
+
265
+
266
+ function getAddOnConfig(
267
+ _izContext,
268
+ mainObjectSchema,
269
+ ) {
270
+ let lockTag = null;
271
+ let storageResourceTag = null;
272
+
273
+ if (Array.isArray(mainObjectSchema.addOnDataStructure)) {
274
+ for (const addOnData of mainObjectSchema.addOnDataStructure) {
275
+ if (
276
+ addOnData.type !== 'reserveQuantity' &&
277
+ addOnData.type !== 'lockUpdate'
278
+ ) {
279
+ continue;
280
+ }
281
+
282
+ let currentLockUpdateTag = null;
283
+
284
+ if (addOnData.type === 'reserveQuantity') {
285
+ currentLockUpdateTag = addOnData.reserveQuantityTag;
286
+ }
287
+
288
+ if (addOnData.type === 'lockUpdate') {
289
+ currentLockUpdateTag = addOnData.lockUpdateTag;
290
+ }
291
+
292
+ if (!currentLockUpdateTag) {
293
+ throw new NoRetryError(`Missing lock update tag for add-on type '${addOnData.type}'`,);
294
+ }
295
+
296
+ lockTag = currentLockUpdateTag;
297
+ storageResourceTag = addOnData.storageResourceTag;
298
+
299
+ break;
300
+ }
301
+ }
302
+
303
+ if (!lockTag) {
304
+ throw new NoRetryError(`
305
+ Add-on type 'reserveQuantity' or 'lockUpdate' not found for objType '${JSON.stringify(mainObjectSchema.objectType)}'`,
306
+ );
307
+ }
308
+
309
+ return {
310
+ lockTag,
311
+ storageResourceTag
312
+ };
313
+ }
314
+
315
+
316
+
317
+
318
+
319
+ /**
320
+ * Checks the main object's lock status and determines whether to process,
321
+ * finalize, or queue the request.
322
+ *
323
+ * @param {Object} _izContext - Execution context.
324
+ * @param {Object} mainObjType - Main object type used to resolve storage configuration.
325
+ * @param {Object} identifiers - Identifiers of the main object record.
326
+ * @param {Object} queueIdentifiers - Identifiers used to construct the queue record's keys.
327
+ * @param {Object} flowType - Flow type used to identify the request's processing flow.
328
+ * @param {string} uniqueRequestId - Unique identifier for the request.
329
+ * @param {Object} requestMessage - Request message to enqueue when the lock is unavailable.
330
+ * @param {Object} errorParam - Found errors and service-level error tags.
331
+ * @returns {Promise<Array|Object>} Processing result and associated record
332
+ * data, or an object containing errorParam as foundErrors.
333
+ */
334
+ async function checkRequestWithLock(
335
+ _izContext,
336
+ mainObjType,
337
+ identifiers,
338
+ queueIdentifiers,
339
+ flowType,
340
+ uniqueRequestId,
341
+ requestMessage,
342
+ errorParams
343
+ ) {
344
+ try {
345
+ _izContext.logger.debug('Function: checkRequestWithLock', {
346
+ mainObjType,
347
+ identifiers,
348
+ queueIdentifiers,
349
+ flowType,
350
+ uniqueRequestId,
351
+ requestMessage
352
+ });
353
+
354
+ const schemaResult = await getObjectSchema.getObjSchemaS3WithCache(
355
+ _izContext,
356
+ mainObjType
357
+ );
358
+
359
+ if (schemaResult.errorsFound.length > 0 || !schemaResult.result) {
360
+ throw new NoRetryError(
361
+ JSON.stringify(schemaResult.errorsFound)
362
+ );
363
+ }
364
+
365
+ const mainObjectSchema = schemaResult.result;
366
+
367
+ const { lockTag, storageResourceTag } =
368
+ getAddOnConfig(
369
+ _izContext,
370
+ mainObjectSchema,
371
+ );
372
+
373
+ const mainObjectStorage = mainObjectSchema.storageResources[storageResourceTag];
374
+
375
+ const lockFieldNames = createLockFieldNames(lockTag);
376
+
377
+ const fieldNameUniqueRequestId = lockFieldNames.fieldNameUniqueRequestId;
378
+ const fieldNameLockStatus = lockFieldNames.fieldNameLockStatus;
379
+
380
+ const lockTableName = dynamodbSharedLib.tableName(
381
+ _izContext,
382
+ mainObjectStorage.tableName,
383
+ mainObjectStorage.serviceTag
384
+ );
385
+
386
+ const identifiersBase = await identifiersObject.identifiersBaseFromIdentifiers(
387
+ _izContext,
388
+ mainObjType,
389
+ identifiers
390
+ );
391
+
392
+ const lockRecord = await dynamodbSharedLib.getItem(
393
+ _izContext,
394
+ lockTableName,
395
+ identifiersBase
396
+ );
397
+
398
+ if (!lockRecord) {
399
+ throw new NoRetryError(
400
+ `Lock record not found. mainObjType: ${JSON.stringify(mainObjType)}, ` +
401
+ `identifiersBase: ${JSON.stringify(identifiersBase)}`
402
+ );
403
+ }
404
+
405
+ const lockUniqueRequestId = lockRecord[fieldNameUniqueRequestId];
406
+ const statusProcessing = lockRecord[fieldNameLockStatus];
407
+
408
+ _izContext.logger.debug('Lock status', {
409
+ lockRecord,
410
+ [fieldNameUniqueRequestId]: lockUniqueRequestId,
411
+ [fieldNameLockStatus]: statusProcessing,
412
+ uniqueRequestId
413
+ });
414
+
415
+ if (
416
+ lockUniqueRequestId === undefined &&
417
+ !statusProcessing
418
+ ) {
419
+
420
+ const validationErrors = graphSharedLib.checkPropertiesValidWithSchema(
421
+ _izContext,
422
+ mainObjectSchema,
423
+ {
424
+ [fieldNameUniqueRequestId]: uniqueRequestId,
425
+ [fieldNameLockStatus]: true
426
+ },
427
+ errorParams
428
+ );
429
+
430
+ if (validationErrors.length > 0) {
431
+ throw new NoRetryError(validationErrors.join('; '));
432
+ }
433
+
434
+ try {
435
+ const queryElementsWhenAcquireLock = {
436
+ additionalAttributes: {
437
+ currentLockStatus: false
438
+ },
439
+ logicalElements: [
440
+ {
441
+ type: 'function',
442
+ function: 'attribute_not_exists',
443
+ attribute: fieldNameUniqueRequestId
444
+ },
445
+ {
446
+ type: 'logicalOperator',
447
+ operator: 'and'
448
+ },
449
+ {
450
+ type: 'group',
451
+ elements: [
452
+ {
453
+ type: 'function',
454
+ function: 'attribute_not_exists',
455
+ attribute: fieldNameLockStatus
456
+ },
457
+ {
458
+ type: 'logicalOperator',
459
+ operator: 'or'
460
+ },
461
+ {
462
+ type: 'logical',
463
+ comparison: 'equals',
464
+ lhsAttribute: fieldNameLockStatus,
465
+ rhsReference: 'currentLockStatus'
466
+ }
467
+ ]
468
+ }
469
+ ],
470
+ returnValues: 'ALL_NEW'
471
+ };
472
+
473
+ const updatedLock = await dynamodbSharedLib.updateItem(
474
+ _izContext,
475
+ lockTableName,
476
+ identifiersBase,
477
+ {
478
+ [fieldNameUniqueRequestId]: uniqueRequestId,
479
+ [fieldNameLockStatus]: true
480
+ },
481
+ queryElementsWhenAcquireLock,
482
+ {
483
+ errorOnConditionalExpNotPass: true
484
+ }
485
+ );
486
+
487
+ return ['process', updatedLock];
488
+ } catch (error) {
489
+
490
+ if (error.message !== 'ConditionalExpNotPass') {
491
+ throw new NoRetryError(
492
+ 'updateItem failed: ' + error.message,
493
+ error
494
+ );
495
+ }
496
+
497
+ return await _handleConflictAndAddToQueue(
498
+ _izContext,
499
+ queueObjType,
500
+ queueIdentifiers,
501
+ flowType,
502
+ requestMessage,
503
+ lockTableName,
504
+ identifiersBase,
505
+ uniqueRequestId,
506
+ lockTag
507
+ );
508
+ }
509
+ } else if (lockUniqueRequestId === uniqueRequestId) {
510
+
511
+ if (statusProcessing === true) {
512
+ return ['process', lockRecord];
513
+ }
514
+
515
+ if (statusProcessing === false) {
516
+ return ['finalize', lockRecord];
517
+ }
518
+
519
+ throw new NoRetryError(
520
+ `Invalid processing status: ${statusProcessing}, identifiers: ${JSON.stringify(identifiers)}`,
521
+ );
522
+
523
+ } else {
524
+ return await _handleConflictAndAddToQueue(
525
+ _izContext,
526
+ queueTableName,
527
+ queueIdentifiers,
528
+ flowType,
529
+ requestMessage,
530
+ lockTableName,
531
+ identifiersBase,
532
+ uniqueRequestId,
533
+ fieldNameUniqueRequestId,
534
+ fieldNameLockStatus,
535
+ // errorParam
536
+ );
537
+ }
538
+ } catch (error) {
539
+ _izContext.logger.error('Error in checkRequestWithLock:', {
540
+ errorMessage: error.message
541
+ });
542
+
543
+ throw error;
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Updates the main object record only when the lock belongs to the current
549
+ * request, then releases the lock.
550
+ *
551
+ * @param {Object} _izContext - Execution context.
552
+ * @param {string} mainObjType - Main object type used to resolve storage configuration.
553
+ * @param {Object} identifiers - Identifiers of the main object record.
554
+ * @param {string} checkUniqueRequestId - Unique request ID used to validate the lock.
555
+ * @param {Object} additionalAttributes - Additional attributes to update on the main object record.
556
+ * @param {Object} errorParam - Found errors and service-level error tags.
557
+ * @returns {Promise<Object>} Updated main object record.
558
+ */
559
+ async function updateMainRecordWithLock(
560
+ _izContext,
561
+ mainObjType,
562
+ identifiers,
563
+ checkUniqueRequestId,
564
+ errorParam,
565
+ additionalAttributes = {},
566
+ ) {
567
+ const schemaResult = await getObjectSchema.getObjSchemaS3WithCache(
568
+ _izContext,
569
+ mainObjType
570
+ );
571
+
572
+ if (schemaResult.errorsFound.length > 0 || !schemaResult.result) {
573
+ throw new NoRetryError(
574
+ JSON.stringify(schemaResult.errorsFound)
575
+ );
576
+ }
577
+
578
+ const mainObjectSchema = schemaResult.result;
579
+
580
+ const {
581
+ lockTag,
582
+ storageResourceTag
583
+ } = getAddOnConfig(
584
+ _izContext,
585
+ mainObjectSchema
586
+ );
587
+
588
+ const mainObjectStorage =
589
+ mainObjectSchema.storageResources[storageResourceTag];
590
+
591
+ const mainTableName = dynamodbSharedLib.tableName(
592
+ _izContext,
593
+ mainObjectStorage.tableName,
594
+ mainObjectStorage.serviceTag
595
+ );
596
+
597
+ const {
598
+ fieldNameUniqueRequestId,
599
+ fieldNameLockStatus
600
+ } = createLockFieldNames(lockTag);
601
+
602
+ // Validate additional attributes and lock release together.
603
+ const propertiesToValidate = Object.assign(
604
+ {},
605
+ additionalAttributes,
606
+ {
607
+ [fieldNameLockStatus]: false
608
+ }
609
+ );
610
+
611
+ const validationErrors = graphSharedLib.checkPropertiesValidWithSchema(
612
+ _izContext,
613
+ mainObjectSchema,
614
+ propertiesToValidate,
615
+ errorParam
616
+ );
617
+
618
+ if (
619
+ Array.isArray(validationErrors) &&
620
+ validationErrors.length > 0
621
+ ) {
622
+ throw new NoRetryError(validationErrors.join('; '));
623
+ }
624
+
625
+ const updateAttributes = [];
626
+
627
+ for (const attributeName of Object.keys(propertiesToValidate)) {
628
+ updateAttributes.push({
629
+ attributeName: attributeName,
630
+ action: 'set',
631
+ value: propertiesToValidate[attributeName]
632
+ });
633
+ }
634
+
635
+ const queryElementsWhenUpdateMainRecord = {
636
+ additionalAttributes: {
637
+ currentUniqueRequestId: checkUniqueRequestId
638
+ },
639
+ logicalElements: [
640
+ {
641
+ type: 'logical',
642
+ comparison: 'equals',
643
+ lhsAttribute: fieldNameUniqueRequestId,
644
+ rhsReference: 'currentUniqueRequestId'
645
+ }
646
+ ],
647
+ returnValues: 'ALL_NEW'
648
+ };
649
+
650
+ try {
651
+ return await dynamodbSharedLib.updateItem(
652
+ _izContext,
653
+ mainTableName,
654
+ identifiers,
655
+ updateAttributes,
656
+ queryElementsWhenUpdateMainRecord,
657
+ {
658
+ complexAttributes: true,
659
+ errorOnConditionalExpNotPass: true
660
+ }
661
+ );
662
+ } catch (error) {
663
+ if (error.message === 'ConditionalExpNotPass') {
664
+ throw new NoRetryError(
665
+ 'Cannot update main record because lock does not belong to current request.'
666
+ );
667
+ }
668
+
669
+ throw error;
670
+ }
671
+ }
672
+
673
+
674
+ function getLockTagFromObjectType(objectType) {
675
+ const suffixes = ['History', 'Queue'];
676
+
677
+ for (const suffix of suffixes) {
678
+ if (objectType.endsWith(suffix)) {
679
+ return objectType.slice(0, -suffix.length);
680
+ }
681
+ }
682
+
683
+ throw new NoRetryError(`Invalid lock objectType: ${objectType}`);
684
+ }
685
+
686
+ /**
687
+ * Ensures a lock history record exists for the supplied identifiers.
688
+ *
689
+ * Creates a history record if it does not exist.
690
+ * Returns the existing record if it already exists.
691
+ *
692
+ * @param {Object} _izContext - Execution context.
693
+ * @param {string} historyObjType - History object type.
694
+ * @param {Object} historyIdentifiers - Identifiers of the history record.
695
+ * @param {string} targetStatus - Status stored in the history record.
696
+ * @param {Object} errorParams - Error handling parameters.
697
+ * @param {Object} additionalAttributes - Additional attributes to store.
698
+ * @param {Object} queryElements - Additional query elements for the operation.
699
+ * @returns {Promise<Object>} The created or existing history record.
700
+ */
701
+ async function ensureHistory(
702
+ _izContext,
703
+ historyObjType,
704
+ historyIdentifiers,
705
+ targetStatus,
706
+ errorParams,
707
+ additionalAttributes = {},
708
+ queryElements = {},
709
+ ) {
710
+
711
+ const schemaResult = await getObjectSchema.getObjSchemaS3WithCache(
712
+ _izContext,
713
+ historyObjType
714
+ );
715
+
716
+ if (schemaResult.errorsFound.length > 0 || !schemaResult.result) {
717
+ throw new NoRetryError(
718
+ JSON.stringify(schemaResult.errorsFound)
719
+ );
720
+ }
721
+
722
+ const historyObjectSchema = schemaResult.result;
723
+
724
+ const historyStorageResource = getDynamoDbStorageResource(
725
+ _izContext,
726
+ historyObjectSchema
727
+ );
728
+
729
+ const historyTableName = dynamodbSharedLib.tableName(
730
+ _izContext,
731
+ historyStorageResource.tableName,
732
+ historyStorageResource.serviceTag
733
+ );
734
+
735
+ const lockTag = getLockTagFromObjectType(historyObjectSchema.objectType);
736
+
737
+ const fieldNameStatus =
738
+ lockUpdateConstants.createStatusFieldName(lockTag);
739
+
740
+ const propertiesToValidate = {
741
+ [fieldNameStatus]: targetStatus
742
+ };
743
+
744
+ if (additionalAttributes && typeof additionalAttributes === 'object') {
745
+ for (const attributeName of Object.keys(additionalAttributes)) {
746
+ propertiesToValidate[attributeName] = additionalAttributes[attributeName];
747
+ }
748
+ }
749
+
750
+ const validationErrors = graphSharedLib.checkPropertiesValidWithSchema(
751
+ _izContext,
752
+ historyObjectSchema,
753
+ propertiesToValidate,
754
+ errorParams
755
+ );
756
+
757
+ if (Array.isArray(validationErrors) && validationErrors.length > 0) {
758
+ throw new NoRetryError(validationErrors.join('; '));
759
+ }
760
+
761
+ const updateAttributes = [
762
+ {
763
+ attributeName: fieldNameStatus,
764
+ action: 'set',
765
+ value: targetStatus
766
+ }
767
+ ];
768
+
769
+ if (additionalAttributes && typeof additionalAttributes === 'object') {
770
+ for (const [attributeName, value] of Object.entries(additionalAttributes)) {
771
+ updateAttributes.push({
772
+ attributeName,
773
+ action: 'set',
774
+ value
775
+ });
776
+ }
777
+ }
778
+
779
+ const identifiersBase = await identifiersObject.dynamoDbIdentifiersByStorageResource(
780
+ _izContext,
781
+ historyObjectSchema,
782
+ historyStorageResource,
783
+ historyIdentifiers,
784
+ );
785
+
786
+ try {
787
+ return await dynamodbSharedLib.updateItem(
788
+ _izContext,
789
+ historyTableName,
790
+ identifiersBase,
791
+ updateAttributes,
792
+ queryElements,
793
+ {
794
+ complexAttributes: true,
795
+ }
796
+ );
797
+ } catch (error) {
798
+ throw error;
799
+ }
800
+ }
801
+
802
+ /**
803
+ * Removes the current request from the lock queue and publishes the next
804
+ * waiting request to its original flow, if one exists.
805
+ *
806
+ * @param {Object} _izContext - Execution context.
807
+ * @param {string} queueObjType - Queue object type.
808
+ * @param {Object} identifiers - Identifiers of the current queue record.
809
+ * @returns {Promise<Object|null>} SNS publish result, or null if the queue is empty.
810
+ */
811
+ async function publishNextWaitingRequest(
812
+ _izContext,
813
+ queueObjType,
814
+ identifiers
815
+ ) {
816
+ // Get Queue ObjectSchema
817
+ const queueSchemaResult = await getObjectSchema.getObjSchemaS3WithCache(
818
+ _izContext,
819
+ queueObjType
820
+ );
821
+
822
+ if (
823
+ queueSchemaResult.errorsFound.length > 0 ||
824
+ !queueSchemaResult.result
825
+ ) {
826
+ throw new NoRetryError(
827
+ `Unable to get Queue ObjectSchema '${queueObjType}': ` +
828
+ JSON.stringify(queueSchemaResult.errorsFound)
829
+ );
830
+ }
831
+
832
+ const queueObjectSchema = queueSchemaResult.result;
833
+
834
+ // Get Queue DynamoDB Table
835
+ const queueStorageResource = getDynamoDbStorageResource(
836
+ _izContext,
837
+ queueObjectSchema
838
+ );
839
+
840
+ const queueTableName = dynamodbSharedLib.tableName(
841
+ _izContext,
842
+ queueStorageResource.tableName,
843
+ queueStorageResource.serviceTag
844
+ );
845
+
846
+ // Convert identifiers to ObjInstanceBase
847
+ const identifiersBase = await identifiersObject.identifiersBaseFromIdentifiers(
848
+ _izContext,
849
+ queueObjType,
850
+ identifiers
851
+ );
852
+
853
+ // Get Partition Key
854
+ const partitionKeyValue = {};
855
+ let hasSortKey = false;
856
+
857
+ for (const identifier of queueObjectSchema.identifiers) {
858
+ if (identifier.type === 'partitionKey') {
859
+ let partitionKeyFieldName = identifier.fieldName;
860
+
861
+ if (
862
+ !Object.prototype.hasOwnProperty.call(
863
+ identifiersBase,
864
+ partitionKeyFieldName
865
+ )
866
+ ) {
867
+ partitionKeyFieldName = identifier.name;
868
+ }
869
+
870
+ if (
871
+ !partitionKeyFieldName ||
872
+ !Object.prototype.hasOwnProperty.call(
873
+ identifiersBase,
874
+ partitionKeyFieldName
875
+ )
876
+ ) {
877
+ throw new NoRetryError(
878
+ `Missing partition key in identifiersBase for Queue '${queueObjType}'`
879
+ );
880
+ }
881
+
882
+ partitionKeyValue[partitionKeyFieldName] = identifiersBase[partitionKeyFieldName];
883
+
884
+ } else if (identifier.type === 'sortKey') {
885
+ if (!identifier.name) {
886
+ throw new NoRetryError(
887
+ `Queue schema '${queueObjType}' has an invalid sortKey identifier`
888
+ );
889
+ }
890
+
891
+ hasSortKey = true;
892
+ }
893
+ }
894
+
895
+ if (!hasSortKey) {
896
+ throw new NoRetryError(
897
+ `Queue schema '${queueObjType}' is missing sortKey`
898
+ );
899
+ }
900
+
901
+ // Delete Current Queue Record
902
+ await dynamodbSharedLib.deleteItem(
903
+ _izContext,
904
+ queueTableName,
905
+ identifiersBase
906
+ );
907
+
908
+ // Get Next Waiting Request
909
+ const queryResult = await dynamodbSharedLib.query(
910
+ _izContext,
911
+ queueTableName,
912
+ partitionKeyValue,
913
+ { limit: 1 }
914
+ );
915
+
916
+ if (queryResult.Items.length === 0) {
917
+ _izContext.logger.debug('Lock queue is empty, there is no next message');
918
+
919
+ return null;
920
+ }
921
+
922
+ const nextQueueRecord = queryResult.Items[0];
923
+
924
+ // Convert Queue ObjInstanceBase to ObjInstance
925
+ const nextQueueObjInstance =
926
+ await identifiersObject.objInstanceFromObjInstanceBase(
927
+ _izContext,
928
+ queueObjType,
929
+ nextQueueRecord
930
+ );
931
+
932
+ const flowType = nextQueueObjInstance.flowType;
933
+ const messageBody = nextQueueObjInstance.messageBody;
934
+ const nextUniqueRequestId = nextQueueObjInstance.uniqueRequestId;
935
+
936
+ // Prepare Next Request Message
937
+ const message = {
938
+ ...messageBody,
939
+ overwriteUniqueRequestId: nextUniqueRequestId
940
+ };
941
+
942
+ // Publish Next Request to Original Flow
943
+ const topicName = `${flowType.flowTag}_In`;
944
+
945
+ const topicArn = snsSharedLib.snsTopicArnByFlowSchema(
946
+ _izContext,
947
+ topicName,
948
+ flowType.serviceTag
949
+ );
950
+
951
+ const snsPublishParams = {
952
+ Message: JSON.stringify(message),
953
+ TopicArn: topicArn
954
+ };
955
+
956
+ return sns.publishAsync(_izContext, snsPublishParams);
957
+ }
958
+
959
+
960
+ /**
961
+ * Safely removes the uniqueRequestId lock only if its current value matches
962
+ * the expected uniqueRequestId.
963
+ *
964
+ * @param {Object} _izContext - Execution context containing the logger.
965
+ * @param {string} mainObjType - Main object type.
966
+ * @param {Object} identifiersBase - Identifiers of the main record.
967
+ * @param {string} uniqueRequestId - Expected uniqueRequestId value for the lock.
968
+ * @returns {Promise<Object>} Updated main record.
969
+ */
970
+ async function removeUniqueRequestIdSafe(
971
+ _izContext,
972
+ mainObjType,
973
+ identifiersBase,
974
+ uniqueRequestId,
975
+ ) {
976
+
977
+ const schemaResult = await getObjectSchema.getObjSchemaS3WithCache(
978
+ _izContext,
979
+ mainObjType
980
+ );
981
+
982
+ if (schemaResult.errorsFound.length > 0 || !schemaResult.result) {
983
+ throw new NoRetryError(
984
+ JSON.stringify(schemaResult.errorsFound)
985
+ );
986
+ }
987
+
988
+ const mainObjectSchema = schemaResult.result;
989
+
990
+
991
+ const { lockTag, storageResourceTag } = getAddOnConfig(
992
+ _izContext,
993
+ mainObjectSchema,
994
+ );
995
+
996
+ const { fieldNameUniqueRequestId } = createLockFieldNames(lockTag);
997
+
998
+ if (!mainObjectSchema.fieldNames || !Object.prototype.hasOwnProperty.call(mainObjectSchema.fieldNames, fieldNameUniqueRequestId)) {
999
+ throw new NoRetryError(
1000
+ `Field '${fieldNameUniqueRequestId}' not found in schema '${mainObjectSchema.objectType}'`
1001
+ );
1002
+ }
1003
+
1004
+ const mainObjectStorage = mainObjectSchema.storageResources[storageResourceTag];
1005
+
1006
+
1007
+ const lockTableName = dynamodbSharedLib.tableName(
1008
+ _izContext,
1009
+ mainObjectStorage.tableName,
1010
+ mainObjectStorage.serviceTag
1011
+ );
1012
+
1013
+ const queryElements = {
1014
+ additionalAttributes: {
1015
+ currentUniqueRequestId: uniqueRequestId
1016
+ },
1017
+ logicalElements: [
1018
+ {
1019
+ type: 'logical',
1020
+ comparison: 'equals',
1021
+ lhsAttribute: fieldNameUniqueRequestId,
1022
+ rhsReference: 'currentUniqueRequestId'
1023
+ }
1024
+ ],
1025
+ returnValues: 'ALL_NEW'
1026
+ };
1027
+
1028
+ try {
1029
+ await dynamodbSharedLib.updateItem(
1030
+ _izContext,
1031
+ lockTableName,
1032
+ identifiersBase,
1033
+ [
1034
+ {
1035
+ attributeName: fieldNameUniqueRequestId,
1036
+ action: 'remove'
1037
+ }
1038
+ ],
1039
+ queryElements,
1040
+ {
1041
+ complexAttributes: true,
1042
+ errorOnConditionalExpNotPass: true
1043
+ }
1044
+ );
1045
+ } catch (error) {
1046
+ if (error.message === 'ConditionalExpNotPass') {
1047
+ _izContext.logger.error(
1048
+ 'Failed to remove uniqueRequestId because it did not match',
1049
+ {
1050
+ tableName: lockTableName,
1051
+ identifiers: identifiersBase,
1052
+ uniqueRequestId
1053
+ }
1054
+ );
1055
+
1056
+ throw new NoRetryError(
1057
+ 'Cannot remove uniqueRequestId because it does not match the current request.'
1058
+ );
1059
+ }
1060
+ throw error;
1061
+ }
1062
+ }
1063
+
1064
+ export {
1065
+ // 1. Configuration and utilities
1066
+ getAddOnConfig,
1067
+ getDynamoDbStorageResource,
1068
+ createLockFieldNames,
1069
+
1070
+ // 2. Lock management
1071
+ checkRequestWithLock,
1072
+ updateMainRecordWithLock,
1073
+ ensureHistory,
1074
+ removeUniqueRequestIdSafe,
1075
+
1076
+ // 3. Queue management
1077
+ publishNextWaitingRequest
1078
+ };