betterddb 0.8.0 → 0.8.2
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/dist/src/betterddb.d.ts +137 -0
- package/dist/src/betterddb.d.ts.map +1 -0
- package/dist/src/betterddb.js +165 -0
- package/dist/src/betterddb.js.map +1 -0
- package/dist/src/builders/batch-get-builder.d.ts +16 -0
- package/dist/src/builders/batch-get-builder.d.ts.map +1 -0
- package/dist/src/builders/batch-get-builder.js +54 -0
- package/dist/src/builders/batch-get-builder.js.map +1 -0
- package/dist/src/builders/create-builder.d.ts +12 -0
- package/dist/src/builders/create-builder.d.ts.map +1 -0
- package/dist/src/builders/create-builder.js +84 -0
- package/dist/src/builders/create-builder.js.map +1 -0
- package/dist/src/builders/delete-builder.d.ts +18 -0
- package/dist/src/builders/delete-builder.d.ts.map +1 -0
- package/dist/src/builders/delete-builder.js +75 -0
- package/dist/src/builders/delete-builder.js.map +1 -0
- package/dist/src/builders/get-builder.d.ts +18 -0
- package/dist/src/builders/get-builder.d.ts.map +1 -0
- package/dist/src/builders/get-builder.js +76 -0
- package/dist/src/builders/get-builder.js.map +1 -0
- package/dist/src/builders/index.d.ts +8 -0
- package/dist/src/builders/index.d.ts.map +1 -0
- package/{src/builders/index.ts → dist/src/builders/index.js} +1 -0
- package/dist/src/builders/index.js.map +1 -0
- package/dist/src/builders/query-builder.d.ts +29 -0
- package/dist/src/builders/query-builder.d.ts.map +1 -0
- package/dist/src/builders/query-builder.js +171 -0
- package/dist/src/builders/query-builder.js.map +1 -0
- package/dist/src/builders/scan-builder.d.ts +21 -0
- package/dist/src/builders/scan-builder.d.ts.map +1 -0
- package/dist/src/builders/scan-builder.js +76 -0
- package/dist/src/builders/scan-builder.js.map +1 -0
- package/dist/src/builders/update-builder.d.ts +37 -0
- package/dist/src/builders/update-builder.d.ts.map +1 -0
- package/dist/src/builders/update-builder.js +301 -0
- package/dist/src/builders/update-builder.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/{src/index.ts → dist/src/index.js} +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/operator.d.ts +3 -0
- package/dist/src/operator.d.ts.map +1 -0
- package/dist/src/operator.js +28 -0
- package/dist/src/operator.js.map +1 -0
- package/dist/src/types/index.d.ts +2 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/{src/types/index.ts → dist/src/types/index.js} +1 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/paginated-result.d.ts +6 -0
- package/dist/src/types/paginated-result.d.ts.map +1 -0
- package/dist/src/types/paginated-result.js +2 -0
- package/dist/src/types/paginated-result.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +9 -8
- package/.github/workflows/npm-publish.yml +0 -33
- package/.github/workflows/test.yml +0 -42
- package/CONTRIBUTING.md +0 -225
- package/LICENCSE +0 -21
- package/babel.config.cjs +0 -6
- package/docker-compose.yml +0 -16
- package/eslint.config.mjs +0 -29
- package/jest.config.cjs +0 -17
- package/prettier.config.js +0 -6
- package/src/betterddb.ts +0 -267
- package/src/builders/batch-get-builder.ts +0 -56
- package/src/builders/create-builder.ts +0 -97
- package/src/builders/delete-builder.ts +0 -87
- package/src/builders/get-builder.ts +0 -78
- package/src/builders/query-builder.ts +0 -242
- package/src/builders/scan-builder.ts +0 -98
- package/src/builders/update-builder.ts +0 -363
- package/src/operator.ts +0 -43
- package/src/types/paginated-result.ts +0 -6
- package/test/batch-get.test.ts +0 -122
- package/test/create.test.ts +0 -121
- package/test/delete.test.ts +0 -93
- package/test/get.test.ts +0 -98
- package/test/query.test.ts +0 -206
- package/test/scan.test.ts +0 -130
- package/test/update.test.ts +0 -355
- package/test/utils/table-setup.ts +0 -62
- package/tsconfig.json +0 -23
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type NativeAttributeValue,
|
|
3
|
-
TransactWriteCommand,
|
|
4
|
-
UpdateCommand,
|
|
5
|
-
type UpdateCommandInput,
|
|
6
|
-
} from "@aws-sdk/lib-dynamodb";
|
|
7
|
-
import { type BetterDDB } from "../betterddb.js";
|
|
8
|
-
import {
|
|
9
|
-
type TransactWriteItem,
|
|
10
|
-
type Update,
|
|
11
|
-
type ReturnValue,
|
|
12
|
-
} from "@aws-sdk/client-dynamodb";
|
|
13
|
-
|
|
14
|
-
interface UpdateActions<T> {
|
|
15
|
-
set?: Partial<T>;
|
|
16
|
-
remove?: (keyof T)[];
|
|
17
|
-
add?: Partial<Record<keyof T, number | Set<NativeAttributeValue>>>;
|
|
18
|
-
delete?: Partial<Record<keyof T, Set<NativeAttributeValue>>>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class UpdateBuilder<T> {
|
|
22
|
-
private actions: UpdateActions<T> = {};
|
|
23
|
-
private condition?: {
|
|
24
|
-
expression: string;
|
|
25
|
-
attributeValues: Record<string, NativeAttributeValue>;
|
|
26
|
-
attributeNames: Record<string, string>;
|
|
27
|
-
};
|
|
28
|
-
// When using transaction mode, we store extra transaction items.
|
|
29
|
-
private extraTransactItems: TransactWriteItem[] = [];
|
|
30
|
-
|
|
31
|
-
// Reference to the parent BetterDDB instance and key.
|
|
32
|
-
constructor(
|
|
33
|
-
private parent: BetterDDB<T>,
|
|
34
|
-
private key: Partial<T>,
|
|
35
|
-
) {}
|
|
36
|
-
|
|
37
|
-
// Chainable methods:
|
|
38
|
-
public set(attrs: Partial<T>): this {
|
|
39
|
-
// Separate values into sets and removes
|
|
40
|
-
const { toSet, toRemove } = Object.entries(attrs).reduce(
|
|
41
|
-
(acc, [key, value]) => {
|
|
42
|
-
if (
|
|
43
|
-
value === undefined ||
|
|
44
|
-
(typeof value === "string" &&
|
|
45
|
-
value.trim() === "" &&
|
|
46
|
-
this.parent.getSchema().shape[key]?.isOptional?.())
|
|
47
|
-
) {
|
|
48
|
-
acc.toRemove.push(key as keyof T);
|
|
49
|
-
} else {
|
|
50
|
-
acc.toSet[key] = value;
|
|
51
|
-
}
|
|
52
|
-
return acc;
|
|
53
|
-
},
|
|
54
|
-
{ toSet: {} as Record<string, any>, toRemove: [] as (keyof T)[] },
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Handle non-empty values with set
|
|
58
|
-
if (Object.keys(toSet).length > 0) {
|
|
59
|
-
const partialSchema = this.parent.getSchema().partial();
|
|
60
|
-
const validated = partialSchema.parse(toSet);
|
|
61
|
-
this.actions.set = { ...this.actions.set, ...validated };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Handle empty/undefined values with remove
|
|
65
|
-
if (toRemove.length > 0) {
|
|
66
|
-
this.remove(toRemove);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return this;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public remove(attrs: (keyof T)[]): this {
|
|
73
|
-
this.actions.remove = [...(this.actions.remove ?? []), ...attrs];
|
|
74
|
-
return this;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public add(
|
|
78
|
-
attrs: Partial<Record<keyof T, number | Set<NativeAttributeValue>>>,
|
|
79
|
-
): this {
|
|
80
|
-
const partialSchema = this.parent.getSchema().partial();
|
|
81
|
-
const validated = partialSchema.parse(attrs);
|
|
82
|
-
this.actions.add = { ...this.actions.add, ...validated };
|
|
83
|
-
|
|
84
|
-
return this;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public delete(
|
|
88
|
-
attrs: Partial<Record<keyof T, Set<NativeAttributeValue>>>,
|
|
89
|
-
): this {
|
|
90
|
-
this.actions.delete = { ...this.actions.delete, ...attrs };
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Adds a condition expression to the update.
|
|
96
|
-
*/
|
|
97
|
-
public setCondition(
|
|
98
|
-
expression: string,
|
|
99
|
-
attributeValues: Record<string, NativeAttributeValue>,
|
|
100
|
-
attributeNames: Record<string, string>,
|
|
101
|
-
): this {
|
|
102
|
-
if (this.condition) {
|
|
103
|
-
// Merge conditions with AND.
|
|
104
|
-
this.condition.expression += ` AND ${expression}`;
|
|
105
|
-
Object.assign(this.condition.attributeValues, attributeValues);
|
|
106
|
-
Object.assign(this.condition.attributeNames, attributeNames);
|
|
107
|
-
} else {
|
|
108
|
-
this.condition = {
|
|
109
|
-
expression,
|
|
110
|
-
attributeValues,
|
|
111
|
-
attributeNames,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return this;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Specifies additional transaction items to include when executing this update as a transaction.
|
|
119
|
-
*/
|
|
120
|
-
public transactWrite(ops: TransactWriteItem[] | TransactWriteItem): this {
|
|
121
|
-
if (Array.isArray(ops)) {
|
|
122
|
-
this.extraTransactItems.push(...ops);
|
|
123
|
-
} else {
|
|
124
|
-
this.extraTransactItems.push(ops);
|
|
125
|
-
}
|
|
126
|
-
return this;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Builds the update expression and associated maps.
|
|
131
|
-
*/
|
|
132
|
-
private buildExpression(newItemForIndexes?: T): {
|
|
133
|
-
updateExpression: string;
|
|
134
|
-
attributeNames: Record<string, string>;
|
|
135
|
-
attributeValues?: Record<string, NativeAttributeValue>;
|
|
136
|
-
} {
|
|
137
|
-
const ExpressionAttributeNames: Record<string, string> = {};
|
|
138
|
-
const ExpressionAttributeValues: Record<string, NativeAttributeValue> = {};
|
|
139
|
-
const clauses: string[] = [];
|
|
140
|
-
|
|
141
|
-
// 1) SET – from actions.set
|
|
142
|
-
const setParts: string[] = [];
|
|
143
|
-
if (this.actions.set) {
|
|
144
|
-
for (const [attr, value] of Object.entries(this.actions.set)) {
|
|
145
|
-
const nameKey = `#n_${attr}`;
|
|
146
|
-
const valueKey = `:v_${attr}`;
|
|
147
|
-
ExpressionAttributeNames[nameKey] = attr;
|
|
148
|
-
ExpressionAttributeValues[valueKey] = value!;
|
|
149
|
-
setParts.push(`${nameKey} = ${valueKey}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 2) also SET – from index‐rebuild if requested
|
|
154
|
-
if (newItemForIndexes) {
|
|
155
|
-
const indexAttrs = this.parent.buildIndexes(newItemForIndexes);
|
|
156
|
-
for (const [attr, value] of Object.entries(indexAttrs)) {
|
|
157
|
-
const nameKey = `#n_idx_${attr}`;
|
|
158
|
-
const valueKey = `:v_idx_${attr}`;
|
|
159
|
-
ExpressionAttributeNames[nameKey] = attr;
|
|
160
|
-
ExpressionAttributeValues[valueKey] = value;
|
|
161
|
-
setParts.push(`${nameKey} = ${valueKey}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (setParts.length > 0) {
|
|
165
|
-
clauses.push(`SET ${setParts.join(", ")}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 3) REMOVE
|
|
169
|
-
if (this.actions.remove?.length) {
|
|
170
|
-
const removeParts = this.actions.remove.map((attr) => {
|
|
171
|
-
const nameKey = `#n_${String(attr)}`;
|
|
172
|
-
ExpressionAttributeNames[nameKey] = String(attr);
|
|
173
|
-
return nameKey;
|
|
174
|
-
});
|
|
175
|
-
clauses.push(`REMOVE ${removeParts.join(", ")}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 4) ADD
|
|
179
|
-
if (this.actions.add) {
|
|
180
|
-
const addParts: string[] = [];
|
|
181
|
-
for (const [attr, value] of Object.entries(this.actions.add)) {
|
|
182
|
-
const nameKey = `#n_${attr}`;
|
|
183
|
-
const valueKey = `:v_${attr}`;
|
|
184
|
-
ExpressionAttributeNames[nameKey] = attr;
|
|
185
|
-
ExpressionAttributeValues[valueKey] = value as NativeAttributeValue;
|
|
186
|
-
addParts.push(`${nameKey} ${valueKey}`);
|
|
187
|
-
}
|
|
188
|
-
if (addParts.length) {
|
|
189
|
-
clauses.push(`ADD ${addParts.join(", ")}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 5) DELETE
|
|
194
|
-
if (this.actions.delete) {
|
|
195
|
-
const deleteParts: string[] = [];
|
|
196
|
-
for (const [attr, value] of Object.entries(this.actions.delete)) {
|
|
197
|
-
const nameKey = `#n_${attr}`;
|
|
198
|
-
const valueKey = `:v_${attr}`;
|
|
199
|
-
ExpressionAttributeNames[nameKey] = attr;
|
|
200
|
-
ExpressionAttributeValues[valueKey] = value as NativeAttributeValue;
|
|
201
|
-
deleteParts.push(`${nameKey} ${valueKey}`);
|
|
202
|
-
}
|
|
203
|
-
if (deleteParts.length) {
|
|
204
|
-
clauses.push(`DELETE ${deleteParts.join(", ")}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 6) merge in condition‐names/values
|
|
209
|
-
if (this.condition) {
|
|
210
|
-
Object.assign(ExpressionAttributeNames, this.condition.attributeNames);
|
|
211
|
-
Object.assign(ExpressionAttributeValues, this.condition.attributeValues);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 7) normalize empty values
|
|
215
|
-
const hasValues = Object.keys(ExpressionAttributeValues).length > 0;
|
|
216
|
-
const finalValues = hasValues ? ExpressionAttributeValues : undefined;
|
|
217
|
-
|
|
218
|
-
if (clauses.length === 0) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
"No attributes to update – all values were empty or undefined",
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
updateExpression: clauses.join(" "),
|
|
226
|
-
attributeNames: ExpressionAttributeNames,
|
|
227
|
-
attributeValues: finalValues,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Returns a transaction update item that can be included in a transactWrite call.
|
|
233
|
-
*/
|
|
234
|
-
public async toTransactUpdate(
|
|
235
|
-
newItemForIndexes?: T,
|
|
236
|
-
): Promise<TransactWriteItem> {
|
|
237
|
-
if (newItemForIndexes) {
|
|
238
|
-
newItemForIndexes = await this.createExpectedNewItem();
|
|
239
|
-
}
|
|
240
|
-
const { updateExpression, attributeNames, attributeValues } =
|
|
241
|
-
this.buildExpression(newItemForIndexes);
|
|
242
|
-
const updateItem: Update = {
|
|
243
|
-
TableName: this.parent.getTableName(),
|
|
244
|
-
Key: this.parent.buildKey(this.key),
|
|
245
|
-
UpdateExpression: updateExpression,
|
|
246
|
-
ExpressionAttributeNames: attributeNames,
|
|
247
|
-
ExpressionAttributeValues: attributeValues,
|
|
248
|
-
};
|
|
249
|
-
if (this.condition?.expression) {
|
|
250
|
-
updateItem.ConditionExpression = this.condition.expression;
|
|
251
|
-
}
|
|
252
|
-
return { Update: updateItem };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private async createExpectedNewItem() {
|
|
256
|
-
const existingItem = await this.parent.get(this.key).execute();
|
|
257
|
-
if (existingItem === null) {
|
|
258
|
-
throw new Error("Item not found");
|
|
259
|
-
}
|
|
260
|
-
const expectedNewItem: Record<string, any> = {
|
|
261
|
-
...existingItem,
|
|
262
|
-
...this.actions.set,
|
|
263
|
-
};
|
|
264
|
-
if (this.actions.remove) {
|
|
265
|
-
this.actions.remove.forEach((attr) => {
|
|
266
|
-
delete expectedNewItem[String(attr)];
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
if (this.actions.add) {
|
|
270
|
-
Object.entries(this.actions.add).forEach(([attr, value]) => {
|
|
271
|
-
const currentValue = expectedNewItem[attr] ?? 0;
|
|
272
|
-
expectedNewItem[attr] =
|
|
273
|
-
typeof value === "number" ? currentValue + value : value;
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
if (this.actions.delete) {
|
|
277
|
-
Object.entries(this.actions.delete).forEach(([attr, value]) => {
|
|
278
|
-
if (value instanceof Set) {
|
|
279
|
-
const currentSet = expectedNewItem[attr];
|
|
280
|
-
if (currentSet instanceof Set) {
|
|
281
|
-
value.forEach((v) => {
|
|
282
|
-
currentSet.delete(v);
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
return this.parent.getSchema().parse(expectedNewItem) as T;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Commits the update immediately by calling the parent's update method.
|
|
293
|
-
*/
|
|
294
|
-
public async execute(): Promise<T> {
|
|
295
|
-
const expectedNewItem = await this.createExpectedNewItem();
|
|
296
|
-
|
|
297
|
-
if (this.parent.getTimestamps()) {
|
|
298
|
-
const now = new Date().toISOString();
|
|
299
|
-
if (!this.actions.set) {
|
|
300
|
-
this.actions.set = {};
|
|
301
|
-
}
|
|
302
|
-
this.actions.set = { ...this.actions.set, updatedAt: now };
|
|
303
|
-
}
|
|
304
|
-
if (this.extraTransactItems.length > 0) {
|
|
305
|
-
// For transactions, we must throw if there's nothing to update
|
|
306
|
-
// since we can't safely skip updates in a transaction
|
|
307
|
-
const myTransactItem = await this.toTransactUpdate(expectedNewItem);
|
|
308
|
-
const allItems = [...this.extraTransactItems, myTransactItem];
|
|
309
|
-
await this.parent.getClient().send(
|
|
310
|
-
new TransactWriteCommand({
|
|
311
|
-
TransactItems: allItems,
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
// After transaction, retrieve the updated item.
|
|
315
|
-
const result = await this.parent.get(this.key).execute();
|
|
316
|
-
if (result === null) {
|
|
317
|
-
throw new Error("Item not found after transaction update");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return result;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// For normal updates, handle empty updates gracefully
|
|
324
|
-
try {
|
|
325
|
-
const { updateExpression, attributeNames, attributeValues } =
|
|
326
|
-
this.buildExpression(expectedNewItem);
|
|
327
|
-
|
|
328
|
-
let params: UpdateCommandInput = {
|
|
329
|
-
TableName: this.parent.getTableName(),
|
|
330
|
-
Key: this.parent.buildKey(this.key),
|
|
331
|
-
UpdateExpression: updateExpression,
|
|
332
|
-
ExpressionAttributeNames: attributeNames,
|
|
333
|
-
ExpressionAttributeValues: attributeValues,
|
|
334
|
-
ReturnValues: "ALL_NEW" as ReturnValue,
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
if (this.condition?.expression) {
|
|
338
|
-
params.ConditionExpression = this.condition.expression;
|
|
339
|
-
}
|
|
340
|
-
const result = await this.parent
|
|
341
|
-
.getClient()
|
|
342
|
-
.send(new UpdateCommand(params));
|
|
343
|
-
if (!result.Attributes) {
|
|
344
|
-
throw new Error("No attributes returned after update");
|
|
345
|
-
}
|
|
346
|
-
return this.parent.getSchema().parse(result.Attributes) as T;
|
|
347
|
-
} catch (error) {
|
|
348
|
-
// If there's nothing to update, just return the existing item
|
|
349
|
-
if (
|
|
350
|
-
error instanceof Error &&
|
|
351
|
-
error.message ===
|
|
352
|
-
"No attributes to update - all values were empty or undefined"
|
|
353
|
-
) {
|
|
354
|
-
const existingItem = await this.parent.get(this.key).execute();
|
|
355
|
-
if (existingItem === null) {
|
|
356
|
-
throw new Error("Item not found");
|
|
357
|
-
}
|
|
358
|
-
return existingItem;
|
|
359
|
-
}
|
|
360
|
-
throw error;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
package/src/operator.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export type Operator =
|
|
2
|
-
| "=="
|
|
3
|
-
| "!="
|
|
4
|
-
| "<"
|
|
5
|
-
| "<="
|
|
6
|
-
| ">"
|
|
7
|
-
| ">="
|
|
8
|
-
| "begins_with"
|
|
9
|
-
| "between"
|
|
10
|
-
| "contains";
|
|
11
|
-
|
|
12
|
-
export function getOperatorExpression(
|
|
13
|
-
operator: Operator,
|
|
14
|
-
nameKey: string,
|
|
15
|
-
valueKey: string,
|
|
16
|
-
secondValueKey?: string,
|
|
17
|
-
): string {
|
|
18
|
-
switch (operator) {
|
|
19
|
-
case "==":
|
|
20
|
-
return `${nameKey} = ${valueKey}`;
|
|
21
|
-
case "!=":
|
|
22
|
-
return `${nameKey} <> ${valueKey}`;
|
|
23
|
-
case "<":
|
|
24
|
-
return `${nameKey} < ${valueKey}`;
|
|
25
|
-
case "<=":
|
|
26
|
-
return `${nameKey} <= ${valueKey}`;
|
|
27
|
-
case ">":
|
|
28
|
-
return `${nameKey} > ${valueKey}`;
|
|
29
|
-
case ">=":
|
|
30
|
-
return `${nameKey} >= ${valueKey}`;
|
|
31
|
-
case "begins_with":
|
|
32
|
-
return `begins_with(${nameKey}, ${valueKey})`;
|
|
33
|
-
case "between":
|
|
34
|
-
if (!secondValueKey) {
|
|
35
|
-
throw new Error("The 'between' operator requires two value keys");
|
|
36
|
-
}
|
|
37
|
-
return `${nameKey} BETWEEN ${valueKey} AND ${secondValueKey}`;
|
|
38
|
-
case "contains":
|
|
39
|
-
return `contains(${nameKey}, ${valueKey})`;
|
|
40
|
-
default:
|
|
41
|
-
throw new Error("Unsupported operator");
|
|
42
|
-
}
|
|
43
|
-
}
|
package/test/batch-get.test.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { BetterDDB } from "../src/betterddb";
|
|
3
|
-
import { createTestTable, deleteTestTable } from "./utils/table-setup";
|
|
4
|
-
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
5
|
-
import { DynamoDB, GlobalSecondaryIndex } from "@aws-sdk/client-dynamodb";
|
|
6
|
-
import {
|
|
7
|
-
KeySchemaElement,
|
|
8
|
-
AttributeDefinition,
|
|
9
|
-
} from "@aws-sdk/client-dynamodb";
|
|
10
|
-
const TEST_TABLE = "batch-get-test-table";
|
|
11
|
-
const ENDPOINT = "http://localhost:4566";
|
|
12
|
-
const REGION = "us-east-1";
|
|
13
|
-
const ENTITY_TYPE = "USER";
|
|
14
|
-
const PRIMARY_KEY = "pk";
|
|
15
|
-
const PRIMARY_KEY_TYPE = "S";
|
|
16
|
-
const SORT_KEY = "sk";
|
|
17
|
-
const SORT_KEY_TYPE = "S";
|
|
18
|
-
const GSI_NAME = "EmailIndex";
|
|
19
|
-
const GSI_PRIMARY_KEY = "gsi1pk";
|
|
20
|
-
const GSI_SORT_KEY = "gsi1sk";
|
|
21
|
-
const KEY_SCHEMA = [
|
|
22
|
-
{ AttributeName: PRIMARY_KEY, KeyType: "HASH" },
|
|
23
|
-
{ AttributeName: SORT_KEY, KeyType: "RANGE" },
|
|
24
|
-
] as KeySchemaElement[];
|
|
25
|
-
const ATTRIBUTE_DEFINITIONS = [
|
|
26
|
-
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
27
|
-
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
28
|
-
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
29
|
-
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
30
|
-
] as AttributeDefinition[];
|
|
31
|
-
const GSIS = [
|
|
32
|
-
{
|
|
33
|
-
IndexName: GSI_NAME,
|
|
34
|
-
KeySchema: [
|
|
35
|
-
{ AttributeName: GSI_PRIMARY_KEY, KeyType: "HASH" },
|
|
36
|
-
{ AttributeName: GSI_SORT_KEY, KeyType: "RANGE" },
|
|
37
|
-
],
|
|
38
|
-
Projection: {
|
|
39
|
-
ProjectionType: "ALL",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
] as GlobalSecondaryIndex[];
|
|
43
|
-
const client = DynamoDBDocumentClient.from(
|
|
44
|
-
new DynamoDB({
|
|
45
|
-
region: REGION,
|
|
46
|
-
endpoint: ENDPOINT,
|
|
47
|
-
}),
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const UserSchema = z.object({
|
|
51
|
-
id: z.string(),
|
|
52
|
-
name: z.string(),
|
|
53
|
-
email: z.string().email(),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
type User = z.infer<typeof UserSchema>;
|
|
57
|
-
|
|
58
|
-
const userDdb = new BetterDDB<User>({
|
|
59
|
-
schema: UserSchema,
|
|
60
|
-
tableName: TEST_TABLE,
|
|
61
|
-
entityType: ENTITY_TYPE,
|
|
62
|
-
keys: {
|
|
63
|
-
primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
|
|
64
|
-
sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
|
|
65
|
-
},
|
|
66
|
-
client,
|
|
67
|
-
timestamps: true,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
beforeAll(async () => {
|
|
71
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
72
|
-
await userDdb
|
|
73
|
-
.create({ id: "user-123", name: "John Doe", email: "john@example.com" })
|
|
74
|
-
.execute();
|
|
75
|
-
await userDdb
|
|
76
|
-
.create({ id: "user-124", name: "John Doe", email: "john@example.com" })
|
|
77
|
-
.execute();
|
|
78
|
-
await userDdb
|
|
79
|
-
.create({ id: "user-125", name: "Bob Doe", email: "bob@example.com" })
|
|
80
|
-
.execute();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
afterAll(async () => {
|
|
84
|
-
await deleteTestTable(TEST_TABLE);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("BetterDDB - Get Operation", () => {
|
|
88
|
-
it("should retrieve an item using GetBuilder", async () => {
|
|
89
|
-
const users = await userDdb
|
|
90
|
-
.batchGet([
|
|
91
|
-
{ id: "user-123", email: "john@example.com" },
|
|
92
|
-
{ id: "user-124", email: "john@example.com" },
|
|
93
|
-
])
|
|
94
|
-
.execute();
|
|
95
|
-
expect(users.length).toEqual(2);
|
|
96
|
-
expect(users.some((user) => user.id === "user-123")).toBe(true);
|
|
97
|
-
expect(users.some((user) => user.id === "user-124")).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("should retrieve an item using GetBuilder that does not exist", async () => {
|
|
101
|
-
const users = await userDdb
|
|
102
|
-
.batchGet([{ id: "user-123", email: "jane@example.com" }])
|
|
103
|
-
.execute();
|
|
104
|
-
expect(users.length).toEqual(0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("should return an empty array if no keys are provided", async () => {
|
|
108
|
-
const users = await userDdb.batchGet([]).execute();
|
|
109
|
-
expect(users.length).toEqual(0);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("should deduplicate keys", async () => {
|
|
113
|
-
const users = await userDdb
|
|
114
|
-
.batchGet([
|
|
115
|
-
{ id: "user-123", email: "john@example.com" },
|
|
116
|
-
{ id: "user-123", email: "john@example.com" },
|
|
117
|
-
])
|
|
118
|
-
.execute();
|
|
119
|
-
expect(users.length).toEqual(1);
|
|
120
|
-
expect(users[0].id).toEqual("user-123");
|
|
121
|
-
});
|
|
122
|
-
});
|
package/test/create.test.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { BetterDDB } from "../src/betterddb";
|
|
3
|
-
import { createTestTable, deleteTestTable } from "./utils/table-setup";
|
|
4
|
-
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
5
|
-
import { DynamoDB, GlobalSecondaryIndex } from "@aws-sdk/client-dynamodb";
|
|
6
|
-
import { GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
7
|
-
import {
|
|
8
|
-
KeySchemaElement,
|
|
9
|
-
AttributeDefinition,
|
|
10
|
-
} from "@aws-sdk/client-dynamodb";
|
|
11
|
-
const TEST_TABLE = "create-test-table";
|
|
12
|
-
const ENDPOINT = "http://localhost:4566";
|
|
13
|
-
const REGION = "us-east-1";
|
|
14
|
-
const ENTITY_TYPE = "USER";
|
|
15
|
-
const PRIMARY_KEY = "pk";
|
|
16
|
-
const PRIMARY_KEY_TYPE = "S";
|
|
17
|
-
const SORT_KEY = "sk";
|
|
18
|
-
const SORT_KEY_TYPE = "S";
|
|
19
|
-
const GSI_NAME = "EmailIndex";
|
|
20
|
-
const GSI_PRIMARY_KEY = "gsi1pk";
|
|
21
|
-
const GSI_SORT_KEY = "gsi1sk";
|
|
22
|
-
const KEY_SCHEMA = [
|
|
23
|
-
{ AttributeName: PRIMARY_KEY, KeyType: "HASH" },
|
|
24
|
-
{ AttributeName: SORT_KEY, KeyType: "RANGE" },
|
|
25
|
-
] as KeySchemaElement[];
|
|
26
|
-
const ATTRIBUTE_DEFINITIONS = [
|
|
27
|
-
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
28
|
-
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
29
|
-
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
30
|
-
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
31
|
-
] as AttributeDefinition[];
|
|
32
|
-
const GSIS = [
|
|
33
|
-
{
|
|
34
|
-
IndexName: GSI_NAME,
|
|
35
|
-
KeySchema: [
|
|
36
|
-
{ AttributeName: GSI_PRIMARY_KEY, KeyType: "HASH" },
|
|
37
|
-
{ AttributeName: GSI_SORT_KEY, KeyType: "RANGE" },
|
|
38
|
-
],
|
|
39
|
-
Projection: {
|
|
40
|
-
ProjectionType: "ALL",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
] as GlobalSecondaryIndex[];
|
|
44
|
-
const client = DynamoDBDocumentClient.from(
|
|
45
|
-
new DynamoDB({
|
|
46
|
-
region: REGION,
|
|
47
|
-
endpoint: ENDPOINT,
|
|
48
|
-
}),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const UserSchema = z.object({
|
|
52
|
-
id: z.string(),
|
|
53
|
-
name: z.string(),
|
|
54
|
-
email: z.string().email(),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
type User = z.infer<typeof UserSchema>;
|
|
58
|
-
|
|
59
|
-
const userDdb = new BetterDDB<User>({
|
|
60
|
-
schema: UserSchema,
|
|
61
|
-
tableName: TEST_TABLE,
|
|
62
|
-
entityType: ENTITY_TYPE,
|
|
63
|
-
keys: {
|
|
64
|
-
primary: { name: "pk", definition: { build: (raw) => `USER#${raw.id}` } },
|
|
65
|
-
sort: { name: "sk", definition: { build: (raw) => `EMAIL#${raw.email}` } },
|
|
66
|
-
gsis: {
|
|
67
|
-
gsi1: {
|
|
68
|
-
name: "gsi1",
|
|
69
|
-
primary: { name: "gsi1pk", definition: { build: (raw) => "NAME" } },
|
|
70
|
-
sort: {
|
|
71
|
-
name: "gsi1sk",
|
|
72
|
-
definition: { build: (raw) => `NAME#${raw.name}` },
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
client,
|
|
78
|
-
timestamps: true,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
beforeAll(async () => {
|
|
82
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
afterAll(async () => {
|
|
86
|
-
await deleteTestTable(TEST_TABLE);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("BetterDDB - Create Operation", () => {
|
|
90
|
-
it("should insert an item using CreateBuilder", async () => {
|
|
91
|
-
const user = {
|
|
92
|
-
id: "user-123",
|
|
93
|
-
name: "John Doe",
|
|
94
|
-
email: "john@example.com",
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
await userDdb.create(user).execute();
|
|
98
|
-
|
|
99
|
-
const result = await client.send(
|
|
100
|
-
new GetCommand({
|
|
101
|
-
TableName: TEST_TABLE,
|
|
102
|
-
Key: { pk: "USER#user-123", sk: "EMAIL#john@example.com" },
|
|
103
|
-
}),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
expect(result).not.toBeNull();
|
|
107
|
-
expect(result.Item).not.toBeNull();
|
|
108
|
-
expect(result.Item?.pk).toBe("USER#user-123");
|
|
109
|
-
expect(result.Item?.sk).toBe("EMAIL#john@example.com");
|
|
110
|
-
expect(result.Item?.gsi1pk).toBe("NAME");
|
|
111
|
-
expect(result.Item?.gsi1sk).toBe("NAME#John Doe");
|
|
112
|
-
expect(result.Item?.id).toBe("user-123");
|
|
113
|
-
expect(result.Item?.createdAt).not.toBeNull();
|
|
114
|
-
expect(result.Item?.updatedAt).not.toBeNull();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("should fails to validate and not insert an item", async () => {
|
|
118
|
-
const user = { id: "user-123", email: "john@example.com" } as User;
|
|
119
|
-
await expect(userDdb.create(user).execute()).rejects.toThrow();
|
|
120
|
-
});
|
|
121
|
-
});
|