@squiz/db-lib 1.71.2 → 1.72.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/AbstractRepository.d.ts +2 -0
  3. package/lib/AbstractRepository.d.ts.map +1 -0
  4. package/lib/AbstractRepository.integration.spec.d.ts +1 -0
  5. package/lib/AbstractRepository.integration.spec.d.ts.map +1 -0
  6. package/lib/AbstractRepository.integration.spec.js +118 -0
  7. package/lib/AbstractRepository.integration.spec.js.map +1 -0
  8. package/lib/AbstractRepository.js +187 -0
  9. package/lib/AbstractRepository.js.map +1 -0
  10. package/lib/ConnectionManager.d.ts +1 -0
  11. package/lib/ConnectionManager.d.ts.map +1 -0
  12. package/lib/ConnectionManager.js +58 -0
  13. package/lib/ConnectionManager.js.map +1 -0
  14. package/lib/Migrator.d.ts +1 -0
  15. package/lib/Migrator.d.ts.map +1 -0
  16. package/lib/Migrator.js +160 -0
  17. package/lib/Migrator.js.map +1 -0
  18. package/lib/PostgresErrorCodes.d.ts +1 -0
  19. package/lib/PostgresErrorCodes.d.ts.map +1 -0
  20. package/lib/PostgresErrorCodes.js +274 -0
  21. package/lib/PostgresErrorCodes.js.map +1 -0
  22. package/lib/Repositories.d.ts +1 -0
  23. package/lib/Repositories.d.ts.map +1 -0
  24. package/lib/Repositories.js +3 -0
  25. package/lib/Repositories.js.map +1 -0
  26. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +50 -9
  27. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -0
  28. package/lib/dynamodb/AbstractDynamoDbRepository.js +456 -0
  29. package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -0
  30. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +1 -0
  31. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -0
  32. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +924 -0
  33. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -0
  34. package/lib/dynamodb/DynamoDbManager.d.ts +1 -0
  35. package/lib/dynamodb/DynamoDbManager.d.ts.map +1 -0
  36. package/lib/dynamodb/DynamoDbManager.js +66 -0
  37. package/lib/dynamodb/DynamoDbManager.js.map +1 -0
  38. package/lib/dynamodb/getDynamoDbOptions.d.ts +1 -0
  39. package/lib/dynamodb/getDynamoDbOptions.d.ts.map +1 -0
  40. package/lib/dynamodb/getDynamoDbOptions.js +15 -0
  41. package/lib/dynamodb/getDynamoDbOptions.js.map +1 -0
  42. package/lib/error/DuplicateItemError.d.ts +1 -0
  43. package/lib/error/DuplicateItemError.d.ts.map +1 -0
  44. package/lib/error/DuplicateItemError.js +12 -0
  45. package/lib/error/DuplicateItemError.js.map +1 -0
  46. package/lib/error/InvalidDataFormatError.d.ts +1 -0
  47. package/lib/error/InvalidDataFormatError.d.ts.map +1 -0
  48. package/lib/error/InvalidDataFormatError.js +12 -0
  49. package/lib/error/InvalidDataFormatError.js.map +1 -0
  50. package/lib/error/InvalidDbSchemaError.d.ts +1 -0
  51. package/lib/error/InvalidDbSchemaError.d.ts.map +1 -0
  52. package/lib/error/InvalidDbSchemaError.js +12 -0
  53. package/lib/error/InvalidDbSchemaError.js.map +1 -0
  54. package/lib/error/MissingKeyValuesError.d.ts +1 -0
  55. package/lib/error/MissingKeyValuesError.d.ts.map +1 -0
  56. package/lib/error/MissingKeyValuesError.js +12 -0
  57. package/lib/error/MissingKeyValuesError.js.map +1 -0
  58. package/lib/error/TransactionError.d.ts +1 -0
  59. package/lib/error/TransactionError.d.ts.map +1 -0
  60. package/lib/error/TransactionError.js +12 -0
  61. package/lib/error/TransactionError.js.map +1 -0
  62. package/lib/getConnectionInfo.d.ts +1 -0
  63. package/lib/getConnectionInfo.d.ts.map +1 -0
  64. package/lib/getConnectionInfo.js +30 -0
  65. package/lib/getConnectionInfo.js.map +1 -0
  66. package/lib/index.d.ts +1 -0
  67. package/lib/index.d.ts.map +1 -0
  68. package/lib/index.js +33 -70416
  69. package/lib/index.js.map +1 -7
  70. package/package.json +5 -5
  71. package/src/AbstractRepository.ts +26 -20
  72. package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +289 -37
  73. package/src/dynamodb/AbstractDynamoDbRepository.ts +140 -31
  74. package/src/dynamodb/getDynamoDbOptions.ts +1 -1
  75. package/tsconfig.json +5 -2
  76. package/tsconfig.tsbuildinfo +1 -1
  77. package/build.js +0 -31
