@squiz/db-lib 1.73.0 → 1.75.0
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/CHANGELOG.md +12 -0
- package/lib/AbstractRepository.postgres.integration.spec.d.ts +2 -0
- package/lib/AbstractRepository.postgres.integration.spec.d.ts.map +1 -0
- package/lib/{AbstractRepository.integration.spec.js → AbstractRepository.postgres.integration.spec.js} +1 -1
- package/lib/AbstractRepository.postgres.integration.spec.js.map +1 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +3 -7
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.js +59 -36
- package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +2 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +214 -2
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
- package/lib/error/UnknownKeyAttributeError.d.ts +6 -0
- package/lib/error/UnknownKeyAttributeError.d.ts.map +1 -0
- package/lib/error/UnknownKeyAttributeError.js +12 -0
- package/lib/error/UnknownKeyAttributeError.js.map +1 -0
- package/package.json +1 -2
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +229 -2
- package/src/dynamodb/AbstractDynamoDbRepository.ts +64 -35
- package/src/error/UnknownKeyAttributeError.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/AbstractRepository.integration.spec.d.ts +0 -2
- package/lib/AbstractRepository.integration.spec.d.ts.map +0 -1
- package/lib/AbstractRepository.integration.spec.js.map +0 -1
- /package/src/{AbstractRepository.integration.spec.ts → AbstractRepository.postgres.integration.spec.ts} +0 -0
@@ -38,6 +38,7 @@ interface ITestItem {
|
|
38
38
|
country: string;
|
39
39
|
data?: object;
|
40
40
|
data2?: object;
|
41
|
+
email?: string;
|
41
42
|
}
|
42
43
|
|
43
44
|
class TestItem implements ITestItem {
|
@@ -46,6 +47,7 @@ class TestItem implements ITestItem {
|
|
46
47
|
public country: string;
|
47
48
|
public data: object;
|
48
49
|
public data2?: object;
|
50
|
+
public email?: string;
|
49
51
|
|
50
52
|
constructor(data: Partial<ITestItem> = {}) {
|
51
53
|
this.name = data.name ?? 'default name';
|
@@ -66,6 +68,12 @@ class TestItem implements ITestItem {
|
|
66
68
|
if (typeof this.data !== 'object' || Array.isArray(this.data)) {
|
67
69
|
throw Error('Invalid "data"');
|
68
70
|
}
|
71
|
+
if (data.email !== undefined) {
|
72
|
+
if (typeof data.email !== 'string') {
|
73
|
+
throw Error('Invalid "name"');
|
74
|
+
}
|
75
|
+
this.email = data.email;
|
76
|
+
}
|
69
77
|
}
|
70
78
|
}
|
71
79
|
|
@@ -99,9 +107,20 @@ const TEST_ITEM_ENTITY_DEFINITION = {
|
|
99
107
|
attributeName: 'gsi1_sk',
|
100
108
|
},
|
101
109
|
},
|
110
|
+
'gsi2_pk-gsi2_sk-index': {
|
111
|
+
pk: {
|
112
|
+
format: 'email#{email}',
|
113
|
+
attributeName: 'gsi2_pk',
|
114
|
+
},
|
115
|
+
sk: {
|
116
|
+
format: '#meta',
|
117
|
+
attributeName: 'gsi2_sk',
|
118
|
+
},
|
119
|
+
},
|
102
120
|
},
|
103
121
|
// field to be stored as JSON string
|
104
122
|
fieldsAsJsonString: ['data2'],
|
123
|
+
optionalIndexes: ['gsi2_pk-gsi2_sk-index'],
|
105
124
|
};
|
106
125
|
|
107
126
|
class TestItemRepository extends AbstractDynamoDbRepository<ITestItem, TestItem> {
|
@@ -176,6 +195,61 @@ describe('AbstractRepository', () => {
|
|
176
195
|
);
|
177
196
|
});
|
178
197
|
|
198
|
+
it('should create and return the item with multiple gsi fields', async () => {
|
199
|
+
ddbClientMock.on(PutCommand).resolves({
|
200
|
+
$metadata: {
|
201
|
+
httpStatusCode: 200,
|
202
|
+
},
|
203
|
+
});
|
204
|
+
const input: PutCommandInput = {
|
205
|
+
TableName: TABLE_NAME,
|
206
|
+
Item: {
|
207
|
+
pk: 'test_item#foo',
|
208
|
+
sk: '#meta',
|
209
|
+
gsi1_pk: 'country#au',
|
210
|
+
gsi1_sk: 'age#99',
|
211
|
+
gsi2_pk: 'email#foo@bar.xyz',
|
212
|
+
gsi2_sk: '#meta',
|
213
|
+
|
214
|
+
name: 'foo',
|
215
|
+
age: 99,
|
216
|
+
country: 'au',
|
217
|
+
data: {},
|
218
|
+
// "data2" property is defined to be stored as JSON string
|
219
|
+
data2: '{"foo":"bar","num":123}',
|
220
|
+
email: 'foo@bar.xyz',
|
221
|
+
},
|
222
|
+
ConditionExpression: `attribute_not_exists(pk)`,
|
223
|
+
};
|
224
|
+
|
225
|
+
const item = {
|
226
|
+
name: 'foo',
|
227
|
+
age: 99,
|
228
|
+
country: 'au',
|
229
|
+
data: {},
|
230
|
+
data2: {
|
231
|
+
foo: 'bar',
|
232
|
+
num: 123,
|
233
|
+
},
|
234
|
+
email: 'foo@bar.xyz',
|
235
|
+
};
|
236
|
+
const result = await repository.createItem(item);
|
237
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(PutCommand, input);
|
238
|
+
expect(result).toEqual(
|
239
|
+
new TestItem({
|
240
|
+
name: 'foo',
|
241
|
+
age: 99,
|
242
|
+
country: 'au',
|
243
|
+
data: {},
|
244
|
+
data2: {
|
245
|
+
foo: 'bar',
|
246
|
+
num: 123,
|
247
|
+
},
|
248
|
+
email: 'foo@bar.xyz',
|
249
|
+
}),
|
250
|
+
);
|
251
|
+
});
|
252
|
+
|
179
253
|
it('should throw error if invalid input', async () => {
|
180
254
|
const item = {
|
181
255
|
name: 'foo',
|
@@ -232,6 +306,8 @@ describe('AbstractRepository', () => {
|
|
232
306
|
Attributes: {
|
233
307
|
name: 'foo',
|
234
308
|
age: 99,
|
309
|
+
// country attribute is part of gsi key
|
310
|
+
// hence updating this will also update gsi key value
|
235
311
|
country: 'au-updated',
|
236
312
|
data: {},
|
237
313
|
},
|
@@ -239,12 +315,14 @@ describe('AbstractRepository', () => {
|
|
239
315
|
const input: UpdateCommandInput = {
|
240
316
|
TableName: TABLE_NAME,
|
241
317
|
Key: { pk: 'test_item#foo', sk: '#meta' },
|
242
|
-
UpdateExpression: 'SET #country = :country',
|
318
|
+
UpdateExpression: 'SET #country = :country, #gsi1_pk = :gsi1_pk',
|
243
319
|
ExpressionAttributeNames: {
|
244
320
|
'#country': 'country',
|
321
|
+
'#gsi1_pk': 'gsi1_pk',
|
245
322
|
},
|
246
323
|
ExpressionAttributeValues: {
|
247
324
|
':country': 'au-updated',
|
325
|
+
':gsi1_pk': 'country#au-updated',
|
248
326
|
},
|
249
327
|
ConditionExpression: `attribute_exists(pk)`,
|
250
328
|
};
|
@@ -265,6 +343,60 @@ describe('AbstractRepository', () => {
|
|
265
343
|
);
|
266
344
|
});
|
267
345
|
|
346
|
+
it('should only update the changed attributes', async () => {
|
347
|
+
ddbClientMock.on(GetCommand).resolves({
|
348
|
+
$metadata: {
|
349
|
+
httpStatusCode: 200,
|
350
|
+
},
|
351
|
+
Item: {
|
352
|
+
name: 'foo',
|
353
|
+
age: 99,
|
354
|
+
country: 'au',
|
355
|
+
data: {},
|
356
|
+
},
|
357
|
+
});
|
358
|
+
ddbClientMock.on(UpdateCommand).resolves({
|
359
|
+
$metadata: {
|
360
|
+
httpStatusCode: 200,
|
361
|
+
},
|
362
|
+
Attributes: {
|
363
|
+
name: 'foo',
|
364
|
+
age: 99,
|
365
|
+
country: 'au',
|
366
|
+
data: { active: true },
|
367
|
+
},
|
368
|
+
});
|
369
|
+
const input: UpdateCommandInput = {
|
370
|
+
TableName: TABLE_NAME,
|
371
|
+
Key: { pk: 'test_item#foo', sk: '#meta' },
|
372
|
+
UpdateExpression: 'SET #data = :data',
|
373
|
+
ExpressionAttributeNames: {
|
374
|
+
'#data': 'data',
|
375
|
+
},
|
376
|
+
ExpressionAttributeValues: {
|
377
|
+
':data': { active: true },
|
378
|
+
},
|
379
|
+
ConditionExpression: `attribute_exists(pk)`,
|
380
|
+
};
|
381
|
+
|
382
|
+
const updateItem = {
|
383
|
+
name: 'foo',
|
384
|
+
age: 99,
|
385
|
+
// this is the only change attribute value
|
386
|
+
data: { active: true },
|
387
|
+
};
|
388
|
+
const result = await repository.updateItem(updateItem);
|
389
|
+
expect(ddbClientMock).toHaveReceivedNthCommandWith(2, UpdateCommand, input);
|
390
|
+
expect(result).toEqual(
|
391
|
+
new TestItem({
|
392
|
+
name: 'foo',
|
393
|
+
age: 99,
|
394
|
+
country: 'au',
|
395
|
+
data: { active: true },
|
396
|
+
}),
|
397
|
+
);
|
398
|
+
});
|
399
|
+
|
268
400
|
it('should not trigger update request if the input attributes are same as in the existing item', async () => {
|
269
401
|
ddbClientMock.on(GetCommand).resolves({
|
270
402
|
$metadata: {
|
@@ -559,6 +691,68 @@ describe('AbstractRepository', () => {
|
|
559
691
|
]);
|
560
692
|
});
|
561
693
|
|
694
|
+
it('should remove duplicate items in BatchGetItem request', async () => {
|
695
|
+
ddbClientMock.on(BatchGetCommand).resolves({
|
696
|
+
$metadata: {
|
697
|
+
httpStatusCode: 200,
|
698
|
+
},
|
699
|
+
Responses: {
|
700
|
+
[TABLE_NAME]: [
|
701
|
+
{
|
702
|
+
name: 'foo',
|
703
|
+
age: 99,
|
704
|
+
country: 'au',
|
705
|
+
data: {},
|
706
|
+
data2: '{"foo":"bar","num":123}',
|
707
|
+
},
|
708
|
+
{
|
709
|
+
name: 'foo2',
|
710
|
+
age: 999,
|
711
|
+
country: 'au',
|
712
|
+
data: {},
|
713
|
+
data2: '{"foo":"bar","num":123}',
|
714
|
+
},
|
715
|
+
],
|
716
|
+
},
|
717
|
+
});
|
718
|
+
const input: BatchGetCommandInput = {
|
719
|
+
RequestItems: {
|
720
|
+
[TABLE_NAME]: {
|
721
|
+
Keys: [
|
722
|
+
{ pk: 'test_item#foo', sk: '#meta' },
|
723
|
+
{ pk: 'test_item#foo2', sk: '#meta' },
|
724
|
+
],
|
725
|
+
},
|
726
|
+
},
|
727
|
+
};
|
728
|
+
|
729
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }, { name: 'foo2' }];
|
730
|
+
const result = await repository.getItems(requestItems);
|
731
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(BatchGetCommand, input);
|
732
|
+
expect(result).toEqual([
|
733
|
+
new TestItem({
|
734
|
+
name: 'foo',
|
735
|
+
age: 99,
|
736
|
+
country: 'au',
|
737
|
+
data: {},
|
738
|
+
data2: {
|
739
|
+
foo: 'bar',
|
740
|
+
num: 123,
|
741
|
+
},
|
742
|
+
}),
|
743
|
+
new TestItem({
|
744
|
+
name: 'foo2',
|
745
|
+
age: 999,
|
746
|
+
country: 'au',
|
747
|
+
data: {},
|
748
|
+
data2: {
|
749
|
+
foo: 'bar',
|
750
|
+
num: 123,
|
751
|
+
},
|
752
|
+
}),
|
753
|
+
]);
|
754
|
+
});
|
755
|
+
|
562
756
|
it('should retry if unprocessed keys returned', async () => {
|
563
757
|
const input1: BatchGetCommandInput = {
|
564
758
|
RequestItems: {
|
@@ -792,6 +986,37 @@ describe('AbstractRepository', () => {
|
|
792
986
|
expect(ddbClientMock).toHaveReceivedCommandWith(BatchWriteCommand, input);
|
793
987
|
});
|
794
988
|
|
989
|
+
it('should remove duplicate items in batchWrite() request', async () => {
|
990
|
+
ddbClientMock.on(BatchWriteCommand).resolves({
|
991
|
+
$metadata: {
|
992
|
+
httpStatusCode: 200,
|
993
|
+
},
|
994
|
+
ItemCollectionMetrics: {
|
995
|
+
[TABLE_NAME]: [{}],
|
996
|
+
},
|
997
|
+
});
|
998
|
+
const input: BatchWriteCommandInput = {
|
999
|
+
RequestItems: {
|
1000
|
+
[TABLE_NAME]: [
|
1001
|
+
{
|
1002
|
+
DeleteRequest: {
|
1003
|
+
Key: { pk: 'test_item#foo', sk: '#meta' },
|
1004
|
+
},
|
1005
|
+
},
|
1006
|
+
{
|
1007
|
+
DeleteRequest: {
|
1008
|
+
Key: { pk: 'test_item#foo2', sk: '#meta' },
|
1009
|
+
},
|
1010
|
+
},
|
1011
|
+
],
|
1012
|
+
},
|
1013
|
+
};
|
1014
|
+
|
1015
|
+
const requestItems = [{ name: 'foo' }, { name: 'foo2' }, { name: 'foo2' }, { name: 'foo' }];
|
1016
|
+
await repository.deleteItems(requestItems);
|
1017
|
+
expect(ddbClientMock).toHaveReceivedCommandWith(BatchWriteCommand, input);
|
1018
|
+
});
|
1019
|
+
|
795
1020
|
it('should use re-try if unprocessed items returned', async () => {
|
796
1021
|
const input1: BatchWriteCommandInput = {
|
797
1022
|
RequestItems: {
|
@@ -1391,16 +1616,18 @@ describe('AbstractRepository', () => {
|
|
1391
1616
|
ConditionExpression: 'attribute_exists(pk)',
|
1392
1617
|
ExpressionAttributeNames: {
|
1393
1618
|
'#age': 'age',
|
1619
|
+
'#gsi1_sk': 'gsi1_sk',
|
1394
1620
|
},
|
1395
1621
|
ExpressionAttributeValues: {
|
1396
1622
|
':age': 55,
|
1623
|
+
':gsi1_sk': 'age#55',
|
1397
1624
|
},
|
1398
1625
|
Key: {
|
1399
1626
|
pk: 'test_item#foo2',
|
1400
1627
|
sk: '#meta',
|
1401
1628
|
},
|
1402
1629
|
TableName: 'test-table',
|
1403
|
-
UpdateExpression: 'SET #age = :age',
|
1630
|
+
UpdateExpression: 'SET #age = :age, #gsi1_sk = :gsi1_sk',
|
1404
1631
|
},
|
1405
1632
|
},
|
1406
1633
|
{
|
@@ -12,6 +12,7 @@ import {
|
|
12
12
|
|
13
13
|
import { Transaction, DynamoDbManager, MissingKeyValuesError, InvalidDbSchemaError } from '..';
|
14
14
|
import { InvalidDataFormatError } from '../error/InvalidDataFormatError';
|
15
|
+
import { UnknownKeyAttributeError } from '../error/UnknownKeyAttributeError';
|
15
16
|
|
16
17
|
export type QueryFilterTypeBeginsWith = {
|
17
18
|
type: 'begins_with';
|
@@ -68,6 +69,7 @@ export type EntityDefinition = {
|
|
68
69
|
keys: TableKeys;
|
69
70
|
indexes: TableIndexes;
|
70
71
|
fieldsAsJsonString: string[];
|
72
|
+
optionalIndexes?: (keyof TableIndexes)[];
|
71
73
|
};
|
72
74
|
|
73
75
|
const MAX_REATTEMPTS = 3;
|
@@ -80,6 +82,7 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
80
82
|
protected keys: TableKeys;
|
81
83
|
protected indexes: TableIndexes;
|
82
84
|
protected keysFormat: KeysFormat;
|
85
|
+
protected optionalIndexes: (keyof TableIndexes)[] = [];
|
83
86
|
|
84
87
|
// fields listed in this property are stored as a JSON string value in db
|
85
88
|
protected fieldsAsJsonString: string[];
|
@@ -105,6 +108,10 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
105
108
|
const index = this.indexes[key];
|
106
109
|
this.keysFormat[index.pk.attributeName] = index.pk.format;
|
107
110
|
this.keysFormat[index.sk.attributeName] = index.sk.format;
|
111
|
+
|
112
|
+
if (entityDefinition.optionalIndexes?.includes(key)) {
|
113
|
+
this.optionalIndexes.push(key);
|
114
|
+
}
|
108
115
|
});
|
109
116
|
}
|
110
117
|
|
@@ -144,10 +151,11 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
144
151
|
public async getItems(items: Partial<SHAPE>[]): Promise<DATA_CLASS[]> {
|
145
152
|
// this is the maximum items allowed by BatchGetItem()
|
146
153
|
const batchSize = 100;
|
154
|
+
const keys = this.getItemsKeys(items);
|
147
155
|
|
148
156
|
let result: DATA_CLASS[] = [];
|
149
|
-
for (let i = 0; i <
|
150
|
-
const batchResult = await this.getBatchItems(
|
157
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
158
|
+
const batchResult = await this.getBatchItems(keys.slice(i, i + batchSize));
|
151
159
|
result = result.concat(batchResult);
|
152
160
|
}
|
153
161
|
return result;
|
@@ -159,12 +167,12 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
159
167
|
* @param items
|
160
168
|
* @returns
|
161
169
|
*/
|
162
|
-
private async getBatchItems(
|
170
|
+
private async getBatchItems(keys: { [key: string]: string }[]): Promise<DATA_CLASS[]> {
|
163
171
|
let resultItems: DATA_CLASS[] = [];
|
164
172
|
|
165
173
|
let requestKeys: BatchGetCommandInput['RequestItems'] = {
|
166
174
|
[this.tableName]: {
|
167
|
-
Keys:
|
175
|
+
Keys: keys,
|
168
176
|
},
|
169
177
|
};
|
170
178
|
let reattemptsCount = 0;
|
@@ -196,9 +204,9 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
196
204
|
// this is the maximum items allowed by BatchWriteItem()
|
197
205
|
const batchSize = 25;
|
198
206
|
|
199
|
-
|
200
|
-
|
201
|
-
await this.deleteBatchItems(keys);
|
207
|
+
const keys = this.getItemsKeys(items);
|
208
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
209
|
+
await this.deleteBatchItems(keys.slice(i, i + batchSize));
|
202
210
|
}
|
203
211
|
}
|
204
212
|
|
@@ -224,7 +232,7 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
224
232
|
}
|
225
233
|
}
|
226
234
|
|
227
|
-
private
|
235
|
+
private getItemsKeys(items: Partial<SHAPE>[]) {
|
228
236
|
const keys: { [key: string]: string }[] = [];
|
229
237
|
for (const item of items) {
|
230
238
|
keys.push({
|
@@ -232,12 +240,17 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
232
240
|
[this.keys.sk.attributeName]: this.getSk(item),
|
233
241
|
});
|
234
242
|
}
|
235
|
-
// keys
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
243
|
+
// filter duplicate items keys
|
244
|
+
return keys.filter((key, index) => {
|
245
|
+
return (
|
246
|
+
index ===
|
247
|
+
keys.findIndex(
|
248
|
+
(key2) =>
|
249
|
+
key[this.keys.pk.attributeName] === key2[this.keys.pk.attributeName] &&
|
250
|
+
key[this.keys.sk.attributeName] === key2[this.keys.sk.attributeName],
|
251
|
+
)
|
252
|
+
);
|
253
|
+
});
|
241
254
|
}
|
242
255
|
|
243
256
|
/**
|
@@ -330,12 +343,15 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
330
343
|
this.assertValueMatchesModel(value);
|
331
344
|
|
332
345
|
this.convertSelectedValuesToJsonString(newValue);
|
346
|
+
this.convertSelectedValuesToJsonString(oldValue as Record<string, unknown>);
|
333
347
|
|
334
348
|
const updateExpression = [];
|
335
349
|
const expressionAttributeNames: Record<string, string> = {};
|
336
350
|
const expressionAttributeValues: Record<string, unknown> = {};
|
351
|
+
const updatedAttributes: string[] = [];
|
337
352
|
for (const modelProperty of Object.keys(newValue)) {
|
338
|
-
const propValue = newValue[modelProperty as keyof SHAPE]
|
353
|
+
const propValue = newValue[modelProperty as keyof SHAPE];
|
354
|
+
|
339
355
|
if (propValue === oldValue[modelProperty as keyof SHAPE]) {
|
340
356
|
// don't need to update the properties that are unchanged
|
341
357
|
continue;
|
@@ -347,19 +363,35 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
347
363
|
updateExpression.push(`${propName} = ${propValuePlaceHolder}`);
|
348
364
|
expressionAttributeNames[propName] = modelProperty;
|
349
365
|
expressionAttributeValues[propValuePlaceHolder] = propValue;
|
366
|
+
updatedAttributes.push(modelProperty);
|
350
367
|
}
|
351
|
-
if (!
|
368
|
+
if (!updatedAttributes.length) {
|
352
369
|
// nothing to update
|
353
370
|
return value;
|
354
371
|
}
|
355
372
|
|
373
|
+
// also update the gsi attributes if needed
|
374
|
+
Object.keys(this.indexes).forEach((key) => {
|
375
|
+
const index = this.indexes[key];
|
376
|
+
[index.pk.attributeName, index.sk.attributeName].forEach((keyAttributeName) => {
|
377
|
+
const keyFormat = this.keysFormat[keyAttributeName];
|
378
|
+
if (updatedAttributes.find((attr) => keyFormat.search(`{${attr}}`) !== -1)) {
|
379
|
+
const propName = `#${keyAttributeName}`;
|
380
|
+
const propValuePlaceHolder = `:${keyAttributeName}`;
|
381
|
+
updateExpression.push(`${propName} = ${propValuePlaceHolder}`);
|
382
|
+
expressionAttributeNames[propName] = keyAttributeName;
|
383
|
+
expressionAttributeValues[propValuePlaceHolder] = this.getKey(value, keyAttributeName);
|
384
|
+
}
|
385
|
+
});
|
386
|
+
});
|
387
|
+
|
356
388
|
const updateCommandInput = {
|
357
389
|
TableName: this.tableName,
|
358
390
|
Key: {
|
359
391
|
[this.keys.pk.attributeName]: this.getPk(newValue),
|
360
392
|
[this.keys.sk.attributeName]: this.getSk(newValue),
|
361
393
|
},
|
362
|
-
UpdateExpression: 'SET ' + updateExpression.join(','),
|
394
|
+
UpdateExpression: 'SET ' + updateExpression.join(', '),
|
363
395
|
ExpressionAttributeValues: expressionAttributeValues,
|
364
396
|
ExpressionAttributeNames: expressionAttributeNames,
|
365
397
|
ConditionExpression: `attribute_exists(${this.keys.pk.attributeName})`,
|
@@ -423,12 +455,20 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
423
455
|
[this.keys.sk.attributeName]: this.getSk({ ...value, ...additionalValue }),
|
424
456
|
};
|
425
457
|
|
426
|
-
Object.
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
458
|
+
for (const [key, index] of Object.entries(this.indexes)) {
|
459
|
+
try {
|
460
|
+
keyFields[index.pk.attributeName] = this.getKey({ ...value, ...additionalValue }, index.pk.attributeName);
|
461
|
+
keyFields[index.sk.attributeName] = this.getKey({ ...value, ...additionalValue }, index.sk.attributeName);
|
462
|
+
} catch (e) {
|
463
|
+
if ((e as Error).name === 'MissingKeyValuesError' && this.optionalIndexes.includes(key)) {
|
464
|
+
// ignore optional index fields missing error
|
465
|
+
delete keyFields[index.pk.attributeName];
|
466
|
+
delete keyFields[index.pk.attributeName];
|
467
|
+
continue;
|
468
|
+
}
|
469
|
+
throw e;
|
470
|
+
}
|
471
|
+
}
|
432
472
|
const putCommandInput: PutCommandInput = {
|
433
473
|
TableName: this.tableName,
|
434
474
|
Item: {
|
@@ -575,7 +615,7 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
575
615
|
protected getKey(item: Partial<SHAPE>, attributeName: keyof KeysFormat): string {
|
576
616
|
let keyFormat = this.keysFormat[attributeName];
|
577
617
|
if (keyFormat == undefined || !keyFormat.length) {
|
578
|
-
throw new
|
618
|
+
throw new UnknownKeyAttributeError(
|
579
619
|
`Key format not defined or empty for key attribute '${attributeName}' in entity ${this.entityName}`,
|
580
620
|
);
|
581
621
|
}
|
@@ -611,17 +651,6 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
|
|
611
651
|
return keyFormat;
|
612
652
|
}
|
613
653
|
|
614
|
-
/**
|
615
|
-
* Whether the given property name is part of the entity's pk/sk string
|
616
|
-
* @param propertyName
|
617
|
-
* @returns boolean
|
618
|
-
*/
|
619
|
-
private isPropertyPartOfKeys(propertyName: string) {
|
620
|
-
if (this.keysFormat[this.keys.pk.attributeName].search(`{${propertyName}}`) !== -1) return true;
|
621
|
-
if (this.keysFormat[this.keys.sk.attributeName].search(`{${propertyName}}`) !== -1) return true;
|
622
|
-
return false;
|
623
|
-
}
|
624
|
-
|
625
654
|
/**
|
626
655
|
* Validate the data matches with "DATA_MODEL"
|
627
656
|
* @param value
|