@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.
Files changed (152) hide show
  1. package/README.md +23 -0
  2. package/dist/errors.d.ts +36 -3
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +231 -38
  5. package/dist/errors.js.map +1 -1
  6. package/dist/errors.test.d.ts +2 -0
  7. package/dist/errors.test.d.ts.map +1 -0
  8. package/dist/errors.test.js +189 -0
  9. package/dist/errors.test.js.map +1 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/anthropic/types.d.ts +1 -1
  14. package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -1
  15. package/dist/integrations/base/decode-worker-binary-response.js +2 -5
  16. package/dist/integrations/base/decode-worker-binary-response.js.map +1 -1
  17. package/dist/integrations/base/index.d.ts +1 -1
  18. package/dist/integrations/base/index.d.ts.map +1 -1
  19. package/dist/integrations/base/request-body.d.ts +18 -10
  20. package/dist/integrations/base/request-body.d.ts.map +1 -1
  21. package/dist/integrations/base/request-body.js +131 -215
  22. package/dist/integrations/base/request-body.js.map +1 -1
  23. package/dist/integrations/base/request-body.test.js +306 -164
  24. package/dist/integrations/base/request-body.test.js.map +1 -1
  25. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  26. package/dist/integrations/base/rest-api-client-base.js +13 -26
  27. package/dist/integrations/base/rest-api-client-base.js.map +1 -1
  28. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  29. package/dist/integrations/base/rest-api-integration-client.js +2 -5
  30. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  31. package/dist/integrations/base/types.d.ts +0 -8
  32. package/dist/integrations/base/types.d.ts.map +1 -1
  33. package/dist/integrations/cosmosdb/client.d.ts.map +1 -1
  34. package/dist/integrations/cosmosdb/client.js +2 -5
  35. package/dist/integrations/cosmosdb/client.js.map +1 -1
  36. package/dist/integrations/documentation-resolver.test.js +88 -2
  37. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  38. package/dist/integrations/documentation.test.js +10 -0
  39. package/dist/integrations/documentation.test.js.map +1 -1
  40. package/dist/integrations/dynamodb/client.d.ts +6 -2
  41. package/dist/integrations/dynamodb/client.d.ts.map +1 -1
  42. package/dist/integrations/dynamodb/client.js +85 -15
  43. package/dist/integrations/dynamodb/client.js.map +1 -1
  44. package/dist/integrations/dynamodb/client.test.d.ts +8 -0
  45. package/dist/integrations/dynamodb/client.test.d.ts.map +1 -0
  46. package/dist/integrations/dynamodb/client.test.js +198 -0
  47. package/dist/integrations/dynamodb/client.test.js.map +1 -0
  48. package/dist/integrations/dynamodb/index.d.ts +1 -1
  49. package/dist/integrations/dynamodb/index.d.ts.map +1 -1
  50. package/dist/integrations/dynamodb/index.js.map +1 -1
  51. package/dist/integrations/dynamodb/types.d.ts +27 -1
  52. package/dist/integrations/dynamodb/types.d.ts.map +1 -1
  53. package/dist/integrations/gcs/client.d.ts.map +1 -1
  54. package/dist/integrations/gcs/client.js +2 -5
  55. package/dist/integrations/gcs/client.js.map +1 -1
  56. package/dist/integrations/gsheets/client.d.ts.map +1 -1
  57. package/dist/integrations/gsheets/client.js +2 -5
  58. package/dist/integrations/gsheets/client.js.map +1 -1
  59. package/dist/integrations/index.d.ts +1 -1
  60. package/dist/integrations/index.d.ts.map +1 -1
  61. package/dist/integrations/index.js.map +1 -1
  62. package/dist/integrations/mongodb/client.d.ts.map +1 -1
  63. package/dist/integrations/mongodb/client.js +2 -5
  64. package/dist/integrations/mongodb/client.js.map +1 -1
  65. package/dist/integrations/restapiintegration/client.body-types.test.js +107 -25
  66. package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -1
  67. package/dist/integrations/restapiintegration/client.test.js +16 -0
  68. package/dist/integrations/restapiintegration/client.test.js.map +1 -1
  69. package/dist/integrations/s3/client.d.ts.map +1 -1
  70. package/dist/integrations/s3/client.js +2 -5
  71. package/dist/integrations/s3/client.js.map +1 -1
  72. package/dist/integrations/salesforce/client.d.ts.map +1 -1
  73. package/dist/integrations/salesforce/client.js +2 -5
  74. package/dist/integrations/salesforce/client.js.map +1 -1
  75. package/dist/integrations/slack/client.d.ts.map +1 -1
  76. package/dist/integrations/slack/client.js +4 -13
  77. package/dist/integrations/slack/client.js.map +1 -1
  78. package/dist/integrations/slack/client.test.js +2 -5
  79. package/dist/integrations/slack/client.test.js.map +1 -1
  80. package/dist/integrations/utils.d.ts +8 -3
  81. package/dist/integrations/utils.d.ts.map +1 -1
  82. package/dist/integrations/utils.js +42 -1
  83. package/dist/integrations/utils.js.map +1 -1
  84. package/dist/runtime/context.d.ts +6 -0
  85. package/dist/runtime/context.d.ts.map +1 -1
  86. package/dist/runtime/context.js +2 -1
  87. package/dist/runtime/context.js.map +1 -1
  88. package/dist/runtime/executor.d.ts +6 -0
  89. package/dist/runtime/executor.d.ts.map +1 -1
  90. package/dist/runtime/executor.js +1 -0
  91. package/dist/runtime/executor.js.map +1 -1
  92. package/dist/types.d.ts +10 -0
  93. package/dist/types.d.ts.map +1 -1
  94. package/package.json +3 -3
  95. package/src/errors.test.ts +257 -0
  96. package/src/errors.ts +289 -46
  97. package/src/index.ts +1 -0
  98. package/src/integrations/anthropic/README.md +12 -59
  99. package/src/integrations/anthropic/types.ts +1 -1
  100. package/src/integrations/base/decode-worker-binary-response.ts +5 -7
  101. package/src/integrations/base/index.ts +0 -5
  102. package/src/integrations/base/request-body.test.ts +358 -195
  103. package/src/integrations/base/request-body.ts +170 -326
  104. package/src/integrations/base/rest-api-client-base.ts +22 -31
  105. package/src/integrations/base/rest-api-integration-client.ts +8 -7
  106. package/src/integrations/base/types.ts +0 -18
  107. package/src/integrations/box/README.md +4 -60
  108. package/src/integrations/cosmosdb/client.ts +8 -7
  109. package/src/integrations/documentation-resolver.test.ts +108 -2
  110. package/src/integrations/documentation.test.ts +12 -0
  111. package/src/integrations/dropbox/README.md +1 -71
  112. package/src/integrations/dropbox/docs.manifest.json +10 -1
  113. package/src/integrations/dropbox/overlays/upload-unsupported.md +3 -0
  114. package/src/integrations/dropbox/overlays/upload.md +89 -0
  115. package/src/integrations/dynamodb/README.md +33 -12
  116. package/src/integrations/dynamodb/client.test.ts +254 -0
  117. package/src/integrations/dynamodb/client.ts +129 -23
  118. package/src/integrations/dynamodb/index.ts +5 -1
  119. package/src/integrations/dynamodb/types.ts +33 -1
  120. package/src/integrations/elasticsearch/README.md +0 -80
  121. package/src/integrations/elasticsearch/docs.manifest.json +10 -1
  122. package/src/integrations/elasticsearch/overlays/bulk-unsupported.md +3 -0
  123. package/src/integrations/elasticsearch/overlays/bulk.md +80 -0
  124. package/src/integrations/gcs/client.ts +8 -7
  125. package/src/integrations/googledrive/README.md +10 -36
  126. package/src/integrations/gsheets/client.ts +8 -7
  127. package/src/integrations/index.ts +1 -0
  128. package/src/integrations/jira/README.md +0 -26
  129. package/src/integrations/jira/docs.manifest.json +10 -1
  130. package/src/integrations/jira/overlays/attachments-unsupported.md +3 -0
  131. package/src/integrations/jira/overlays/attachments.md +30 -0
  132. package/src/integrations/mongodb/client.ts +8 -7
  133. package/src/integrations/restapiintegration/client.body-types.test.ts +135 -26
  134. package/src/integrations/restapiintegration/client.test.ts +23 -0
  135. package/src/integrations/restapiintegration/docs.manifest.json +2 -2
  136. package/src/integrations/restapiintegration/overlays/request-body-types.md +26 -17
  137. package/src/integrations/s3/client.ts +8 -7
  138. package/src/integrations/salesforce/client.ts +9 -7
  139. package/src/integrations/slack/client.test.ts +2 -5
  140. package/src/integrations/slack/client.ts +13 -19
  141. package/src/integrations/stabilityai/README.md +0 -87
  142. package/src/integrations/stabilityai/docs.manifest.json +10 -1
  143. package/src/integrations/stabilityai/overlays/image-uploads-unsupported.md +3 -0
  144. package/src/integrations/stabilityai/overlays/image-uploads.md +120 -0
  145. package/src/integrations/stripe/README.md +0 -201
  146. package/src/integrations/stripe/docs.manifest.json +10 -1
  147. package/src/integrations/stripe/overlays/request-bodies-unsupported.md +3 -0
  148. package/src/integrations/stripe/overlays/request-bodies.md +204 -0
  149. package/src/integrations/utils.ts +51 -1
  150. package/src/runtime/context.ts +9 -0
  151. package/src/runtime/executor.ts +8 -0
  152. 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