@@ -9,14 +9,34 @@ import {
9
9
  import { Transaction, DynamoDbManager, MissingKeyValuesError, InvalidDbSchemaError } from '..';
10
10
  import { InvalidDataFormatError } from '../error/InvalidDataFormatError';
11
11
 
12
+ export type QueryFilterTypeBeginsWith = {
13
+ type: 'begins_with';
14
+ keyword: string;
15
+ };
16
+
17
+ export type QueryOptions = {
18
+ // whether to match sort key along with the partition key
19
+ // if set to true this will return 1 item maximum
20
+ useSortKey?: boolean;
21
+ // table index to use (one of tables GSIs)
22
+ index?: keyof TableIndexes;
23
+ // number of items to limit in the result
24
+ limit?: number;
25
+ // result order based on the sort key
26
+ order?: 'desc' | 'asc';
27
+ // filter operation on the sort key
28
+ filter?: QueryFilterTypeBeginsWith;
29
+ };
30
+
12
31
  interface Reader<T> {
13
- queryItems(partialItem: Partial<T>, useSortKey?: boolean, index?: keyof TableIndexes): Promise<T[]>;
32
+ queryItems(partialItem: Partial<T>, options?: QueryOptions): Promise<T[]>;
33
+
14
34
  getItem(id: string | Partial<T>): Promise<T | undefined>;
15
35
  }
16
36
 
17
37
  interface Writer<T> {
18
38
  createItem(item: Partial<T>): Promise<T>;
19
- updateItem(partialItem: Partial<T>, newValue: Partial<T>): Promise<T | undefined>;
39
+ updateItem(partialItem: Partial<T>): Promise<T | undefined>;
20
40
  deleteItem(partialItem: Partial<T>): Promise<number>;
21
41
  }
22
42
 
@@ -104,20 +124,68 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
104
124
  return this.hydrateItem(output.Item);
105
125
  }
106
126
 
127
+ /**
128
+ * Get the single item each matching the key fields value in the given
129
+ * partial items. Will throw MissingKeyValuesError if key field values
130
+ * are missing
131
+ * Uses batchGet() to request 100 items in a batch
132
+ *
133
+ * @param item
134
+ *
135
+ * @throws MissingKeyValuesError
136
+ */
137
+ public async getItems(items: Partial<SHAPE>[]): Promise<DATA_CLASS[]> {
138
+ // this is the maximum items allowed by BatchGetItem()
139
+ const batchSize = 100;
140
+
141
+ let result: DATA_CLASS[] = [];
142
+ for (let i = 0; i < items.length; i += batchSize) {
143
+ const batchResult = await this.getBatchItems(items.slice(i, i + batchSize));
144
+ result = result.concat(batchResult);
145
+ }
146
+ return result;
147
+ }
148
+
149
+ /**
150
+ * Returns the batch items from the items primary key
151
+ *
152
+ * @param items
153
+ * @returns
154
+ */
155
+ private async getBatchItems(items: Partial<SHAPE>[]): Promise<DATA_CLASS[]> {
156
+ const keys: Record<string, string>[] = [];
157
+ for (const item of items) {
158
+ keys.push({
159
+ [this.keys.pk.attributeName]: this.getPk(item),
160
+ [this.keys.sk.attributeName]: this.getSk(item),
161
+ });
162
+ }
163
+ const output = await this.client.batchGet({
164
+ RequestItems: {
165
+ [this.tableName]: {
166
+ Keys: keys,
167
+ },
168
+ },
169
+ });
170
+
171
+ if (output.Responses && output.Responses[this.tableName].length) {
172
+ return output.Responses[this.tableName].map((i) => this.hydrateItem(i));
173
+ }
174
+ return [];
175
+ }
176
+
107
177
  /**
108
178
  * Finds all the items matching the partition key or
109
179
  * the gsi key (when gsi index name is specified)
110
180
  *
111
181
  * @param item
112
- * @param useSortKey
113
- * @param index
182
+ * @param options
114
183
  * @throws MissingKeyValuesError
115
184
  */
