@squiz/db-lib 1.68.0 → 1.69.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/db-lib",
3
- "version": "1.68.0",
3
+ "version": "1.69.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "private": false,
@@ -23,6 +23,7 @@ import { mockClient } from 'aws-sdk-client-mock';
23
23
  import 'aws-sdk-client-mock-jest';
24
24
  import { DynamoDB } from '@aws-sdk/client-dynamodb';
25
25
  import crypto from 'crypto';
26
+ import { InvalidDataFormatError } from '../error/InvalidDataFormatError';
26
27
 
27
28
  const ddbClientMock = mockClient(DynamoDBDocumentClient);
28
29
  const ddbDoc = DynamoDBDocument.from(new DynamoDB({}));
@@ -32,6 +33,7 @@ interface ITestItem {
32
33
  age: number;
33
34
  country: string;
34
35
  data?: object;
36
+ data2?: object;
35
37
  }
36
38
 
37
39
  class TestItem implements ITestItem {
@@ -39,12 +41,14 @@ class TestItem implements ITestItem {
39
41
  public age: number;
40
42
  public country: string;
41
43
  public data: object;
44
+ public data2?: object;
42
45
 
43
46
  constructor(data: Partial<ITestItem> = {}) {
44
47
  this.name = data.name ?? 'default name';
45
48
  this.age = data.age ?? 0;
46
49
  this.country = data.country ?? 'default country';
47
50
  this.data = data.data ?? {};
51
+ this.data2 = data.data2 ?? {};
48
52
 
49
53
  if (typeof this.name !== 'string') {
50
54
  throw Error('Invalid "name"');
@@ -92,6 +96,8 @@ const TEST_ITEM_ENTITY_DEFINITION = {
92
96
  },
93
97
  },
94
98
  },
99
+ // field to be stored as JSON string
100
+ fieldsAsJsonString: ['data2'],
95
101
  };
96
102
 
97
103
  class TestItemRepository extends AbstractDynamoDbRepository<ITestItem, TestItem> {
@@ -134,6 +140,8 @@ describe('AbstractRepository', () => {
134
140
  age: 99,
135
141
  country: 'au',
136
142
  data: {},
143
+ // "data2" property is defined to be stored as JSON string
144
+ data2: '{"foo":"bar","num":123}',
137
145
  },
138
146
  ConditionExpression: `attribute_not_exists(pk)`,
139
147
  };
@@ -143,6 +151,10 @@ describe('AbstractRepository', () => {
143
151
  age: 99,
144
152
  country: 'au',
145
153
  data: {},
154
+ data2: {
155
+ foo: 'bar',
156
+ num: 123,
157
+ },
146
158
  };
147
159
  const result = await repository.createItem(item);
148
160
  expect(ddbClientMock).toHaveReceivedCommandWith(PutCommand, input);
@@ -152,6 +164,10 @@ describe('AbstractRepository', () => {
152
164
  age: 99,
153
165
  country: 'au',
154
166
  data: {},
167
+ data2: {
168
+ foo: 'bar',
169
+ num: 123,
170
+ },
155
171
  }),
156
172
  );
157
173
  });
@@ -367,6 +383,7 @@ describe('AbstractRepository', () => {
367
383
  age: 99,
368
384
  country: 'au',
369
385
  data: {},
386
+ data2: '{"foo":"bar","num":123}',
370
387
  },
371
388
  });
372
389
  const input: GetCommandInput = {
@@ -383,6 +400,10 @@ describe('AbstractRepository', () => {
383
400
  age: 99,
384
401
  country: 'au',
385
402
  data: {},
403
+ data2: {
404
+ foo: 'bar',
405
+ num: 123,
406
+ },
386
407
  }),
387
408
  );
388
409
  });
@@ -427,6 +448,29 @@ describe('AbstractRepository', () => {
427
448
  new MissingKeyValuesError('Key field "name" must be specified in the input item in entity test-item-entity'),
428
449
  );
429
450
  });
451
+
452
+ it('should throw error if JSON string field has non-string data', async () => {
453
+ ddbClientMock.on(GetCommand).resolves({
454
+ $metadata: {
455
+ httpStatusCode: 200,
456
+ },
457
+ Item: {
458
+ name: 'foo',
459
+ age: 99, // should be number
460
+ country: 'au',
461
+ data: {},
462
+ data2: {
463
+ foo: 'bar',
464
+ num: 123,
465
+ },
466
+ },
467
+ });
468
+
469
+ const partialItem = { name: 'foo' };
470
+ await expect(repository.getItem(partialItem)).rejects.toEqual(
471
+ new InvalidDataFormatError(`Field 'data2' defined as JSON String has a non-string data`),
472
+ );
473
+ });
430
474
  });
