betterddb 0.2.0 → 0.4.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/.github/workflows/npm-publish.yml +33 -0
- package/README.md +152 -46
- package/lib/betterddb.d.ts +34 -41
- package/lib/betterddb.js +47 -363
- package/lib/builders/create-builder.d.ts +14 -0
- package/lib/builders/create-builder.js +74 -0
- package/lib/builders/delete-builder.d.ts +19 -0
- package/lib/builders/delete-builder.js +80 -0
- package/lib/builders/get-builder.d.ts +20 -0
- package/lib/builders/get-builder.js +79 -0
- package/lib/builders/query-builder.d.ts +27 -0
- package/lib/builders/query-builder.js +106 -0
- package/lib/builders/scan-builder.d.ts +20 -0
- package/lib/builders/scan-builder.js +76 -0
- package/lib/builders/update-builder.d.ts +35 -0
- package/lib/builders/update-builder.js +205 -0
- package/package.json +1 -1
- package/src/betterddb.ts +62 -424
- package/src/builders/create-builder.ts +82 -0
- package/src/builders/delete-builder.ts +84 -0
- package/src/builders/get-builder.ts +83 -0
- package/src/builders/query-builder.ts +127 -0
- package/src/builders/scan-builder.ts +90 -0
- package/src/builders/update-builder.ts +230 -0
- package/test/create.test.ts +59 -0
- package/test/delete.test.ts +58 -0
- package/test/get.test.ts +58 -0
- package/test/query.test.ts +73 -0
- package/test/scan.test.ts +66 -0
- package/test/update.test.ts +58 -0
- package/test/utils/table-setup.ts +55 -0
- package/test/placeholder.test.ts +0 -98
package/src/betterddb.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import { z, ZodSchema } from 'zod';
|
|
1
|
+
import { ZodSchema } from 'zod';
|
|
3
2
|
import { DynamoDB } from 'aws-sdk';
|
|
3
|
+
import { QueryBuilder } from './builders/query-builder';
|
|
4
|
+
import { ScanBuilder } from './builders/scan-builder';
|
|
5
|
+
import { UpdateBuilder } from './builders/update-builder';
|
|
6
|
+
import { CreateBuilder } from './builders/create-builder';
|
|
7
|
+
import { GetBuilder } from './builders/get-builder';
|
|
8
|
+
import { DeleteBuilder } from './builders/delete-builder';
|
|
4
9
|
|
|
5
10
|
export type PrimaryKeyValue = string | number;
|
|
6
11
|
|
|
@@ -66,6 +71,7 @@ export interface KeysConfig<T> {
|
|
|
66
71
|
export interface BetterDDBOptions<T> {
|
|
67
72
|
schema: ZodSchema<T>;
|
|
68
73
|
tableName: string;
|
|
74
|
+
entityName: string;
|
|
69
75
|
keys: KeysConfig<T>;
|
|
70
76
|
client: DynamoDB.DocumentClient;
|
|
71
77
|
/**
|
|
@@ -84,6 +90,7 @@ export interface BetterDDBOptions<T> {
|
|
|
84
90
|
export class BetterDDB<T> {
|
|
85
91
|
protected schema: ZodSchema<T>;
|
|
86
92
|
protected tableName: string;
|
|
93
|
+
protected entityName: string;
|
|
87
94
|
protected client: DynamoDB.DocumentClient;
|
|
88
95
|
protected keys: KeysConfig<T>;
|
|
89
96
|
protected autoTimestamps: boolean;
|
|
@@ -91,11 +98,33 @@ export class BetterDDB<T> {
|
|
|
91
98
|
constructor(options: BetterDDBOptions<T>) {
|
|
92
99
|
this.schema = options.schema;
|
|
93
100
|
this.tableName = options.tableName;
|
|
101
|
+
this.entityName = options.entityName.toUpperCase();
|
|
94
102
|
this.keys = options.keys;
|
|
95
103
|
this.client = options.client;
|
|
96
104
|
this.autoTimestamps = options.autoTimestamps ?? false;
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
public getKeys(): KeysConfig<T> {
|
|
108
|
+
return this.keys;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public getTableName(): string {
|
|
112
|
+
return this.tableName;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public getClient(): DynamoDB.DocumentClient {
|
|
116
|
+
return this.client;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
public getSchema(): ZodSchema<T> {
|
|
121
|
+
return this.schema;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public getAutoTimestamps(): boolean {
|
|
125
|
+
return this.autoTimestamps;
|
|
126
|
+
}
|
|
127
|
+
|
|
99
128
|
// Helper: Retrieve the key value from a KeyDefinition.
|
|
100
129
|
protected getKeyValue(def: KeyDefinition<T>, rawKey: Partial<T>): string {
|
|
101
130
|
if (typeof def === 'string' || typeof def === 'number' || typeof def === 'symbol') {
|
|
@@ -108,7 +137,7 @@ export class BetterDDB<T> {
|
|
|
108
137
|
/**
|
|
109
138
|
* Build the primary key from a raw key object.
|
|
110
139
|
*/
|
|
111
|
-
|
|
140
|
+
public buildKey(rawKey: Partial<T>): Record<string, any> {
|
|
112
141
|
const keyObj: Record<string, any> = {};
|
|
113
142
|
|
|
114
143
|
// For primary (partition) key:
|
|
@@ -136,7 +165,7 @@ export class BetterDDB<T> {
|
|
|
136
165
|
/**
|
|
137
166
|
* Build index attributes for each defined GSI.
|
|
138
167
|
*/
|
|
139
|
-
|
|
168
|
+
public buildIndexes(rawItem: Partial<T>): Record<string, any> {
|
|
140
169
|
const indexAttributes: Record<string, any> = {};
|
|
141
170
|
if (this.keys.gsis) {
|
|
142
171
|
for (const gsiName in this.keys.gsis) {
|
|
@@ -172,433 +201,42 @@ export class BetterDDB<T> {
|
|
|
172
201
|
* - Optionally injects timestamps,
|
|
173
202
|
* - Validates the item and writes it to DynamoDB.
|
|
174
203
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const now = new Date().toISOString();
|
|
178
|
-
item = { ...item, createdAt: now, updatedAt: now } as T;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const validated = this.schema.parse(item);
|
|
182
|
-
let finalItem = { ...validated };
|
|
183
|
-
|
|
184
|
-
// Compute and merge primary key.
|
|
185
|
-
const computedKeys = this.buildKey(validated);
|
|
186
|
-
finalItem = { ...finalItem, ...computedKeys };
|
|
187
|
-
|
|
188
|
-
// Compute and merge index attributes.
|
|
189
|
-
const indexAttributes = this.buildIndexes(validated);
|
|
190
|
-
finalItem = { ...finalItem, ...indexAttributes };
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
await this.client.put({ TableName: this.tableName, Item: finalItem as DynamoDB.DocumentClient.PutItemInputAttributeMap }).promise();
|
|
194
|
-
return validated;
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error('Error during create operation:', error);
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async get(rawKey: Partial<T>): Promise<T | null> {
|
|
202
|
-
const Key = this.buildKey(rawKey);
|
|
203
|
-
try {
|
|
204
|
-
const result = await this.client.get({ TableName: this.tableName, Key }).promise();
|
|
205
|
-
if (!result.Item) return null;
|
|
206
|
-
return this.schema.parse(result.Item);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
console.error('Error during get operation:', error);
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async update(
|
|
214
|
-
rawKey: Partial<T>,
|
|
215
|
-
update: Partial<T>,
|
|
216
|
-
options?: { expectedVersion?: number }
|
|
217
|
-
): Promise<T> {
|
|
218
|
-
const ExpressionAttributeNames: Record<string, string> = {};
|
|
219
|
-
const ExpressionAttributeValues: Record<string, any> = {};
|
|
220
|
-
const UpdateExpressionParts: string[] = [];
|
|
221
|
-
const ConditionExpressionParts: string[] = [];
|
|
222
|
-
|
|
223
|
-
// Exclude key fields from update.
|
|
224
|
-
const keyFieldNames = [
|
|
225
|
-
this.keys.primary.name,
|
|
226
|
-
this.keys.sort ? this.keys.sort.name : undefined
|
|
227
|
-
].filter(Boolean) as string[];
|
|
228
|
-
|
|
229
|
-
for (const [attr, value] of Object.entries(update)) {
|
|
230
|
-
if (keyFieldNames.includes(attr)) continue;
|
|
231
|
-
const attributeKey = `#${attr}`;
|
|
232
|
-
const valueKey = `:${attr}`;
|
|
233
|
-
ExpressionAttributeNames[attributeKey] = attr;
|
|
234
|
-
ExpressionAttributeValues[valueKey] = value;
|
|
235
|
-
UpdateExpressionParts.push(`${attributeKey} = ${valueKey}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (this.autoTimestamps) {
|
|
239
|
-
const now = new Date().toISOString();
|
|
240
|
-
ExpressionAttributeNames['#updatedAt'] = 'updatedAt';
|
|
241
|
-
ExpressionAttributeValues[':updatedAt'] = now;
|
|
242
|
-
UpdateExpressionParts.push('#updatedAt = :updatedAt');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (options?.expectedVersion !== undefined) {
|
|
246
|
-
ExpressionAttributeNames['#version'] = 'version';
|
|
247
|
-
ExpressionAttributeValues[':expectedVersion'] = options.expectedVersion;
|
|
248
|
-
ExpressionAttributeValues[':newVersion'] = options.expectedVersion + 1;
|
|
249
|
-
UpdateExpressionParts.push('#version = :newVersion');
|
|
250
|
-
ConditionExpressionParts.push('#version = :expectedVersion');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (UpdateExpressionParts.length === 0) {
|
|
254
|
-
throw new Error('No attributes provided to update');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ');
|
|
258
|
-
const params: DynamoDB.DocumentClient.UpdateItemInput = {
|
|
259
|
-
TableName: this.tableName,
|
|
260
|
-
Key: this.buildKey(rawKey),
|
|
261
|
-
UpdateExpression,
|
|
262
|
-
ExpressionAttributeNames,
|
|
263
|
-
ExpressionAttributeValues,
|
|
264
|
-
ReturnValues: 'ALL_NEW'
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
if (ConditionExpressionParts.length > 0) {
|
|
268
|
-
params.ConditionExpression = ConditionExpressionParts.join(' AND ');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const result = await this.client.update(params).promise();
|
|
273
|
-
if (!result.Attributes) {
|
|
274
|
-
throw new Error('No attributes returned after update');
|
|
275
|
-
}
|
|
276
|
-
return this.schema.parse(result.Attributes);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
console.error('Error during update operation:', error);
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async delete(rawKey: Partial<T>): Promise<void> {
|
|
284
|
-
const Key = this.buildKey(rawKey);
|
|
285
|
-
try {
|
|
286
|
-
await this.client.delete({ TableName: this.tableName, Key }).promise();
|
|
287
|
-
} catch (error) {
|
|
288
|
-
console.error('Error during delete operation:', error);
|
|
289
|
-
throw error;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async queryByGsi(
|
|
294
|
-
gsiName: string,
|
|
295
|
-
key: Partial<T>,
|
|
296
|
-
sortKeyCondition?: { operator: 'eq' | 'begins_with' | 'between'; values: any | [any, any] }
|
|
297
|
-
): Promise<T[]> {
|
|
298
|
-
if (!this.keys.gsis || !this.keys.gsis[gsiName]) {
|
|
299
|
-
throw new Error(`GSI "${gsiName}" is not configured`);
|
|
300
|
-
}
|
|
301
|
-
const indexConfig = this.keys.gsis[gsiName];
|
|
302
|
-
const ExpressionAttributeNames: Record<string, string> = {
|
|
303
|
-
[`#${indexConfig.primary.name}`]: indexConfig.primary.name
|
|
304
|
-
};
|
|
305
|
-
const ExpressionAttributeValues: Record<string, any> = {
|
|
306
|
-
[`:${indexConfig.primary.name}`]:
|
|
307
|
-
(typeof indexConfig.primary.definition === 'string' ||
|
|
308
|
-
typeof indexConfig.primary.definition === 'number' ||
|
|
309
|
-
typeof indexConfig.primary.definition === 'symbol')
|
|
310
|
-
? String((key as any)[indexConfig.primary.definition])
|
|
311
|
-
: indexConfig.primary.definition.build(key)
|
|
312
|
-
};
|
|
313
|
-
let KeyConditionExpression = `#${indexConfig.primary.name} = :${indexConfig.primary.name}`;
|
|
314
|
-
|
|
315
|
-
if (indexConfig.sort && sortKeyCondition) {
|
|
316
|
-
const skFieldName = indexConfig.sort.name;
|
|
317
|
-
ExpressionAttributeNames['#gsiSk'] = skFieldName;
|
|
318
|
-
switch (sortKeyCondition.operator) {
|
|
319
|
-
case 'eq':
|
|
320
|
-
ExpressionAttributeValues[':gsiSk'] = sortKeyCondition.values;
|
|
321
|
-
KeyConditionExpression += ' AND #gsiSk = :gsiSk';
|
|
322
|
-
break;
|
|
323
|
-
case 'begins_with':
|
|
324
|
-
ExpressionAttributeValues[':gsiSk'] = sortKeyCondition.values;
|
|
325
|
-
KeyConditionExpression += ' AND begins_with(#gsiSk, :gsiSk)';
|
|
326
|
-
break;
|
|
327
|
-
case 'between':
|
|
328
|
-
if (!Array.isArray(sortKeyCondition.values) || sortKeyCondition.values.length !== 2) {
|
|
329
|
-
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
330
|
-
}
|
|
331
|
-
ExpressionAttributeValues[':gsiSkStart'] = sortKeyCondition.values[0];
|
|
332
|
-
ExpressionAttributeValues[':gsiSkEnd'] = sortKeyCondition.values[1];
|
|
333
|
-
KeyConditionExpression += ' AND #gsiSk BETWEEN :gsiSkStart AND :gsiSkEnd';
|
|
334
|
-
break;
|
|
335
|
-
default:
|
|
336
|
-
throw new Error(`Unsupported sort key operator: ${sortKeyCondition.operator}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
const result = await this.client
|
|
341
|
-
.query({
|
|
342
|
-
TableName: this.tableName,
|
|
343
|
-
IndexName: gsiName,
|
|
344
|
-
KeyConditionExpression,
|
|
345
|
-
ExpressionAttributeNames,
|
|
346
|
-
ExpressionAttributeValues
|
|
347
|
-
})
|
|
348
|
-
.promise();
|
|
349
|
-
return (result.Items || []).map(item => this.schema.parse(item));
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error('Error during queryByGsi operation:', error);
|
|
352
|
-
throw error;
|
|
353
|
-
}
|
|
204
|
+
public create(item: T): CreateBuilder<T> {
|
|
205
|
+
return new CreateBuilder<T>(this, item);
|
|
354
206
|
}
|
|
355
207
|
|
|
356
208
|
/**
|
|
357
|
-
*
|
|
209
|
+
* Get an item by its primary key.
|
|
358
210
|
*/
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
sortKeyCondition?: { operator: 'eq' | 'begins_with' | 'between'; values: any | [any, any] },
|
|
362
|
-
options?: { limit?: number; lastKey?: Record<string, any> }
|
|
363
|
-
): Promise<{ items: T[]; lastKey?: Record<string, any> }> {
|
|
364
|
-
const pkAttrName = this.keys.primary.name;
|
|
365
|
-
const pkValue =
|
|
366
|
-
(typeof this.keys.primary.definition === 'string' ||
|
|
367
|
-
typeof this.keys.primary.definition === 'number' ||
|
|
368
|
-
typeof this.keys.primary.definition === 'symbol')
|
|
369
|
-
? String((rawKey as any)[this.keys.primary.definition])
|
|
370
|
-
: this.keys.primary.definition.build(rawKey);
|
|
371
|
-
|
|
372
|
-
const ExpressionAttributeNames: Record<string, string> = {
|
|
373
|
-
[`#${pkAttrName}`]: pkAttrName
|
|
374
|
-
};
|
|
375
|
-
const ExpressionAttributeValues: Record<string, any> = {
|
|
376
|
-
[`:${pkAttrName}`]: pkValue
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
let KeyConditionExpression = `#${pkAttrName} = :${pkAttrName}`;
|
|
380
|
-
|
|
381
|
-
if (this.keys.sort && sortKeyCondition) {
|
|
382
|
-
const skAttrName = this.keys.sort.name;
|
|
383
|
-
ExpressionAttributeNames[`#${skAttrName}`] = skAttrName;
|
|
384
|
-
switch (sortKeyCondition.operator) {
|
|
385
|
-
case 'eq':
|
|
386
|
-
ExpressionAttributeValues[':skValue'] = sortKeyCondition.values;
|
|
387
|
-
KeyConditionExpression += ` AND #${skAttrName} = :skValue`;
|
|
388
|
-
break;
|
|
389
|
-
case 'begins_with':
|
|
390
|
-
ExpressionAttributeValues[':skValue'] = sortKeyCondition.values;
|
|
391
|
-
KeyConditionExpression += ` AND begins_with(#${skAttrName}, :skValue)`;
|
|
392
|
-
break;
|
|
393
|
-
case 'between':
|
|
394
|
-
if (!Array.isArray(sortKeyCondition.values) || sortKeyCondition.values.length !== 2) {
|
|
395
|
-
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
396
|
-
}
|
|
397
|
-
ExpressionAttributeValues[':skStart'] = sortKeyCondition.values[0];
|
|
398
|
-
ExpressionAttributeValues[':skEnd'] = sortKeyCondition.values[1];
|
|
399
|
-
KeyConditionExpression += ` AND #${skAttrName} BETWEEN :skStart AND :skEnd`;
|
|
400
|
-
break;
|
|
401
|
-
default:
|
|
402
|
-
throw new Error(`Unsupported sort key operator: ${sortKeyCondition.operator}`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const queryParams: DynamoDB.DocumentClient.QueryInput = {
|
|
407
|
-
TableName: this.tableName,
|
|
408
|
-
KeyConditionExpression,
|
|
409
|
-
ExpressionAttributeNames,
|
|
410
|
-
ExpressionAttributeValues
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
if (options?.limit) {
|
|
414
|
-
queryParams.Limit = options.limit;
|
|
415
|
-
}
|
|
416
|
-
if (options?.lastKey) {
|
|
417
|
-
queryParams.ExclusiveStartKey = options.lastKey;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const result = await this.client.query(queryParams).promise();
|
|
421
|
-
const items = (result.Items || []).map(item => this.schema.parse(item));
|
|
422
|
-
return { items, lastKey: result.LastEvaluatedKey };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// ───── Transaction Helpers ─────────────────────────────
|
|
426
|
-
|
|
427
|
-
buildTransactPut(item: T): DynamoDB.DocumentClient.TransactWriteItem {
|
|
428
|
-
const computedKeys = this.buildKey(item);
|
|
429
|
-
const indexAttributes = this.buildIndexes(item);
|
|
430
|
-
const finalItem = { ...item, ...computedKeys, ...indexAttributes };
|
|
431
|
-
const validated = this.schema.parse(finalItem);
|
|
432
|
-
return {
|
|
433
|
-
Put: {
|
|
434
|
-
TableName: this.tableName,
|
|
435
|
-
Item: validated as DynamoDB.DocumentClient.PutItemInputAttributeMap
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
buildTransactUpdate(
|
|
441
|
-
rawKey: Partial<T>,
|
|
442
|
-
update: Partial<T>,
|
|
443
|
-
options?: { expectedVersion?: number }
|
|
444
|
-
): DynamoDB.DocumentClient.TransactWriteItem {
|
|
445
|
-
const ExpressionAttributeNames: Record<string, string> = {};
|
|
446
|
-
const ExpressionAttributeValues: Record<string, any> = {};
|
|
447
|
-
const UpdateExpressionParts: string[] = [];
|
|
448
|
-
const ConditionExpressionParts: string[] = [];
|
|
449
|
-
|
|
450
|
-
const keyFieldNames = [
|
|
451
|
-
this.keys.primary.name,
|
|
452
|
-
this.keys.sort ? this.keys.sort.name : undefined
|
|
453
|
-
].filter(Boolean) as string[];
|
|
454
|
-
|
|
455
|
-
for (const [attr, value] of Object.entries(update)) {
|
|
456
|
-
if (keyFieldNames.includes(attr)) continue;
|
|
457
|
-
const attributeKey = `#${attr}`;
|
|
458
|
-
const valueKey = `:${attr}`;
|
|
459
|
-
ExpressionAttributeNames[attributeKey] = attr;
|
|
460
|
-
ExpressionAttributeValues[valueKey] = value;
|
|
461
|
-
UpdateExpressionParts.push(`${attributeKey} = ${valueKey}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (this.autoTimestamps) {
|
|
465
|
-
const now = new Date().toISOString();
|
|
466
|
-
ExpressionAttributeNames['#updatedAt'] = 'updatedAt';
|
|
467
|
-
ExpressionAttributeValues[':updatedAt'] = now;
|
|
468
|
-
UpdateExpressionParts.push('#updatedAt = :updatedAt');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (options?.expectedVersion !== undefined) {
|
|
472
|
-
ExpressionAttributeNames['#version'] = 'version';
|
|
473
|
-
ExpressionAttributeValues[':expectedVersion'] = options.expectedVersion;
|
|
474
|
-
ExpressionAttributeValues[':newVersion'] = options.expectedVersion + 1;
|
|
475
|
-
UpdateExpressionParts.push('#version = :newVersion');
|
|
476
|
-
ConditionExpressionParts.push('#version = :expectedVersion');
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (UpdateExpressionParts.length === 0) {
|
|
480
|
-
throw new Error('No attributes provided to update in transactUpdate');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ');
|
|
484
|
-
const updateItem: DynamoDB.DocumentClient.Update = {
|
|
485
|
-
TableName: this.tableName,
|
|
486
|
-
Key: this.buildKey(rawKey),
|
|
487
|
-
UpdateExpression,
|
|
488
|
-
ExpressionAttributeNames,
|
|
489
|
-
ExpressionAttributeValues
|
|
490
|
-
};
|
|
491
|
-
if (ConditionExpressionParts.length > 0) {
|
|
492
|
-
updateItem.ConditionExpression = ConditionExpressionParts.join(' AND ');
|
|
493
|
-
}
|
|
494
|
-
return { Update: updateItem };
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
buildTransactDelete(rawKey: Partial<T>): DynamoDB.DocumentClient.TransactWriteItem {
|
|
498
|
-
return {
|
|
499
|
-
Delete: {
|
|
500
|
-
TableName: this.tableName,
|
|
501
|
-
Key: this.buildKey(rawKey)
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async transactWrite(
|
|
507
|
-
operations: DynamoDB.DocumentClient.TransactWriteItemList
|
|
508
|
-
): Promise<void> {
|
|
509
|
-
try {
|
|
510
|
-
await this.client.transactWrite({ TransactItems: operations }).promise();
|
|
511
|
-
} catch (error) {
|
|
512
|
-
console.error('Error during transactWrite operation:', error);
|
|
513
|
-
throw error;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
async transactGetByKeys(rawKeys: Partial<T>[]): Promise<T[]> {
|
|
518
|
-
const getItems = rawKeys.map(key => ({ TableName: this.tableName, Key: this.buildKey(key) }));
|
|
519
|
-
return this.transactGet(getItems);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
async transactGet(
|
|
523
|
-
getItems: { TableName: string; Key: any }[]
|
|
524
|
-
): Promise<T[]> {
|
|
525
|
-
try {
|
|
526
|
-
const response = await this.client
|
|
527
|
-
.transactGet({
|
|
528
|
-
TransactItems: getItems.map(item => ({ Get: item }))
|
|
529
|
-
})
|
|
530
|
-
.promise();
|
|
531
|
-
return (response.Responses || [])
|
|
532
|
-
.filter(r => r.Item)
|
|
533
|
-
.map(r => this.schema.parse(r.Item));
|
|
534
|
-
} catch (error) {
|
|
535
|
-
console.error('Error during transactGet operation:', error);
|
|
536
|
-
throw error;
|
|
537
|
-
}
|
|
211
|
+
public get(rawKey: Partial<T>): GetBuilder<T> {
|
|
212
|
+
return new GetBuilder<T>(this, rawKey);
|
|
538
213
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const indexAttributes = this.buildIndexes(item);
|
|
546
|
-
const finalItem = { ...item, ...computedKeys, ...indexAttributes };
|
|
547
|
-
const validated = this.schema.parse(finalItem);
|
|
548
|
-
return { PutRequest: { Item: validated } };
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
const deleteRequests = (ops.deletes || []).map(rawKey => {
|
|
552
|
-
const key = this.buildKey(rawKey);
|
|
553
|
-
return { DeleteRequest: { Key: key } };
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
const allRequests = [...putRequests, ...deleteRequests];
|
|
557
|
-
|
|
558
|
-
for (let i = 0; i < allRequests.length; i += 25) {
|
|
559
|
-
const chunk = allRequests.slice(i, i + 25);
|
|
560
|
-
let unprocessed = await this.batchWriteChunk(chunk as DynamoDB.DocumentClient.WriteRequest[]);
|
|
561
|
-
while (unprocessed && Object.keys(unprocessed).length > 0) {
|
|
562
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
563
|
-
unprocessed = await this.retryBatchWrite(unprocessed);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Update an item.
|
|
217
|
+
*/
|
|
218
|
+
public update(key: Partial<T>, expectedVersion?: number): UpdateBuilder<T> {
|
|
219
|
+
return new UpdateBuilder<T>(this, key, expectedVersion);
|
|
566
220
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
};
|
|
574
|
-
const result = await this.client.batchWrite(params as DynamoDB.DocumentClient.BatchWriteItemInput).promise();
|
|
575
|
-
return result.UnprocessedItems;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Delete an item.
|
|
224
|
+
*/
|
|
225
|
+
public delete(rawKey: Partial<T>): DeleteBuilder<T> {
|
|
226
|
+
return new DeleteBuilder<T>(this, rawKey);
|
|
576
227
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Query items.
|
|
231
|
+
*/
|
|
232
|
+
public query(key: Partial<T>): QueryBuilder<T> {
|
|
233
|
+
return new QueryBuilder<T>(this, key);
|
|
582
234
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
for (let i = 0; i < keys.length; i += 100) {
|
|
590
|
-
const chunk = keys.slice(i, i + 100);
|
|
591
|
-
const params = {
|
|
592
|
-
RequestItems: {
|
|
593
|
-
[this.tableName]: {
|
|
594
|
-
Keys: chunk
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
};
|
|
598
|
-
const result = await this.client.batchGet(params).promise();
|
|
599
|
-
const items = result.Responses ? result.Responses[this.tableName] : [];
|
|
600
|
-
results.push(...items.map(item => this.schema.parse(item)));
|
|
601
|
-
}
|
|
602
|
-
return results;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Scan for items.
|
|
238
|
+
*/
|
|
239
|
+
public scan(): ScanBuilder<T> {
|
|
240
|
+
return new ScanBuilder<T>(this);
|
|
603
241
|
}
|
|
604
242
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DynamoDB } from 'aws-sdk';
|
|
2
|
+
import { BetterDDB } from '../betterddb';
|
|
3
|
+
|
|
4
|
+
export class CreateBuilder<T> {
|
|
5
|
+
private extraTransactItems: DynamoDB.DocumentClient.TransactWriteItemList = [];
|
|
6
|
+
|
|
7
|
+
constructor(private parent: BetterDDB<T>, private item: T) {}
|
|
8
|
+
|
|
9
|
+
public async execute(): Promise<T> {
|
|
10
|
+
if (this.extraTransactItems.length > 0) {
|
|
11
|
+
// Build our update transaction item.
|
|
12
|
+
const myTransactItem = this.toTransactPut();
|
|
13
|
+
// Combine with extra transaction items.
|
|
14
|
+
const allItems = [...this.extraTransactItems, myTransactItem];
|
|
15
|
+
await this.parent.getClient().transactWrite({
|
|
16
|
+
TransactItems: allItems
|
|
17
|
+
}).promise();
|
|
18
|
+
// After transaction, retrieve the updated item.
|
|
19
|
+
const result = await this.parent.get(this.item).execute();
|
|
20
|
+
if (result === null) {
|
|
21
|
+
throw new Error('Item not found after transaction create');
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
} else {
|
|
25
|
+
let item = this.item;
|
|
26
|
+
if (this.parent.getAutoTimestamps()) {
|
|
27
|
+
const now = new Date().toISOString();
|
|
28
|
+
item = { ...item, createdAt: now, updatedAt: now } as T;
|
|
29
|
+
}
|
|
30
|
+
// Validate the item using the schema.
|
|
31
|
+
const validated = this.parent.getSchema().parse(item);
|
|
32
|
+
let finalItem = { ...validated };
|
|
33
|
+
|
|
34
|
+
// Compute and merge primary key.
|
|
35
|
+
const computedKeys = this.parent.buildKey(validated);
|
|
36
|
+
finalItem = { ...finalItem, ...computedKeys };
|
|
37
|
+
|
|
38
|
+
// Compute and merge index attributes.
|
|
39
|
+
const indexAttributes = this.parent.buildIndexes(validated);
|
|
40
|
+
finalItem = { ...finalItem, ...indexAttributes };
|
|
41
|
+
|
|
42
|
+
await this.parent.getClient().put({
|
|
43
|
+
TableName: this.parent.getTableName(),
|
|
44
|
+
Item: finalItem as DynamoDB.DocumentClient.PutItemInputAttributeMap
|
|
45
|
+
}).promise();
|
|
46
|
+
|
|
47
|
+
return validated;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public transactWrite(ops: DynamoDB.DocumentClient.TransactWriteItemList | DynamoDB.DocumentClient.TransactWriteItem): this {
|
|
52
|
+
if (Array.isArray(ops)) {
|
|
53
|
+
this.extraTransactItems.push(...ops);
|
|
54
|
+
} else {
|
|
55
|
+
this.extraTransactItems.push(ops);
|
|
56
|
+
}
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public toTransactPut(): DynamoDB.DocumentClient.TransactWriteItem {
|
|
61
|
+
const putItem: DynamoDB.DocumentClient.Put = {
|
|
62
|
+
TableName: this.parent.getTableName(),
|
|
63
|
+
Item: this.item as DynamoDB.DocumentClient.PutItemInputAttributeMap,
|
|
64
|
+
};
|
|
65
|
+
return { Put: putItem };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public then<TResult1 = T, TResult2 = never>(
|
|
69
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
70
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
|
71
|
+
): Promise<TResult1 | TResult2> {
|
|
72
|
+
return this.execute().then(onfulfilled, onrejected);
|
|
73
|
+
}
|
|
74
|
+
public catch<TResult = never>(
|
|
75
|
+
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
|
76
|
+
): Promise<T | TResult> {
|
|
77
|
+
return this.execute().catch(onrejected);
|
|
78
|
+
}
|
|
79
|
+
public finally(onfinally?: (() => void) | null): Promise<T> {
|
|
80
|
+
return this.execute().finally(onfinally);
|
|
81
|
+
}
|
|
82
|
+
}
|