@superblocksteam/sdk-api 0.0.11 → 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.
Files changed (83) hide show
  1. package/README.md +0 -23
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +44 -1
  4. package/dist/errors.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/integrations/base/index.d.ts +2 -1
  9. package/dist/integrations/base/index.d.ts.map +1 -1
  10. package/dist/integrations/base/index.js +1 -0
  11. package/dist/integrations/base/index.js.map +1 -1
  12. package/dist/integrations/base/request-body.d.ts +21 -0
  13. package/dist/integrations/base/request-body.d.ts.map +1 -0
  14. package/dist/integrations/base/request-body.js +322 -0
  15. package/dist/integrations/base/request-body.js.map +1 -0
  16. package/dist/integrations/base/request-body.test.d.ts +2 -0
  17. package/dist/integrations/base/request-body.test.d.ts.map +1 -0
  18. package/dist/integrations/base/request-body.test.js +324 -0
  19. package/dist/integrations/base/request-body.test.js.map +1 -0
  20. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  21. package/dist/integrations/base/rest-api-client-base.js +18 -4
  22. package/dist/integrations/base/rest-api-client-base.js.map +1 -1
  23. package/dist/integrations/base/types.d.ts +40 -5
  24. package/dist/integrations/base/types.d.ts.map +1 -1
  25. package/dist/integrations/base/types.js +7 -0
  26. package/dist/integrations/base/types.js.map +1 -1
  27. package/dist/integrations/documentation-resolver.test.js +20 -1
  28. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  29. package/dist/integrations/dynamodb/client.d.ts +2 -6
  30. package/dist/integrations/dynamodb/client.d.ts.map +1 -1
  31. package/dist/integrations/dynamodb/client.js +10 -83
  32. package/dist/integrations/dynamodb/client.js.map +1 -1
  33. package/dist/integrations/dynamodb/index.d.ts +1 -1
  34. package/dist/integrations/dynamodb/index.d.ts.map +1 -1
  35. package/dist/integrations/dynamodb/index.js.map +1 -1
  36. package/dist/integrations/dynamodb/types.d.ts +1 -27
  37. package/dist/integrations/dynamodb/types.d.ts.map +1 -1
  38. package/dist/integrations/index.d.ts +1 -1
  39. package/dist/integrations/index.d.ts.map +1 -1
  40. package/dist/integrations/index.js.map +1 -1
  41. package/dist/integrations/restapiintegration/client.body-types.test.d.ts +2 -0
  42. package/dist/integrations/restapiintegration/client.body-types.test.d.ts.map +1 -0
  43. package/dist/integrations/restapiintegration/client.body-types.test.js +145 -0
  44. package/dist/integrations/restapiintegration/client.body-types.test.js.map +1 -0
  45. package/dist/integrations/slack/client.test.js +23 -0
  46. package/dist/integrations/slack/client.test.js.map +1 -1
  47. package/dist/runtime/context.d.ts +0 -6
  48. package/dist/runtime/context.d.ts.map +1 -1
  49. package/dist/runtime/context.js +1 -2
  50. package/dist/runtime/context.js.map +1 -1
  51. package/dist/runtime/executor.d.ts +0 -6
  52. package/dist/runtime/executor.d.ts.map +1 -1
  53. package/dist/runtime/executor.js +0 -1
  54. package/dist/runtime/executor.js.map +1 -1
  55. package/dist/types.d.ts +0 -10
  56. package/dist/types.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/errors.ts +52 -1
  59. package/src/index.ts +0 -1
  60. package/src/integrations/base/index.ts +7 -0
  61. package/src/integrations/base/request-body.test.ts +402 -0
  62. package/src/integrations/base/request-body.ts +548 -0
  63. package/src/integrations/base/rest-api-client-base.ts +18 -4
  64. package/src/integrations/base/types.ts +59 -5
  65. package/src/integrations/documentation-resolver.test.ts +24 -1
  66. package/src/integrations/dynamodb/README.md +12 -33
  67. package/src/integrations/dynamodb/client.ts +16 -121
  68. package/src/integrations/dynamodb/index.ts +1 -5
  69. package/src/integrations/dynamodb/types.ts +1 -33
  70. package/src/integrations/index.ts +0 -1
  71. package/src/integrations/restapiintegration/client.body-types.test.ts +214 -0
  72. package/src/integrations/restapiintegration/docs.manifest.json +8 -0
  73. package/src/integrations/restapiintegration/overlays/request-body-types-unsupported.md +7 -0
  74. package/src/integrations/restapiintegration/overlays/request-body-types.md +106 -0
  75. package/src/integrations/slack/client.test.ts +33 -0
  76. package/src/runtime/context.ts +0 -9
  77. package/src/runtime/executor.ts +0 -8
  78. package/src/types.ts +0 -11
  79. package/dist/integrations/dynamodb/client.test.d.ts +0 -8
  80. package/dist/integrations/dynamodb/client.test.d.ts.map +0 -1
  81. package/dist/integrations/dynamodb/client.test.js +0 -198
  82. package/dist/integrations/dynamodb/client.test.js.map +0 -1
  83. package/src/integrations/dynamodb/client.test.ts +0 -254
