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.
Files changed (5) hide show
  1. package/README.md +785 -0
  2. package/dist/index.d.ts +2869 -274
  3. package/dist/index.js +3145 -939
  4. package/package.json +21 -26
  5. package/readme.md +0 -132
package/README.md ADDED
@@ -0,0 +1,785 @@
1
+ # 🦖 dyno-table [![npm version](https://img.shields.io/npm/v/dyno-table.svg?style=flat-square)](https://www.npmjs.com/package/dyno-table) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](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
+ ```