@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.
- package/CHANGELOG.md +19 -0
- package/lib/AbstractRepository.d.ts +2 -0
- package/lib/AbstractRepository.d.ts.map +1 -0
- package/lib/AbstractRepository.integration.spec.d.ts +1 -0
- package/lib/AbstractRepository.integration.spec.d.ts.map +1 -0
- package/lib/AbstractRepository.integration.spec.js +118 -0
- package/lib/AbstractRepository.integration.spec.js.map +1 -0
- package/lib/AbstractRepository.js +187 -0
- package/lib/AbstractRepository.js.map +1 -0
- package/lib/ConnectionManager.d.ts +1 -0
- package/lib/ConnectionManager.d.ts.map +1 -0
- package/lib/ConnectionManager.js +58 -0
- package/lib/ConnectionManager.js.map +1 -0
- package/lib/Migrator.d.ts +1 -0
- package/lib/Migrator.d.ts.map +1 -0
- package/lib/Migrator.js +160 -0
- package/lib/Migrator.js.map +1 -0
- package/lib/PostgresErrorCodes.d.ts +1 -0
- package/lib/PostgresErrorCodes.d.ts.map +1 -0
- package/lib/PostgresErrorCodes.js +274 -0
- package/lib/PostgresErrorCodes.js.map +1 -0
- package/lib/Repositories.d.ts +1 -0
- package/lib/Repositories.d.ts.map +1 -0
- package/lib/Repositories.js +3 -0
- package/lib/Repositories.js.map +1 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +50 -9
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.js +456 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +1 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +924 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -0
- package/lib/dynamodb/DynamoDbManager.d.ts +1 -0
- package/lib/dynamodb/DynamoDbManager.d.ts.map +1 -0
- package/lib/dynamodb/DynamoDbManager.js +66 -0
- package/lib/dynamodb/DynamoDbManager.js.map +1 -0
- package/lib/dynamodb/getDynamoDbOptions.d.ts +1 -0
- package/lib/dynamodb/getDynamoDbOptions.d.ts.map +1 -0
- package/lib/dynamodb/getDynamoDbOptions.js +15 -0
- package/lib/dynamodb/getDynamoDbOptions.js.map +1 -0
- package/lib/error/DuplicateItemError.d.ts +1 -0
- package/lib/error/DuplicateItemError.d.ts.map +1 -0
- package/lib/error/DuplicateItemError.js +12 -0
- package/lib/error/DuplicateItemError.js.map +1 -0
- package/lib/error/InvalidDataFormatError.d.ts +1 -0
- package/lib/error/InvalidDataFormatError.d.ts.map +1 -0
- package/lib/error/InvalidDataFormatError.js +12 -0
- package/lib/error/InvalidDataFormatError.js.map +1 -0
- package/lib/error/InvalidDbSchemaError.d.ts +1 -0
- package/lib/error/InvalidDbSchemaError.d.ts.map +1 -0
- package/lib/error/InvalidDbSchemaError.js +12 -0
- package/lib/error/InvalidDbSchemaError.js.map +1 -0
- package/lib/error/MissingKeyValuesError.d.ts +1 -0
- package/lib/error/MissingKeyValuesError.d.ts.map +1 -0
- package/lib/error/MissingKeyValuesError.js +12 -0
- package/lib/error/MissingKeyValuesError.js.map +1 -0
- package/lib/error/TransactionError.d.ts +1 -0
- package/lib/error/TransactionError.d.ts.map +1 -0
- package/lib/error/TransactionError.js +12 -0
- package/lib/error/TransactionError.js.map +1 -0
- package/lib/getConnectionInfo.d.ts +1 -0
- package/lib/getConnectionInfo.d.ts.map +1 -0
- package/lib/getConnectionInfo.js +30 -0
- package/lib/getConnectionInfo.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +33 -70416
- package/lib/index.js.map +1 -7
- package/package.json +5 -5
- package/src/AbstractRepository.ts +26 -20
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +289 -37
- package/src/dynamodb/AbstractDynamoDbRepository.ts +140 -31
- package/src/dynamodb/getDynamoDbOptions.ts +1 -1
- package/tsconfig.json +5 -2
- package/tsconfig.tsbuildinfo +1 -1
- 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>,
|
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
|
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
|
113
|
-
* @param index
|
182
|
+
* @param options
|
114
183
|
* @throws MissingKeyValuesError
|
115
184
|
*/
|
116
|
-
public async queryItems(
|
117
|
-
|
118
|
-
|
119
|
-
|
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(
|
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
|
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
|
-
|
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
|
279
|
+
const propValuePlaceHolder = `:${modelProperty}`;
|
190
280
|
|
191
|
-
updateExpression.push(`${propName} = ${
|
281
|
+
updateExpression.push(`${propName} = ${propValuePlaceHolder}`);
|
192
282
|
expressionAttributeNames[propName] = modelProperty;
|
193
|
-
|
194
|
-
|
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(
|
201
|
-
[this.keys.sk.attributeName]: this.getSk(
|
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(
|
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
|
-
"
|
8
|
-
"
|
9
|
+
"rootDir": "./src",
|
10
|
+
"declaration": true,
|
11
|
+
"declarationMap": true
|
9
12
|
},
|
10
13
|
"references": [
|
11
14
|
{
|