431
475
 
432
476
  describe('queryItems()', () => {
@@ -7,6 +7,7 @@ import {
7
7
  } from '@aws-sdk/lib-dynamodb';
8
8
 
9
9
  import { Transaction, DynamoDbManager, MissingKeyValuesError, InvalidDbSchemaError } from '..';
10
+ import { InvalidDataFormatError } from '../error/InvalidDataFormatError';
10
11
 
11
12
  interface Reader<T> {
12
13
  queryItems(partialItem: Partial<T>, useSortKey?: boolean, index?: keyof TableIndexes): Promise<T[]>;
@@ -41,6 +42,7 @@ export type KeysFormat = Record<keyof TableKeys | keyof TableIndexes, string>;
41
42
  export type EntityDefinition = {
42
43
  keys: TableKeys;
43
44
  indexes: TableIndexes;
45
+ fieldsAsJsonString: string[];
44
46
  };
45
47
 
46
48
  export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLASS extends SHAPE>
@@ -52,6 +54,9 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
52
54
  protected indexes: TableIndexes;
53
55
  protected keysFormat: KeysFormat;
54
56
 
57
+ // fields listed in this property are stored as a JSON string value in db
58
+ protected fieldsAsJsonString: string[];
59
+
55
60
  constructor(
56
61
  protected tableName: string,
57
62
  protected dbManager: DynamoDbManager<Repositories>,
@@ -63,6 +68,7 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
63
68
 
64
69
  this.keys = entityDefinition.keys;
65
70
  this.indexes = entityDefinition.indexes;
71
+ this.fieldsAsJsonString = entityDefinition.fieldsAsJsonString;
66
72
 
67
73
  this.keysFormat = {
68
74
  [this.keys.pk.attributeName]: this.keys.pk.format,
@@ -170,7 +176,10 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
170
176
  return undefined;
171
177
  }
172
178
 
173
- this.assertValueMatchesModel({ ...oldValue, ...newValue });
179
+ const value = { ...oldValue, ...newValue };
180
+ this.assertValueMatchesModel(value);
181
+
182
+ this.convertSelectedValuesToJsonString(newValue);
174
183
 
175
184
  const updateExpression = [];
176
185
  const expressionAttributeNames: Record<string, string> = {};
@@ -203,7 +212,7 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
203
212
  this.dbManager.addWriteTransactionItem(transaction.id, {
204
213
  Update: updateCommandInput,
205
214
  });
206
- return new this.classRef({ ...oldValue, ...newValue });
215
+ return new this.classRef(value);
207
216
  }
208
217
 
209
218
  let output: UpdateCommandOutput;
@@ -243,6 +252,7 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
243
252
  for (const modelProperty of Object.keys(value)) {
244
253
  columns[modelProperty] = value[modelProperty as keyof DATA_CLASS];
245
254
  }
255
+ this.convertSelectedValuesToJsonString(columns);
246
256
 
247
257
  const keyFields: Record<string, unknown> = {
248
258
  [this.keys.pk.attributeName]: this.getPk(value),
@@ -322,9 +332,26 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
322
332
  * @returns
323
333
  */
324
334
  protected hydrateItem(item: Record<string, unknown>): DATA_CLASS {
335
+ for (const fieldName of Object.keys(item)) {
336
+ if (this.fieldsAsJsonString.includes(fieldName)) {
337
+ if (typeof item[fieldName] === 'string') {
338
+ item[fieldName] = JSON.parse(item[fieldName] as string);
339
+ } else {
340
+ throw new InvalidDataFormatError(`Field '${fieldName}' defined as JSON String has a non-string data`);
341
+ }
342
+ }
343
+ }
325
344
  return new this.classRef(item);
326
345
  }
327
346
 
347
+ protected convertSelectedValuesToJsonString(item: Record<string, unknown>) {
348
+ for (const fieldName of Object.keys(item)) {
349
+ if (this.fieldsAsJsonString.includes(fieldName)) {
350
+ item[fieldName] = JSON.stringify(item[fieldName]);
351
+ }
352
+ }
353
+ }
354
+
328
355
  /**
329
356
  * Evaluate the partition key value from the partial item
330
357
  * @param item
@@ -0,0 +1,8 @@
1
+ import { InternalServerError } from '@squiz/dx-common-lib';
2
+
3
+ export class InvalidDataFormatError extends InternalServerError {
4
+ name = 'InvalidDataFormatError';
5
+ constructor(message: string) {
6
+ super(message);
7
+ }
8
+ }