@superblocksteam/sdk-api 0.0.12-body-types → 0.0.13
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 +23 -0
- package/dist/errors.d.ts +36 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +231 -38
- package/dist/errors.js.map +1 -1
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +189 -0
- package/dist/errors.test.js.map +1 -0
- 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/anthropic/types.d.ts +1 -1
- package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.js +2 -5
- package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
- package/dist/integrations/base/index.d.ts +1 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/request-body.d.ts +18 -10
- package/dist/integrations/base/request-body.d.ts.map +1 -1
- package/dist/integrations/base/request-body.js +131 -215
- package/dist/integrations/base/request-body.js.map +1 -1
- package/dist/integrations/base/request-body.test.js +306 -164
- package/dist/integrations/base/request-body.test.js.map +1 -1
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +13 -26
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +2 -5
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +0 -8
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/cosmosdb/client.d.ts.map +1 -1
- package/dist/integrations/cosmosdb/client.js +2 -5
- package/dist/integrations/cosmosdb/client.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +88 -2
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/documentation.test.js +10 -0
- package/dist/integrations/documentation.test.js.map +1 -1
- package/dist/integrations/dynamodb/client.d.ts +6 -2
- package/dist/integrations/dynamodb/client.d.ts.map +1 -1
- package/dist/integrations/dynamodb/client.js +85 -15
- package/dist/integrations/dynamodb/client.js.map +1 -1
- package/dist/integrations/dynamodb/client.test.d.ts +8 -0
- package/dist/integrations/dynamodb/client.test.d.ts.map +1 -0
- package/dist/integrations/dynamodb/client.test.js +198 -0
- package/dist/integrations/dynamodb/client.test.js.map +1 -0
- 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 +27 -1
- package/dist/integrations/dynamodb/types.d.ts.map +1 -1
- package/dist/integrations/gcs/client.d.ts.map +1 -1
- package/dist/integrations/gcs/client.js +2 -5
- package/dist/integrations/gcs/client.js.map +1 -1
- package/dist/integrations/gsheets/client.d.ts.map +1 -1
- package/dist/integrations/gsheets/client.js +2 -5
- package/dist/integrations/gsheets/client.js.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/mongodb/client.d.ts.map +1 -1
- package/dist/integrations/mongodb/client.js +2 -5
- package/dist/integrations/mongodb/client.js.map +1 -1
- package/dist/integrations/restapiintegration/client.body-types.test.js +107 -25
- package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -1
- package/dist/integrations/restapiintegration/client.test.js +16 -0
- package/dist/integrations/restapiintegration/client.test.js.map +1 -1
- package/dist/integrations/s3/client.d.ts.map +1 -1
- package/dist/integrations/s3/client.js +2 -5
- package/dist/integrations/s3/client.js.map +1 -1
- package/dist/integrations/salesforce/client.d.ts.map +1 -1
- package/dist/integrations/salesforce/client.js +2 -5
- package/dist/integrations/salesforce/client.js.map +1 -1
- package/dist/integrations/slack/client.d.ts.map +1 -1
- package/dist/integrations/slack/client.js +4 -13
- package/dist/integrations/slack/client.js.map +1 -1
- package/dist/integrations/slack/client.test.js +2 -5
- package/dist/integrations/slack/client.test.js.map +1 -1
- package/dist/integrations/utils.d.ts +8 -3
- package/dist/integrations/utils.d.ts.map +1 -1
- package/dist/integrations/utils.js +42 -1
- package/dist/integrations/utils.js.map +1 -1
- package/dist/runtime/context.d.ts +6 -0
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js +2 -1
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/executor.d.ts +6 -0
- package/dist/runtime/executor.d.ts.map +1 -1
- package/dist/runtime/executor.js +1 -0
- package/dist/runtime/executor.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/errors.test.ts +257 -0
- package/src/errors.ts +289 -46
- package/src/index.ts +1 -0
- package/src/integrations/anthropic/README.md +12 -59
- package/src/integrations/anthropic/types.ts +1 -1
- package/src/integrations/base/decode-worker-binary-response.ts +5 -7
- package/src/integrations/base/index.ts +0 -5
- package/src/integrations/base/request-body.test.ts +358 -195
- package/src/integrations/base/request-body.ts +170 -326
- package/src/integrations/base/rest-api-client-base.ts +22 -31
- package/src/integrations/base/rest-api-integration-client.ts +8 -7
- package/src/integrations/base/types.ts +0 -18
- package/src/integrations/box/README.md +4 -60
- package/src/integrations/cosmosdb/client.ts +8 -7
- package/src/integrations/documentation-resolver.test.ts +108 -2
- package/src/integrations/documentation.test.ts +12 -0
- package/src/integrations/dropbox/README.md +1 -71
- package/src/integrations/dropbox/docs.manifest.json +10 -1
- package/src/integrations/dropbox/overlays/upload-unsupported.md +3 -0
- package/src/integrations/dropbox/overlays/upload.md +89 -0
- package/src/integrations/dynamodb/README.md +33 -12
- package/src/integrations/dynamodb/client.test.ts +254 -0
- package/src/integrations/dynamodb/client.ts +129 -23
- package/src/integrations/dynamodb/index.ts +5 -1
- package/src/integrations/dynamodb/types.ts +33 -1
- package/src/integrations/elasticsearch/README.md +0 -80
- package/src/integrations/elasticsearch/docs.manifest.json +10 -1
- package/src/integrations/elasticsearch/overlays/bulk-unsupported.md +3 -0
- package/src/integrations/elasticsearch/overlays/bulk.md +80 -0
- package/src/integrations/gcs/client.ts +8 -7
- package/src/integrations/googledrive/README.md +10 -36
- package/src/integrations/gsheets/client.ts +8 -7
- package/src/integrations/index.ts +1 -0
- package/src/integrations/jira/README.md +0 -26
- package/src/integrations/jira/docs.manifest.json +10 -1
- package/src/integrations/jira/overlays/attachments-unsupported.md +3 -0
- package/src/integrations/jira/overlays/attachments.md +30 -0
- package/src/integrations/mongodb/client.ts +8 -7
- package/src/integrations/restapiintegration/client.body-types.test.ts +135 -26
- package/src/integrations/restapiintegration/client.test.ts +23 -0
- package/src/integrations/restapiintegration/docs.manifest.json +2 -2
- package/src/integrations/restapiintegration/overlays/request-body-types.md +26 -17
- package/src/integrations/s3/client.ts +8 -7
- package/src/integrations/salesforce/client.ts +9 -7
- package/src/integrations/slack/client.test.ts +2 -5
- package/src/integrations/slack/client.ts +13 -19
- package/src/integrations/stabilityai/README.md +0 -87
- package/src/integrations/stabilityai/docs.manifest.json +10 -1
- package/src/integrations/stabilityai/overlays/image-uploads-unsupported.md +3 -0
- package/src/integrations/stabilityai/overlays/image-uploads.md +120 -0
- package/src/integrations/stripe/README.md +0 -201
- package/src/integrations/stripe/docs.manifest.json +10 -1
- package/src/integrations/stripe/overlays/request-bodies-unsupported.md +3 -0
- package/src/integrations/stripe/overlays/request-bodies.md +204 -0
- package/src/integrations/utils.ts +51 -1
- package/src/runtime/context.ts +9 -0
- package/src/runtime/executor.ts +8 -0
- package/src/types.ts +11 -0
|
@@ -12,6 +12,7 @@ Execute operations against Amazon DynamoDB with full type safety and runtime val
|
|
|
12
12
|
| `updateItem(table, key, expr, values, names?, metadata?)` | Update an existing item |
|
|
13
13
|
| `deleteItem(table, key, metadata?)` | Delete an item by key |
|
|
14
14
|
| `scan(table, schema, filter?, values?, names?, metadata?)` | Scan a table |
|
|
15
|
+
| `scan(table, schema, options, metadata?)` | Scan with pagination options |
|
|
15
16
|
| `queryTable(table, keyExpr, values, schema, names?, metadata?)` | Query by key condition |
|
|
16
17
|
| `batchWriteItem(requestItems, metadata?)` | Batch write multiple items |
|
|
17
18
|
| `listTables(schema, metadata?)` | List all tables |
|
|
@@ -133,21 +134,41 @@ await ctx.integrations.dynamodb.deleteItem("users", { id: { S: "user-123" } });
|
|
|
133
134
|
### Scan Table
|
|
134
135
|
|
|
135
136
|
```typescript
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// DynamoDB Scan returns raw AttributeValue maps.
|
|
138
|
+
// import type { DynamoDBAttributeValue } from "@superblocksteam/sdk-api";
|
|
139
|
+
const AttributeValueSchema = z.custom<DynamoDBAttributeValue>();
|
|
140
|
+
const ItemSchema = z.record(AttributeValueSchema);
|
|
141
|
+
const ScanPageSchema = z.object({
|
|
142
|
+
Items: z.array(ItemSchema).optional(),
|
|
143
|
+
LastEvaluatedKey: z.record(AttributeValueSchema).optional(),
|
|
144
|
+
});
|
|
141
145
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
146
|
+
// Paginate past DynamoDB's 1 MB per-Scan limit. Bound the number of
|
|
147
|
+
// requests so throttling or an unexpectedly large table cannot consume
|
|
148
|
+
// the entire API execution timeout.
|
|
149
|
+
const MAX_PAGES = 100;
|
|
150
|
+
let exclusiveStartKey: Record<string, DynamoDBAttributeValue> | undefined;
|
|
151
|
+
const allItems: Array<Record<string, DynamoDBAttributeValue>> = [];
|
|
152
|
+
for (let pageNumber = 0; pageNumber < MAX_PAGES; pageNumber += 1) {
|
|
153
|
+
const page = await ctx.integrations.dynamodb.scan("users", ScanPageSchema, {
|
|
154
|
+
exclusiveStartKey,
|
|
155
|
+
filterExpression: "status = :s",
|
|
156
|
+
expressionAttributeValues: { ":s": { S: "active" } },
|
|
157
|
+
});
|
|
158
|
+
allItems.push(...(page.Items ?? []));
|
|
159
|
+
exclusiveStartKey = page.LastEvaluatedKey;
|
|
160
|
+
if (!exclusiveStartKey) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (pageNumber === MAX_PAGES - 1) {
|
|
164
|
+
throw new Error(`Scan exceeded the ${MAX_PAGES}-page safety limit`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
149
167
|
```
|
|
150
168
|
|
|
169
|
+
Choose a page cap appropriate for the API timeout and provisioned read
|
|
170
|
+
capacity. For very large tables, process and persist pages incrementally.
|
|
171
|
+
|
|
151
172
|
### Query Table by Key Condition
|
|
152
173
|
|
|
153
174
|
```typescript
|
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
});
|
|
@@ -8,11 +8,30 @@
|
|
|
8
8
|
|
|
9
9
|
import type { z } from "zod";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
RestApiValidationError,
|
|
13
|
+
restApiValidationErrorFromZodError,
|
|
14
|
+
} from "../../errors.js";
|
|
12
15
|
import { IntegrationError } from "../../runtime/errors.js";
|
|
13
16
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
14
17
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
15
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
DynamoDBClient,
|
|
20
|
+
DynamoDBAttributeValue,
|
|
21
|
+
DynamoDBScanOptions,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
const DYNAMODB_SCAN_OPTION_KEYS = new Set<string>([
|
|
25
|
+
"filterExpression",
|
|
26
|
+
"expressionAttributeValues",
|
|
27
|
+
"expressionAttributeNames",
|
|
28
|
+
"exclusiveStartKey",
|
|
29
|
+
"limit",
|
|
30
|
+
"projectionExpression",
|
|
31
|
+
"indexName",
|
|
32
|
+
"segment",
|
|
33
|
+
"totalSegments",
|
|
34
|
+
]);
|
|
16
35
|
|
|
17
36
|
/**
|
|
18
37
|
* Internal implementation of DynamoDBClient.
|
|
@@ -69,12 +88,10 @@ export class DynamoDBClientImpl
|
|
|
69
88
|
const parseResult = schema.safeParse(result);
|
|
70
89
|
|
|
71
90
|
if (!parseResult.success) {
|
|
72
|
-
throw
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
data: result,
|
|
77
|
-
},
|
|
91
|
+
throw restApiValidationErrorFromZodError(
|
|
92
|
+
"Result validation failed",
|
|
93
|
+
parseResult.error,
|
|
94
|
+
result,
|
|
78
95
|
);
|
|
79
96
|
}
|
|
80
97
|
|
|
@@ -189,33 +206,120 @@ export class DynamoDBClientImpl
|
|
|
189
206
|
return this.executeWithErrorHandling(request, "deleteItem", metadata);
|
|
190
207
|
}
|
|
191
208
|
|
|
209
|
+
/** Copy optional AWS Scan fields onto the request body. */
|
|
210
|
+
private applyScanParams(
|
|
211
|
+
params: Record<string, unknown>,
|
|
212
|
+
options: DynamoDBScanOptions,
|
|
213
|
+
): void {
|
|
214
|
+
if (options.filterExpression) {
|
|
215
|
+
params.FilterExpression = options.filterExpression;
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
options.expressionAttributeValues &&
|
|
219
|
+
Object.keys(options.expressionAttributeValues).length > 0
|
|
220
|
+
) {
|
|
221
|
+
params.ExpressionAttributeValues = options.expressionAttributeValues;
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
options.expressionAttributeNames &&
|
|
225
|
+
Object.keys(options.expressionAttributeNames).length > 0
|
|
226
|
+
) {
|
|
227
|
+
params.ExpressionAttributeNames = options.expressionAttributeNames;
|
|
228
|
+
}
|
|
229
|
+
if (options.exclusiveStartKey) {
|
|
230
|
+
params.ExclusiveStartKey = options.exclusiveStartKey;
|
|
231
|
+
}
|
|
232
|
+
if (options.limit !== undefined) {
|
|
233
|
+
params.Limit = options.limit;
|
|
234
|
+
}
|
|
235
|
+
if (options.projectionExpression) {
|
|
236
|
+
params.ProjectionExpression = options.projectionExpression;
|
|
237
|
+
}
|
|
238
|
+
if (options.indexName) {
|
|
239
|
+
params.IndexName = options.indexName;
|
|
240
|
+
}
|
|
241
|
+
if (options.segment !== undefined) {
|
|
242
|
+
params.Segment = options.segment;
|
|
243
|
+
}
|
|
244
|
+
if (options.totalSegments !== undefined) {
|
|
245
|
+
params.TotalSegments = options.totalSegments;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private isScanOptions(value: unknown): value is DynamoDBScanOptions {
|
|
250
|
+
return (
|
|
251
|
+
typeof value === "object" &&
|
|
252
|
+
value !== null &&
|
|
253
|
+
Object.keys(value).every((key) => DYNAMODB_SCAN_OPTION_KEYS.has(key))
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private isTraceMetadata(value: unknown): value is TraceMetadata {
|
|
258
|
+
return (
|
|
259
|
+
typeof value === "object" &&
|
|
260
|
+
value !== null &&
|
|
261
|
+
Object.keys(value).every(
|
|
262
|
+
(key) => key === "label" || key === "description",
|
|
263
|
+
) &&
|
|
264
|
+
Object.values(value).every(
|
|
265
|
+
(entry) => entry === undefined || typeof entry === "string",
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
192
270
|
async scan<T>(
|
|
193
271
|
table: string,
|
|
194
272
|
schema: z.ZodSchema<T>,
|
|
195
|
-
|
|
196
|
-
|
|
273
|
+
filterExpressionOrOptions?: string | DynamoDBScanOptions,
|
|
274
|
+
expressionAttributeValuesOrMetadata?:
|
|
275
|
+
| Record<string, DynamoDBAttributeValue>
|
|
276
|
+
| TraceMetadata,
|
|
197
277
|
expressionAttributeNames?: Record<string, string>,
|
|
198
278
|
metadata?: TraceMetadata,
|
|
199
279
|
): Promise<T> {
|
|
200
280
|
const params: Record<string, unknown> = { TableName: table };
|
|
281
|
+
let resolvedMetadata = metadata;
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
typeof filterExpressionOrOptions === "object" &&
|
|
285
|
+
filterExpressionOrOptions !== null
|
|
286
|
+
) {
|
|
287
|
+
if (!this.isScanOptions(filterExpressionOrOptions)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Invalid DynamoDB scan options: ${Object.keys(filterExpressionOrOptions).join(", ")}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
this.applyScanParams(params, filterExpressionOrOptions);
|
|
201
293
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
294
|
+
if (expressionAttributeValuesOrMetadata !== undefined) {
|
|
295
|
+
if (!this.isTraceMetadata(expressionAttributeValuesOrMetadata)) {
|
|
296
|
+
throw new Error("Invalid DynamoDB scan trace metadata");
|
|
297
|
+
}
|
|
298
|
+
resolvedMetadata = expressionAttributeValuesOrMetadata;
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
if (filterExpressionOrOptions) {
|
|
302
|
+
params.FilterExpression = filterExpressionOrOptions;
|
|
303
|
+
}
|
|
304
|
+
if (
|
|
305
|
+
expressionAttributeValuesOrMetadata &&
|
|
306
|
+
Object.keys(expressionAttributeValuesOrMetadata).length > 0
|
|
307
|
+
) {
|
|
308
|
+
params.ExpressionAttributeValues = expressionAttributeValuesOrMetadata;
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
expressionAttributeNames &&
|
|
312
|
+
Object.keys(expressionAttributeNames).length > 0
|
|
313
|
+
) {
|
|
314
|
+
params.ExpressionAttributeNames = expressionAttributeNames;
|
|
315
|
+
}
|
|
212
316
|
}
|
|
213
317
|
|
|
214
318
|
const request = this.buildRequest("scan", params);
|
|
215
319
|
const result = await this.executeWithErrorHandling(
|
|
216
320
|
request,
|
|
217
321
|
"scan",
|
|
218
|
-
|
|
322
|
+
resolvedMetadata,
|
|
219
323
|
);
|
|
220
324
|
return this.validateResult(result, schema, "scan");
|
|
221
325
|
}
|
|
@@ -233,8 +337,10 @@ export class DynamoDBClientImpl
|
|
|
233
337
|
KeyConditionExpression: keyConditionExpression,
|
|
234
338
|
ExpressionAttributeValues: expressionAttributeValues,
|
|
235
339
|
};
|
|
236
|
-
|
|
237
|
-
|
|
340
|
+
if (
|
|
341
|
+
expressionAttributeNames &&
|
|
342
|
+
Object.keys(expressionAttributeNames).length > 0
|
|
343
|
+
) {
|
|
238
344
|
params.ExpressionAttributeNames = expressionAttributeNames;
|
|
239
345
|
}
|
|
240
346
|
|
|
@@ -37,6 +37,24 @@ export type DynamoDBAttributeValue =
|
|
|
37
37
|
| { NS: string[] }
|
|
38
38
|
| { BS: string[] };
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Optional Scan parameters forwarded to the AWS SDK.
|
|
42
|
+
*
|
|
43
|
+
* Use `exclusiveStartKey` with `LastEvaluatedKey` from a previous page to
|
|
44
|
+
* continue past DynamoDB's 1 MB per-Scan limit.
|
|
45
|
+
*/
|
|
46
|
+
export interface DynamoDBScanOptions {
|
|
47
|
+
filterExpression?: string;
|
|
48
|
+
expressionAttributeValues?: Record<string, DynamoDBAttributeValue>;
|
|
49
|
+
expressionAttributeNames?: Record<string, string>;
|
|
50
|
+
exclusiveStartKey?: Record<string, DynamoDBAttributeValue>;
|
|
51
|
+
limit?: number;
|
|
52
|
+
projectionExpression?: string;
|
|
53
|
+
indexName?: string;
|
|
54
|
+
segment?: number;
|
|
55
|
+
totalSegments?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
/**
|
|
41
59
|
* DynamoDB client for database operations.
|
|
42
60
|
*
|
|
@@ -84,6 +102,11 @@ export type DynamoDBAttributeValue =
|
|
|
84
102
|
* 'status = :s',
|
|
85
103
|
* { ':s': { S: 'active' } }
|
|
86
104
|
* );
|
|
105
|
+
*
|
|
106
|
+
* // Paginate past the 1 MB Scan limit
|
|
107
|
+
* const page = await ctx.integrations.db.scan('users', UsersSchema, {
|
|
108
|
+
* exclusiveStartKey: lastEvaluatedKey,
|
|
109
|
+
* });
|
|
87
110
|
* ```
|
|
88
111
|
*/
|
|
89
112
|
export interface DynamoDBClient extends BaseIntegrationClient {
|
|
@@ -122,7 +145,10 @@ export interface DynamoDBClient extends BaseIntegrationClient {
|
|
|
122
145
|
): Promise<T>;
|
|
123
146
|
|
|
124
147
|
/**
|
|
125
|
-
* Scan a DynamoDB table
|
|
148
|
+
* Scan a DynamoDB table.
|
|
149
|
+
*
|
|
150
|
+
* Prefer the options-object form when paginating (`exclusiveStartKey`) or
|
|
151
|
+
* when you need `limit`, `indexName`, or parallel scan segments.
|
|
126
152
|
*
|
|
127
153
|
* @param table - The table name
|
|
128
154
|
* @param schema - Zod schema for validating the result
|
|
@@ -133,6 +159,12 @@ export interface DynamoDBClient extends BaseIntegrationClient {
|
|
|
133
159
|
* @param metadata - Optional trace metadata for diagnostics
|
|
134
160
|
* @returns The validated result
|
|
135
161
|
*/
|
|
162
|
+
scan<T>(
|
|
163
|
+
table: string,
|
|
164
|
+
schema: z.ZodSchema<T>,
|
|
165
|
+
options: DynamoDBScanOptions,
|
|
166
|
+
metadata?: TraceMetadata,
|
|
167
|
+
): Promise<T>;
|
|
136
168
|
scan<T>(
|
|
137
169
|
table: string,
|
|
138
170
|
schema: z.ZodSchema<T>,
|
|
@@ -226,70 +226,6 @@ const result = await ctx.integrations.es.apiRequest(
|
|
|
226
226
|
);
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
### Bulk Operations
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
const BulkResponseSchema = z.object({
|
|
233
|
-
took: z.number(),
|
|
234
|
-
errors: z.boolean(),
|
|
235
|
-
items: z.array(
|
|
236
|
-
z.object({
|
|
237
|
-
index: z
|
|
238
|
-
.object({
|
|
239
|
-
_index: z.string(),
|
|
240
|
-
_id: z.string(),
|
|
241
|
-
status: z.number(),
|
|
242
|
-
result: z.string().optional(),
|
|
243
|
-
error: z
|
|
244
|
-
.object({
|
|
245
|
-
type: z.string(),
|
|
246
|
-
reason: z.string(),
|
|
247
|
-
})
|
|
248
|
-
.optional(),
|
|
249
|
-
})
|
|
250
|
-
.optional(),
|
|
251
|
-
delete: z
|
|
252
|
-
.object({
|
|
253
|
-
_index: z.string(),
|
|
254
|
-
_id: z.string(),
|
|
255
|
-
status: z.number(),
|
|
256
|
-
result: z.string().optional(),
|
|
257
|
-
})
|
|
258
|
-
.optional(),
|
|
259
|
-
}),
|
|
260
|
-
),
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Bulk index
|
|
264
|
-
const result = await ctx.integrations.es.apiRequest(
|
|
265
|
-
{
|
|
266
|
-
method: "POST",
|
|
267
|
-
path: "/_bulk",
|
|
268
|
-
headers: {
|
|
269
|
-
"Content-Type": "application/x-ndjson",
|
|
270
|
-
},
|
|
271
|
-
body:
|
|
272
|
-
[
|
|
273
|
-
{ index: { _index: "products", _id: "1" } },
|
|
274
|
-
{ name: "Product 1", price: 10 },
|
|
275
|
-
{ index: { _index: "products", _id: "2" } },
|
|
276
|
-
{ name: "Product 2", price: 20 },
|
|
277
|
-
]
|
|
278
|
-
.map(JSON.stringify)
|
|
279
|
-
.join("\n") + "\n",
|
|
280
|
-
},
|
|
281
|
-
{ response: BulkResponseSchema },
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
if (result.errors) {
|
|
285
|
-
result.items.forEach((item) => {
|
|
286
|
-
if (item.index?.error) {
|
|
287
|
-
console.error(`Failed to index: ${item.index.error.reason}`);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
229
|
### Aggregations
|
|
294
230
|
|
|
295
231
|
```typescript
|
|
@@ -493,22 +429,6 @@ const body = {
|
|
|
493
429
|
};
|
|
494
430
|
```
|
|
495
431
|
|
|
496
|
-
### Bulk Format
|
|
497
|
-
|
|
498
|
-
Bulk operations require NDJSON format:
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
|
-
// WRONG - Regular JSON array
|
|
502
|
-
const body = [{ index: { _index: "products" } }, { name: "Product" }];
|
|
503
|
-
|
|
504
|
-
// CORRECT - Newline-delimited JSON
|
|
505
|
-
const body = '{"index":{"_index":"products"}}\n{"name":"Product"}\n';
|
|
506
|
-
|
|
507
|
-
// Or build programmatically
|
|
508
|
-
const operations = [{ index: { _index: "products" } }, { name: "Product" }];
|
|
509
|
-
const body = operations.map(JSON.stringify).join("\n") + "\n";
|
|
510
|
-
```
|
|
511
|
-
|
|
512
432
|
### Total Hits Accuracy
|
|
513
433
|
|
|
514
434
|
For large result sets, total is approximate by default:
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginId": "elasticsearch",
|
|
3
3
|
"base": "README.md",
|
|
4
|
-
"overlays": [
|
|
4
|
+
"overlays": [
|
|
5
|
+
{
|
|
6
|
+
"file": "overlays/bulk.md",
|
|
7
|
+
"sdkVersionRange": ">=0.0.7"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"file": "overlays/bulk-unsupported.md",
|
|
11
|
+
"sdkVersionRange": "<0.0.7"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
5
14
|
}
|