dyno-table 1.4.0 → 1.6.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 +324 -55
- package/dist/builders/condition-check-builder.d.cts +1 -1
- package/dist/builders/condition-check-builder.d.ts +1 -1
- package/dist/builders/delete-builder.d.cts +1 -1
- package/dist/builders/delete-builder.d.ts +1 -1
- package/dist/builders/put-builder.d.cts +1 -1
- package/dist/builders/put-builder.d.ts +1 -1
- package/dist/builders/query-builder.d.cts +2 -2
- package/dist/builders/query-builder.d.ts +2 -2
- package/dist/builders/transaction-builder.d.cts +1 -1
- package/dist/builders/transaction-builder.d.ts +1 -1
- package/dist/builders/update-builder.d.cts +1 -1
- package/dist/builders/update-builder.d.ts +1 -1
- package/dist/{conditions-x6kGWMR7.d.cts → conditions-3ae5znV_.d.cts} +1 -1
- package/dist/{conditions-BIpBkh4m.d.ts → conditions-BtynAviC.d.ts} +1 -1
- package/dist/conditions.d.cts +1 -1
- package/dist/conditions.d.ts +1 -1
- package/dist/entity.cjs +93 -49
- package/dist/entity.cjs.map +1 -1
- package/dist/entity.d.cts +14 -10
- package/dist/entity.d.ts +14 -10
- package/dist/entity.js +93 -49
- package/dist/entity.js.map +1 -1
- package/dist/index.cjs +3701 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +3674 -0
- package/dist/index.js.map +1 -0
- package/dist/{query-builder-H9Dn0qaS.d.ts → query-builder-BhrR31oO.d.ts} +2 -2
- package/dist/{query-builder-Dg9Loeco.d.cts → query-builder-CbHvimBk.d.cts} +2 -2
- package/dist/{table-CmIQe-jD.d.cts → table-CY9byPEg.d.cts} +2 -2
- package/dist/{table-TI4ULfra.d.ts → table-Des8C2od.d.ts} +2 -2
- package/dist/table.d.cts +3 -3
- package/dist/table.d.ts +3 -3
- package/dist/utils.cjs +34 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/{utils/sort-key-template.d.cts → utils.d.cts} +32 -1
- package/dist/{utils/sort-key-template.d.ts → utils.d.ts} +32 -1
- package/dist/utils.js +31 -0
- package/dist/utils.js.map +1 -0
- package/package.json +114 -49
- package/dist/utils/partition-key-template.cjs +0 -19
- package/dist/utils/partition-key-template.cjs.map +0 -1
- package/dist/utils/partition-key-template.d.cts +0 -32
- package/dist/utils/partition-key-template.d.ts +0 -32
- package/dist/utils/partition-key-template.js +0 -17
- package/dist/utils/partition-key-template.js.map +0 -1
- package/dist/utils/sort-key-template.cjs +0 -19
- package/dist/utils/sort-key-template.cjs.map +0 -1
- package/dist/utils/sort-key-template.js +0 -17
- package/dist/utils/sort-key-template.js.map +0 -1
package/README.md
CHANGED
|
@@ -45,10 +45,14 @@ await dinoTable
|
|
|
45
45
|
</td>
|
|
46
46
|
</tr>
|
|
47
47
|
<tr>
|
|
48
|
-
<td>
|
|
48
|
+
<td width="50%">
|
|
49
49
|
<h3>⚡ Velociraptor-fast API</h3>
|
|
50
50
|
<p>Intuitive chainable builder pattern for complex operations that feels natural and reduces boilerplate</p>
|
|
51
51
|
</td>
|
|
52
|
+
<td width="50%">
|
|
53
|
+
<h3>🎯 Semantic data access patterns</h3>
|
|
54
|
+
<p>Encourages meaningful, descriptive method names like <code>getUserByEmail()</code> instead of cryptic <code>gsi1</code> references</p>
|
|
55
|
+
</td>
|
|
52
56
|
</tr>
|
|
53
57
|
<tr>
|
|
54
58
|
<td width="50%">
|
|
@@ -65,6 +69,10 @@ await dinoTable
|
|
|
65
69
|
## 📑 Table of Contents
|
|
66
70
|
|
|
67
71
|
- [📦 Installation](#-installation)
|
|
72
|
+
- [🎯 DynamoDB Best Practices](#-dynamodb-best-practices)
|
|
73
|
+
- [Semantic Data Access Patterns](#semantic-data-access-patterns)
|
|
74
|
+
- [The Problem with Generic Index Names](#the-problem-with-generic-index-names)
|
|
75
|
+
- [The Solution: Meaningful Method Names](#the-solution-meaningful-method-names)
|
|
68
76
|
- [🚀 Quick Start](#-quick-start)
|
|
69
77
|
- [1. Configure Your Jurassic Table](#1-configure-your-jurassic-table)
|
|
70
78
|
- [2. Perform Type-Safe Dinosaur Operations](#2-perform-type-safe-dinosaur-operations)
|
|
@@ -137,6 +145,133 @@ pnpm add dyno-table @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
|
137
145
|
```
|
|
138
146
|
</details>
|
|
139
147
|
|
|
148
|
+
## 🎯 DynamoDB Best Practices
|
|
149
|
+
|
|
150
|
+
<div align="center">
|
|
151
|
+
|
|
152
|
+
### **Design Your Data Access Patterns First, Name Them Meaningfully**
|
|
153
|
+
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
dyno-table follows DynamoDB best practices by encouraging developers to **define their data access patterns upfront** and assign them **meaningful, descriptive names**. This approach ensures that when writing business logic, developers call semantically clear methods instead of cryptic index references.
|
|
157
|
+
|
|
158
|
+
### Semantic Data Access Patterns
|
|
159
|
+
|
|
160
|
+
The core principle is simple: **your code should read like business logic, not database implementation details**.
|
|
161
|
+
|
|
162
|
+
<table>
|
|
163
|
+
<tr>
|
|
164
|
+
<th>❌ Cryptic Implementation</th>
|
|
165
|
+
<th>✅ Semantic Business Logic</th>
|
|
166
|
+
</tr>
|
|
167
|
+
<tr>
|
|
168
|
+
<td>
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// Hard to understand what this does - using raw AWS Document Client
|
|
172
|
+
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
173
|
+
import { QueryCommand } from "@aws-sdk/lib-dynamodb";
|
|
174
|
+
|
|
175
|
+
const docClient = DynamoDBDocument.from(new DynamoDBClient({}));
|
|
176
|
+
|
|
177
|
+
const users = await docClient.send(new QueryCommand({
|
|
178
|
+
TableName: "MyTable",
|
|
179
|
+
IndexName: "gsi1",
|
|
180
|
+
KeyConditionExpression: "#pk = :pk",
|
|
181
|
+
ExpressionAttributeNames: { "#pk": "pk" },
|
|
182
|
+
ExpressionAttributeValues: { ":pk": "STATUS#active" }
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
const orders = await docClient.send(new QueryCommand({
|
|
186
|
+
TableName: "MyTable",
|
|
187
|
+
IndexName: "gsi2",
|
|
188
|
+
KeyConditionExpression: "#pk = :pk",
|
|
189
|
+
ExpressionAttributeNames: { "#pk": "pk" },
|
|
190
|
+
ExpressionAttributeValues: { ":pk": "CUSTOMER#123" }
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
const products = await docClient.send(new QueryCommand({
|
|
194
|
+
TableName: "MyTable",
|
|
195
|
+
IndexName: "gsi3",
|
|
196
|
+
KeyConditionExpression: "#pk = :pk",
|
|
197
|
+
ExpressionAttributeNames: { "#pk": "pk" },
|
|
198
|
+
ExpressionAttributeValues: { ":pk": "CATEGORY#electronics" }
|
|
199
|
+
}));
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
</td>
|
|
203
|
+
<td>
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// Clear business intent
|
|
207
|
+
const activeUsers = await userRepo.query
|
|
208
|
+
.getActiveUsers()
|
|
209
|
+
.execute();
|
|
210
|
+
|
|
211
|
+
const customerOrders = await orderRepo.query
|
|
212
|
+
.getOrdersByCustomer({ customerId: "123" })
|
|
213
|
+
.execute();
|
|
214
|
+
|
|
215
|
+
const electronics = await productRepo.query
|
|
216
|
+
.getProductsByCategory({ category: "electronics" })
|
|
217
|
+
.execute();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
</td>
|
|
221
|
+
</tr>
|
|
222
|
+
</table>
|
|
223
|
+
|
|
224
|
+
### The Problem with Generic Index Names
|
|
225
|
+
|
|
226
|
+
When you use generic names like `gsi1`, `gsi2`, `gsi3`, you create several problems:
|
|
227
|
+
|
|
228
|
+
- **🧠 Cognitive Load**: Developers must remember what each index does
|
|
229
|
+
- **📚 Poor Documentation**: Code doesn't self-document its purpose
|
|
230
|
+
- **🐛 Error-Prone**: Easy to use the wrong index for a query
|
|
231
|
+
- **👥 Team Friction**: New team members struggle to understand data access patterns
|
|
232
|
+
- **🔄 Maintenance Issues**: Refactoring becomes risky and unclear
|
|
233
|
+
|
|
234
|
+
### The Solution: Meaningful Method Names
|
|
235
|
+
|
|
236
|
+
dyno-table encourages you to define your access patterns with descriptive names that reflect their business purpose:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Define your access patterns with meaningful names
|
|
240
|
+
const UserEntity = defineEntity({
|
|
241
|
+
name: "User",
|
|
242
|
+
schema: userSchema,
|
|
243
|
+
primaryKey,
|
|
244
|
+
queries: {
|
|
245
|
+
// ✅ Clear business purpose
|
|
246
|
+
getActiveUsers: createQuery
|
|
247
|
+
.input(z.object({}))
|
|
248
|
+
.query(({ entity }) => entity.query({ pk: "STATUS#active" }).useIndex("gsi1")),
|
|
249
|
+
|
|
250
|
+
getUsersByEmail: createQuery
|
|
251
|
+
.input(z.object({ email: z.string() }))
|
|
252
|
+
.query(({ input, entity }) => entity.query({ pk: `EMAIL#${input.email}` }).useIndex("gsi1")),
|
|
253
|
+
|
|
254
|
+
getUsersByDepartment: createQuery
|
|
255
|
+
.input(z.object({ department: z.string() }))
|
|
256
|
+
.query(({ input, entity }) => entity.query({ pk: `DEPT#${input.department}` }).useIndex("gsi2")),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Usage in business logic is now self-documenting
|
|
261
|
+
const activeUsers = await userRepo.query.getActiveUsers().execute();
|
|
262
|
+
const engineeringTeam = await userRepo.query.getUsersByDepartment({ department: "engineering" }).execute();
|
|
263
|
+
const user = await userRepo.query.getUsersByEmail({ email: "john@company.com" }).execute();
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**This pattern promotes:**
|
|
267
|
+
- ✅ **Better code readability and maintainability**
|
|
268
|
+
- ✅ **Self-documenting API design**
|
|
269
|
+
- ✅ **Easier onboarding for new team members**
|
|
270
|
+
- ✅ **Reduced cognitive load when understanding data access patterns**
|
|
271
|
+
- ✅ **Clear separation between business logic and database implementation**
|
|
272
|
+
|
|
273
|
+
> **🏗️ Important Note**: Keep your actual DynamoDB table GSI names generic (`gsi1`, `gsi2`, etc.) for flexibility across different entities. The meaningful, descriptive names should live at the entity/repository level, not at the table level. This allows multiple entities to share the same GSIs while maintaining semantic clarity in your business logic.
|
|
274
|
+
|
|
140
275
|
## 🚀 Quick Start
|
|
141
276
|
|
|
142
277
|
<div align="center">
|
|
@@ -177,6 +312,8 @@ const dinoTable = new Table({
|
|
|
177
312
|
|
|
178
313
|
### 2. Perform Type-Safe Operations directly on the table instance
|
|
179
314
|
|
|
315
|
+
> **💡 Pro Tip**: While you can use the table directly, we recommend using the [Entity Pattern](#-entity-pattern-with-standard-schema-validators) with meaningful, descriptive method names like `getUserByEmail()` instead of generic index references. This follows DynamoDB best practices and makes your code self-documenting.
|
|
316
|
+
|
|
180
317
|
<table>
|
|
181
318
|
<tr>
|
|
182
319
|
<td>
|
|
@@ -271,28 +408,17 @@ await dinoTable.transaction((tx) => {
|
|
|
271
408
|
|
|
272
409
|
<table>
|
|
273
410
|
<tr>
|
|
274
|
-
<th
|
|
275
|
-
<th
|
|
411
|
+
<th>❌ Without dyno-table</th>
|
|
412
|
+
<th>✅ With dyno-table (Entity Pattern)</th>
|
|
276
413
|
</tr>
|
|
277
414
|
<tr>
|
|
278
415
|
<td>
|
|
279
416
|
|
|
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
|
-
)
|
|
289
|
-
.execute();
|
|
290
|
-
```
|
|
291
|
-
|
|
292
417
|
```ts
|
|
293
418
|
// Verbose, error-prone, no type safety
|
|
294
419
|
await docClient.send(new QueryCommand({
|
|
295
420
|
TableName: "JurassicPark",
|
|
421
|
+
IndexName: "gsi1", // What does gsi1 do?
|
|
296
422
|
KeyConditionExpression: "#pk = :pk",
|
|
297
423
|
FilterExpression: "contains(#features, :feathers)",
|
|
298
424
|
ExpressionAttributeNames: {
|
|
@@ -306,10 +432,37 @@ await docClient.send(new QueryCommand({
|
|
|
306
432
|
}));
|
|
307
433
|
```
|
|
308
434
|
|
|
435
|
+
</td>
|
|
436
|
+
<td>
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// Self-documenting, type-safe, semantic
|
|
440
|
+
const featheredTRexes = await dinosaurRepo.query
|
|
441
|
+
.getFeatheredDinosaursBySpecies({
|
|
442
|
+
species: "trex"
|
|
443
|
+
})
|
|
444
|
+
.execute();
|
|
445
|
+
|
|
446
|
+
// Or using table directly (still better than raw SDK)
|
|
447
|
+
await dinoTable
|
|
448
|
+
.query<Dinosaur>({
|
|
449
|
+
pk: "SPECIES#trex"
|
|
450
|
+
})
|
|
451
|
+
.filter(op =>
|
|
452
|
+
op.contains("features", "feathers")
|
|
453
|
+
)
|
|
454
|
+
.execute();
|
|
455
|
+
```
|
|
456
|
+
|
|
309
457
|
</td>
|
|
310
458
|
</tr>
|
|
311
459
|
</table>
|
|
312
460
|
|
|
461
|
+
**Key improvements:**
|
|
462
|
+
- 🛡️ **Type Safety**: Compile-time error checking prevents runtime failures
|
|
463
|
+
- 📖 **Self-Documenting**: Code clearly expresses business intent
|
|
464
|
+
- 🧠 **Reduced Complexity**: No manual expression building or attribute mapping
|
|
465
|
+
|
|
313
466
|
## 🏗️ Entity Pattern with Standard Schema validators
|
|
314
467
|
|
|
315
468
|
<div align="center">
|
|
@@ -336,7 +489,6 @@ await docClient.send(new QueryCommand({
|
|
|
336
489
|
- 🔑 **Automatic key generation**
|
|
337
490
|
- 📦 **Repository pattern**
|
|
338
491
|
- 🔍 **Custom query builders**
|
|
339
|
-
- 🔄 **Lifecycle hooks**
|
|
340
492
|
|
|
341
493
|
</td>
|
|
342
494
|
</tr>
|
|
@@ -460,7 +612,7 @@ await dinosaurRepo.delete({
|
|
|
460
612
|
|
|
461
613
|
#### 3. Custom Queries
|
|
462
614
|
|
|
463
|
-
Define custom queries with
|
|
615
|
+
Define custom queries with **meaningful, descriptive names** that reflect their business purpose. This follows DynamoDB best practices by making your data access patterns self-documenting:
|
|
464
616
|
|
|
465
617
|
```ts
|
|
466
618
|
import { createQueries } from "dyno-table/entity";
|
|
@@ -472,7 +624,8 @@ const DinosaurEntity = defineEntity({
|
|
|
472
624
|
schema: dinosaurSchema,
|
|
473
625
|
primaryKey,
|
|
474
626
|
queries: {
|
|
475
|
-
|
|
627
|
+
// ✅ Semantic method names that describe business intent
|
|
628
|
+
getDinosaursByDiet: createQuery
|
|
476
629
|
.input(
|
|
477
630
|
z.object({
|
|
478
631
|
diet: z.enum(["carnivore", "herbivore", "omnivore"]),
|
|
@@ -485,7 +638,7 @@ const DinosaurEntity = defineEntity({
|
|
|
485
638
|
});
|
|
486
639
|
}),
|
|
487
640
|
|
|
488
|
-
|
|
641
|
+
findDinosaursBySpecies: createQuery
|
|
489
642
|
.input(
|
|
490
643
|
z.object({
|
|
491
644
|
species: z.string(),
|
|
@@ -496,40 +649,89 @@ const DinosaurEntity = defineEntity({
|
|
|
496
649
|
.scan()
|
|
497
650
|
.filter((op) => op.eq("species", input.species));
|
|
498
651
|
}),
|
|
652
|
+
|
|
653
|
+
getActiveCarnivores: createQuery
|
|
654
|
+
.input(z.object({}))
|
|
655
|
+
.query(({ entity }) => {
|
|
656
|
+
return entity
|
|
657
|
+
.query({
|
|
658
|
+
pk: dinosaurPK({diet: "carnivore"})
|
|
659
|
+
})
|
|
660
|
+
.filter((op) => op.eq("status", "active"));
|
|
661
|
+
}),
|
|
662
|
+
|
|
663
|
+
getDangerousDinosaursInEnclosure: createQuery
|
|
664
|
+
.input(
|
|
665
|
+
z.object({
|
|
666
|
+
enclosureId: z.string(),
|
|
667
|
+
minDangerLevel: z.number().min(1).max(10),
|
|
668
|
+
})
|
|
669
|
+
)
|
|
670
|
+
.query(({ input, entity }) => {
|
|
671
|
+
return entity
|
|
672
|
+
.scan()
|
|
673
|
+
.filter((op) => op.and(
|
|
674
|
+
op.contains("enclosureId", input.enclosureId),
|
|
675
|
+
op.gte("dangerLevel", input.minDangerLevel)
|
|
676
|
+
));
|
|
677
|
+
}),
|
|
499
678
|
},
|
|
500
679
|
});
|
|
501
680
|
|
|
502
|
-
//
|
|
503
|
-
const carnivores = await dinosaurRepo.query.
|
|
504
|
-
const trexes = await dinosaurRepo.query.
|
|
681
|
+
// Usage in business logic is now self-documenting
|
|
682
|
+
const carnivores = await dinosaurRepo.query.getDinosaursByDiet({ diet: "carnivore" }).execute();
|
|
683
|
+
const trexes = await dinosaurRepo.query.findDinosaursBySpecies({ species: "Tyrannosaurus Rex" }).execute();
|
|
684
|
+
const activeCarnivores = await dinosaurRepo.query.getActiveCarnivores().execute();
|
|
685
|
+
const dangerousDinos = await dinosaurRepo.query.getDangerousDinosaursInEnclosure({
|
|
686
|
+
enclosureId: "PADDOCK-A",
|
|
687
|
+
minDangerLevel: 8
|
|
688
|
+
}).execute();
|
|
505
689
|
```
|
|
506
690
|
|
|
507
|
-
|
|
691
|
+
**Benefits of semantic naming:**
|
|
692
|
+
- 🎯 **Clear Intent**: Method names immediately convey what data you're accessing
|
|
693
|
+
- 📖 **Self-Documenting**: No need to look up what `gsi1` or `gsi2` does
|
|
694
|
+
- 🧠 **Reduced Cognitive Load**: Developers can focus on business logic, not database details
|
|
695
|
+
- 👥 **Team Collaboration**: New team members understand the codebase faster
|
|
696
|
+
- 🔍 **Better IDE Support**: Autocomplete shows meaningful method names
|
|
697
|
+
|
|
698
|
+
#### 4. Defining GSI Access Patterns
|
|
508
699
|
|
|
509
|
-
Define GSI
|
|
700
|
+
Define GSI access patterns with **meaningful names** that reflect their business purpose. This is crucial for maintaining readable, self-documenting code:
|
|
510
701
|
|
|
511
702
|
```ts
|
|
512
703
|
import { createIndex } from "dyno-table/entity";
|
|
513
704
|
|
|
514
|
-
// Define
|
|
515
|
-
const
|
|
516
|
-
const
|
|
705
|
+
// Define GSI templates with descriptive names that reflect their purpose
|
|
706
|
+
const speciesPK = partitionKey`SPECIES#${"species"}`
|
|
707
|
+
const speciesSK = sortKey`DINOSAUR#${"id"}`
|
|
708
|
+
|
|
709
|
+
const enclosurePK = partitionKey`ENCLOSURE#${"enclosureId"}`
|
|
710
|
+
const enclosureSK = sortKey`DANGER#${"dangerLevel"}#ID#${"id"}`
|
|
517
711
|
|
|
518
|
-
//
|
|
712
|
+
// Create indexes with meaningful names
|
|
519
713
|
const speciesIndex = createIndex()
|
|
520
714
|
.input(dinosaurSchema)
|
|
521
|
-
.partitionKey(({ species }) =>
|
|
522
|
-
.sortKey(({ id }) =>
|
|
715
|
+
.partitionKey(({ species }) => speciesPK({ species }))
|
|
716
|
+
.sortKey(({ id }) => speciesSK({ id }));
|
|
717
|
+
|
|
718
|
+
const enclosureIndex = createIndex()
|
|
719
|
+
.input(dinosaurSchema)
|
|
720
|
+
.partitionKey(({ enclosureId }) => enclosurePK({ enclosureId }))
|
|
721
|
+
.sortKey(({ dangerLevel, id }) => enclosureSK({ dangerLevel, id }));
|
|
523
722
|
|
|
524
723
|
const DinosaurEntity = defineEntity({
|
|
525
724
|
name: "Dinosaur",
|
|
526
725
|
schema: dinosaurSchema,
|
|
527
726
|
primaryKey,
|
|
528
727
|
indexes: {
|
|
529
|
-
|
|
728
|
+
// ✅ Map to generic GSI names for table flexibility
|
|
729
|
+
gsi1: speciesIndex,
|
|
730
|
+
gsi2: enclosureIndex,
|
|
530
731
|
},
|
|
531
732
|
queries: {
|
|
532
|
-
|
|
733
|
+
// ✅ Semantic method names that describe business intent
|
|
734
|
+
getDinosaursBySpecies: createQuery
|
|
533
735
|
.input(
|
|
534
736
|
z.object({
|
|
535
737
|
species: z.string(),
|
|
@@ -538,16 +740,59 @@ const DinosaurEntity = defineEntity({
|
|
|
538
740
|
.query(({ input, entity }) => {
|
|
539
741
|
return entity
|
|
540
742
|
.query({
|
|
541
|
-
|
|
542
|
-
pk: gsi1PK({species: input.species}),
|
|
743
|
+
pk: speciesPK({species: input.species}),
|
|
543
744
|
})
|
|
544
|
-
//
|
|
545
|
-
|
|
745
|
+
.useIndex("gsi1"); // Generic GSI name for table flexibility
|
|
746
|
+
}),
|
|
747
|
+
|
|
748
|
+
getDinosaursByEnclosure: createQuery
|
|
749
|
+
.input(
|
|
750
|
+
z.object({
|
|
751
|
+
enclosureId: z.string(),
|
|
752
|
+
})
|
|
753
|
+
)
|
|
754
|
+
.query(({ input, entity }) => {
|
|
755
|
+
return entity
|
|
756
|
+
.query({
|
|
757
|
+
pk: enclosurePK({enclosureId: input.enclosureId}),
|
|
758
|
+
})
|
|
759
|
+
.useIndex("gsi2");
|
|
760
|
+
}),
|
|
761
|
+
|
|
762
|
+
getMostDangerousInEnclosure: createQuery
|
|
763
|
+
.input(
|
|
764
|
+
z.object({
|
|
765
|
+
enclosureId: z.string(),
|
|
766
|
+
minDangerLevel: z.number().min(1).max(10),
|
|
767
|
+
})
|
|
768
|
+
)
|
|
769
|
+
.query(({ input, entity }) => {
|
|
770
|
+
return entity
|
|
771
|
+
.query({
|
|
772
|
+
pk: enclosurePK({enclosureId: input.enclosureId}),
|
|
773
|
+
sk: (op) => op.gte(`DANGER#${input.minDangerLevel}`)
|
|
774
|
+
})
|
|
775
|
+
.useIndex("gsi2")
|
|
776
|
+
.sortDescending(); // Get most dangerous first
|
|
546
777
|
}),
|
|
547
778
|
},
|
|
548
779
|
});
|
|
780
|
+
|
|
781
|
+
// Usage is now self-documenting
|
|
782
|
+
const trexes = await dinosaurRepo.query.getDinosaursBySpecies({ species: "Tyrannosaurus Rex" }).execute();
|
|
783
|
+
const paddockADinos = await dinosaurRepo.query.getDinosaursByEnclosure({ enclosureId: "PADDOCK-A" }).execute();
|
|
784
|
+
const dangerousDinos = await dinosaurRepo.query.getMostDangerousInEnclosure({
|
|
785
|
+
enclosureId: "PADDOCK-A",
|
|
786
|
+
minDangerLevel: 8
|
|
787
|
+
}).execute();
|
|
549
788
|
```
|
|
550
789
|
|
|
790
|
+
**Key principles for access pattern naming:**
|
|
791
|
+
- 🎯 **Generic GSI Names**: Keep table-level GSI names generic (`gsi1`, `gsi2`) for flexibility across entities
|
|
792
|
+
- 🔍 **Business-Focused**: Method names should reflect what the query achieves, not how it works
|
|
793
|
+
- 📚 **Self-Documenting**: Anyone reading the code should understand the purpose immediately
|
|
794
|
+
- 🏗️ **Entity-Level Semantics**: The meaningful names live at the entity/repository level, not the table level
|
|
795
|
+
|
|
551
796
|
### Complete Entity Example
|
|
552
797
|
|
|
553
798
|
Here's a complete example of using Zod schemas directly:
|
|
@@ -621,7 +866,8 @@ const DinosaurEntity = defineEntity({
|
|
|
621
866
|
gsi2: enclosureIndex,
|
|
622
867
|
},
|
|
623
868
|
queries: {
|
|
624
|
-
|
|
869
|
+
// ✅ Semantic method names that describe business intent
|
|
870
|
+
getDinosaursBySpecies: createQuery
|
|
625
871
|
.input(
|
|
626
872
|
z.object({
|
|
627
873
|
species: z.string(),
|
|
@@ -635,7 +881,7 @@ const DinosaurEntity = defineEntity({
|
|
|
635
881
|
.useIndex("gsi1");
|
|
636
882
|
}),
|
|
637
883
|
|
|
638
|
-
|
|
884
|
+
getDinosaursByEnclosure: createQuery
|
|
639
885
|
.input(
|
|
640
886
|
z.object({
|
|
641
887
|
enclosureId: z.string(),
|
|
@@ -649,7 +895,7 @@ const DinosaurEntity = defineEntity({
|
|
|
649
895
|
.useIndex("gsi2");
|
|
650
896
|
}),
|
|
651
897
|
|
|
652
|
-
|
|
898
|
+
getDangerousDinosaursInEnclosure: createQuery
|
|
653
899
|
.input(
|
|
654
900
|
z.object({
|
|
655
901
|
enclosureId: z.string(),
|
|
@@ -688,13 +934,13 @@ async function main() {
|
|
|
688
934
|
})
|
|
689
935
|
.execute();
|
|
690
936
|
|
|
691
|
-
// Query dinosaurs by species
|
|
692
|
-
const trexes = await dinosaurRepo.query.
|
|
693
|
-
species: "Tyrannosaurus Rex"
|
|
937
|
+
// Query dinosaurs by species using semantic method names
|
|
938
|
+
const trexes = await dinosaurRepo.query.getDinosaursBySpecies({
|
|
939
|
+
species: "Tyrannosaurus Rex"
|
|
694
940
|
}).execute();
|
|
695
941
|
|
|
696
942
|
// Query dangerous dinosaurs in an enclosure
|
|
697
|
-
const dangerousDinos = await dinosaurRepo.query.
|
|
943
|
+
const dangerousDinos = await dinosaurRepo.query.getDangerousDinosaursInEnclosure({
|
|
698
944
|
enclosureId: "enc-001",
|
|
699
945
|
minDangerLevel: 8,
|
|
700
946
|
}).execute();
|
|
@@ -702,12 +948,14 @@ async function main() {
|
|
|
702
948
|
```
|
|
703
949
|
|
|
704
950
|
**Key benefits:**
|
|
705
|
-
-
|
|
706
|
-
-
|
|
707
|
-
-
|
|
708
|
-
-
|
|
709
|
-
-
|
|
710
|
-
-
|
|
951
|
+
- 🎯 **Semantic Data Access**: Method names like `getDinosaursBySpecies()` clearly express business intent
|
|
952
|
+
- 🚫 **Prevents Accidental Cross-Type Access**: Type-safe operations prevent data corruption
|
|
953
|
+
- 🔍 **Self-Documenting Code**: No need to remember what `gsi1` or `gsi2` does
|
|
954
|
+
- 🛡️ **Consistent Key Structure**: Ensures uniform key patterns across entities
|
|
955
|
+
- 📦 **Encapsulated Domain Logic**: Business rules are contained within entity definitions
|
|
956
|
+
- 🧪 **Schema Validation**: Automatic data validation with your preferred schema library
|
|
957
|
+
- 🔄 **Full Type Inference**: Complete TypeScript support from schema to queries
|
|
958
|
+
- 👥 **Team Collaboration**: New developers understand the codebase immediately
|
|
711
959
|
|
|
712
960
|
## 🧩 Advanced Features
|
|
713
961
|
|
|
@@ -995,6 +1243,7 @@ Dyno-table provides comprehensive query methods that match DynamoDB's capabiliti
|
|
|
995
1243
|
| **Greater Than** | `.filter(op => op.gt("price", 50))` | `price > :v1` |
|
|
996
1244
|
| **Greater Than or Equal** | `.filter(op => op.gte("rating", 4))` | `rating >= :v1` |
|
|
997
1245
|
| **Between** | `.filter(op => op.between("age", 18, 65))` | `age BETWEEN :v1 AND :v2` |
|
|
1246
|
+
| **In Array** | `.filter(op => op.inArray("status", ["ACTIVE", "PENDING"]))` | `status IN (:v1, :v2)` |
|
|
998
1247
|
| **Begins With** | `.filter(op => op.beginsWith("email", "@example.com"))` | `begins_with(email, :v1)` |
|
|
999
1248
|
| **Contains** | `.filter(op => op.contains("tags", "important"))` | `contains(tags, :v1)` |
|
|
1000
1249
|
| **Attribute Exists** | `.filter(op => op.attributeExists("email"))` | `attribute_exists(email)` |
|
|
@@ -1101,12 +1350,12 @@ const oldDino = await table.put<Dinosaur>({
|
|
|
1101
1350
|
|
|
1102
1351
|
The library supports a comprehensive set of type-safe condition operators:
|
|
1103
1352
|
|
|
1104
|
-
| Category | Operators
|
|
1105
|
-
|
|
1106
|
-
| **Comparison** | `eq`, `ne`, `lt`, `lte`, `gt`, `gte`
|
|
1107
|
-
| **String/Set** | `between`, `beginsWith`, `contains`
|
|
1108
|
-
| **Existence** | `attributeExists`, `attributeNotExists`
|
|
1109
|
-
| **Logical** | `and`, `or`, `not`
|
|
1353
|
+
| Category | Operators | Example |
|
|
1354
|
+
|----------------|----------------------------------------------|-------------------------------------------------------------------------|
|
|
1355
|
+
| **Comparison** | `eq`, `ne`, `lt`, `lte`, `gt`, `gte` | `.condition(op => op.gt("age", 18))` |
|
|
1356
|
+
| **String/Set** | `between`, `beginsWith`, `contains`, `inArray` | `.condition(op => op.inArray("status", ["active", "pending"]))` |
|
|
1357
|
+
| **Existence** | `attributeExists`, `attributeNotExists` | `.condition(op => op.attributeExists("email"))` |
|
|
1358
|
+
| **Logical** | `and`, `or`, `not` | `.condition(op => op.and(op.eq("status", "active"), op.gt("age", 18)))` |
|
|
1110
1359
|
|
|
1111
1360
|
All operators are type-safe and will provide proper TypeScript inference for nested attributes.
|
|
1112
1361
|
|
|
@@ -1191,6 +1440,8 @@ await table.query<DinosaurMonitoring>({
|
|
|
1191
1440
|
op.lt("health", "90"), // ❌ TypeScript Error: health expects number
|
|
1192
1441
|
op.gt("temperature", 38), // ✓ Valid
|
|
1193
1442
|
op.contains("behavior", "aggressive"), // ✓ Valid
|
|
1443
|
+
op.inArray("alertLevel", ["LOW", "MEDIUM", "HIGH"]), // ✓ Valid: matches union type
|
|
1444
|
+
op.inArray("alertLevel", ["UNKNOWN", "INVALID"]), // ❌ TypeScript Error: invalid alert levels
|
|
1194
1445
|
op.eq("alertLevel", "UNKNOWN") // ❌ TypeScript Error: invalid alert level
|
|
1195
1446
|
))
|
|
1196
1447
|
.execute();
|
|
@@ -1285,6 +1536,7 @@ All condition operators are type-safe and will validate against your item type.
|
|
|
1285
1536
|
- `gt(attr, value)` - Greater than (>)
|
|
1286
1537
|
- `gte(attr, value)` - Greater than or equal to (≥)
|
|
1287
1538
|
- `between(attr, lower, upper)` - Between two values (inclusive)
|
|
1539
|
+
- `inArray(attr, values)` - Checks if value is in a list of values (IN operator, max 100 values)
|
|
1288
1540
|
- `beginsWith(attr, value)` - Checks if string begins with value
|
|
1289
1541
|
- `contains(attr, value)` - Checks if string/set contains value
|
|
1290
1542
|
|
|
@@ -1300,6 +1552,18 @@ await dinoTable
|
|
|
1300
1552
|
op.between("stats.weight", 1000, 5000) // Medium-sized dinosaurs
|
|
1301
1553
|
))
|
|
1302
1554
|
.execute();
|
|
1555
|
+
|
|
1556
|
+
// Example: Filter dinosaurs by multiple status values using inArray
|
|
1557
|
+
await dinoTable
|
|
1558
|
+
.query<Dinosaur>({
|
|
1559
|
+
pk: "SPECIES#trex"
|
|
1560
|
+
})
|
|
1561
|
+
.filter((op) => op.and(
|
|
1562
|
+
op.inArray("status", ["ACTIVE", "FEEDING", "RESTING"]), // Multiple valid statuses
|
|
1563
|
+
op.inArray("diet", ["carnivore", "omnivore"]), // Meat-eating dinosaurs
|
|
1564
|
+
op.gt("dangerLevel", 5) // High danger level
|
|
1565
|
+
))
|
|
1566
|
+
.execute();
|
|
1303
1567
|
```
|
|
1304
1568
|
|
|
1305
1569
|
#### Attribute Operators
|
|
@@ -1349,6 +1613,11 @@ await dinoTable
|
|
|
1349
1613
|
op.lt("care.feeding.lastFed", new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString()),
|
|
1350
1614
|
op.contains("behavior", "stressed")
|
|
1351
1615
|
),
|
|
1616
|
+
// Alert: Critical status dinosaurs requiring immediate attention
|
|
1617
|
+
op.and(
|
|
1618
|
+
op.inArray("status", ["SICK", "INJURED", "QUARANTINE"]), // Critical statuses
|
|
1619
|
+
op.inArray("priority", ["HIGH", "URGENT"]) // High priority levels
|
|
1620
|
+
),
|
|
1352
1621
|
// Alert: Enclosure climate issues
|
|
1353
1622
|
op.and(
|
|
1354
1623
|
op.not(op.eq("habitat.enclosure.climate", "Optimal")),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as PrimaryKeyWithoutExpression, C as Condition, q as ConditionOperator } from '../conditions-3ae5znV_.cjs';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.cjs';
|
|
3
3
|
import { DynamoItem } from '../types.cjs';
|
|
4
4
|
import '@aws-sdk/lib-dynamodb';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as PrimaryKeyWithoutExpression, C as Condition, q as ConditionOperator } from '../conditions-BtynAviC.js';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.js';
|
|
3
3
|
import { DynamoItem } from '../types.js';
|
|
4
4
|
import '@aws-sdk/lib-dynamodb';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as PrimaryKeyWithoutExpression, C as Condition, q as ConditionOperator } from '../conditions-3ae5znV_.cjs';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.cjs';
|
|
3
3
|
import { D as DeleteCommandParams } from '../builder-types-DlaUSc-b.cjs';
|
|
4
4
|
import { DynamoItem } from '../types.cjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as PrimaryKeyWithoutExpression, C as Condition, q as ConditionOperator } from '../conditions-BtynAviC.js';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.js';
|
|
3
3
|
import { D as DeleteCommandParams } from '../builder-types-B_tCpn9F.js';
|
|
4
4
|
import { DynamoItem } from '../types.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { s as Path, t as PathType, C as Condition, q as ConditionOperator } from '../conditions-3ae5znV_.cjs';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.cjs';
|
|
3
3
|
import { a as PutCommandParams } from '../builder-types-DlaUSc-b.cjs';
|
|
4
4
|
import { DynamoItem } from '../types.cjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { s as Path, t as PathType, C as Condition, q as ConditionOperator } from '../conditions-BtynAviC.js';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.js';
|
|
3
3
|
import { a as PutCommandParams } from '../builder-types-B_tCpn9F.js';
|
|
4
4
|
import { DynamoItem } from '../types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import '../conditions-
|
|
2
|
-
export { Q as QueryBuilder,
|
|
1
|
+
import '../conditions-3ae5znV_.cjs';
|
|
2
|
+
export { Q as QueryBuilder, a as QueryOptions } from '../query-builder-CbHvimBk.cjs';
|
|
3
3
|
import '../types.cjs';
|
|
4
4
|
import '../builder-types-DlaUSc-b.cjs';
|
|
5
5
|
import './paginator.cjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import '../conditions-
|
|
2
|
-
export { Q as QueryBuilder,
|
|
1
|
+
import '../conditions-BtynAviC.js';
|
|
2
|
+
export { Q as QueryBuilder, a as QueryOptions } from '../query-builder-BhrR31oO.js';
|
|
3
3
|
import '../types.js';
|
|
4
4
|
import '../builder-types-B_tCpn9F.js';
|
|
5
5
|
import './paginator.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TransactWriteCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
|
-
import {
|
|
2
|
+
import { r as PrimaryKeyWithoutExpression, C as Condition } from '../conditions-3ae5znV_.cjs';
|
|
3
3
|
import { a as PutCommandParams, D as DeleteCommandParams, U as UpdateCommandParams, C as ConditionCheckCommandParams } from '../builder-types-DlaUSc-b.cjs';
|
|
4
4
|
import { DynamoItem } from '../types.cjs';
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TransactWriteCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
|
-
import {
|
|
2
|
+
import { r as PrimaryKeyWithoutExpression, C as Condition } from '../conditions-BtynAviC.js';
|
|
3
3
|
import { a as PutCommandParams, D as DeleteCommandParams, U as UpdateCommandParams, C as ConditionCheckCommandParams } from '../builder-types-B_tCpn9F.js';
|
|
4
4
|
import { DynamoItem } from '../types.js';
|
|
5
5
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as PrimaryKeyWithoutExpression, s as Path, t as PathType, C as Condition, q as ConditionOperator } from '../conditions-3ae5znV_.cjs';
|
|
2
2
|
import { TransactionBuilder } from './transaction-builder.cjs';
|
|
3
3
|
import { U as UpdateCommandParams } from '../builder-types-DlaUSc-b.cjs';
|
|
4
4
|
import { DynamoItem } from '../types.cjs';
|