@squiz/db-lib 1.64.0 → 1.66.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.d.ts +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +158 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts +24 -0
- package/lib/dynamodb/DynamoDbManager.d.ts +18 -0
- package/lib/error/DuplicateItemError.d.ts +5 -0
- package/lib/error/InvalidDbSchemaError.d.ts +5 -0
- package/lib/error/MissingKeyValuesError.d.ts +5 -0
- package/lib/error/TransactionError.d.ts +5 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +29504 -1605
- package/lib/index.js.map +4 -4
- package/package.json +7 -5
- package/src/AbstractRepository.ts +5 -4
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +804 -0
- package/src/dynamodb/AbstractDynamoDbRepository.ts +445 -0
- package/src/dynamodb/DynamoDbManager.ts +66 -0
- package/src/error/DuplicateItemError.ts +8 -0
- package/src/error/InvalidDbSchemaError.ts +8 -0
- package/src/error/MissingKeyValuesError.ts +8 -0
- package/src/error/TransactionError.ts +8 -0
- package/src/index.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,445 @@
|
|
1
|
+
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
|
2
|
+
|
3
|
+
import {
|
4
|
+
DynamoDBDocument,
|
5
|
+
QueryCommandInput,
|
6
|
+
UpdateCommandOutput,
|
7
|
+
PutCommandInput,
|
8
|
+
DeleteCommandInput,
|
9
|
+
} from '@aws-sdk/lib-dynamodb';
|
10
|
+
|
11
|
+
import { Transaction, DynamoDbManager, MissingKeyValuesError, DuplicateItemError, InvalidDbSchemaError } from '..';
|
12
|
+
|
13
|
+
interface Reader<T> {
|
14
|
+
queryItems(partialItem: Partial<T>, useSortKey?: boolean, index?: keyof TableIndexes): Promise<T[]>;
|
15
|
+
getItem(id: string | Partial<T>): Promise<T | undefined>;
|
16
|
+
}
|
17
|
+
|
18
|
+
interface Writer<T> {
|
19
|
+
createItem(item: Partial<T>): Promise<T>;
|
20
|
+
updateItem(partialItem: Partial<T>, newValue: Partial<T>): Promise<T | undefined>;
|
21
|
+
deleteItem(partialItem: Partial<T>): Promise<number>;
|
22
|
+
}
|
23
|
+
|
24
|
+
type Repository<T> = Reader<T> & Writer<T>;
|
25
|
+
|
26
|
+
type Repositories = Record<string, Repository<any>>;
|
27
|
+
|
28
|
+
export type TableKeys = {
|
29
|
+
pk: {
|
30
|
+
attributeName: string;
|
31
|
+
format: string;
|
32
|
+
};
|
33
|
+
sk: {
|
34
|
+
attributeName: string;
|
35
|
+
format: string;
|
36
|
+
};
|
37
|
+
};
|
38
|
+
|
39
|
+
export type TableIndexes = Record<string, TableKeys>;
|
40
|
+
|
41
|
+
export type KeysFormat = Record<keyof TableKeys | keyof TableIndexes, string>;
|
42
|
+
|
43
|
+
export type EntityDefinition = {
|
44
|
+
keys: TableKeys;
|
45
|
+
indexes: TableIndexes;
|
46
|
+
};
|
47
|
+
|
48
|
+
export abstract class AbstractDynamoDbRepository<SHAPE extends object, DATA_CLASS extends SHAPE>
|
49
|
+
implements Reader<SHAPE>, Writer<SHAPE>
|
50
|
+
{
|
51
|
+
protected client: DynamoDBDocument;
|
52
|
+
|
53
|
+
protected keys: TableKeys;
|
54
|
+
protected indexes: TableIndexes;
|
55
|
+
protected keysFormat: KeysFormat;
|
56
|
+
|
57
|
+
constructor(
|
58
|
+
protected tableName: string,
|
59
|
+
protected dbManager: DynamoDbManager<Repositories>,
|
60
|
+
protected entityName: string,
|
61
|
+
protected entityDefinition: EntityDefinition,
|
62
|
+
protected classRef: { new (data?: Record<string, unknown>): DATA_CLASS },
|
63
|
+
) {
|
64
|
+
this.client = dbManager.client;
|
65
|
+
|
66
|
+
this.keys = entityDefinition.keys;
|
67
|
+
this.indexes = entityDefinition.indexes;
|
68
|
+
|
69
|
+
this.keysFormat = {
|
70
|
+
[this.keys.pk.attributeName]: this.keys.pk.format,
|
71
|
+
[this.keys.sk.attributeName]: this.keys.sk.format,
|
72
|
+
};
|
73
|
+
Object.keys(this.indexes).forEach((key) => {
|
74
|
+
const index = this.indexes[key];
|
75
|
+
this.keysFormat[index.pk.attributeName] = index.pk.format;
|
76
|
+
this.keysFormat[index.sk.attributeName] = index.sk.format;
|
77
|
+
});
|
78
|
+
}
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Get the single item matching the key fields value in the given
|
82
|
+
* partial item. Will throw MissingKeyValuesError if key field values
|
83
|
+
* are missing
|
84
|
+
*
|
85
|
+
* @param item
|
86
|
+
*
|
87
|
+
* @throws MissingKeyValuesError
|
88
|
+
*/
|
89
|
+
public async getItem(item: Partial<SHAPE>): Promise<DATA_CLASS | undefined> {
|
90
|
+
const output = await this.client.get({
|
91
|
+
TableName: this.tableName,
|
92
|
+
Key: {
|
93
|
+
[this.keys.pk.attributeName]: this.getPk(item),
|
94
|
+
[this.keys.sk.attributeName]: this.getSk(item),
|
95
|
+
},
|
96
|
+
});
|
97
|
+
if (output.Item === undefined) {
|
98
|
+
return undefined;
|
99
|
+
}
|
100
|
+
return this.hydrateItem(output.Item);
|
101
|
+
}
|
102
|
+
|
103
|
+
/**
|
104
|
+
* Finds all the items matching the partition key or
|
105
|
+
* the gsi key (when gsi index name is specified)
|
106
|
+
*
|
107
|
+
* @param item
|
108
|
+
* @param useSortKey
|
109
|
+
* @param index
|
110
|
+
* @throws MissingKeyValuesError
|
111
|
+
*/
|
112
|
+
public async queryItems(
|
113
|
+
item: Partial<SHAPE>,
|
114
|
+
useSortKey: boolean = false,
|
115
|
+
index?: keyof TableIndexes,
|
116
|
+
): Promise<DATA_CLASS[]> {
|
117
|
+
let pkName = this.keys.pk.attributeName;
|
118
|
+
let skName = this.keys.sk.attributeName;
|
119
|
+
let indexName = null;
|
120
|
+
|
121
|
+
if (index) {
|
122
|
+
if (this.indexes[index] === undefined) {
|
123
|
+
throw new MissingKeyValuesError(`Table index '${index}' not defined on entity ${this.entityName}`);
|
124
|
+
}
|
125
|
+
indexName = index;
|
126
|
+
pkName = this.indexes[index].pk.attributeName;
|
127
|
+
skName = this.indexes[index].sk.attributeName;
|
128
|
+
}
|
129
|
+
|
130
|
+
const pk = this.getKey(item, pkName);
|
131
|
+
const keyConditionExpression = ['#pkName = :pkValue'];
|
132
|
+
const expressionAttributeNames: Record<string, string> = { '#pkName': pkName };
|
133
|
+
const expressionAttributeValues: Record<string, unknown> = { ':pkValue': pk };
|
134
|
+
if (useSortKey) {
|
135
|
+
const sk = this.getKey(item, skName);
|
136
|
+
keyConditionExpression.push('#skName = :skValue');
|
137
|
+
expressionAttributeNames['#skName'] = skName;
|
138
|
+
expressionAttributeValues[':skValue'] = sk;
|
139
|
+
}
|
140
|
+
|
141
|
+
const queryCommandInput: QueryCommandInput = {
|
142
|
+
TableName: this.tableName,
|
143
|
+
KeyConditionExpression: keyConditionExpression.join(' AND '),
|
144
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
145
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
146
|
+
};
|
147
|
+
if (indexName) {
|
148
|
+
queryCommandInput['IndexName'] = String(indexName);
|
149
|
+
}
|
150
|
+
const output = await this.client.query(queryCommandInput);
|
151
|
+
|
152
|
+
return !output.Items ? [] : output.Items.map((item) => this.hydrateItem(item));
|
153
|
+
}
|
154
|
+
|
155
|
+
/**
|
156
|
+
* Update the existing item matching the key fields value
|
157
|
+
* in the passed in partialItem
|
158
|
+
* @param partialItem
|
159
|
+
* @param newValue
|
160
|
+
* @param transaction
|
161
|
+
*
|
162
|
+
* @returns Promise<SHAPE | undefined>
|
163
|
+
* @throws MissingKeyValuesError
|
164
|
+
*/
|
165
|
+
public async updateItem(
|
166
|
+
partialItem: Partial<SHAPE>,
|
167
|
+
newValue: Exclude<Partial<SHAPE>, Record<string, never>>,
|
168
|
+
transaction: Transaction = {},
|
169
|
+
): Promise<DATA_CLASS | undefined> {
|
170
|
+
const oldValue = await this.getItem(partialItem);
|
171
|
+
if (oldValue === undefined) {
|
172
|
+
return undefined;
|
173
|
+
}
|
174
|
+
|
175
|
+
this.assertValueMatchesModel({ ...oldValue, ...newValue });
|
176
|
+
|
177
|
+
const updateExpression = [];
|
178
|
+
const expressionAttributeNames: Record<string, string> = {};
|
179
|
+
const expressionAttributeValues: Record<string, unknown> = {};
|
180
|
+
for (const modelProperty of Object.keys(newValue)) {
|
181
|
+
const propName = `#${modelProperty}`;
|
182
|
+
const propValue = `:${modelProperty}`;
|
183
|
+
|
184
|
+
updateExpression.push(`${propName} = ${propValue}`);
|
185
|
+
expressionAttributeNames[propName] = modelProperty;
|
186
|
+
|
187
|
+
expressionAttributeValues[propValue] = newValue[modelProperty as keyof SHAPE];
|
188
|
+
}
|
189
|
+
|
190
|
+
const updateCommandInput = {
|
191
|
+
TableName: this.tableName,
|
192
|
+
Key: {
|
193
|
+
[this.keys.pk.attributeName]: this.getPk(partialItem),
|
194
|
+
[this.keys.sk.attributeName]: this.getSk(partialItem),
|
195
|
+
},
|
196
|
+
UpdateExpression: 'SET ' + updateExpression.join(','),
|
197
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
198
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
199
|
+
ConditionExpression: `attribute_exists(${this.keys.pk.attributeName})`,
|
200
|
+
};
|
201
|
+
|
202
|
+
if (transaction.id?.length) {
|
203
|
+
// this command will be executed together with
|
204
|
+
// other db write commands in the "transaction" block
|
205
|
+
this.dbManager.addWriteTransactionItem(transaction.id, {
|
206
|
+
Update: updateCommandInput,
|
207
|
+
});
|
208
|
+
return new this.classRef({ ...oldValue, ...newValue });
|
209
|
+
}
|
210
|
+
|
211
|
+
let output: UpdateCommandOutput;
|
212
|
+
try {
|
213
|
+
output = await this.client.update({
|
214
|
+
...updateCommandInput,
|
215
|
+
ReturnValues: 'ALL_NEW',
|
216
|
+
});
|
217
|
+
} catch (e) {
|
218
|
+
if (e instanceof ConditionalCheckFailedException) {
|
219
|
+
return undefined;
|
220
|
+
}
|
221
|
+
throw e;
|
222
|
+
}
|
223
|
+
|
224
|
+
let item: DATA_CLASS | undefined = undefined;
|
225
|
+
if (output.Attributes) {
|
226
|
+
item = this.hydrateItem(output.Attributes);
|
227
|
+
}
|
228
|
+
return item ? item : undefined;
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Adds new item to the table
|
233
|
+
*
|
234
|
+
* @param value
|
235
|
+
* @param transaction
|
236
|
+
*
|
237
|
+
* @returns Promise<SHAPE>
|
238
|
+
* @throws DuplicateItemError
|
239
|
+
* @throws MissingKeyValuesError
|
240
|
+
*/
|
241
|
+
public async createItem(value: DATA_CLASS, transaction: Transaction = {}): Promise<DATA_CLASS> {
|
242
|
+
this.assertValueMatchesModel(value);
|
243
|
+
|
244
|
+
const columns: any = {};
|
245
|
+
for (const modelProperty of Object.keys(value)) {
|
246
|
+
columns[modelProperty] = value[modelProperty as keyof DATA_CLASS];
|
247
|
+
}
|
248
|
+
|
249
|
+
const keyFields: Record<string, unknown> = {
|
250
|
+
[this.keys.pk.attributeName]: this.getPk(value),
|
251
|
+
[this.keys.sk.attributeName]: this.getSk(value),
|
252
|
+
};
|
253
|
+
|
254
|
+
Object.keys(this.indexes).forEach((key) => {
|
255
|
+
const index = this.indexes[key];
|
256
|
+
keyFields[index.pk.attributeName] = this.getKey(value, index.pk.attributeName);
|
257
|
+
keyFields[index.sk.attributeName] = this.getKey(value, index.sk.attributeName);
|
258
|
+
});
|
259
|
+
|
260
|
+
const putCommandInput: PutCommandInput = {
|
261
|
+
TableName: this.tableName,
|
262
|
+
Item: {
|
263
|
+
...keyFields,
|
264
|
+
...columns,
|
265
|
+
},
|
266
|
+
ConditionExpression: `attribute_not_exists(${this.keys.pk.attributeName})`,
|
267
|
+
};
|
268
|
+
|
269
|
+
if (transaction.id?.length) {
|
270
|
+
// this command will be executed together with
|
271
|
+
// other db write commands in the "transaction block"
|
272
|
+
this.dbManager.addWriteTransactionItem(transaction.id, {
|
273
|
+
Put: putCommandInput,
|
274
|
+
});
|
275
|
+
return value;
|
276
|
+
}
|
277
|
+
|
278
|
+
try {
|
279
|
+
await this.client.put(putCommandInput);
|
280
|
+
} catch (e) {
|
281
|
+
if (e instanceof ConditionalCheckFailedException) {
|
282
|
+
throw new DuplicateItemError(`Item already exists`);
|
283
|
+
}
|
284
|
+
throw e;
|
285
|
+
}
|
286
|
+
return value;
|
287
|
+
}
|
288
|
+
|
289
|
+
/**
|
290
|
+
* Deletes an item from the table
|
291
|
+
*
|
292
|
+
* @param partialItem
|
293
|
+
* @param transaction
|
294
|
+
* @returns number
|
295
|
+
* @throw MissingKeyValuesError
|
296
|
+
*/
|
297
|
+
public async deleteItem(partialItem: Partial<SHAPE>, transaction: Transaction = {}): Promise<number> {
|
298
|
+
const deleteCommandInput: DeleteCommandInput = {
|
299
|
+
TableName: this.tableName,
|
300
|
+
Key: {
|
301
|
+
[this.keys.pk.attributeName]: this.getPk(partialItem),
|
302
|
+
[this.keys.sk.attributeName]: this.getSk(partialItem),
|
303
|
+
},
|
304
|
+
ConditionExpression: `attribute_exists(${this.keys.pk.attributeName})`,
|
305
|
+
};
|
306
|
+
|
307
|
+
if (transaction.id?.length) {
|
308
|
+
// this command will be executed together with
|
309
|
+
// other db write commands in the "transaction block"
|
310
|
+
this.dbManager.addWriteTransactionItem(transaction.id, {
|
311
|
+
Delete: deleteCommandInput,
|
312
|
+
});
|
313
|
+
return 1;
|
314
|
+
}
|
315
|
+
|
316
|
+
try {
|
317
|
+
await this.client.delete(deleteCommandInput);
|
318
|
+
} catch (e) {
|
319
|
+
if (e instanceof ConditionalCheckFailedException) {
|
320
|
+
return 0;
|
321
|
+
}
|
322
|
+
throw e;
|
323
|
+
}
|
324
|
+
return 1;
|
325
|
+
}
|
326
|
+
|
327
|
+
/**
|
328
|
+
* Return repo model object from the db value
|
329
|
+
* @param item
|
330
|
+
* @returns
|
331
|
+
*/
|
332
|
+
protected hydrateItem(item: Record<string, unknown>): DATA_CLASS {
|
333
|
+
return new this.classRef(item);
|
334
|
+
}
|
335
|
+
|
336
|
+
/**
|
337
|
+
* Evaluate the partition key value from the partial item
|
338
|
+
* @param item
|
339
|
+
* @returns string
|
340
|
+
* @throw MissingKeyValuesError
|
341
|
+
*/
|
342
|
+
protected getPk(item: Partial<SHAPE>): string {
|
343
|
+
return this.getKey(item, this.keys.pk.attributeName);
|
344
|
+
}
|
345
|
+
|
346
|
+
/**
|
347
|
+
* Evaluate the sort key value from the partial item
|
348
|
+
* @param item
|
349
|
+
* @returns string
|
350
|
+
*
|
351
|
+
* @throw MissingKeyValuesError
|
352
|
+
*/
|
353
|
+
protected getSk(item: Partial<SHAPE>): string {
|
354
|
+
return this.getKey(item, this.keys.sk.attributeName);
|
355
|
+
}
|
356
|
+
|
357
|
+
/**
|
358
|
+
* Evaluate the key value from the
|
359
|
+
*
|
360
|
+
* Example 1:
|
361
|
+
* Input:
|
362
|
+
* - item: {id: foo, name: 'some-name' }
|
363
|
+
* - attributeName: pk
|
364
|
+
* - this.keysFormat = { pk: 'item#{id}', 'sk': '#meta', ... }
|
365
|
+
* Output:
|
366
|
+
* - 'item#foo'
|
367
|
+
*
|
368
|
+
* Example 2:
|
369
|
+
* Input:
|
370
|
+
* - item: {id: foo, name: 'some-name', itemType: 'A' }
|
371
|
+
* - attributeName: sk
|
372
|
+
* - this.keysFormat = { pk: 'item#{id}', 'sk': 'type#{itemType}', ... }
|
373
|
+
* Output:
|
374
|
+
* - 'type#A'
|
375
|
+
*
|
376
|
+
* Example 3:
|
377
|
+
* Input:
|
378
|
+
* - item: {id: foo, name: 'some-name' }
|
379
|
+
* - attributeName: sk
|
380
|
+
* - this.keysFormat = { pk: 'item#{id}', 'sk': 'name-type#{itemType}{name}', ... }
|
381
|
+
* Output:
|
382
|
+
* - Error: "Key field "itemType" must be specified in the input item"
|
383
|
+
*
|
384
|
+
* @param item
|
385
|
+
* @param attributeName
|
386
|
+
*
|
387
|
+
* @returns string
|
388
|
+
* @throw MissingKeyValuesError
|
389
|
+
*/
|
390
|
+
protected getKey(item: Partial<SHAPE>, attributeName: keyof KeysFormat): string {
|
391
|
+
let keyFormat = this.keysFormat[attributeName];
|
392
|
+
if (keyFormat == undefined || !keyFormat.length) {
|
393
|
+
throw new MissingKeyValuesError(
|
394
|
+
`Key format not defined or empty for key attribute '${attributeName}' in entity ${this.entityName}`,
|
395
|
+
);
|
396
|
+
}
|
397
|
+
|
398
|
+
const matches = keyFormat.match(/{[a-zA-Z]+?}/g);
|
399
|
+
const replacements: { property: keyof SHAPE; placeholder: string }[] = !matches
|
400
|
+
? []
|
401
|
+
: matches.map((match) => {
|
402
|
+
return {
|
403
|
+
property: match.slice(1, -1) as keyof SHAPE,
|
404
|
+
placeholder: match,
|
405
|
+
};
|
406
|
+
});
|
407
|
+
|
408
|
+
for (let i = 0; i < replacements.length; i++) {
|
409
|
+
const field = replacements[i].property;
|
410
|
+
if (item[field] === undefined) {
|
411
|
+
throw new MissingKeyValuesError(
|
412
|
+
`Key field "${String(field)}" must be specified in the input item in entity ${this.entityName}`,
|
413
|
+
);
|
414
|
+
}
|
415
|
+
keyFormat = keyFormat.replace(replacements[i].placeholder, String(item[field] ?? ''));
|
416
|
+
}
|
417
|
+
|
418
|
+
const moreMatches = keyFormat.match(/{[a-zA-Z]+?}/g);
|
419
|
+
if (moreMatches?.length) {
|
420
|
+
throw new MissingKeyValuesError(
|
421
|
+
`Cannot resolve key placeholder(s) for key attribute format '${this.keysFormat[attributeName]} in entity ${
|
422
|
+
this.entityName
|
423
|
+
}: '${moreMatches.join("','")}'`,
|
424
|
+
);
|
425
|
+
}
|
426
|
+
return keyFormat;
|
427
|
+
}
|
428
|
+
|
429
|
+
/**
|
430
|
+
* Validate the data matches with "DATA_MODEL"
|
431
|
+
* @param value
|
432
|
+
* @return void
|
433
|
+
*/
|
434
|
+
private assertValueMatchesModel(value: unknown) {
|
435
|
+
// Trigger AssertionError if model instantiation fails
|
436
|
+
// see the DATA_CLASS model class
|
437
|
+
const obj = new this.classRef(value as Record<string, any>);
|
438
|
+
const inputProperties = Object.keys(value as object);
|
439
|
+
const modelProperties = Object.keys(obj);
|
440
|
+
const excessProperties = inputProperties.filter((prop) => !modelProperties.includes(prop));
|
441
|
+
if (excessProperties.length > 0) {
|
442
|
+
throw new InvalidDbSchemaError(`Excess properties in entity ${this.entityName}: ${excessProperties.join(', ')}`);
|
443
|
+
}
|
444
|
+
}
|
445
|
+
}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import { DynamoDBDocument, TransactWriteCommandInput } from '@aws-sdk/lib-dynamodb';
|
2
|
+
import { randomUUID } from 'crypto';
|
3
|
+
import { TransactionError } from '../error/TransactionError';
|
4
|
+
|
5
|
+
export type Transaction = {
|
6
|
+
id?: string;
|
7
|
+
};
|
8
|
+
|
9
|
+
type TransactionItems = TransactWriteCommandInput['TransactItems'];
|
10
|
+
export type TransactionItem = NonNullable<TransactionItems>[number];
|
11
|
+
|
12
|
+
export class DynamoDbManager<TRepositories> {
|
13
|
+
private transactionItems: Record<string, TransactionItem[]>;
|
14
|
+
public repositories: TRepositories;
|
15
|
+
|
16
|
+
constructor(
|
17
|
+
public client: DynamoDBDocument,
|
18
|
+
repositoryCreator: (dbManager: DynamoDbManager<TRepositories>) => TRepositories,
|
19
|
+
) {
|
20
|
+
this.transactionItems = {};
|
21
|
+
this.repositories = repositoryCreator(this);
|
22
|
+
}
|
23
|
+
|
24
|
+
public async executeInTransaction<T>(func: (transaction: Transaction) => Promise<T>): Promise<T> {
|
25
|
+
const transactionId = randomUUID();
|
26
|
+
|
27
|
+
try {
|
28
|
+
this.startTransaction(transactionId);
|
29
|
+
const value = await func({ id: transactionId });
|
30
|
+
await this.executeTransaction(transactionId);
|
31
|
+
return value;
|
32
|
+
} finally {
|
33
|
+
this.closeTransaction(transactionId);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
public addWriteTransactionItem(transactionId: string, item: TransactionItem) {
|
38
|
+
if (this.transactionItems[transactionId] === undefined) {
|
39
|
+
throw new TransactionError(`No items in transaction '${transactionId}' to add transaction item to`);
|
40
|
+
}
|
41
|
+
|
42
|
+
this.transactionItems[transactionId].push(item);
|
43
|
+
}
|
44
|
+
|
45
|
+
private async executeTransaction(transactionId: string) {
|
46
|
+
if (this.transactionItems[transactionId] === undefined) {
|
47
|
+
throw new TransactionError(`No items in transaction '${transactionId}' to execute`);
|
48
|
+
}
|
49
|
+
|
50
|
+
return await this.client.transactWrite({
|
51
|
+
ClientRequestToken: transactionId,
|
52
|
+
TransactItems: this.transactionItems[transactionId],
|
53
|
+
});
|
54
|
+
}
|
55
|
+
|
56
|
+
private startTransaction(transactionId: string) {
|
57
|
+
if (this.transactionItems[transactionId] !== undefined) {
|
58
|
+
throw new TransactionError(`Transaction '${transactionId}' already started`);
|
59
|
+
}
|
60
|
+
this.transactionItems[transactionId] = [];
|
61
|
+
}
|
62
|
+
|
63
|
+
private closeTransaction(transactionId: string) {
|
64
|
+
delete this.transactionItems[transactionId];
|
65
|
+
}
|
66
|
+
}
|
package/src/index.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
export * from './AbstractRepository';
|
2
2
|
export * from './ConnectionManager';
|
3
|
+
export * from './dynamodb/DynamoDbManager';
|
4
|
+
export * from './dynamodb/AbstractDynamoDbRepository';
|
3
5
|
export * from './Migrator';
|
4
6
|
export * from './Repositories';
|
5
7
|
export * from './getConnectionInfo';
|
8
|
+
export * from './error/DuplicateItemError';
|
9
|
+
export * from './error/TransactionError';
|
10
|
+
export * from './error/MissingKeyValuesError';
|
11
|
+
export * from './error/InvalidDbSchemaError';
|
6
12
|
|
7
13
|
// Postgres
|
8
14
|
export * from './PostgresErrorCodes';
|