@nikovirtala/typesafe-dynamodb 0.0.0 → 0.0.2

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 (49) hide show
  1. package/README.md +56 -56
  2. package/examples/schema-validated-delete-document.ts +63 -0
  3. package/examples/schema-validated-get-document.ts +59 -0
  4. package/examples/schema-validated-put-document.ts +64 -0
  5. package/examples/schema-validated-query-document.ts +69 -0
  6. package/examples/schema-validated-scan-document.ts +57 -0
  7. package/examples/schema-validated-stream-event.ts +60 -0
  8. package/examples/schema-validated-update-document.ts +75 -0
  9. package/lib/client-v3.d.ts +2 -8
  10. package/lib/client-v3.js +1 -1
  11. package/lib/delete-item.d.ts +3 -4
  12. package/lib/delete-item.js +1 -1
  13. package/lib/document-client-v3.d.ts +2 -8
  14. package/lib/document-client-v3.js +1 -1
  15. package/lib/get-item.d.ts +3 -3
  16. package/lib/get-item.js +1 -1
  17. package/lib/put-item.d.ts +3 -4
  18. package/lib/put-item.js +1 -1
  19. package/lib/query.d.ts +3 -4
  20. package/lib/query.js +1 -1
  21. package/lib/scan.d.ts +3 -4
  22. package/lib/scan.js +1 -1
  23. package/lib/schema-validated-delete-document-command.d.ts +6 -0
  24. package/lib/schema-validated-delete-document-command.js +10 -0
  25. package/lib/schema-validated-document-client.d.ts +35 -0
  26. package/lib/schema-validated-document-client.js +25 -0
  27. package/lib/schema-validated-get-document-command.d.ts +6 -0
  28. package/lib/schema-validated-get-document-command.js +10 -0
  29. package/lib/schema-validated-put-document-command.d.ts +6 -0
  30. package/lib/schema-validated-put-document-command.js +10 -0
  31. package/lib/schema-validated-query-document-command.d.ts +6 -0
  32. package/lib/schema-validated-query-document-command.js +10 -0
  33. package/lib/schema-validated-scan-document-command.d.ts +6 -0
  34. package/lib/schema-validated-scan-document-command.js +10 -0
  35. package/lib/schema-validated-stream-event.d.ts +4 -0
  36. package/lib/schema-validated-stream-event.js +30 -0
  37. package/lib/schema-validated-update-document-command.d.ts +6 -0
  38. package/lib/schema-validated-update-document-command.js +10 -0
  39. package/lib/update-item.d.ts +3 -4
  40. package/lib/update-item.js +1 -1
  41. package/package.json +5 -4
  42. package/perf/README.md +114 -0
  43. package/perf/performance-test.ts +563 -0
  44. package/lib/callback.d.ts +0 -2
  45. package/lib/callback.js +0 -3
  46. package/lib/client-v2.d.ts +0 -18
  47. package/lib/client-v2.js +0 -3
  48. package/lib/document-client-v2.d.ts +0 -18
  49. package/lib/document-client-v2.js +0 -3
package/README.md CHANGED
@@ -1,32 +1,30 @@
1
- # typesafe-dynamodb
1
+ # @nikovirtala/typesafe-dynamodb
2
2
 
