dyno-table 1.5.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 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>With dyno-table</th>
275
- <th>Without dyno-table</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 input validation:
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
- byDiet: createQuery
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
- bySpecies: createQuery
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
- // Use the custom queries
503
- const carnivores = await dinosaurRepo.query.byDiet({ diet: "carnivore" }).execute();
504
- const trexes = await dinosaurRepo.query.bySpecies({ species: "Tyrannosaurus Rex" }).execute();
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
- #### 4. Defining GSI access patterns
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 (LSI support coming later)
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 GSIs templates for querying by species
515
- const gsi1PK = partitionKey`SPECIES#${"species"}`
516
- const gsi1SK = sortKey`DINOSAUR#${"id"}`
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
- // Implement typesafe generator for the GSI - This is used in create calls to ensure the GSI is generated
712
+ // Create indexes with meaningful names
519
713
  const speciesIndex = createIndex()
520
714
  .input(dinosaurSchema)
521
- .partitionKey(({ species }) => gsi1PK({ species }))
522
- .sortKey(({ id }) => gsi1SK({ 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
- species: speciesIndex,
728
+ // โœ… Map to generic GSI names for table flexibility
729
+ gsi1: speciesIndex,
730
+ gsi2: enclosureIndex,
530
731
  },
531
732
  queries: {
532
- bySpecies: createQuery
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
- // Use the GSI template generator to avoid typos
542
- pk: gsi1PK({species: input.species}),
743
+ pk: speciesPK({species: input.species}),
543
744
  })
544
- // Use the template name as defined in the table instance
545
- .useIndex("gsi1");
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
- bySpecies: createQuery
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
- byEnclosure: createQuery
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
- dangerousInEnclosure: createQuery
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.bySpecies({
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.dangerousInEnclosure({
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
- - ๐Ÿšซ Prevents accidental cross-type data access
706
- - ๐Ÿ” Automatically filters queries/scans to a repository type
707
- - ๐Ÿ›ก๏ธ Ensures consistent key structure across entities
708
- - ๐Ÿ“ฆ Encapsulates domain-specific query logic
709
- - ๐Ÿงช Validates data with Zod schemas
710
- - ๐Ÿ”„ Provides type inference from schemas
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 | Example |
1105
- |----------------|-----------------------------------------|-------------------------------------------------------------------------|
1106
- | **Comparison** | `eq`, `ne`, `lt`, `lte`, `gt`, `gte` | `.condition(op => op.gt("age", 18))` |
1107
- | **String/Set** | `between`, `beginsWith`, `contains` | `.condition(op => op.beginsWith("email", "@example"))` |
1108
- | **Existence** | `attributeExists`, `attributeNotExists` | `.condition(op => op.attributeExists("email"))` |
1109
- | **Logical** | `and`, `or`, `not` | `.condition(op => op.and(op.eq("status", "active"), op.gt("age", 18)))` |
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")),