dyno-table 0.2.0-0 → 1.0.0-alpha.1

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.
Files changed (96) hide show
  1. package/README.md +182 -577
  2. package/dist/{table.cjs → index.cjs} +175 -87
  3. package/dist/index.d.cts +2971 -0
  4. package/dist/index.d.ts +2971 -0
  5. package/dist/{table.js → index.js} +127 -86
  6. package/package.json +10 -77
  7. package/dist/builder-types-C_PDZhnP.d.ts +0 -118
  8. package/dist/builder-types-DtwbqMeF.d.cts +0 -118
  9. package/dist/builders/condition-check-builder.cjs +0 -394
  10. package/dist/builders/condition-check-builder.cjs.map +0 -1
  11. package/dist/builders/condition-check-builder.d.cts +0 -157
  12. package/dist/builders/condition-check-builder.d.ts +0 -157
  13. package/dist/builders/condition-check-builder.js +0 -392
  14. package/dist/builders/condition-check-builder.js.map +0 -1
  15. package/dist/builders/delete-builder.cjs +0 -405
  16. package/dist/builders/delete-builder.cjs.map +0 -1
  17. package/dist/builders/delete-builder.d.cts +0 -166
  18. package/dist/builders/delete-builder.d.ts +0 -166
  19. package/dist/builders/delete-builder.js +0 -403
  20. package/dist/builders/delete-builder.js.map +0 -1
  21. package/dist/builders/paginator.cjs +0 -199
  22. package/dist/builders/paginator.cjs.map +0 -1
  23. package/dist/builders/paginator.d.cts +0 -179
  24. package/dist/builders/paginator.d.ts +0 -179
  25. package/dist/builders/paginator.js +0 -197
  26. package/dist/builders/paginator.js.map +0 -1
  27. package/dist/builders/put-builder.cjs +0 -476
  28. package/dist/builders/put-builder.cjs.map +0 -1
  29. package/dist/builders/put-builder.d.cts +0 -274
  30. package/dist/builders/put-builder.d.ts +0 -274
  31. package/dist/builders/put-builder.js +0 -474
  32. package/dist/builders/put-builder.js.map +0 -1
  33. package/dist/builders/query-builder.cjs +0 -674
  34. package/dist/builders/query-builder.cjs.map +0 -1
  35. package/dist/builders/query-builder.d.cts +0 -6
  36. package/dist/builders/query-builder.d.ts +0 -6
  37. package/dist/builders/query-builder.js +0 -672
  38. package/dist/builders/query-builder.js.map +0 -1
  39. package/dist/builders/transaction-builder.cjs +0 -894
  40. package/dist/builders/transaction-builder.cjs.map +0 -1
  41. package/dist/builders/transaction-builder.d.cts +0 -511
  42. package/dist/builders/transaction-builder.d.ts +0 -511
  43. package/dist/builders/transaction-builder.js +0 -892
  44. package/dist/builders/transaction-builder.js.map +0 -1
  45. package/dist/builders/update-builder.cjs +0 -627
  46. package/dist/builders/update-builder.cjs.map +0 -1
  47. package/dist/builders/update-builder.d.cts +0 -365
  48. package/dist/builders/update-builder.d.ts +0 -365
  49. package/dist/builders/update-builder.js +0 -625
  50. package/dist/builders/update-builder.js.map +0 -1
  51. package/dist/conditions--ld9a78i.d.ts +0 -331
  52. package/dist/conditions-ChhQWd6z.d.cts +0 -331
  53. package/dist/conditions.cjs +0 -59
  54. package/dist/conditions.cjs.map +0 -1
  55. package/dist/conditions.d.cts +0 -3
  56. package/dist/conditions.d.ts +0 -3
  57. package/dist/conditions.js +0 -43
  58. package/dist/conditions.js.map +0 -1
  59. package/dist/entity.cjs +0 -228
  60. package/dist/entity.cjs.map +0 -1
  61. package/dist/entity.d.cts +0 -149
  62. package/dist/entity.d.ts +0 -149
  63. package/dist/entity.js +0 -224
  64. package/dist/entity.js.map +0 -1
  65. package/dist/query-builder-Csror9Iu.d.ts +0 -507
  66. package/dist/query-builder-D2FM9rsu.d.cts +0 -507
  67. package/dist/standard-schema.cjs +0 -4
  68. package/dist/standard-schema.cjs.map +0 -1
  69. package/dist/standard-schema.d.cts +0 -57
  70. package/dist/standard-schema.d.ts +0 -57
  71. package/dist/standard-schema.js +0 -3
  72. package/dist/standard-schema.js.map +0 -1
  73. package/dist/table-BEhBPy2G.d.cts +0 -364
  74. package/dist/table-BW3cmUqr.d.ts +0 -364
  75. package/dist/table.cjs.map +0 -1
  76. package/dist/table.d.cts +0 -12
  77. package/dist/table.d.ts +0 -12
  78. package/dist/table.js.map +0 -1
  79. package/dist/types.cjs +0 -4
  80. package/dist/types.cjs.map +0 -1
  81. package/dist/types.d.cts +0 -22
  82. package/dist/types.d.ts +0 -22
  83. package/dist/types.js +0 -3
  84. package/dist/types.js.map +0 -1
  85. package/dist/utils/partition-key-template.cjs +0 -19
  86. package/dist/utils/partition-key-template.cjs.map +0 -1
  87. package/dist/utils/partition-key-template.d.cts +0 -32
  88. package/dist/utils/partition-key-template.d.ts +0 -32
  89. package/dist/utils/partition-key-template.js +0 -17
  90. package/dist/utils/partition-key-template.js.map +0 -1
  91. package/dist/utils/sort-key-template.cjs +0 -19
  92. package/dist/utils/sort-key-template.cjs.map +0 -1
  93. package/dist/utils/sort-key-template.d.cts +0 -35
  94. package/dist/utils/sort-key-template.d.ts +0 -35
  95. package/dist/utils/sort-key-template.js +0 -17
  96. package/dist/utils/sort-key-template.js.map +0 -1
