betterddb 0.6.7 → 0.6.9

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.
Files changed (48) hide show
  1. package/dist/betterddb.js +4 -4
  2. package/dist/builders/query-builder.js +9 -5
  3. package/dist/builders/update-builder.js +3 -3
  4. package/dist/types/betterddb.d.ts +8 -10
  5. package/dist/types/betterddb.d.ts.map +1 -1
  6. package/dist/types/builders/batch-get-builder.d.ts +1 -1
  7. package/dist/types/builders/batch-get-builder.d.ts.map +1 -1
  8. package/dist/types/builders/create-builder.d.ts +2 -2
  9. package/dist/types/builders/create-builder.d.ts.map +1 -1
  10. package/dist/types/builders/delete-builder.d.ts +4 -3
  11. package/dist/types/builders/delete-builder.d.ts.map +1 -1
  12. package/dist/types/builders/get-builder.d.ts +2 -2
  13. package/dist/types/builders/get-builder.d.ts.map +1 -1
  14. package/dist/types/builders/query-builder.d.ts +6 -5
  15. package/dist/types/builders/query-builder.d.ts.map +1 -1
  16. package/dist/types/builders/scan-builder.d.ts +6 -5
  17. package/dist/types/builders/scan-builder.d.ts.map +1 -1
  18. package/dist/types/builders/update-builder.d.ts +6 -5
  19. package/dist/types/builders/update-builder.d.ts.map +1 -1
  20. package/dist/types/types/paginated-result.d.ts +2 -1
  21. package/dist/types/types/paginated-result.d.ts.map +1 -1
  22. package/package.json +6 -3
  23. package/.eslintrc.cjs +0 -41
  24. package/.github/workflows/npm-publish.yml +0 -33
  25. package/LICENCSE +0 -21
  26. package/docker-compose.yml +0 -16
  27. package/jest.config.js +0 -16
  28. package/prettier.config.js +0 -6
  29. package/src/betterddb.ts +0 -263
  30. package/src/builders/batch-get-builder.ts +0 -56
  31. package/src/builders/create-builder.ts +0 -97
  32. package/src/builders/delete-builder.ts +0 -80
  33. package/src/builders/get-builder.ts +0 -75
  34. package/src/builders/query-builder.ts +0 -196
  35. package/src/builders/scan-builder.ts +0 -87
  36. package/src/builders/update-builder.ts +0 -231
  37. package/src/index.ts +0 -1
  38. package/src/operator.ts +0 -43
  39. package/src/types/paginated-result.ts +0 -4
  40. package/test/batch-get.test.ts +0 -97
  41. package/test/create.test.ts +0 -95
  42. package/test/delete.test.ts +0 -79
  43. package/test/get.test.ts +0 -85
  44. package/test/query.test.ts +0 -192
  45. package/test/scan.test.ts +0 -113
  46. package/test/update.test.ts +0 -84
  47. package/test/utils/table-setup.ts +0 -56
  48. package/tsconfig.json +0 -21
