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