package/README.md CHANGED
@@ -6,46 +6,35 @@
6
6
  <img src="docs/images/geoff-the-dyno.png" width="400" height="250" alt="Geoff the Dyno" style="float: right; margin-left: 20px; margin-bottom: 20px;">
7
7
 
8
8
  ```ts
9
- // Type-safe dinosaur tracking operations made simple
10
- await dinoTable
9
+ // Type-safe DynamoDB operations made simple
10
+ await table
11
11
  .update<Dinosaur>({
12
12
  pk: 'SPECIES#trex',
13
13
  sk: 'PROFILE#001'
14
14
  })
15
- .set('diet', 'Carnivore') // Update dietary classification
16
- .add('sightings', 1) // Increment sighting counter
17
- .condition(op => op.eq('status', 'ACTIVE')) // Only if dinosaur is active
15
+ .set('diet', 'Carnivore')
16
+ .add('sightings', 1)
17
+ .condition(op => op.eq('status', 'ACTIVE'))
18
18
  .execute();
19
19
  ```
20
20
 
21
- > This README provides a concise overview of dyno-table's features with dinosaur-themed examples. For detailed documentation on specific features, please refer to the individual markdown files in the `docs/` directory.
22
-
23
- ## 🌟 Why dyno-table for your Dinosaur Data?
21
+ ## 🌟 Why dyno-table?
24
22
 
25
- - **🦕 Dinosaur-sized data made manageable** - Clean abstraction layer for complex DynamoDB patterns
26
- - **🛡️ Extinction-proof type safety** - Full TypeScript support with strict type checking
27
- - **⚡ Velociraptor-fast API** - Chainable builder pattern for complex operations
28
- - **🔒 T-Rex-proof transactional safety** - ACID-compliant operations with easy-to-use transactions
29
- - **📈 Jurassic-scale performance** - Automatic batch chunking and pagination handling
23
+ - **🧩 Single-table design made simple** - Clean abstraction layer for complex DynamoDB patterns
24
+ - **🛡️ Type-safe operations** - Full TypeScript support with strict type checking
25
+ - **⚡ Fluent API** - Chainable builder pattern for complex operations
26
+ - **🔒 Transactional safety** - ACID-compliant operations with easy-to-use transactions
27
+ - **📈 Scalability built-in** - Automatic batch chunking and pagination handling
30
28
 
31
29
  ## 📑 Table of Contents
32
30
 
