@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
|
@@ -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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
Object.keys(expressionAttributeNames).length > 0
|
|
342
|
-
) {
|
|
236
|
+
|
|
237
|
+
if (expressionAttributeNames) {
|
|
343
238
|
params.ExpressionAttributeNames = expressionAttributeNames;
|
|
344
239
|
}
|
|
345
240
|
|
|
@@ -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>,
|
|
@@ -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¤cy=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.
|