@ftschopp/dynatable-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +17 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts +18 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -0
- package/dist/builders/batch-get/create-batch-get-builder.js +71 -0
- package/dist/builders/batch-get/index.d.ts +3 -0
- package/dist/builders/batch-get/index.d.ts.map +1 -0
- package/dist/builders/batch-get/index.js +18 -0
- package/dist/builders/batch-get/types.d.ts +25 -0
- package/dist/builders/batch-get/types.d.ts.map +1 -0
- package/dist/builders/batch-get/types.js +2 -0
- package/dist/builders/batch-write/create-batch-write-builder.d.ts +12 -0
- package/dist/builders/batch-write/create-batch-write-builder.d.ts.map +1 -0
- package/dist/builders/batch-write/create-batch-write-builder.js +35 -0
- package/dist/builders/batch-write/index.d.ts +3 -0
- package/dist/builders/batch-write/index.d.ts.map +1 -0
- package/dist/builders/batch-write/index.js +18 -0
- package/dist/builders/batch-write/types.d.ts +30 -0
- package/dist/builders/batch-write/types.d.ts.map +1 -0
- package/dist/builders/batch-write/types.js +2 -0
- package/dist/builders/delete/create-delete-builder.d.ts +9 -0
- package/dist/builders/delete/create-delete-builder.d.ts.map +1 -0
- package/dist/builders/delete/create-delete-builder.js +72 -0
- package/dist/builders/delete/index.d.ts +3 -0
- package/dist/builders/delete/index.d.ts.map +1 -0
- package/dist/builders/delete/index.js +18 -0
- package/dist/builders/delete/types.d.ts +11 -0
- package/dist/builders/delete/types.d.ts.map +1 -0
- package/dist/builders/delete/types.js +2 -0
- package/dist/builders/get/create-get-builder.d.ts +12 -0
- package/dist/builders/get/create-get-builder.d.ts.map +1 -0
- package/dist/builders/get/create-get-builder.js +95 -0
- package/dist/builders/get/index.d.ts +3 -0
- package/dist/builders/get/index.d.ts.map +1 -0
- package/dist/builders/get/index.js +18 -0
- package/dist/builders/get/types.d.ts +28 -0
- package/dist/builders/get/types.d.ts.map +1 -0
- package/dist/builders/get/types.js +2 -0
- package/dist/builders/index.d.ts +12 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +29 -0
- package/dist/builders/put/create-put-builder.d.ts +9 -0
- package/dist/builders/put/create-put-builder.d.ts.map +1 -0
- package/dist/builders/put/create-put-builder.js +104 -0
- package/dist/builders/put/index.d.ts +3 -0
- package/dist/builders/put/index.d.ts.map +1 -0
- package/dist/builders/put/index.js +18 -0
- package/dist/builders/put/types.d.ts +17 -0
- package/dist/builders/put/types.d.ts.map +1 -0
- package/dist/builders/put/types.js +2 -0
- package/dist/builders/query/create-query-builder.d.ts +9 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -0
- package/dist/builders/query/create-query-builder.js +283 -0
- package/dist/builders/query/index.d.ts +3 -0
- package/dist/builders/query/index.d.ts.map +1 -0
- package/dist/builders/query/index.js +18 -0
- package/dist/builders/query/types.d.ts +79 -0
- package/dist/builders/query/types.d.ts.map +1 -0
- package/dist/builders/query/types.js +2 -0
- package/dist/builders/scan/create-scan-builder.d.ts +12 -0
- package/dist/builders/scan/create-scan-builder.d.ts.map +1 -0
- package/dist/builders/scan/create-scan-builder.js +103 -0
- package/dist/builders/scan/index.d.ts +3 -0
- package/dist/builders/scan/index.d.ts.map +1 -0
- package/dist/builders/scan/index.js +18 -0
- package/dist/builders/scan/types.d.ts +43 -0
- package/dist/builders/scan/types.d.ts.map +1 -0
- package/dist/builders/scan/types.js +2 -0
- package/dist/builders/shared/conditions.d.ts +18 -0
- package/dist/builders/shared/conditions.d.ts.map +1 -0
- package/dist/builders/shared/conditions.js +48 -0
- package/dist/builders/shared/index.d.ts +4 -0
- package/dist/builders/shared/index.d.ts.map +1 -0
- package/dist/builders/shared/index.js +19 -0
- package/dist/builders/shared/operators.d.ts +12 -0
- package/dist/builders/shared/operators.d.ts.map +1 -0
- package/dist/builders/shared/operators.js +197 -0
- package/dist/builders/shared/types.d.ts +77 -0
- package/dist/builders/shared/types.d.ts.map +1 -0
- package/dist/builders/shared/types.js +6 -0
- package/dist/builders/transact-get/create-transact-get-builder.d.ts +18 -0
- package/dist/builders/transact-get/create-transact-get-builder.d.ts.map +1 -0
- package/dist/builders/transact-get/create-transact-get-builder.js +60 -0
- package/dist/builders/transact-get/index.d.ts +3 -0
- package/dist/builders/transact-get/index.d.ts.map +1 -0
- package/dist/builders/transact-get/index.js +18 -0
- package/dist/builders/transact-get/types.d.ts +28 -0
- package/dist/builders/transact-get/types.d.ts.map +1 -0
- package/dist/builders/transact-get/types.js +2 -0
- package/dist/builders/transact-write/create-transact-write-builder.d.ts +18 -0
- package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -0
- package/dist/builders/transact-write/create-transact-write-builder.js +97 -0
- package/dist/builders/transact-write/index.d.ts +3 -0
- package/dist/builders/transact-write/index.d.ts.map +1 -0
- package/dist/builders/transact-write/index.js +18 -0
- package/dist/builders/transact-write/types.d.ts +34 -0
- package/dist/builders/transact-write/types.d.ts.map +1 -0
- package/dist/builders/transact-write/types.js +2 -0
- package/dist/builders/update/create-update-builder.d.ts +14 -0
- package/dist/builders/update/create-update-builder.d.ts.map +1 -0
- package/dist/builders/update/create-update-builder.js +180 -0
- package/dist/builders/update/index.d.ts +3 -0
- package/dist/builders/update/index.d.ts.map +1 -0
- package/dist/builders/update/index.js +18 -0
- package/dist/builders/update/types.d.ts +35 -0
- package/dist/builders/update/types.d.ts.map +1 -0
- package/dist/builders/update/types.js +2 -0
- package/dist/core/types.d.ts +224 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/entity.d.ts +73 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +161 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/table.d.ts +70 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +69 -0
- package/dist/utils/dynamodb-logger.d.ts +118 -0
- package/dist/utils/dynamodb-logger.d.ts.map +1 -0
- package/dist/utils/dynamodb-logger.js +125 -0
- package/dist/utils/model-utils.d.ts +24 -0
- package/dist/utils/model-utils.d.ts.map +1 -0
- package/dist/utils/model-utils.js +88 -0
- package/dist/utils/zod-utils.d.ts +13 -0
- package/dist/utils/zod-utils.d.ts.map +1 -0
- package/dist/utils/zod-utils.js +35 -0
- package/eslint.config.mjs +4 -0
- package/jest.config.js +11 -0
- package/package.json +36 -0
- package/src/builders/README.md +272 -0
- package/src/builders/batch-get/README.md +98 -0
- package/src/builders/batch-get/create-batch-get-builder.test.ts +165 -0
- package/src/builders/batch-get/create-batch-get-builder.ts +106 -0
- package/src/builders/batch-get/index.ts +2 -0
- package/src/builders/batch-get/types.ts +29 -0
- package/src/builders/batch-write/README.md +204 -0
- package/src/builders/batch-write/create-batch-write-builder.test.ts +173 -0
- package/src/builders/batch-write/create-batch-write-builder.ts +49 -0
- package/src/builders/batch-write/index.ts +2 -0
- package/src/builders/batch-write/types.ts +33 -0
- package/src/builders/delete/create-delete-builder.test.ts +294 -0
- package/src/builders/delete/create-delete-builder.ts +100 -0
- package/src/builders/delete/index.ts +2 -0
- package/src/builders/delete/types.ts +11 -0
- package/src/builders/get/create-get-builder.test.ts +272 -0
- package/src/builders/get/create-get-builder.ts +140 -0
- package/src/builders/get/index.ts +2 -0
- package/src/builders/get/types.ts +30 -0
- package/src/builders/index.ts +14 -0
- package/src/builders/put/create-put-builder.test.ts +213 -0
- package/src/builders/put/create-put-builder.ts +151 -0
- package/src/builders/put/index.ts +2 -0
- package/src/builders/put/types.ts +18 -0
- package/src/builders/query/create-query-builder.test.ts +230 -0
- package/src/builders/query/create-query-builder.ts +353 -0
- package/src/builders/query/index.ts +2 -0
- package/src/builders/query/types.ts +95 -0
- package/src/builders/scan/create-scan-builder.test.ts +260 -0
- package/src/builders/scan/create-scan-builder.ts +217 -0
- package/src/builders/scan/index.ts +2 -0
- package/src/builders/scan/types.ts +49 -0
- package/src/builders/shared/conditions.ts +58 -0
- package/src/builders/shared/index.ts +3 -0
- package/src/builders/shared/operators.test.ts +270 -0
- package/src/builders/shared/operators.ts +200 -0
- package/src/builders/shared/types.ts +100 -0
- package/src/builders/transact-get/README.md +167 -0
- package/src/builders/transact-get/create-transact-get-builder.test.ts +239 -0
- package/src/builders/transact-get/create-transact-get-builder.ts +67 -0
- package/src/builders/transact-get/index.ts +2 -0
- package/src/builders/transact-get/types.ts +31 -0
- package/src/builders/transact-write/README.md +166 -0
- package/src/builders/transact-write/create-transact-write-builder.test.ts +288 -0
- package/src/builders/transact-write/create-transact-write-builder.ts +118 -0
- package/src/builders/transact-write/index.ts +2 -0
- package/src/builders/transact-write/types.ts +33 -0
- package/src/builders/update/create-update-builder.test.ts +333 -0
- package/src/builders/update/create-update-builder.ts +284 -0
- package/src/builders/update/index.ts +2 -0
- package/src/builders/update/types.ts +42 -0
- package/src/core/types.test.ts +506 -0
- package/src/core/types.ts +290 -0
- package/src/entity.ts +337 -0
- package/src/index.ts +22 -0
- package/src/table.ts +109 -0
- package/src/utils/dynamodb-logger.test.ts +246 -0
- package/src/utils/dynamodb-logger.ts +175 -0
- package/src/utils/model-utils.test.ts +232 -0
- package/src/utils/model-utils.ts +101 -0
- package/src/utils/zod-utils.test.ts +272 -0
- package/src/utils/zod-utils.ts +36 -0
- package/tests/integration/instagram-clone.integration.test.ts +966 -0
- package/tests/integration/pagination-timestamps.integration.test.ts +375 -0
- package/tests/integration/transactions.integration.test.ts +529 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Builders Architecture
|
|
2
|
+
|
|
3
|
+
This directory contains the builder pattern implementation for DynamoDB operations.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
builders/
|
|
9
|
+
├── shared/ # Shared utilities and types
|
|
10
|
+
│ ├── types.ts # Base types (Condition, OpBuilder, etc.)
|
|
11
|
+
│ ├── operators.ts # Condition operators (eq, ne, lt, etc.)
|
|
12
|
+
│ └── conditions.ts # Condition expression builders
|
|
13
|
+
├── get/ # GET operation builder
|
|
14
|
+
│ ├── types.ts
|
|
15
|
+
│ ├── create-get-builder.ts
|
|
16
|
+
│ └── index.ts
|
|
17
|
+
├── put/ # PUT operation builder
|
|
18
|
+
│ ├── types.ts
|
|
19
|
+
│ ├── create-put-builder.ts
|
|
20
|
+
│ └── index.ts
|
|
21
|
+
└── query/ # QUERY operation builder
|
|
22
|
+
├── types.ts
|
|
23
|
+
├── create-query-builder.ts
|
|
24
|
+
└── index.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Design Principles
|
|
28
|
+
|
|
29
|
+
1. **Modularity**: Each operation has its own directory with dedicated types and implementation
|
|
30
|
+
2. **Reusability**: Common utilities are centralized in `shared/`
|
|
31
|
+
3. **Scalability**: Easy to add new operations (query, scan, delete, etc.) by following the same pattern
|
|
32
|
+
4. **Type Safety**: Full TypeScript support with proper type inference
|
|
33
|
+
|
|
34
|
+
## Adding a New Builder
|
|
35
|
+
|
|
36
|
+
To add a new operation (e.g., `query`):
|
|
37
|
+
|
|
38
|
+
1. Create a new directory: `builders/query/`
|
|
39
|
+
2. Add three files:
|
|
40
|
+
- `types.ts` - Interface definition extending `OperationBuilder` or `ExecutableBuilder`
|
|
41
|
+
- `create-query-builder.ts` - Implementation with immutable builder pattern
|
|
42
|
+
- `index.ts` - Export all public APIs
|
|
43
|
+
3. Import shared utilities from `../shared`
|
|
44
|
+
4. Export from main `builders/index.ts`
|
|
45
|
+
|
|
46
|
+
## Usage Examples
|
|
47
|
+
|
|
48
|
+
### GET Operation
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createGetBuilder } from './builders';
|
|
52
|
+
|
|
53
|
+
const getBuilder = createGetBuilder(tableName, key, client)
|
|
54
|
+
.select(['name', 'email'])
|
|
55
|
+
.consistentRead();
|
|
56
|
+
|
|
57
|
+
const item = await getBuilder.execute();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### PUT Operation
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { createPutBuilder } from './builders';
|
|
64
|
+
|
|
65
|
+
const putBuilder = createPutBuilder(tableName, item, client).ifNotExists().returning('ALL_NEW');
|
|
66
|
+
|
|
67
|
+
const result = await putBuilder.execute();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### QUERY Operation
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { createQueryBuilder } from './builders';
|
|
74
|
+
|
|
75
|
+
// Basic query with partition key
|
|
76
|
+
const query1 = createQueryBuilder(tableName, client, model)
|
|
77
|
+
.where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
78
|
+
.execute();
|
|
79
|
+
|
|
80
|
+
// Query with AND filter
|
|
81
|
+
const query2 = createQueryBuilder(tableName, client, model)
|
|
82
|
+
.where((attr, op) => op.and(op.eq(attr.username, 'juanca'), op.gt(attr.likesCount, 10)))
|
|
83
|
+
.limit(10)
|
|
84
|
+
.scanIndexForward(false)
|
|
85
|
+
.select(['id', 'title', 'createdAt'])
|
|
86
|
+
.execute();
|
|
87
|
+
|
|
88
|
+
// Query with OR filter
|
|
89
|
+
const query3 = createQueryBuilder(tableName, client, model)
|
|
90
|
+
.where((attr, op) =>
|
|
91
|
+
op.and(
|
|
92
|
+
op.eq(attr.username, 'juanca'),
|
|
93
|
+
op.or(op.gt(attr.likesCount, 100), op.gt(attr.commentCount, 50))
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
.execute();
|
|
97
|
+
|
|
98
|
+
// Complex nested conditions
|
|
99
|
+
const query4 = createQueryBuilder(tableName, client, model)
|
|
100
|
+
.where((attr, op) =>
|
|
101
|
+
op.and(
|
|
102
|
+
op.eq(attr.username, 'juanca'),
|
|
103
|
+
op.or(
|
|
104
|
+
op.and(op.gt(attr.likesCount, 100), op.lt(attr.commentCount, 10)),
|
|
105
|
+
op.gt(attr.likesCount, 500)
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
.execute();
|
|
110
|
+
|
|
111
|
+
// Query using a secondary index
|
|
112
|
+
const query5 = createQueryBuilder(tableName, client, model)
|
|
113
|
+
.where((attr, op) => op.eq(attr.status, 'active'))
|
|
114
|
+
.useIndex('GSI1')
|
|
115
|
+
.execute();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Type-Safe Queries
|
|
119
|
+
|
|
120
|
+
The Query Builder provides full TypeScript type safety with an intuitive API:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// Your model type
|
|
124
|
+
type Photo = {
|
|
125
|
+
username: string; // Used in partition key template
|
|
126
|
+
photoId: string; // Used in sort key template
|
|
127
|
+
url: string;
|
|
128
|
+
likesCount: number;
|
|
129
|
+
commentCount: number;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Type-safe query - TypeScript knows all fields and their types!
|
|
133
|
+
const photos = await createQueryBuilder<Photo>(tableName, client, model)
|
|
134
|
+
.where((attr, op) =>
|
|
135
|
+
op.and(
|
|
136
|
+
op.eq(attr.username, 'juanca'), // ✓ Key field - goes to KeyConditionExpression
|
|
137
|
+
op.gt(attr.likesCount, 10) // ✓ Non-key field - goes to FilterExpression
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
.select(['username', 'url', 'likesCount']) // ✓ Only Photo keys allowed
|
|
141
|
+
.execute();
|
|
142
|
+
|
|
143
|
+
// TypeScript will catch errors at compile time:
|
|
144
|
+
// .where((attr, op) => op.eq(attr.invalid, 'x')) // ✗ Property 'invalid' does not exist
|
|
145
|
+
// .where((attr, op) => op.eq(attr.username, 123)) // ✗ Argument of type 'number' not assignable
|
|
146
|
+
// .select(['invalidField']) // ✗ Type error!
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Important: Automatic Separation of Key Conditions vs Filters
|
|
150
|
+
|
|
151
|
+
The query builder **automatically separates** your conditions:
|
|
152
|
+
|
|
153
|
+
**KeyConditionExpression** (efficient, uses indexes):
|
|
154
|
+
|
|
155
|
+
- Fields used in partition key (pk) or sort key (sk) templates
|
|
156
|
+
- Evaluated during the query at the index level
|
|
157
|
+
- Very efficient - only reads matching items
|
|
158
|
+
|
|
159
|
+
**FilterExpression** (less efficient, post-processing):
|
|
160
|
+
|
|
161
|
+
- All other fields not in key templates
|
|
162
|
+
- Applied AFTER items are retrieved
|
|
163
|
+
- OR operators automatically go here (DynamoDB requirement)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// ✓ The builder handles this automatically!
|
|
167
|
+
.where((attr, op) => op.and(
|
|
168
|
+
op.eq(attr.username, 'juanca'), // → KeyConditionExpression (username is in pk template)
|
|
169
|
+
op.gt(attr.likesCount, 10) // → FilterExpression (likesCount is not a key)
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
// Generates:
|
|
173
|
+
// KeyConditionExpression: "#username = :username_0"
|
|
174
|
+
// FilterExpression: "#likesCount > :likesCount_1"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Important: Operator Isolation
|
|
178
|
+
|
|
179
|
+
Each builder creates its own isolated instance of operators with independent counters. This prevents naming conflicts when building multiple queries concurrently or reusing builder code.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// ✓ Each builder has isolated state
|
|
183
|
+
const query1 = createQueryBuilder(table, client).where((attr, op) => op.eq(attr.id, '1')); // Uses :id_0
|
|
184
|
+
|
|
185
|
+
const query2 = createQueryBuilder(table, client).where((attr, op) => op.eq(attr.id, '2')); // Also uses :id_0 (isolated counter)
|
|
186
|
+
|
|
187
|
+
// No conflicts! Each builder has its own counter starting at 0
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
For advanced use cases where you need to manually create operators, use `createOpBuilder()`:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { createOpBuilder } from './builders/shared';
|
|
194
|
+
|
|
195
|
+
const op = createOpBuilder(); // Creates isolated operator instance
|
|
196
|
+
const condition = op.eq(attr, value);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Available Operators
|
|
200
|
+
|
|
201
|
+
The `op` parameter in `.where()` provides these operators:
|
|
202
|
+
|
|
203
|
+
**Comparison Operators:**
|
|
204
|
+
|
|
205
|
+
- `op.eq(attr, value)` - Equals (=)
|
|
206
|
+
- `op.ne(attr, value)` - Not equals (<>)
|
|
207
|
+
- `op.lt(attr, value)` - Less than (<)
|
|
208
|
+
- `op.lte(attr, value)` - Less than or equal (<=)
|
|
209
|
+
- `op.gt(attr, value)` - Greater than (>)
|
|
210
|
+
- `op.gte(attr, value)` - Greater than or equal (>=)
|
|
211
|
+
- `op.between(attr, low, high)` - Between two values
|
|
212
|
+
- `op.beginsWith(attr, prefix)` - Begins with a string (for string fields)
|
|
213
|
+
|
|
214
|
+
**Logical Operators:**
|
|
215
|
+
|
|
216
|
+
- `op.and(...conditions)` - Combines conditions with AND
|
|
217
|
+
- `op.or(...conditions)` - Combines conditions with OR
|
|
218
|
+
- `op.not(condition)` - Negates a condition
|
|
219
|
+
|
|
220
|
+
### Example Usage
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Basic equality
|
|
224
|
+
.where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
225
|
+
|
|
226
|
+
// Comparison operators
|
|
227
|
+
.where((attr, op) => op.and(
|
|
228
|
+
op.eq(attr.username, 'juanca'),
|
|
229
|
+
op.gt(attr.age, 18),
|
|
230
|
+
op.lt(attr.score, 100)
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
// String operations
|
|
234
|
+
.where((attr, op) => op.beginsWith(attr.email, 'admin@'))
|
|
235
|
+
|
|
236
|
+
// Between operator
|
|
237
|
+
.where((attr, op) => op.between(attr.createdAt, '2024-01-01', '2024-12-31'))
|
|
238
|
+
|
|
239
|
+
// OR conditions
|
|
240
|
+
.where((attr, op) => op.or(
|
|
241
|
+
op.eq(attr.status, 'active'),
|
|
242
|
+
op.eq(attr.status, 'pending')
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
// Complex nested conditions
|
|
246
|
+
.where((attr, op) => op.and(
|
|
247
|
+
op.eq(attr.org, 'acme'),
|
|
248
|
+
op.or(
|
|
249
|
+
op.and(
|
|
250
|
+
op.gt(attr.score, 80),
|
|
251
|
+
op.eq(attr.verified, true)
|
|
252
|
+
),
|
|
253
|
+
op.eq(attr.role, 'admin')
|
|
254
|
+
)
|
|
255
|
+
))
|
|
256
|
+
|
|
257
|
+
// NOT operator
|
|
258
|
+
.where((attr, op) => op.not(op.eq(attr.status, 'deleted')))
|
|
259
|
+
|
|
260
|
+
// Complete example with all features
|
|
261
|
+
await table.entities.Photo.query()
|
|
262
|
+
.where((attr, op) => op.and(
|
|
263
|
+
op.eq(attr.username, 'juanca'), // Key field
|
|
264
|
+
op.or(
|
|
265
|
+
op.gt(attr.likesCount, 100), // Filter field
|
|
266
|
+
op.gt(attr.commentCount, 50) // Filter field
|
|
267
|
+
)
|
|
268
|
+
))
|
|
269
|
+
.limit(50)
|
|
270
|
+
.scanIndexForward(false)
|
|
271
|
+
.execute();
|
|
272
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# BatchGet Builder
|
|
2
|
+
|
|
3
|
+
The `BatchGetBuilder` allows retrieving multiple items from one or more tables in a single request to DynamoDB.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Retrieve multiple items by their keys
|
|
8
|
+
- Supports attribute projection (select only specific fields)
|
|
9
|
+
- Supports consistent read
|
|
10
|
+
- Can retrieve items from multiple tables in a single request
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { createBatchGetBuilder } from './builders/batch-get';
|
|
16
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
17
|
+
|
|
18
|
+
const client = new DynamoDBClient({});
|
|
19
|
+
|
|
20
|
+
// Retrieve multiple users
|
|
21
|
+
const result = await createBatchGetBuilder(
|
|
22
|
+
{
|
|
23
|
+
Users: {
|
|
24
|
+
Keys: [
|
|
25
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
26
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
27
|
+
{ pk: 'USER#charlie', sk: 'USER#charlie' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
client
|
|
32
|
+
).execute();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## With Attribute Projection
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
interface User {
|
|
39
|
+
username: string;
|
|
40
|
+
name: string;
|
|
41
|
+
followerCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await createBatchGetBuilder<User>(
|
|
45
|
+
{
|
|
46
|
+
Users: {
|
|
47
|
+
Keys: [
|
|
48
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
49
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
client
|
|
54
|
+
)
|
|
55
|
+
.select(['username', 'name', 'followerCount'])
|
|
56
|
+
.execute();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## With Consistent Read
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const result = await createBatchGetBuilder(
|
|
63
|
+
{
|
|
64
|
+
Users: {
|
|
65
|
+
Keys: [
|
|
66
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
67
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
client
|
|
72
|
+
)
|
|
73
|
+
.consistentRead()
|
|
74
|
+
.execute();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Using the Table Class
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Retrieve multiple users using the Table class
|
|
81
|
+
const result = await table
|
|
82
|
+
.batchGet({
|
|
83
|
+
InstagramClone: {
|
|
84
|
+
Keys: [
|
|
85
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
86
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
.select(['username', 'name'])
|
|
91
|
+
.execute();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## DynamoDB Limitations
|
|
95
|
+
|
|
96
|
+
- Maximum 100 items per request
|
|
97
|
+
- Maximum 16 MB of data per request
|
|
98
|
+
- Unprocessed items are returned in `UnprocessedKeys` and must be retried
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import { createBatchGetBuilder } from './create-batch-get-builder';
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
pk: string;
|
|
7
|
+
sk: string;
|
|
8
|
+
username: string;
|
|
9
|
+
name: string;
|
|
10
|
+
followerCount: number;
|
|
11
|
+
followingCount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('BatchGetBuilder', () => {
|
|
15
|
+
const client = new DynamoDBClient({});
|
|
16
|
+
|
|
17
|
+
describe('dbParams', () => {
|
|
18
|
+
test('should build basic BatchGetItem params', () => {
|
|
19
|
+
const requestItems = {
|
|
20
|
+
Users: {
|
|
21
|
+
Keys: [
|
|
22
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
23
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const builder = createBatchGetBuilder(requestItems, client);
|
|
29
|
+
const params = builder.dbParams();
|
|
30
|
+
|
|
31
|
+
expect(params).toEqual({
|
|
32
|
+
RequestItems: {
|
|
33
|
+
Users: {
|
|
34
|
+
Keys: [
|
|
35
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
36
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should build BatchGetItem params with projection', () => {
|
|
44
|
+
const requestItems = {
|
|
45
|
+
Users: {
|
|
46
|
+
Keys: [
|
|
47
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
48
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const builder = createBatchGetBuilder<User>(requestItems, client).select([
|
|
54
|
+
'username',
|
|
55
|
+
'name',
|
|
56
|
+
'followerCount',
|
|
57
|
+
]);
|
|
58
|
+
const params = builder.dbParams();
|
|
59
|
+
|
|
60
|
+
expect(params).toEqual({
|
|
61
|
+
RequestItems: {
|
|
62
|
+
Users: {
|
|
63
|
+
Keys: [
|
|
64
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
65
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
66
|
+
],
|
|
67
|
+
ProjectionExpression: 'username, name, followerCount',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should build BatchGetItem params with consistent read', () => {
|
|
74
|
+
const requestItems = {
|
|
75
|
+
Users: {
|
|
76
|
+
Keys: [
|
|
77
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
78
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const builder = createBatchGetBuilder(requestItems, client).consistentRead();
|
|
84
|
+
const params = builder.dbParams();
|
|
85
|
+
|
|
86
|
+
expect(params).toEqual({
|
|
87
|
+
RequestItems: {
|
|
88
|
+
Users: {
|
|
89
|
+
Keys: [
|
|
90
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
91
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
92
|
+
],
|
|
93
|
+
ConsistentRead: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should build BatchGetItem params with multiple tables', () => {
|
|
100
|
+
const requestItems = {
|
|
101
|
+
Users: {
|
|
102
|
+
Keys: [
|
|
103
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
104
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
Photos: {
|
|
108
|
+
Keys: [
|
|
109
|
+
{ pk: 'UP#alice', sk: 'PHOTO#photo1' },
|
|
110
|
+
{ pk: 'UP#bob', sk: 'PHOTO#photo2' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const builder = createBatchGetBuilder(requestItems, client);
|
|
116
|
+
const params = builder.dbParams();
|
|
117
|
+
|
|
118
|
+
expect(params).toEqual({
|
|
119
|
+
RequestItems: {
|
|
120
|
+
Users: {
|
|
121
|
+
Keys: [
|
|
122
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
123
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
Photos: {
|
|
127
|
+
Keys: [
|
|
128
|
+
{ pk: 'UP#alice', sk: 'PHOTO#photo1' },
|
|
129
|
+
{ pk: 'UP#bob', sk: 'PHOTO#photo2' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should build BatchGetItem params with projection and consistent read', () => {
|
|
137
|
+
const requestItems = {
|
|
138
|
+
Users: {
|
|
139
|
+
Keys: [
|
|
140
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
141
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const builder = createBatchGetBuilder<User>(requestItems, client)
|
|
147
|
+
.select(['username', 'name'])
|
|
148
|
+
.consistentRead();
|
|
149
|
+
const params = builder.dbParams();
|
|
150
|
+
|
|
151
|
+
expect(params).toEqual({
|
|
152
|
+
RequestItems: {
|
|
153
|
+
Users: {
|
|
154
|
+
Keys: [
|
|
155
|
+
{ pk: 'USER#alice', sk: 'USER#alice' },
|
|
156
|
+
{ pk: 'USER#bob', sk: 'USER#bob' },
|
|
157
|
+
],
|
|
158
|
+
ProjectionExpression: 'username, name',
|
|
159
|
+
ConsistentRead: true,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import {
|
|
4
|
+
BatchGetCommand,
|
|
5
|
+
BatchGetCommandInput,
|
|
6
|
+
BatchGetCommandOutput,
|
|
7
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
8
|
+
import { BatchGetBuilder } from './types';
|
|
9
|
+
import { DynamoDBLogger } from '../../utils/dynamodb-logger';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a BatchGetBuilder to retrieve multiple items by their keys.
|
|
13
|
+
*
|
|
14
|
+
* @param requestItems - An object where keys are table names and values are objects containing Keys array
|
|
15
|
+
* @param client - DynamoDB client instance
|
|
16
|
+
* @param options - Optional configuration (projection, consistentRead)
|
|
17
|
+
* @param logger - Optional logger instance
|
|
18
|
+
*/
|
|
19
|
+
export function createBatchGetBuilder<Model>(
|
|
20
|
+
requestItems: Record<string, { Keys: any[] }>,
|
|
21
|
+
client: DynamoDBClient,
|
|
22
|
+
options?: {
|
|
23
|
+
projection?: (keyof Model)[];
|
|
24
|
+
consistentRead?: boolean;
|
|
25
|
+
},
|
|
26
|
+
logger?: DynamoDBLogger
|
|
27
|
+
): BatchGetBuilder<Model> {
|
|
28
|
+
const projection = options?.projection ?? [];
|
|
29
|
+
const isConsistent = options?.consistentRead ?? false;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
/**
|
|
33
|
+
* Select only specific attributes to retrieve.
|
|
34
|
+
*/
|
|
35
|
+
select(attrs) {
|
|
36
|
+
return createBatchGetBuilder(
|
|
37
|
+
requestItems,
|
|
38
|
+
client,
|
|
39
|
+
{
|
|
40
|
+
projection: attrs,
|
|
41
|
+
consistentRead: isConsistent,
|
|
42
|
+
},
|
|
43
|
+
logger
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enable strongly consistent read.
|
|
49
|
+
*/
|
|
50
|
+
consistentRead() {
|
|
51
|
+
return createBatchGetBuilder(
|
|
52
|
+
requestItems,
|
|
53
|
+
client,
|
|
54
|
+
{
|
|
55
|
+
projection,
|
|
56
|
+
consistentRead: true,
|
|
57
|
+
},
|
|
58
|
+
logger
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the underlying DynamoDB input parameters.
|
|
64
|
+
*/
|
|
65
|
+
dbParams(): BatchGetCommandInput {
|
|
66
|
+
// Apply projection and consistentRead to all tables
|
|
67
|
+
const enhancedRequestItems = Object.entries(requestItems).reduce(
|
|
68
|
+
(acc, [tableName, tableRequest]) => {
|
|
69
|
+
acc[tableName] = {
|
|
70
|
+
Keys: tableRequest.Keys,
|
|
71
|
+
...(projection.length > 0 && {
|
|
72
|
+
ProjectionExpression: projection.join(', '),
|
|
73
|
+
}),
|
|
74
|
+
...(isConsistent && { ConsistentRead: true }),
|
|
75
|
+
};
|
|
76
|
+
return acc;
|
|
77
|
+
},
|
|
78
|
+
{} as Record<string, any>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
RequestItems: enhancedRequestItems,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute the BatchGetItem command and return the items.
|
|
88
|
+
*/
|
|
89
|
+
async execute(): Promise<Record<string, Model[]>> {
|
|
90
|
+
const params = this.dbParams();
|
|
91
|
+
const result: BatchGetCommandOutput = await client.send(new BatchGetCommand(params));
|
|
92
|
+
logger?.log('BatchGetCommand', params, result);
|
|
93
|
+
|
|
94
|
+
// Transform the response into a more usable format
|
|
95
|
+
const responses: Record<string, Model[]> = {};
|
|
96
|
+
|
|
97
|
+
if (result.Responses) {
|
|
98
|
+
for (const [tableName, items] of Object.entries(result.Responses)) {
|
|
99
|
+
responses[tableName] = items as unknown as Model[];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return responses;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { BatchGetItemCommandInput } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BatchGetBuilder allows you to retrieve multiple items from one or more tables
|
|
6
|
+
* in a single request.
|
|
7
|
+
*/
|
|
8
|
+
export type BatchGetBuilder<Model> = {
|
|
9
|
+
/**
|
|
10
|
+
* Select only specific attributes to retrieve for all items.
|
|
11
|
+
*/
|
|
12
|
+
select(attrs: (keyof Model)[]): BatchGetBuilder<Model>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enable strongly consistent read for all items.
|
|
16
|
+
*/
|
|
17
|
+
consistentRead(): BatchGetBuilder<Model>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the underlying DynamoDB BatchGetItem input parameters.
|
|
21
|
+
*/
|
|
22
|
+
dbParams(): BatchGetItemCommandInput;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute the BatchGetItem command and return the items.
|
|
26
|
+
* Returns an object mapping table names to arrays of items.
|
|
27
|
+
*/
|
|
28
|
+
execute(): Promise<Record<string, Model[]>>;
|
|
29
|
+
};
|