@superblocksteam/sdk-api 0.0.10 → 0.0.12-body-types
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/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/base/index.d.ts +2 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/index.js +1 -0
- package/dist/integrations/base/index.js.map +1 -1
- package/dist/integrations/base/request-body.d.ts +21 -0
- package/dist/integrations/base/request-body.d.ts.map +1 -0
- package/dist/integrations/base/request-body.js +322 -0
- package/dist/integrations/base/request-body.js.map +1 -0
- package/dist/integrations/base/request-body.test.d.ts +2 -0
- package/dist/integrations/base/request-body.test.d.ts.map +1 -0
- package/dist/integrations/base/request-body.test.js +324 -0
- package/dist/integrations/base/request-body.test.js.map +1 -0
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +18 -4
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/types.d.ts +40 -5
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/base/types.js +7 -0
- package/dist/integrations/base/types.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +20 -1
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/dynamodb/client.d.ts +2 -6
- package/dist/integrations/dynamodb/client.d.ts.map +1 -1
- package/dist/integrations/dynamodb/client.js +10 -83
- package/dist/integrations/dynamodb/client.js.map +1 -1
- package/dist/integrations/dynamodb/index.d.ts +1 -1
- package/dist/integrations/dynamodb/index.d.ts.map +1 -1
- package/dist/integrations/dynamodb/index.js.map +1 -1
- package/dist/integrations/dynamodb/types.d.ts +1 -27
- package/dist/integrations/dynamodb/types.d.ts.map +1 -1
- package/dist/integrations/index.d.ts +1 -1
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/restapiintegration/client.body-types.test.d.ts +2 -0
- package/dist/integrations/restapiintegration/client.body-types.test.d.ts.map +1 -0
- package/dist/integrations/restapiintegration/client.body-types.test.js +145 -0
- package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -0
- package/dist/integrations/slack/client.test.js +23 -0
- package/dist/integrations/slack/client.test.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +52 -1
- package/src/index.ts +0 -1
- package/src/integrations/base/index.ts +7 -0
- package/src/integrations/base/request-body.test.ts +402 -0
- package/src/integrations/base/request-body.ts +548 -0
- package/src/integrations/base/rest-api-client-base.ts +18 -4
- package/src/integrations/base/types.ts +59 -5
- package/src/integrations/documentation-resolver.test.ts +24 -1
- package/src/integrations/dynamodb/README.md +12 -33
- package/src/integrations/dynamodb/client.ts +16 -121
- package/src/integrations/dynamodb/index.ts +1 -5
- package/src/integrations/dynamodb/types.ts +1 -33
- package/src/integrations/index.ts +0 -1
- package/src/integrations/restapiintegration/client.body-types.test.ts +214 -0
- package/src/integrations/restapiintegration/docs.manifest.json +8 -0
- package/src/integrations/restapiintegration/overlays/request-body-types-unsupported.md +7 -0
- package/src/integrations/restapiintegration/overlays/request-body-types.md +106 -0
- package/src/integrations/slack/client.test.ts +33 -0
- package/dist/integrations/dynamodb/client.test.d.ts +0 -8
- package/dist/integrations/dynamodb/client.test.d.ts.map +0 -1
- package/dist/integrations/dynamodb/client.test.js +0 -198
- package/dist/integrations/dynamodb/client.test.js.map +0 -1
- package/src/integrations/dynamodb/client.test.ts +0 -254
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
## Request Body Types
|
|
2
|
+
|
|
3
|
+
`apiRequest()` serializes request bodies as JSON by default. For endpoints that expect another encoding, pass `bodyType`:
|
|
4
|
+
|
|
5
|
+
- `"json"` for JSON payloads. This is the default.
|
|
6
|
+
- `"raw"` for caller-encoded string bodies such as XML, CSV, or plain text.
|
|
7
|
+
- `"formUrlencoded"` for `application/x-www-form-urlencoded` payloads.
|
|
8
|
+
- `"multipartForm"` for multipart fields and file parts.
|
|
9
|
+
- `"binary"` for byte payloads such as `Uint8Array`, `ArrayBuffer`, `DataView`, or `Buffer`.
|
|
10
|
+
|
|
11
|
+
### Form URL-Encoded Requests
|
|
12
|
+
|
|
13
|
+
Use `bodyType: "formUrlencoded"` for Stripe, OAuth token exchanges, and APIs that expect HTML form encoding. Arrays repeat the key in order, and nested fields should be flattened with bracket notation:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
const token = await ctx.integrations.authApi.apiRequest(
|
|
17
|
+
{
|
|
18
|
+
method: "POST",
|
|
19
|
+
path: "/oauth/token",
|
|
20
|
+
bodyType: "formUrlencoded",
|
|
21
|
+
body: {
|
|
22
|
+
grant_type: "client_credentials",
|
|
23
|
+
client_id: input.clientId,
|
|
24
|
+
scope: ["read:orders", "write:orders"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{ response: TokenResponseSchema },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const charge = await ctx.integrations.stripe.apiRequest(
|
|
31
|
+
{
|
|
32
|
+
method: "POST",
|
|
33
|
+
path: "/v1/charges",
|
|
34
|
+
bodyType: "formUrlencoded",
|
|
35
|
+
body: {
|
|
36
|
+
amount: 2000,
|
|
37
|
+
currency: "usd",
|
|
38
|
+
"metadata[order_id]": "6735",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{ response: ChargeResponseSchema },
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The SDK adds `Content-Type: application/x-www-form-urlencoded` unless you already provided a `Content-Type` header.
|
|
46
|
+
Fields whose value is `undefined` are omitted. Empty arrays are omitted. The body must encode at least one field.
|
|
47
|
+
|
|
48
|
+
### Binary Uploads
|
|
49
|
+
|
|
50
|
+
Use `bodyType: "binary"` for APIs that accept the request body as bytes, such as SharePoint-style upload endpoints:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
await ctx.integrations.sharepoint.apiRequest(
|
|
54
|
+
{
|
|
55
|
+
method: "PUT",
|
|
56
|
+
path: `/sites/${siteId}/drive/root:/reports/monthly.pdf:/content`,
|
|
57
|
+
bodyType: "binary",
|
|
58
|
+
body: pdfBytes,
|
|
59
|
+
},
|
|
60
|
+
{ response: UploadResponseSchema },
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The SDK sends the exact byte window for views such as `Uint8Array.subarray()` and `DataView`. It adds `Content-Type: application/octet-stream` unless you already provided a `Content-Type` header such as `application/pdf`.
|
|
65
|
+
Request bodies are limited to 20 MiB before serialization so the agent request stays under its 28 MiB serialized size cap.
|
|
66
|
+
|
|
67
|
+
### Multipart Uploads
|
|
68
|
+
|
|
69
|
+
Use `bodyType: "multipartForm"` for endpoints that expect fields plus file parts. File parts must be wrapped as `{ value, filename }`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const upload = await ctx.integrations.filesApi.apiRequest(
|
|
73
|
+
{
|
|
74
|
+
method: "POST",
|
|
75
|
+
path: "/files",
|
|
76
|
+
bodyType: "multipartForm",
|
|
77
|
+
body: {
|
|
78
|
+
folder: "invoices",
|
|
79
|
+
invoice: { value: invoicePdfBytes, filename: "invoice.pdf" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{ response: UploadResponseSchema },
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Do not set `Content-Type` for multipart requests. The worker generates the multipart boundary during execution.
|
|
87
|
+
Do not set per-part `contentType`. Fields whose value is `undefined` are omitted. Empty file parts are allowed. The total multipart field and file-part content is limited to 20 MiB before serialization.
|
|
88
|
+
|
|
89
|
+
### Raw String Bodies
|
|
90
|
+
|
|
91
|
+
Use `bodyType: "raw"` for pre-encoded text bodies. The SDK does not add a `Content-Type` header in raw mode, so provide the header expected by the API:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
await ctx.integrations.legacyApi.apiRequest(
|
|
95
|
+
{
|
|
96
|
+
method: "POST",
|
|
97
|
+
path: "/soap",
|
|
98
|
+
bodyType: "raw",
|
|
99
|
+
body: `<Envelope><Body>${payload}</Body></Envelope>`,
|
|
100
|
+
headers: { "Content-Type": "text/xml" },
|
|
101
|
+
},
|
|
102
|
+
{ response: SoapResponseSchema },
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Raw string bodies are limited to 20 MiB.
|
|
@@ -307,6 +307,39 @@ describe("SlackClientImpl", () => {
|
|
|
307
307
|
expect(request.params).toEqual([{ key: "unfurl_links", value: "false" }]);
|
|
308
308
|
});
|
|
309
309
|
|
|
310
|
+
it("sends formUrlencoded bodies through the shared request builder", async () => {
|
|
311
|
+
const { client, executeQuery } = createClient({
|
|
312
|
+
ok: true,
|
|
313
|
+
channel: "C123",
|
|
314
|
+
ts: "1234567890.123456",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await client.apiRequest(
|
|
318
|
+
{
|
|
319
|
+
method: "POST",
|
|
320
|
+
path: "/chat.postMessage",
|
|
321
|
+
body: { channel: "#alerts", text: "hello" },
|
|
322
|
+
bodyType: "formUrlencoded",
|
|
323
|
+
},
|
|
324
|
+
{ response: PostMessageSchema },
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
328
|
+
expect.objectContaining({
|
|
329
|
+
body: "channel=%23alerts&text=hello",
|
|
330
|
+
bodyType: "rawBody",
|
|
331
|
+
headers: [
|
|
332
|
+
{
|
|
333
|
+
key: "Content-Type",
|
|
334
|
+
value: "application/x-www-form-urlencoded",
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
}),
|
|
338
|
+
undefined,
|
|
339
|
+
undefined,
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
310
343
|
it("passes trace metadata to executeQuery", async () => {
|
|
311
344
|
const { client, executeQuery } = createClient({
|
|
312
345
|
ok: true,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for DynamoDB client request bodies.
|
|
3
|
-
*
|
|
4
|
-
* The orchestrator forwards `action` + JSON `body` to the AWS SDK, so these
|
|
5
|
-
* tests lock the wire params - especially ExclusiveStartKey pagination.
|
|
6
|
-
*/
|
|
7
|
-
export {};
|
|
8
|
-
//# sourceMappingURL=client.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../../src/integrations/dynamodb/client.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for DynamoDB client request bodies.
|
|
3
|
-
*
|
|
4
|
-
* The orchestrator forwards `action` + JSON `body` to the AWS SDK, so these
|
|
5
|
-
* tests lock the wire params - especially ExclusiveStartKey pagination.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect, vi } from "vitest";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
import { DynamoDBClientImpl } from "./client.js";
|
|
10
|
-
const TEST_CONFIG = {
|
|
11
|
-
id: "dynamodb-test-id",
|
|
12
|
-
name: "Test DynamoDB",
|
|
13
|
-
pluginId: "dynamodb",
|
|
14
|
-
configuration: {},
|
|
15
|
-
};
|
|
16
|
-
const ItemsSchema = z.object({ Items: z.array(z.unknown()).optional() });
|
|
17
|
-
function createClient(mockResult = { Items: [] }) {
|
|
18
|
-
const executeQuery = vi.fn().mockResolvedValue(mockResult);
|
|
19
|
-
const client = new DynamoDBClientImpl(TEST_CONFIG, executeQuery);
|
|
20
|
-
return { client, executeQuery };
|
|
21
|
-
}
|
|
22
|
-
function parsedBody(executeQuery, callIndex = 0) {
|
|
23
|
-
const request = z
|
|
24
|
-
.object({ action: z.string(), body: z.string() })
|
|
25
|
-
.parse(executeQuery.mock.calls[callIndex]?.[0]);
|
|
26
|
-
return { action: request.action, body: JSON.parse(request.body) };
|
|
27
|
-
}
|
|
28
|
-
describe("DynamoDBClientImpl", () => {
|
|
29
|
-
describe("scan", () => {
|
|
30
|
-
it("omits ExclusiveStartKey on a positional scan", async () => {
|
|
31
|
-
const { client, executeQuery } = createClient();
|
|
32
|
-
await client.scan("retailers", ItemsSchema, "status = :s", {
|
|
33
|
-
":s": { S: "active" },
|
|
34
|
-
});
|
|
35
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
36
|
-
action: "scan",
|
|
37
|
-
body: {
|
|
38
|
-
TableName: "retailers",
|
|
39
|
-
FilterExpression: "status = :s",
|
|
40
|
-
ExpressionAttributeValues: { ":s": { S: "active" } },
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
it("forwards ExclusiveStartKey from the options object", async () => {
|
|
45
|
-
const { client, executeQuery } = createClient();
|
|
46
|
-
const exclusiveStartKey = { id: { S: "retailer-18" } };
|
|
47
|
-
await client.scan("retailers", ItemsSchema, { exclusiveStartKey });
|
|
48
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
49
|
-
action: "scan",
|
|
50
|
-
body: {
|
|
51
|
-
TableName: "retailers",
|
|
52
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
it("treats an empty object as empty scan options", async () => {
|
|
57
|
-
const { client, executeQuery } = createClient();
|
|
58
|
-
await client.scan("retailers", ItemsSchema, {});
|
|
59
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
60
|
-
action: "scan",
|
|
61
|
-
body: { TableName: "retailers" },
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
it("rejects objects with unknown scan option keys", async () => {
|
|
65
|
-
const { client, executeQuery } = createClient();
|
|
66
|
-
await expect(Reflect.apply(client.scan, client, [
|
|
67
|
-
"retailers",
|
|
68
|
-
ItemsSchema,
|
|
69
|
-
{ label: "not scan options" },
|
|
70
|
-
])).rejects.toThrow("Invalid DynamoDB scan options: label");
|
|
71
|
-
expect(executeQuery).not.toHaveBeenCalled();
|
|
72
|
-
});
|
|
73
|
-
it("forwards pagination and parallel-scan options", async () => {
|
|
74
|
-
const { client, executeQuery } = createClient();
|
|
75
|
-
await client.scan("retailers", ItemsSchema, {
|
|
76
|
-
exclusiveStartKey: { id: { S: "page-2" } },
|
|
77
|
-
limit: 25,
|
|
78
|
-
projectionExpression: "id, name",
|
|
79
|
-
indexName: "gsi1",
|
|
80
|
-
segment: 0,
|
|
81
|
-
totalSegments: 4,
|
|
82
|
-
filterExpression: "#s = :s",
|
|
83
|
-
expressionAttributeNames: { "#s": "status" },
|
|
84
|
-
expressionAttributeValues: { ":s": { S: "active" } },
|
|
85
|
-
}, { label: "page retailers" });
|
|
86
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
87
|
-
action: "scan",
|
|
88
|
-
body: {
|
|
89
|
-
TableName: "retailers",
|
|
90
|
-
ExclusiveStartKey: { id: { S: "page-2" } },
|
|
91
|
-
Limit: 25,
|
|
92
|
-
ProjectionExpression: "id, name",
|
|
93
|
-
IndexName: "gsi1",
|
|
94
|
-
Segment: 0,
|
|
95
|
-
TotalSegments: 4,
|
|
96
|
-
FilterExpression: "#s = :s",
|
|
97
|
-
ExpressionAttributeNames: { "#s": "status" },
|
|
98
|
-
ExpressionAttributeValues: { ":s": { S: "active" } },
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
expect(executeQuery).toHaveBeenCalledWith(expect.anything(), undefined, {
|
|
102
|
-
label: "page retailers",
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
it("keeps positional names and metadata working together", async () => {
|
|
106
|
-
const { client, executeQuery } = createClient();
|
|
107
|
-
await client.scan("retailers", ItemsSchema, "#s = :s", { ":s": { S: "active" } }, { "#s": "status" }, { label: "filtered scan" });
|
|
108
|
-
expect(parsedBody(executeQuery).body).toEqual({
|
|
109
|
-
TableName: "retailers",
|
|
110
|
-
FilterExpression: "#s = :s",
|
|
111
|
-
ExpressionAttributeValues: { ":s": { S: "active" } },
|
|
112
|
-
ExpressionAttributeNames: { "#s": "status" },
|
|
113
|
-
});
|
|
114
|
-
expect(executeQuery.mock.calls[0][2]).toEqual({ label: "filtered scan" });
|
|
115
|
-
});
|
|
116
|
-
it("keeps schemas that strip pagination cursors backward compatible", async () => {
|
|
117
|
-
const lastEvaluatedKey = {
|
|
118
|
-
id: { S: "retailer-18" },
|
|
119
|
-
version: { N: "2" },
|
|
120
|
-
};
|
|
121
|
-
const { client } = createClient({
|
|
122
|
-
Items: [{ id: { S: "retailer-1" } }],
|
|
123
|
-
LastEvaluatedKey: lastEvaluatedKey,
|
|
124
|
-
});
|
|
125
|
-
await expect(client.scan("retailers", ItemsSchema, { limit: 25 })).resolves.toEqual({
|
|
126
|
-
Items: [{ id: { S: "retailer-1" } }],
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
it("preserves mixed AttributeValue pagination cursors", async () => {
|
|
130
|
-
const lastEvaluatedKey = {
|
|
131
|
-
id: { S: "retailer-18" },
|
|
132
|
-
version: { N: "2" },
|
|
133
|
-
};
|
|
134
|
-
const PageSchema = z.object({
|
|
135
|
-
Items: z.array(z.unknown()).optional(),
|
|
136
|
-
LastEvaluatedKey: z.record(z.unknown()).optional(),
|
|
137
|
-
});
|
|
138
|
-
const { client } = createClient({
|
|
139
|
-
Items: [],
|
|
140
|
-
LastEvaluatedKey: lastEvaluatedKey,
|
|
141
|
-
});
|
|
142
|
-
const page = await client.scan("retailers", PageSchema, {});
|
|
143
|
-
expect(page.LastEvaluatedKey).toEqual(lastEvaluatedKey);
|
|
144
|
-
});
|
|
145
|
-
it("threads each returned pagination cursor into the next request", async () => {
|
|
146
|
-
const LastEvaluatedKeySchema = z.object({
|
|
147
|
-
id: z.object({ S: z.string() }),
|
|
148
|
-
});
|
|
149
|
-
const PageSchema = z.object({
|
|
150
|
-
Items: z.array(z.string()),
|
|
151
|
-
LastEvaluatedKey: LastEvaluatedKeySchema.optional(),
|
|
152
|
-
});
|
|
153
|
-
const lastEvaluatedKey = { id: { S: "retailer-1" } };
|
|
154
|
-
const executeQuery = vi
|
|
155
|
-
.fn()
|
|
156
|
-
.mockResolvedValueOnce({
|
|
157
|
-
Items: ["retailer-1"],
|
|
158
|
-
LastEvaluatedKey: lastEvaluatedKey,
|
|
159
|
-
})
|
|
160
|
-
.mockResolvedValueOnce({ Items: ["retailer-2"] });
|
|
161
|
-
const client = new DynamoDBClientImpl(TEST_CONFIG, executeQuery);
|
|
162
|
-
const items = [];
|
|
163
|
-
let exclusiveStartKey;
|
|
164
|
-
do {
|
|
165
|
-
const page = await client.scan("retailers", PageSchema, {
|
|
166
|
-
exclusiveStartKey,
|
|
167
|
-
});
|
|
168
|
-
items.push(...page.Items);
|
|
169
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
170
|
-
} while (exclusiveStartKey);
|
|
171
|
-
expect(items).toEqual(["retailer-1", "retailer-2"]);
|
|
172
|
-
expect(executeQuery).toHaveBeenCalledTimes(2);
|
|
173
|
-
expect(parsedBody(executeQuery, 0).body).toEqual({
|
|
174
|
-
TableName: "retailers",
|
|
175
|
-
});
|
|
176
|
-
expect(parsedBody(executeQuery, 1).body).toEqual({
|
|
177
|
-
TableName: "retailers",
|
|
178
|
-
ExclusiveStartKey: lastEvaluatedKey,
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
describe("queryTable", () => {
|
|
183
|
-
it("treats a plain names map as ExpressionAttributeNames", async () => {
|
|
184
|
-
const { client, executeQuery } = createClient();
|
|
185
|
-
await client.queryTable("orders", "#uid = :uid", { ":uid": { S: "user-123" } }, ItemsSchema, { "#uid": "userId" });
|
|
186
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
187
|
-
action: "query",
|
|
188
|
-
body: {
|
|
189
|
-
TableName: "orders",
|
|
190
|
-
KeyConditionExpression: "#uid = :uid",
|
|
191
|
-
ExpressionAttributeValues: { ":uid": { S: "user-123" } },
|
|
192
|
-
ExpressionAttributeNames: { "#uid": "userId" },
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
//# sourceMappingURL=client.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.test.js","sourceRoot":"","sources":["../../../src/integrations/dynamodb/client.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for DynamoDB client request bodies.
|
|
3
|
-
*
|
|
4
|
-
* The orchestrator forwards `action` + JSON `body` to the AWS SDK, so these
|
|
5
|
-
* tests lock the wire params - especially ExclusiveStartKey pagination.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, vi } from "vitest";
|
|
9
|
-
import { z } from "zod";
|
|
10
|
-
|
|
11
|
-
import type { IntegrationConfig } from "../types.js";
|
|
12
|
-
import { DynamoDBClientImpl } from "./client.js";
|
|
13
|
-
|
|
14
|
-
const TEST_CONFIG: IntegrationConfig = {
|
|
15
|
-
id: "dynamodb-test-id",
|
|
16
|
-
name: "Test DynamoDB",
|
|
17
|
-
pluginId: "dynamodb",
|
|
18
|
-
configuration: {},
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const ItemsSchema = z.object({ Items: z.array(z.unknown()).optional() });
|
|
22
|
-
|
|
23
|
-
function createClient(mockResult: unknown = { Items: [] }) {
|
|
24
|
-
const executeQuery = vi.fn().mockResolvedValue(mockResult);
|
|
25
|
-
const client = new DynamoDBClientImpl(TEST_CONFIG, executeQuery);
|
|
26
|
-
return { client, executeQuery };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parsedBody(executeQuery: ReturnType<typeof vi.fn>, callIndex = 0) {
|
|
30
|
-
const request = z
|
|
31
|
-
.object({ action: z.string(), body: z.string() })
|
|
32
|
-
.parse(executeQuery.mock.calls[callIndex]?.[0]);
|
|
33
|
-
return { action: request.action, body: JSON.parse(request.body) };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe("DynamoDBClientImpl", () => {
|
|
37
|
-
describe("scan", () => {
|
|
38
|
-
it("omits ExclusiveStartKey on a positional scan", async () => {
|
|
39
|
-
const { client, executeQuery } = createClient();
|
|
40
|
-
|
|
41
|
-
await client.scan("retailers", ItemsSchema, "status = :s", {
|
|
42
|
-
":s": { S: "active" },
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
46
|
-
action: "scan",
|
|
47
|
-
body: {
|
|
48
|
-
TableName: "retailers",
|
|
49
|
-
FilterExpression: "status = :s",
|
|
50
|
-
ExpressionAttributeValues: { ":s": { S: "active" } },
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("forwards ExclusiveStartKey from the options object", async () => {
|
|
56
|
-
const { client, executeQuery } = createClient();
|
|
57
|
-
const exclusiveStartKey = { id: { S: "retailer-18" } };
|
|
58
|
-
|
|
59
|
-
await client.scan("retailers", ItemsSchema, { exclusiveStartKey });
|
|
60
|
-
|
|
61
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
62
|
-
action: "scan",
|
|
63
|
-
body: {
|
|
64
|
-
TableName: "retailers",
|
|
65
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("treats an empty object as empty scan options", async () => {
|
|
71
|
-
const { client, executeQuery } = createClient();
|
|
72
|
-
|
|
73
|
-
await client.scan("retailers", ItemsSchema, {});
|
|
74
|
-
|
|
75
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
76
|
-
action: "scan",
|
|
77
|
-
body: { TableName: "retailers" },
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("rejects objects with unknown scan option keys", async () => {
|
|
82
|
-
const { client, executeQuery } = createClient();
|
|
83
|
-
|
|
84
|
-
await expect(
|
|
85
|
-
Reflect.apply(client.scan, client, [
|
|
86
|
-
"retailers",
|
|
87
|
-
ItemsSchema,
|
|
88
|
-
{ label: "not scan options" },
|
|
89
|
-
]),
|
|
90
|
-
).rejects.toThrow("Invalid DynamoDB scan options: label");
|
|
91
|
-
expect(executeQuery).not.toHaveBeenCalled();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("forwards pagination and parallel-scan options", async () => {
|
|
95
|
-
const { client, executeQuery } = createClient();
|
|
96
|
-
|
|
97
|
-
await client.scan(
|
|
98
|
-
"retailers",
|
|
99
|
-
ItemsSchema,
|
|
100
|
-
{
|
|
101
|
-
exclusiveStartKey: { id: { S: "page-2" } },
|
|
102
|
-
limit: 25,
|
|
103
|
-
projectionExpression: "id, name",
|
|
104
|
-
indexName: "gsi1",
|
|
105
|
-
segment: 0,
|
|
106
|
-
totalSegments: 4,
|
|
107
|
-
filterExpression: "#s = :s",
|
|
108
|
-
expressionAttributeNames: { "#s": "status" },
|
|
109
|
-
expressionAttributeValues: { ":s": { S: "active" } },
|
|
110
|
-
},
|
|
111
|
-
{ label: "page retailers" },
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
115
|
-
action: "scan",
|
|
116
|
-
body: {
|
|
117
|
-
TableName: "retailers",
|
|
118
|
-
ExclusiveStartKey: { id: { S: "page-2" } },
|
|
119
|
-
Limit: 25,
|
|
120
|
-
ProjectionExpression: "id, name",
|
|
121
|
-
IndexName: "gsi1",
|
|
122
|
-
Segment: 0,
|
|
123
|
-
TotalSegments: 4,
|
|
124
|
-
FilterExpression: "#s = :s",
|
|
125
|
-
ExpressionAttributeNames: { "#s": "status" },
|
|
126
|
-
ExpressionAttributeValues: { ":s": { S: "active" } },
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
expect(executeQuery).toHaveBeenCalledWith(expect.anything(), undefined, {
|
|
130
|
-
label: "page retailers",
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("keeps positional names and metadata working together", async () => {
|
|
135
|
-
const { client, executeQuery } = createClient();
|
|
136
|
-
|
|
137
|
-
await client.scan(
|
|
138
|
-
"retailers",
|
|
139
|
-
ItemsSchema,
|
|
140
|
-
"#s = :s",
|
|
141
|
-
{ ":s": { S: "active" } },
|
|
142
|
-
{ "#s": "status" },
|
|
143
|
-
{ label: "filtered scan" },
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
expect(parsedBody(executeQuery).body).toEqual({
|
|
147
|
-
TableName: "retailers",
|
|
148
|
-
FilterExpression: "#s = :s",
|
|
149
|
-
ExpressionAttributeValues: { ":s": { S: "active" } },
|
|
150
|
-
ExpressionAttributeNames: { "#s": "status" },
|
|
151
|
-
});
|
|
152
|
-
expect(executeQuery.mock.calls[0][2]).toEqual({ label: "filtered scan" });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("keeps schemas that strip pagination cursors backward compatible", async () => {
|
|
156
|
-
const lastEvaluatedKey = {
|
|
157
|
-
id: { S: "retailer-18" },
|
|
158
|
-
version: { N: "2" },
|
|
159
|
-
};
|
|
160
|
-
const { client } = createClient({
|
|
161
|
-
Items: [{ id: { S: "retailer-1" } }],
|
|
162
|
-
LastEvaluatedKey: lastEvaluatedKey,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
await expect(
|
|
166
|
-
client.scan("retailers", ItemsSchema, { limit: 25 }),
|
|
167
|
-
).resolves.toEqual({
|
|
168
|
-
Items: [{ id: { S: "retailer-1" } }],
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("preserves mixed AttributeValue pagination cursors", async () => {
|
|
173
|
-
const lastEvaluatedKey = {
|
|
174
|
-
id: { S: "retailer-18" },
|
|
175
|
-
version: { N: "2" },
|
|
176
|
-
};
|
|
177
|
-
const PageSchema = z.object({
|
|
178
|
-
Items: z.array(z.unknown()).optional(),
|
|
179
|
-
LastEvaluatedKey: z.record(z.unknown()).optional(),
|
|
180
|
-
});
|
|
181
|
-
const { client } = createClient({
|
|
182
|
-
Items: [],
|
|
183
|
-
LastEvaluatedKey: lastEvaluatedKey,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const page = await client.scan("retailers", PageSchema, {});
|
|
187
|
-
|
|
188
|
-
expect(page.LastEvaluatedKey).toEqual(lastEvaluatedKey);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("threads each returned pagination cursor into the next request", async () => {
|
|
192
|
-
const LastEvaluatedKeySchema = z.object({
|
|
193
|
-
id: z.object({ S: z.string() }),
|
|
194
|
-
});
|
|
195
|
-
const PageSchema = z.object({
|
|
196
|
-
Items: z.array(z.string()),
|
|
197
|
-
LastEvaluatedKey: LastEvaluatedKeySchema.optional(),
|
|
198
|
-
});
|
|
199
|
-
const lastEvaluatedKey = { id: { S: "retailer-1" } };
|
|
200
|
-
const executeQuery = vi
|
|
201
|
-
.fn()
|
|
202
|
-
.mockResolvedValueOnce({
|
|
203
|
-
Items: ["retailer-1"],
|
|
204
|
-
LastEvaluatedKey: lastEvaluatedKey,
|
|
205
|
-
})
|
|
206
|
-
.mockResolvedValueOnce({ Items: ["retailer-2"] });
|
|
207
|
-
const client = new DynamoDBClientImpl(TEST_CONFIG, executeQuery);
|
|
208
|
-
|
|
209
|
-
const items: string[] = [];
|
|
210
|
-
let exclusiveStartKey: z.infer<typeof LastEvaluatedKeySchema> | undefined;
|
|
211
|
-
do {
|
|
212
|
-
const page = await client.scan("retailers", PageSchema, {
|
|
213
|
-
exclusiveStartKey,
|
|
214
|
-
});
|
|
215
|
-
items.push(...page.Items);
|
|
216
|
-
exclusiveStartKey = page.LastEvaluatedKey;
|
|
217
|
-
} while (exclusiveStartKey);
|
|
218
|
-
|
|
219
|
-
expect(items).toEqual(["retailer-1", "retailer-2"]);
|
|
220
|
-
expect(executeQuery).toHaveBeenCalledTimes(2);
|
|
221
|
-
expect(parsedBody(executeQuery, 0).body).toEqual({
|
|
222
|
-
TableName: "retailers",
|
|
223
|
-
});
|
|
224
|
-
expect(parsedBody(executeQuery, 1).body).toEqual({
|
|
225
|
-
TableName: "retailers",
|
|
226
|
-
ExclusiveStartKey: lastEvaluatedKey,
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe("queryTable", () => {
|
|
232
|
-
it("treats a plain names map as ExpressionAttributeNames", async () => {
|
|
233
|
-
const { client, executeQuery } = createClient();
|
|
234
|
-
|
|
235
|
-
await client.queryTable(
|
|
236
|
-
"orders",
|
|
237
|
-
"#uid = :uid",
|
|
238
|
-
{ ":uid": { S: "user-123" } },
|
|
239
|
-
ItemsSchema,
|
|
240
|
-
{ "#uid": "userId" },
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
expect(parsedBody(executeQuery)).toEqual({
|
|
244
|
-
action: "query",
|
|
245
|
-
body: {
|
|
246
|
-
TableName: "orders",
|
|
247
|
-
KeyConditionExpression: "#uid = :uid",
|
|
248
|
-
ExpressionAttributeValues: { ":uid": { S: "user-123" } },
|
|
249
|
-
ExpressionAttributeNames: { "#uid": "userId" },
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
});
|