@@ -1,196 +0,0 @@
1
- import { QueryCommand, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
2
- import { BetterDDB, GSIConfig } from "../betterddb";
3
- import { getOperatorExpression, Operator } from "../operator";
4
- import { PaginatedResult } from "../types/paginated-result";
5
-
6
- export class QueryBuilder<T> {
7
- private keyConditions: string[] = [];
8
- private filterConditions: string[] = [];
9
- private expressionAttributeNames: Record<string, string> = {};
10
- private expressionAttributeValues: Record<string, any> = {};
11
- private index?: GSIConfig<T>;
12
- private limit?: number;
13
- private lastKey?: Record<string, any>;
14
- private ascending: boolean = true;
15
-
16
- constructor(
17
- private parent: BetterDDB<T>,
18
- private key: Partial<T>,
19
- ) {
20
- const keys = this.parent.getKeys();
21
- let pkName = keys.primary.name;
22
- let builtKey = this.parent.buildKey(this.key) as Record<string, any>;
23
-
24
- this.expressionAttributeNames["#pk"] = pkName;
25
- this.expressionAttributeValues[":pk_value"] = builtKey[pkName];
26
- }
27
-
28
- public usingIndex(indexName: string): this {
29
- if (!this.parent.getKeys().gsis) {
30
- throw new Error("No global secondary indexes defined for this table");
31
- }
32
- if (!(indexName in this.parent.getKeys().gsis!)) {
33
- throw new Error("index does not exist");
34
- }
35
-
36
- this.index = this.parent.getKeys().gsis![indexName];
37
-
38
- const pkName = this.index!.primary.name;
39
- const builtKey = this.parent.buildIndexes(this.key);
40
- this.expressionAttributeNames["#pk"] = pkName;
41
- this.expressionAttributeValues[":pk_value"] = builtKey[pkName];
42
-
43
- return this;
44
- }
45
-
46
- public sortAscending(): this {
47
- this.ascending = true;
48
- return this;
49
- }
50
-
51
- public sortDescending(): this {
52
- this.ascending = false;
53
- return this;
54
- }
55
-
56
- public where(
57
- operator: Operator,
58
- values: Partial<T> | [Partial<T>, Partial<T>],
59
- ): this {
60
- const keys = this.parent.getKeys();
61
- // Determine the sort key name from either the index or the primary keys.
62
- const sortKeyName = this.index ? this.index.sort?.name : keys.sort?.name;
63
- if (!sortKeyName) {
64
- throw new Error("Sort key is not defined for this table/index.");
65
- }
66
- const nameKey = "#sk";
67
- this.expressionAttributeNames[nameKey] = sortKeyName;
68
-
69
- // Enforce that a complex sort key requires an object input.
70
- if (typeof values !== "object" || values === null) {
71
- throw new Error(
72
- `For complex sort keys, please provide an object with all necessary properties.`,
73
- );
74
- }
75
-
76
- if (operator === "between") {
77
- if (!Array.isArray(values) || values.length !== 2) {
78
- throw new Error(
79
- `For 'between' operator, values must be a tuple of two objects`,
80
- );
81
- }
82
- const valueKeyStart = ":sk_start";
83
- const valueKeyEnd = ":sk_end";
84
- // Use the key definition's build function to build the key from the full object.
85
- this.expressionAttributeValues[valueKeyStart] = this.index
86
- ? this.parent.buildIndexes(values[0])[sortKeyName]
87
- : this.parent.buildKey(values[0])[sortKeyName];
88
- this.expressionAttributeValues[valueKeyEnd] = this.index
89
- ? this.parent.buildIndexes(values[1])[sortKeyName]
90
- : this.parent.buildKey(values[1])[sortKeyName];
91
- this.keyConditions.push(
92
- `${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`,
93
- );
94
- } else if (operator === "begins_with") {
95
- const valueKey = ":sk_value";
96
- this.expressionAttributeValues[valueKey] = this.index
97
- ? this.parent.buildIndexes(values as Partial<T>)[sortKeyName]
98
- : this.parent.buildKey(values as Partial<T>)[sortKeyName];
99
- this.keyConditions.push(`begins_with(${nameKey}, ${valueKey})`);
100
- } else {
101
- // For eq, lt, lte, gt, gte:
102
- const valueKey = ":sk_value";
103
- this.expressionAttributeValues[valueKey] = this.index
104
- ? this.parent.buildIndexes(values as Partial<T>)[sortKeyName]
105
- : this.parent.buildKey(values as Partial<T>)[sortKeyName];
106
- const condition = getOperatorExpression(operator, nameKey, valueKey);
107
- this.keyConditions.push(condition);
108
- }
109
- return this;
110
- }
111
-
112
- public filter(
113
- attribute: keyof T,
114
- operator: Operator,
115
- values: any | [any, any],
116
- ): this {
117
- const attrStr = String(attribute);
118
- const randomString = Math.random().toString(36).substring(2, 15);
119
- const placeholderName = `#attr_${attrStr}_${randomString}`;
120
- this.expressionAttributeNames[placeholderName] = attrStr;
121
- if (operator === "between") {
122
- if (!Array.isArray(values) || values.length !== 2) {
123
- throw new Error(
124
- "For 'between' operator, values must be a tuple of two items",
125
- );
126
- }
127
- const placeholderValueStart = `:val_start_${attrStr}_${randomString}`;
128
- const placeholderValueEnd = `:val_end_${attrStr}_${randomString}`;
129
- this.expressionAttributeValues[placeholderValueStart] = values[0];
130
- this.expressionAttributeValues[placeholderValueEnd] = values[1];
131
- this.filterConditions.push(
132
- `${placeholderName} BETWEEN ${placeholderValueStart} AND ${placeholderValueEnd}`,
133
- );
134
- } else if (operator === "begins_with" || operator === "contains") {
135
- const placeholderValue = `:val_${attrStr}_${randomString}`;
136
- this.expressionAttributeValues[placeholderValue] = values;
137
- this.filterConditions.push(
138
- `${operator}(${placeholderName}, ${placeholderValue})`,
139
- );
140
- } else {
141
- const placeholderValue = `:val_${attrStr}_${randomString}`;
142
- this.expressionAttributeValues[placeholderValue] = values;
143
- const condition = getOperatorExpression(
144
- operator,
145
- placeholderName,
146
- placeholderValue,
147
- );
148
- this.filterConditions.push(condition);
149
- }
150
-
151
- return this;
152
- }
153
-
154
- public limitResults(limit: number): this {
155
- this.limit = limit;
156
- return this;
157
- }
158
-
159
- public startFrom(lastKey: Record<string, any>): this {
160
- this.lastKey = lastKey;
161
- return this;
162
- }
163
-
164
- /**
165
- * Executes the query and returns a Promise that resolves with an array of items.
166
- */
167
- public async execute(): Promise<PaginatedResult<T>> {
168
- this.keyConditions.unshift(`#pk = :pk_value`);
169
- const keyConditionExpression = this.keyConditions.join(" AND ");
170
-
171
- const params: QueryCommandInput = {
172
- TableName: this.parent.getTableName(),
173
- KeyConditionExpression: keyConditionExpression,
174
- ExpressionAttributeNames: this.expressionAttributeNames,
175
- ExpressionAttributeValues: this.expressionAttributeValues,
176
- ScanIndexForward: this.ascending,
177
- Limit: this.limit,
178
- ExclusiveStartKey: this.lastKey,
179
- IndexName: this.index?.name ?? undefined,
180
- };
181
-
182
- if (this.parent.getEntityType()) {
183
- this.filterConditions.push(`#entity = :entity_value`);
184
- this.expressionAttributeNames["#entity"] = "entityType";
185
- this.expressionAttributeValues[":entity_value"] =
186
- this.parent.getEntityType();
187
- }
188
- params.FilterExpression = this.filterConditions.join(" AND ");
189
-
190
- const result = await this.parent.getClient().send(new QueryCommand(params));
191
- return {
192
- items: this.parent.getSchema().array().parse(result.Items) as T[],
193
- lastKey: result.LastEvaluatedKey ?? undefined,
194
- };
195
- }
196
- }
@@ -1,87 +0,0 @@
1
- import { ScanCommand, ScanCommandInput } from "@aws-sdk/lib-dynamodb";
2
- import { BetterDDB } from "../betterddb";
3
- import { getOperatorExpression, Operator } from "../operator";
4
- import { PaginatedResult } from "../types/paginated-result";
5
-
6
- export class ScanBuilder<T> {
7
- private filters: string[] = [];
8
- private expressionAttributeNames: Record<string, string> = {};
9
- private expressionAttributeValues: Record<string, any> = {};
10
- private limit?: number;
11
- private lastKey?: Record<string, any>;
12
-
13
- constructor(private parent: BetterDDB<T>) {}
14
-
15
- public where(
16
- attribute: keyof T,
17
- operator: Operator,
18
- values: any | [any, any],
19
- ): this {
20
- const attrStr = String(attribute);
21
- const nameKey = `#attr_${attrStr}`;
22
- this.expressionAttributeNames[nameKey] = attrStr;
23
-
24
- if (operator === "between") {
25
- if (!Array.isArray(values) || values.length !== 2) {
26
- throw new Error(
27
- `For 'between' operator, values must be a tuple of two items`,
28
- );
29
- }
30
- const valueKeyStart = `:val_start_${attrStr}`;
31
- const valueKeyEnd = `:val_end_${attrStr}`;
32
- this.expressionAttributeValues[valueKeyStart] = values[0];
33
- this.expressionAttributeValues[valueKeyEnd] = values[1];
34
- this.filters.push(
35
- `${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`,
36
- );
37
- } else if (operator === "begins_with" || operator === "contains") {
38
- const valueKey = `:val_${attrStr}`;
39
- this.expressionAttributeValues[valueKey] = values;
40
- this.filters.push(`${operator}(${nameKey}, ${valueKey})`);
41
- } else {
42
- const valueKey = `:val_${attrStr}`;
43
- this.expressionAttributeValues[valueKey] = values;
44
- const condition = getOperatorExpression(operator, nameKey, valueKey);
45
- this.filters.push(condition);
46
- }
47
- return this;
48
- }
49
-
50
- public limitResults(limit: number): this {
51
- this.limit = limit;
52
- return this;
53
- }
54
-
55
- public startFrom(lastKey: Record<string, any>): this {
56
- this.lastKey = lastKey;
57
- return this;
58
- }
59
-
60
- /**
61
- * Executes the scan and returns a Promise that resolves with an array of items.
62
- */
63
- public async execute(): Promise<PaginatedResult<T>> {
64
- const params: ScanCommandInput = {
65
- TableName: this.parent.getTableName(),
66
- ExpressionAttributeNames: this.expressionAttributeNames,
67
- ExpressionAttributeValues: this.expressionAttributeValues,
68
- Limit: this.limit,
69
- ExclusiveStartKey: this.lastKey,
70
- };
71
-
72
- if (this.parent.getEntityType()) {
73
- this.filters.push(`#entity = :entity_value`);
74
- this.expressionAttributeNames["#entity"] = "entityType";
75
- this.expressionAttributeValues[":entity_value"] =
76
- this.parent.getEntityType();
77
- }
78
- params.FilterExpression = this.filters.join(" AND ");
79
-
80
- const result = await this.parent.getClient().send(new ScanCommand(params));
81
-
82
- return {
83
- items: this.parent.getSchema().array().parse(result.Items) as T[],
84
- lastKey: result.LastEvaluatedKey ?? undefined,
85
- };
86
- }
87
- }
@@ -1,231 +0,0 @@
1
- import { TransactWriteCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
2
- import { BetterDDB } from "../betterddb";
3
- import {
4
- TransactWriteItem,
5
- Update,
6
- UpdateItemInput,
7
- } from "@aws-sdk/client-dynamodb";
8
- import { z } from "zod";
9
- interface UpdateActions<T> {
10
- set?: Partial<T>;
11
- remove?: (keyof T)[];
12
- add?: Partial<Record<keyof T, number | Set<any>>>;
13
- delete?: Partial<Record<keyof T, Set<any>>>;
14
- }
15
-
16
- export class UpdateBuilder<T> {
17
- private actions: UpdateActions<T> = {};
18
- private condition?: {
19
- expression: string;
20
- attributeValues: Record<string, any>;
21
- };
22
- // When using transaction mode, we store extra transaction items.
23
- private extraTransactItems: TransactWriteItem[] = [];
24
-
25
- // Reference to the parent BetterDDB instance and key.
26
- constructor(
27
- private parent: BetterDDB<T>,
28
- private key: Partial<T>,
29
- ) {}
30
-
31
- // Chainable methods:
32
- public set(attrs: Partial<T>): this {
33
- const partialSchema = (
34
- this.parent.getSchema() as unknown as z.ZodObject<any>
35
- ).partial();
36
- const validated = partialSchema.parse(attrs);
37
- this.actions.set = { ...this.actions.set, ...validated };
38
- return this;
39
- }
40
-
41
- public remove(attrs: (keyof T)[]): this {
42
- this.actions.remove = [...(this.actions.remove || []), ...attrs];
43
- return this;
44
- }
45
-
46
- public add(attrs: Partial<Record<keyof T, number | Set<any>>>): this {
47
- const partialSchema = (
48
- this.parent.getSchema() as unknown as z.ZodObject<any>
49
- ).partial();
50
- const validated = partialSchema.parse(attrs);
51
- this.actions.add = { ...this.actions.add, ...validated };
52
- return this;
53
- }
54
-
55
- public delete(attrs: Partial<Record<keyof T, Set<any>>>): this {
56
- this.actions.delete = { ...this.actions.delete, ...attrs };
57
- return this;
58
- }
59
-
60
- /**
61
- * Adds a condition expression to the update.
62
- */
63
- public setCondition(
64
- expression: string,
65
- attributeValues: Record<string, any>,
66
- ): this {
67
- if (this.condition) {
68
- // Merge conditions with AND.
69
- this.condition.expression += ` AND ${expression}`;
70
- Object.assign(this.condition.attributeValues, attributeValues);
71
- } else {
72
- this.condition = { expression, attributeValues };
73
- }
74
- return this;
75
- }
76
-
77
- /**
78
- * Specifies additional transaction items to include when executing this update as a transaction.
79
- */
80
- public transactWrite(ops: TransactWriteItem[] | TransactWriteItem): this {
81
- if (Array.isArray(ops)) {
82
- this.extraTransactItems.push(...ops);
83
- } else {
84
- this.extraTransactItems.push(ops);
85
- }
86
- return this;
87
- }
88
-
89
- /**
90
- * Builds the update expression and associated maps.
91
- */
92
- private buildExpression(): {
93
- updateExpression: string;
94
- attributeNames: Record<string, string>;
95
- attributeValues: Record<string, any>;
96
- } {
97
- const ExpressionAttributeNames: Record<string, string> = {};
98
- const ExpressionAttributeValues: Record<string, any> = {};
99
- const clauses: string[] = [];
100
-
101
- // Build SET clause.
102
- if (this.actions.set) {
103
- const setParts: string[] = [];
104
- for (const [attr, value] of Object.entries(this.actions.set)) {
105
- const nameKey = `#set_${attr}`;
106
- const valueKey = `:set_${attr}`;
107
- ExpressionAttributeNames[nameKey] = attr;
108
- ExpressionAttributeValues[valueKey] = value;
109
- setParts.push(`${nameKey} = ${valueKey}`);
110
- }
111
- if (setParts.length > 0) {
112
- clauses.push(`SET ${setParts.join(", ")}`);
113
- }
114
- }
115
-
116
- // Build REMOVE clause.
117
- if (this.actions.remove && this.actions.remove.length > 0) {
118
- const removeParts = this.actions.remove.map((attr) => {
119
- const nameKey = `#remove_${String(attr)}`;
120
- ExpressionAttributeNames[nameKey] = String(attr);
121
- return nameKey;
122
- });
123
- clauses.push(`REMOVE ${removeParts.join(", ")}`);
124
- }
125
-
126
- // Build ADD clause.
127
- if (this.actions.add) {
128
- const addParts: string[] = [];
129
- for (const [attr, value] of Object.entries(this.actions.add)) {
130
- const nameKey = `#add_${attr}`;
131
- const valueKey = `:add_${attr}`;
132
- ExpressionAttributeNames[nameKey] = attr;
133
- ExpressionAttributeValues[valueKey] = value;
134
- addParts.push(`${nameKey} ${valueKey}`);
135
- }
136
- if (addParts.length > 0) {
137
- clauses.push(`ADD ${addParts.join(", ")}`);
138
- }
139
- }
140
-
141
- // Build DELETE clause.
142
- if (this.actions.delete) {
143
- const deleteParts: string[] = [];
144
- for (const [attr, value] of Object.entries(this.actions.delete)) {
145
- const nameKey = `#delete_${attr}`;
146
- const valueKey = `:delete_${attr}`;
147
- ExpressionAttributeNames[nameKey] = attr;
148
- ExpressionAttributeValues[valueKey] = value;
149
- deleteParts.push(`${nameKey} ${valueKey}`);
150
- }
151
- if (deleteParts.length > 0) {
152
- clauses.push(`DELETE ${deleteParts.join(", ")}`);
153
- }
154
- }
155
-
156
- // Merge any provided condition attribute values.
157
- if (this.condition) {
158
- Object.assign(ExpressionAttributeValues, this.condition.attributeValues);
159
- }
160
-
161
- return {
162
- updateExpression: clauses.join(" "),
163
- attributeNames: ExpressionAttributeNames,
164
- attributeValues: ExpressionAttributeValues,
165
- };
166
- }
167
-
168
- /**
169
- * Returns a transaction update item that can be included in a transactWrite call.
170
- */
171
- public toTransactUpdate(): TransactWriteItem {
172
- const { updateExpression, attributeNames, attributeValues } =
173
- this.buildExpression();
174
- const updateItem: Update = {
175
- TableName: this.parent.getTableName(),
176
- Key: this.parent.buildKey(this.key),
177
- UpdateExpression: updateExpression,
178
- ExpressionAttributeNames: attributeNames,
179
- ExpressionAttributeValues: attributeValues,
180
- };
181
- if (this.condition && this.condition.expression) {
182
- updateItem.ConditionExpression = this.condition.expression;
183
- }
184
- return { Update: updateItem };
185
- }
186
-
187
- /**
188
- * Commits the update immediately by calling the parent's update method.
189
- */
190
- public async execute(): Promise<T> {
191
- if (this.extraTransactItems.length > 0) {
192
- // Build our update transaction item.
193
- const myTransactItem = this.toTransactUpdate();
194
- // Combine with extra transaction items.
195
- const allItems = [...this.extraTransactItems, myTransactItem];
196
- await this.parent.getClient().send(
197
- new TransactWriteCommand({
198
- TransactItems: allItems,
199
- }),
200
- );
201
- // After transaction, retrieve the updated item.
202
- const result = await this.parent.get(this.key).execute();
203
- if (result === null) {
204
- throw new Error("Item not found after transaction update");
205
- }
206
- return result;
207
- } else {
208
- // Normal update flow.
209
- const { updateExpression, attributeNames, attributeValues } =
210
- this.buildExpression();
211
- const params: UpdateItemInput = {
212
- TableName: this.parent.getTableName(),
213
- Key: this.parent.buildKey(this.key),
214
- UpdateExpression: updateExpression,
215
- ExpressionAttributeNames: attributeNames,
216
- ExpressionAttributeValues: attributeValues,
217
- ReturnValues: "ALL_NEW",
218
- };
219
- if (this.condition && this.condition.expression) {
220
- params.ConditionExpression = this.condition.expression;
221
- }
222
- const result = await this.parent
223
- .getClient()
224
- .send(new UpdateCommand(params));
225
- if (!result.Attributes) {
226
- throw new Error("No attributes returned after update");
227
- }
228
- return this.parent.getSchema().parse(result.Attributes) as T;
229
- }
230
- }
231
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./betterddb";
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
- }
@@ -1,4 +0,0 @@
1
- export type PaginatedResult<T> = {
2
- items: T[];
3
- lastKey: Record<string, any> | undefined;
4
- };
@@ -1,97 +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 { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
7
- const TEST_TABLE = "batch-get-test-table";
8
- const ENDPOINT = 'http://localhost:4566';
9
- const REGION = 'us-east-1';
10
- const ENTITY_TYPE = 'USER';
11
- const PRIMARY_KEY = 'pk';
12
- const PRIMARY_KEY_TYPE = 'S';
13
- const SORT_KEY = 'sk';
14
- const SORT_KEY_TYPE = 'S';
15
- const GSI_NAME = 'EmailIndex';
16
- const GSI_PRIMARY_KEY = 'gsi1pk';
17
- const GSI_SORT_KEY = 'gsi1sk';
18
- const KEY_SCHEMA = [
19
- { AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
20
- { AttributeName: SORT_KEY, KeyType: 'RANGE' }
21
- ] as KeySchemaElement[];
22
- const ATTRIBUTE_DEFINITIONS = [
23
- { AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
24
- { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
25
- { AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
26
- { AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
27
- ] as AttributeDefinition[];
28
- const GSIS = [
29
- {
30
- IndexName: GSI_NAME,
31
- KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
32
- Projection: {
33
- ProjectionType: 'ALL',
34
- },
35
- },
36
- ] as GlobalSecondaryIndex[];
37
- const client = DynamoDBDocumentClient.from(new DynamoDB({
38
- region: REGION,
39
- endpoint: ENDPOINT,
40
- }));
41
-
42
-
43
- const UserSchema = z.object({
44
- id: z.string(),
45
- name: z.string(),
46
- email: z.string().email(),
47
- });
48
-
49
- type User = z.infer<typeof UserSchema>;
50
-
51
- const userDdb = new BetterDDB({
52
- schema: UserSchema,
53
- tableName: TEST_TABLE,
54
- entityType: ENTITY_TYPE,
55
- keys: {
56
- primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
57
- sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
58
- },
59
- client,
60
- timestamps: true,
61
- });
62
-
63
- beforeAll(async () => {
64
- await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
65
- await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
66
- await userDdb.create({ id: 'user-124', name: 'John Doe', email: 'john@example.com' } as any).execute();
67
- await userDdb.create({ id: 'user-125', name: 'Bob Doe', email: 'bob@example.com' } as any).execute();
68
- });
69
-
70
- afterAll(async () => {
71
- await deleteTestTable(TEST_TABLE);
72
- });
73
-
74
- describe('BetterDDB - Get Operation', () => {
75
- it('should retrieve an item using GetBuilder', async () => {
76
- const users = await userDdb.batchGet([{ id: 'user-123', email: 'john@example.com' }, { id: 'user-124', email: 'john@example.com' }]).execute();
77
- expect(users.length).toEqual(2);
78
- expect(users.some(user => user.id === 'user-123')).toBe(true);
79
- expect(users.some(user => user.id === 'user-124')).toBe(true);
80
- });
81
-
82
- it('should retrieve an item using GetBuilder that does not exist', async () => {
83
- const users = await userDdb.batchGet([{ id: 'user-123', email: 'jane@example.com' }]).execute();
84
- expect(users.length).toEqual(0);
85
- });
86
-
87
- it('should return an empty array if no keys are provided', async () => {
88
- const users = await userDdb.batchGet([]).execute();
89
- expect(users.length).toEqual(0);
90
- });
91
-
92
- it('should deduplicate keys', async () => {
93
- const users = await userDdb.batchGet([{ id: 'user-123', email: 'john@example.com' }, { id: 'user-123', email: 'john@example.com' }]).execute();
94
- expect(users.length).toEqual(1);
95
- expect(users[0].id).toEqual('user-123');
96
- });
97
- });