dyno-table 0.0.2 → 0.1.2

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