dyno-table 0.0.2 → 0.1.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/README.md +680 -239
- package/dist/index.d.ts +2836 -362
- package/dist/index.js +3075 -963
- package/package.json +6 -11
package/README.md
CHANGED
|
@@ -1,344 +1,785 @@
|
|
|
1
|
-
# 🦖 dyno-table
|
|
1
|
+
# 🦖 dyno-table [](https://www.npmjs.com/package/dyno-table) [](https://opensource.org/licenses/MIT)
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
**A type-safe, fluent interface for DynamoDB single-table designs**
|
|
4
|
+
*Tame the NoSQL wilderness with a robust abstraction layer that brings order to DynamoDB operations*
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
<img src="docs/images/geoff-the-dyno.png" width="400" height="250" alt="Geoff the Dyno" style="float: right; margin-left: 20px; margin-bottom: 20px;">
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
```ts
|
|
9
|
+
// Type-safe DynamoDB operations made simple
|
|
10
|
+
await table
|
|
11
|
+
.update<Dinosaur>({
|
|
12
|
+
pk: 'SPECIES#trex',
|
|
13
|
+
sk: 'PROFILE#001'
|
|
14
|
+
})
|
|
15
|
+
.set('diet', 'Carnivore')
|
|
16
|
+
.add('sightings', 1)
|
|
17
|
+
.condition(op => op.eq('status', 'ACTIVE'))
|
|
18
|
+
.execute();
|
|
19
|
+
```
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
-
|
|
13
|
-
- **
|
|
14
|
-
-
|
|
15
|
-
-
|
|
21
|
+
## 🌟 Why dyno-table?
|
|
22
|
+
|
|
23
|
+
- **🧩 Single-table design made simple** - Clean abstraction layer for complex DynamoDB patterns
|
|
24
|
+
- **🛡️ Type-safe operations** - Full TypeScript support with strict type checking
|
|
25
|
+
- **⚡ Fluent API** - Chainable builder pattern for complex operations
|
|
26
|
+
- **🔒 Transactional safety** - ACID-compliant operations with easy-to-use transactions
|
|
27
|
+
- **📈 Scalability built-in** - Automatic batch chunking and pagination handling
|
|
28
|
+
|
|
29
|
+
## 📑 Table of Contents
|
|
30
|
+
|
|
31
|
+
- [Installation](#-installation)
|
|
32
|
+
- [Quick Start](#-quick-start)
|
|
33
|
+
- [Query](#-type-safe-query-building)
|
|
34
|
+
- [Update](#update-operations)
|
|
35
|
+
- [Condition Operators](#condition-operators)
|
|
36
|
+
- [Multiple Operations](#multiple-operations)
|
|
37
|
+
- [Type Safety Features](#-type-safety-features)
|
|
38
|
+
- [Nested Object Support](#nested-object-support)
|
|
39
|
+
- [Type-Safe Conditions](#type-safe-conditions)
|
|
40
|
+
- [Batch Operations](#-batch-operations)
|
|
41
|
+
- [Batch Get](#batch-get)
|
|
42
|
+
- [Batch Write](#batch-write)
|
|
43
|
+
- [Transaction Operations](#-transaction-operations)
|
|
44
|
+
- [Transaction Builder](#transaction-builder)
|
|
45
|
+
- [Transaction Options](#transaction-options)
|
|
46
|
+
- [Error Handling](#-error-handling)
|
|
47
|
+
- [API Reference](#-api-reference)
|
|
16
48
|
|
|
17
49
|
## 📦 Installation
|
|
18
50
|
|
|
19
|
-
Get started with Dyno Table by installing it via npm:
|
|
20
|
-
|
|
21
51
|
```bash
|
|
22
52
|
npm install dyno-table
|
|
23
53
|
```
|
|
24
54
|
|
|
25
|
-
|
|
55
|
+
*Note: Requires AWS SDK v3 as peer dependency*
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
59
|
+
```
|
|
26
60
|
|
|
27
|
-
|
|
61
|
+
## 🚀 Quick Start
|
|
28
62
|
|
|
29
|
-
|
|
63
|
+
### 1. Configure Your Table
|
|
30
64
|
|
|
31
65
|
```ts
|
|
66
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
32
67
|
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
33
68
|
import { Table } from "dyno-table";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
|
|
70
|
+
// Configure AWS SDK clients
|
|
71
|
+
const client = new DynamoDBClient({ region: "us-west-2" });
|
|
72
|
+
const docClient = DynamoDBDocument.from(client);
|
|
73
|
+
|
|
74
|
+
// Initialize table with single-table design schema
|
|
75
|
+
const dinoTable = new Table(docClient, {
|
|
76
|
+
name: "DinosaurPark",
|
|
77
|
+
partitionKey: "pk",
|
|
78
|
+
sortKey: "sk",
|
|
79
|
+
gsis: [
|
|
80
|
+
{
|
|
81
|
+
name: "GSI1",
|
|
82
|
+
keySchema: {
|
|
83
|
+
pk: "GSI1PK",
|
|
84
|
+
sk: "GSI1SK",
|
|
85
|
+
},
|
|
47
86
|
},
|
|
48
|
-
|
|
87
|
+
],
|
|
49
88
|
});
|
|
50
89
|
```
|
|
51
90
|
|
|
52
|
-
###
|
|
91
|
+
### 2. Perform Type-Safe Operations
|
|
53
92
|
|
|
54
|
-
|
|
93
|
+
**🦖 Creating a new dinosaur**
|
|
94
|
+
```ts
|
|
95
|
+
const rex = await dinoTable
|
|
96
|
+
.create<Dinosaur>({
|
|
97
|
+
pk: "SPECIES#trex",
|
|
98
|
+
sk: "PROFILE#trex",
|
|
99
|
+
speciesId: "trex",
|
|
100
|
+
name: "Tyrannosaurus Rex",
|
|
101
|
+
diet: "carnivore",
|
|
102
|
+
length: 12.3,
|
|
103
|
+
discoveryYear: 1902
|
|
104
|
+
})
|
|
105
|
+
.execute();
|
|
106
|
+
```
|
|
55
107
|
|
|
108
|
+
**🔍 Query with conditions**
|
|
56
109
|
```ts
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await table.put(dino).execute();
|
|
68
|
-
|
|
69
|
-
// Conditional put
|
|
70
|
-
await table
|
|
71
|
-
.put(dino)
|
|
72
|
-
.whereNotExists("pk") // Only insert if dinosaur doesn't exist
|
|
73
|
-
.whereNotExists("sk")
|
|
110
|
+
const largeDinos = await dinoTable
|
|
111
|
+
.query<Dinosaur>({
|
|
112
|
+
pk: "SPECIES#trex",
|
|
113
|
+
sk: (op) => op.beginsWith("PROFILE#")
|
|
114
|
+
})
|
|
115
|
+
.filter((op) => op.and(
|
|
116
|
+
op.gte("length", 10),
|
|
117
|
+
op.eq("diet", "carnivore")
|
|
118
|
+
))
|
|
119
|
+
.limit(10)
|
|
74
120
|
.execute();
|
|
75
121
|
```
|
|
76
122
|
|
|
77
|
-
|
|
123
|
+
**🔄 Complex update operation**
|
|
124
|
+
```ts
|
|
125
|
+
await dinoTable
|
|
126
|
+
.update<Dinosaur>({
|
|
127
|
+
pk: "SPECIES#trex",
|
|
128
|
+
sk: "PROFILE#trex"
|
|
129
|
+
})
|
|
130
|
+
.set("diet", "omnivore")
|
|
131
|
+
.add("discoveryYear", 1)
|
|
132
|
+
.remove("outdatedField")
|
|
133
|
+
.condition((op) => op.attributeExists("discoverySite"))
|
|
134
|
+
.execute();
|
|
135
|
+
```
|
|
78
136
|
|
|
137
|
+
## 🧩 Advanced Features
|
|
138
|
+
|
|
139
|
+
### Transactional Operations
|
|
140
|
+
|
|
141
|
+
**Safe dinosaur transfer between enclosures**
|
|
79
142
|
```ts
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
143
|
+
// Start a transaction session for transferring a dinosaur
|
|
144
|
+
await dinoTable.transaction(async (tx) => {
|
|
145
|
+
// All operations are executed as a single transaction (up to 100 operations)
|
|
146
|
+
|
|
147
|
+
// Check if destination enclosure is ready and compatible
|
|
148
|
+
await dinoTable
|
|
149
|
+
.conditionCheck({
|
|
150
|
+
pk: "ENCLOSURE#B",
|
|
151
|
+
sk: "STATUS"
|
|
152
|
+
})
|
|
153
|
+
.condition(op => op.and(
|
|
154
|
+
op.eq("status", "READY"),
|
|
155
|
+
op.eq("diet", "Carnivore") // Ensure enclosure matches dinosaur diet
|
|
156
|
+
))
|
|
157
|
+
.withTransaction(tx);
|
|
158
|
+
|
|
159
|
+
// Remove dinosaur from current enclosure
|
|
160
|
+
await dinoTable
|
|
161
|
+
.delete<Dinosaur>({
|
|
162
|
+
pk: "ENCLOSURE#A",
|
|
163
|
+
sk: "DINO#001"
|
|
164
|
+
})
|
|
165
|
+
.condition(op => op.and(
|
|
166
|
+
op.eq("status", "HEALTHY"),
|
|
167
|
+
op.gte("health", 80) // Only transfer healthy dinosaurs
|
|
168
|
+
))
|
|
169
|
+
.withTransaction(tx);
|
|
170
|
+
|
|
171
|
+
// Add dinosaur to new enclosure
|
|
172
|
+
await dinoTable
|
|
173
|
+
.create<Dinosaur>({
|
|
174
|
+
pk: "ENCLOSURE#B",
|
|
175
|
+
sk: "DINO#001",
|
|
176
|
+
name: "Rex",
|
|
177
|
+
species: "Tyrannosaurus",
|
|
178
|
+
diet: "Carnivore",
|
|
179
|
+
status: "HEALTHY",
|
|
180
|
+
health: 100,
|
|
181
|
+
enclosureId: "B",
|
|
182
|
+
lastFed: new Date().toISOString()
|
|
183
|
+
})
|
|
184
|
+
.withTransaction(tx);
|
|
185
|
+
|
|
186
|
+
// Update enclosure occupancy tracking
|
|
187
|
+
await dinoTable
|
|
188
|
+
.update<Dinosaur>({
|
|
189
|
+
pk: "ENCLOSURE#B",
|
|
190
|
+
sk: "OCCUPANCY"
|
|
191
|
+
})
|
|
192
|
+
.add("currentOccupants", 1)
|
|
193
|
+
.set("lastUpdated", new Date().toISOString())
|
|
194
|
+
.withTransaction(tx);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Transaction with feeding and health monitoring
|
|
198
|
+
await dinoTable.transaction(
|
|
199
|
+
async (tx) => {
|
|
200
|
+
// Update dinosaur health and feeding status
|
|
201
|
+
await dinoTable
|
|
202
|
+
.update<Dinosaur>({
|
|
203
|
+
pk: "ENCLOSURE#D",
|
|
204
|
+
sk: "DINO#003"
|
|
205
|
+
})
|
|
206
|
+
.set({
|
|
207
|
+
status: "HEALTHY",
|
|
208
|
+
lastFed: new Date().toISOString(),
|
|
209
|
+
health: 100
|
|
210
|
+
})
|
|
211
|
+
.deleteElementsFromSet("tags", ["needs_feeding"])
|
|
212
|
+
.withTransaction(tx);
|
|
213
|
+
|
|
214
|
+
// Update enclosure feeding schedule
|
|
215
|
+
await dinoTable
|
|
216
|
+
.update<Dinosaur>({
|
|
217
|
+
pk: "ENCLOSURE#D",
|
|
218
|
+
sk: "SCHEDULE"
|
|
219
|
+
})
|
|
220
|
+
.set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString())
|
|
221
|
+
.withTransaction(tx);
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
clientRequestToken: "feeding-session-001",
|
|
225
|
+
returnConsumedCapacity: "TOTAL"
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
```
|
|
83
229
|
|
|
84
|
-
|
|
85
|
-
|
|
230
|
+
**Benefits of this transaction approach:**
|
|
231
|
+
- 🔄 Uses the same familiar API as non-transactional operations
|
|
232
|
+
- 🧠 Maintains consistent mental model for developers
|
|
233
|
+
- 🔒 All operations within the callback are executed as a single transaction
|
|
234
|
+
- ✅ All-or-nothing operations (ACID compliance)
|
|
235
|
+
- 🛡️ Prevents race conditions and data inconsistencies
|
|
236
|
+
- 📊 Supports up to 100 actions per transaction
|
|
86
237
|
```
|
|
87
238
|
|
|
88
|
-
|
|
239
|
+
### Batch Processing
|
|
89
240
|
|
|
241
|
+
**Efficient dinosaur park management with bulk operations**
|
|
90
242
|
```ts
|
|
91
|
-
//
|
|
92
|
-
const
|
|
93
|
-
|
|
243
|
+
// Batch health check for multiple dinosaurs
|
|
244
|
+
const healthCheckKeys = [
|
|
245
|
+
{ pk: "ENCLOSURE#A", sk: "DINO#001" }, // T-Rex
|
|
246
|
+
{ pk: "ENCLOSURE#B", sk: "DINO#002" }, // Velociraptor
|
|
247
|
+
{ pk: "ENCLOSURE#C", sk: "DINO#003" } // Stegosaurus
|
|
248
|
+
];
|
|
94
249
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
250
|
+
const { items: dinosaurs, unprocessedKeys } = await dinoTable.batchGet<Dinosaur>(healthCheckKeys);
|
|
251
|
+
console.log(`Health check completed for ${dinosaurs.length} dinosaurs`);
|
|
252
|
+
dinosaurs.forEach(dino => {
|
|
253
|
+
if (dino.health < 80) {
|
|
254
|
+
console.log(`Health alert for ${dino.name} in Enclosure ${dino.enclosureId}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Batch update feeding schedule for herbivore group
|
|
259
|
+
const newHerbivores = [
|
|
260
|
+
{
|
|
261
|
+
pk: "ENCLOSURE#D", sk: "DINO#004",
|
|
262
|
+
name: "Triceratops Alpha",
|
|
263
|
+
species: "Triceratops",
|
|
264
|
+
diet: "Herbivore",
|
|
265
|
+
status: "HEALTHY",
|
|
266
|
+
health: 95,
|
|
267
|
+
lastFed: new Date().toISOString()
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
pk: "ENCLOSURE#D", sk: "DINO#005",
|
|
271
|
+
name: "Brachy",
|
|
272
|
+
species: "Brachiosaurus",
|
|
273
|
+
diet: "Herbivore",
|
|
274
|
+
status: "HEALTHY",
|
|
275
|
+
health: 90,
|
|
276
|
+
lastFed: new Date().toISOString()
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
// Add new herbivores to enclosure
|
|
281
|
+
await dinoTable.batchWrite(
|
|
282
|
+
newHerbivores.map(dino => ({
|
|
283
|
+
type: "put",
|
|
284
|
+
item: dino
|
|
285
|
+
}))
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Mixed operations: relocate dinosaurs and update enclosure status
|
|
289
|
+
await dinoTable.batchWrite([
|
|
290
|
+
// Remove dinosaur from quarantine
|
|
291
|
+
{ type: "delete", key: { pk: "ENCLOSURE#QUARANTINE", sk: "DINO#006" } },
|
|
292
|
+
// Add recovered dinosaur to main enclosure
|
|
293
|
+
{
|
|
294
|
+
type: "put",
|
|
295
|
+
item: {
|
|
296
|
+
pk: "ENCLOSURE#E", sk: "DINO#006",
|
|
297
|
+
name: "Raptor Beta",
|
|
298
|
+
species: "Velociraptor",
|
|
299
|
+
diet: "Carnivore",
|
|
300
|
+
status: "HEALTHY",
|
|
301
|
+
health: 100,
|
|
302
|
+
lastFed: new Date().toISOString()
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
// Clear quarantine status
|
|
306
|
+
{ type: "delete", key: { pk: "ENCLOSURE#QUARANTINE", sk: "STATUS#DINO#006" } }
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
// Handle large-scale park operations
|
|
310
|
+
// (25 items per batch write, 100 items per batch get)
|
|
311
|
+
const dailyHealthUpdates = generateDinosaurHealthUpdates(); // Hundreds of updates
|
|
312
|
+
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked
|
|
104
313
|
```
|
|
105
314
|
|
|
106
|
-
|
|
315
|
+
### Pagination Made Simple
|
|
107
316
|
|
|
317
|
+
**Efficient dinosaur record browsing**
|
|
108
318
|
```ts
|
|
109
|
-
//
|
|
110
|
-
|
|
319
|
+
// Create a paginator for viewing herbivores by health status
|
|
320
|
+
const healthyHerbivores = dinoTable
|
|
321
|
+
.query<Dinosaur>({
|
|
322
|
+
pk: "DIET#herbivore",
|
|
323
|
+
sk: op => op.beginsWith("STATUS#HEALTHY")
|
|
324
|
+
})
|
|
325
|
+
.filter((op) => op.and(
|
|
326
|
+
op.gte("health", 90),
|
|
327
|
+
op.attributeExists("lastFed")
|
|
328
|
+
))
|
|
329
|
+
.paginate(5); // View 5 dinosaurs at a time
|
|
330
|
+
|
|
331
|
+
// Monitor all enclosures page by page
|
|
332
|
+
while (healthyHerbivores.hasNextPage()) {
|
|
333
|
+
const page = await healthyHerbivores.getNextPage();
|
|
334
|
+
console.log(`Checking herbivores page ${page.page}, found ${page.items.length} dinosaurs`);
|
|
335
|
+
page.items.forEach(dino => {
|
|
336
|
+
console.log(`${dino.name}: Health ${dino.health}%, Last fed: ${dino.lastFed}`);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
111
339
|
|
|
112
|
-
//
|
|
113
|
-
await
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
340
|
+
// Get all carnivores for daily feeding schedule
|
|
341
|
+
const carnivoreSchedule = await dinoTable
|
|
342
|
+
.query<Dinosaur>({
|
|
343
|
+
pk: "DIET#carnivore",
|
|
344
|
+
sk: op => op.beginsWith("ENCLOSURE#")
|
|
345
|
+
})
|
|
346
|
+
.filter(op => op.attributeExists("lastFed"))
|
|
347
|
+
.paginate(10)
|
|
348
|
+
.getAllPages();
|
|
349
|
+
|
|
350
|
+
console.log(`Scheduling feeding for ${carnivoreSchedule.length} carnivores`);
|
|
351
|
+
|
|
352
|
+
// Limited view for visitor information kiosk
|
|
353
|
+
const visitorKiosk = dinoTable
|
|
354
|
+
.query<Dinosaur>({
|
|
355
|
+
pk: "VISITOR_VIEW",
|
|
356
|
+
sk: op => op.beginsWith("SPECIES#")
|
|
357
|
+
})
|
|
358
|
+
.filter(op => op.eq("status", "ON_DISPLAY"))
|
|
359
|
+
.limit(12) // Show max 12 dinosaurs per view
|
|
360
|
+
.paginate(4); // Display 4 at a time
|
|
361
|
+
|
|
362
|
+
// Get first page for kiosk display
|
|
363
|
+
const firstPage = await visitorKiosk.getNextPage();
|
|
364
|
+
console.log(`Now showing: ${firstPage.items.map(d => d.name).join(", ")}`);
|
|
118
365
|
```
|
|
119
366
|
|
|
367
|
+
## 🛡️ Type-Safe Query Building
|
|
368
|
+
|
|
369
|
+
Dyno-table provides comprehensive query methods that match DynamoDB's capabilities while maintaining type safety:
|
|
370
|
+
|
|
371
|
+
### Comparison Operators
|
|
372
|
+
|
|
373
|
+
| Operation | Method Example | Generated Expression |
|
|
374
|
+
|---------------------------|---------------------------------------------------------|-----------------------------------|
|
|
375
|
+
| **Equals** | `.filter(op => op.eq("status", "ACTIVE"))` | `status = :v1` |
|
|
376
|
+
| **Not Equals** | `.filter(op => op.ne("status", "DELETED"))` | `status <> :v1` |
|
|
377
|
+
| **Less Than** | `.filter(op => op.lt("age", 18))` | `age < :v1` |
|
|
378
|
+
| **Less Than or Equal** | `.filter(op => op.lte("score", 100))` | `score <= :v1` |
|
|
379
|
+
| **Greater Than** | `.filter(op => op.gt("price", 50))` | `price > :v1` |
|
|
380
|
+
| **Greater Than or Equal** | `.filter(op => op.gte("rating", 4))` | `rating >= :v1` |
|
|
381
|
+
| **Between** | `.filter(op => op.between("age", 18, 65))` | `age BETWEEN :v1 AND :v2` |
|
|
382
|
+
| **Begins With** | `.filter(op => op.beginsWith("email", "@example.com"))` | `begins_with(email, :v1)` |
|
|
383
|
+
| **Contains** | `.filter(op => op.contains("tags", "important"))` | `contains(tags, :v1)` |
|
|
384
|
+
| **Attribute Exists** | `.filter(op => op.attributeExists("email"))` | `attribute_exists(email)` |
|
|
385
|
+
| **Attribute Not Exists** | `.filter(op => op.attributeNotExists("deletedAt"))` | `attribute_not_exists(deletedAt)` |
|
|
386
|
+
| **Nested Attributes** | `.filter(op => op.eq("address.city", "London"))` | `address.city = :v1` |
|
|
387
|
+
|
|
388
|
+
### Logical Operators
|
|
389
|
+
|
|
390
|
+
| Operation | Method Example | Generated Expression |
|
|
391
|
+
|-----------|-----------------------------------------------------------------------------------|--------------------------------|
|
|
392
|
+
| **AND** | `.filter(op => op.and(op.eq("status", "ACTIVE"), op.gt("age", 18)))` | `status = :v1 AND age > :v2` |
|
|
393
|
+
| **OR** | `.filter(op => op.or(op.eq("status", "PENDING"), op.eq("status", "PROCESSING")))` | `status = :v1 OR status = :v2` |
|
|
394
|
+
| **NOT** | `.filter(op => op.not(op.eq("status", "DELETED")))` | `NOT status = :v1` |
|
|
395
|
+
|
|
120
396
|
### Query Operations
|
|
121
397
|
|
|
398
|
+
| Operation | Method Example | Generated Expression |
|
|
399
|
+
|--------------------------|--------------------------------------------------------------------------------------|---------------------------------------|
|
|
400
|
+
| **Partition Key Equals** | `.query({ pk: "USER#123" })` | `pk = :pk` |
|
|
401
|
+
| **Sort Key Begins With** | `.query({ pk: "USER#123", sk: op => op.beginsWith("ORDER#2023") })` | `pk = :pk AND begins_with(sk, :v1)` |
|
|
402
|
+
| **Sort Key Between** | `.query({ pk: "USER#123", sk: op => op.between("ORDER#2023-01", "ORDER#2023-12") })` | `pk = :pk AND sk BETWEEN :v1 AND :v2` |
|
|
403
|
+
|
|
404
|
+
Additional query options:
|
|
122
405
|
```ts
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
.query({ pk: "
|
|
406
|
+
// Sort order
|
|
407
|
+
const ascending = await table
|
|
408
|
+
.query({ pk: "USER#123" })
|
|
409
|
+
.sortAscending()
|
|
126
410
|
.execute();
|
|
127
411
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
pk: "SPECIES#velociraptor",
|
|
132
|
-
sk: { operator: "begins_with", value: "PROFILE#" }
|
|
133
|
-
})
|
|
134
|
-
.where("type", "=", "DINOSAUR")
|
|
135
|
-
.whereGreaterThan("length", 6)
|
|
136
|
-
.limit(10)
|
|
137
|
-
.useIndex("GSI1")
|
|
412
|
+
const descending = await table
|
|
413
|
+
.query({ pk: "USER#123" })
|
|
414
|
+
.sortDescending()
|
|
138
415
|
.execute();
|
|
139
416
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// .whereContains(field, value) // contains check
|
|
152
|
-
// .whereNotContains(field, value) // not_contains check
|
|
153
|
-
// .whereExists(field) // attribute_exists check
|
|
154
|
-
// .whereNotExists(field) // attribute_not_exists check
|
|
155
|
-
// .whereAttributeType(field, type) // attribute_type check
|
|
417
|
+
// Projection (select specific attributes)
|
|
418
|
+
const partial = await table
|
|
419
|
+
.query({ pk: "USER#123" })
|
|
420
|
+
.select(["name", "email"])
|
|
421
|
+
.execute();
|
|
422
|
+
|
|
423
|
+
// Limit results
|
|
424
|
+
const limited = await table
|
|
425
|
+
.query({ pk: "USER#123" })
|
|
426
|
+
.limit(10)
|
|
427
|
+
.execute();
|
|
156
428
|
```
|
|
157
429
|
|
|
158
|
-
###
|
|
430
|
+
### Update Operations
|
|
159
431
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
432
|
+
| Operation | Method Example | Generated Expression |
|
|
433
|
+
|----------------------|-------------------------------------------------------|----------------------|
|
|
434
|
+
| **Set Attributes** | `.update(key).set("name", "New Name")` | `SET #name = :v1` |
|
|
435
|
+
| **Add to Number** | `.update(key).add("score", 10)` | `ADD #score :v1` |
|
|
436
|
+
| **Remove Attribute** | `.update(key).remove("temporary")` | `REMOVE #temporary` |
|
|
437
|
+
| **Delete From Set** | `.update(key).deleteElementsFromSet("tags", ["old"])` | `DELETE #tags :v1` |
|
|
438
|
+
|
|
439
|
+
#### Condition Operators
|
|
440
|
+
|
|
441
|
+
The library supports a comprehensive set of type-safe condition operators:
|
|
442
|
+
|
|
443
|
+
| Category | Operators | Example |
|
|
444
|
+
|----------------|-----------------------------------------|-------------------------------------------------------------------------|
|
|
445
|
+
| **Comparison** | `eq`, `ne`, `lt`, `lte`, `gt`, `gte` | `.condition(op => op.gt("age", 18))` |
|
|
446
|
+
| **String/Set** | `between`, `beginsWith`, `contains` | `.condition(op => op.beginsWith("email", "@example"))` |
|
|
447
|
+
| **Existence** | `attributeExists`, `attributeNotExists` | `.condition(op => op.attributeExists("email"))` |
|
|
448
|
+
| **Logical** | `and`, `or`, `not` | `.condition(op => op.and(op.eq("status", "active"), op.gt("age", 18)))` |
|
|
449
|
+
|
|
450
|
+
All operators are type-safe and will provide proper TypeScript inference for nested attributes.
|
|
163
451
|
|
|
164
|
-
|
|
452
|
+
#### Multiple Operations
|
|
453
|
+
Operations can be combined in a single update:
|
|
454
|
+
```ts
|
|
165
455
|
const result = await table
|
|
166
|
-
.
|
|
167
|
-
.
|
|
168
|
-
.
|
|
169
|
-
.
|
|
456
|
+
.update({ pk: "USER#123", sk: "PROFILE" })
|
|
457
|
+
.set("name", "Updated Name")
|
|
458
|
+
.add("loginCount", 1)
|
|
459
|
+
.remove("temporaryFlag")
|
|
460
|
+
.condition(op => op.attributeExists("email"))
|
|
170
461
|
.execute();
|
|
171
|
-
|
|
172
|
-
// Scan supports all the same conditions as Query operations
|
|
173
462
|
```
|
|
174
463
|
|
|
175
|
-
|
|
464
|
+
## 🔄 Type Safety Features
|
|
176
465
|
|
|
466
|
+
The library provides comprehensive type safety for all operations:
|
|
467
|
+
|
|
468
|
+
### Nested Object Support
|
|
177
469
|
```ts
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
470
|
+
interface Dinosaur {
|
|
471
|
+
pk: string;
|
|
472
|
+
sk: string;
|
|
473
|
+
name: string;
|
|
474
|
+
species: string;
|
|
475
|
+
stats: {
|
|
476
|
+
health: number;
|
|
477
|
+
weight: number;
|
|
478
|
+
length: number;
|
|
479
|
+
age: number;
|
|
480
|
+
};
|
|
481
|
+
habitat: {
|
|
482
|
+
enclosure: {
|
|
483
|
+
id: string;
|
|
484
|
+
section: string;
|
|
485
|
+
climate: string;
|
|
486
|
+
};
|
|
487
|
+
requirements: {
|
|
488
|
+
temperature: number;
|
|
489
|
+
humidity: number;
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
care: {
|
|
493
|
+
feeding: {
|
|
494
|
+
schedule: string;
|
|
495
|
+
diet: string;
|
|
496
|
+
lastFed: string;
|
|
497
|
+
};
|
|
498
|
+
medical: {
|
|
499
|
+
lastCheckup: string;
|
|
500
|
+
vaccinations: string[];
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
}
|
|
183
504
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
505
|
+
// TypeScript ensures type safety for all nested dinosaur attributes
|
|
506
|
+
await table.update<Dinosaur>({ pk: "ENCLOSURE#F", sk: "DINO#007" })
|
|
507
|
+
.set("stats.health", 95) // ✓ Valid
|
|
508
|
+
.set("habitat.enclosure.climate", "Tropical") // ✓ Valid
|
|
509
|
+
.set("care.feeding.lastFed", new Date().toISOString()) // ✓ Valid
|
|
510
|
+
.set("stats.invalid", true) // ❌ TypeScript Error: property doesn't exist
|
|
511
|
+
.execute();
|
|
512
|
+
```
|
|
187
513
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
514
|
+
### Type-Safe Conditions
|
|
515
|
+
```ts
|
|
516
|
+
interface DinosaurMonitoring {
|
|
517
|
+
species: string;
|
|
518
|
+
health: number;
|
|
519
|
+
lastFed: string;
|
|
520
|
+
temperature: number;
|
|
521
|
+
behavior: string[];
|
|
522
|
+
alertLevel: "LOW" | "MEDIUM" | "HIGH";
|
|
523
|
+
}
|
|
193
524
|
|
|
194
|
-
|
|
525
|
+
await table.query<DinosaurMonitoring>({
|
|
526
|
+
pk: "MONITORING",
|
|
527
|
+
sk: op => op.beginsWith("ENCLOSURE#")
|
|
528
|
+
})
|
|
529
|
+
.filter(op => op.and(
|
|
530
|
+
op.lt("health", "90"), // ❌ TypeScript Error: health expects number
|
|
531
|
+
op.gt("temperature", 38), // ✓ Valid
|
|
532
|
+
op.contains("behavior", "aggressive"), // ✓ Valid
|
|
533
|
+
op.eq("alertLevel", "UNKNOWN") // ❌ TypeScript Error: invalid alert level
|
|
534
|
+
))
|
|
535
|
+
.execute();
|
|
195
536
|
```
|
|
196
537
|
|
|
197
|
-
|
|
538
|
+
## 🔄 Batch Operations
|
|
198
539
|
|
|
199
|
-
|
|
200
|
-
// Limit to 10 items per page
|
|
201
|
-
const paginator = await table.query({ pk: "SPECIES#trex" }).limit(10).paginate();
|
|
202
|
-
// const paginator = await table.scan().limit(10).paginate();
|
|
540
|
+
The library supports efficient batch operations for both reading and writing multiple items:
|
|
203
541
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
542
|
+
### Batch Get
|
|
543
|
+
```ts
|
|
544
|
+
const { items, unprocessedKeys } = await table.batchGet<User>([
|
|
545
|
+
{ pk: "USER#1", sk: "PROFILE" },
|
|
546
|
+
{ pk: "USER#2", sk: "PROFILE" }
|
|
547
|
+
]);
|
|
208
548
|
```
|
|
209
549
|
|
|
210
|
-
###
|
|
550
|
+
### Batch Write
|
|
551
|
+
```ts
|
|
552
|
+
const { unprocessedItems } = await table.batchWrite<User>([
|
|
553
|
+
{ type: "put", item: newUser },
|
|
554
|
+
{ type: "delete", key: { pk: "USER#123", sk: "PROFILE" } }
|
|
555
|
+
]);
|
|
556
|
+
```
|
|
211
557
|
|
|
212
|
-
|
|
558
|
+
## 🔒 Transaction Operations
|
|
213
559
|
|
|
214
|
-
|
|
560
|
+
Perform multiple operations atomically with transaction support:
|
|
215
561
|
|
|
562
|
+
### Transaction Builder
|
|
216
563
|
```ts
|
|
217
|
-
await table.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
564
|
+
const result = await table.transaction(async (tx) => {
|
|
565
|
+
// Building the expression manually
|
|
566
|
+
tx.put("TableName", { pk: "123", sk: "123"}, and(op.attributeNotExists("pk"), op.attributeExists("sk")));
|
|
567
|
+
|
|
568
|
+
// Using table to build the operation
|
|
569
|
+
table
|
|
570
|
+
.put({ pk: "123", sk: "123" })
|
|
571
|
+
.condition((op) => {
|
|
572
|
+
return op.and(op.attributeNotExists("pk"), op.attributeExists("sk"));
|
|
573
|
+
})
|
|
574
|
+
.withTransaction(tx);
|
|
575
|
+
|
|
576
|
+
// Building raw condition check
|
|
577
|
+
tx.conditionCheck(
|
|
578
|
+
"TestTable",
|
|
579
|
+
{ pk: "transaction#test", sk: "condition#item" },
|
|
580
|
+
eq("status", "active"),
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
// Using table to build the condition check
|
|
584
|
+
table
|
|
585
|
+
.conditionCheck({
|
|
586
|
+
pk: "transaction#test",
|
|
587
|
+
sk: "conditional#item",
|
|
588
|
+
})
|
|
589
|
+
.condition((op) => op.eq("status", "active"));
|
|
221
590
|
});
|
|
222
591
|
```
|
|
223
592
|
|
|
224
|
-
|
|
225
|
-
|
|
593
|
+
### Transaction Options
|
|
226
594
|
```ts
|
|
227
|
-
const
|
|
595
|
+
const result = await table.transaction(
|
|
596
|
+
async (tx) => {
|
|
597
|
+
// ... transaction operations
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
// Optional transaction settings
|
|
601
|
+
idempotencyToken: "unique-token",
|
|
602
|
+
returnValuesOnConditionCheckFailure: true
|
|
603
|
+
}
|
|
604
|
+
);
|
|
605
|
+
```
|
|
228
606
|
|
|
229
|
-
|
|
230
|
-
.addOperation({
|
|
231
|
-
put: { item: trex }
|
|
232
|
-
})
|
|
233
|
-
.addOperation({
|
|
234
|
-
put: { item: raptor }
|
|
235
|
-
})
|
|
236
|
-
.addOperation({
|
|
237
|
-
delete: { key: brontoKey }
|
|
238
|
-
});
|
|
607
|
+
## 🏗️ Entity Pattern Best Practices (Coming Soon TM)
|
|
239
608
|
|
|
240
|
-
|
|
609
|
+
The entity implementation provides automatic type isolation:
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
// All operations are automatically scoped to DINOSAUR type
|
|
613
|
+
const dinosaur = await dinoEntity.get("SPECIES#trex", "PROFILE#trex");
|
|
614
|
+
// Returns Dinosaur | undefined
|
|
615
|
+
|
|
616
|
+
// Cross-type operations are prevented at compile time
|
|
617
|
+
dinoEntity.create({ /* invalid shape */ }); // TypeScript error
|
|
241
618
|
```
|
|
242
619
|
|
|
243
|
-
|
|
620
|
+
**Key benefits:**
|
|
621
|
+
- 🚫 Prevents accidental cross-type data access
|
|
622
|
+
- 🔍 Automatically filters queries/scans to repository type
|
|
623
|
+
- 🛡️ Ensures consistent key structure across entities
|
|
624
|
+
- 📦 Encapsulates domain-specific query logic
|
|
244
625
|
|
|
245
|
-
|
|
626
|
+
## 🚨 Error Handling
|
|
246
627
|
|
|
247
|
-
|
|
248
|
-
|
|
628
|
+
**TODO:**
|
|
629
|
+
to provide a more clear set of error classes and additional information to allow for an easier debugging experience
|
|
249
630
|
|
|
250
|
-
|
|
251
|
-
id: string;
|
|
252
|
-
name: string;
|
|
253
|
-
diet: string;
|
|
254
|
-
length: number;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
class DinoRepository extends BaseRepository<DinoRecord> {
|
|
258
|
-
protected createPrimaryKey(data: DinoRecord) {
|
|
259
|
-
return {
|
|
260
|
-
pk: `SPECIES#${data.id}`,
|
|
261
|
-
sk: `PROFILE#${data.id}`,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
631
|
+
## 📚 API Reference
|
|
264
632
|
|
|
265
|
-
|
|
266
|
-
return "DINOSAUR";
|
|
267
|
-
}
|
|
633
|
+
### Condition Operators
|
|
268
634
|
|
|
269
|
-
|
|
270
|
-
async findByDiet(diet: string) {
|
|
271
|
-
return this.scan()
|
|
272
|
-
.whereEquals("diet", diet)
|
|
273
|
-
.execute();
|
|
274
|
-
}
|
|
635
|
+
All condition operators are type-safe and will validate against your item type. For detailed information about DynamoDB conditions and expressions, see the [AWS DynamoDB Developer Guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html).
|
|
275
636
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
637
|
+
#### Comparison Operators
|
|
638
|
+
- `eq(attr, value)` - Equals (=)
|
|
639
|
+
- `ne(attr, value)` - Not equals (≠)
|
|
640
|
+
- `lt(attr, value)` - Less than (<)
|
|
641
|
+
- `lte(attr, value)` - Less than or equal to (≤)
|
|
642
|
+
- `gt(attr, value)` - Greater than (>)
|
|
643
|
+
- `gte(attr, value)` - Greater than or equal to (≥)
|
|
644
|
+
- `between(attr, lower, upper)` - Between two values (inclusive)
|
|
645
|
+
- `beginsWith(attr, value)` - Checks if string begins with value
|
|
646
|
+
- `contains(attr, value)` - Checks if string/set contains value
|
|
283
647
|
|
|
284
|
-
|
|
648
|
+
```ts
|
|
649
|
+
// Example: Health and feeding monitoring
|
|
650
|
+
await dinoTable
|
|
651
|
+
.query<Dinosaur>({
|
|
652
|
+
pk: "ENCLOSURE#G"
|
|
653
|
+
})
|
|
654
|
+
.filter((op) => op.and(
|
|
655
|
+
op.lt("stats.health", 85), // Health below 85%
|
|
656
|
+
op.lt("care.feeding.lastFed", new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString()), // Not fed in 12 hours
|
|
657
|
+
op.between("stats.weight", 1000, 5000) // Medium-sized dinosaurs
|
|
658
|
+
))
|
|
659
|
+
.execute();
|
|
660
|
+
```
|
|
285
661
|
|
|
286
|
-
|
|
662
|
+
#### Attribute Operators
|
|
663
|
+
- `attributeExists(attr)` - Checks if attribute exists
|
|
664
|
+
- `attributeNotExists(attr)` - Checks if attribute does not exist
|
|
287
665
|
|
|
288
666
|
```ts
|
|
289
|
-
|
|
667
|
+
// Example: Validate required attributes for dinosaur transfer
|
|
668
|
+
await dinoTable
|
|
669
|
+
.update<Dinosaur>({
|
|
670
|
+
pk: "ENCLOSURE#H",
|
|
671
|
+
sk: "DINO#008"
|
|
672
|
+
})
|
|
673
|
+
.set("habitat.enclosure.id", "ENCLOSURE#J")
|
|
674
|
+
.condition((op) => op.and(
|
|
675
|
+
// Ensure all required health data is present
|
|
676
|
+
op.attributeExists("stats.health"),
|
|
677
|
+
op.attributeExists("care.medical.lastCheckup"),
|
|
678
|
+
// Ensure not already in transfer
|
|
679
|
+
op.attributeNotExists("transfer.inProgress"),
|
|
680
|
+
// Verify required monitoring tags
|
|
681
|
+
op.attributeExists("care.medical.vaccinations")
|
|
682
|
+
))
|
|
683
|
+
.execute();
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
#### Logical Operators
|
|
687
|
+
- `and(...conditions)` - Combines conditions with AND
|
|
688
|
+
- `or(...conditions)` - Combines conditions with OR
|
|
689
|
+
- `not(condition)` - Negates a condition
|
|
290
690
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
691
|
+
```ts
|
|
692
|
+
// Example: Complex safety monitoring conditions
|
|
693
|
+
await dinoTable
|
|
694
|
+
.query<Dinosaur>({
|
|
695
|
+
pk: "MONITORING#ALERTS"
|
|
696
|
+
})
|
|
697
|
+
.filter((op) => op.or(
|
|
698
|
+
// Alert: Aggressive carnivores with low health
|
|
699
|
+
op.and(
|
|
700
|
+
op.eq("care.feeding.diet", "Carnivore"),
|
|
701
|
+
op.lt("stats.health", 70),
|
|
702
|
+
op.contains("behavior", "aggressive")
|
|
703
|
+
),
|
|
704
|
+
// Alert: Any dinosaur not fed recently and showing stress
|
|
705
|
+
op.and(
|
|
706
|
+
op.lt("care.feeding.lastFed", new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString()),
|
|
707
|
+
op.contains("behavior", "stressed")
|
|
708
|
+
),
|
|
709
|
+
// Alert: Enclosure climate issues
|
|
710
|
+
op.and(
|
|
711
|
+
op.not(op.eq("habitat.enclosure.climate", "Optimal")),
|
|
712
|
+
op.or(
|
|
713
|
+
op.gt("habitat.requirements.temperature", 40),
|
|
714
|
+
op.lt("habitat.requirements.humidity", 50)
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
))
|
|
294
718
|
.execute();
|
|
719
|
+
```
|
|
295
720
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
721
|
+
### Key Condition Operators
|
|
722
|
+
|
|
723
|
+
Special operators for sort key conditions in queries. See [AWS DynamoDB Key Condition Expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions) for more details.
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
// Example: Query recent health checks by enclosure
|
|
727
|
+
const recentHealthChecks = await dinoTable
|
|
728
|
+
.query<Dinosaur>({
|
|
729
|
+
pk: "ENCLOSURE#K",
|
|
730
|
+
sk: (op) => op.beginsWith(`HEALTH#${new Date().toISOString().slice(0, 10)}`) // Today's checks
|
|
731
|
+
})
|
|
301
732
|
.execute();
|
|
302
733
|
|
|
303
|
-
//
|
|
304
|
-
await
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await dinoRepo
|
|
313
|
-
.update({ pk: "SPECIES#trex", sk: "PROFILE#001" })
|
|
314
|
-
.set("diet", "Omnivore")
|
|
734
|
+
// Example: Query dinosaurs by weight range in specific enclosure
|
|
735
|
+
const largeHerbivores = await dinoTable
|
|
736
|
+
.query<Dinosaur>({
|
|
737
|
+
pk: "DIET#herbivore",
|
|
738
|
+
sk: (op) => op.between(
|
|
739
|
+
`WEIGHT#${5000}`, // 5 tons minimum
|
|
740
|
+
`WEIGHT#${15000}` // 15 tons maximum
|
|
741
|
+
)
|
|
742
|
+
})
|
|
315
743
|
.execute();
|
|
316
744
|
|
|
317
|
-
//
|
|
318
|
-
await
|
|
319
|
-
.
|
|
745
|
+
// Example: Find all dinosaurs in quarantine by date range
|
|
746
|
+
const quarantinedDinos = await dinoTable
|
|
747
|
+
.query<Dinosaur>({
|
|
748
|
+
pk: "STATUS#quarantine",
|
|
749
|
+
sk: (op) => op.between(
|
|
750
|
+
`DATE#${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)}`, // Last 7 days
|
|
751
|
+
`DATE#${new Date().toISOString().slice(0, 10)}` // Today
|
|
752
|
+
)
|
|
753
|
+
})
|
|
320
754
|
.execute();
|
|
321
755
|
```
|
|
322
756
|
|
|
323
|
-
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
757
|
+
Available key conditions for dinosaur queries:
|
|
758
|
+
- `eq(value)` - Exact match (e.g., specific enclosure)
|
|
759
|
+
- `lt(value)` - Earlier than date/time
|
|
760
|
+
- `lte(value)` - Up to and including date/time
|
|
761
|
+
- `gt(value)` - Later than date/time
|
|
762
|
+
- `gte(value)` - From date/time onwards
|
|
763
|
+
- `between(lower, upper)` - Range (e.g., weight range, date range)
|
|
764
|
+
- `beginsWith(value)` - Prefix match (e.g., all health checks today)
|
|
328
765
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
## Contributing 🤝
|
|
332
|
-
```bash
|
|
333
|
-
# Installing the dependencies
|
|
334
|
-
pnpm i
|
|
766
|
+
## 🔮 Future Roadmap
|
|
335
767
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
768
|
+
- [ ] Enhanced query plan visualization
|
|
769
|
+
- [ ] Migration tooling
|
|
770
|
+
- [ ] Local secondary index support
|
|
771
|
+
- [ ] Multi-table transaction support
|
|
339
772
|
|
|
340
|
-
|
|
773
|
+
## 🤝 Contributing
|
|
341
774
|
|
|
342
775
|
```bash
|
|
343
|
-
|
|
344
|
-
|
|
776
|
+
# Set up development environment
|
|
777
|
+
pnpm install
|
|
778
|
+
|
|
779
|
+
# Run tests (requires local DynamoDB)
|
|
780
|
+
pnpm run ddb:start
|
|
781
|
+
pnpm test
|
|
782
|
+
|
|
783
|
+
# Build the project
|
|
784
|
+
pnpm build
|
|
785
|
+
```
|