dyno-table 0.0.1 → 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 +785 -0
- package/dist/index.d.ts +2869 -274
- package/dist/index.js +3145 -939
- package/package.json +21 -26
- package/readme.md +0 -132
package/README.md
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
# 🦖 dyno-table [](https://www.npmjs.com/package/dyno-table) [](https://opensource.org/licenses/MIT)
|
|
2
|
+
|
|
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*
|
|
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;">
|
|
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
|
+
```
|
|
20
|
+
|
|
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)
|
|
48
|
+
|
|
49
|
+
## 📦 Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install dyno-table
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
*Note: Requires AWS SDK v3 as peer dependency*
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 🚀 Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Configure Your Table
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
67
|
+
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
68
|
+
import { Table } from "dyno-table";
|
|
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
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Perform Type-Safe Operations
|
|
92
|
+
|
|
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
|
+
```
|
|
107
|
+
|
|
108
|
+
**🔍 Query with conditions**
|
|
109
|
+
```ts
|
|
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)
|
|
120
|
+
.execute();
|
|
121
|
+
```
|
|
122
|
+
|
|
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
|
+
```
|
|
136
|
+
|
|
137
|
+
## 🧩 Advanced Features
|
|
138
|
+
|
|
139
|
+
### Transactional Operations
|
|
140
|
+
|
|
141
|
+
**Safe dinosaur transfer between enclosures**
|
|
142
|
+
```ts
|
|
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
|
+
```
|
|
229
|
+
|
|
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
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Batch Processing
|
|
240
|
+
|
|
241
|
+
**Efficient dinosaur park management with bulk operations**
|
|
242
|
+
```ts
|
|
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
|
+
];
|
|
249
|
+
|
|
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
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Pagination Made Simple
|
|
316
|
+
|
|
317
|
+
**Efficient dinosaur record browsing**
|
|
318
|
+
```ts
|
|
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
|
+
}
|
|
339
|
+
|
|
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(", ")}`);
|
|
365
|
+
```
|
|
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
|
+
|
|
396
|
+
### Query Operations
|
|
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:
|
|
405
|
+
```ts
|
|
406
|
+
// Sort order
|
|
407
|
+
const ascending = await table
|
|
408
|
+
.query({ pk: "USER#123" })
|
|
409
|
+
.sortAscending()
|
|
410
|
+
.execute();
|
|
411
|
+
|
|
412
|
+
const descending = await table
|
|
413
|
+
.query({ pk: "USER#123" })
|
|
414
|
+
.sortDescending()
|
|
415
|
+
.execute();
|
|
416
|
+
|
|
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();
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Update Operations
|
|
431
|
+
|
|
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.
|
|
451
|
+
|
|
452
|
+
#### Multiple Operations
|
|
453
|
+
Operations can be combined in a single update:
|
|
454
|
+
```ts
|
|
455
|
+
const result = await table
|
|
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"))
|
|
461
|
+
.execute();
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## 🔄 Type Safety Features
|
|
465
|
+
|
|
466
|
+
The library provides comprehensive type safety for all operations:
|
|
467
|
+
|
|
468
|
+
### Nested Object Support
|
|
469
|
+
```ts
|
|
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
|
+
}
|
|
504
|
+
|
|
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
|
+
```
|
|
513
|
+
|
|
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
|
+
}
|
|
524
|
+
|
|
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();
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## 🔄 Batch Operations
|
|
539
|
+
|
|
540
|
+
The library supports efficient batch operations for both reading and writing multiple items:
|
|
541
|
+
|
|
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
|
+
]);
|
|
548
|
+
```
|
|
549
|
+
|
|
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
|
+
```
|
|
557
|
+
|
|
558
|
+
## 🔒 Transaction Operations
|
|
559
|
+
|
|
560
|
+
Perform multiple operations atomically with transaction support:
|
|
561
|
+
|
|
562
|
+
### Transaction Builder
|
|
563
|
+
```ts
|
|
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"));
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Transaction Options
|
|
594
|
+
```ts
|
|
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
|
+
```
|
|
606
|
+
|
|
607
|
+
## 🏗️ Entity Pattern Best Practices (Coming Soon TM)
|
|
608
|
+
|
|
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
|
|
618
|
+
```
|
|
619
|
+
|
|
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
|
|
625
|
+
|
|
626
|
+
## 🚨 Error Handling
|
|
627
|
+
|
|
628
|
+
**TODO:**
|
|
629
|
+
to provide a more clear set of error classes and additional information to allow for an easier debugging experience
|
|
630
|
+
|
|
631
|
+
## 📚 API Reference
|
|
632
|
+
|
|
633
|
+
### Condition Operators
|
|
634
|
+
|
|
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).
|
|
636
|
+
|
|
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
|
|
647
|
+
|
|
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
|
+
```
|
|
661
|
+
|
|
662
|
+
#### Attribute Operators
|
|
663
|
+
- `attributeExists(attr)` - Checks if attribute exists
|
|
664
|
+
- `attributeNotExists(attr)` - Checks if attribute does not exist
|
|
665
|
+
|
|
666
|
+
```ts
|
|
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
|
|
690
|
+
|
|
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
|
+
))
|
|
718
|
+
.execute();
|
|
719
|
+
```
|
|
720
|
+
|
|
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
|
+
})
|
|
732
|
+
.execute();
|
|
733
|
+
|
|
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
|
+
})
|
|
743
|
+
.execute();
|
|
744
|
+
|
|
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
|
+
})
|
|
754
|
+
.execute();
|
|
755
|
+
```
|
|
756
|
+
|
|
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)
|
|
765
|
+
|
|
766
|
+
## 🔮 Future Roadmap
|
|
767
|
+
|
|
768
|
+
- [ ] Enhanced query plan visualization
|
|
769
|
+
- [ ] Migration tooling
|
|
770
|
+
- [ ] Local secondary index support
|
|
771
|
+
- [ ] Multi-table transaction support
|
|
772
|
+
|
|
773
|
+
## 🤝 Contributing
|
|
774
|
+
|
|
775
|
+
```bash
|
|
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
|
+
```
|