dyno-table 1.0.0-alpha.1 → 1.1.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 +752 -172
- 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 +918 -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 +916 -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 +211 -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 +207 -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.cjs → table.cjs} +108 -175
- package/dist/table.cjs.map +1 -0
- package/dist/table.d.cts +12 -0
- package/dist/table.d.ts +12 -0
- package/dist/{index.js → table.js} +107 -127
- 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 +77 -7
- package/dist/index.d.cts +0 -2971
- package/dist/index.d.ts +0 -2971
package/README.md
CHANGED
|
@@ -1,107 +1,172 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# 🦖 dyno-table
|
|
4
|
+
|
|
5
|
+
### **Tame Your DynamoDB Data with Type-Safe Precision**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/dyno-table)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://aws.amazon.com/dynamodb/)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<p align="center"><strong>A powerful, type-safe abstraction layer for DynamoDB single-table designs</strong><br/>
|
|
15
|
+
<em>Write cleaner, safer, and more maintainable DynamoDB code</em></p>
|
|
5
16
|
|
|
6
17
|
<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
18
|
|
|
19
|
+
## 🔥 Why Developers Choose dyno-table
|
|
20
|
+
|
|
8
21
|
```ts
|
|
9
|
-
// Type-safe
|
|
10
|
-
await
|
|
22
|
+
// Type-safe dinosaur tracking operations made simple
|
|
23
|
+
await dinoTable
|
|
11
24
|
.update<Dinosaur>({
|
|
12
25
|
pk: 'SPECIES#trex',
|
|
13
26
|
sk: 'PROFILE#001'
|
|
14
27
|
})
|
|
15
|
-
.set('diet', 'Carnivore')
|
|
16
|
-
.add('sightings', 1)
|
|
17
|
-
.condition(op => op.eq('status', 'ACTIVE'))
|
|
28
|
+
.set('diet', 'Carnivore') // Update dietary classification
|
|
29
|
+
.add('sightings', 1) // Increment sighting counter
|
|
30
|
+
.condition(op => op.eq('status', 'ACTIVE')) // Only if dinosaur is active
|
|
18
31
|
.execute();
|
|
19
32
|
```
|
|
20
33
|
|
|
21
|
-
## 🌟 Why dyno-table
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
## 🌟 Why dyno-table Stands Out From The Pack
|
|
35
|
+
|
|
36
|
+
<table>
|
|
37
|
+
<tr>
|
|
38
|
+
<td width="50%">
|
|
39
|
+
<h3>🦕 Dinosaur-sized data made manageable</h3>
|
|
40
|
+
<p>Clean abstraction layer that simplifies complex DynamoDB patterns and makes single-table design approachable</p>
|
|
41
|
+
</td>
|
|
42
|
+
<td width="50%">
|
|
43
|
+
<h3>🛡️ Extinction-proof type safety</h3>
|
|
44
|
+
<p>Full TypeScript support with strict type checking that catches errors at compile time, not runtime</p>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<tr>
|
|
48
|
+
<td>
|
|
49
|
+
<h3>⚡ Velociraptor-fast API</h3>
|
|
50
|
+
<p>Intuitive chainable builder pattern for complex operations that feels natural and reduces boilerplate</p>
|
|
51
|
+
</td>
|
|
52
|
+
</tr>
|
|
53
|
+
<tr>
|
|
54
|
+
<td width="50%">
|
|
55
|
+
<h3>📈 Jurassic-scale performance</h3>
|
|
56
|
+
<p>Automatic batch chunking and pagination handling that scales with your data without extra code</p>
|
|
57
|
+
</td>
|
|
58
|
+
<td width="50%">
|
|
59
|
+
<h3>🧩 Flexible schema validation</h3>
|
|
60
|
+
<p>Works with your favorite validation libraries including Zod, ArkType, and Valibot</p>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
</table>
|
|
28
64
|
|
|
29
65
|
## 📑 Table of Contents
|
|
30
66
|
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- [
|
|
61
|
-
- [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- [
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
- [📦 Installation](#-installation)
|
|
68
|
+
- [🚀 Quick Start](#-quick-start)
|
|
69
|
+
- [1. Configure Your Jurassic Table](#1-configure-your-jurassic-table)
|
|
70
|
+
- [2. Perform Type-Safe Dinosaur Operations](#2-perform-type-safe-dinosaur-operations)
|
|
71
|
+
- [🏗️ Entity Pattern](#-entity-pattern-with-standard-schema-validators)
|
|
72
|
+
- [Defining Entities](#defining-entities)
|
|
73
|
+
- [Entity Features](#entity-features)
|
|
74
|
+
- [1. Schema Validation](#1-schema-validation)
|
|
75
|
+
- [2. CRUD Operations](#2-crud-operations)
|
|
76
|
+
- [3. Custom Queries](#3-custom-queries)
|
|
77
|
+
- [4. Defining GSI Access Patterns](#4-defining-gsi-access-patterns)
|
|
78
|
+
- [5. Lifecycle Hooks](#5-lifecycle-hooks)
|
|
79
|
+
- [Complete Entity Example](#complete-entity-example)
|
|
80
|
+
- [🧩 Advanced Features](#-advanced-features)
|
|
81
|
+
- [Transactional Operations](#transactional-operations)
|
|
82
|
+
- [Batch Processing](#batch-processing)
|
|
83
|
+
- [Pagination Made Simple](#pagination-made-simple)
|
|
84
|
+
- [🛡️ Type-Safe Query Building](#️-type-safe-query-building)
|
|
85
|
+
- [Comparison Operators](#comparison-operators)
|
|
86
|
+
- [Logical Operators](#logical-operators)
|
|
87
|
+
- [Query Operations](#query-operations)
|
|
88
|
+
- [Put Operations](#put-operations)
|
|
89
|
+
- [Update Operations](#update-operations)
|
|
90
|
+
- [Condition Operators](#condition-operators)
|
|
91
|
+
- [Multiple Operations](#multiple-operations)
|
|
92
|
+
- [🔄 Type Safety Features](#-type-safety-features)
|
|
93
|
+
- [Nested Object Support](#nested-object-support)
|
|
94
|
+
- [Type-Safe Conditions](#type-safe-conditions)
|
|
95
|
+
- [🔄 Batch Operations](#-batch-operations)
|
|
96
|
+
- [Batch Get](#batch-get)
|
|
97
|
+
- [Batch Write](#batch-write)
|
|
98
|
+
- [🔒 Transaction Operations](#-transaction-operations)
|
|
99
|
+
- [Transaction Builder](#transaction-builder)
|
|
100
|
+
- [Transaction Options](#transaction-options)
|
|
101
|
+
- [🚨 Error Handling](#-error-handling)
|
|
102
|
+
- [📚 API Reference](#-api-reference)
|
|
103
|
+
- [Condition Operators](#condition-operators-1)
|
|
104
|
+
- [Comparison Operators](#comparison-operators-1)
|
|
105
|
+
- [Attribute Operators](#attribute-operators)
|
|
106
|
+
- [Logical Operators](#logical-operators-1)
|
|
107
|
+
- [Key Condition Operators](#key-condition-operators)
|
|
108
|
+
- [🔮 Future Roadmap](#-future-roadmap)
|
|
109
|
+
- [🤝 Contributing](#-contributing)
|
|
110
|
+
- [🦔 Running Examples](#-running-examples)
|
|
70
111
|
|
|
71
112
|
## 📦 Installation
|
|
72
113
|
|
|
114
|
+
<div align="center">
|
|
115
|
+
|
|
116
|
+
### Get Started in Seconds
|
|
117
|
+
|
|
118
|
+
</div>
|
|
119
|
+
|
|
73
120
|
```bash
|
|
121
|
+
# Install the core library
|
|
74
122
|
npm install dyno-table
|
|
123
|
+
|
|
124
|
+
# Install required AWS SDK v3 peer dependencies
|
|
125
|
+
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
75
126
|
```
|
|
76
127
|
|
|
77
|
-
|
|
128
|
+
<details>
|
|
129
|
+
<summary><b>📋 Other Package Managers</b></summary>
|
|
78
130
|
|
|
79
131
|
```bash
|
|
80
|
-
|
|
132
|
+
# Using Yarn
|
|
133
|
+
yarn add dyno-table @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
134
|
+
|
|
135
|
+
# Using PNPM
|
|
136
|
+
pnpm add dyno-table @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
81
137
|
```
|
|
138
|
+
</details>
|
|
82
139
|
|
|
83
140
|
## 🚀 Quick Start
|
|
84
141
|
|
|
85
|
-
|
|
142
|
+
<div align="center">
|
|
143
|
+
|
|
144
|
+
### From Zero to DynamoDB Hero in Minutes
|
|
145
|
+
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
### 1. Configure Your Jurassic Table
|
|
149
|
+
|
|
150
|
+
> **Note:** dyno-table does not create or manage the actual DynamoDB table for you. We recommend using infrastructure as code tools like Terraform, OpenTofu, SST, or AWS CDK to provision and manage your DynamoDB tables.
|
|
86
151
|
|
|
87
152
|
```ts
|
|
88
153
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
89
154
|
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
90
|
-
import { Table } from "dyno-table";
|
|
155
|
+
import { Table } from "dyno-table/table";
|
|
91
156
|
|
|
92
157
|
// Configure AWS SDK clients
|
|
93
158
|
const client = new DynamoDBClient({ region: "us-west-2" });
|
|
94
159
|
const docClient = DynamoDBDocument.from(client);
|
|
95
160
|
|
|
96
|
-
//
|
|
161
|
+
// Initialise table
|
|
97
162
|
const dinoTable = new Table({
|
|
98
163
|
client: docClient,
|
|
99
|
-
tableName: "
|
|
164
|
+
tableName: "JurassicPark",
|
|
100
165
|
indexes: {
|
|
101
166
|
partitionKey: "pk",
|
|
102
167
|
sortKey: "sk",
|
|
103
168
|
gsis: {
|
|
104
|
-
|
|
169
|
+
gsi1: {
|
|
105
170
|
partitionKey: "gsi1pk",
|
|
106
171
|
sortKey: "gsi1sk",
|
|
107
172
|
},
|
|
@@ -110,10 +175,16 @@ const dinoTable = new Table({
|
|
|
110
175
|
});
|
|
111
176
|
```
|
|
112
177
|
|
|
113
|
-
### 2. Perform Type-Safe Operations
|
|
178
|
+
### 2. Perform Type-Safe Operations directly on the table instance
|
|
179
|
+
|
|
180
|
+
<table>
|
|
181
|
+
<tr>
|
|
182
|
+
<td>
|
|
183
|
+
|
|
184
|
+
#### 🦖 Creating a new dinosaur specimen
|
|
114
185
|
|
|
115
|
-
**🦖 Creating a new dinosaur**
|
|
116
186
|
```ts
|
|
187
|
+
// Add a new T-Rex with complete type safety
|
|
117
188
|
const rex = await dinoTable
|
|
118
189
|
.create<Dinosaur>({
|
|
119
190
|
pk: "SPECIES#trex",
|
|
@@ -127,8 +198,13 @@ const rex = await dinoTable
|
|
|
127
198
|
.execute();
|
|
128
199
|
```
|
|
129
200
|
|
|
130
|
-
|
|
201
|
+
</td>
|
|
202
|
+
<td>
|
|
203
|
+
|
|
204
|
+
#### 🔍 Query with powerful conditions
|
|
205
|
+
|
|
131
206
|
```ts
|
|
207
|
+
// Find large carnivorous dinosaurs
|
|
132
208
|
const largeDinos = await dinoTable
|
|
133
209
|
.query<Dinosaur>({
|
|
134
210
|
pk: "SPECIES#trex",
|
|
@@ -142,109 +218,599 @@ const largeDinos = await dinoTable
|
|
|
142
218
|
.execute();
|
|
143
219
|
```
|
|
144
220
|
|
|
145
|
-
|
|
221
|
+
</td>
|
|
222
|
+
</tr>
|
|
223
|
+
<tr>
|
|
224
|
+
<td>
|
|
225
|
+
|
|
226
|
+
#### 🔄 Update with type-safe operations
|
|
227
|
+
|
|
146
228
|
```ts
|
|
229
|
+
// Update a dinosaur's classification
|
|
147
230
|
await dinoTable
|
|
148
231
|
.update<Dinosaur>({
|
|
149
|
-
pk: "SPECIES#trex",
|
|
150
|
-
sk: "PROFILE#trex"
|
|
232
|
+
pk: "SPECIES#trex",
|
|
233
|
+
sk: "PROFILE#trex"
|
|
151
234
|
})
|
|
152
235
|
.set("diet", "omnivore")
|
|
153
236
|
.add("discoveryYear", 1)
|
|
154
237
|
.remove("outdatedField")
|
|
155
|
-
.condition((op) =>
|
|
238
|
+
.condition((op) =>
|
|
239
|
+
op.attributeExists("discoverySite")
|
|
240
|
+
)
|
|
241
|
+
.execute();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
</td>
|
|
245
|
+
<td>
|
|
246
|
+
|
|
247
|
+
#### 🔒 Transactional operations
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// Perform multiple operations atomically
|
|
251
|
+
await dinoTable.transaction(async (tx) => {
|
|
252
|
+
// Move dinosaur to new enclosure
|
|
253
|
+
await dinoTable
|
|
254
|
+
.delete({ pk: "ENCLOSURE#A", sk: "DINO#1" })
|
|
255
|
+
.withTransaction(tx);
|
|
256
|
+
|
|
257
|
+
await dinoTable
|
|
258
|
+
.create({ pk: "ENCLOSURE#B", sk: "DINO#1",
|
|
259
|
+
status: "ACTIVE" })
|
|
260
|
+
.withTransaction(tx);
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
</td>
|
|
265
|
+
</tr>
|
|
266
|
+
</table>
|
|
267
|
+
|
|
268
|
+
<div align="center">
|
|
269
|
+
<h3>💡 See the difference with dyno-table</h3>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<table>
|
|
273
|
+
<tr>
|
|
274
|
+
<th>With dyno-table</th>
|
|
275
|
+
<th>Without dyno-table</th>
|
|
276
|
+
</tr>
|
|
277
|
+
<tr>
|
|
278
|
+
<td>
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// Type-safe, clean, and intuitive
|
|
282
|
+
await dinoTable
|
|
283
|
+
.query<Dinosaur>({
|
|
284
|
+
pk: "SPECIES#trex"
|
|
285
|
+
})
|
|
286
|
+
.filter(op =>
|
|
287
|
+
op.contains("features", "feathers")
|
|
288
|
+
)
|
|
156
289
|
.execute();
|
|
157
290
|
```
|
|
158
291
|
|
|
292
|
+
</td>
|
|
293
|
+
<td>
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// Verbose, error-prone, no type safety
|
|
297
|
+
await docClient.send(new QueryCommand({
|
|
298
|
+
TableName: "JurassicPark",
|
|
299
|
+
KeyConditionExpression: "#pk = :pk",
|
|
300
|
+
FilterExpression: "contains(#features, :feathers)",
|
|
301
|
+
ExpressionAttributeNames: {
|
|
302
|
+
"#pk": "pk",
|
|
303
|
+
"#features": "features"
|
|
304
|
+
},
|
|
305
|
+
ExpressionAttributeValues: {
|
|
306
|
+
":pk": "SPECIES#trex",
|
|
307
|
+
":feathers": "feathers"
|
|
308
|
+
}
|
|
309
|
+
}));
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
</td>
|
|
313
|
+
</tr>
|
|
314
|
+
</table>
|
|
315
|
+
|
|
316
|
+
## 🏗️ Entity Pattern with Standard Schema validators
|
|
317
|
+
|
|
318
|
+
<div align="center">
|
|
319
|
+
|
|
320
|
+
### The Most Type-Safe Way to Model Your DynamoDB Data
|
|
321
|
+
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<table>
|
|
325
|
+
<tr>
|
|
326
|
+
<td width="70%">
|
|
327
|
+
<p>The entity pattern provides a structured, type-safe way to work with DynamoDB items. It combines schema validation, key management, and repository operations into a cohesive abstraction.</p>
|
|
328
|
+
|
|
329
|
+
<p>✨ This library supports all <a href="https://github.com/standard-schema/standard-schema#what-schema-libraries-implement-the-spec">Standard Schema</a> validation libraries, including <strong>zod</strong>, <strong>arktype</strong>, and <strong>valibot</strong>, allowing you to choose your preferred validation tool!</p>
|
|
330
|
+
|
|
331
|
+
<p>You can find a full example implementation here of <a href="https://github.com/Kysumi/dyno-table/blob/main/examples/entity-example/src/dinosaur-entity.ts">Entities</a></p>
|
|
332
|
+
</td>
|
|
333
|
+
<td width="30%">
|
|
334
|
+
|
|
335
|
+
#### Entity Pattern Benefits
|
|
336
|
+
|
|
337
|
+
- 🛡️ **Type-safe operations**
|
|
338
|
+
- 🧪 **Schema validation**
|
|
339
|
+
- 🔑 **Automatic key generation**
|
|
340
|
+
- 📦 **Repository pattern**
|
|
341
|
+
- 🔍 **Custom query builders**
|
|
342
|
+
- 🔄 **Lifecycle hooks**
|
|
343
|
+
|
|
344
|
+
</td>
|
|
345
|
+
</tr>
|
|
346
|
+
</table>
|
|
347
|
+
|
|
348
|
+
### Defining Entities
|
|
349
|
+
|
|
350
|
+
Entities are defined using the `defineEntity` function, which takes a configuration object that includes a schema, primary key definition, and optional indexes and queries.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { z } from "zod";
|
|
354
|
+
import { defineEntity, createIndex } from "dyno-table/entity";
|
|
355
|
+
|
|
356
|
+
// Define your schema using Zod
|
|
357
|
+
const dinosaurSchema = z.object({
|
|
358
|
+
id: z.string(),
|
|
359
|
+
species: z.string(),
|
|
360
|
+
name: z.string(),
|
|
361
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
362
|
+
dangerLevel: z.number().int().min(1).max(10),
|
|
363
|
+
height: z.number().positive(),
|
|
364
|
+
weight: z.number().positive(),
|
|
365
|
+
status: z.enum(["active", "inactive", "sick", "deceased"]),
|
|
366
|
+
createdAt: z.string().optional(),
|
|
367
|
+
updatedAt: z.string().optional(),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Infer the type from the schema
|
|
371
|
+
type Dinosaur = z.infer<typeof dinosaurSchema>;
|
|
372
|
+
|
|
373
|
+
// Define key templates for Dinosaur entity
|
|
374
|
+
const dinosaurPK = partitionKey`ENTITY#DINOSAUR#DIET#${"diet"}`;
|
|
375
|
+
const dinosaurSK = sortKey`ID#${"id"}#SPECIES#${"species"}`;
|
|
376
|
+
|
|
377
|
+
// Create a primary index for Dinosaur entity
|
|
378
|
+
const primaryKey = createIndex()
|
|
379
|
+
.input(z.object({ id: z.string(), diet: z.string(), species: z.string() }))
|
|
380
|
+
.partitionKey(({ diet }) => dinosaurPK({ diet }))
|
|
381
|
+
.sortKey(({ id, species }) => dinosaurSK({ species, id }));
|
|
382
|
+
|
|
383
|
+
// Define the entity
|
|
384
|
+
const DinosaurEntity = defineEntity({
|
|
385
|
+
name: "Dinosaur",
|
|
386
|
+
schema: dinosaurSchema,
|
|
387
|
+
primaryKey,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Create a repository
|
|
391
|
+
const dinosaurRepo = DinosaurEntity.createRepository(table);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Entity Features
|
|
395
|
+
|
|
396
|
+
#### 1. Schema Validation
|
|
397
|
+
|
|
398
|
+
Entities use Zod schemas to validate data before operations:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
// Define a schema with Zod
|
|
402
|
+
const dinosaurSchema = z.object({
|
|
403
|
+
id: z.string(),
|
|
404
|
+
species: z.string(),
|
|
405
|
+
name: z.string(),
|
|
406
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
407
|
+
dangerLevel: z.number().int().min(1).max(10),
|
|
408
|
+
height: z.number().positive(),
|
|
409
|
+
weight: z.number().positive(),
|
|
410
|
+
status: z.enum(["active", "inactive", "sick", "deceased"]),
|
|
411
|
+
tags: z.array(z.string()).optional(),
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Create an entity with the schema
|
|
415
|
+
const DinosaurEntity = defineEntity({
|
|
416
|
+
name: "Dinosaur",
|
|
417
|
+
schema: dinosaurSchema,
|
|
418
|
+
primaryKey: createIndex()
|
|
419
|
+
.input(z.object({ id: z.string(), diet: z.string(), species: z.string() }))
|
|
420
|
+
.partitionKey(({ diet }) => dinosaurPK({ diet }))
|
|
421
|
+
// could also be .withoutSortKey() if your table doesn't use sort keys
|
|
422
|
+
.sortKey(({ id, species }) => dinosaurSK({ species, id }))
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### 2. CRUD Operations
|
|
427
|
+
|
|
428
|
+
Entities provide type-safe CRUD operations:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
// Create a new dinosaur
|
|
432
|
+
await dinosaurRepo.create({
|
|
433
|
+
id: "dino-001",
|
|
434
|
+
species: "Tyrannosaurus Rex",
|
|
435
|
+
name: "Rexy",
|
|
436
|
+
diet: "carnivore",
|
|
437
|
+
dangerLevel: 10,
|
|
438
|
+
height: 5.2,
|
|
439
|
+
weight: 7000,
|
|
440
|
+
status: "active",
|
|
441
|
+
}).execute();
|
|
442
|
+
|
|
443
|
+
// Get a dinosaur
|
|
444
|
+
const dino = await dinosaurRepo.get({
|
|
445
|
+
id: "dino-001",
|
|
446
|
+
diet: "carnivore",
|
|
447
|
+
species: "Tyrannosaurus Rex",
|
|
448
|
+
}).execute();
|
|
449
|
+
|
|
450
|
+
// Update a dinosaur
|
|
451
|
+
await dinosaurRepo.update(
|
|
452
|
+
{ id: "dino-001", diet: "carnivore", species: "Tyrannosaurus Rex" },
|
|
453
|
+
{ weight: 7200, status: "sick" }
|
|
454
|
+
).execute();
|
|
455
|
+
|
|
456
|
+
// Delete a dinosaur
|
|
457
|
+
await dinosaurRepo.delete({
|
|
458
|
+
id: "dino-001",
|
|
459
|
+
diet: "carnivore",
|
|
460
|
+
species: "Tyrannosaurus Rex",
|
|
461
|
+
}).execute();
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
#### 3. Custom Queries
|
|
465
|
+
|
|
466
|
+
Define custom queries with input validation:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import { createQueries } from "dyno-table/entity";
|
|
470
|
+
|
|
471
|
+
const createQuery = createQueries<Dinosaur>();
|
|
472
|
+
|
|
473
|
+
const DinosaurEntity = defineEntity({
|
|
474
|
+
name: "Dinosaur",
|
|
475
|
+
schema: dinosaurSchema,
|
|
476
|
+
primaryKey,
|
|
477
|
+
queries: {
|
|
478
|
+
byDiet: createQuery
|
|
479
|
+
.input(
|
|
480
|
+
z.object({
|
|
481
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
482
|
+
})
|
|
483
|
+
)
|
|
484
|
+
.query(({ input, entity }) => {
|
|
485
|
+
return entity
|
|
486
|
+
.query({
|
|
487
|
+
pk: dinosaurPK({diet: input.diet})
|
|
488
|
+
});
|
|
489
|
+
}),
|
|
490
|
+
|
|
491
|
+
bySpecies: createQuery
|
|
492
|
+
.input(
|
|
493
|
+
z.object({
|
|
494
|
+
species: z.string(),
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
.query(({ input, entity }) => {
|
|
498
|
+
return entity
|
|
499
|
+
.scan()
|
|
500
|
+
.filter((op) => op.eq("species", input.species));
|
|
501
|
+
}),
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Use the custom queries
|
|
506
|
+
const carnivores = await dinosaurRepo.query.byDiet({ diet: "carnivore" }).execute();
|
|
507
|
+
const trexes = await dinosaurRepo.query.bySpecies({ species: "Tyrannosaurus Rex" }).execute();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### 4. Defining GSI access patterns
|
|
511
|
+
|
|
512
|
+
Define GSI (LSI support coming later)
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
import { createIndex } from "dyno-table/entity";
|
|
516
|
+
|
|
517
|
+
// Define GSIs templates for querying by species
|
|
518
|
+
const gsi1PK = partitionKey`SPECIES#${"species"}`
|
|
519
|
+
const gsi1SK = sortKey`DINOSAUR#${"id"}`
|
|
520
|
+
|
|
521
|
+
// Implement typesafe generator for the GSI - This is used in create calls to ensure the GSI is generated
|
|
522
|
+
const speciesIndex = createIndex()
|
|
523
|
+
.input(dinosaurSchema)
|
|
524
|
+
.partitionKey(({ species }) => gsi1PK({ species }))
|
|
525
|
+
.sortKey(({ id }) => gsi1SK({ id }));
|
|
526
|
+
|
|
527
|
+
const DinosaurEntity = defineEntity({
|
|
528
|
+
name: "Dinosaur",
|
|
529
|
+
schema: dinosaurSchema,
|
|
530
|
+
primaryKey,
|
|
531
|
+
indexes: {
|
|
532
|
+
species: speciesIndex,
|
|
533
|
+
},
|
|
534
|
+
queries: {
|
|
535
|
+
bySpecies: createQuery
|
|
536
|
+
.input(
|
|
537
|
+
z.object({
|
|
538
|
+
species: z.string(),
|
|
539
|
+
})
|
|
540
|
+
)
|
|
541
|
+
.query(({ input, entity }) => {
|
|
542
|
+
return entity
|
|
543
|
+
.query({
|
|
544
|
+
// Use the GSI template generator to avoid typos
|
|
545
|
+
pk: gsi1PK({species: input.species}),
|
|
546
|
+
})
|
|
547
|
+
// Use the template name as defined in the table instance
|
|
548
|
+
.useIndex("gsi1");
|
|
549
|
+
}),
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Complete Entity Example
|
|
555
|
+
|
|
556
|
+
Here's a complete example of using Zod schemas directly:
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
import { z } from "zod";
|
|
560
|
+
import { defineEntity, createQueries, createIndex } from "dyno-table/entity";
|
|
561
|
+
import { Table } from "dyno-table/table";
|
|
562
|
+
import { sortKey } from "dyno-table/utils/sort-key-template";
|
|
563
|
+
import { partitionKey } from "dyno-table/utils/partition-key-template";
|
|
564
|
+
|
|
565
|
+
// Define the schema with Zod
|
|
566
|
+
const dinosaurSchema = z.object({
|
|
567
|
+
id: z.string(),
|
|
568
|
+
species: z.string(),
|
|
569
|
+
name: z.string(),
|
|
570
|
+
enclosureId: z.string(),
|
|
571
|
+
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
572
|
+
dangerLevel: z.number().int().min(1).max(10),
|
|
573
|
+
height: z.number().positive(),
|
|
574
|
+
weight: z.number().positive(),
|
|
575
|
+
status: z.enum(["active", "inactive", "sick", "deceased"]),
|
|
576
|
+
trackingChipId: z.string().optional(),
|
|
577
|
+
lastFed: z.string().optional(),
|
|
578
|
+
createdAt: z.string().optional(),
|
|
579
|
+
updatedAt: z.string().optional(),
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Infer the type from the schema
|
|
583
|
+
type Dinosaur = z.infer<typeof dinosaurSchema>;
|
|
584
|
+
|
|
585
|
+
// Define key templates
|
|
586
|
+
const dinosaurPK = partitionKey`DINOSAUR#${"id"}`;
|
|
587
|
+
const dinosaurSK = sortKey`STATUS#${"status"}`;
|
|
588
|
+
|
|
589
|
+
const gsi1PK = partitionKey`SPECIES#${"species"}`
|
|
590
|
+
const gsi1SK = sortKey`DINOSAUR#${"id"}`
|
|
591
|
+
|
|
592
|
+
const gsi2PK = partitionKey`ENCLOSURE#${"enclosureId"}`
|
|
593
|
+
const gsi2SK = sortKey`DINOSAUR#${"id"}`
|
|
594
|
+
|
|
595
|
+
// Create a primary index
|
|
596
|
+
const primaryKey = createIndex()
|
|
597
|
+
.input(dinosaurSchema)
|
|
598
|
+
.partitionKey(({ id }) => dinosaurPK(id))
|
|
599
|
+
.sortKey(({ status }) => dinosaurSK(status));
|
|
600
|
+
|
|
601
|
+
// Create a GSI for querying by species
|
|
602
|
+
const speciesIndex = createIndex()
|
|
603
|
+
.input(dinosaurSchema)
|
|
604
|
+
.partitionKey(({ species }) => gsi1PK({ species }))
|
|
605
|
+
.sortKey(({ id }) => gsiSK({ id }));
|
|
606
|
+
|
|
607
|
+
// Create a GSI for querying by enclosure
|
|
608
|
+
const enclosureIndex = createIndex()
|
|
609
|
+
.input(dinosaurSchema)
|
|
610
|
+
.partitionKey(({ enclosureId }) => gsi2PK({ enclosureId }))
|
|
611
|
+
.sortKey(({ id }) => gsi2SK({ id }));
|
|
612
|
+
|
|
613
|
+
// Create query builders
|
|
614
|
+
const createQuery = createQueries<Dinosaur>();
|
|
615
|
+
|
|
616
|
+
// Define the entity
|
|
617
|
+
const DinosaurEntity = defineEntity({
|
|
618
|
+
name: "Dinosaur",
|
|
619
|
+
schema: dinosaurSchema,
|
|
620
|
+
primaryKey,
|
|
621
|
+
indexes: {
|
|
622
|
+
// These keys need to be named after the name of the GSI that is defined in your table instance
|
|
623
|
+
gsi1: speciesIndex,
|
|
624
|
+
gsi2: enclosureIndex,
|
|
625
|
+
},
|
|
626
|
+
queries: {
|
|
627
|
+
bySpecies: createQuery
|
|
628
|
+
.input(
|
|
629
|
+
z.object({
|
|
630
|
+
species: z.string(),
|
|
631
|
+
})
|
|
632
|
+
)
|
|
633
|
+
.query(({ input, entity }) => {
|
|
634
|
+
return entity
|
|
635
|
+
.query({
|
|
636
|
+
pk: gsi1PK({ species: input.species }),
|
|
637
|
+
})
|
|
638
|
+
.useIndex("gsi1");
|
|
639
|
+
}),
|
|
640
|
+
|
|
641
|
+
byEnclosure: createQuery
|
|
642
|
+
.input(
|
|
643
|
+
z.object({
|
|
644
|
+
enclosureId: z.string(),
|
|
645
|
+
})
|
|
646
|
+
)
|
|
647
|
+
.query(({ input, entity }) => {
|
|
648
|
+
return entity
|
|
649
|
+
.query({
|
|
650
|
+
pk: gsi2PK({ enclosureId: input.enclosureId }),
|
|
651
|
+
})
|
|
652
|
+
.useIndex("gsi2");
|
|
653
|
+
}),
|
|
654
|
+
|
|
655
|
+
dangerousInEnclosure: createQuery
|
|
656
|
+
.input(
|
|
657
|
+
z.object({
|
|
658
|
+
enclosureId: z.string(),
|
|
659
|
+
minDangerLevel: z.number().int().min(1).max(10),
|
|
660
|
+
})
|
|
661
|
+
)
|
|
662
|
+
.query(({ input, entity }) => {
|
|
663
|
+
return entity
|
|
664
|
+
.query({
|
|
665
|
+
pk: gsi2PK({ enclosureId: input.enclosureId }),
|
|
666
|
+
})
|
|
667
|
+
.useIndex("gsi2")
|
|
668
|
+
.filter((op) => op.gte("dangerLevel", input.minDangerLevel));
|
|
669
|
+
}),
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Create a repository
|
|
674
|
+
const dinosaurRepo = DinosaurEntity.createRepository(table);
|
|
675
|
+
|
|
676
|
+
// Use the repository
|
|
677
|
+
async function main() {
|
|
678
|
+
// Create a dinosaur
|
|
679
|
+
await dinosaurRepo
|
|
680
|
+
.create({
|
|
681
|
+
id: "dino-001",
|
|
682
|
+
species: "Tyrannosaurus Rex",
|
|
683
|
+
name: "Rexy",
|
|
684
|
+
enclosureId: "enc-001",
|
|
685
|
+
diet: "carnivore",
|
|
686
|
+
dangerLevel: 10,
|
|
687
|
+
height: 5.2,
|
|
688
|
+
weight: 7000,
|
|
689
|
+
status: "active",
|
|
690
|
+
trackingChipId: "TRX-001",
|
|
691
|
+
})
|
|
692
|
+
.execute();
|
|
693
|
+
|
|
694
|
+
// Query dinosaurs by species
|
|
695
|
+
const trexes = await dinosaurRepo.query.bySpecies({
|
|
696
|
+
species: "Tyrannosaurus Rex"
|
|
697
|
+
}).execute();
|
|
698
|
+
|
|
699
|
+
// Query dangerous dinosaurs in an enclosure
|
|
700
|
+
const dangerousDinos = await dinosaurRepo.query.dangerousInEnclosure({
|
|
701
|
+
enclosureId: "enc-001",
|
|
702
|
+
minDangerLevel: 8,
|
|
703
|
+
}).execute();
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Key benefits:**
|
|
708
|
+
- 🚫 Prevents accidental cross-type data access
|
|
709
|
+
- 🔍 Automatically filters queries/scans to a repository type
|
|
710
|
+
- 🛡️ Ensures consistent key structure across entities
|
|
711
|
+
- 📦 Encapsulates domain-specific query logic
|
|
712
|
+
- 🧪 Validates data with Zod schemas
|
|
713
|
+
- 🔄 Provides type inference from schemas
|
|
714
|
+
|
|
159
715
|
## 🧩 Advanced Features
|
|
160
716
|
|
|
161
717
|
### Transactional Operations
|
|
162
718
|
|
|
163
719
|
**Safe dinosaur transfer between enclosures**
|
|
164
720
|
```ts
|
|
165
|
-
// Start a transaction session for transferring a
|
|
721
|
+
// Start a transaction session for transferring a T-Rex to a new enclosure
|
|
722
|
+
// Critical for safety: All operations must succeed or none will be applied
|
|
166
723
|
await dinoTable.transaction(async (tx) => {
|
|
167
724
|
// All operations are executed as a single transaction (up to 100 operations)
|
|
725
|
+
// This ensures the dinosaur transfer is atomic - preventing half-completed transfers
|
|
168
726
|
|
|
169
|
-
// Check if destination enclosure is ready and compatible
|
|
727
|
+
// STEP 1: Check if destination enclosure is ready and compatible with the dinosaur
|
|
728
|
+
// We must verify the enclosure is prepared and suitable for a carnivore
|
|
170
729
|
await dinoTable
|
|
171
730
|
.conditionCheck({
|
|
172
|
-
pk: "ENCLOSURE#B",
|
|
173
|
-
sk: "STATUS"
|
|
731
|
+
pk: "ENCLOSURE#B", // Target enclosure B
|
|
732
|
+
sk: "STATUS" // Check the enclosure status record
|
|
174
733
|
})
|
|
175
734
|
.condition(op => op.and(
|
|
176
|
-
op.eq("status", "READY"),
|
|
177
|
-
op.eq("diet", "Carnivore") //
|
|
735
|
+
op.eq("status", "READY"), // Enclosure must be in READY state
|
|
736
|
+
op.eq("diet", "Carnivore") // Must support carnivorous dinosaurs
|
|
178
737
|
))
|
|
179
738
|
.withTransaction(tx);
|
|
180
739
|
|
|
181
|
-
// Remove dinosaur from current enclosure
|
|
740
|
+
// STEP 2: Remove dinosaur from current enclosure
|
|
741
|
+
// Only proceed if the dinosaur is healthy enough for transfer
|
|
182
742
|
await dinoTable
|
|
183
743
|
.delete<Dinosaur>({
|
|
184
|
-
pk: "ENCLOSURE#A",
|
|
185
|
-
sk: "DINO#001"
|
|
744
|
+
pk: "ENCLOSURE#A", // Source enclosure A
|
|
745
|
+
sk: "DINO#001" // T-Rex with ID 001
|
|
186
746
|
})
|
|
187
747
|
.condition(op => op.and(
|
|
188
|
-
op.eq("status", "HEALTHY"),
|
|
189
|
-
op.gte("health", 80)
|
|
748
|
+
op.eq("status", "HEALTHY"), // Dinosaur must be in HEALTHY state
|
|
749
|
+
op.gte("health", 80) // Health must be at least 80%
|
|
190
750
|
))
|
|
191
751
|
.withTransaction(tx);
|
|
192
752
|
|
|
193
|
-
// Add dinosaur to new enclosure
|
|
753
|
+
// STEP 3: Add dinosaur to new enclosure
|
|
754
|
+
// Create a fresh record in the destination enclosure
|
|
194
755
|
await dinoTable
|
|
195
756
|
.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()
|
|
757
|
+
pk: "ENCLOSURE#B", // Destination enclosure B
|
|
758
|
+
sk: "DINO#001", // Same dinosaur ID for tracking
|
|
759
|
+
name: "Rex", // Dinosaur name
|
|
760
|
+
species: "Tyrannosaurus", // Species classification
|
|
761
|
+
diet: "Carnivore", // Dietary requirements
|
|
762
|
+
status: "HEALTHY", // Current health status
|
|
763
|
+
health: 100, // Reset health to 100% after transfer
|
|
764
|
+
enclosureId: "B", // Update enclosure reference
|
|
765
|
+
lastFed: new Date().toISOString() // Reset feeding clock
|
|
205
766
|
})
|
|
206
767
|
.withTransaction(tx);
|
|
207
768
|
|
|
208
|
-
// Update enclosure occupancy tracking
|
|
769
|
+
// STEP 4: Update enclosure occupancy tracking
|
|
770
|
+
// Keep accurate count of dinosaurs in each enclosure
|
|
209
771
|
await dinoTable
|
|
210
772
|
.update<Dinosaur>({
|
|
211
|
-
pk: "ENCLOSURE#B",
|
|
212
|
-
sk: "OCCUPANCY"
|
|
773
|
+
pk: "ENCLOSURE#B", // Target enclosure B
|
|
774
|
+
sk: "OCCUPANCY" // Occupancy tracking record
|
|
213
775
|
})
|
|
214
|
-
.add("currentOccupants", 1)
|
|
215
|
-
.set("lastUpdated", new Date().toISOString())
|
|
776
|
+
.add("currentOccupants", 1) // Increment occupant count
|
|
777
|
+
.set("lastUpdated", new Date().toISOString()) // Update timestamp
|
|
216
778
|
.withTransaction(tx);
|
|
217
779
|
});
|
|
218
780
|
|
|
219
|
-
// Transaction
|
|
781
|
+
// Transaction for dinosaur feeding and health monitoring
|
|
782
|
+
// Ensures feeding status and schedule are updated atomically
|
|
220
783
|
await dinoTable.transaction(
|
|
221
784
|
async (tx) => {
|
|
222
|
-
// Update
|
|
785
|
+
// STEP 1: Update Stegosaurus health and feeding status
|
|
786
|
+
// Record that the dinosaur has been fed and update its health metrics
|
|
223
787
|
await dinoTable
|
|
224
788
|
.update<Dinosaur>({
|
|
225
|
-
pk: "ENCLOSURE#D",
|
|
226
|
-
sk: "DINO#003"
|
|
789
|
+
pk: "ENCLOSURE#D", // Herbivore enclosure D
|
|
790
|
+
sk: "DINO#003" // Stegosaurus with ID 003
|
|
227
791
|
})
|
|
228
792
|
.set({
|
|
229
|
-
status: "HEALTHY",
|
|
230
|
-
lastFed: new Date().toISOString(),
|
|
231
|
-
health: 100
|
|
793
|
+
status: "HEALTHY", // Update health status
|
|
794
|
+
lastFed: new Date().toISOString(), // Record feeding time
|
|
795
|
+
health: 100 // Reset health to 100%
|
|
232
796
|
})
|
|
233
|
-
.deleteElementsFromSet("tags", ["needs_feeding"])
|
|
797
|
+
.deleteElementsFromSet("tags", ["needs_feeding"]) // Remove feeding alert tag
|
|
234
798
|
.withTransaction(tx);
|
|
235
799
|
|
|
236
|
-
// Update enclosure feeding schedule
|
|
800
|
+
// STEP 2: Update enclosure feeding schedule
|
|
801
|
+
// Schedule next feeding time for tomorrow
|
|
237
802
|
await dinoTable
|
|
238
803
|
.update<Dinosaur>({
|
|
239
|
-
pk: "ENCLOSURE#D",
|
|
240
|
-
sk: "SCHEDULE"
|
|
804
|
+
pk: "ENCLOSURE#D", // Same herbivore enclosure
|
|
805
|
+
sk: "SCHEDULE" // Feeding schedule record
|
|
241
806
|
})
|
|
242
|
-
.set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString())
|
|
807
|
+
.set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()) // 24 hours from now
|
|
243
808
|
.withTransaction(tx);
|
|
244
809
|
},
|
|
245
810
|
{
|
|
246
|
-
|
|
247
|
-
|
|
811
|
+
// Transaction options for tracking and idempotency
|
|
812
|
+
clientRequestToken: "feeding-session-001", // Prevents duplicate feeding operations
|
|
813
|
+
returnConsumedCapacity: "TOTAL" // Track capacity usage for park operations
|
|
248
814
|
}
|
|
249
815
|
);
|
|
250
816
|
```
|
|
@@ -253,7 +819,6 @@ await dinoTable.transaction(
|
|
|
253
819
|
- 🔄 Uses the same familiar API as non-transactional operations
|
|
254
820
|
- 🧠 Maintains consistent mental model for developers
|
|
255
821
|
- 🔒 All operations within the callback are executed as a single transaction
|
|
256
|
-
- ✅ All-or-nothing operations (ACID compliance)
|
|
257
822
|
- 🛡️ Prevents race conditions and data inconsistencies
|
|
258
823
|
- 📊 Supports up to 100 actions per transaction
|
|
259
824
|
|
|
@@ -261,35 +826,42 @@ await dinoTable.transaction(
|
|
|
261
826
|
|
|
262
827
|
**Efficient dinosaur park management with bulk operations**
|
|
263
828
|
```ts
|
|
264
|
-
//
|
|
829
|
+
// SCENARIO 1: Morning health check for multiple dinosaurs across enclosures
|
|
830
|
+
// Retrieve health status for multiple dinosaurs in a single operation
|
|
265
831
|
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
|
|
832
|
+
{ pk: "ENCLOSURE#A", sk: "DINO#001" }, // T-Rex in Paddock A
|
|
833
|
+
{ pk: "ENCLOSURE#B", sk: "DINO#002" }, // Velociraptor in Paddock B
|
|
834
|
+
{ pk: "ENCLOSURE#C", sk: "DINO#003" } // Stegosaurus in Paddock C
|
|
269
835
|
];
|
|
270
836
|
|
|
837
|
+
// Perform batch get operation to retrieve all dinosaurs at once
|
|
838
|
+
// This is much more efficient than individual gets
|
|
271
839
|
const { items: dinosaurs, unprocessedKeys } = await dinoTable.batchGet<Dinosaur>(healthCheckKeys);
|
|
272
840
|
console.log(`Health check completed for ${dinosaurs.length} dinosaurs`);
|
|
841
|
+
|
|
842
|
+
// Process health check results and identify any dinosaurs needing attention
|
|
273
843
|
dinosaurs.forEach(dino => {
|
|
274
844
|
if (dino.health < 80) {
|
|
275
845
|
console.log(`Health alert for ${dino.name} in Enclosure ${dino.enclosureId}`);
|
|
846
|
+
// In a real application, you might trigger alerts or schedule veterinary visits
|
|
276
847
|
}
|
|
277
848
|
});
|
|
278
849
|
|
|
279
|
-
//
|
|
850
|
+
// SCENARIO 2: Adding new herbivores to the park after quarantine
|
|
851
|
+
// Prepare data for multiple new herbivores joining the collection
|
|
280
852
|
const newHerbivores = [
|
|
281
853
|
{
|
|
282
854
|
pk: "ENCLOSURE#D", sk: "DINO#004",
|
|
283
|
-
name: "Triceratops Alpha",
|
|
855
|
+
name: "Triceratops Alpha", // Three-horned herbivore
|
|
284
856
|
species: "Triceratops",
|
|
285
857
|
diet: "Herbivore",
|
|
286
858
|
status: "HEALTHY",
|
|
287
|
-
health: 95,
|
|
288
|
-
lastFed: new Date().toISOString()
|
|
859
|
+
health: 95, // Excellent health after quarantine
|
|
860
|
+
lastFed: new Date().toISOString() // Just fed before joining main enclosure
|
|
289
861
|
},
|
|
290
862
|
{
|
|
291
863
|
pk: "ENCLOSURE#D", sk: "DINO#005",
|
|
292
|
-
name: "Brachy",
|
|
864
|
+
name: "Brachy", // Long-necked herbivore
|
|
293
865
|
species: "Brachiosaurus",
|
|
294
866
|
diet: "Herbivore",
|
|
295
867
|
status: "HEALTHY",
|
|
@@ -298,91 +870,117 @@ const newHerbivores = [
|
|
|
298
870
|
}
|
|
299
871
|
];
|
|
300
872
|
|
|
301
|
-
// Add new herbivores to enclosure
|
|
873
|
+
// Add all new herbivores to the enclosure in a single batch operation
|
|
874
|
+
// More efficient than individual writes and ensures consistent state
|
|
302
875
|
await dinoTable.batchWrite(
|
|
303
876
|
newHerbivores.map(dino => ({
|
|
304
|
-
type: "put",
|
|
305
|
-
item: dino
|
|
877
|
+
type: "put", // Create or replace operation
|
|
878
|
+
item: dino // Full dinosaur record
|
|
306
879
|
}))
|
|
307
880
|
);
|
|
308
881
|
|
|
309
|
-
//
|
|
882
|
+
// SCENARIO 3: Releasing a dinosaur from quarantine to general population
|
|
883
|
+
// Multiple related operations performed as a batch
|
|
310
884
|
await dinoTable.batchWrite([
|
|
311
|
-
// Remove dinosaur from quarantine
|
|
312
|
-
{
|
|
313
|
-
|
|
885
|
+
// Step 1: Remove dinosaur from quarantine enclosure
|
|
886
|
+
{
|
|
887
|
+
type: "delete",
|
|
888
|
+
key: { pk: "ENCLOSURE#QUARANTINE", sk: "DINO#006" }
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
// Step 2: Add recovered dinosaur to main raptor enclosure
|
|
314
892
|
{
|
|
315
893
|
type: "put",
|
|
316
894
|
item: {
|
|
317
895
|
pk: "ENCLOSURE#E", sk: "DINO#006",
|
|
318
|
-
name: "Raptor Beta",
|
|
896
|
+
name: "Raptor Beta", // Juvenile Velociraptor
|
|
319
897
|
species: "Velociraptor",
|
|
320
898
|
diet: "Carnivore",
|
|
321
|
-
status: "HEALTHY",
|
|
899
|
+
status: "HEALTHY", // Now healthy after treatment
|
|
322
900
|
health: 100,
|
|
323
901
|
lastFed: new Date().toISOString()
|
|
324
902
|
}
|
|
325
903
|
},
|
|
326
|
-
|
|
327
|
-
|
|
904
|
+
|
|
905
|
+
// Step 3: Clear quarantine status record
|
|
906
|
+
{
|
|
907
|
+
type: "delete",
|
|
908
|
+
key: { pk: "ENCLOSURE#QUARANTINE", sk: "STATUS#DINO#006" }
|
|
909
|
+
}
|
|
328
910
|
]);
|
|
329
911
|
|
|
330
|
-
//
|
|
331
|
-
//
|
|
912
|
+
// SCENARIO 4: Daily park-wide health monitoring
|
|
913
|
+
// Handle large-scale operations across all dinosaurs
|
|
914
|
+
// The library automatically handles chunking for large batches:
|
|
915
|
+
// - 25 items per batch write
|
|
916
|
+
// - 100 items per batch get
|
|
332
917
|
const dailyHealthUpdates = generateDinosaurHealthUpdates(); // Hundreds of updates
|
|
333
|
-
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked
|
|
918
|
+
await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked into multiple requests
|
|
334
919
|
```
|
|
335
920
|
|
|
336
921
|
### Pagination Made Simple
|
|
337
922
|
|
|
338
|
-
**Efficient dinosaur record browsing**
|
|
923
|
+
**Efficient dinosaur record browsing for park management**
|
|
339
924
|
```ts
|
|
340
|
-
//
|
|
925
|
+
// SCENARIO 1: Herbivore health monitoring with pagination
|
|
926
|
+
// Create a paginator for viewing healthy herbivores in manageable chunks
|
|
927
|
+
// Perfect for veterinary staff doing routine health checks
|
|
341
928
|
const healthyHerbivores = dinoTable
|
|
342
929
|
.query<Dinosaur>({
|
|
343
|
-
pk: "DIET#herbivore",
|
|
344
|
-
sk: op => op.beginsWith("STATUS#HEALTHY")
|
|
930
|
+
pk: "DIET#herbivore", // Target all herbivorous dinosaurs
|
|
931
|
+
sk: op => op.beginsWith("STATUS#HEALTHY") // Only those with HEALTHY status
|
|
345
932
|
})
|
|
346
933
|
.filter((op) => op.and(
|
|
347
|
-
op.gte("health", 90),
|
|
348
|
-
op.attributeExists("lastFed")
|
|
934
|
+
op.gte("health", 90), // Only those with excellent health (90%+)
|
|
935
|
+
op.attributeExists("lastFed") // Must have feeding records
|
|
349
936
|
))
|
|
350
|
-
.paginate(5);
|
|
937
|
+
.paginate(5); // Process in small batches of 5 dinosaurs
|
|
351
938
|
|
|
352
|
-
//
|
|
939
|
+
// Iterate through all pages of results - useful for processing large datasets
|
|
940
|
+
// without loading everything into memory at once
|
|
941
|
+
console.log("🦕 Beginning herbivore health inspection rounds...");
|
|
353
942
|
while (healthyHerbivores.hasNextPage()) {
|
|
943
|
+
// Get the next page of dinosaurs
|
|
354
944
|
const page = await healthyHerbivores.getNextPage();
|
|
355
945
|
console.log(`Checking herbivores page ${page.page}, found ${page.items.length} dinosaurs`);
|
|
946
|
+
|
|
947
|
+
// Process each dinosaur in the current page
|
|
356
948
|
page.items.forEach(dino => {
|
|
357
949
|
console.log(`${dino.name}: Health ${dino.health}%, Last fed: ${dino.lastFed}`);
|
|
950
|
+
// In a real app, you might update health records or schedule next checkup
|
|
358
951
|
});
|
|
359
952
|
}
|
|
360
953
|
|
|
361
|
-
//
|
|
954
|
+
// SCENARIO 2: Preparing carnivore feeding schedule
|
|
955
|
+
// Get all carnivores at once for daily feeding planning
|
|
956
|
+
// This approach loads all matching items into memory
|
|
362
957
|
const carnivoreSchedule = await dinoTable
|
|
363
958
|
.query<Dinosaur>({
|
|
364
|
-
pk: "DIET#carnivore",
|
|
365
|
-
sk: op => op.beginsWith("ENCLOSURE#")
|
|
959
|
+
pk: "DIET#carnivore", // Target all carnivorous dinosaurs
|
|
960
|
+
sk: op => op.beginsWith("ENCLOSURE#") // Organized by enclosure
|
|
366
961
|
})
|
|
367
|
-
.filter(op => op.attributeExists("lastFed"))
|
|
368
|
-
.paginate(10)
|
|
369
|
-
.getAllPages();
|
|
962
|
+
.filter(op => op.attributeExists("lastFed")) // Only those with feeding records
|
|
963
|
+
.paginate(10) // Process in pages of 10
|
|
964
|
+
.getAllPages(); // But collect all results at once
|
|
370
965
|
|
|
371
966
|
console.log(`Scheduling feeding for ${carnivoreSchedule.length} carnivores`);
|
|
967
|
+
// Now we can sort and organize feeding times based on species, size, etc.
|
|
372
968
|
|
|
373
|
-
//
|
|
969
|
+
// SCENARIO 3: Visitor information kiosk with limited display
|
|
970
|
+
// Create a paginated view for the public-facing dinosaur information kiosk
|
|
374
971
|
const visitorKiosk = dinoTable
|
|
375
972
|
.query<Dinosaur>({
|
|
376
|
-
pk: "VISITOR_VIEW",
|
|
377
|
-
sk: op => op.beginsWith("SPECIES#")
|
|
973
|
+
pk: "VISITOR_VIEW", // Special partition for visitor-facing data
|
|
974
|
+
sk: op => op.beginsWith("SPECIES#") // Organized by species
|
|
378
975
|
})
|
|
379
|
-
.filter(op => op.eq("status", "ON_DISPLAY"))
|
|
380
|
-
.limit(12)
|
|
381
|
-
.paginate(4);
|
|
976
|
+
.filter(op => op.eq("status", "ON_DISPLAY")) // Only show dinosaurs currently on display
|
|
977
|
+
.limit(12) // Show maximum 12 dinosaurs total
|
|
978
|
+
.paginate(4); // Display 4 at a time for easy viewing
|
|
382
979
|
|
|
383
|
-
// Get first page for kiosk display
|
|
980
|
+
// Get first page for initial kiosk display
|
|
384
981
|
const firstPage = await visitorKiosk.getNextPage();
|
|
385
|
-
console.log(
|
|
982
|
+
console.log(`🦖 Now showing: ${firstPage.items.map(d => d.name).join(", ")}`);
|
|
983
|
+
// Visitors can press "Next" to see more dinosaurs in the collection
|
|
386
984
|
```
|
|
387
985
|
|
|
388
986
|
## 🛡️ Type-Safe Query Building
|
|
@@ -670,24 +1268,6 @@ const result = await table.transaction(
|
|
|
670
1268
|
);
|
|
671
1269
|
```
|
|
672
1270
|
|
|
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
1271
|
|
|
692
1272
|
## 🚨 Error Handling
|
|
693
1273
|
|