33
31
  - [🦖 dyno-table ](#-dyno-table--)
34
- - [🌟 Why dyno-table for your Dinosaur Data?](#-why-dyno-table-for-your-dinosaur-data)
32
+ - [🌟 Why dyno-table?](#-why-dyno-table)
35
33
  - [📑 Table of Contents](#-table-of-contents)
36
34
  - [📦 Installation](#-installation)
37
35
  - [🚀 Quick Start](#-quick-start)
38
- - [1. Configure Your Jurassic Table](#1-configure-your-jurassic-table)
39
- - [2. Perform Type-Safe Dinosaur Operations](#2-perform-type-safe-dinosaur-operations)
40
- - [🏗️ Entity Pattern](#️-entity-pattern-with-standard-schema-validators)
41
- - [Defining Entities](#defining-entities)
42
- - [Entity Features](#entity-features)
43
- - [1. Schema Validation](#1-schema-validation)
44
- - [2. CRUD Operations](#2-crud-operations)
45
- - [3. Custom Queries](#3-custom-queries)
46
- - [4. Indexes for Efficient Querying](#4-indexes-for-efficient-querying)
47
- - [5. Lifecycle Hooks](#5-lifecycle-hooks)
48
- - [Complete Entity Example](#complete-entity-example)
36
+ - [1. Configure Your Table](#1-configure-your-table)
37
+ - [2. Perform Type-Safe Operations](#2-perform-type-safe-operations)
49
38
  - [🧩 Advanced Features](#-advanced-features)
50
39
  - [Transactional Operations](#transactional-operations)
51
40
  - [Batch Processing](#batch-processing)
@@ -67,6 +56,7 @@ await dinoTable
67
56
  - [🔒 Transaction Operations](#-transaction-operations)
68
57
  - [Transaction Builder](#transaction-builder)
69
58
  - [Transaction Options](#transaction-options)
59
+ - [🏗️ Entity Pattern Best Practices (Coming Soon TM)](#️-entity-pattern-best-practices-coming-soon-tm)
70
60
  - [🚨 Error Handling](#-error-handling)
71
61
  - [📚 API Reference](#-api-reference)
72
62
  - [Condition Operators](#condition-operators-1)
@@ -92,26 +82,25 @@ npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
92
82
 
93
83
  ## 🚀 Quick Start
94
84
 
95
- ### 1. Configure Your Jurassic Table
85
+ ### 1. Configure Your Table
96
86
 
97
87
  ```ts
98
88
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
99
89
  import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
100
- import { Table } from "dyno-table/table";
90
+ import { Table } from "dyno-table";
101
91
 
102
- // Configure AWS SDK clients - your gateway to the prehistoric database
92
+ // Configure AWS SDK clients
103
93
  const client = new DynamoDBClient({ region: "us-west-2" });
104
94
  const docClient = DynamoDBDocument.from(client);
105
95
 
106
- // Initialize table with single-table design schema - your dinosaur park database
96
+ // Initialize table with single-table design schema
107
97
  const dinoTable = new Table({
108
98
  client: docClient,
109
- tableName: "JurassicPark", // Your central dinosaur tracking system
99
+ tableName: "DinosaurPark",
110
100
  indexes: {
111
- partitionKey: "pk", // Primary partition key for fast dinosaur lookups
112
- sortKey: "sk", // Sort key for organizing dinosaur data
101
+ partitionKey: "pk",
102
+ sortKey: "sk",
113
103
  gsis: {
114
- // Global Secondary Index for querying dinosaurs by species
115
104
  speciesId: {
116
105
  partitionKey: "gsi1pk",
117
106
  sortKey: "gsi1sk",
@@ -121,538 +110,141 @@ const dinoTable = new Table({
121
110
  });
122
111
  ```
123
112
 
124
- ### 2. Perform Type-Safe Dinosaur Operations
113
+ ### 2. Perform Type-Safe Operations
125
114
 
126
- **🦖 Creating a new dinosaur specimen**
115
+ **🦖 Creating a new dinosaur**
127
116
  ```ts
128
- // Add a new T-Rex to your collection with complete type safety
129
117
  const rex = await dinoTable
130
118
  .create<Dinosaur>({
131
- pk: "SPECIES#trex", // Partition key identifies the species
132
- sk: "PROFILE#trex", // Sort key for the specific profile
133
- speciesId: "trex", // For GSI queries
134
- name: "Tyrannosaurus Rex", // Display name
135
- diet: "carnivore", // Dietary classification
136
- length: 12.3, // Size in meters
137
- discoveryYear: 1902 // When first discovered
119
+ pk: "SPECIES#trex",
120
+ sk: "PROFILE#trex",
121
+ speciesId: "trex",
122
+ name: "Tyrannosaurus Rex",
123
+ diet: "carnivore",
124
+ length: 12.3,
125
+ discoveryYear: 1902
138
126
  })
139
127
  .execute();
140
128
  ```
141
129
 
142
- **🔍 Query for specific dinosaurs with conditions**
130
+ **🔍 Query with conditions**
143
131
  ```ts
144
- // Find large carnivorous dinosaurs in the T-Rex species
145
132
  const largeDinos = await dinoTable
146
133
  .query<Dinosaur>({
147
- pk: "SPECIES#trex", // Target the T-Rex species
148
- sk: (op) => op.beginsWith("PROFILE#") // Look in profile records
134
+ pk: "SPECIES#trex",
135
+ sk: (op) => op.beginsWith("PROFILE#")
149
136
  })
150
137
  .filter((op) => op.and(
151
- op.gte("length", 10), // Only dinosaurs longer than 10 meters
152
- op.eq("diet", "carnivore") // Must be carnivores
138
+ op.gte("length", 10),
139
+ op.eq("diet", "carnivore")
153
140
  ))
154
- .limit(10) // Limit to 10 results
141
+ .limit(10)
155
142
  .execute();
156
143
  ```
157
144
 
158
- **🔄 Update dinosaur classification**
145
+ **🔄 Complex update operation**
159
146
  ```ts
160
- // Update a dinosaur's diet classification based on new research
161
147
  await dinoTable
162
148
  .update<Dinosaur>({
163
- pk: "SPECIES#trex", // Target the T-Rex species
164
- sk: "PROFILE#trex" // Specific profile to update
149
+ pk: "SPECIES#trex",
150
+ sk: "PROFILE#trex"
165
151
  })
166
- .set("diet", "omnivore") // New diet classification based on fossil evidence
167
- .add("discoveryYear", 1) // Adjust discovery year with new findings
168
- .remove("outdatedField") // Remove deprecated information
169
- .condition((op) => op.attributeExists("discoverySite")) // Only if discovery site is documented
152
+ .set("diet", "omnivore")
153
+ .add("discoveryYear", 1)
154
+ .remove("outdatedField")
155
+ .condition((op) => op.attributeExists("discoverySite"))
170
156
  .execute();
171
157
  ```
172
158
 
173
- ## 🏗️ Entity Pattern with Standard Schema validators
174
-
175
- The entity pattern provides a structured, type-safe way to work with DynamoDB items.
176
- It combines schema validation, key management, and repository operations into a cohesive abstraction.
177
-
178
- ✨ This library supports all standard schema validation libraries, including **zod**, **arktype**, and **valibot**,
179
- allowing you to choose your preferred validation tool!
180
-
181
- ### Defining Entities
182
-
183
- Entities are defined using the `defineEntity` function, which takes a configuration object that includes a schema, primary key definition, and optional indexes and queries.
184
-
185
- ```ts
186
- import { z } from "zod";
187
- import { defineEntity, createIndex } from "dyno-table/entity";
188
-
189
- // Define your schema using Zod
190
- const dinosaurSchema = z.object({
191
- id: z.string(),
192
- species: z.string(),
193
- name: z.string(),
194
- diet: z.enum(["carnivore", "herbivore", "omnivore"]),
195
- dangerLevel: z.number().int().min(1).max(10),
196
- height: z.number().positive(),
197
- weight: z.number().positive(),
198
- status: z.enum(["active", "inactive", "sick", "deceased"]),
199
- createdAt: z.string().optional(),
200
- updatedAt: z.string().optional(),
201
- });
202
-
203
- // Infer the type from the schema
204
- type Dinosaur = z.infer<typeof dinosaurSchema>;
205
-
206
- // Define key templates for Dinosaur entity
207
- const dinosaurPK = partitionKey`ENTITY#DINOSAUR#DIET#${"diet"}`;
208
- const dinosaurSK = sortKey`ID#${"id"}#SPECIES#${"species"}`;
209
-
210
- // Create a primary index for Dinosaur entity
211
- const primaryKey = createIndex()
212
- .input(z.object({ id: z.string(), diet: z.string(), species: z.string() }))
213
- .partitionKey(({ diet }) => dinosaurPK({ diet }))
214
- .sortKey(({ id, species }) => dinosaurSK({ species, id }));
215
-
216
- // Define the entity
217
- const DinosaurEntity = defineEntity({
218
- name: "Dinosaur",
219
- schema: dinosaurSchema,
220
- primaryKey,
221
- });
222
-
223
- // Create a repository
224
- const dinosaurRepo = DinosaurEntity.createRepository(table);
225
- ```
226
-
227
- ### Entity Features
228
-
229
- #### 1. Schema Validation
230
-
231
- Entities use Zod schemas to validate data before operations:
232
-
233
- ```ts
234
- // Define a schema with Zod
235
- const dinosaurSchema = z.object({
236
- id: z.string(),
237
- species: z.string(),
238
- name: z.string(),
239
- diet: z.enum(["carnivore", "herbivore", "omnivore"]),
240
- dangerLevel: z.number().int().min(1).max(10),
241
- height: z.number().positive(),
242
- weight: z.number().positive(),
243
- status: z.enum(["active", "inactive", "sick", "deceased"]),
244
- tags: z.array(z.string()).optional(),
245
- });
246
-
247
- // Create an entity with the schema
248
- const DinosaurEntity = defineEntity({
249
- name: "Dinosaur",
250
- schema: dinosaurSchema,
251
- primaryKey: createIndex()
252
- .input(z.object({ id: z.string(), diet: z.string(), species: z.string() }))
253
- .partitionKey(({ diet }) => dinosaurPK({ diet }))
254
- .sortKey(({ id, species }) => dinosaurSK({ species, id }))
255
- });
256
- ```
257
-
258
- #### 2. CRUD Operations
259
-
260
- Entities provide type-safe CRUD operations:
261
-
262
- ```ts
263
- // Create a new dinosaur
264
- await dinosaurRepo.create({
265
- id: "dino-001",
266
- species: "Tyrannosaurus Rex",
267
- name: "Rexy",
268
- diet: "carnivore",
269
- dangerLevel: 10,
270
- height: 5.2,
271
- weight: 7000,
272
- status: "active",
273
- }).execute();
274
-
275
- // Get a dinosaur
276
- const dino = await dinosaurRepo.get({
277
- id: "dino-001",
278
- diet: "carnivore",
279
- species: "Tyrannosaurus Rex",
280
- }).execute();
281
-
282
- // Update a dinosaur
283
- await dinosaurRepo.update(
284
- { id: "dino-001", diet: "carnivore", species: "Tyrannosaurus Rex" },
285
- { weight: 7200, status: "sick" }
286
- ).execute();
287
-
288
- // Delete a dinosaur
289
- await dinosaurRepo.delete({
290
- id: "dino-001",
291
- diet: "carnivore",
292
- species: "Tyrannosaurus Rex",
293
- }).execute();
294
- ```
295
-
296
- #### 3. Custom Queries
297
-
298
- Define custom queries with input validation:
299
-
300
- ```ts
301
- import { createQueries } from "dyno-table/entity";
302
-
303
- const createQuery = createQueries<Dinosaur>();
304
-
305
- const DinosaurEntity = defineEntity({
306
- name: "Dinosaur",
307
- schema: dinosaurSchema,
308
- primaryKey,
309
- queries: {
310
- byDiet: createQuery
311
- .input(
312
- z.object({
313
- diet: z.enum(["carnivore", "herbivore", "omnivore"]),
314
- })
315
- )
316
- .query(({ input, entity }) => {
317
- return entity
318
- .scan()
319
- .filter((op) => op.eq("diet", input.diet));
320
- }),
321
-
322
- bySpecies: createQuery
323
- .input(
324
- z.object({
325
- species: z.string(),
326
- })
327
- )
328
- .query(({ input, entity }) => {
329
- return entity
330
- .scan()
331
- .filter((op) => op.eq("species", input.species));
332
- }),
333
- },
334
- });
335
-
336
- // Use the custom queries
337
- const carnivores = await dinosaurRepo.query.byDiet({ diet: "carnivore" }).execute();
338
- const trexes = await dinosaurRepo.query.bySpecies({ species: "Tyrannosaurus Rex" }).execute();
339
- ```
340
-
341
- #### 4. Indexes for Efficient Querying
342
-
343
- Define indexes for efficient access patterns:
344
-
345
- ```ts
346
- import { createIndex } from "dyno-table/entity";
347
-
348
- // Define GSI for querying by species
349
- const speciesIndex = createIndex()
350
- .input(dinosaurSchema)
351
- .partitionKey(({ species }) => `SPECIES#${species}`)
352
- .sortKey(({ id }) => `DINOSAUR#${id}`);
353
-
354
- const DinosaurEntity = defineEntity({
355
- name: "Dinosaur",
356
- schema: dinosaurSchema,
357
- primaryKey,
358
- indexes: {
359
- species: speciesIndex,
360
- },
361
- queries: {
362
- bySpecies: createQuery
363
- .input(
364
- z.object({
365
- species: z.string(),
366
- })
367
- )
368
- .query(({ input, entity }) => {
369
- return entity
370
- .queryBuilder({
371
- pk: `SPECIES#${input.species}`,
372
- })
373
- .useIndex("species");
374
- }),
375
- },
376
- });
377
- ```
378
-
379
- #### 5. Lifecycle Hooks
380
-
381
- Add hooks for pre/post processing:
382
-
383
- ```ts
384
- const dinosaurHooks = {
385
- afterGet: async (data: Dinosaur | undefined) => {
386
- if (data) {
387
- return {
388
- ...data,
389
- displayName: `${data.name} (${data.species})`,
390
- threatLevel: data.dangerLevel > 7 ? "HIGH" : "MODERATE",
391
- };
392
- }
393
- return data;
394
- },
395
- };
396
-
397
- const DinosaurEntity = defineEntity({
398
- name: "Dinosaur",
399
- schema: dinosaurSchema,
400
- primaryKey,
401
- hooks: dinosaurHooks,
402
- });
403
- ```
404
-
405
- ### Complete Entity Example
406
-
407
- Here's a complete example using Zod schemas directly:
408
-
409
- ```ts
410
- import { z } from "zod";
411
- import { defineEntity, createQueries, createIndex } from "dyno-table/entity";
412
- import { Table } from "dyno-table/table";
413
-
414
- // Define the schema with Zod
415
- const dinosaurSchema = z.object({
416
- id: z.string(),
417
- species: z.string(),
418
- name: z.string(),
419
- enclosureId: z.string(),
420
- diet: z.enum(["carnivore", "herbivore", "omnivore"]),
421
- dangerLevel: z.number().int().min(1).max(10),
422
- height: z.number().positive(),
423
- weight: z.number().positive(),
424
- status: z.enum(["active", "inactive", "sick", "deceased"]),
425
- trackingChipId: z.string().optional(),
426
- lastFed: z.string().optional(),
427
- createdAt: z.string().optional(),
428
- updatedAt: z.string().optional(),
429
- });
430
-
431
- // Infer the type from the schema
432
- type Dinosaur = z.infer<typeof dinosaurSchema>;
433
-
434
- // Define key templates
435
- const dinosaurPK = (id: string) => `DINOSAUR#${id}`;
436
- const dinosaurSK = (status: string) => `STATUS#${status}`;
437
-
438
- // Create a primary index
439
- const primaryKey = createIndex()
440
- .input(dinosaurSchema)
441
- .partitionKey(({ id }) => dinosaurPK(id))
442
- .sortKey(({ status }) => dinosaurSK(status));
443
-
444
- // Create a GSI for querying by species
445
- const speciesIndex = createIndex()
446
- .input(dinosaurSchema)
447
- .partitionKey(({ species }) => `SPECIES#${species}`)
448
- .sortKey(({ id }) => `DINOSAUR#${id}`);
449
-
450
- // Create a GSI for querying by enclosure
451
- const enclosureIndex = createIndex()
452
- .input(dinosaurSchema)
453
- .partitionKey(({ enclosureId }) => `ENCLOSURE#${enclosureId}`)
454
- .sortKey(({ id }) => `DINOSAUR#${id}`);
455
-
456
- // Create query builders
457
- const createQuery = createQueries<Dinosaur>();
458
-
459
- // Define the entity
460
- const DinosaurEntity = defineEntity({
461
- name: "Dinosaur",
462
- schema: dinosaurSchema,
463
- primaryKey,
464
- indexes: {
465
- species: speciesIndex,
466
- enclosure: enclosureIndex,
467
- },
468
- queries: {
469
- bySpecies: createQuery
470
- .input(
471
- z.object({
472
- species: z.string(),
473
- })
474
- )
475
- .query(({ input, entity }) => {
476
- return entity
477
- .queryBuilder({
478
- pk: `SPECIES#${input.species}`,
479
- })
480
- .useIndex("species");
481
- }),
482
-
483
- byEnclosure: createQuery
484
- .input(
485
- z.object({
486
- enclosureId: z.string(),
487
- })
488
- )
489
- .query(({ input, entity }) => {
490
- return entity
491
- .queryBuilder({
492
- pk: `ENCLOSURE#${input.enclosureId}`,
493
- })
494
- .useIndex("enclosure");
495
- }),
496
-
497
- dangerousInEnclosure: createQuery
498
- .input(
499
- z.object({
500
- enclosureId: z.string(),
501
- minDangerLevel: z.number().int().min(1).max(10),
502
- })
503
- )
504
- .query(({ input, entity }) => {
505
- return entity
506
- .queryBuilder({
507
- pk: `ENCLOSURE#${input.enclosureId}`,
508
- })
509
- .useIndex("enclosure")
510
- .filter((op) => op.gte("dangerLevel", input.minDangerLevel));
511
- }),
512
- },
513
- });
514
-
515
- // Create a repository
516
- const dinosaurRepo = DinosaurEntity.createRepository(table);
517
-
518
- // Use the repository
519
- async function main() {
520
- // Create a dinosaur
521
- await dinosaurRepo
522
- .create({
523
- id: "dino-001",
524
- species: "Tyrannosaurus Rex",
525
- name: "Rexy",
526
- enclosureId: "enc-001",
527
- diet: "carnivore",
528
- dangerLevel: 10,
529
- height: 5.2,
530
- weight: 7000,
531
- status: "active",
532
- trackingChipId: "TRX-001",
533
- })
534
- .execute();
535
-
536
- // Query dinosaurs by species
537
- const trexes = await dinosaurRepo.query.bySpecies({
538
- species: "Tyrannosaurus Rex"
539
- }).execute();
540
-
541
- // Query dangerous dinosaurs in an enclosure
542
- const dangerousDinos = await dinosaurRepo.query.dangerousInEnclosure({
543
- enclosureId: "enc-001",
544
- minDangerLevel: 8,
545
- }).execute();
546
- }
547
- ```
548
-
549
- **Key benefits:**
550
- - 🚫 Prevents accidental cross-type data access
551
- - 🔍 Automatically filters queries/scans to repository type
552
- - 🛡️ Ensures consistent key structure across entities
553
- - 📦 Encapsulates domain-specific query logic
554
- - 🧪 Validates data with Zod schemas
555
- - 🔄 Provides type inference from schemas
556
-
557
159
  ## 🧩 Advanced Features
558
160
 
559
161
  ### Transactional Operations
560
162
 
561
163
  **Safe dinosaur transfer between enclosures**
562
164
  ```ts
563
- // Start a transaction session for transferring a T-Rex to a new enclosure
564
- // Critical for safety: All operations must succeed or none will be applied
165
+ // Start a transaction session for transferring a dinosaur
565
166
  await dinoTable.transaction(async (tx) => {
566
167
  // All operations are executed as a single transaction (up to 100 operations)
567
- // This ensures the dinosaur transfer is atomic - preventing half-completed transfers
568
168
 
569
- // STEP 1: Check if destination enclosure is ready and compatible with the dinosaur
570
- // We must verify the enclosure is prepared and suitable for a carnivore
169
+ // Check if destination enclosure is ready and compatible
571
170
  await dinoTable
572
171
  .conditionCheck({
573
- pk: "ENCLOSURE#B", // Target enclosure B
574
- sk: "STATUS" // Check the enclosure status record
172
+ pk: "ENCLOSURE#B",
173
+ sk: "STATUS"
575
174
  })
576
175
  .condition(op => op.and(
577
- op.eq("status", "READY"), // Enclosure must be in READY state
578
- op.eq("diet", "Carnivore") // Must support carnivorous dinosaurs
176
+ op.eq("status", "READY"),
177
+ op.eq("diet", "Carnivore") // Ensure enclosure matches dinosaur diet
579
178
  ))
580
179
  .withTransaction(tx);
581
180
 
582
- // STEP 2: Remove dinosaur from current enclosure
583
- // Only proceed if the dinosaur is healthy enough for transfer
181
+ // Remove dinosaur from current enclosure
584
182
  await dinoTable
585
183
  .delete<Dinosaur>({
586
- pk: "ENCLOSURE#A", // Source enclosure A
587
- sk: "DINO#001" // T-Rex with ID 001
184
+ pk: "ENCLOSURE#A",
185
+ sk: "DINO#001"
588
186
  })
589
187
  .condition(op => op.and(
590
- op.eq("status", "HEALTHY"), // Dinosaur must be in HEALTHY state
591
- op.gte("health", 80) // Health must be at least 80%
188
+ op.eq("status", "HEALTHY"),
189
+ op.gte("health", 80) // Only transfer healthy dinosaurs
592
190
  ))
593
191
  .withTransaction(tx);
594
192
 
595
- // STEP 3: Add dinosaur to new enclosure
596
- // Create a fresh record in the destination enclosure
193
+ // Add dinosaur to new enclosure
597
194
  await dinoTable
598
195
  .create<Dinosaur>({
599
- pk: "ENCLOSURE#B", // Destination enclosure B
600
- sk: "DINO#001", // Same dinosaur ID for tracking
601
- name: "Rex", // Dinosaur name
602
- species: "Tyrannosaurus", // Species classification
603
- diet: "Carnivore", // Dietary requirements
604
- status: "HEALTHY", // Current health status
605
- health: 100, // Reset health to 100% after transfer
606
- enclosureId: "B", // Update enclosure reference
607
- lastFed: new Date().toISOString() // Reset feeding clock
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()
608
205
  })
609
206
  .withTransaction(tx);
610
207
 
611
- // STEP 4: Update enclosure occupancy tracking
612
- // Keep accurate count of dinosaurs in each enclosure
208
+ // Update enclosure occupancy tracking
613
209
  await dinoTable
614
210
  .update<Dinosaur>({
615
- pk: "ENCLOSURE#B", // Target enclosure B
616
- sk: "OCCUPANCY" // Occupancy tracking record
211
+ pk: "ENCLOSURE#B",
212
+ sk: "OCCUPANCY"
617
213
  })
618
- .add("currentOccupants", 1) // Increment occupant count
619
- .set("lastUpdated", new Date().toISOString()) // Update timestamp
214
+ .add("currentOccupants", 1)
215
+ .set("lastUpdated", new Date().toISOString())
620
216
  .withTransaction(tx);
621
217
  });
622
218
 
623
- // Transaction for dinosaur feeding and health monitoring
624
- // Ensures feeding status and schedule are updated atomically
219
+ // Transaction with feeding and health monitoring
625
220
  await dinoTable.transaction(
626
221
  async (tx) => {
627
- // STEP 1: Update Stegosaurus health and feeding status
628
- // Record that the dinosaur has been fed and update its health metrics
222
+ // Update dinosaur health and feeding status
629
223
  await dinoTable
630
224
  .update<Dinosaur>({
631
- pk: "ENCLOSURE#D", // Herbivore enclosure D
632
- sk: "DINO#003" // Stegosaurus with ID 003
225
+ pk: "ENCLOSURE#D",
226
+ sk: "DINO#003"
633
227
  })
634
228
  .set({
635
- status: "HEALTHY", // Update health status
636
- lastFed: new Date().toISOString(), // Record feeding time
637
- health: 100 // Reset health to 100%
229
+ status: "HEALTHY",
230
+ lastFed: new Date().toISOString(),
231
+ health: 100
638
232
  })
639
- .deleteElementsFromSet("tags", ["needs_feeding"]) // Remove feeding alert tag
233
+ .deleteElementsFromSet("tags", ["needs_feeding"])
640
234
  .withTransaction(tx);
641
235
 
642
- // STEP 2: Update enclosure feeding schedule
643
- // Schedule next feeding time for tomorrow
236
+ // Update enclosure feeding schedule
644
237
  await dinoTable
645
238
  .update<Dinosaur>({
646
- pk: "ENCLOSURE#D", // Same herbivore enclosure
647
- sk: "SCHEDULE" // Feeding schedule record
239
+ pk: "ENCLOSURE#D",
240
+ sk: "SCHEDULE"
648
241
  })
649
- .set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()) // 24 hours from now
242
+ .set("nextFeedingTime", new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString())
650
243
  .withTransaction(tx);
651
244
  },
652
245
  {
653
- // Transaction options for tracking and idempotency
654
- clientRequestToken: "feeding-session-001", // Prevents duplicate feeding operations
655
- returnConsumedCapacity: "TOTAL" // Track capacity usage for park operations
246
+ clientRequestToken: "feeding-session-001",
247
+ returnConsumedCapacity: "TOTAL"
656
248
  }
657
249
  );
658
250
  ```
@@ -669,42 +261,35 @@ await dinoTable.transaction(
669
261
 
670
262
  **Efficient dinosaur park management with bulk operations**
671
263
  ```ts
672
- // SCENARIO 1: Morning health check for multiple dinosaurs across enclosures
673
- // Retrieve health status for multiple dinosaurs in a single operation
264
+ // Batch health check for multiple dinosaurs
674
265
  const healthCheckKeys = [
675
- { pk: "ENCLOSURE#A", sk: "DINO#001" }, // T-Rex in Paddock A
676
- { pk: "ENCLOSURE#B", sk: "DINO#002" }, // Velociraptor in Paddock B
677
- { pk: "ENCLOSURE#C", sk: "DINO#003" } // Stegosaurus in Paddock C
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
678
269
  ];
679
270
 
680
- // Perform batch get operation to retrieve all dinosaurs at once
681
- // This is much more efficient than individual gets
682
271
  const { items: dinosaurs, unprocessedKeys } = await dinoTable.batchGet<Dinosaur>(healthCheckKeys);
683
272
  console.log(`Health check completed for ${dinosaurs.length} dinosaurs`);
684
-
685
- // Process health check results and identify any dinosaurs needing attention
686
273
  dinosaurs.forEach(dino => {
687
274
  if (dino.health < 80) {
688
275
  console.log(`Health alert for ${dino.name} in Enclosure ${dino.enclosureId}`);
689
- // In a real application, you might trigger alerts or schedule veterinary visits
690
276
  }
691
277
  });
692
278
 
693
- // SCENARIO 2: Adding new herbivores to the park after quarantine
694
- // Prepare data for multiple new herbivores joining the collection
279
+ // Batch update feeding schedule for herbivore group
695
280
  const newHerbivores = [
696
281
  {
697
282
  pk: "ENCLOSURE#D", sk: "DINO#004",
698
- name: "Triceratops Alpha", // Three-horned herbivore
283
+ name: "Triceratops Alpha",
699
284
  species: "Triceratops",
700
285
  diet: "Herbivore",
701
286
  status: "HEALTHY",
702
- health: 95, // Excellent health after quarantine
703
- lastFed: new Date().toISOString() // Just fed before joining main enclosure
287
+ health: 95,
288
+ lastFed: new Date().toISOString()
704
289
  },
705
290
  {
706
291
  pk: "ENCLOSURE#D", sk: "DINO#005",
707
- name: "Brachy", // Long-necked herbivore
292
+ name: "Brachy",
708
293
  species: "Brachiosaurus",
709
294
  diet: "Herbivore",
710
295
  status: "HEALTHY",
@@ -713,117 +298,91 @@ const newHerbivores = [
713
298
  }
714
299
  ];
715
300
 
716
- // Add all new herbivores to the enclosure in a single batch operation
717
- // More efficient than individual writes and ensures consistent state
301
+ // Add new herbivores to enclosure
718
302
  await dinoTable.batchWrite(
719
303
  newHerbivores.map(dino => ({
720
- type: "put", // Create or replace operation
721
- item: dino // Full dinosaur record
304
+ type: "put",
305
+ item: dino
722
306
  }))
723
307
  );
724
308
 
725
- // SCENARIO 3: Releasing a dinosaur from quarantine to general population
726
- // Multiple related operations performed as a batch
309
+ // Mixed operations: relocate dinosaurs and update enclosure status
727
310
  await dinoTable.batchWrite([
728
- // Step 1: Remove dinosaur from quarantine enclosure
729
- {
730
- type: "delete",
731
- key: { pk: "ENCLOSURE#QUARANTINE", sk: "DINO#006" }
732
- },
733
-
734
- // Step 2: Add recovered dinosaur to main raptor enclosure
311
+ // Remove dinosaur from quarantine
312
+ { type: "delete", key: { pk: "ENCLOSURE#QUARANTINE", sk: "DINO#006" } },
313
+ // Add recovered dinosaur to main enclosure
735
314
  {
736
315
  type: "put",
737
316
  item: {
738
317
  pk: "ENCLOSURE#E", sk: "DINO#006",
739
- name: "Raptor Beta", // Juvenile Velociraptor
318
+ name: "Raptor Beta",
740
319
  species: "Velociraptor",
741
320
  diet: "Carnivore",
742
- status: "HEALTHY", // Now healthy after treatment
321
+ status: "HEALTHY",
743
322
  health: 100,
744
323
  lastFed: new Date().toISOString()
745
324
  }
746
325
  },
747
-
748
- // Step 3: Clear quarantine status record
749
- {
750
- type: "delete",
751
- key: { pk: "ENCLOSURE#QUARANTINE", sk: "STATUS#DINO#006" }
752
- }
326
+ // Clear quarantine status
327
+ { type: "delete", key: { pk: "ENCLOSURE#QUARANTINE", sk: "STATUS#DINO#006" } }
753
328
  ]);
754
329
 
755
- // SCENARIO 4: Daily park-wide health monitoring
756
- // Handle large-scale operations across all dinosaurs
757
- // The library automatically handles chunking for large batches:
758
- // - 25 items per batch write
759
- // - 100 items per batch get
330
+ // Handle large-scale park operations
331
+ // (25 items per batch write, 100 items per batch get)
760
332
  const dailyHealthUpdates = generateDinosaurHealthUpdates(); // Hundreds of updates
761
- await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked into multiple requests
333
+ await dinoTable.batchWrite(dailyHealthUpdates); // Automatically chunked
762
334
  ```
763
335
 
764
336
  ### Pagination Made Simple
765
337
 
766
- **Efficient dinosaur record browsing for park management**
338
+ **Efficient dinosaur record browsing**
767
339
  ```ts
768
- // SCENARIO 1: Herbivore health monitoring with pagination
769
- // Create a paginator for viewing healthy herbivores in manageable chunks
770
- // Perfect for veterinary staff doing routine health checks
340
+ // Create a paginator for viewing herbivores by health status
771
341
  const healthyHerbivores = dinoTable
772
342
  .query<Dinosaur>({
773
- pk: "DIET#herbivore", // Target all herbivorous dinosaurs
774
- sk: op => op.beginsWith("STATUS#HEALTHY") // Only those with HEALTHY status
343
+ pk: "DIET#herbivore",
344
+ sk: op => op.beginsWith("STATUS#HEALTHY")
775
345
  })
776
346
  .filter((op) => op.and(
777
- op.gte("health", 90), // Only those with excellent health (90%+)
778
- op.attributeExists("lastFed") // Must have feeding records
347
+ op.gte("health", 90),
348
+ op.attributeExists("lastFed")
779
349
  ))
780
- .paginate(5); // Process in small batches of 5 dinosaurs
350
+ .paginate(5); // View 5 dinosaurs at a time
781
351
 
782
- // Iterate through all pages of results - useful for processing large datasets
783
- // without loading everything into memory at once
784
- console.log("🦕 Beginning herbivore health inspection rounds...");
352
+ // Monitor all enclosures page by page
785
353
  while (healthyHerbivores.hasNextPage()) {
786
- // Get the next page of dinosaurs
787
354
  const page = await healthyHerbivores.getNextPage();
788
355
  console.log(`Checking herbivores page ${page.page}, found ${page.items.length} dinosaurs`);
789
-
790
- // Process each dinosaur in the current page
791
356
  page.items.forEach(dino => {
792
357
  console.log(`${dino.name}: Health ${dino.health}%, Last fed: ${dino.lastFed}`);
793
- // In a real app, you might update health records or schedule next checkup
794
358
  });
795
359
  }
796
360
 
797
- // SCENARIO 2: Preparing carnivore feeding schedule
798
- // Get all carnivores at once for daily feeding planning
799
- // This approach loads all matching items into memory
361
+ // Get all carnivores for daily feeding schedule
800
362
  const carnivoreSchedule = await dinoTable
801
363
  .query<Dinosaur>({
802
- pk: "DIET#carnivore", // Target all carnivorous dinosaurs
803
- sk: op => op.beginsWith("ENCLOSURE#") // Organized by enclosure
364
+ pk: "DIET#carnivore",
365
+ sk: op => op.beginsWith("ENCLOSURE#")
804
366
  })
805
- .filter(op => op.attributeExists("lastFed")) // Only those with feeding records
806
- .paginate(10) // Process in pages of 10
807
- .getAllPages(); // But collect all results at once
367
+ .filter(op => op.attributeExists("lastFed"))
368
+ .paginate(10)
369
+ .getAllPages();
808
370
 
809
371
  console.log(`Scheduling feeding for ${carnivoreSchedule.length} carnivores`);
810
- // Now we can sort and organize feeding times based on species, size, etc.
811
372
 
812
- // SCENARIO 3: Visitor information kiosk with limited display
813
- // Create a paginated view for the public-facing dinosaur information kiosk
373
+ // Limited view for visitor information kiosk
814
374
  const visitorKiosk = dinoTable
815
375
  .query<Dinosaur>({
816
- pk: "VISITOR_VIEW", // Special partition for visitor-facing data
817
- sk: op => op.beginsWith("SPECIES#") // Organized by species
376
+ pk: "VISITOR_VIEW",
377
+ sk: op => op.beginsWith("SPECIES#")
818
378
  })
819
- .filter(op => op.eq("status", "ON_DISPLAY")) // Only show dinosaurs currently on display
820
- .limit(12) // Show maximum 12 dinosaurs total
821
- .paginate(4); // Display 4 at a time for easy viewing
379
+ .filter(op => op.eq("status", "ON_DISPLAY"))
380
+ .limit(12) // Show max 12 dinosaurs per view
381
+ .paginate(4); // Display 4 at a time
822
382
 
823
- // Get first page for initial kiosk display
383
+ // Get first page for kiosk display
824
384
  const firstPage = await visitorKiosk.getNextPage();
825
- console.log(`🦖 Now showing: ${firstPage.items.map(d => d.name).join(", ")}`);
826
- // Visitors can press "Next" to see more dinosaurs in the collection
385
+ console.log(`Now showing: ${firstPage.items.map(d => d.name).join(", ")}`);
827
386
  ```
828
387
 
829
388
  ## 🛡️ Type-Safe Query Building
@@ -1111,6 +670,24 @@ const result = await table.transaction(
1111
670
  );
1112
671
  ```
1113
672
 
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
1114
691
 
1115
692
  ## 🚨 Error Handling
1116
693
 
@@ -1273,6 +850,41 @@ pnpm test
1273
850
  pnpm build
1274
851
  ```
1275
852
 
853
+ ## 📦 Release Process
854
+
855
+ This project uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated versioning and package publishing. The configuration is maintained in the `.releaserc.json` file. Releases are automatically triggered by commits to specific branches:
856
+
857
+ - **Main Channel**: Stable releases from the `main` branch
858
+ - **Alpha Channel**: Pre-releases from the `alpha` branch
859
+
860
+ ### Commit Message Format
861
+
862
+ We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages, which determines the release type:
863
+
864
+ - `fix: ...` - Patch release (bug fixes)
865
+ - `feat: ...` - Minor release (new features)
866
+ - `feat!: ...` or `fix!: ...` or any commit with `BREAKING CHANGE:` in the footer - Major release
867
+
868
+ ### Release Workflow
869
+
870
+ 1. For regular features and fixes:
871
+ - Create a PR against the `main` branch
872
+ - Once merged, a new release will be automatically published
873
+
874
+ 2. For experimental features:
875
+ - Create a PR against the `alpha` branch
876
+ - Once merged, a new alpha release will be published with an alpha tag
877
+
878
+ ### Installing Specific Channels
879
+
880
+ ```bash
881
+ # Install the latest stable version
882
+ npm install dyno-table
883
+
884
+ # Install the latest alpha version
885
+ npm install dyno-table@alpha
886
+ ```
887
+
1276
888
  ## 🦔 Running Examples
1277
889
 
1278
890
  There's a few pre-configured example scripts in the `examples` directory.
@@ -1296,10 +908,3 @@ npx tsx examples/[EXAMPLE_NAME].ts
1296
908
  ```
1297
909
 
1298
910
  To view the test table GUI in action: [DynamoDB Admin](http://localhost:8001/)
1299
-
1300
- <br />
1301
- To teardown the test table when you're done, run the following command:
1302
-
1303
- ```bash
1304
- pnpm run local:teardown
1305
- ```