116
- public async queryItems(
117
- item: Partial<SHAPE>,
118
- useSortKey: boolean = false,
119
- index?: keyof TableIndexes,
120
- ): Promise<DATA_CLASS[]> {
185
+ public async queryItems(item: Partial<SHAPE>, options?: QueryOptions): Promise<DATA_CLASS[]> {
186
+ const useSortKey = options?.useSortKey || options?.filter;
187
+ const index = options?.index;
188
+
121
189
  let pkName = this.keys.pk.attributeName;
122
190
  let skName = this.keys.sk.attributeName;
123
191
  let indexName = null;
@@ -136,8 +204,8 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
136
204
  const expressionAttributeNames: Record<string, string> = { '#pkName': pkName };
137
205
  const expressionAttributeValues: Record<string, unknown> = { ':pkValue': pk };
138
206
  if (useSortKey) {
139
- const sk = this.getKey(item, skName);
140
- keyConditionExpression.push('#skName = :skValue');
207
+ const sk = options?.filter?.keyword ?? this.getKey(item, skName);
208
+ keyConditionExpression.push(this.getFilterKeyConditionExpression(options?.filter));
141
209
  expressionAttributeNames['#skName'] = skName;
142
210
  expressionAttributeValues[':skValue'] = sk;
143
211
  }
@@ -151,27 +219,43 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
151
219
  if (indexName) {
152
220
  queryCommandInput['IndexName'] = String(indexName);
153
221
  }
154
- const output = await this.client.query(queryCommandInput);
155
222
 
223
+ if (options?.order !== undefined) {
224
+ queryCommandInput.ScanIndexForward = options.order === 'asc';
225
+ }
226
+ if (options?.limit !== undefined) {
227
+ queryCommandInput.Limit = options.limit;
228
+ }
229
+
230
+ const output = await this.client.query(queryCommandInput);
156
231
  return !output.Items ? [] : output.Items.map((item) => this.hydrateItem(item));
157
232
  }
158
233
 
234
+ /**
235
+ * Evaluate filter condition for sort key
236
+ * @param filter
237
+ * @returns string
238
+ */
239
+ private getFilterKeyConditionExpression(filter: QueryOptions['filter']): string {
240
+ if (filter === undefined) {
241
+ return '#skName = :skValue';
242
+ } else if (filter.type === 'begins_with') {
243
+ return 'begins_with(#skName, :skValue)';
244
+ }
245
+ throw new Error(`Invalid query filter type: ${(filter as any)?.type}`);
246
+ }
247
+
159
248
  /**
160
249
  * Update the existing item matching the key fields value
161
- * in the passed in partialItem
162
- * @param partialItem
250
+ * in the passed newValue
163
251
  * @param newValue
164
252
  * @param transaction
165
253
  *
166
254
  * @returns Promise<SHAPE | undefined>
167
255
  * @throws MissingKeyValuesError
168
256
  */
169
- public async updateItem(
170
- partialItem: Partial<SHAPE>,
171
- newValue: Exclude<Partial<SHAPE>, Record<string, never>>,
172
- transaction: Transaction = {},
173
- ): Promise<DATA_CLASS | undefined> {
174
- const oldValue = await this.getItem(partialItem);
257
+ public async updateItem(newValue: Partial<SHAPE>, transaction: Transaction = {}): Promise<DATA_CLASS | undefined> {
258
+ const oldValue = await this.getItem(newValue);
175
259
  if (oldValue === undefined) {
176
260
  return undefined;
177
261
  }
@@ -185,20 +269,29 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
185
269
  const expressionAttributeNames: Record<string, string> = {};
186
270
  const expressionAttributeValues: Record<string, unknown> = {};
187
271
  for (const modelProperty of Object.keys(newValue)) {
272
+ const propValue = newValue[modelProperty as keyof SHAPE] ?? null;
273
+ if (propValue === oldValue[modelProperty as keyof SHAPE]) {
274
+ // don't need to update the properties that are unchanged
275
+ continue;
276
+ }
277
+
188
278
  const propName = `#${modelProperty}`;
189
- const propValue = `:${modelProperty}`;
279
+ const propValuePlaceHolder = `:${modelProperty}`;
190
280
 
191
- updateExpression.push(`${propName} = ${propValue}`);
281
+ updateExpression.push(`${propName} = ${propValuePlaceHolder}`);
192
282
  expressionAttributeNames[propName] = modelProperty;
193
-
194
- expressionAttributeValues[propValue] = newValue[modelProperty as keyof SHAPE];
283
+ expressionAttributeValues[propValuePlaceHolder] = propValue;
284
+ }
285
+ if (!updateExpression.length) {
286
+ // nothing to update
287
+ return value;
195
288
  }
196
289
 
197
290
  const updateCommandInput = {
198
291
  TableName: this.tableName,
199
292
  Key: {
200
- [this.keys.pk.attributeName]: this.getPk(partialItem),
201
- [this.keys.sk.attributeName]: this.getSk(partialItem),
293
+ [this.keys.pk.attributeName]: this.getPk(newValue),
294
+ [this.keys.sk.attributeName]: this.getSk(newValue),
202
295
  },
203
296
  UpdateExpression: 'SET ' + updateExpression.join(','),
204
297
  ExpressionAttributeValues: expressionAttributeValues,
@@ -240,12 +333,17 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
240
333
  *
241
334
  * @param value
242
335
  * @param transaction
336
+ * @param additionalValue Additional item properties that are not part of the DATA_CLASS
243
337
  *
244
338
  * @returns Promise<SHAPE>
245
339
  * @throws DuplicateItemError
246
340
  * @throws MissingKeyValuesError
247
341
  */
248
- public async createItem(value: DATA_CLASS, transaction: Transaction = {}): Promise<DATA_CLASS> {
342
+ public async createItem(
343
+ value: DATA_CLASS,
344
+ transaction: Transaction = {},
345
+ additionalValue: Partial<SHAPE> = {},
346
+ ): Promise<DATA_CLASS> {
249
347
  this.assertValueMatchesModel(value);
250
348
 
251
349
  const columns: any = {};
@@ -255,14 +353,14 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
255
353
  this.convertSelectedValuesToJsonString(columns);
256
354
 
257
355
  const keyFields: Record<string, unknown> = {
258
- [this.keys.pk.attributeName]: this.getPk(value),
259
- [this.keys.sk.attributeName]: this.getSk(value),
356
+ [this.keys.pk.attributeName]: this.getPk({ ...value, ...additionalValue }),
357
+ [this.keys.sk.attributeName]: this.getSk({ ...value, ...additionalValue }),
260
358
  };
261
359
 
262
360
  Object.keys(this.indexes).forEach((key) => {
263
361
  const index = this.indexes[key];
264
- keyFields[index.pk.attributeName] = this.getKey(value, index.pk.attributeName);
265
- keyFields[index.sk.attributeName] = this.getKey(value, index.sk.attributeName);
362
+ keyFields[index.pk.attributeName] = this.getKey({ ...value, ...additionalValue }, index.pk.attributeName);
363
+ keyFields[index.sk.attributeName] = this.getKey({ ...value, ...additionalValue }, index.sk.attributeName);
266
364
  });
267
365
 
268
366
  const putCommandInput: PutCommandInput = {
@@ -447,6 +545,17 @@ export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLAS
447
545
  return keyFormat;
448
546
  }
449
547
 
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
+
450
559
  /**
451
560
  * Validate the data matches with "DATA_MODEL"
452
561
  * @param value
@@ -4,6 +4,6 @@ export const getDynamoDbOptions = (awsRegion: string, nodeEnv: 'production' | 'd
4
4
  }
5
5
  return {
6
6
  credentials: { accessKeyId: 'key', secretAccessKey: 'key' },
7
- endpoint: process.env.CI ? 'http://dynamodb-local:8000' : 'http://localhost:8000',
7
+ endpoint: process.env.CI ? 'http://dynamodb-local:8000' : process.env.DYNAMO_DB_HOST ?? 'http://localhost:8000',
8
8
  };
9
9
  };
package/tsconfig.json CHANGED
@@ -3,9 +3,12 @@
3
3
  "compilerOptions": {
4
4
  "outDir": "lib/",
5
5
  "resolveJsonModule": false,
6
+ "module": "Node16",
7
+ "moduleResolution": "Node16",
6
8
  "composite": true,
7
- "emitDeclarationOnly": true,
8
- "rootDir": "./src"
9
+ "rootDir": "./src",
10
+ "declaration": true,
11
+ "declarationMap": true
9
12
  },
10
13
  "references": [
11
14
  {