@squiz/db-lib 1.72.0 → 1.74.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,6 +16,8 @@ import {
16
16
  TransactWriteCommandInput,
17
17
  BatchGetCommand,
18
18
  BatchGetCommandInput,
19
+ BatchWriteCommand,
20
+ BatchWriteCommandInput,
19
21
  } from '@aws-sdk/lib-dynamodb';
20
22
  import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
21
23
  import { DynamoDbManager, Transaction } from './DynamoDbManager';
@@ -230,6 +232,8 @@ describe('AbstractRepository', () => {
230
232
  Attributes: {
231
233
  name: 'foo',
232
234
  age: 99,
235
+ // country attribute is part of gsi key
236
+ // hence updating this will also update gsi key value
233
237
  country: 'au-updated',
234
238
  data: {},
235
239
  },
@@ -237,12 +241,14 @@ describe('AbstractRepository', () => {
237
241
  const input: UpdateCommandInput = {
238
242
  TableName: TABLE_NAME,
239
243
  Key: { pk: 'test_item#foo', sk: '#meta' },
240
- UpdateExpression: 'SET #country = :country',
244
+ UpdateExpression: 'SET #country = :country, #gsi1_pk = :gsi1_pk',
241
245
  ExpressionAttributeNames: {
242
246
  '#country': 'country',
247
+ '#gsi1_pk': 'gsi1_pk',
243
248
  },
244
249
  ExpressionAttributeValues: {
245
250
  ':country': 'au-updated',
251
+ ':gsi1_pk': 'country#au-updated',
246
252
  },
247
253
  ConditionExpression: `attribute_exists(pk)`,
248
254
  };
@@ -263,6 +269,60 @@ describe('AbstractRepository', () => {
263
269
  );
264
270
  });
265
271
 
272
+ it('should only update the changed attributes', async () => {
273
+ ddbClientMock.on(GetCommand).resolves({
274
+ $metadata: {
275
+ httpStatusCode: 200,
276
+ },
277
+ Item: {
278
+ name: 'foo',
279
+ age: 99,
280
+ country: 'au',
281
+ data: {},
282
+ },
283
+ });
284
+ ddbClientMock.on(UpdateCommand).resolves({
285
+ $metadata: {
286
+ httpStatusCode: 200,
287
+ },
288
+ Attributes: {
289
+ name: 'foo',
290
+ age: 99,
291
+ country: 'au',
292
+ data: { active: true },
293
+ },
294
+ });
295
+ const input: UpdateCommandInput = {
296
+ TableName: TABLE_NAME,
297
+ Key: { pk: 'test_item#foo', sk: '#meta' },
298
+ UpdateExpression: 'SET #data = :data',
299
+ ExpressionAttributeNames: {
300
+ '#data': 'data',
301
+ },
302
+ ExpressionAttributeValues: {
303
+ ':data': { active: true },
304
+ },
305
+ ConditionExpression: `attribute_exists(pk)`,
306
+ };
307
+
308
+ const updateItem = {
309
+ name: 'foo',
310
+ age: 99,
311
+ // this is the only change attribute value
312
+ data: { active: true },
313
+ };
314
+ const result = await repository.updateItem(updateItem);
315
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, UpdateCommand, input);
316
+ expect(result).toEqual(
317
+ new TestItem({
318
+ name: 'foo',
319
+ age: 99,
320
+ country: 'au',
321
+ data: { active: true },
322
+ }),
323
+ );
324
+ });
325
+
266
326
  it('should not trigger update request if the input attributes are same as in the existing item', async () => {
267
327
  ddbClientMock.on(GetCommand).resolves({
268
328
  $metadata: {
@@ -557,6 +617,156 @@ describe('AbstractRepository', () => {
557
617
  ]);
558
618
  });
559
619
 
620
+ it('should retry if unprocessed keys returned', async () => {
621
+ const input1: BatchGetCommandInput = {
622
+ RequestItems: {
623
+ [TABLE_NAME]: {
624
+ Keys: [
625
+ { pk: 'test_item#foo', sk: '#meta' },
626
+ { pk: 'test_item#foo2', sk: '#meta' },
627
+ ],
628
+ },
629
+ },
630
+ };
631
+ const input2: BatchGetCommandInput = {
632
+ RequestItems: {
633
+ [TABLE_NAME]: {
634
+ Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
635
+ },
636
+ },
637
+ };
638
+
639
+ ddbClientMock.on(BatchGetCommand, input1).resolves({
640
+ $metadata: {
641
+ httpStatusCode: 200,
642
+ },
643
+ Responses: {
644
+ [TABLE_NAME]: [
645
+ {
646
+ name: 'foo',
647
+ age: 99,
648
+ country: 'au',
649
+ data: {},
650
+ data2: '{"foo":"bar","num":123}',
651
+ },
652
+ ],
653
+ },
654
+ UnprocessedKeys: {
655
+ [TABLE_NAME]: {
656
+ Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
657
+ },
658
+ },
659
+ });
660
+
661
+ ddbClientMock.on(BatchGetCommand, input2).resolves({
662
+ $metadata: {
663
+ httpStatusCode: 200,
664
+ },
665
+ Responses: {
666
+ [TABLE_NAME]: [
667
+ {
668
+ name: 'foo2',
669
+ age: 999,
670
+ country: 'au',
671
+ data: {},
672
+ data2: '{"foo":"bar","num":123}',
673
+ },
674
+ ],
675
+ },
676
+ UnprocessedKeys: {},
677
+ });
678
+
679
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
680
+ const result = await repository.getItems(requestItems);
681
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
682
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
683
+ expect(result).toEqual([
684
+ new TestItem({
685
+ name: 'foo',
686
+ age: 99,
687
+ country: 'au',
688
+ data: {},
689
+ data2: {
690
+ foo: 'bar',
691
+ num: 123,
692
+ },
693
+ }),
694
+ new TestItem({
695
+ name: 'foo2',
696
+ age: 999,
697
+ country: 'au',
698
+ data: {},
699
+ data2: {
700
+ foo: 'bar',
701
+ num: 123,
702
+ },
703
+ }),
704
+ ]);
705
+ });
706
+
707
+ it('should fail after max retries for unprocessed keys', async () => {
708
+ const input1: BatchGetCommandInput = {
709
+ RequestItems: {
710
+ [TABLE_NAME]: {
711
+ Keys: [
712
+ { pk: 'test_item#foo', sk: '#meta' },
713
+ { pk: 'test_item#foo2', sk: '#meta' },
714
+ ],
715
+ },
716
+ },
717
+ };
718
+ const input2: BatchGetCommandInput = {
719
+ RequestItems: {
720
+ [TABLE_NAME]: {
721
+ Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
722
+ },
723
+ },
724
+ };
725
+
726
+ ddbClientMock.on(BatchGetCommand, input1).resolves({
727
+ $metadata: {
728
+ httpStatusCode: 200,
729
+ },
730
+ Responses: {
731
+ [TABLE_NAME]: [
732
+ {
733
+ name: 'foo',
734
+ age: 99,
735
+ country: 'au',
736
+ data: {},
737
+ data2: '{"foo":"bar","num":123}',
738
+ },
739
+ ],
740
+ },
741
+ UnprocessedKeys: {
742
+ [TABLE_NAME]: {
743
+ Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
744
+ },
745
+ },
746
+ });
747
+
748
+ ddbClientMock.on(BatchGetCommand, input2).resolves({
749
+ $metadata: {
750
+ httpStatusCode: 200,
751
+ },
752
+ Responses: {},
753
+ UnprocessedKeys: {
754
+ [TABLE_NAME]: {
755
+ Keys: [{ pk: 'test_item#foo2', sk: '#meta' }],
756
+ },
757
+ },
758
+ });
759
+
760
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
761
+ await expect(repository.getItems(requestItems)).rejects.toEqual(
762
+ new Error('Maximum allowed retries exceeded for unprocessed items'),
763
+ );
764
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchGetCommand, input1);
765
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchGetCommand, input2);
766
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(3, BatchGetCommand, input2);
767
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(4, BatchGetCommand, input2);
768
+ });
769
+
560
770
  it('should request BatchGetItem in batch of 100 items to get result', async () => {
561
771
  ddbClientMock.on(BatchGetCommand).resolves({
562
772
  $metadata: {
@@ -608,6 +818,207 @@ describe('AbstractRepository', () => {
608
818
  });
609
819
  });
610
820
 
821
+ describe('deleteItems()', () => {
822
+ it('should use batchWrite() to get result', async () => {
823
+ ddbClientMock.on(BatchWriteCommand).resolves({
824
+ $metadata: {
825
+ httpStatusCode: 200,
826
+ },
827
+ ItemCollectionMetrics: {
828
+ [TABLE_NAME]: [{}],
829
+ },
830
+ });
831
+ const input: BatchWriteCommandInput = {
832
+ RequestItems: {
833
+ [TABLE_NAME]: [
834
+ {
835
+ DeleteRequest: {
836
+ Key: { pk: 'test_item#foo', sk: '#meta' },
837
+ },
838
+ },
839
+ {
840
+ DeleteRequest: {
841
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
842
+ },
843
+ },
844
+ ],
845
+ },
846
+ };
847
+
848
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
849
+ await repository.deleteItems(requestItems);
850
+ expect(ddbClientMock).toHaveReceivedCommandWith(BatchWriteCommand, input);
851
+ });
852
+
853
+ it('should use re-try if unprocessed items returned', async () => {
854
+ const input1: BatchWriteCommandInput = {
855
+ RequestItems: {
856
+ [TABLE_NAME]: [
857
+ {
858
+ DeleteRequest: {
859
+ Key: { pk: 'test_item#foo', sk: '#meta' },
860
+ },
861
+ },
862
+ {
863
+ DeleteRequest: {
864
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
865
+ },
866
+ },
867
+ ],
868
+ },
869
+ };
870
+ const input2: BatchWriteCommandInput = {
871
+ RequestItems: {
872
+ [TABLE_NAME]: [
873
+ {
874
+ DeleteRequest: {
875
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
876
+ },
877
+ },
878
+ ],
879
+ },
880
+ };
881
+
882
+ ddbClientMock.on(BatchWriteCommand, input1).resolves({
883
+ $metadata: {
884
+ httpStatusCode: 200,
885
+ },
886
+ UnprocessedItems: {
887
+ [TABLE_NAME]: [
888
+ {
889
+ DeleteRequest: {
890
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
891
+ },
892
+ },
893
+ ],
894
+ },
895
+ });
896
+ ddbClientMock.on(BatchWriteCommand, input2).resolves({
897
+ $metadata: {
898
+ httpStatusCode: 200,
899
+ },
900
+ UnprocessedItems: {},
901
+ });
902
+
903
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
904
+ await repository.deleteItems(requestItems);
905
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchWriteCommand, input1);
906
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchWriteCommand, input2);
907
+ });
908
+
909
+ it('should fail after max number of retries', async () => {
910
+ const input1: BatchWriteCommandInput = {
911
+ RequestItems: {
912
+ [TABLE_NAME]: [
913
+ {
914
+ DeleteRequest: {
915
+ Key: { pk: 'test_item#foo', sk: '#meta' },
916
+ },
917
+ },
918
+ {
919
+ DeleteRequest: {
920
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
921
+ },
922
+ },
923
+ ],
924
+ },
925
+ };
926
+ const input2: BatchWriteCommandInput = {
927
+ RequestItems: {
928
+ [TABLE_NAME]: [
929
+ {
930
+ DeleteRequest: {
931
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
932
+ },
933
+ },
934
+ ],
935
+ },
936
+ };
937
+
938
+ ddbClientMock.on(BatchWriteCommand).resolves({
939
+ $metadata: {
940
+ httpStatusCode: 200,
941
+ },
942
+ UnprocessedItems: {
943
+ [TABLE_NAME]: [
944
+ {
945
+ DeleteRequest: {
946
+ Key: { pk: 'test_item#foo2', sk: '#meta' },
947
+ },
948
+ },
949
+ ],
950
+ },
951
+ });
952
+
953
+ const requestItems = [{ name: 'foo' }, { name: 'foo2' }];
954
+ await expect(repository.deleteItems(requestItems)).rejects.toEqual(
955
+ new Error('Maximum allowed retries exceeded for unprocessed items'),
956
+ );
957
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchWriteCommand, input1);
958
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchWriteCommand, input2);
959
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(3, BatchWriteCommand, input2);
960
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(4, BatchWriteCommand, input2);
961
+ });
962
+
963
+ it('should request batchWrite in batch of 25 items to get result', async () => {
964
+ ddbClientMock.on(BatchWriteCommand).resolves({
965
+ $metadata: {
966
+ httpStatusCode: 200,
967
+ },
968
+ });
969
+
970
+ const requestItems = [];
971
+ for (let i = 0; i < 30; i++) {
972
+ requestItems.push({ name: `foo${i}` });
973
+ }
974
+ // keys for first batch request
975
+ const keys1 = [];
976
+ for (let i = 0; i < 25; i++) {
977
+ keys1.push({ pk: `test_item#foo${i}`, sk: '#meta' });
978
+ }
979
+ // keys for second batch request
980
+ const keys2 = [];
981
+ for (let i = 25; i < 30; i++) {
982
+ keys2.push({ pk: `test_item#foo${i}`, sk: '#meta' });
983
+ }
984
+
985
+ const input1: BatchWriteCommandInput = {
986
+ RequestItems: {
987
+ [TABLE_NAME]: keys1.map((key) => {
988
+ return {
989
+ DeleteRequest: {
990
+ Key: key,
991
+ },
992
+ };
993
+ }),
994
+ },
995
+ };
996
+ const input2: BatchWriteCommandInput = {
997
+ RequestItems: {
998
+ [TABLE_NAME]: keys2.map((key) => {
999
+ return {
1000
+ DeleteRequest: {
1001
+ Key: key,
1002
+ },
1003
+ };
1004
+ }),
1005
+ },
1006
+ };
1007
+
1008
+ await repository.deleteItems(requestItems);
1009
+ expect(ddbClientMock).toHaveReceivedCommandTimes(BatchWriteCommand, 2);
1010
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(1, BatchWriteCommand, input1);
1011
+ expect(ddbClientMock).toHaveReceivedNthCommandWith(2, BatchWriteCommand, input2);
1012
+ });
1013
+
1014
+ it('should throw error if any input item does not includes key field(s)', async () => {
1015
+ const requestItems = [{ name: 'foo' }, { age: 22 }];
1016
+ await expect(repository.deleteItems(requestItems)).rejects.toEqual(
1017
+ new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
1018
+ );
1019
+ });
1020
+ });
1021
+
611
1022
  describe('queryItems()', () => {
612
1023
  it('should return the items if found', async () => {
613
1024
  ddbClientMock.on(QueryCommand).resolves({
@@ -1038,16 +1449,18 @@ describe('AbstractRepository', () => {
1038
1449
  ConditionExpression: 'attribute_exists(pk)',
1039
1450
  ExpressionAttributeNames: {
1040
1451
  '#age': 'age',
1452
+ '#gsi1_sk': 'gsi1_sk',
1041
1453
  },
1042
1454
  ExpressionAttributeValues: {
1043
1455
  ':age': 55,
1456
+ ':gsi1_sk': 'age#55',
1044
1457
  },
1045
1458
  Key: {
1046
1459
  pk: 'test_item#foo2',
1047
1460
  sk: '#meta',
1048
1461
  },
1049
1462
  TableName: 'test-table',
1050
- UpdateExpression: 'SET #age = :age',
1463
+ UpdateExpression: 'SET #age = :age, #gsi1_sk = :gsi1_sk',
1051
1464
  },
1052
1465
  },
1053
1466
  {
@@ -4,6 +4,10 @@ import {
4
4
  UpdateCommandOutput,
5
5
  PutCommandInput,
6
6
  DeleteCommandInput,
7
+ BatchWriteCommandInput,
8
+ BatchWriteCommandOutput,
9
+ BatchGetCommandInput,
10
+ BatchGetCommandOutput,
7
11
  } from '@aws-sdk/lib-dynamodb';
8
12
 
9
13
  import { Transaction, DynamoDbManager, MissingKeyValuesError, InvalidDbSchemaError } from '..';
@@ -30,14 +34,15 @@ export type QueryOptions = {
30
34
 
31
35
  interface Reader<T> {
32
36
  queryItems(partialItem: Partial<T>, options?: QueryOptions): Promise<T[]>;
33
-
34
37
  getItem(id: string | Partial<T>): Promise<T | undefined>;
38
+ getItems(partialItem: Partial<T>[]): Promise<T[]>;
35
39
  }
36
40
 
37
41
  interface Writer<T> {
38
42
  createItem(item: Partial<T>): Promise<T>;
39
43
  updateItem(partialItem: Partial<T>): Promise<T | undefined>;
40
44
  deleteItem(partialItem: Partial<T>): Promise<number>;
45
+ deleteItems(partialItem: Partial<T>[]): Promise<void>;
41
46
  }
42
47
 
43
48
  type Repository<T> = Reader<T> & Writer<T>;
@@ -65,6 +70,8 @@ export type EntityDefinition = {
65
70
  fieldsAsJsonString: string[];
66
71
  };
67
72
 
73
+ const MAX_REATTEMPTS = 3;
74
+
68
75
  export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLASS extends SHAPE>
69
76
  implements Reader<SHAPE>, Writer<SHAPE>
70
77
  {
@@ -153,25 +160,84 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
153
160
  * @returns
154
161
  */
155
162
  private async getBatchItems(items: Partial<SHAPE>[]): Promise<DATA_CLASS[]> {
156
- const keys: Record<string, string>[] = [];
163
+ let resultItems: DATA_CLASS[] = [];
164
+
165
+ let requestKeys: BatchGetCommandInput['RequestItems'] = {
166
+ [this.tableName]: {
167
+ Keys: this.getBatchKeys(items),
168
+ },
169
+ };
170
+ let reattemptsCount = 0;
171
+ while (requestKeys && Object.keys(requestKeys).length) {
172
+ if (reattemptsCount++ > MAX_REATTEMPTS) {
173
+ throw Error('Maximum allowed retries exceeded for unprocessed items');
174
+ }
175
+ const output: BatchGetCommandOutput = await this.client.batchGet({
176
+ RequestItems: requestKeys,
177
+ });
178
+ requestKeys = output.UnprocessedKeys;
179
+ if (output.Responses && output.Responses[this.tableName] && output.Responses[this.tableName].length) {
180
+ resultItems = resultItems.concat(output.Responses[this.tableName].map((i) => this.hydrateItem(i)));
181
+ }
182
+ }
183
+
184
+ return resultItems;
185
+ }
186
+
187
+ /**
188
+ * Delete items in a batch
189
+ * Uses batchWrite() with 25 items
190
+ *
191
+ * @param item
192
+ *
193
+ * @throws MissingKeyValuesError
194
+ */
195
+ public async deleteItems(items: Partial<SHAPE>[]): Promise<void> {
196
+ // this is the maximum items allowed by BatchWriteItem()
197
+ const batchSize = 25;
198
+
199
+ for (let i = 0; i < items.length; i += batchSize) {
200
+ const keys = this.getBatchKeys(items.slice(i, i + batchSize));
201
+ await this.deleteBatchItems(keys);
202
+ }
203
+ }
204
+
205
+ private async deleteBatchItems(keys: { [key: string]: string }[]): Promise<void> {
206
+ let requestItems: BatchWriteCommandInput['RequestItems'] = {
207
+ [this.tableName]: Object.values(keys).map((key) => {
208
+ return {
209
+ DeleteRequest: {
210
+ Key: key,
211
+ },
212
+ };
213
+ }),
214
+ };
215
+ let reattemptsCount = 0;
216
+ while (requestItems && Object.keys(requestItems).length) {
217
+ if (reattemptsCount++ > MAX_REATTEMPTS) {
218
+ throw Error('Maximum allowed retries exceeded for unprocessed items');
219
+ }
220
+ const response: BatchWriteCommandOutput = await this.client.batchWrite({
221
+ RequestItems: requestItems,
222
+ });
223
+ requestItems = response.UnprocessedItems;
224
+ }
225
+ }
226
+
227
+ private getBatchKeys(items: Partial<SHAPE>[]) {
228
+ const keys: { [key: string]: string }[] = [];
157
229
  for (const item of items) {
158
230
  keys.push({
159
231
  [this.keys.pk.attributeName]: this.getPk(item),
160
232
  [this.keys.sk.attributeName]: this.getSk(item),
161
233
  });
162
234
  }
163
- const output = await this.client.batchGet({
164
- RequestItems: {
165
- [this.tableName]: {
166
- Keys: keys,
167
- },
168
- },
169
- });
235
+ // keys.push({
236
+ // [this.keys.pk.attributeName]: 'foo1',
237
+ // [this.keys.sk.attributeName]: 'foo2',
238
+ // });
170
239
 
171
- if (output.Responses && output.Responses[this.tableName].length) {
172
- return output.Responses[this.tableName].map((i) => this.hydrateItem(i));
173
- }
174
- return [];
240
+ return keys;
175
241
  }
176
242
 
177
243
  /**
@@ -264,12 +330,15 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
264
330
  this.assertValueMatchesModel(value);
265
331
 
266
332
  this.convertSelectedValuesToJsonString(newValue);
333
+ this.convertSelectedValuesToJsonString(oldValue as Record<string, unknown>);
267
334
 
268
335
  const updateExpression = [];
269
336
  const expressionAttributeNames: Record<string, string> = {};
270
337
  const expressionAttributeValues: Record<string, unknown> = {};
338
+ const updatedAttributes: string[] = [];
271
339
  for (const modelProperty of Object.keys(newValue)) {
272
340
  const propValue = newValue[modelProperty as keyof SHAPE] ?? null;
341
+
273
342
  if (propValue === oldValue[modelProperty as keyof SHAPE]) {
274
343
  // don't need to update the properties that are unchanged
275
344
  continue;
@@ -281,19 +350,35 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
281
350
  updateExpression.push(`${propName} = ${propValuePlaceHolder}`);
282
351
  expressionAttributeNames[propName] = modelProperty;
283
352
  expressionAttributeValues[propValuePlaceHolder] = propValue;
353
+ updatedAttributes.push(modelProperty);
284
354
  }
285
- if (!updateExpression.length) {
355
+ if (!updatedAttributes.length) {
286
356
  // nothing to update
287
357
  return value;
288
358
  }
289
359
 
360
+ // also update the gsi attributes if needed
361
+ Object.keys(this.indexes).forEach((key) => {
362
+ const index = this.indexes[key];
363
+ [index.pk.attributeName, index.sk.attributeName].forEach((keyAttributeName) => {
364
+ const keyFormat = this.keysFormat[keyAttributeName];
365
+ if (updatedAttributes.find((attr) => keyFormat.search(`{${attr}}`) !== -1)) {
366
+ const propName = `#${keyAttributeName}`;
367
+ const propValuePlaceHolder = `:${keyAttributeName}`;
368
+ updateExpression.push(`${propName} = ${propValuePlaceHolder}`);
369
+ expressionAttributeNames[propName] = keyAttributeName;
370
+ expressionAttributeValues[propValuePlaceHolder] = this.getKey(value, keyAttributeName);
371
+ }
372
+ });
373
+ });
374
+
290
375
  const updateCommandInput = {
291
376
  TableName: this.tableName,
292
377
  Key: {
293
378
  [this.keys.pk.attributeName]: this.getPk(newValue),
294
379
  [this.keys.sk.attributeName]: this.getSk(newValue),
295
380
  },
296
- UpdateExpression: 'SET ' + updateExpression.join(','),
381
+ UpdateExpression: 'SET ' + updateExpression.join(', '),
297
382
  ExpressionAttributeValues: expressionAttributeValues,
298
383
  ExpressionAttributeNames: expressionAttributeNames,
299
384
  ConditionExpression: `attribute_exists(${this.keys.pk.attributeName})`,
@@ -545,17 +630,6 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
545
630
  return keyFormat;
546
631
  }
547
632
 
548
- /**
549
- * Whether the given property name is part of the entity's pk/sk string
550
- * @param propertyName
551
- * @returns boolean
552
- */
553
- private isPropertyPartOfKeys(propertyName: string) {
554
- if (this.keysFormat[this.keys.pk.attributeName].search(`{${propertyName}}`) !== -1) return true;
555
- if (this.keysFormat[this.keys.sk.attributeName].search(`{${propertyName}}`) !== -1) return true;
556
- return false;
557
- }
558
-
559
633
  /**
560
634
  * Validate the data matches with "DATA_MODEL"
561
635
  * @param value