3
- [![npm version](https://badge.fury.io/js/typesafe-dynamodb.svg)](https://badge.fury.io/js/typesafe-dynamodb)
3
+ [![npm version](https://badge.fury.io/js/@nikovirtala%2Ftypesafe-dynamodb.svg)](https://badge.fury.io/js/@nikovirtala%2Ftypesafe-dynamodb)
4
4
 
5
- `typesafe-dynamodb` is a type-only library which replaces the type signatures of the AWS SDK's DynamoDB client. It substitutes `getItem`, `putItem`, `deleteItem` and `query` API methods with type-safe alternatives that are aware of the data in your tables and also adaptive to the semantics of the API request, e.g. by validating `ExpressionAttributeNames` and `ExpressionAttributeValues` contain all the values used in a `ConditionExpression` string, or by understanding the effect of a `ProjectionExpression` on the returned data type.
5
+ `@nikovirtala/typesafe-dynamodb` is a fork of `typesafe-dynamodb` (a type-only library which replaces the type signatures of the AWS SDK v3's DynamoDB client) with schema validation based on [zod](https://zod.dev).
6
6
 
7
- The end goal is to provide types that have total understanding of the AWS DynamoDB API and enable full utilization of the TypeScript type system for modeling complex DynamoDB tables, such as the application of union types and template string literals for single-table designs.
7
+ It substitutes `getItem`, `putItem`, `deleteItem` and `query` API methods with type-safe and schema validated alternatives that are aware of the data in your tables and also adaptive to the semantics of the API request, e.g. by validating `ExpressionAttributeNames` and `ExpressionAttributeValues` contain all the values used in a `ConditionExpression` string, or by understanding the effect of a `ProjectionExpression` on the returned data type.
8
+
9
+ The end goal is to provide types and validation that have total understanding of the AWS DynamoDB API and enable full utilization of the TypeScript type system for modeling complex DynamoDB Tables, such as the application of union types and template string literals for single-table designs without forgetting runtime safety.
8
10
 
9
11
  ![typesafe putItem ConditionExpression](img/put-item-expression.gif)
10
12
 
11
13
  ## Installation
12
14
 
13
15
  ```
14
- npm install --save-dev typesafe-dynamodb
16
+ npm install --save-dev @nikovirtala/typesafe-dynamodb
15
17
  ```
16
18
 
17
19
  ## Usage
18
20
 
19
- This library contains type definitions for both AWS SDK v2 (`aws-sdk`) and AWS SDK v3 (`@aws-sdk/client-dynamodb`);
20
-
21
- ### AWS SDK v2
21
+ This library contains type definitions for AWS SDK v3 (`@aws-sdk/client-dynamodb`).
22
22
 
23
- To use `typesafe-dynamodb` with the AWS SDK v2, there is no need to change anything about your existing runtime code. It is purely type definitions, so you only need to cast an instance of `AWS.DynamoDB` to the `TypeSafeDynamoDBv2<T, HashKey, RangeKey>` interface and use the client as normal, except now you can enjoy a dynamic, type-safe experience in your IDE instead.
23
+ ### AWS SDK v3
24
24
 
25
- ```ts
26
- import { DynamoDB } from "aws-sdk";
25
+ #### Option 1 - DynamoDB
27
26
 
28
- const client = new DynamoDB();
29
- ```
27
+ `DynamoDB` is a convenient way of using the DynamoDB API, except it is not optimized for tree-shaking (for that, see Option 2).
30
28
 
31
29
  Start by declaring a standard TypeScript interface which describes the structure of data in your DynamoDB Table:
32
30
 
@@ -40,25 +38,7 @@ interface Record {
40
38
  }
41
39
  ```
42
40
 
43
- Then, cast the `DynamoDB` client instance to `TypeSafeDynamoDB`;
44
-
45
- ```ts
46
- import { TypeSafeDynamoDBv2 } from "typesafe-dynamodb/lib/client-v2";
47
-
48
- const typesafeClient: TypeSafeDynamoDBv2<Record, "key", "sort"> = client;
49
- ```
50
-
51
- `"key"` is the name of the Hash Key attribute, and `"sort"` is the name of the Range Key attribute.
52
-
53
- Finally, use the client as you normally would, except now with intelligent type hints and validations.
54
-
55
- ### AWS SDK v3
56
-
57
- #### Option 1 - DynamoDB (similar to SDK v2)
58
-
59
- `DynamoDB` is an almost identical implementation to the AWS SDK v2, except with minor changes such as returning a `Promise` by default. It is a convenient way of using the DynamoDB API, except it is not optimized for tree-shaking (for that, see Option 2).
60
-
61
- To override the types, follow a similar method to v2, except by importing TypeSafeDynamoDBv3 (instead of v2):
41
+ Then, cast the `DynamoDB` client instance to `TypeSafeDynamoDBv3`:
62
42
 
63
43
  ```ts
64
44
  import { DynamoDB } from "@aws-sdk/client-dynamodb";
@@ -68,6 +48,10 @@ const client = new DynamoDB({..});
68
48
  const typesafeClient: TypeSafeDynamoDBv3<Record, "key", "sort"> = client;
69
49
  ```
70
50
 
51
+ `"key"` is the name of the Hash Key attribute, and `"sort"` is the name of the Range Key attribute.
52
+
53
+ Finally, use the client as you normally would, except now with intelligent type hints and validations.
54
+
71
55
  #### Option 2 - DynamoDBClient (a Command-Response interface optimized for tree-shaking)
72
56
 
73
57
  `DynamoDBClient` is a generic interface with a single method, `send`. To invoke an API, call `send` with an instance of the API's corresponding `Command`.
@@ -96,26 +80,7 @@ await client.send(
96
80
 
97
81
  ### Document Client
98
82
 
99
- Both the AWS SDK v2 and v3 provide a javascript-friendly interface called the `DocumentClient`. Instead of using the AttributeValue format, such as `{ S: "hello" }` or `{ N: "123" }`, the `DocumentClient` enables you to use native javascript types, e.g. `"hello"` or `123`.
100
-
101
- #### AWS SDK V2
102
-
103
- For the SDK V2 client, cast it to `TypeSafeDocumentClientV2`.
104
-
105
- See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html
106
-
107
- ```ts
108
- import { DynamoDB } from "aws-sdk";
109
- import { TypeSafeDocumentClientV2 } from "typesafe-dynamodb/lib/document-client-v2";
110
-
111
- const table = new DynamoDB.DocumentClient() as TypeSafeDocumentClientV2<
112
- MyItem,
113
- "pk",
114
- "sk"
115
- >;
116
- ```
117
-
118
- #### AWS SDK V3
83
+ The AWS SDK v3 provides a javascript-friendly interface called the `DocumentClient`. Instead of using the AttributeValue format, such as `{ S: "hello" }` or `{ N: "123" }`, the `DocumentClient` enables you to use native javascript types, e.g. `"hello"` or `123`.
119
84
 
120
85
  When defining your Command types, use the corresponding `TypeSafe*DocumentCommand` type, for example `TypeSafeGetDocumentCommand` instead of `TypeSafeGetItemCommand`:
121
86
 
@@ -135,7 +100,7 @@ import { TypeSafeGetDocumentCommand } from "typesafe-dynamodb/lib/get-document-c
135
100
  const MyGetItemCommand = TypeSafeGetDocumentCommand<MyType, "key", "sort">();
136
101
  ```
137
102
 
138
- For the SDK V3 client, cast it to `TypeSafeDynamoDBv3`.
103
+ For the SDK V3 client, cast it to `TypeSafeDocumentClientV3`:
139
104
 
140
105
  ```ts
141
106
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
@@ -145,7 +110,7 @@ import { TypeSafeDocumentClientV3 } from "typesafe-dynamodb/lib/document-client-
145
110
  const client = new DynamoDBClient({});
146
111
 
147
112
  const docClient = DynamoDBDocumentClient.from(
148
- client
113
+ client,
149
114
  ) as TypeSafeDocumentClientV3<MyType, "key", "sort">;
150
115
  ```
151
116
 
@@ -171,7 +136,7 @@ Same for the `Item` in the response:
171
136
 
172
137
  ### Single Table Design
173
138
 
174
- Below are two `interface` declarations, representing two types of data stored in a single DynamoDB table - `User` and `Order`. Single table design in DynamoDB is achieved by creating "composite keys", e.g. `USER#${UserID}`. In TypeScript, we use template literal types to encode this in the Type System.
139
+ Below are two `interface` declarations, representing two types of data stored in a single DynamoDB Table - `User` and `Order`. Single table design in DynamoDB is achieved by creating "composite keys", e.g. `USER#${UserID}`. In TypeScript, we use template literal types to encode this in the Type System.
175
140
 
176
141
  ```ts
177
142
  interface User<UserID extends string = string> {
@@ -186,7 +151,7 @@ interface User<UserID extends string = string> {
186
151
 
187
152
  interface Order<
188
153
  UserID extends string = string,
189
- OrderID extends string = string
154
+ OrderID extends string = string,
190
155
  > {
191
156
  PK: `USER#${UserID}`;
192
157
  SK: `ORDER#${OrderID}`;
@@ -224,6 +189,41 @@ export async function handle(
224
189
 
225
190
  The event's type is derived from the data type and the the `StreamViewType`, e.g. `"NEW_IMAGE" | "OLD_IMAGE" | "KEYS_ONLY" | "NEW_AND_OLD_IMAGES"`.
226
191
 
192
+ ### Schema-Validated DynamoDBStreamEvent
193
+
194
+ Validate DynamoDB stream events at runtime using Zod schemas:
195
+
196
+ ```ts
197
+ import { z } from "zod";
198
+ import { validateStreamEvent } from "typesafe-dynamodb";
199
+ import type { DynamoDBStreamEvent } from "typesafe-dynamodb/lib/stream-event";
200
+
201
+ const UserSchema = z.object({
202
+ PK: z.string(),
203
+ SK: z.string(),
204
+ name: z.string(),
205
+ email: z.string().email(),
206
+ });
207
+
208
+ type User = z.infer<typeof UserSchema>;
209
+
210
+ export async function handle(
211
+ event: DynamoDBStreamEvent<User, "PK", "SK", "NEW_AND_OLD_IMAGES">,
212
+ ) {
213
+ try {
214
+ const validatedEvent = validateStreamEvent(event, UserSchema);
215
+ // Process validated stream records
216
+ } catch (error) {
217
+ if (error instanceof z.ZodError) {
218
+ console.error("Schema validation failed:", error.issues);
219
+ }
220
+ throw error;
221
+ }
222
+ }
223
+ ```
224
+
225
+ The `validateStreamEvent` function validates both `NewImage` and `OldImage` data against your Zod schema, ensuring runtime type safety for your stream processing logic.
226
+
227
227
  ### Filter result with ProjectionExpression
228
228
 
229
229
  The `ProjectionExpression` field is parsed and applied to filter the returned type of `getItem` and `query`.
@@ -0,0 +1,63 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { z } from "zod";
4
+ import { SchemaValidatedDocumentClient } from "../src/schema-validated-document-client";
5
+ import { SchemaValidatedDeleteDocumentCommand } from "../src/schema-validated-delete-document-command";
6
+
7
+ const UserSchema = z.object({
8
+ pk: z.templateLiteral(["USER", "#", z.uuid()]),
9
+ sk: z.literal("METADATA"),
10
+ userId: z.string(),
11
+ email: z.string().email(),
12
+ name: z.string(),
13
+ age: z.number().min(0),
14
+ preferences: z.object({
15
+ theme: z.enum(["light", "dark"]),
16
+ notifications: z.boolean(),
17
+ }),
18
+ });
19
+
20
+ type User = z.infer<typeof UserSchema>;
21
+
22
+ const client = new DynamoDBClient({ region: "eu-west-1" });
23
+ const documentClient = DynamoDBDocumentClient.from(client);
24
+ const schemaValidatedDocumentClient = new SchemaValidatedDocumentClient(
25
+ documentClient,
26
+ );
27
+
28
+ const DeleteUserCommand = SchemaValidatedDeleteDocumentCommand<
29
+ User,
30
+ "pk",
31
+ "sk"
32
+ >(UserSchema);
33
+
34
+ async function deleteUserDocumentExample() {
35
+ try {
36
+ const result = await schemaValidatedDocumentClient.send(
37
+ new DeleteUserCommand({
38
+ TableName: "Users",
39
+ Key: {
40
+ pk: "USER#00000000-0000-0000-0000-000000000000",
41
+ sk: "METADATA",
42
+ },
43
+ ConditionExpression: "attribute_exists(pk)",
44
+ ReturnValues: "ALL_OLD",
45
+ }),
46
+ UserSchema,
47
+ );
48
+
49
+ if (result.Attributes) {
50
+ console.log("Deleted user:", result.Attributes);
51
+ return result.Attributes;
52
+ }
53
+ } catch (error) {
54
+ if (error instanceof z.ZodError) {
55
+ console.error("Schema validation failed:", error.issues);
56
+ } else {
57
+ console.error("DynamoDB error:", error);
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ export { deleteUserDocumentExample, UserSchema };
@@ -0,0 +1,59 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { z } from "zod";
4
+ import { SchemaValidatedDocumentClient } from "../src/schema-validated-document-client";
5
+ import { SchemaValidatedGetDocumentCommand } from "../src/schema-validated-get-document-command";
6
+
7
+ const UserSchema = z.object({
8
+ pk: z.templateLiteral(["USER", "#", z.uuid()]),
9
+ sk: z.literal("METADATA"),
10
+ userId: z.string(),
11
+ email: z.string().email(),
12
+ name: z.string(),
13
+ age: z.number().min(0),
14
+ preferences: z.object({
15
+ theme: z.enum(["light", "dark"]),
16
+ notifications: z.boolean(),
17
+ }),
18
+ });
19
+
20
+ type User = z.infer<typeof UserSchema>;
21
+
22
+ const client = new DynamoDBClient({ region: "eu-west-1" });
23
+ const documentClient = DynamoDBDocumentClient.from(client);
24
+ const schemaValidatedDocumentClient = new SchemaValidatedDocumentClient(
25
+ documentClient,
26
+ );
27
+
28
+ const GetUserCommand = SchemaValidatedGetDocumentCommand<User, "pk", "sk">(
29
+ UserSchema,
30
+ );
31
+
32
+ async function getUserDocumentExample() {
33
+ try {
34
+ const result = await schemaValidatedDocumentClient.send(
35
+ new GetUserCommand({
36
+ TableName: "Users",
37
+ Key: {
38
+ pk: "USER#00000000-0000-0000-0000-000000000000",
39
+ sk: "METADATA",
40
+ },
41
+ }),
42
+ UserSchema,
43
+ );
44
+
45
+ if (result.Item) {
46
+ console.log("Validated user:", result.Item);
47
+ return result.Item;
48
+ }
49
+ } catch (error) {
50
+ if (error instanceof z.ZodError) {
51
+ console.error("Schema validation failed:", error.issues);
52
+ } else {
53
+ console.error("DynamoDB error:", error);
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ export { getUserDocumentExample, UserSchema };
@@ -0,0 +1,64 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { z } from "zod";
4
+ import { SchemaValidatedDocumentClient } from "../src/schema-validated-document-client";
5
+ import { SchemaValidatedPutDocumentCommand } from "../src/schema-validated-put-document-command";
6
+
7
+ const UserSchema = z.object({
8
+ pk: z.templateLiteral(["USER", "#", z.uuid()]),
9
+ sk: z.literal("METADATA"),
10
+ userId: z.string(),
11
+ email: z.string().email(),
12
+ name: z.string(),
13
+ age: z.number().min(0),
14
+ preferences: z.object({
15
+ theme: z.enum(["light", "dark"]),
16
+ notifications: z.boolean(),
17
+ }),
18
+ });
19
+
20
+ type User = z.infer<typeof UserSchema>;
21
+
22
+ const client = new DynamoDBClient({ region: "eu-west-1" });
23
+ const documentClient = DynamoDBDocumentClient.from(client);
24
+ const schemaValidatedDocumentClient = new SchemaValidatedDocumentClient(
25
+ documentClient,
26
+ );
27
+
28
+ const PutUserCommand = SchemaValidatedPutDocumentCommand<User>(UserSchema);
29
+
30
+ async function putUserDocumentExample() {
31
+ try {
32
+ const result = await schemaValidatedDocumentClient.send(
33
+ new PutUserCommand({
34
+ TableName: "Users",
35
+ Item: {
36
+ pk: "USER#00000000-0000-0000-0000-000000000000",
37
+ sk: "METADATA",
38
+ userId: "user123",
39
+ email: "user@example.com",
40
+ name: "John Doe",
41
+ age: 30,
42
+ preferences: {
43
+ theme: "dark",
44
+ notifications: true,
45
+ },
46
+ },
47
+ ConditionExpression: "attribute_not_exists(pk)",
48
+ }),
49
+ UserSchema,
50
+ );
51
+
52
+ console.log("User created successfully:", result);
53
+ return result;
54
+ } catch (error) {
55
+ if (error instanceof z.ZodError) {
56
+ console.error("Schema validation failed:", error.issues);
57
+ } else {
58
+ console.error("DynamoDB error:", error);
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ export { putUserDocumentExample, UserSchema };
@@ -0,0 +1,69 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { z } from "zod";
4
+ import { SchemaValidatedDocumentClient } from "../src/schema-validated-document-client";
5
+ import { SchemaValidatedQueryDocumentCommand } from "../src/schema-validated-query-document-command";
6
+
7
+ const OrderSchema = z.object({
8
+ pk: z.templateLiteral(["USER", "#", z.uuid()]),
9
+ sk: z.templateLiteral(["ORDER", "#", z.string()]),
10
+ userId: z.string(),
11
+ orderId: z.string(),
12
+ status: z.enum(["PLACED", "SHIPPED", "DELIVERED"]),
13
+ amount: z.number().min(0),
14
+ items: z.array(
15
+ z.object({
16
+ productId: z.string(),
17
+ quantity: z.number().min(1),
18
+ price: z.number().min(0),
19
+ }),
20
+ ),
21
+ createdAt: z.string().datetime(),
22
+ });
23
+
24
+ type Order = z.infer<typeof OrderSchema>;
25
+
26
+ const client = new DynamoDBClient({ region: "eu-west-1" });
27
+ const documentClient = DynamoDBDocumentClient.from(client);
28
+ const schemaValidatedDocumentClient = new SchemaValidatedDocumentClient(
29
+ documentClient,
30
+ );
31
+
32
+ const QueryOrdersCommand =
33
+ SchemaValidatedQueryDocumentCommand<Order>(OrderSchema);
34
+
35
+ async function queryUserOrdersExample() {
36
+ try {
37
+ const result = await schemaValidatedDocumentClient.send(
38
+ new QueryOrdersCommand({
39
+ TableName: "Orders",
40
+ KeyConditionExpression:
41
+ "pk = :userId AND begins_with(sk, :orderPrefix)",
42
+ FilterExpression: "#status = :status",
43
+ ExpressionAttributeNames: {
44
+ "#status": "status",
45
+ },
46
+ ExpressionAttributeValues: {
47
+ ":userId": "USER#00000000-0000-0000-0000-000000000000",
48
+ ":orderPrefix": "ORDER#",
49
+ ":status": "SHIPPED",
50
+ },
51
+ }),
52
+ OrderSchema,
53
+ );
54
+
55
+ if (result.Items) {
56
+ console.log("Validated orders:", result.Items);
57
+ return result.Items;
58
+ }
59
+ } catch (error) {
60
+ if (error instanceof z.ZodError) {
61
+ console.error("Schema validation failed:", error.issues);
62
+ } else {
63
+ console.error("DynamoDB error:", error);
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ export { queryUserOrdersExample, OrderSchema };
@@ -0,0 +1,57 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { z } from "zod";
4
+ import { SchemaValidatedDocumentClient } from "../src/schema-validated-document-client";
5
+ import { SchemaValidatedScanDocumentCommand } from "../src/schema-validated-scan-document-command";
6
+
7
+ const UserSchema = z.object({
8
+ pk: z.templateLiteral(["USER", "#", z.uuid()]),
9
+ sk: z.literal("METADATA"),
10
+ userId: z.string(),
11
+ email: z.string().email(),
12
+ name: z.string(),
13
+ age: z.number().min(0),
14
+ preferences: z.object({
15
+ theme: z.enum(["light", "dark"]),
16
+ notifications: z.boolean(),
17
+ }),
18
+ });
19
+
20
+ type User = z.infer<typeof UserSchema>;
21
+
22
+ const client = new DynamoDBClient({ region: "eu-west-1" });
23
+ const documentClient = DynamoDBDocumentClient.from(client);
24
+ const schemaValidatedDocumentClient = new SchemaValidatedDocumentClient(
25
+ documentClient,
26
+ );
27
+
28
+ const ScanUsersCommand = SchemaValidatedScanDocumentCommand<User>(UserSchema);
29
+
30
+ async function scanUsersDocumentExample() {
31
+ try {
32
+ const result = await schemaValidatedDocumentClient.send(
33
+ new ScanUsersCommand({
34
+ TableName: "Users",
35
+ FilterExpression: "age > :minAge",
36
+ ExpressionAttributeValues: {
37
+ ":minAge": 18,
38
+ },
39
+ }),
40
+ UserSchema,
41
+ );
42
+
43
+ if (result.Items) {
44
+ console.log("Validated users:", result.Items);
45
+ return result.Items;
46
+ }
47
+ } catch (error) {
48
+ if (error instanceof z.ZodError) {
49
+ console.error("Schema validation failed:", error.issues);
50
+ } else {
51
+ console.error("DynamoDB error:", error);
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ export { scanUsersDocumentExample, UserSchema };
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { validateStreamEvent } from "../src/schema-validated-stream-event";
3
+ import type { DynamoDBStreamEvent } from "../src/stream-event";
4
+
5
+ const UserSchema = z.object({
6
+ pk: z.templateLiteral(["USER", "#", z.string()]),
7
+ sk: z.literal("PROFILE"),
8
+ userId: z.string(),
9
+ email: z.string().email(),
10
+ name: z.string(),
11
+ age: z.number().min(0),
12
+ preferences: z.object({
13
+ theme: z.enum(["light", "dark"]),
14
+ notifications: z.boolean(),
15
+ }),
16
+ });
17
+
18
+ type User = z.infer<typeof UserSchema>;
19
+
20
+ export async function handleUserStreamEvent(
21
+ event: DynamoDBStreamEvent<User, "pk", "sk", "NEW_AND_OLD_IMAGES">,
22
+ ) {
23
+ try {
24
+ const validatedEvent = validateStreamEvent(event, UserSchema);
25
+
26
+ for (const record of validatedEvent.Records) {
27
+ console.log(`Processing ${record.eventName} event for ${record.eventID}`);
28
+
29
+ if (record.dynamodb?.NewImage) {
30
+ console.log("New user data validated successfully");
31
+ }
32
+
33
+ if (record.dynamodb?.OldImage) {
34
+ console.log("Old user data validated successfully");
35
+ }
36
+
37
+ switch (record.eventName) {
38
+ case "INSERT":
39
+ console.log("User created");
40
+ break;
41
+ case "MODIFY":
42
+ console.log("User updated");
43
+ break;
44
+ case "REMOVE":
45
+ console.log("User deleted");
46
+ break;
47
+ }
48
+ }
49
+ } catch (error) {
50
+ if (error instanceof z.ZodError) {
51
+ console.error("Schema validation failed:", error.issues);
52
+ throw new Error(`Invalid stream data: ${error.message}`);
53
+ } else {
54
+ console.error("Stream processing error:", error);
55
+ throw error;
56
+ }
57
+ }
58
+ }
59
+
60
+ export { UserSchema };
@@ -0,0 +1,75 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { z } from "zod";
4
+ import { SchemaValidatedDocumentClient } from "../src/schema-validated-document-client";
5
+ import { SchemaValidatedUpdateDocumentCommand } from "../src/schema-validated-update-document-command";
6
+
7
+ const UserSchema = z.object({
8
+ pk: z.templateLiteral(["USER", "#", z.uuid()]),
9
+ sk: z.literal("METADATA"),
10
+ userId: z.string(),
11
+ email: z.string().email(),
12
+ name: z.string(),
13
+ age: z.number().min(0),
14
+ preferences: z.object({
15
+ theme: z.enum(["light", "dark"]),
16
+ notifications: z.boolean(),
17
+ }),
18
+ });
19
+
20
+ type User = z.infer<typeof UserSchema>;
21
+
22
+ const client = new DynamoDBClient({ region: "eu-west-1" });
23
+ const documentClient = DynamoDBDocumentClient.from(client);
24
+ const schemaValidatedDocumentClient = new SchemaValidatedDocumentClient(
25
+ documentClient,
26
+ );
27
+
28
+ const UpdateUserCommand = SchemaValidatedUpdateDocumentCommand<
29
+ User,
30
+ "pk",
31
+ "sk"
32
+ >(UserSchema);
33
+
34
+ async function updateUserDocumentExample() {
35
+ try {
36
+ const result = await schemaValidatedDocumentClient.send(
37
+ new UpdateUserCommand({
38
+ TableName: "Users",
39
+ Key: {
40
+ pk: "USER#00000000-0000-0000-0000-000000000000",
41
+ sk: "METADATA",
42
+ },
43
+ UpdateExpression:
44
+ "SET #name = :name, #age = :age, #preferences.#theme = :theme",
45
+ ExpressionAttributeNames: {
46
+ "#name": "name",
47
+ "#age": "age",
48
+ "#preferences": "preferences",
49
+ "#theme": "theme",
50
+ },
51
+ ExpressionAttributeValues: {
52
+ ":name": "John Doe",
53
+ ":age": 30,
54
+ ":theme": "dark",
55
+ },
56
+ ReturnValues: "ALL_NEW",
57
+ }),
58
+ UserSchema,
59
+ );
60
+
61
+ if (result.Attributes) {
62
+ console.log("Updated user:", result.Attributes);
63
+ return result.Attributes;
64
+ }
65
+ } catch (error) {
66
+ if (error instanceof z.ZodError) {
67
+ console.error("Schema validation failed:", error.issues);
68
+ } else {
69
+ console.error("DynamoDB error:", error);
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ export { updateUserDocumentExample, UserSchema };
@@ -1,7 +1,5 @@
1
1
  import type { DynamoDB, ReturnValue as DynamoDBReturnValue } from "@aws-sdk/client-dynamodb";
2
2
  import { MetadataBearer } from "@aws-sdk/types";
3
- import { ProjectionExpression } from "aws-sdk/clients/dynamodb";
4
- import { Callback } from "./callback";
5
3
  import { DeleteItemInput, DeleteItemOutput } from "./delete-item";
6
4
  import { GetItemInput, GetItemOutput } from "./get-item";
7
5
  import { JsonFormat } from "./json-format";
@@ -12,15 +10,11 @@ import { ScanInput, ScanOutput } from "./scan";
12
10
  import { UpdateItemInput, UpdateItemOutput } from "./update-item";
13
11
  export interface TypeSafeDynamoDBv3<Item extends object, PartitionKey extends keyof Item, RangeKey extends keyof Item | undefined = undefined> extends Omit<DynamoDB, "getItem" | "deleteItem" | "putItem" | "updateItem" | "query" | "scan"> {
14
12
  getItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, AttributesToGet extends keyof Item | undefined = undefined, ProjectionExpression extends string | undefined = undefined>(params: GetItemInput<Item, PartitionKey, RangeKey, Key, AttributesToGet, ProjectionExpression, JsonFormat.AttributeValue>): Promise<GetItemOutput<Item, PartitionKey, RangeKey, Key, AttributesToGet, ProjectionExpression, JsonFormat.AttributeValue> & MetadataBearer>;
15
- getItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, AttributesToGet extends keyof Item | undefined = undefined, ProjectionExpression extends string | undefined = undefined>(params: GetItemInput<Item, PartitionKey, RangeKey, Key, AttributesToGet, ProjectionExpression, JsonFormat.AttributeValue>, callback: Callback<GetItemOutput<Item, PartitionKey, RangeKey, Key, AttributesToGet, ProjectionExpression, JsonFormat.AttributeValue> & MetadataBearer, any>): void;
16
13
  deleteItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: DeleteItemInput<Item, PartitionKey, RangeKey, Key, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>): Promise<DeleteItemOutput<Item, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer>;
17
- deleteItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: DeleteItemInput<Item, PartitionKey, RangeKey, Key, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>, callback: Callback<DeleteItemOutput<Item, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer, any>): void;
18
14
  putItem<ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: PutItemInput<Item, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>): Promise<PutItemOutput<Item, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer>;
19
- putItem<ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: PutItemInput<Item, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>, callback: Callback<PutItemOutput<Item, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer, any>): void;
20
15
  updateItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, UpdateExpression extends string, ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: UpdateItemInput<Item, PartitionKey, RangeKey, Key, UpdateExpression, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>): Promise<UpdateItemOutput<Item, PartitionKey, RangeKey, Key, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer>;
21
- updateItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, UpdateExpression extends string, ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: UpdateItemInput<Item, PartitionKey, RangeKey, Key, UpdateExpression, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>, callback?: Callback<UpdateItemOutput<Item, PartitionKey, RangeKey, Key, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer, any>): void;
16
+ updateItem<Key extends TableKey<Item, PartitionKey, RangeKey, JsonFormat.AttributeValue>, UpdateExpression extends string, ConditionExpression extends string | undefined, ReturnValue extends DynamoDBReturnValue = "NONE">(params: UpdateItemInput<Item, PartitionKey, RangeKey, Key, UpdateExpression, ConditionExpression, ReturnValue, JsonFormat.AttributeValue>): Promise<UpdateItemOutput<Item, PartitionKey, RangeKey, Key, ReturnValue, JsonFormat.AttributeValue> & MetadataBearer>;
22
17
  query<KeyConditionExpression extends string | undefined = undefined, FilterExpression extends string | undefined = undefined, ProjectionExpression extends string | undefined = undefined, AttributesToGet extends keyof Item | undefined = undefined>(params: QueryInput<Item, KeyConditionExpression, FilterExpression, ProjectionExpression, AttributesToGet, JsonFormat.AttributeValue>): Promise<QueryOutput<Item, AttributesToGet, JsonFormat.AttributeValue> & MetadataBearer>;
23
- query<KeyConditionExpression extends string | undefined = undefined, FilterExpression extends string | undefined = undefined, AttributesToGet extends keyof Item | undefined = undefined>(params: QueryInput<Item, KeyConditionExpression, FilterExpression, ProjectionExpression, AttributesToGet, JsonFormat.AttributeValue>, callback: Callback<QueryOutput<Item, AttributesToGet, JsonFormat.AttributeValue> & MetadataBearer, any>): void;
24
18
  scan<FilterExpression extends string | undefined = undefined, ProjectionExpression extends string | undefined = undefined, AttributesToGet extends keyof Item | undefined = undefined>(params: ScanInput<Item, FilterExpression, ProjectionExpression, AttributesToGet, JsonFormat.Document>): Promise<ScanOutput<Item, AttributesToGet, JsonFormat.AttributeValue> & MetadataBearer>;
25
- scan<FilterExpression extends string | undefined = undefined, ProjectionExpression extends string | undefined = undefined, AttributesToGet extends keyof Item | undefined = undefined>(params: ScanInput<Item, FilterExpression, ProjectionExpression, AttributesToGet, JsonFormat.AttributeValue>, callback?: Callback<ScanOutput<Item, AttributesToGet, JsonFormat.AttributeValue> & MetadataBearer, any>): void;
19
+ scan<FilterExpression extends string | undefined = undefined, ProjectionExpression extends string | undefined = undefined, AttributesToGet extends keyof Item | undefined = undefined>(params: ScanInput<Item, FilterExpression, ProjectionExpression, AttributesToGet, JsonFormat.AttributeValue>): Promise<ScanOutput<Item, AttributesToGet, JsonFormat.AttributeValue> & MetadataBearer>;
26
20
  }