@squiz/db-lib 1.73.0 → 1.75.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/AbstractRepository.postgres.integration.spec.d.ts +2 -0
  3. package/lib/AbstractRepository.postgres.integration.spec.d.ts.map +1 -0
  4. package/lib/{AbstractRepository.integration.spec.js → AbstractRepository.postgres.integration.spec.js} +1 -1
  5. package/lib/AbstractRepository.postgres.integration.spec.js.map +1 -0
  6. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +3 -7
  7. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
  8. package/lib/dynamodb/AbstractDynamoDbRepository.js +59 -36
  9. package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -1
  10. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +2 -0
  11. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -1
  12. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +214 -2
  13. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
  14. package/lib/error/UnknownKeyAttributeError.d.ts +6 -0
  15. package/lib/error/UnknownKeyAttributeError.d.ts.map +1 -0
  16. package/lib/error/UnknownKeyAttributeError.js +12 -0
  17. package/lib/error/UnknownKeyAttributeError.js.map +1 -0
  18. package/package.json +1 -2
  19. package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +229 -2
  20. package/src/dynamodb/AbstractDynamoDbRepository.ts +64 -35
  21. package/src/error/UnknownKeyAttributeError.ts +8 -0
  22. package/tsconfig.tsbuildinfo +1 -1
  23. package/lib/AbstractRepository.integration.spec.d.ts +0 -2
  24. package/lib/AbstractRepository.integration.spec.d.ts.map +0 -1
  25. package/lib/AbstractRepository.integration.spec.js.map +0 -1
  26. /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 < items.length; i += batchSize) {
150
- const batchResult = await this.getBatchItems(items.slice(i, i + batchSize));
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(items: Partial<SHAPE>[]): Promise<DATA_CLASS[]> {
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: this.getBatchKeys(items),
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
- for (let i = 0; i < items.length; i += batchSize) {
200
- const keys = this.getBatchKeys(items.slice(i, i + batchSize));
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 getBatchKeys(items: Partial<SHAPE>[]) {
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.push({
236
- // [this.keys.pk.attributeName]: 'foo1',
237
- // [this.keys.sk.attributeName]: 'foo2',
238
- // });
239
-
240
- return keys;
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] ?? null;
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 (!updateExpression.length) {
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.keys(this.indexes).forEach((key) => {
427
- const index = this.indexes[key];
428
- keyFields[index.pk.attributeName] = this.getKey({ ...value, ...additionalValue }, index.pk.attributeName);
429
- keyFields[index.sk.attributeName] = this.getKey({ ...value, ...additionalValue }, index.sk.attributeName);
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 MissingKeyValuesError(
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
@@ -0,0 +1,8 @@
1
+ import { InternalServerError } from '@squiz/dx-common-lib';
2
+
3
+ export class UnknownKeyAttributeError extends InternalServerError {
4
+ name = 'UnknownKeyAttributeError';
5
+ constructor(message: string) {
6
+ super(message);
7
+ }
8
+ }