@@ -859,6 +859,28 @@ describe("resolveIntegrationDocumentation", () => {
859
859
  expect(docs).not.toContain("does not support the `responseType`");
860
860
  });
861
861
 
862
+ it("documents request bodyType only for agents whose sdk-api supports it", async () => {
863
+ const docs = await resolveIntegrationDocumentation("restapiintegration", {
864
+ sdkVersion: "0.0.5",
865
+ });
866
+
867
+ expect(docs).toContain('responseType: "binary"');
868
+ expect(docs).toContain('bodyType: "formUrlencoded"');
869
+ expect(docs).toContain('bodyType: "multipartForm"');
870
+ expect(docs).toContain('bodyType: "binary"');
871
+ expect(docs).not.toContain("does not support the `bodyType`");
872
+ });
873
+
874
+ it("notes bodyType non-support before sdk-api request body support", async () => {
875
+ const docs = await resolveIntegrationDocumentation("restapiintegration", {
876
+ sdkVersion: "0.0.4",
877
+ });
878
+
879
+ expect(docs).toContain('responseType: "binary"');
880
+ expect(docs).toContain("does not support the `bodyType` option");
881
+ expect(docs).not.toContain('bodyType: "formUrlencoded"');
882
+ });
883
+
862
884
  it("notes non-support instead of documenting responseType on older agents", async () => {
863
885
  const docs = await resolveIntegrationDocumentation("restapiintegration", {
864
886
  sdkVersion: "0.0.2",
@@ -869,10 +891,11 @@ describe("resolveIntegrationDocumentation", () => {
869
891
  expect(docs).not.toContain('responseType: "binary"');
870
892
  });
871
893
 
872
- it("stays silent about responseType when the agent reports no sdk-api version", async () => {
894
+ it("stays silent about responseType and request bodyType when the agent reports no sdk-api version", async () => {
873
895
  const docs = await resolveIntegrationDocumentation("restapiintegration");
874
896
 
875
897
  expect(docs).not.toContain("responseType");
898
+ expect(docs).not.toContain("bodyType");
876
899
  });
877
900
  });
878
901
 
@@ -12,7 +12,6 @@ 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 |
16
15
  | `queryTable(table, keyExpr, values, schema, names?, metadata?)` | Query by key condition |
17
16
  | `batchWriteItem(requestItems, metadata?)` | Batch write multiple items |
18
17
  | `listTables(schema, metadata?)` | List all tables |
@@ -134,41 +133,21 @@ await ctx.integrations.dynamodb.deleteItem("users", { id: { S: "user-123" } });
134
133
  ### Scan Table
135
134
 
136
135
  ```typescript
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
- });
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
+ );
145
141
 
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
- }
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
+ );
167
149
  ```
168
150
 
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
-
172
151
  ### Query Table by Key Condition
173
152
 
174
153
  ```typescript
@@ -12,23 +12,7 @@ import { RestApiValidationError } from "../../errors.js";
12
12
  import { IntegrationError } from "../../runtime/errors.js";
13
13
  import type { QueryExecutor, TraceMetadata } from "../registry.js";
14
14
  import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
15
- import type {
16
- DynamoDBClient,
17
- DynamoDBAttributeValue,
18
- DynamoDBScanOptions,
19
- } from "./types.js";
20
-
21
- const DYNAMODB_SCAN_OPTION_KEYS = new Set<string>([
22
- "filterExpression",
23
- "expressionAttributeValues",
24
- "expressionAttributeNames",
25
- "exclusiveStartKey",
26
- "limit",
27
- "projectionExpression",
28
- "indexName",
29
- "segment",
30
- "totalSegments",
31
- ]);
15
+ import type { DynamoDBClient, DynamoDBAttributeValue } from "./types.js";
32
16
 
33
17
  /**
34
18
  * Internal implementation of DynamoDBClient.
@@ -205,120 +189,33 @@ export class DynamoDBClientImpl
205
189
  return this.executeWithErrorHandling(request, "deleteItem", metadata);
206
190
  }
207
191
 
208
- /** Copy optional AWS Scan fields onto the request body. */
209
- private applyScanParams(
210
- params: Record<string, unknown>,
211
- options: DynamoDBScanOptions,
212
- ): void {
213
- if (options.filterExpression) {
214
- params.FilterExpression = options.filterExpression;
215
- }
216
- if (
217
- options.expressionAttributeValues &&
218
- Object.keys(options.expressionAttributeValues).length > 0
219
- ) {
220
- params.ExpressionAttributeValues = options.expressionAttributeValues;
221
- }
222
- if (
223
- options.expressionAttributeNames &&
224
- Object.keys(options.expressionAttributeNames).length > 0
225
- ) {
226
- params.ExpressionAttributeNames = options.expressionAttributeNames;
227
- }
228
- if (options.exclusiveStartKey) {
229
- params.ExclusiveStartKey = options.exclusiveStartKey;
230
- }
231
- if (options.limit !== undefined) {
232
- params.Limit = options.limit;
233
- }
234
- if (options.projectionExpression) {
235
- params.ProjectionExpression = options.projectionExpression;
236
- }
237
- if (options.indexName) {
238
- params.IndexName = options.indexName;
239
- }
240
- if (options.segment !== undefined) {
241
- params.Segment = options.segment;
242
- }
243
- if (options.totalSegments !== undefined) {
244
- params.TotalSegments = options.totalSegments;
245
- }
246
- }
247
-
248
- private isScanOptions(value: unknown): value is DynamoDBScanOptions {
249
- return (
250
- typeof value === "object" &&
251
- value !== null &&
252
- Object.keys(value).every((key) => DYNAMODB_SCAN_OPTION_KEYS.has(key))
253
- );
254
- }
255
-
256
- private isTraceMetadata(value: unknown): value is TraceMetadata {
257
- return (
258
- typeof value === "object" &&
259
- value !== null &&
260
- Object.keys(value).every(
261
- (key) => key === "label" || key === "description",
262
- ) &&
263
- Object.values(value).every(
264
- (entry) => entry === undefined || typeof entry === "string",
265
- )
266
- );
267
- }
268
-
269
192
  async scan<T>(
270
193
  table: string,
271
194
  schema: z.ZodSchema<T>,
272
- filterExpressionOrOptions?: string | DynamoDBScanOptions,
273
- expressionAttributeValuesOrMetadata?:
274
- | Record<string, DynamoDBAttributeValue>
275
- | TraceMetadata,
195
+ filterExpression?: string,
196
+ expressionAttributeValues?: Record<string, DynamoDBAttributeValue>,
276
197
  expressionAttributeNames?: Record<string, string>,
277
198
  metadata?: TraceMetadata,
278
199
  ): Promise<T> {
279
200
  const params: Record<string, unknown> = { TableName: table };
280
- let resolvedMetadata = metadata;
281
-
282
- if (
283
- typeof filterExpressionOrOptions === "object" &&
284
- filterExpressionOrOptions !== null
285
- ) {
286
- if (!this.isScanOptions(filterExpressionOrOptions)) {
287
- throw new Error(
288
- `Invalid DynamoDB scan options: ${Object.keys(filterExpressionOrOptions).join(", ")}`,
289
- );
290
- }
291
- this.applyScanParams(params, filterExpressionOrOptions);
292
201
 
293
- if (expressionAttributeValuesOrMetadata !== undefined) {
294
- if (!this.isTraceMetadata(expressionAttributeValuesOrMetadata)) {
295
- throw new Error("Invalid DynamoDB scan trace metadata");
296
- }
297
- resolvedMetadata = expressionAttributeValuesOrMetadata;
298
- }
299
- } else {
300
- if (filterExpressionOrOptions) {
301
- params.FilterExpression = filterExpressionOrOptions;
302
- }
303
- if (
304
- expressionAttributeValuesOrMetadata &&
305
- Object.keys(expressionAttributeValuesOrMetadata).length > 0
306
- ) {
307
- params.ExpressionAttributeValues = expressionAttributeValuesOrMetadata;
308
- }
309
- if (
310
- expressionAttributeNames &&
311
- Object.keys(expressionAttributeNames).length > 0
312
- ) {
313
- params.ExpressionAttributeNames = expressionAttributeNames;
314
- }
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;
315
212
  }
316
213
 
317
214
  const request = this.buildRequest("scan", params);
318
215
  const result = await this.executeWithErrorHandling(
319
216
  request,
320
217
  "scan",
321
- resolvedMetadata,
218
+ metadata,
322
219
  );
323
220
  return this.validateResult(result, schema, "scan");
324
221
  }
@@ -336,10 +233,8 @@ export class DynamoDBClientImpl
336
233
  KeyConditionExpression: keyConditionExpression,
337
234
  ExpressionAttributeValues: expressionAttributeValues,
338
235
  };
339
- if (
340
- expressionAttributeNames &&
341
- Object.keys(expressionAttributeNames).length > 0
342
- ) {
236
+
237
+ if (expressionAttributeNames) {
343
238
  params.ExpressionAttributeNames = expressionAttributeNames;
344
239
  }
345
240
 
@@ -4,9 +4,5 @@
4
4
  * @module
5
5
  */
6
6
 
7
- export type {
8
- DynamoDBClient,
9
- DynamoDBAttributeValue,
10
- DynamoDBScanOptions,
11
- } from "./types.js";
7
+ export type { DynamoDBClient, DynamoDBAttributeValue } from "./types.js";
12
8
  export { DynamoDBClientImpl } from "./client.js";
@@ -37,24 +37,6 @@ 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
-
58
40
  /**
59
41
  * DynamoDB client for database operations.
60
42
  *
@@ -102,11 +84,6 @@ export interface DynamoDBScanOptions {
102
84
  * 'status = :s',
103
85
  * { ':s': { S: 'active' } }
104
86
  * );
105
- *
106
- * // Paginate past the 1 MB Scan limit
107
- * const page = await ctx.integrations.db.scan('users', UsersSchema, {
108
- * exclusiveStartKey: lastEvaluatedKey,
109
- * });
110
87
  * ```
111
88
  */
112
89
  export interface DynamoDBClient extends BaseIntegrationClient {
@@ -145,10 +122,7 @@ export interface DynamoDBClient extends BaseIntegrationClient {
145
122
  ): Promise<T>;
146
123
 
147
124
  /**
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.
125
+ * Scan a DynamoDB table with optional filter.
152
126
  *
153
127
  * @param table - The table name
154
128
  * @param schema - Zod schema for validating the result
@@ -159,12 +133,6 @@ export interface DynamoDBClient extends BaseIntegrationClient {
159
133
  * @param metadata - Optional trace metadata for diagnostics
160
134
  * @returns The validated result
161
135
  */
162
- scan<T>(
163
- table: string,
164
- schema: z.ZodSchema<T>,
165
- options: DynamoDBScanOptions,
166
- metadata?: TraceMetadata,
167
- ): Promise<T>;
168
136
  scan<T>(
169
137
  table: string,
170
138
  schema: z.ZodSchema<T>,
@@ -363,7 +363,6 @@ export { MongoDBClientImpl } from "./mongodb/index.js";
363
363
  export type {
364
364
  DynamoDBClient,
365
365
  DynamoDBAttributeValue,
366
- DynamoDBScanOptions,
367
366
  } from "./dynamodb/index.js";
368
367
  export { DynamoDBClientImpl } from "./dynamodb/index.js";
369
368
 
@@ -0,0 +1,214 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { z } from "zod";
3
+
4
+ import { RestApiValidationError } from "../../errors.js";
5
+ import type { IntegrationConfig } from "../types.js";
6
+ import { RestApiIntegrationPluginClientImpl } from "./client.js";
7
+
8
+ const TEST_CONFIG: IntegrationConfig = {
9
+ id: "restapi-test-id",
10
+ name: "Test REST API",
11
+ pluginId: "restapiintegration",
12
+ configuration: {},
13
+ };
14
+
15
+ const JsonResponseSchema = z.object({ ok: z.boolean() });
16
+
17
+ function createClient() {
18
+ const executeQuery = vi.fn().mockResolvedValue({ ok: true });
19
+ const client = new RestApiIntegrationPluginClientImpl(
20
+ TEST_CONFIG,
21
+ executeQuery,
22
+ );
23
+ return { client, executeQuery };
24
+ }
25
+
26
+ function callApiRequest(
27
+ client: RestApiIntegrationPluginClientImpl,
28
+ ...args: unknown[]
29
+ ): Promise<unknown> {
30
+ return Reflect.apply(client.apiRequest, client, args);
31
+ }
32
+
33
+ describe("RestApiIntegrationPluginClientImpl request body types", () => {
34
+ it("preserves omitted and explicit JSON body wire behavior", async () => {
35
+ for (const bodyType of [undefined, "json"] as const) {
36
+ const { client, executeQuery } = createClient();
37
+ await client.apiRequest(
38
+ { method: "POST", path: "/payments", body: { amount: 42 }, bodyType },
39
+ { response: JsonResponseSchema },
40
+ );
41
+ expect(executeQuery).toHaveBeenCalledWith(
42
+ expect.objectContaining({
43
+ body: JSON.stringify({ amount: 42 }),
44
+ bodyType: "jsonBody",
45
+ }),
46
+ undefined,
47
+ undefined,
48
+ );
49
+ }
50
+ });
51
+
52
+ it("sends raw string bodies as rawBody", async () => {
53
+ const { client, executeQuery } = createClient();
54
+ await client.apiRequest(
55
+ {
56
+ method: "POST",
57
+ path: "/xml",
58
+ body: "<note>hello</note>",
59
+ bodyType: "raw",
60
+ },
61
+ { response: JsonResponseSchema },
62
+ );
63
+ expect(executeQuery).toHaveBeenCalledWith(
64
+ expect.objectContaining({
65
+ body: "<note>hello</note>",
66
+ bodyType: "rawBody",
67
+ }),
68
+ undefined,
69
+ undefined,
70
+ );
71
+ });
72
+
73
+ it("sends formUrlencoded bodies as rawBody with injected Content-Type", async () => {
74
+ const { client, executeQuery } = createClient();
75
+ await client.apiRequest(
76
+ {
77
+ method: "POST",
78
+ path: "/charges",
79
+ body: { amount: 2000, currency: "usd", "metadata[order_id]": "6735" },
80
+ bodyType: "formUrlencoded",
81
+ },
82
+ { response: JsonResponseSchema },
83
+ );
84
+ expect(executeQuery).toHaveBeenCalledWith(
85
+ expect.objectContaining({
86
+ body: "amount=2000&currency=usd&metadata%5Border_id%5D=6735",
87
+ bodyType: "rawBody",
88
+ headers: [
89
+ { key: "Content-Type", value: "application/x-www-form-urlencoded" },
90
+ ],
91
+ }),
92
+ undefined,
93
+ undefined,
94
+ );
95
+ });
96
+
97
+ it("sends binary bodies as base64 binaryBody with default Content-Type", async () => {
98
+ const bytes = Uint8Array.from([137, 80, 78, 71]);
99
+ const { client, executeQuery } = createClient();
100
+ await client.apiRequest(
101
+ {
102
+ method: "PUT",
103
+ path: "/files/report.png",
104
+ body: bytes,
105
+ bodyType: "binary",
106
+ },
107
+ { response: JsonResponseSchema },
108
+ );
109
+ expect(executeQuery).toHaveBeenCalledWith(
110
+ expect.objectContaining({
111
+ body: Buffer.from(bytes).toString("base64"),
112
+ bodyType: "binaryBody",
113
+ headers: [{ key: "Content-Type", value: "application/octet-stream" }],
114
+ }),
115
+ undefined,
116
+ undefined,
117
+ );
118
+ });
119
+
120
+ it("sends text multipart file parts as formData", async () => {
121
+ const { client, executeQuery } = createClient();
122
+ await client.apiRequest(
123
+ {
124
+ method: "POST",
125
+ path: "/upload",
126
+ body: {
127
+ note: "hello",
128
+ file: { value: "contents", filename: "note.txt" },
129
+ },
130
+ bodyType: "multipartForm",
131
+ },
132
+ { response: JsonResponseSchema },
133
+ );
134
+ expect(executeQuery).toHaveBeenCalledWith(
135
+ expect.objectContaining({
136
+ bodyType: "formData",
137
+ formData: [
138
+ { key: "note", value: "hello" },
139
+ { key: "file", value: "contents", file: { filename: "note.txt" } },
140
+ ],
141
+ }),
142
+ undefined,
143
+ undefined,
144
+ );
145
+ });
146
+
147
+ it("sends mixed multipart file parts as formDataBase64 with no top-level body", async () => {
148
+ const bytes = Uint8Array.from([137, 80, 78, 71]);
149
+ const { client, executeQuery } = createClient();
150
+ await client.apiRequest(
151
+ {
152
+ method: "POST",
153
+ path: "/upload",
154
+ body: { note: "mixed", image: { value: bytes, filename: "b.png" } },
155
+ bodyType: "multipartForm",
156
+ },
157
+ { response: JsonResponseSchema },
158
+ );
159
+ expect(executeQuery).toHaveBeenCalledWith(
160
+ expect.objectContaining({
161
+ bodyType: "formDataBase64",
162
+ formData: [
163
+ { key: "note", value: "mixed" },
164
+ {
165
+ key: "image",
166
+ value: Buffer.from(bytes).toString("base64"),
167
+ file: { filename: "b.png" },
168
+ },
169
+ ],
170
+ }),
171
+ undefined,
172
+ undefined,
173
+ );
174
+ expect(executeQuery.mock.calls[0][0]).not.toHaveProperty("body");
175
+ });
176
+
177
+ it("rejects invalid bodyType and invalid bodies before executeQuery", async () => {
178
+ for (const bodyType of [
179
+ "form",
180
+ "jsonBody",
181
+ "rawBody",
182
+ "formData",
183
+ "fileForm",
184
+ "binaryBody",
185
+ "formDataBase64",
186
+ ]) {
187
+ const { client, executeQuery } = createClient();
188
+ await expect(
189
+ callApiRequest(
190
+ client,
191
+ { method: "POST", path: "/bad", body: {}, bodyType },
192
+ { response: JsonResponseSchema },
193
+ ),
194
+ ).rejects.toThrow(RestApiValidationError);
195
+ expect(executeQuery).not.toHaveBeenCalled();
196
+ }
197
+
198
+ const invalidCases = [
199
+ { method: "POST", path: "/bad", body: Uint8Array.from([1]) },
200
+ { method: "POST", path: "/bad", body: () => undefined },
201
+ { method: "POST", path: "/bad", bodyType: "raw" },
202
+ { method: "POST", path: "/bad", bodyType: "formUrlencoded" },
203
+ { method: "POST", path: "/bad", bodyType: "multipartForm" },
204
+ { method: "POST", path: "/bad", bodyType: "binary" },
205
+ ];
206
+ for (const options of invalidCases) {
207
+ const { client, executeQuery } = createClient();
208
+ await expect(
209
+ callApiRequest(client, options, { response: JsonResponseSchema }),
210
+ ).rejects.toThrow(RestApiValidationError);
211
+ expect(executeQuery).not.toHaveBeenCalled();
212
+ }
213
+ });
214
+ });
@@ -13,6 +13,14 @@
13
13
  {
14
14
  "file": "overlays/response-types-unsupported.md",
15
15
  "sdkVersionRange": "<0.0.3"
16
+ },
17
+ {
18
+ "file": "overlays/request-body-types.md",
19
+ "sdkVersionRange": ">=0.0.5"
20
+ },
21
+ {
22
+ "file": "overlays/request-body-types-unsupported.md",
23
+ "sdkVersionRange": "<0.0.5"
16
24
  }
17
25
  ]
18
26
  }
@@ -0,0 +1,7 @@
1
+ ## Request Body Types
2
+
3
+ This organization's agent does not support the `bodyType` option on `apiRequest()` yet. Its worker runs an sdk-api version that always serializes request bodies as JSON, so raw strings, binary uploads, multipart/form-data, and application/x-www-form-urlencoded payloads cannot be sent from SDK APIs until the agent is upgraded.
4
+
5
+ Do not generate code that passes a `bodyType` value before that upgrade. It may typecheck against a newer local copy of the SDK but fail at runtime on this agent.
6
+
7
+ When the user needs a non-JSON request body, tell them the required action: an administrator must upgrade this organization's Superblocks agent (On-Premise Agent / OPA) to a newer version that supports the `bodyType` option. Until that upgrade happens, there is no code-level workaround.