betterddb 0.1.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/LICENCSE +21 -0
- package/README.md +111 -0
- package/docker-compose.yml +16 -0
- package/jest.config.js +16 -0
- package/lib/betterddb.d.ts +132 -0
- package/lib/betterddb.js +458 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +17 -0
- package/package.json +41 -0
- package/src/betterddb.ts +600 -0
- package/src/index.ts +1 -0
- package/test/placeholder.test.ts +98 -0
- package/tsconfig.json +12 -0
package/lib/betterddb.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BetterDDB = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* BetterDDB is a definition-based DynamoDB wrapper library.
|
|
6
|
+
*/
|
|
7
|
+
class BetterDDB {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
var _a;
|
|
10
|
+
this.schema = options.schema;
|
|
11
|
+
this.tableName = options.tableName;
|
|
12
|
+
this.keys = options.keys;
|
|
13
|
+
this.client = options.client;
|
|
14
|
+
this.autoTimestamps = (_a = options.autoTimestamps) !== null && _a !== void 0 ? _a : false;
|
|
15
|
+
}
|
|
16
|
+
// Helper: Retrieve the key value from a KeyDefinition.
|
|
17
|
+
getKeyValue(def, rawKey) {
|
|
18
|
+
if (typeof def === 'string' || typeof def === 'number' || typeof def === 'symbol') {
|
|
19
|
+
return String(rawKey[def]);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
return def.build(rawKey);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the primary key from a raw key object.
|
|
27
|
+
*/
|
|
28
|
+
buildKey(rawKey) {
|
|
29
|
+
const keyObj = {};
|
|
30
|
+
// For primary (partition) key:
|
|
31
|
+
const pkConfig = this.keys.primary;
|
|
32
|
+
keyObj[pkConfig.name] =
|
|
33
|
+
(typeof pkConfig.definition === 'string' ||
|
|
34
|
+
typeof pkConfig.definition === 'number' ||
|
|
35
|
+
typeof pkConfig.definition === 'symbol')
|
|
36
|
+
? String(rawKey[pkConfig.definition])
|
|
37
|
+
: pkConfig.definition.build(rawKey);
|
|
38
|
+
// For sort key, if defined:
|
|
39
|
+
if (this.keys.sort) {
|
|
40
|
+
const skConfig = this.keys.sort;
|
|
41
|
+
keyObj[skConfig.name] =
|
|
42
|
+
(typeof skConfig.definition === 'string' ||
|
|
43
|
+
typeof skConfig.definition === 'number' ||
|
|
44
|
+
typeof skConfig.definition === 'symbol')
|
|
45
|
+
? String(rawKey[skConfig.definition])
|
|
46
|
+
: skConfig.definition.build(rawKey);
|
|
47
|
+
}
|
|
48
|
+
return keyObj;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build index attributes for each defined GSI.
|
|
52
|
+
*/
|
|
53
|
+
buildIndexes(rawItem) {
|
|
54
|
+
const indexAttributes = {};
|
|
55
|
+
if (this.keys.gsis) {
|
|
56
|
+
for (const gsiName in this.keys.gsis) {
|
|
57
|
+
const gsiConfig = this.keys.gsis[gsiName];
|
|
58
|
+
// Compute primary index attribute.
|
|
59
|
+
const primaryConfig = gsiConfig.primary;
|
|
60
|
+
indexAttributes[primaryConfig.name] =
|
|
61
|
+
(typeof primaryConfig.definition === 'string' ||
|
|
62
|
+
typeof primaryConfig.definition === 'number' ||
|
|
63
|
+
typeof primaryConfig.definition === 'symbol')
|
|
64
|
+
? String(rawItem[primaryConfig.definition])
|
|
65
|
+
: primaryConfig.definition.build(rawItem);
|
|
66
|
+
// Compute sort index attribute if provided.
|
|
67
|
+
if (gsiConfig.sort) {
|
|
68
|
+
const sortConfig = gsiConfig.sort;
|
|
69
|
+
indexAttributes[sortConfig.name] =
|
|
70
|
+
(typeof sortConfig.definition === 'string' ||
|
|
71
|
+
typeof sortConfig.definition === 'number' ||
|
|
72
|
+
typeof sortConfig.definition === 'symbol')
|
|
73
|
+
? String(rawItem[sortConfig.definition])
|
|
74
|
+
: sortConfig.definition.build(rawItem);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return indexAttributes;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create an item:
|
|
82
|
+
* - Computes primary key and index attributes,
|
|
83
|
+
* - Optionally injects timestamps,
|
|
84
|
+
* - Validates the item and writes it to DynamoDB.
|
|
85
|
+
*/
|
|
86
|
+
async create(item) {
|
|
87
|
+
if (this.autoTimestamps) {
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
item = { ...item, createdAt: now, updatedAt: now };
|
|
90
|
+
}
|
|
91
|
+
const validated = this.schema.parse(item);
|
|
92
|
+
let finalItem = { ...validated };
|
|
93
|
+
// Compute and merge primary key.
|
|
94
|
+
const computedKeys = this.buildKey(validated);
|
|
95
|
+
finalItem = { ...finalItem, ...computedKeys };
|
|
96
|
+
// Compute and merge index attributes.
|
|
97
|
+
const indexAttributes = this.buildIndexes(validated);
|
|
98
|
+
finalItem = { ...finalItem, ...indexAttributes };
|
|
99
|
+
try {
|
|
100
|
+
await this.client.put({ TableName: this.tableName, Item: finalItem }).promise();
|
|
101
|
+
return validated;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error('Error during create operation:', error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async get(rawKey) {
|
|
109
|
+
const Key = this.buildKey(rawKey);
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.client.get({ TableName: this.tableName, Key }).promise();
|
|
112
|
+
if (!result.Item)
|
|
113
|
+
return null;
|
|
114
|
+
return this.schema.parse(result.Item);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Error during get operation:', error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async update(rawKey, update, options) {
|
|
122
|
+
const ExpressionAttributeNames = {};
|
|
123
|
+
const ExpressionAttributeValues = {};
|
|
124
|
+
const UpdateExpressionParts = [];
|
|
125
|
+
const ConditionExpressionParts = [];
|
|
126
|
+
// Exclude key fields from update.
|
|
127
|
+
const keyFieldNames = [
|
|
128
|
+
this.keys.primary.name,
|
|
129
|
+
this.keys.sort ? this.keys.sort.name : undefined
|
|
130
|
+
].filter(Boolean);
|
|
131
|
+
for (const [attr, value] of Object.entries(update)) {
|
|
132
|
+
if (keyFieldNames.includes(attr))
|
|
133
|
+
continue;
|
|
134
|
+
const attributeKey = `#${attr}`;
|
|
135
|
+
const valueKey = `:${attr}`;
|
|
136
|
+
ExpressionAttributeNames[attributeKey] = attr;
|
|
137
|
+
ExpressionAttributeValues[valueKey] = value;
|
|
138
|
+
UpdateExpressionParts.push(`${attributeKey} = ${valueKey}`);
|
|
139
|
+
}
|
|
140
|
+
if (this.autoTimestamps) {
|
|
141
|
+
const now = new Date().toISOString();
|
|
142
|
+
ExpressionAttributeNames['#updatedAt'] = 'updatedAt';
|
|
143
|
+
ExpressionAttributeValues[':updatedAt'] = now;
|
|
144
|
+
UpdateExpressionParts.push('#updatedAt = :updatedAt');
|
|
145
|
+
}
|
|
146
|
+
if ((options === null || options === void 0 ? void 0 : options.expectedVersion) !== undefined) {
|
|
147
|
+
ExpressionAttributeNames['#version'] = 'version';
|
|
148
|
+
ExpressionAttributeValues[':expectedVersion'] = options.expectedVersion;
|
|
149
|
+
ExpressionAttributeValues[':newVersion'] = options.expectedVersion + 1;
|
|
150
|
+
UpdateExpressionParts.push('#version = :newVersion');
|
|
151
|
+
ConditionExpressionParts.push('#version = :expectedVersion');
|
|
152
|
+
}
|
|
153
|
+
if (UpdateExpressionParts.length === 0) {
|
|
154
|
+
throw new Error('No attributes provided to update');
|
|
155
|
+
}
|
|
156
|
+
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ');
|
|
157
|
+
const params = {
|
|
158
|
+
TableName: this.tableName,
|
|
159
|
+
Key: this.buildKey(rawKey),
|
|
160
|
+
UpdateExpression,
|
|
161
|
+
ExpressionAttributeNames,
|
|
162
|
+
ExpressionAttributeValues,
|
|
163
|
+
ReturnValues: 'ALL_NEW'
|
|
164
|
+
};
|
|
165
|
+
if (ConditionExpressionParts.length > 0) {
|
|
166
|
+
params.ConditionExpression = ConditionExpressionParts.join(' AND ');
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const result = await this.client.update(params).promise();
|
|
170
|
+
if (!result.Attributes) {
|
|
171
|
+
throw new Error('No attributes returned after update');
|
|
172
|
+
}
|
|
173
|
+
return this.schema.parse(result.Attributes);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error('Error during update operation:', error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async delete(rawKey) {
|
|
181
|
+
const Key = this.buildKey(rawKey);
|
|
182
|
+
try {
|
|
183
|
+
await this.client.delete({ TableName: this.tableName, Key }).promise();
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.error('Error during delete operation:', error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async queryByGsi(gsiName, key, sortKeyCondition) {
|
|
191
|
+
if (!this.keys.gsis || !this.keys.gsis[gsiName]) {
|
|
192
|
+
throw new Error(`GSI "${gsiName}" is not configured`);
|
|
193
|
+
}
|
|
194
|
+
const indexConfig = this.keys.gsis[gsiName];
|
|
195
|
+
const ExpressionAttributeNames = {
|
|
196
|
+
[`#${indexConfig.primary.name}`]: indexConfig.primary.name
|
|
197
|
+
};
|
|
198
|
+
const ExpressionAttributeValues = {
|
|
199
|
+
[`:${indexConfig.primary.name}`]: (typeof indexConfig.primary.definition === 'string' ||
|
|
200
|
+
typeof indexConfig.primary.definition === 'number' ||
|
|
201
|
+
typeof indexConfig.primary.definition === 'symbol')
|
|
202
|
+
? String(key[indexConfig.primary.definition])
|
|
203
|
+
: indexConfig.primary.definition.build(key)
|
|
204
|
+
};
|
|
205
|
+
let KeyConditionExpression = `#${indexConfig.primary.name} = :${indexConfig.primary.name}`;
|
|
206
|
+
if (indexConfig.sort && sortKeyCondition) {
|
|
207
|
+
const skFieldName = indexConfig.sort.name;
|
|
208
|
+
ExpressionAttributeNames['#gsiSk'] = skFieldName;
|
|
209
|
+
switch (sortKeyCondition.operator) {
|
|
210
|
+
case 'eq':
|
|
211
|
+
ExpressionAttributeValues[':gsiSk'] = sortKeyCondition.values;
|
|
212
|
+
KeyConditionExpression += ' AND #gsiSk = :gsiSk';
|
|
213
|
+
break;
|
|
214
|
+
case 'begins_with':
|
|
215
|
+
ExpressionAttributeValues[':gsiSk'] = sortKeyCondition.values;
|
|
216
|
+
KeyConditionExpression += ' AND begins_with(#gsiSk, :gsiSk)';
|
|
217
|
+
break;
|
|
218
|
+
case 'between':
|
|
219
|
+
if (!Array.isArray(sortKeyCondition.values) || sortKeyCondition.values.length !== 2) {
|
|
220
|
+
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
221
|
+
}
|
|
222
|
+
ExpressionAttributeValues[':gsiSkStart'] = sortKeyCondition.values[0];
|
|
223
|
+
ExpressionAttributeValues[':gsiSkEnd'] = sortKeyCondition.values[1];
|
|
224
|
+
KeyConditionExpression += ' AND #gsiSk BETWEEN :gsiSkStart AND :gsiSkEnd';
|
|
225
|
+
break;
|
|
226
|
+
default:
|
|
227
|
+
throw new Error(`Unsupported sort key operator: ${sortKeyCondition.operator}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const result = await this.client
|
|
232
|
+
.query({
|
|
233
|
+
TableName: this.tableName,
|
|
234
|
+
IndexName: gsiName,
|
|
235
|
+
KeyConditionExpression,
|
|
236
|
+
ExpressionAttributeNames,
|
|
237
|
+
ExpressionAttributeValues
|
|
238
|
+
})
|
|
239
|
+
.promise();
|
|
240
|
+
return (result.Items || []).map(item => this.schema.parse(item));
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error('Error during queryByGsi operation:', error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Query by primary key (using the computed primary key) and an optional sort key condition.
|
|
249
|
+
*/
|
|
250
|
+
async queryByPrimaryKey(rawKey, sortKeyCondition, options) {
|
|
251
|
+
const pkAttrName = this.keys.primary.name;
|
|
252
|
+
const pkValue = (typeof this.keys.primary.definition === 'string' ||
|
|
253
|
+
typeof this.keys.primary.definition === 'number' ||
|
|
254
|
+
typeof this.keys.primary.definition === 'symbol')
|
|
255
|
+
? String(rawKey[this.keys.primary.definition])
|
|
256
|
+
: this.keys.primary.definition.build(rawKey);
|
|
257
|
+
const ExpressionAttributeNames = {
|
|
258
|
+
[`#${pkAttrName}`]: pkAttrName
|
|
259
|
+
};
|
|
260
|
+
const ExpressionAttributeValues = {
|
|
261
|
+
[`:${pkAttrName}`]: pkValue
|
|
262
|
+
};
|
|
263
|
+
let KeyConditionExpression = `#${pkAttrName} = :${pkAttrName}`;
|
|
264
|
+
if (this.keys.sort && sortKeyCondition) {
|
|
265
|
+
const skAttrName = this.keys.sort.name;
|
|
266
|
+
ExpressionAttributeNames[`#${skAttrName}`] = skAttrName;
|
|
267
|
+
switch (sortKeyCondition.operator) {
|
|
268
|
+
case 'eq':
|
|
269
|
+
ExpressionAttributeValues[':skValue'] = sortKeyCondition.values;
|
|
270
|
+
KeyConditionExpression += ` AND #${skAttrName} = :skValue`;
|
|
271
|
+
break;
|
|
272
|
+
case 'begins_with':
|
|
273
|
+
ExpressionAttributeValues[':skValue'] = sortKeyCondition.values;
|
|
274
|
+
KeyConditionExpression += ` AND begins_with(#${skAttrName}, :skValue)`;
|
|
275
|
+
break;
|
|
276
|
+
case 'between':
|
|
277
|
+
if (!Array.isArray(sortKeyCondition.values) || sortKeyCondition.values.length !== 2) {
|
|
278
|
+
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
279
|
+
}
|
|
280
|
+
ExpressionAttributeValues[':skStart'] = sortKeyCondition.values[0];
|
|
281
|
+
ExpressionAttributeValues[':skEnd'] = sortKeyCondition.values[1];
|
|
282
|
+
KeyConditionExpression += ` AND #${skAttrName} BETWEEN :skStart AND :skEnd`;
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
throw new Error(`Unsupported sort key operator: ${sortKeyCondition.operator}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const queryParams = {
|
|
289
|
+
TableName: this.tableName,
|
|
290
|
+
KeyConditionExpression,
|
|
291
|
+
ExpressionAttributeNames,
|
|
292
|
+
ExpressionAttributeValues
|
|
293
|
+
};
|
|
294
|
+
if (options === null || options === void 0 ? void 0 : options.limit) {
|
|
295
|
+
queryParams.Limit = options.limit;
|
|
296
|
+
}
|
|
297
|
+
if (options === null || options === void 0 ? void 0 : options.lastKey) {
|
|
298
|
+
queryParams.ExclusiveStartKey = options.lastKey;
|
|
299
|
+
}
|
|
300
|
+
const result = await this.client.query(queryParams).promise();
|
|
301
|
+
const items = (result.Items || []).map(item => this.schema.parse(item));
|
|
302
|
+
return { items, lastKey: result.LastEvaluatedKey };
|
|
303
|
+
}
|
|
304
|
+
// ───── Transaction Helpers ─────────────────────────────
|
|
305
|
+
buildTransactPut(item) {
|
|
306
|
+
const computedKeys = this.buildKey(item);
|
|
307
|
+
const indexAttributes = this.buildIndexes(item);
|
|
308
|
+
const finalItem = { ...item, ...computedKeys, ...indexAttributes };
|
|
309
|
+
const validated = this.schema.parse(finalItem);
|
|
310
|
+
return {
|
|
311
|
+
Put: {
|
|
312
|
+
TableName: this.tableName,
|
|
313
|
+
Item: validated
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
buildTransactUpdate(rawKey, update, options) {
|
|
318
|
+
const ExpressionAttributeNames = {};
|
|
319
|
+
const ExpressionAttributeValues = {};
|
|
320
|
+
const UpdateExpressionParts = [];
|
|
321
|
+
const ConditionExpressionParts = [];
|
|
322
|
+
const keyFieldNames = [
|
|
323
|
+
this.keys.primary.name,
|
|
324
|
+
this.keys.sort ? this.keys.sort.name : undefined
|
|
325
|
+
].filter(Boolean);
|
|
326
|
+
for (const [attr, value] of Object.entries(update)) {
|
|
327
|
+
if (keyFieldNames.includes(attr))
|
|
328
|
+
continue;
|
|
329
|
+
const attributeKey = `#${attr}`;
|
|
330
|
+
const valueKey = `:${attr}`;
|
|
331
|
+
ExpressionAttributeNames[attributeKey] = attr;
|
|
332
|
+
ExpressionAttributeValues[valueKey] = value;
|
|
333
|
+
UpdateExpressionParts.push(`${attributeKey} = ${valueKey}`);
|
|
334
|
+
}
|
|
335
|
+
if (this.autoTimestamps) {
|
|
336
|
+
const now = new Date().toISOString();
|
|
337
|
+
ExpressionAttributeNames['#updatedAt'] = 'updatedAt';
|
|
338
|
+
ExpressionAttributeValues[':updatedAt'] = now;
|
|
339
|
+
UpdateExpressionParts.push('#updatedAt = :updatedAt');
|
|
340
|
+
}
|
|
341
|
+
if ((options === null || options === void 0 ? void 0 : options.expectedVersion) !== undefined) {
|
|
342
|
+
ExpressionAttributeNames['#version'] = 'version';
|
|
343
|
+
ExpressionAttributeValues[':expectedVersion'] = options.expectedVersion;
|
|
344
|
+
ExpressionAttributeValues[':newVersion'] = options.expectedVersion + 1;
|
|
345
|
+
UpdateExpressionParts.push('#version = :newVersion');
|
|
346
|
+
ConditionExpressionParts.push('#version = :expectedVersion');
|
|
347
|
+
}
|
|
348
|
+
if (UpdateExpressionParts.length === 0) {
|
|
349
|
+
throw new Error('No attributes provided to update in transactUpdate');
|
|
350
|
+
}
|
|
351
|
+
const UpdateExpression = 'SET ' + UpdateExpressionParts.join(', ');
|
|
352
|
+
const updateItem = {
|
|
353
|
+
TableName: this.tableName,
|
|
354
|
+
Key: this.buildKey(rawKey),
|
|
355
|
+
UpdateExpression,
|
|
356
|
+
ExpressionAttributeNames,
|
|
357
|
+
ExpressionAttributeValues
|
|
358
|
+
};
|
|
359
|
+
if (ConditionExpressionParts.length > 0) {
|
|
360
|
+
updateItem.ConditionExpression = ConditionExpressionParts.join(' AND ');
|
|
361
|
+
}
|
|
362
|
+
return { Update: updateItem };
|
|
363
|
+
}
|
|
364
|
+
buildTransactDelete(rawKey) {
|
|
365
|
+
return {
|
|
366
|
+
Delete: {
|
|
367
|
+
TableName: this.tableName,
|
|
368
|
+
Key: this.buildKey(rawKey)
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async transactWrite(operations) {
|
|
373
|
+
try {
|
|
374
|
+
await this.client.transactWrite({ TransactItems: operations }).promise();
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
console.error('Error during transactWrite operation:', error);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async transactGetByKeys(rawKeys) {
|
|
382
|
+
const getItems = rawKeys.map(key => ({ TableName: this.tableName, Key: this.buildKey(key) }));
|
|
383
|
+
return this.transactGet(getItems);
|
|
384
|
+
}
|
|
385
|
+
async transactGet(getItems) {
|
|
386
|
+
try {
|
|
387
|
+
const response = await this.client
|
|
388
|
+
.transactGet({
|
|
389
|
+
TransactItems: getItems.map(item => ({ Get: item }))
|
|
390
|
+
})
|
|
391
|
+
.promise();
|
|
392
|
+
return (response.Responses || [])
|
|
393
|
+
.filter(r => r.Item)
|
|
394
|
+
.map(r => this.schema.parse(r.Item));
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
console.error('Error during transactGet operation:', error);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// ───── Batch Write Support ─────────────────────────────
|
|
402
|
+
async batchWrite(ops) {
|
|
403
|
+
const putRequests = (ops.puts || []).map(item => {
|
|
404
|
+
const computedKeys = this.buildKey(item);
|
|
405
|
+
const indexAttributes = this.buildIndexes(item);
|
|
406
|
+
const finalItem = { ...item, ...computedKeys, ...indexAttributes };
|
|
407
|
+
const validated = this.schema.parse(finalItem);
|
|
408
|
+
return { PutRequest: { Item: validated } };
|
|
409
|
+
});
|
|
410
|
+
const deleteRequests = (ops.deletes || []).map(rawKey => {
|
|
411
|
+
const key = this.buildKey(rawKey);
|
|
412
|
+
return { DeleteRequest: { Key: key } };
|
|
413
|
+
});
|
|
414
|
+
const allRequests = [...putRequests, ...deleteRequests];
|
|
415
|
+
for (let i = 0; i < allRequests.length; i += 25) {
|
|
416
|
+
const chunk = allRequests.slice(i, i + 25);
|
|
417
|
+
let unprocessed = await this.batchWriteChunk(chunk);
|
|
418
|
+
while (unprocessed && Object.keys(unprocessed).length > 0) {
|
|
419
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
420
|
+
unprocessed = await this.retryBatchWrite(unprocessed);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async batchWriteChunk(chunk) {
|
|
425
|
+
const params = {
|
|
426
|
+
RequestItems: {
|
|
427
|
+
[this.tableName]: chunk
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const result = await this.client.batchWrite(params).promise();
|
|
431
|
+
return result.UnprocessedItems;
|
|
432
|
+
}
|
|
433
|
+
async retryBatchWrite(unprocessed) {
|
|
434
|
+
const params = { RequestItems: unprocessed };
|
|
435
|
+
const result = await this.client.batchWrite(params).promise();
|
|
436
|
+
return result.UnprocessedItems;
|
|
437
|
+
}
|
|
438
|
+
// ───── Batch Get Support ─────────────────────────────
|
|
439
|
+
async batchGet(rawKeys) {
|
|
440
|
+
const keys = rawKeys.map(key => this.buildKey(key));
|
|
441
|
+
const results = [];
|
|
442
|
+
for (let i = 0; i < keys.length; i += 100) {
|
|
443
|
+
const chunk = keys.slice(i, i + 100);
|
|
444
|
+
const params = {
|
|
445
|
+
RequestItems: {
|
|
446
|
+
[this.tableName]: {
|
|
447
|
+
Keys: chunk
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const result = await this.client.batchGet(params).promise();
|
|
452
|
+
const items = result.Responses ? result.Responses[this.tableName] : [];
|
|
453
|
+
results.push(...items.map(item => this.schema.parse(item)));
|
|
454
|
+
}
|
|
455
|
+
return results;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
exports.BetterDDB = BetterDDB;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './betterddb';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./betterddb"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "betterddb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A definition-based DynamoDB wrapper library that provides a schema-driven and fully typesafe DAL.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build",
|
|
10
|
+
"docker:up": "docker-compose up -d",
|
|
11
|
+
"docker:down": "docker-compose down",
|
|
12
|
+
"test": "npm run docker:up && jest && npm run docker:down",
|
|
13
|
+
"test:watch": "npm run docker:up && jest --watch",
|
|
14
|
+
"test:coverage": "npm run docker:up && jest --coverage && npm run docker:down"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dynamodb",
|
|
18
|
+
"dal",
|
|
19
|
+
"aws-sdk",
|
|
20
|
+
"zod",
|
|
21
|
+
"typescript",
|
|
22
|
+
"wrapper",
|
|
23
|
+
"compound-key",
|
|
24
|
+
"transactions",
|
|
25
|
+
"batch",
|
|
26
|
+
"pagination"
|
|
27
|
+
],
|
|
28
|
+
"author": "R.R. Wang",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"aws-sdk": "^2.1370.0",
|
|
32
|
+
"zod": "^3.21.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/jest": "^29.5.14",
|
|
36
|
+
"@types/node": "^18.15.0",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"ts-jest": "^29.2.5",
|
|
39
|
+
"typescript": "^4.9.5"
|
|
40
|
+
}
|
|
41
|
+
}
|