dyno-table 0.1.7 → 0.2.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +681 -160
- package/dist/builder-types-C_PDZhnP.d.ts +118 -0
- package/dist/builder-types-DtwbqMeF.d.cts +118 -0
- package/dist/builders/condition-check-builder.cjs +394 -0
- package/dist/builders/condition-check-builder.cjs.map +1 -0
- package/dist/builders/condition-check-builder.d.cts +157 -0
- package/dist/builders/condition-check-builder.d.ts +157 -0
- package/dist/builders/condition-check-builder.js +392 -0
- package/dist/builders/condition-check-builder.js.map +1 -0
- package/dist/builders/delete-builder.cjs +405 -0
- package/dist/builders/delete-builder.cjs.map +1 -0
- package/dist/builders/delete-builder.d.cts +166 -0
- package/dist/builders/delete-builder.d.ts +166 -0
- package/dist/builders/delete-builder.js +403 -0
- package/dist/builders/delete-builder.js.map +1 -0
- package/dist/builders/paginator.cjs +199 -0
- package/dist/builders/paginator.cjs.map +1 -0
- package/dist/builders/paginator.d.cts +179 -0
- package/dist/builders/paginator.d.ts +179 -0
- package/dist/builders/paginator.js +197 -0
- package/dist/builders/paginator.js.map +1 -0
- package/dist/builders/put-builder.cjs +476 -0
- package/dist/builders/put-builder.cjs.map +1 -0
- package/dist/builders/put-builder.d.cts +274 -0
- package/dist/builders/put-builder.d.ts +274 -0
- package/dist/builders/put-builder.js +474 -0
- package/dist/builders/put-builder.js.map +1 -0
- package/dist/builders/query-builder.cjs +674 -0
- package/dist/builders/query-builder.cjs.map +1 -0
- package/dist/builders/query-builder.d.cts +6 -0
- package/dist/builders/query-builder.d.ts +6 -0
- package/dist/builders/query-builder.js +672 -0
- package/dist/builders/query-builder.js.map +1 -0
- package/dist/builders/transaction-builder.cjs +894 -0
- package/dist/builders/transaction-builder.cjs.map +1 -0
- package/dist/builders/transaction-builder.d.cts +511 -0
- package/dist/builders/transaction-builder.d.ts +511 -0
- package/dist/builders/transaction-builder.js +892 -0
- package/dist/builders/transaction-builder.js.map +1 -0
- package/dist/builders/update-builder.cjs +627 -0
- package/dist/builders/update-builder.cjs.map +1 -0
- package/dist/builders/update-builder.d.cts +365 -0
- package/dist/builders/update-builder.d.ts +365 -0
- package/dist/builders/update-builder.js +625 -0
- package/dist/builders/update-builder.js.map +1 -0
- package/dist/conditions--ld9a78i.d.ts +331 -0
- package/dist/conditions-ChhQWd6z.d.cts +331 -0
- package/dist/conditions.cjs +59 -0
- package/dist/conditions.cjs.map +1 -0
- package/dist/conditions.d.cts +3 -0
- package/dist/conditions.d.ts +3 -0
- package/dist/conditions.js +43 -0
- package/dist/conditions.js.map +1 -0
- package/dist/entity.cjs +228 -0
- package/dist/entity.cjs.map +1 -0
- package/dist/entity.d.cts +149 -0
- package/dist/entity.d.ts +149 -0
- package/dist/entity.js +224 -0
- package/dist/entity.js.map +1 -0
- package/dist/query-builder-Csror9Iu.d.ts +507 -0
- package/dist/query-builder-D2FM9rsu.d.cts +507 -0
- package/dist/standard-schema.cjs +4 -0
- package/dist/standard-schema.cjs.map +1 -0
- package/dist/standard-schema.d.cts +57 -0
- package/dist/standard-schema.d.ts +57 -0
- package/dist/standard-schema.js +3 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/table-BEhBPy2G.d.cts +364 -0
- package/dist/table-BW3cmUqr.d.ts +364 -0
- package/dist/{index.js → table.cjs} +330 -354
- package/dist/table.cjs.map +1 -0
- package/dist/table.d.cts +12 -0
- package/dist/table.d.ts +12 -0
- package/dist/table.js +3243 -0
- package/dist/table.js.map +1 -0
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +22 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/partition-key-template.cjs +19 -0
- package/dist/utils/partition-key-template.cjs.map +1 -0
- package/dist/utils/partition-key-template.d.cts +32 -0
- package/dist/utils/partition-key-template.d.ts +32 -0
- package/dist/utils/partition-key-template.js +17 -0
- package/dist/utils/partition-key-template.js.map +1 -0
- package/dist/utils/sort-key-template.cjs +19 -0
- package/dist/utils/sort-key-template.cjs.map +1 -0
- package/dist/utils/sort-key-template.d.cts +35 -0
- package/dist/utils/sort-key-template.d.ts +35 -0
- package/dist/utils/sort-key-template.js +17 -0
- package/dist/utils/sort-key-template.js.map +1 -0
- package/package.json +97 -22
- package/dist/index.d.ts +0 -2923
package/README.md
CHANGED
|
@@ -6,45 +6,77 @@
|
|
|
6
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
7
|
|
|
8
8
|
```ts
|
|
9
|
-
// Type-safe
|
|
10
|
-
await
|
|
9
|
+
// Type-safe dinosaur tracking operations made simple
|
|
10
|
+
await dinoTable
|
|
11
11
|
.update<Dinosaur>({
|
|
12
12
|
pk: 'SPECIES#trex',
|
|
13
13
|
sk: 'PROFILE#001'
|
|
14
14
|
})
|
|
15
|
-
.set('diet', 'Carnivore')
|
|
16
|
-
.add('sightings', 1)
|
|
17
|
-
.condition(op => op.eq('status', 'ACTIVE'))
|
|
15
|
+
.set('diet', 'Carnivore') // Update dietary classification
|
|
16
|
+
.add('sightings', 1) // Increment sighting counter
|
|
17
|
+
.condition(op => op.eq('status', 'ACTIVE')) // Only if dinosaur is active
|
|
18
18
|
.execute();
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
> This README provides a concise overview of dyno-table's features with dinosaur-themed examples. For detailed documentation on specific features, please refer to the individual markdown files in the `docs/` directory.
|
|
22
|
+
|
|
23
|
+
## 🌟 Why dyno-table for your Dinosaur Data?
|
|
22
24
|
|
|
23
|
-
-
|
|
24
|
-
- **🛡️
|
|
25
|
-
- **⚡
|
|
26
|
-
- **🔒
|
|
27
|
-
- **📈
|
|
25
|
+
- **🦕 Dinosaur-sized data made manageable** - Clean abstraction layer for complex DynamoDB patterns
|
|
26
|
+
- **🛡️ Extinction-proof type safety** - Full TypeScript support with strict type checking
|
|
27
|
+
- **⚡ Velociraptor-fast API** - Chainable builder pattern for complex operations
|
|
28
|
+
- **🔒 T-Rex-proof transactional safety** - ACID-compliant operations with easy-to-use transactions
|
|
29
|
+
- **📈 Jurassic-scale performance** - Automatic batch chunking and pagination handling
|
|
28
30
|
|
|
29
31
|
## 📑 Table of Contents
|
|
30
32
|
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
|
|
37
|
-
- [Type
|
|
38
|
-
- [
|
|
39
|
-
|
|
40
|
-
- [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
33
|
+
- [🦖 dyno-table ](#-dyno-table--)
|
|
34
|
+
- [🌟 Why dyno-table for your Dinosaur Data?](#-why-dyno-table-for-your-dinosaur-data)
|
|
35
|
+
- [📑 Table of Contents](#-table-of-contents)
|
|
36
|
+
- [📦 Installation](#-installation)
|
|
37
|
+
- [🚀 Quick Start](#-quick-start)
|
|
38
|
+
- [1. Configure Your Jurassic Table](#1-configure-your-jurassic-table)
|
|
39
|
+
- [2. Perform Type-Safe Dinosaur Operations](#2-perform-type-safe-dinosaur-operations)
|
|
40
|
+
- [🏗️ Entity Pattern](#️-entity-pattern-with-standard-schema-validators)
|
|
41
|
+
- [Defining Entities](#defining-entities)
|
|
42
|
+
- [Entity Features](#entity-features)
|
|
43
|
+
- [1. Schema Validation](#1-schema-validation)
|
|
44
|
+
- [2. CRUD Operations](#2-crud-operations)
|
|
45
|
+
- [3. Custom Queries](#3-custom-queries)
|
|
46
|
+
- [4. Indexes for Efficient Querying](#4-indexes-for-efficient-querying)
|
|
47
|
+
- [5. Lifecycle Hooks](#5-lifecycle-hooks)
|
|
48
|
+
- [Complete Entity Example](#complete-entity-example)
|
|
49
|
+
- [🧩 Advanced Features](#-advanced-features)
|
|
50
|
+
- [Transactional Operations](#transactional-operations)
|
|
51
|
+
- [Batch Processing](#batch-processing)
|
|
52
|
+
- [Pagination Made Simple](#pagination-made-simple)
|
|
53
|
+
- [🛡️ Type-Safe Query Building](#️-type-safe-query-building)
|
|
54
|
+
- [Comparison Operators](#comparison-operators)
|
|
55
|
+
- [Logical Operators](#logical-operators)
|
|
56
|
+
- [Query Operations](#query-operations)
|
|
57
|
+
- [Put Operations](#put-operations)
|
|
58
|
+
- [Update Operations](#update-operations)
|
|
59
|
+
- [Condition Operators](#condition-operators)
|
|
60
|
+
- [Multiple Operations](#multiple-operations)
|
|
61
|
+
- [🔄 Type Safety Features](#-type-safety-features)
|
|
62
|
+
- [Nested Object Support](#nested-object-support)
|
|
63
|
+
- [Type-Safe Conditions](#type-safe-conditions)
|
|
64
|
+
- [🔄 Batch Operations](#-batch-operations)
|
|
65
|
+
- [Batch Get](#batch-get)
|
|
66
|
+
- [Batch Write](#batch-write)
|
|
67
|
+
- [🔒 Transaction Operations](#-transaction-operations)
|
|
68
|
+
- [Transaction Builder](#transaction-builder)
|
|
69
|
+
- [Transaction Options](#transaction-options)
|
|
70
|
+
- [🚨 Error Handling](#-error-handling)
|
|
71
|
+
- [📚 API Reference](#-api-reference)
|
|
72
|
+
- [Condition Operators](#condition-operators-1)
|
|
73
|
+
- [Comparison Operators](#comparison-operators-1)
|
|
74
|
+
- [Attribute Operators](#attribute-operators)
|
|
75
|
+
- [Logical Operators](#logical-operators-1)
|
|
76
|
+
- [Key Condition Operators](#key-condition-operators)
|
|
77
|
+
- [🔮 Future Roadmap](#-future-roadmap)
|
|
78
|
+
- [🤝 Contributing](#-contributing)
|
|
79
|
+
- [🦔 Running Examples](#-running-examples)
|
|
48
80
|
|
|
49
81
|
## 📦 Installation
|
|
50
82
|
|
|
@@ -60,25 +92,26 @@ npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
|
60
92
|
|
|
61
93
|
## 🚀 Quick Start
|
|
62
94
|
|
|
63
|
-
### 1. Configure Your Table
|
|
95
|
+
### 1. Configure Your Jurassic Table
|
|
64
96
|
|
|
65
97
|
```ts
|
|
66
98
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
67
99
|
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
68
|
-
import { Table } from "dyno-table";
|
|
100
|
+
import { Table } from "dyno-table/table";
|
|
69
101
|
|
|
70
|
-
// Configure AWS SDK clients
|
|
102
|
+
// Configure AWS SDK clients - your gateway to the prehistoric database
|
|
71
103
|
const client = new DynamoDBClient({ region: "us-west-2" });
|
|
72
104
|
const docClient = DynamoDBDocument.from(client);
|
|
73
105
|
|
|
74
|
-
// Initialize table with single-table design schema
|
|
106
|
+
// Initialize table with single-table design schema - your dinosaur park database
|
|
75
107
|
const dinoTable = new Table({
|
|
76
108
|
client: docClient,
|
|
77
|
-
tableName: "
|
|
109
|
+
tableName: "JurassicPark", // Your central dinosaur tracking system
|
|
78
110
|
indexes: {
|
|
79
|
-
partitionKey: "pk",
|
|
80
|
-
sortKey: "sk",
|
|
111
|
+
partitionKey: "pk", // Primary partition key for fast dinosaur lookups
|
|
112
|
+
sortKey: "sk", // Sort key for organizing dinosaur data
|
|
81
113
|
gsis: {
|
|
114
|
+
// Global Secondary Index for querying dinosaurs by species
|
|
82
115
|
speciesId: {
|
|
83
116
|
partitionKey: "gsi1pk",
|
|
84
117
|
sortKey: "gsi1sk",
|
|
@@ -88,141 +121,538 @@ const dinoTable = new Table({
|
|
|
88
121
|
});
|
|
89
122
|
```
|
|
90
123
|
|
|
91
|
-
### 2. Perform Type-Safe Operations
|
|
124
|
+
### 2. Perform Type-Safe Dinosaur Operations
|
|
92
125
|
|
|
93
|
-
**🦖 Creating a new dinosaur**
|
|
126
|
+
**🦖 Creating a new dinosaur specimen**
|
|
94
127
|
```ts
|
|
128
|
+
// Add a new T-Rex to your collection with complete type safety
|
|
95
129
|
const rex = await dinoTable
|
|
96
130
|
.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
|
|
131
|
+
pk: "SPECIES#trex", // Partition key identifies the species
|
|
132
|
+
sk: "PROFILE#trex", // Sort key for the specific profile
|
|
133
|
+
speciesId: "trex", // For GSI queries
|
|
134
|
+
name: "Tyrannosaurus Rex", // Display name
|
|
135
|
+
diet: "carnivore", // Dietary classification
|
|
136
|
+
length: 12.3, // Size in meters
|
|
137
|
+
discoveryYear: 1902 // When first discovered
|
|
104
138
|
})
|
|
105
139
|
.execute();
|
|
106
140
|
```
|
|
107
141
|
|
|
108
|
-
**🔍 Query with conditions**
|
|
142
|
+
**🔍 Query for specific dinosaurs with conditions**
|
|
109
143
|
```ts
|
|
144
|
+
// Find large carnivorous dinosaurs in the T-Rex species
|
|
110
145
|
const largeDinos = await dinoTable
|
|
111
146
|
.query<Dinosaur>({
|
|
112
|
-
pk: "SPECIES#trex",
|
|
113
|
-
sk: (op) => op.beginsWith("PROFILE#")
|
|
147
|
+
pk: "SPECIES#trex", // Target the T-Rex species
|
|
148
|
+
sk: (op) => op.beginsWith("PROFILE#") // Look in profile records
|
|
114
149
|
})
|
|
115
150
|
.filter((op) => op.and(
|
|
116
|
-
op.gte("length", 10),
|
|
117
|
-
op.eq("diet", "carnivore")
|
|
151
|
+
op.gte("length", 10), // Only dinosaurs longer than 10 meters
|
|
152
|
+
op.eq("diet", "carnivore") // Must be carnivores
|
|
118
153
|
))
|
|
119
|
-
.limit(10)
|
|
154
|
+
.limit(10) // Limit to 10 results
|
|
120
155
|
.execute();
|
|
121
156
|
```
|
|
122
157
|
|
|
123
|
-
**🔄
|
|
158
|
+
**🔄 Update dinosaur classification**
|
|
124
159
|
```ts
|
|
160
|
+
// Update a dinosaur's diet classification based on new research
|
|
125
161
|
await dinoTable
|
|
126
162
|
.update<Dinosaur>({
|
|
127
|
-
pk: "SPECIES#trex",
|
|
128
|
-
sk: "PROFILE#trex"
|
|
163
|
+
pk: "SPECIES#trex", // Target the T-Rex species
|
|
164
|
+
sk: "PROFILE#trex" // Specific profile to update
|
|
129
165
|
})
|
|
130
|
-
.set("diet", "omnivore")
|
|
131
|
-
.add("discoveryYear", 1)
|
|
132
|
-
.remove("outdatedField")
|
|
133
|
-
.condition((op) => op.attributeExists("discoverySite"))
|
|
166
|
+
.set("diet", "omnivore") // New diet classification based on fossil evidence
|
|
167
|
+
.add("discoveryYear", 1) // Adjust discovery year with new findings
|
|
168
|
+
.remove("outdatedField") // Remove deprecated information
|
|
169
|
+
.condition((op) => op.attributeExists("discoverySite")) // Only if discovery site is documented
|
|
134
170
|
.execute();
|
|
135
171
|
```
|
|
136
172
|
|
|
173
|
+
## 🏗️ Entity Pattern with Standard Schema validators
|
|
174
|
+
|
|
175
|
+
The entity pattern provides a structured, type-safe way to work with DynamoDB items.
|
|
176
|
+
It combines schema validation, key management, and repository operations into a cohesive abstraction.
|
|
177
|
+
|
|
178
|
+
✨ This library supports all standard schema validation libraries, including **zod**, **arktype**, and **valibot**,
|
|
179
|
+
allowing you to choose your preferred validation tool!
|
|
180
|
+
|
|
181
|
+
### Defining Entities
|
|
182
|
+
|
|
183
|
+
Entities are defined using the `defineEntity` function, which takes a configuration object that includes a schema, primary key definition, and optional indexes and queries.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { z } from "zod";
|
|
187
|
+
import { defineEntity, createIndex } from "dyno-table/entity";
|
|
188
|
+
|
|
189
|
+
// Define your schema using Zod
|
|
190
|
+
const dinosaurSchema = z.object({
|
|
191
|
+
id: z.string(),
|
|
192
|
+
species: z.string(),
|
|
193
|
+
name: z.string(),
|
|
194
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
195
|
+
dangerLevel: z.number().int().min(1).max(10),
|
|
196
|
+
height: z.number().positive(),
|
|
197
|
+
weight: z.number().positive(),
|
|
198
|
+
status: z.enum(["active", "inactive", "sick", "deceased"]),
|
|
199
|
+
createdAt: z.string().optional(),
|
|
200
|
+
updatedAt: z.string().optional(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Infer the type from the schema
|
|
204
|
+
type Dinosaur = z.infer<typeof dinosaurSchema>;
|
|
205
|
+
|
|
206
|
+
// Define key templates for Dinosaur entity
|
|
207
|
+
const dinosaurPK = partitionKey`ENTITY#DINOSAUR#DIET#${"diet"}`;
|
|
208
|
+
const dinosaurSK = sortKey`ID#${"id"}#SPECIES#${"species"}`;
|
|
209
|
+
|
|
210
|
+
// Create a primary index for Dinosaur entity
|
|
211
|
+
const primaryKey = createIndex()
|
|
212
|
+
.input(z.object({ id: z.string(), diet: z.string(), species: z.string() }))
|
|
213
|
+
.partitionKey(({ diet }) => dinosaurPK({ diet }))
|
|
214
|
+
.sortKey(({ id, species }) => dinosaurSK({ species, id }));
|
|
215
|
+
|
|
216
|
+
// Define the entity
|
|
217
|
+
const DinosaurEntity = defineEntity({
|
|
218
|
+
name: "Dinosaur",
|
|
219
|
+
schema: dinosaurSchema,
|
|
220
|
+
primaryKey,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Create a repository
|
|
224
|
+
const dinosaurRepo = DinosaurEntity.createRepository(table);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Entity Features
|
|
228
|
+
|
|
229
|
+
#### 1. Schema Validation
|
|
230
|
+
|
|
231
|
+
Entities use Zod schemas to validate data before operations:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// Define a schema with Zod
|
|
235
|
+
const dinosaurSchema = z.object({
|
|
236
|
+
id: z.string(),
|
|
237
|
+
species: z.string(),
|
|
238
|
+
name: z.string(),
|
|
239
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
240
|
+
dangerLevel: z.number().int().min(1).max(10),
|
|
241
|
+
height: z.number().positive(),
|
|
242
|
+
weight: z.number().positive(),
|
|
243
|
+
status: z.enum(["active", "inactive", "sick", "deceased"]),
|
|
244
|
+
tags: z.array(z.string()).optional(),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Create an entity with the schema
|
|
248
|
+
const DinosaurEntity = defineEntity({
|
|
249
|
+
name: "Dinosaur",
|
|
250
|
+
schema: dinosaurSchema,
|
|
251
|
+
primaryKey: createIndex()
|
|
252
|
+
.input(z.object({ id: z.string(), diet: z.string(), species: z.string() }))
|
|
253
|
+
.partitionKey(({ diet }) => dinosaurPK({ diet }))
|
|
254
|
+
.sortKey(({ id, species }) => dinosaurSK({ species, id }))
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### 2. CRUD Operations
|
|
259
|
+
|
|
260
|
+
Entities provide type-safe CRUD operations:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
// Create a new dinosaur
|
|
264
|
+
await dinosaurRepo.create({
|
|
265
|
+
id: "dino-001",
|
|
266
|
+
species: "Tyrannosaurus Rex",
|
|
267
|
+
name: "Rexy",
|
|
268
|
+
diet: "carnivore",
|
|
269
|
+
dangerLevel: 10,
|
|
270
|
+
height: 5.2,
|
|
271
|
+
weight: 7000,
|
|
272
|
+
status: "active",
|
|
273
|
+
}).execute();
|
|
274
|
+
|
|
275
|
+
// Get a dinosaur
|
|
276
|
+
const dino = await dinosaurRepo.get({
|
|
277
|
+
id: "dino-001",
|
|
278
|
+
diet: "carnivore",
|
|
279
|
+
species: "Tyrannosaurus Rex",
|
|
280
|
+
}).execute();
|
|
281
|
+
|
|
282
|
+
// Update a dinosaur
|
|
283
|
+
await dinosaurRepo.update(
|
|
284
|
+
{ id: "dino-001", diet: "carnivore", species: "Tyrannosaurus Rex" },
|
|
285
|
+
{ weight: 7200, status: "sick" }
|
|
286
|
+
).execute();
|
|
287
|
+
|
|
288
|
+
// Delete a dinosaur
|
|
289
|
+
await dinosaurRepo.delete({
|
|
290
|
+
id: "dino-001",
|
|
291
|
+
diet: "carnivore",
|
|
292
|
+
species: "Tyrannosaurus Rex",
|
|
293
|
+
}).execute();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### 3. Custom Queries
|
|
297
|
+
|
|
298
|
+
Define custom queries with input validation:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { createQueries } from "dyno-table/entity";
|
|
302
|
+
|
|
303
|
+
const createQuery = createQueries<Dinosaur>();
|
|
304
|
+
|
|
305
|
+
const DinosaurEntity = defineEntity({
|
|
306
|
+
name: "Dinosaur",
|
|
307
|
+
schema: dinosaurSchema,
|
|
308
|
+
primaryKey,
|
|
309
|
+
queries: {
|
|
310
|
+
byDiet: createQuery
|
|
311
|
+
.input(
|
|
312
|
+
z.object({
|
|
313
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
314
|
+
})
|
|
315
|
+
)
|
|
316
|
+
.query(({ input, entity }) => {
|
|
317
|
+
return entity
|
|
318
|
+
.scan()
|
|
319
|
+
.filter((op) => op.eq("diet", input.diet));
|
|
320
|
+
}),
|
|
321
|
+
|
|
322
|
+
bySpecies: createQuery
|
|
323
|
+
.input(
|
|
324
|
+
z.object({
|
|
325
|
+
species: z.string(),
|
|
326
|
+
})
|
|
327
|
+
)
|
|
328
|
+
.query(({ input, entity }) => {
|
|
329
|
+
return entity
|
|
330
|
+
.scan()
|
|
331
|
+
.filter((op) => op.eq("species", input.species));
|
|
332
|
+
}),
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Use the custom queries
|
|
337
|
+
const carnivores = await dinosaurRepo.query.byDiet({ diet: "carnivore" }).execute();
|
|
338
|
+
const trexes = await dinosaurRepo.query.bySpecies({ species: "Tyrannosaurus Rex" }).execute();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### 4. Indexes for Efficient Querying
|
|
342
|
+
|
|
343
|
+
Define indexes for efficient access patterns:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { createIndex } from "dyno-table/entity";
|
|
347
|
+
|
|
348
|
+
// Define GSI for querying by species
|
|
349
|
+
const speciesIndex = createIndex()
|
|
350
|
+
.input(dinosaurSchema)
|
|
351
|
+
.partitionKey(({ species }) => `SPECIES#${species}`)
|
|
352
|
+
.sortKey(({ id }) => `DINOSAUR#${id}`);
|
|
353
|
+
|
|
354
|
+
const DinosaurEntity = defineEntity({
|
|
355
|
+
name: "Dinosaur",
|
|
356
|
+
schema: dinosaurSchema,
|
|
357
|
+
primaryKey,
|
|
358
|
+
indexes: {
|
|
359
|
+
species: speciesIndex,
|
|
360
|
+
},
|
|
361
|
+
queries: {
|
|
362
|
+
bySpecies: createQuery
|
|
363
|
+
.input(
|
|
364
|
+
z.object({
|
|
365
|
+
species: z.string(),
|
|
366
|
+
})
|
|
367
|
+
)
|
|
368
|
+
.query(({ input, entity }) => {
|
|
369
|
+
return entity
|
|
370
|
+
.queryBuilder({
|
|
371
|
+
pk: `SPECIES#${input.species}`,
|
|
372
|
+
})
|
|
373
|
+
.useIndex("species");
|
|
374
|
+
}),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### 5. Lifecycle Hooks
|
|
380
|
+
|
|
381
|
+
Add hooks for pre/post processing:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const dinosaurHooks = {
|
|
385
|
+
afterGet: async (data: Dinosaur | undefined) => {
|
|
386
|
+
if (data) {
|
|
387
|
+
return {
|
|
388
|
+
...data,
|
|
389
|
+
displayName: `${data.name} (${data.species})`,
|
|
390
|
+
threatLevel: data.dangerLevel > 7 ? "HIGH" : "MODERATE",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return data;
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const DinosaurEntity = defineEntity({
|
|
398
|
+
name: "Dinosaur",
|
|
399
|
+
schema: dinosaurSchema,
|
|
400
|
+
primaryKey,
|
|
401
|
+
hooks: dinosaurHooks,
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Complete Entity Example
|
|
406
|
+
|
|
407
|
+
Here's a complete example using Zod schemas directly:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { z } from "zod";
|
|
411
|
+
import { defineEntity, createQueries, createIndex } from "dyno-table/entity";
|
|
412
|
+
import { Table } from "dyno-table/table";
|
|
413
|
+
|
|
414
|
+
// Define the schema with Zod
|
|
415
|
+
const dinosaurSchema = z.object({
|
|
416
|
+
id: z.string(),
|
|
417
|
+
species: z.string(),
|
|
418
|
+
name: z.string(),
|
|
419
|
+
enclosureId: z.string(),
|
|
420
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
421
|
+
dangerLevel: z.number().int().min(1).max(10),
|
|
422
|
+
height: z.number().positive(),
|
|
423
|
+
weight: z.number().positive(),
|
|
424
|
+
status: z.enum(["active", "inactive", "sick", "deceased"]),
|
|
425
|
+
trackingChipId: z.string().optional(),
|
|
426
|
+
lastFed: z.string().optional(),
|
|
427
|
+
createdAt: z.string().optional(),
|
|
428
|
+
updatedAt: z.string().optional(),
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Infer the type from the schema
|
|
432
|
+
type Dinosaur = z.infer<typeof dinosaurSchema>;
|
|
433
|
+
|
|
434
|
+
// Define key templates
|
|
435
|
+
const dinosaurPK = (id: string) => `DINOSAUR#${id}`;
|
|
436
|
+
const dinosaurSK = (status: string) => `STATUS#${status}`;
|
|
437
|
+
|
|
438
|
+
// Create a primary index
|
|
439
|
+
const primaryKey = createIndex()
|
|
440
|
+
.input(dinosaurSchema)
|
|
441
|
+
.partitionKey(({ id }) => dinosaurPK(id))
|
|
442
|
+
.sortKey(({ status }) => dinosaurSK(status));
|
|
443
|
+
|
|
444
|
+
// Create a GSI for querying by species
|
|
445
|
+
const speciesIndex = createIndex()
|
|
446
|
+
.input(dinosaurSchema)
|
|
447
|
+
.partitionKey(({ species }) => `SPECIES#${species}`)
|
|
448
|
+
.sortKey(({ id }) => `DINOSAUR#${id}`);
|
|
449
|
+
|
|
450
|
+
// Create a GSI for querying by enclosure
|
|
451
|
+
const enclosureIndex = createIndex()
|
|
452
|
+
.input(dinosaurSchema)
|
|
453
|
+
.partitionKey(({ enclosureId }) => `ENCLOSURE#${enclosureId}`)
|
|
454
|
+
.sortKey(({ id }) => `DINOSAUR#${id}`);
|
|
455
|
+
|
|
456
|
+
// Create query builders
|
|
457
|
+
const createQuery = createQueries<Dinosaur>();
|
|
458
|
+
|
|
459
|
+
// Define the entity
|
|
460
|
+
const DinosaurEntity = defineEntity({
|
|
461
|
+
name: "Dinosaur",
|
|
462
|
+
schema: dinosaurSchema,
|
|
463
|
+
primaryKey,
|
|
464
|
+
indexes: {
|
|
465
|
+
species: speciesIndex,
|
|
466
|
+
enclosure: enclosureIndex,
|
|
467
|
+
},
|
|
468
|
+
queries: {
|
|
469
|
+
bySpecies: createQuery
|
|
470
|
+
.input(
|
|
471
|
+
z.object({
|
|
472
|
+
species: z.string(),
|
|
473
|
+
})
|
|
474
|
+
)
|
|
475
|
+
.query(({ input, entity }) => {
|
|
476
|
+
return entity
|
|
477
|
+
.queryBuilder({
|
|
478
|
+
pk: `SPECIES#${input.species}`,
|
|
479
|
+
})
|
|
480
|
+
.useIndex("species");
|
|
481
|
+
}),
|
|
482
|
+
|
|
483
|
+
byEnclosure: createQuery
|
|
484
|
+
.input(
|
|
485
|
+
z.object({
|
|
486
|
+
enclosureId: z.string(),
|
|
487
|
+
})
|
|
488
|
+
)
|
|
489
|
+
.query(({ input, entity }) => {
|
|
490
|
+
return entity
|
|
491
|
+
.queryBuilder({
|
|
492
|
+
pk: `ENCLOSURE#${input.enclosureId}`,
|
|
493
|
+
})
|
|
494
|
+
.useIndex("enclosure");
|
|
495
|
+
}),
|
|
496
|
+
|
|
497
|
+
dangerousInEnclosure: createQuery
|
|
498
|
+
.input(
|
|
499
|
+
z.object({
|
|
500
|
+
enclosureId: z.string(),
|
|
501
|
+
minDangerLevel: z.number().int().min(1).max(10),
|
|
502
|
+
})
|
|
503
|
+
)
|
|
504
|
+
.query(({ input, entity }) => {
|
|
505
|
+
return entity
|
|
506
|
+
.queryBuilder({
|
|
507
|
+
pk: `ENCLOSURE#${input.enclosureId}`,
|
|
508
|
+
})
|
|
509
|
+
.useIndex("enclosure")
|
|
510
|
+
.filter((op) => op.gte("dangerLevel", input.minDangerLevel));
|
|
511
|
+
}),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Create a repository
|
|
516
|
+
const dinosaurRepo = DinosaurEntity.createRepository(table);
|
|
517
|
+
|
|
518
|
+
// Use the repository
|
|
519
|
+
async function main() {
|
|
520
|
+
// Create a dinosaur
|
|
521
|
+
await dinosaurRepo
|
|
522
|
+
.create({
|
|
523
|
+
id: "dino-001",
|
|
524
|
+
species: "Tyrannosaurus Rex",
|
|
525
|
+
name: "Rexy",
|
|
526
|
+
enclosureId: "enc-001",
|
|
527
|
+
diet: "carnivore",
|
|
528
|
+
dangerLevel: 10,
|
|
529
|
+
height: 5.2,
|
|
530
|
+
weight: 7000,
|
|
531
|
+
status: "active",
|
|
532
|
+
trackingChipId: "TRX-001",
|
|
533
|
+
})
|
|
534
|
+
.execute();
|
|
535
|
+
|
|
536
|
+
// Query dinosaurs by species
|
|
537
|
+
const trexes = await dinosaurRepo.query.bySpecies({
|
|
538
|
+
species: "Tyrannosaurus Rex"
|
|
539
|
+
}).execute();
|
|
540
|
+
|
|
541
|
+
// Query dangerous dinosaurs in an enclosure
|
|
542
|
+
const dangerousDinos = await dinosaurRepo.query.dangerousInEnclosure({
|
|
543
|
+
enclosureId: "enc-001",
|
|
544
|
+
minDangerLevel: 8,
|
|
545
|
+
}).execute();
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Key benefits:**
|
|
550
|
+
- 🚫 Prevents accidental cross-type data access
|
|
551
|
+
- 🔍 Automatically filters queries/scans to repository type
|
|
552
|
+
- 🛡️ Ensures consistent key structure across entities
|
|
553
|
+
- 📦 Encapsulates domain-specific query logic
|
|
554
|
+
- 🧪 Validates data with Zod schemas
|
|
555
|
+
- 🔄 Provides type inference from schemas
|
|
556
|
+
|
|
137
557
|
## 🧩 Advanced Features
|
|
138
558
|
|
|
139
559
|
### Transactional Operations
|
|
140
560
|
|
|
141
561
|
**Safe dinosaur transfer between enclosures**
|
|
142
562
|
```ts
|
|
143
|
-
// Start a transaction session for transferring a
|
|
563
|
+
// Start a transaction session for transferring a T-Rex to a new enclosure
|
|
564
|
+
// Critical for safety: All operations must succeed or none will be applied
|
|
144
565
|
await dinoTable.transaction(async (tx) => {
|
|
145
566
|
// All operations are executed as a single transaction (up to 100 operations)
|
|
567
|
+
// This ensures the dinosaur transfer is atomic - preventing half-completed transfers
|
|
146
568
|
|
|
147
|
-
// Check if destination enclosure is ready and compatible
|
|
569
|
+
// STEP 1: Check if destination enclosure is ready and compatible with the dinosaur
|
|
570
|
+
// We must verify the enclosure is prepared and suitable for a carnivore
|
|
148
571
|
await dinoTable
|
|
149
572
|
.conditionCheck({
|
|
150
|
-
pk: "ENCLOSURE#B",
|
|
151
|
-
sk: "STATUS"
|
|
573
|
+
pk: "ENCLOSURE#B", // Target enclosure B
|
|
574
|
+
sk: "STATUS" // Check the enclosure status record
|
|
152
575
|
})
|
|
153
576
|
.condition(op => op.and(
|
|
154
|
-
op.eq("status", "READY"),
|
|
155
|
-
op.eq("diet", "Carnivore") //
|
|
577
|
+
op.eq("status", "READY"), // Enclosure must be in READY state
|
|
578
|
+
op.eq("diet", "Carnivore") // Must support carnivorous dinosaurs
|
|
156
579
|
))
|
|
157
580
|
.withTransaction(tx);
|
|
158
581
|
|
|
159
|
-
// Remove dinosaur from current enclosure
|
|
582
|
+
// STEP 2: Remove dinosaur from current enclosure
|
|
583
|
+
// Only proceed if the dinosaur is healthy enough for transfer
|
|
160
584
|
await dinoTable
|
|
161
585
|
.delete<Dinosaur>({
|
|
162
|
-
pk: "ENCLOSURE#A",
|
|
163
|
-
sk: "DINO#001"
|
|
586
|
+
pk: "ENCLOSURE#A", // Source enclosure A
|
|
587
|
+
sk: "DINO#001" // T-Rex with ID 001
|
|
164
588
|
})
|
|
165
589
|
.condition(op => op.and(
|
|
166
|
-
op.eq("status", "HEALTHY"),
|
|
167
|
-
op.gte("health", 80)
|
|
590
|
+
op.eq("status", "HEALTHY"), // Dinosaur must be in HEALTHY state
|
|
591
|
+
op.gte("health", 80) // Health must be at least 80%
|
|
168
592
|
))
|
|
169
593
|
.withTransaction(tx);
|
|
170
594
|
|
|
171
|
-
// Add dinosaur to new enclosure
|
|
595
|
+
// STEP 3: Add dinosaur to new enclosure
|
|
596
|
+
// Create a fresh record in the destination enclosure
|
|
172
597
|
await dinoTable
|
|
173
598
|
.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()
|
|
599
|
+
pk: "ENCLOSURE#B", // Destination enclosure B
|
|
600
|
+
sk: "DINO#001", // Same dinosaur ID for tracking
|
|
601
|
+
name: "Rex", // Dinosaur name
|
|
602
|
+
species: "Tyrannosaurus", // Species classification
|
|
603
|
+
diet: "Carnivore", // Dietary requirements
|
|
604
|
+
status: "HEALTHY", // Current health status
|
|
605
|
+
health: 100, // Reset health to 100% after transfer
|
|
606
|
+
enclosureId: "B", // Update enclosure reference
|
|
607
|
+
lastFed: new Date().toISOString() // Reset feeding clock
|
|
183
608
|
})
|
|
184
609
|
.withTransaction(tx);
|
|
185
610
|
|
|
186
|
-
// Update enclosure occupancy tracking
|
|
611
|
+
// STEP 4: Update enclosure occupancy tracking
|
|
612
|
+
// Keep accurate count of dinosaurs in each enclosure
|
|
187
613
|
await dinoTable
|
|
188
614
|
.update<Dinosaur>({
|
|
189
|
-
pk: "ENCLOSURE#B",
|
|
190
|
-
sk: "OCCUPANCY"
|
|
615
|
+
pk: "ENCLOSURE#B", // Target enclosure B
|
|
616
|
+
sk: "OCCUPANCY" // Occupancy tracking record
|
|
191
617
|
})
|
|
192
|
-
.add("currentOccupants", 1)
|
|
193
|
-
.set("lastUpdated", new Date().toISOString())
|
|
618
|
+
.add("currentOccupants", 1) // Increment occupant count
|
|
619
|
+
.set("lastUpdated", new Date().toISOString()) // Update timestamp
|
|
194
620
|
.withTransaction(tx);
|
|
195
621
|
});
|
|
196
622
|
|
|
197
|
-
// Transaction
|
|
623
|
+
// Transaction for dinosaur feeding and health monitoring
|
|
624
|
+
// Ensures feeding status and schedule are updated atomically
|
|
198
625
|
await dinoTable.transaction(
|
|
199
626
|
async (tx) => {
|
|
200
|
-
// Update
|
|
627
|
+
// STEP 1: Update Stegosaurus health and feeding status
|
|
628
|
+
// Record that the dinosaur has been fed and update its health metrics
|
|
201
629
|
await dinoTable
|
|
202
630
|
.update<Dinosaur>({
|
|
203
|
-
pk: "ENCLOSURE#D",
|
|
204
|
-
sk: "DINO#003"
|
|
631
|
+
pk: "ENCLOSURE#D", // Herbivore enclosure D
|
|
632
|
+
sk: "DINO#003" // Stegosaurus with ID 003
|
|
205
633
|
})
|
|
206
634
|
.set({
|
|
207
|
-
status: "HEALTHY",
|
|
208
|
-
lastFed: new Date().toISOString(),
|
|
209
|
-
health: 100
|
|
635
|
+
status: "HEALTHY", // Update health status
|
|
636
|
+
lastFed: new Date().toISOString(), // Record feeding time
|
|
637
|
+
health: 100 // Reset health to 100%
|
|
210
638
|
})
|
|
211
|
-
.deleteElementsFromSet("tags", ["needs_feeding"])
|
|
639
|
+
.deleteElementsFromSet("tags", ["needs_feeding"]) // Remove feeding alert tag
|
|
212
640
|
.withTransaction(tx);
|
|
213
641
|
|
|
214
|
-
// Update enclosure feeding schedule
|
|
642
|
+
// STEP 2: Update enclosure feeding schedule
|
|
643
|
+
// Schedule next feeding time for tomorrow
|
|
215
644
|
await dinoTable
|
|
216
645
|
.update<Dinosaur>({
|
|
217
|
-
pk: "ENCLOSURE#D",
|
|
218
|
-
sk: "SCHEDULE"
|
|
646
|
+
pk: "ENCLOSURE#D", // Same herbivore enclosure
|
|
647
|
+
sk: "SCHEDULE" // Feeding schedule record
|
|
219
648
|
})
|
|
220
|
-
.set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString())
|
|
649
|
+
.set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()) // 24 hours from now
|
|
221
650
|
.withTransaction(tx);
|
|
222
651
|
},
|
|
223
652
|
{
|
|
224
|
-
|
|
225
|
-
|
|
653
|
+
// Transaction options for tracking and idempotency
|
|
654
|
+
clientRequestToken: "feeding-session-001", // Prevents duplicate feeding operations
|
|
655
|
+
returnConsumedCapacity: "TOTAL" // Track capacity usage for park operations
|
|
226
656
|
}
|
|
227
657
|
);
|
|
228
658
|
```
|
|
@@ -239,35 +669,42 @@ await dinoTable.transaction(
|
|
|
239
669
|
|
|
240
670
|
**Efficient dinosaur park management with bulk operations**
|
|
241
671
|
```ts
|
|
242
|
-
//
|
|
672
|
+
// SCENARIO 1: Morning health check for multiple dinosaurs across enclosures
|
|
673
|
+
// Retrieve health status for multiple dinosaurs in a single operation
|
|
243
674
|
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
|
|
675
|
+
{ pk: "ENCLOSURE#A", sk: "DINO#001" }, // T-Rex in Paddock A
|
|
676
|
+
{ pk: "ENCLOSURE#B", sk: "DINO#002" }, // Velociraptor in Paddock B
|
|
677
|
+
{ pk: "ENCLOSURE#C", sk: "DINO#003" } // Stegosaurus in Paddock C
|
|
247
678
|
];
|
|
248
679
|
|
|
680
|
+
// Perform batch get operation to retrieve all dinosaurs at once
|
|
681
|
+
// This is much more efficient than individual gets
|
|
249
682
|
const { items: dinosaurs, unprocessedKeys } = await dinoTable.batchGet<Dinosaur>(healthCheckKeys);
|
|
250
683
|
console.log(`Health check completed for ${dinosaurs.length} dinosaurs`);
|
|
684
|
+
|
|
685
|
+
// Process health check results and identify any dinosaurs needing attention
|
|
251
686
|
dinosaurs.forEach(dino => {
|
|
252
687
|
if (dino.health < 80) {
|
|
253
688
|
console.log(`Health alert for ${dino.name} in Enclosure ${dino.enclosureId}`);
|
|
689
|
+
// In a real application, you might trigger alerts or schedule veterinary visits
|
|
254
690
|
}
|
|
255
691
|
});
|
|
256
692
|
|
|
257
|
-
//
|
|
693
|
+
// SCENARIO 2: Adding new herbivores to the park after quarantine
|
|
694
|
+
// Prepare data for multiple new herbivores joining the collection
|
|
258
695
|
const newHerbivores = [
|
|
259
696
|
{
|
|
260
697
|
pk: "ENCLOSURE#D", sk: "DINO#004",
|
|
261
|
-
name: "Triceratops Alpha",
|
|
698
|
+
name: "Triceratops Alpha", // Three-horned herbivore
|
|
262
699
|
species: "Triceratops",
|
|
263
700
|
diet: "Herbivore",
|
|
264
701
|
status: "HEALTHY",
|
|
265
|
-
health: 95,
|
|
266
|
-
lastFed: new Date().toISOString()
|
|
702
|
+
health: 95, // Excellent health after quarantine
|
|
703
|
+
lastFed: new Date().toISOString() // Just fed before joining main enclosure
|
|
267
704
|
},
|
|
268
705
|
{
|
|
269
706
|
pk: "ENCLOSURE#D", sk: "DINO#005",
|
|
270
|
-
name: "Brachy",
|
|
707
|
+
name: "Brachy", // Long-necked herbivore
|
|
271
708
|
species: "Brachiosaurus",
|
|
272
709
|
diet: "Herbivore",
|
|
273
710
|
status: "HEALTHY",
|
|
@@ -276,91 +713,117 @@ const newHerbivores = [
|
|
|
276
713
|
}
|
|
277
714
|
];
|
|
278
715
|
|
|
279
|
-
// Add new herbivores to enclosure
|
|
716
|
+
// Add all new herbivores to the enclosure in a single batch operation
|
|
717
|
+
// More efficient than individual writes and ensures consistent state
|
|
280
718
|
await dinoTable.batchWrite(
|
|
281
719
|
newHerbivores.map(dino => ({
|
|
282
|
-
type: "put",
|
|
283
|
-
item: dino
|
|
720
|
+
type: "put", // Create or replace operation
|
|
721
|
+
item: dino // Full dinosaur record
|
|
284
722
|
}))
|
|
285
723
|
);
|
|
286
724
|
|
|
287
|
-
//
|
|
725
|
+
// SCENARIO 3: Releasing a dinosaur from quarantine to general population
|
|
726
|
+
// Multiple related operations performed as a batch
|
|
288
727
|
await dinoTable.batchWrite([
|
|
289
|
-
// Remove dinosaur from quarantine
|
|
290
|
-
{
|
|
291
|
-
|
|
728
|
+
// Step 1: Remove dinosaur from quarantine enclosure
|
|
729
|
+
{
|
|
730
|
+
type: "delete",
|
|
731
|
+
key: { pk: "ENCLOSURE#QUARANTINE", sk: "DINO#006" }
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
// Step 2: Add recovered dinosaur to main raptor enclosure
|
|
292
735
|
{
|
|
293
736
|
type: "put",
|
|
294
737
|
item: {
|
|
295
738
|
pk: "ENCLOSURE#E", sk: "DINO#006",
|
|
296
|
-
name: "Raptor Beta",
|
|
739
|
+
name: "Raptor Beta", // Juvenile Velociraptor
|
|
297
740
|
species: "Velociraptor",
|
|
298
741
|
diet: "Carnivore",
|
|
299
|
-
status: "HEALTHY",
|
|
742
|
+
status: "HEALTHY", // Now healthy after treatment
|
|
300
743
|
health: 100,
|
|
301
744
|
lastFed: new Date().toISOString()
|
|
302
745
|
}
|
|
303
746
|
},
|
|
304
|
-
|
|
305
|
-
|
|
747
|
+
|
|
748
|
+
// Step 3: Clear quarantine status record
|
|
749
|
+
{
|
|
750
|
+
type: "delete",
|
|
751
|
+
key: { pk: "ENCLOSURE#QUARANTINE", sk: "STATUS#DINO#006" }
|
|
752
|
+
}
|
|
306
753
|
]);
|
|
307
754
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
755
|
+
// SCENARIO 4: Daily park-wide health monitoring
|
|
756
|
+
// Handle large-scale operations across all dinosaurs
|
|
757
|
+
// The library automatically handles chunking for large batches:
|
|
758
|
+
// - 25 items per batch write
|
|
759
|
+
// - 100 items per batch get
|
|
310
760
|
const dailyHealthUpdates = generateDinosaurHealthUpdates(); // Hundreds of updates
|
|
311
|
-
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked
|
|
761
|
+
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked into multiple requests
|
|
312
762
|
```
|
|
313
763
|
|
|
314
764
|
### Pagination Made Simple
|
|
315
765
|
|
|
316
|
-
**Efficient dinosaur record browsing**
|
|
766
|
+
**Efficient dinosaur record browsing for park management**
|
|
317
767
|
```ts
|
|
318
|
-
//
|
|
768
|
+
// SCENARIO 1: Herbivore health monitoring with pagination
|
|
769
|
+
// Create a paginator for viewing healthy herbivores in manageable chunks
|
|
770
|
+
// Perfect for veterinary staff doing routine health checks
|
|
319
771
|
const healthyHerbivores = dinoTable
|
|
320
772
|
.query<Dinosaur>({
|
|
321
|
-
pk: "DIET#herbivore",
|
|
322
|
-
sk: op => op.beginsWith("STATUS#HEALTHY")
|
|
773
|
+
pk: "DIET#herbivore", // Target all herbivorous dinosaurs
|
|
774
|
+
sk: op => op.beginsWith("STATUS#HEALTHY") // Only those with HEALTHY status
|
|
323
775
|
})
|
|
324
776
|
.filter((op) => op.and(
|
|
325
|
-
op.gte("health", 90),
|
|
326
|
-
op.attributeExists("lastFed")
|
|
777
|
+
op.gte("health", 90), // Only those with excellent health (90%+)
|
|
778
|
+
op.attributeExists("lastFed") // Must have feeding records
|
|
327
779
|
))
|
|
328
|
-
.paginate(5);
|
|
780
|
+
.paginate(5); // Process in small batches of 5 dinosaurs
|
|
329
781
|
|
|
330
|
-
//
|
|
782
|
+
// Iterate through all pages of results - useful for processing large datasets
|
|
783
|
+
// without loading everything into memory at once
|
|
784
|
+
console.log("🦕 Beginning herbivore health inspection rounds...");
|
|
331
785
|
while (healthyHerbivores.hasNextPage()) {
|
|
786
|
+
// Get the next page of dinosaurs
|
|
332
787
|
const page = await healthyHerbivores.getNextPage();
|
|
333
788
|
console.log(`Checking herbivores page ${page.page}, found ${page.items.length} dinosaurs`);
|
|
789
|
+
|
|
790
|
+
// Process each dinosaur in the current page
|
|
334
791
|
page.items.forEach(dino => {
|
|
335
792
|
console.log(`${dino.name}: Health ${dino.health}%, Last fed: ${dino.lastFed}`);
|
|
793
|
+
// In a real app, you might update health records or schedule next checkup
|
|
336
794
|
});
|
|
337
795
|
}
|
|
338
796
|
|
|
339
|
-
//
|
|
797
|
+
// SCENARIO 2: Preparing carnivore feeding schedule
|
|
798
|
+
// Get all carnivores at once for daily feeding planning
|
|
799
|
+
// This approach loads all matching items into memory
|
|
340
800
|
const carnivoreSchedule = await dinoTable
|
|
341
801
|
.query<Dinosaur>({
|
|
342
|
-
pk: "DIET#carnivore",
|
|
343
|
-
sk: op => op.beginsWith("ENCLOSURE#")
|
|
802
|
+
pk: "DIET#carnivore", // Target all carnivorous dinosaurs
|
|
803
|
+
sk: op => op.beginsWith("ENCLOSURE#") // Organized by enclosure
|
|
344
804
|
})
|
|
345
|
-
.filter(op => op.attributeExists("lastFed"))
|
|
346
|
-
.paginate(10)
|
|
347
|
-
.getAllPages();
|
|
805
|
+
.filter(op => op.attributeExists("lastFed")) // Only those with feeding records
|
|
806
|
+
.paginate(10) // Process in pages of 10
|
|
807
|
+
.getAllPages(); // But collect all results at once
|
|
348
808
|
|
|
349
809
|
console.log(`Scheduling feeding for ${carnivoreSchedule.length} carnivores`);
|
|
810
|
+
// Now we can sort and organize feeding times based on species, size, etc.
|
|
350
811
|
|
|
351
|
-
//
|
|
812
|
+
// SCENARIO 3: Visitor information kiosk with limited display
|
|
813
|
+
// Create a paginated view for the public-facing dinosaur information kiosk
|
|
352
814
|
const visitorKiosk = dinoTable
|
|
353
815
|
.query<Dinosaur>({
|
|
354
|
-
pk: "VISITOR_VIEW",
|
|
355
|
-
sk: op => op.beginsWith("SPECIES#")
|
|
816
|
+
pk: "VISITOR_VIEW", // Special partition for visitor-facing data
|
|
817
|
+
sk: op => op.beginsWith("SPECIES#") // Organized by species
|
|
356
818
|
})
|
|
357
|
-
.filter(op => op.eq("status", "ON_DISPLAY"))
|
|
358
|
-
.limit(12)
|
|
359
|
-
.paginate(4);
|
|
819
|
+
.filter(op => op.eq("status", "ON_DISPLAY")) // Only show dinosaurs currently on display
|
|
820
|
+
.limit(12) // Show maximum 12 dinosaurs total
|
|
821
|
+
.paginate(4); // Display 4 at a time for easy viewing
|
|
360
822
|
|
|
361
|
-
// Get first page for kiosk display
|
|
823
|
+
// Get first page for initial kiosk display
|
|
362
824
|
const firstPage = await visitorKiosk.getNextPage();
|
|
363
|
-
console.log(
|
|
825
|
+
console.log(`🦖 Now showing: ${firstPage.items.map(d => d.name).join(", ")}`);
|
|
826
|
+
// Visitors can press "Next" to see more dinosaurs in the collection
|
|
364
827
|
```
|
|
365
828
|
|
|
366
829
|
## 🛡️ Type-Safe Query Building
|
|
@@ -426,6 +889,51 @@ const limited = await table
|
|
|
426
889
|
.execute();
|
|
427
890
|
```
|
|
428
891
|
|
|
892
|
+
### Put Operations
|
|
893
|
+
|
|
894
|
+
| Operation | Method Example | Description |
|
|
895
|
+
|---------------------|---------------------------------------------------------------------|------------------------------------------------------------------------|
|
|
896
|
+
| **Create New Item** | `.create<Dinosaur>({ pk: "SPECIES#trex", sk: "PROFILE#001", ... })` | Creates a new item with a condition to ensure it doesn't already exist |
|
|
897
|
+
| **Put Item** | `.put<Dinosaur>({ pk: "SPECIES#trex", sk: "PROFILE#001", ... })` | Creates or replaces an item |
|
|
898
|
+
| **With Condition** | `.put(item).condition(op => op.attributeNotExists("pk"))` | Adds a condition that must be satisfied |
|
|
899
|
+
|
|
900
|
+
#### Return Values
|
|
901
|
+
|
|
902
|
+
Control what data is returned from put operations:
|
|
903
|
+
|
|
904
|
+
| Option | Description | Example |
|
|
905
|
+
|----------------|--------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|
|
|
906
|
+
| **NONE** | Default. No return value. | `.put(item).returnValues("NONE").execute()` |
|
|
907
|
+
| **ALL_OLD** | Returns the item's previous state if it existed. (Does not consume any RCU and returns strongly consistent values) | `.put(item).returnValues("ALL_OLD").execute()` |
|
|
908
|
+
| **CONSISTENT** | Performs a consistent GET operation after the put to retrieve the item's new state. (Does consume RCU) | `.put(item).returnValues("CONSISTENT").execute()` |
|
|
909
|
+
|
|
910
|
+
```ts
|
|
911
|
+
// Create with no return value (default)
|
|
912
|
+
await table.put<Dinosaur>({
|
|
913
|
+
pk: "SPECIES#trex",
|
|
914
|
+
sk: "PROFILE#001",
|
|
915
|
+
name: "Tyrannosaurus Rex",
|
|
916
|
+
diet: "carnivore"
|
|
917
|
+
}).execute();
|
|
918
|
+
|
|
919
|
+
// Create and return the newly created item
|
|
920
|
+
const newDino = await table.put<Dinosaur>({
|
|
921
|
+
pk: "SPECIES#trex",
|
|
922
|
+
sk: "PROFILE#002",
|
|
923
|
+
name: "Tyrannosaurus Rex",
|
|
924
|
+
diet: "carnivore"
|
|
925
|
+
}).returnValues("CONSISTENT").execute();
|
|
926
|
+
|
|
927
|
+
// Update with condition and get previous values
|
|
928
|
+
const oldDino = await table.put<Dinosaur>({
|
|
929
|
+
pk: "SPECIES#trex",
|
|
930
|
+
sk: "PROFILE#001",
|
|
931
|
+
name: "Tyrannosaurus Rex",
|
|
932
|
+
diet: "omnivore", // Updated diet
|
|
933
|
+
discoveryYear: 1905
|
|
934
|
+
}).returnValues("ALL_OLD").execute();
|
|
935
|
+
```
|
|
936
|
+
|
|
429
937
|
### Update Operations
|
|
430
938
|
|
|
431
939
|
| Operation | Method Example | Generated Expression |
|
|
@@ -603,24 +1111,6 @@ const result = await table.transaction(
|
|
|
603
1111
|
);
|
|
604
1112
|
```
|
|
605
1113
|
|
|
606
|
-
## 🏗️ Entity Pattern Best Practices (Coming Soon TM)
|
|
607
|
-
|
|
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
|
|
617
|
-
```
|
|
618
|
-
|
|
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
|
|
624
1114
|
|
|
625
1115
|
## 🚨 Error Handling
|
|
626
1116
|
|
|
@@ -782,3 +1272,34 @@ pnpm test
|
|
|
782
1272
|
# Build the project
|
|
783
1273
|
pnpm build
|
|
784
1274
|
```
|
|
1275
|
+
|
|
1276
|
+
## 🦔 Running Examples
|
|
1277
|
+
|
|
1278
|
+
There's a few pre-configured example scripts in the `examples` directory.
|
|
1279
|
+
|
|
1280
|
+
First you'll need to install the dependencies:
|
|
1281
|
+
|
|
1282
|
+
```bash
|
|
1283
|
+
pnpm install
|
|
1284
|
+
```
|
|
1285
|
+
Then setup the test table in local DynamoDB by running the following command:
|
|
1286
|
+
|
|
1287
|
+
```bash
|
|
1288
|
+
pnpm run ddb:start
|
|
1289
|
+
pnpm run local:setup
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
To run the examples, you can use the following command:
|
|
1293
|
+
|
|
1294
|
+
```bash
|
|
1295
|
+
npx tsx examples/[EXAMPLE_NAME].ts
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
To view the test table GUI in action: [DynamoDB Admin](http://localhost:8001/)
|
|
1299
|
+
|
|
1300
|
+
<br />
|
|
1301
|
+
To teardown the test table when you're done, run the following command:
|
|
1302
|
+
|
|
1303
|
+
```bash
|
|
1304
|
+
pnpm run local:teardown
|
|
1305
|
+
```
|