dyno-table 0.1.8 → 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/README.md +570 -147
- 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 +1 -1
- package/dist/builders/condition-check-builder.cjs.map +1 -1
- 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 +1 -1
- package/dist/builders/condition-check-builder.js.map +1 -1
- package/dist/builders/delete-builder.cjs +0 -17
- package/dist/builders/delete-builder.cjs.map +1 -1
- package/dist/builders/delete-builder.d.cts +166 -0
- package/dist/builders/delete-builder.d.ts +166 -0
- package/dist/builders/delete-builder.js +0 -17
- package/dist/builders/delete-builder.js.map +1 -1
- package/dist/builders/paginator.cjs.map +1 -1
- package/dist/builders/paginator.d.cts +179 -0
- package/dist/builders/paginator.d.ts +179 -0
- package/dist/builders/paginator.js.map +1 -1
- package/dist/builders/put-builder.cjs +8 -0
- package/dist/builders/put-builder.cjs.map +1 -1
- package/dist/builders/put-builder.d.cts +274 -0
- package/dist/builders/put-builder.d.ts +274 -0
- package/dist/builders/put-builder.js +8 -0
- package/dist/builders/put-builder.js.map +1 -1
- package/dist/builders/query-builder.cjs.map +1 -1
- package/dist/builders/query-builder.d.cts +6 -0
- package/dist/builders/query-builder.d.ts +6 -0
- package/dist/builders/query-builder.js.map +1 -1
- package/dist/builders/transaction-builder.cjs +40 -22
- package/dist/builders/transaction-builder.cjs.map +1 -1
- package/dist/builders/transaction-builder.d.cts +511 -0
- package/dist/builders/transaction-builder.d.ts +511 -0
- package/dist/builders/transaction-builder.js +40 -22
- package/dist/builders/transaction-builder.js.map +1 -1
- package/dist/builders/update-builder.cjs +3 -38
- package/dist/builders/update-builder.cjs.map +1 -1
- package/dist/builders/update-builder.d.cts +365 -0
- package/dist/builders/update-builder.d.ts +365 -0
- package/dist/builders/update-builder.js +3 -38
- package/dist/builders/update-builder.js.map +1 -1
- package/dist/conditions--ld9a78i.d.ts +331 -0
- package/dist/conditions-ChhQWd6z.d.cts +331 -0
- package/dist/conditions.cjs.map +1 -1
- package/dist/conditions.d.cts +3 -0
- package/dist/conditions.d.ts +3 -0
- package/dist/conditions.js.map +1 -1
- package/dist/entity.cjs +156 -97
- package/dist/entity.cjs.map +1 -1
- package/dist/entity.d.cts +149 -0
- package/dist/entity.d.ts +149 -0
- package/dist/entity.js +156 -97
- package/dist/entity.js.map +1 -1
- package/dist/query-builder-Csror9Iu.d.ts +507 -0
- package/dist/query-builder-D2FM9rsu.d.cts +507 -0
- package/dist/standard-schema.d.cts +57 -0
- package/dist/standard-schema.d.ts +57 -0
- package/dist/table-BEhBPy2G.d.cts +364 -0
- package/dist/table-BW3cmUqr.d.ts +364 -0
- package/dist/table.cjs +82 -102
- package/dist/table.cjs.map +1 -1
- package/dist/table.d.cts +12 -0
- package/dist/table.d.ts +12 -0
- package/dist/table.js +82 -102
- package/dist/table.js.map +1 -1
- package/dist/types.d.cts +22 -0
- package/dist/types.d.ts +22 -0
- package/dist/utils/{key-template.cjs → partition-key-template.cjs} +3 -3
- 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/{key-template.js → partition-key-template.js} +3 -3
- package/dist/utils/partition-key-template.js.map +1 -0
- package/dist/utils/sort-key-template.d.cts +35 -0
- package/dist/utils/sort-key-template.d.ts +35 -0
- package/package.json +86 -9
- package/dist/index.cjs +0 -3333
- package/dist/index.d.cts +0 -2971
- package/dist/index.d.ts +0 -2971
- package/dist/index.js +0 -3284
- package/dist/utils/key-template.cjs.map +0 -1
- package/dist/utils/key-template.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,35 +6,46 @@
|
|
|
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
33
|
- [🦖 dyno-table ](#-dyno-table--)
|
|
32
|
-
- [🌟 Why dyno-table?](#-why-dyno-table)
|
|
34
|
+
- [🌟 Why dyno-table for your Dinosaur Data?](#-why-dyno-table-for-your-dinosaur-data)
|
|
33
35
|
- [📑 Table of Contents](#-table-of-contents)
|
|
34
36
|
- [📦 Installation](#-installation)
|
|
35
37
|
- [🚀 Quick Start](#-quick-start)
|
|
36
|
-
- [1. Configure Your Table](#1-configure-your-table)
|
|
37
|
-
- [2. Perform Type-Safe Operations](#2-perform-type-safe-operations)
|
|
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)
|
|
38
49
|
- [🧩 Advanced Features](#-advanced-features)
|
|
39
50
|
- [Transactional Operations](#transactional-operations)
|
|
40
51
|
- [Batch Processing](#batch-processing)
|
|
@@ -56,7 +67,6 @@ await table
|
|
|
56
67
|
- [🔒 Transaction Operations](#-transaction-operations)
|
|
57
68
|
- [Transaction Builder](#transaction-builder)
|
|
58
69
|
- [Transaction Options](#transaction-options)
|
|
59
|
-
- [🏗️ Entity Pattern Best Practices (Coming Soon TM)](#️-entity-pattern-best-practices-coming-soon-tm)
|
|
60
70
|
- [🚨 Error Handling](#-error-handling)
|
|
61
71
|
- [📚 API Reference](#-api-reference)
|
|
62
72
|
- [Condition Operators](#condition-operators-1)
|
|
@@ -82,25 +92,26 @@ npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
|
82
92
|
|
|
83
93
|
## 🚀 Quick Start
|
|
84
94
|
|
|
85
|
-
### 1. Configure Your Table
|
|
95
|
+
### 1. Configure Your Jurassic Table
|
|
86
96
|
|
|
87
97
|
```ts
|
|
88
98
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
89
99
|
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
90
|
-
import { Table } from "dyno-table";
|
|
100
|
+
import { Table } from "dyno-table/table";
|
|
91
101
|
|
|
92
|
-
// Configure AWS SDK clients
|
|
102
|
+
// Configure AWS SDK clients - your gateway to the prehistoric database
|
|
93
103
|
const client = new DynamoDBClient({ region: "us-west-2" });
|
|
94
104
|
const docClient = DynamoDBDocument.from(client);
|
|
95
105
|
|
|
96
|
-
// Initialize table with single-table design schema
|
|
106
|
+
// Initialize table with single-table design schema - your dinosaur park database
|
|
97
107
|
const dinoTable = new Table({
|
|
98
108
|
client: docClient,
|
|
99
|
-
tableName: "
|
|
109
|
+
tableName: "JurassicPark", // Your central dinosaur tracking system
|
|
100
110
|
indexes: {
|
|
101
|
-
partitionKey: "pk",
|
|
102
|
-
sortKey: "sk",
|
|
111
|
+
partitionKey: "pk", // Primary partition key for fast dinosaur lookups
|
|
112
|
+
sortKey: "sk", // Sort key for organizing dinosaur data
|
|
103
113
|
gsis: {
|
|
114
|
+
// Global Secondary Index for querying dinosaurs by species
|
|
104
115
|
speciesId: {
|
|
105
116
|
partitionKey: "gsi1pk",
|
|
106
117
|
sortKey: "gsi1sk",
|
|
@@ -110,141 +121,538 @@ const dinoTable = new Table({
|
|
|
110
121
|
});
|
|
111
122
|
```
|
|
112
123
|
|
|
113
|
-
### 2. Perform Type-Safe Operations
|
|
124
|
+
### 2. Perform Type-Safe Dinosaur Operations
|
|
114
125
|
|
|
115
|
-
**🦖 Creating a new dinosaur**
|
|
126
|
+
**🦖 Creating a new dinosaur specimen**
|
|
116
127
|
```ts
|
|
128
|
+
// Add a new T-Rex to your collection with complete type safety
|
|
117
129
|
const rex = await dinoTable
|
|
118
130
|
.create<Dinosaur>({
|
|
119
|
-
pk: "SPECIES#trex",
|
|
120
|
-
sk: "PROFILE#trex",
|
|
121
|
-
speciesId: "trex",
|
|
122
|
-
name: "Tyrannosaurus Rex",
|
|
123
|
-
diet: "carnivore",
|
|
124
|
-
length: 12.3,
|
|
125
|
-
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
|
|
126
138
|
})
|
|
127
139
|
.execute();
|
|
128
140
|
```
|
|
129
141
|
|
|
130
|
-
**🔍 Query with conditions**
|
|
142
|
+
**🔍 Query for specific dinosaurs with conditions**
|
|
131
143
|
```ts
|
|
144
|
+
// Find large carnivorous dinosaurs in the T-Rex species
|
|
132
145
|
const largeDinos = await dinoTable
|
|
133
146
|
.query<Dinosaur>({
|
|
134
|
-
pk: "SPECIES#trex",
|
|
135
|
-
sk: (op) => op.beginsWith("PROFILE#")
|
|
147
|
+
pk: "SPECIES#trex", // Target the T-Rex species
|
|
148
|
+
sk: (op) => op.beginsWith("PROFILE#") // Look in profile records
|
|
136
149
|
})
|
|
137
150
|
.filter((op) => op.and(
|
|
138
|
-
op.gte("length", 10),
|
|
139
|
-
op.eq("diet", "carnivore")
|
|
151
|
+
op.gte("length", 10), // Only dinosaurs longer than 10 meters
|
|
152
|
+
op.eq("diet", "carnivore") // Must be carnivores
|
|
140
153
|
))
|
|
141
|
-
.limit(10)
|
|
154
|
+
.limit(10) // Limit to 10 results
|
|
142
155
|
.execute();
|
|
143
156
|
```
|
|
144
157
|
|
|
145
|
-
**🔄
|
|
158
|
+
**🔄 Update dinosaur classification**
|
|
146
159
|
```ts
|
|
160
|
+
// Update a dinosaur's diet classification based on new research
|
|
147
161
|
await dinoTable
|
|
148
162
|
.update<Dinosaur>({
|
|
149
|
-
pk: "SPECIES#trex",
|
|
150
|
-
sk: "PROFILE#trex"
|
|
163
|
+
pk: "SPECIES#trex", // Target the T-Rex species
|
|
164
|
+
sk: "PROFILE#trex" // Specific profile to update
|
|
151
165
|
})
|
|
152
|
-
.set("diet", "omnivore")
|
|
153
|
-
.add("discoveryYear", 1)
|
|
154
|
-
.remove("outdatedField")
|
|
155
|
-
.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
|
|
156
170
|
.execute();
|
|
157
171
|
```
|
|
158
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
|
+
|
|
159
557
|
## 🧩 Advanced Features
|
|
160
558
|
|
|
161
559
|
### Transactional Operations
|
|
162
560
|
|
|
163
561
|
**Safe dinosaur transfer between enclosures**
|
|
164
562
|
```ts
|
|
165
|
-
// 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
|
|
166
565
|
await dinoTable.transaction(async (tx) => {
|
|
167
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
|
|
168
568
|
|
|
169
|
-
// 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
|
|
170
571
|
await dinoTable
|
|
171
572
|
.conditionCheck({
|
|
172
|
-
pk: "ENCLOSURE#B",
|
|
173
|
-
sk: "STATUS"
|
|
573
|
+
pk: "ENCLOSURE#B", // Target enclosure B
|
|
574
|
+
sk: "STATUS" // Check the enclosure status record
|
|
174
575
|
})
|
|
175
576
|
.condition(op => op.and(
|
|
176
|
-
op.eq("status", "READY"),
|
|
177
|
-
op.eq("diet", "Carnivore") //
|
|
577
|
+
op.eq("status", "READY"), // Enclosure must be in READY state
|
|
578
|
+
op.eq("diet", "Carnivore") // Must support carnivorous dinosaurs
|
|
178
579
|
))
|
|
179
580
|
.withTransaction(tx);
|
|
180
581
|
|
|
181
|
-
// Remove dinosaur from current enclosure
|
|
582
|
+
// STEP 2: Remove dinosaur from current enclosure
|
|
583
|
+
// Only proceed if the dinosaur is healthy enough for transfer
|
|
182
584
|
await dinoTable
|
|
183
585
|
.delete<Dinosaur>({
|
|
184
|
-
pk: "ENCLOSURE#A",
|
|
185
|
-
sk: "DINO#001"
|
|
586
|
+
pk: "ENCLOSURE#A", // Source enclosure A
|
|
587
|
+
sk: "DINO#001" // T-Rex with ID 001
|
|
186
588
|
})
|
|
187
589
|
.condition(op => op.and(
|
|
188
|
-
op.eq("status", "HEALTHY"),
|
|
189
|
-
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%
|
|
190
592
|
))
|
|
191
593
|
.withTransaction(tx);
|
|
192
594
|
|
|
193
|
-
// Add dinosaur to new enclosure
|
|
595
|
+
// STEP 3: Add dinosaur to new enclosure
|
|
596
|
+
// Create a fresh record in the destination enclosure
|
|
194
597
|
await dinoTable
|
|
195
598
|
.create<Dinosaur>({
|
|
196
|
-
pk: "ENCLOSURE#B",
|
|
197
|
-
sk: "DINO#001",
|
|
198
|
-
name: "Rex",
|
|
199
|
-
species: "Tyrannosaurus",
|
|
200
|
-
diet: "Carnivore",
|
|
201
|
-
status: "HEALTHY",
|
|
202
|
-
health: 100,
|
|
203
|
-
enclosureId: "B",
|
|
204
|
-
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
|
|
205
608
|
})
|
|
206
609
|
.withTransaction(tx);
|
|
207
610
|
|
|
208
|
-
// Update enclosure occupancy tracking
|
|
611
|
+
// STEP 4: Update enclosure occupancy tracking
|
|
612
|
+
// Keep accurate count of dinosaurs in each enclosure
|
|
209
613
|
await dinoTable
|
|
210
614
|
.update<Dinosaur>({
|
|
211
|
-
pk: "ENCLOSURE#B",
|
|
212
|
-
sk: "OCCUPANCY"
|
|
615
|
+
pk: "ENCLOSURE#B", // Target enclosure B
|
|
616
|
+
sk: "OCCUPANCY" // Occupancy tracking record
|
|
213
617
|
})
|
|
214
|
-
.add("currentOccupants", 1)
|
|
215
|
-
.set("lastUpdated", new Date().toISOString())
|
|
618
|
+
.add("currentOccupants", 1) // Increment occupant count
|
|
619
|
+
.set("lastUpdated", new Date().toISOString()) // Update timestamp
|
|
216
620
|
.withTransaction(tx);
|
|
217
621
|
});
|
|
218
622
|
|
|
219
|
-
// Transaction
|
|
623
|
+
// Transaction for dinosaur feeding and health monitoring
|
|
624
|
+
// Ensures feeding status and schedule are updated atomically
|
|
220
625
|
await dinoTable.transaction(
|
|
221
626
|
async (tx) => {
|
|
222
|
-
// Update
|
|
627
|
+
// STEP 1: Update Stegosaurus health and feeding status
|
|
628
|
+
// Record that the dinosaur has been fed and update its health metrics
|
|
223
629
|
await dinoTable
|
|
224
630
|
.update<Dinosaur>({
|
|
225
|
-
pk: "ENCLOSURE#D",
|
|
226
|
-
sk: "DINO#003"
|
|
631
|
+
pk: "ENCLOSURE#D", // Herbivore enclosure D
|
|
632
|
+
sk: "DINO#003" // Stegosaurus with ID 003
|
|
227
633
|
})
|
|
228
634
|
.set({
|
|
229
|
-
status: "HEALTHY",
|
|
230
|
-
lastFed: new Date().toISOString(),
|
|
231
|
-
health: 100
|
|
635
|
+
status: "HEALTHY", // Update health status
|
|
636
|
+
lastFed: new Date().toISOString(), // Record feeding time
|
|
637
|
+
health: 100 // Reset health to 100%
|
|
232
638
|
})
|
|
233
|
-
.deleteElementsFromSet("tags", ["needs_feeding"])
|
|
639
|
+
.deleteElementsFromSet("tags", ["needs_feeding"]) // Remove feeding alert tag
|
|
234
640
|
.withTransaction(tx);
|
|
235
641
|
|
|
236
|
-
// Update enclosure feeding schedule
|
|
642
|
+
// STEP 2: Update enclosure feeding schedule
|
|
643
|
+
// Schedule next feeding time for tomorrow
|
|
237
644
|
await dinoTable
|
|
238
645
|
.update<Dinosaur>({
|
|
239
|
-
pk: "ENCLOSURE#D",
|
|
240
|
-
sk: "SCHEDULE"
|
|
646
|
+
pk: "ENCLOSURE#D", // Same herbivore enclosure
|
|
647
|
+
sk: "SCHEDULE" // Feeding schedule record
|
|
241
648
|
})
|
|
242
|
-
.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
|
|
243
650
|
.withTransaction(tx);
|
|
244
651
|
},
|
|
245
652
|
{
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
248
656
|
}
|
|
249
657
|
);
|
|
250
658
|
```
|
|
@@ -261,35 +669,42 @@ await dinoTable.transaction(
|
|
|
261
669
|
|
|
262
670
|
**Efficient dinosaur park management with bulk operations**
|
|
263
671
|
```ts
|
|
264
|
-
//
|
|
672
|
+
// SCENARIO 1: Morning health check for multiple dinosaurs across enclosures
|
|
673
|
+
// Retrieve health status for multiple dinosaurs in a single operation
|
|
265
674
|
const healthCheckKeys = [
|
|
266
|
-
{ pk: "ENCLOSURE#A", sk: "DINO#001" }, // T-Rex
|
|
267
|
-
{ pk: "ENCLOSURE#B", sk: "DINO#002" }, // Velociraptor
|
|
268
|
-
{ 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
|
|
269
678
|
];
|
|
270
679
|
|
|
680
|
+
// Perform batch get operation to retrieve all dinosaurs at once
|
|
681
|
+
// This is much more efficient than individual gets
|
|
271
682
|
const { items: dinosaurs, unprocessedKeys } = await dinoTable.batchGet<Dinosaur>(healthCheckKeys);
|
|
272
683
|
console.log(`Health check completed for ${dinosaurs.length} dinosaurs`);
|
|
684
|
+
|
|
685
|
+
// Process health check results and identify any dinosaurs needing attention
|
|
273
686
|
dinosaurs.forEach(dino => {
|
|
274
687
|
if (dino.health < 80) {
|
|
275
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
|
|
276
690
|
}
|
|
277
691
|
});
|
|
278
692
|
|
|
279
|
-
//
|
|
693
|
+
// SCENARIO 2: Adding new herbivores to the park after quarantine
|
|
694
|
+
// Prepare data for multiple new herbivores joining the collection
|
|
280
695
|
const newHerbivores = [
|
|
281
696
|
{
|
|
282
697
|
pk: "ENCLOSURE#D", sk: "DINO#004",
|
|
283
|
-
name: "Triceratops Alpha",
|
|
698
|
+
name: "Triceratops Alpha", // Three-horned herbivore
|
|
284
699
|
species: "Triceratops",
|
|
285
700
|
diet: "Herbivore",
|
|
286
701
|
status: "HEALTHY",
|
|
287
|
-
health: 95,
|
|
288
|
-
lastFed: new Date().toISOString()
|
|
702
|
+
health: 95, // Excellent health after quarantine
|
|
703
|
+
lastFed: new Date().toISOString() // Just fed before joining main enclosure
|
|
289
704
|
},
|
|
290
705
|
{
|
|
291
706
|
pk: "ENCLOSURE#D", sk: "DINO#005",
|
|
292
|
-
name: "Brachy",
|
|
707
|
+
name: "Brachy", // Long-necked herbivore
|
|
293
708
|
species: "Brachiosaurus",
|
|
294
709
|
diet: "Herbivore",
|
|
295
710
|
status: "HEALTHY",
|
|
@@ -298,91 +713,117 @@ const newHerbivores = [
|
|
|
298
713
|
}
|
|
299
714
|
];
|
|
300
715
|
|
|
301
|
-
// 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
|
|
302
718
|
await dinoTable.batchWrite(
|
|
303
719
|
newHerbivores.map(dino => ({
|
|
304
|
-
type: "put",
|
|
305
|
-
item: dino
|
|
720
|
+
type: "put", // Create or replace operation
|
|
721
|
+
item: dino // Full dinosaur record
|
|
306
722
|
}))
|
|
307
723
|
);
|
|
308
724
|
|
|
309
|
-
//
|
|
725
|
+
// SCENARIO 3: Releasing a dinosaur from quarantine to general population
|
|
726
|
+
// Multiple related operations performed as a batch
|
|
310
727
|
await dinoTable.batchWrite([
|
|
311
|
-
// Remove dinosaur from quarantine
|
|
312
|
-
{
|
|
313
|
-
|
|
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
|
|
314
735
|
{
|
|
315
736
|
type: "put",
|
|
316
737
|
item: {
|
|
317
738
|
pk: "ENCLOSURE#E", sk: "DINO#006",
|
|
318
|
-
name: "Raptor Beta",
|
|
739
|
+
name: "Raptor Beta", // Juvenile Velociraptor
|
|
319
740
|
species: "Velociraptor",
|
|
320
741
|
diet: "Carnivore",
|
|
321
|
-
status: "HEALTHY",
|
|
742
|
+
status: "HEALTHY", // Now healthy after treatment
|
|
322
743
|
health: 100,
|
|
323
744
|
lastFed: new Date().toISOString()
|
|
324
745
|
}
|
|
325
746
|
},
|
|
326
|
-
|
|
327
|
-
|
|
747
|
+
|
|
748
|
+
// Step 3: Clear quarantine status record
|
|
749
|
+
{
|
|
750
|
+
type: "delete",
|
|
751
|
+
key: { pk: "ENCLOSURE#QUARANTINE", sk: "STATUS#DINO#006" }
|
|
752
|
+
}
|
|
328
753
|
]);
|
|
329
754
|
|
|
330
|
-
//
|
|
331
|
-
//
|
|
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
|
|
332
760
|
const dailyHealthUpdates = generateDinosaurHealthUpdates(); // Hundreds of updates
|
|
333
|
-
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked
|
|
761
|
+
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked into multiple requests
|
|
334
762
|
```
|
|
335
763
|
|
|
336
764
|
### Pagination Made Simple
|
|
337
765
|
|
|
338
|
-
**Efficient dinosaur record browsing**
|
|
766
|
+
**Efficient dinosaur record browsing for park management**
|
|
339
767
|
```ts
|
|
340
|
-
//
|
|
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
|
|
341
771
|
const healthyHerbivores = dinoTable
|
|
342
772
|
.query<Dinosaur>({
|
|
343
|
-
pk: "DIET#herbivore",
|
|
344
|
-
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
|
|
345
775
|
})
|
|
346
776
|
.filter((op) => op.and(
|
|
347
|
-
op.gte("health", 90),
|
|
348
|
-
op.attributeExists("lastFed")
|
|
777
|
+
op.gte("health", 90), // Only those with excellent health (90%+)
|
|
778
|
+
op.attributeExists("lastFed") // Must have feeding records
|
|
349
779
|
))
|
|
350
|
-
.paginate(5);
|
|
780
|
+
.paginate(5); // Process in small batches of 5 dinosaurs
|
|
351
781
|
|
|
352
|
-
//
|
|
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...");
|
|
353
785
|
while (healthyHerbivores.hasNextPage()) {
|
|
786
|
+
// Get the next page of dinosaurs
|
|
354
787
|
const page = await healthyHerbivores.getNextPage();
|
|
355
788
|
console.log(`Checking herbivores page ${page.page}, found ${page.items.length} dinosaurs`);
|
|
789
|
+
|
|
790
|
+
// Process each dinosaur in the current page
|
|
356
791
|
page.items.forEach(dino => {
|
|
357
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
|
|
358
794
|
});
|
|
359
795
|
}
|
|
360
796
|
|
|
361
|
-
//
|
|
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
|
|
362
800
|
const carnivoreSchedule = await dinoTable
|
|
363
801
|
.query<Dinosaur>({
|
|
364
|
-
pk: "DIET#carnivore",
|
|
365
|
-
sk: op => op.beginsWith("ENCLOSURE#")
|
|
802
|
+
pk: "DIET#carnivore", // Target all carnivorous dinosaurs
|
|
803
|
+
sk: op => op.beginsWith("ENCLOSURE#") // Organized by enclosure
|
|
366
804
|
})
|
|
367
|
-
.filter(op => op.attributeExists("lastFed"))
|
|
368
|
-
.paginate(10)
|
|
369
|
-
.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
|
|
370
808
|
|
|
371
809
|
console.log(`Scheduling feeding for ${carnivoreSchedule.length} carnivores`);
|
|
810
|
+
// Now we can sort and organize feeding times based on species, size, etc.
|
|
372
811
|
|
|
373
|
-
//
|
|
812
|
+
// SCENARIO 3: Visitor information kiosk with limited display
|
|
813
|
+
// Create a paginated view for the public-facing dinosaur information kiosk
|
|
374
814
|
const visitorKiosk = dinoTable
|
|
375
815
|
.query<Dinosaur>({
|
|
376
|
-
pk: "VISITOR_VIEW",
|
|
377
|
-
sk: op => op.beginsWith("SPECIES#")
|
|
816
|
+
pk: "VISITOR_VIEW", // Special partition for visitor-facing data
|
|
817
|
+
sk: op => op.beginsWith("SPECIES#") // Organized by species
|
|
378
818
|
})
|
|
379
|
-
.filter(op => op.eq("status", "ON_DISPLAY"))
|
|
380
|
-
.limit(12)
|
|
381
|
-
.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
|
|
382
822
|
|
|
383
|
-
// Get first page for kiosk display
|
|
823
|
+
// Get first page for initial kiosk display
|
|
384
824
|
const firstPage = await visitorKiosk.getNextPage();
|
|
385
|
-
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
|
|
386
827
|
```
|
|
387
828
|
|
|
388
829
|
## 🛡️ Type-Safe Query Building
|
|
@@ -670,24 +1111,6 @@ const result = await table.transaction(
|
|
|
670
1111
|
);
|
|
671
1112
|
```
|
|
672
1113
|
|
|
673
|
-
## 🏗️ Entity Pattern Best Practices (Coming Soon TM)
|
|
674
|
-
|
|
675
|
-
The entity implementation provides automatic type isolation:
|
|
676
|
-
|
|
677
|
-
```ts
|
|
678
|
-
// All operations are automatically scoped to DINOSAUR type
|
|
679
|
-
const dinosaur = await dinoEntity.get("SPECIES#trex", "PROFILE#trex");
|
|
680
|
-
// Returns Dinosaur | undefined
|
|
681
|
-
|
|
682
|
-
// Cross-type operations are prevented at compile time
|
|
683
|
-
dinoEntity.create({ /* invalid shape */ }); // TypeScript error
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
**Key benefits:**
|
|
687
|
-
- 🚫 Prevents accidental cross-type data access
|
|
688
|
-
- 🔍 Automatically filters queries/scans to repository type
|
|
689
|
-
- 🛡️ Ensures consistent key structure across entities
|
|
690
|
-
- 📦 Encapsulates domain-specific query logic
|
|
691
1114
|
|
|
692
1115
|
## 🚨 Error Handling
|
|
693
1116
|
|