@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.
- package/README.md +56 -56
- package/examples/schema-validated-delete-document.ts +63 -0
- package/examples/schema-validated-get-document.ts +59 -0
- package/examples/schema-validated-put-document.ts +64 -0
- package/examples/schema-validated-query-document.ts +69 -0
- package/examples/schema-validated-scan-document.ts +57 -0
- package/examples/schema-validated-stream-event.ts +60 -0
- package/examples/schema-validated-update-document.ts +75 -0
- package/lib/client-v3.d.ts +2 -8
- package/lib/client-v3.js +1 -1
- package/lib/delete-item.d.ts +3 -4
- package/lib/delete-item.js +1 -1
- package/lib/document-client-v3.d.ts +2 -8
- package/lib/document-client-v3.js +1 -1
- package/lib/get-item.d.ts +3 -3
- package/lib/get-item.js +1 -1
- package/lib/put-item.d.ts +3 -4
- package/lib/put-item.js +1 -1
- package/lib/query.d.ts +3 -4
- package/lib/query.js +1 -1
- package/lib/scan.d.ts +3 -4
- package/lib/scan.js +1 -1
- package/lib/schema-validated-delete-document-command.d.ts +6 -0
- package/lib/schema-validated-delete-document-command.js +10 -0
- package/lib/schema-validated-document-client.d.ts +35 -0
- package/lib/schema-validated-document-client.js +25 -0
- package/lib/schema-validated-get-document-command.d.ts +6 -0
- package/lib/schema-validated-get-document-command.js +10 -0
- package/lib/schema-validated-put-document-command.d.ts +6 -0
- package/lib/schema-validated-put-document-command.js +10 -0
- package/lib/schema-validated-query-document-command.d.ts +6 -0
- package/lib/schema-validated-query-document-command.js +10 -0
- package/lib/schema-validated-scan-document-command.d.ts +6 -0
- package/lib/schema-validated-scan-document-command.js +10 -0
- package/lib/schema-validated-stream-event.d.ts +4 -0
- package/lib/schema-validated-stream-event.js +30 -0
- package/lib/schema-validated-update-document-command.d.ts +6 -0
- package/lib/schema-validated-update-document-command.js +10 -0
- package/lib/update-item.d.ts +3 -4
- package/lib/update-item.js +1 -1
- package/package.json +5 -4
- package/perf/README.md +114 -0
- package/perf/performance-test.ts +563 -0
- package/lib/callback.d.ts +0 -2
- package/lib/callback.js +0 -3
- package/lib/client-v2.d.ts +0 -18
- package/lib/client-v2.js +0 -3
- package/lib/document-client-v2.d.ts +0 -18
- 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
|
-
[](https://badge.fury.io/js/@nikovirtala%2Ftypesafe-dynamodb)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|

|
|
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
|
|
20
|
-
|
|
21
|
-
### AWS SDK v2
|
|
21
|
+
This library contains type definitions for AWS SDK v3 (`@aws-sdk/client-dynamodb`).
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### AWS SDK v3
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
import { DynamoDB } from "aws-sdk";
|
|
25
|
+
#### Option 1 - DynamoDB
|
|
27
26
|
|
|
28
|
-
|
|
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 `
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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 };
|
package/lib/client-v3.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|