- // Full scan
137
- const allUsers = await ctx.integrations.dynamodb.scan(
138
- "users",
139
- z.array(z.object({ id: z.string(), name: z.string() })),
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
- // With filter expression
143
- const activeUsers = await ctx.integrations.dynamodb.scan(
144
- "users",
145
- z.array(z.object({ id: z.string(), name: z.string() })),
146
- "status = :s",
147
- { ":s": { S: "active" } },
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 { RestApiValidationError } from "../../errors.js";
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 { DynamoDBClient, DynamoDBAttributeValue } from "./types.js";
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 new RestApiValidationError(
73
- `Result validation failed: ${parseResult.error.message}`,
74
- {
75
- zodError: parseResult.error,
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
- filterExpression?: string,
196
- expressionAttributeValues?: Record<string, DynamoDBAttributeValue>,
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
- if (filterExpression) {
203
- params.FilterExpression = filterExpression;
204
- }
205
-
206
- if (expressionAttributeValues) {
207
- params.ExpressionAttributeValues = expressionAttributeValues;
208
- }
209
-
210
- if (expressionAttributeNames) {
211
- params.ExpressionAttributeNames = expressionAttributeNames;
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
- metadata,
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
- if (expressionAttributeNames) {
340
+ if (
341
+ expressionAttributeNames &&
342
+ Object.keys(expressionAttributeNames).length > 0
343
+ ) {
238
344
  params.ExpressionAttributeNames = expressionAttributeNames;
239
345
  }
240
346
 
@@ -4,5 +4,9 @@
4
4
  * @module
5
5
  */
6
6
 
7
- export type { DynamoDBClient, DynamoDBAttributeValue } from "./types.js";
7
+ export type {
8
+ DynamoDBClient,
9
+ DynamoDBAttributeValue,
10
+ DynamoDBScanOptions,
11
+ } from "./types.js";
8
12
  export { DynamoDBClientImpl } from "./client.js";
@@ -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 with optional filter.
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
  }