dyno-table 0.0.2 → 0.1.3

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