@superblocksteam/sdk-api 2.0.157 → 2.0.158
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 +6 -6
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -2
- package/dist/errors.js.map +1 -1
- package/dist/integrations/base/decode-worker-binary-response.d.ts +4 -0
- package/dist/integrations/base/decode-worker-binary-response.d.ts.map +1 -0
- package/dist/integrations/base/decode-worker-binary-response.js +49 -0
- package/dist/integrations/base/decode-worker-binary-response.js.map +1 -0
- package/dist/integrations/base/decode-worker-binary-response.test.d.ts +2 -0
- package/dist/integrations/base/decode-worker-binary-response.test.d.ts.map +1 -0
- package/dist/integrations/base/decode-worker-binary-response.test.js +81 -0
- package/dist/integrations/base/decode-worker-binary-response.test.js.map +1 -0
- package/dist/integrations/base/rest-api-client-base.d.ts +0 -27
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +23 -46
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts +7 -19
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +2 -29
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +36 -43
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/base/types.js +1 -26
- package/dist/integrations/base/types.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +11 -5
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/restapiintegration/client.test.d.ts +0 -10
- package/dist/integrations/restapiintegration/client.test.d.ts.map +1 -1
- package/dist/integrations/restapiintegration/client.test.js +146 -69
- package/dist/integrations/restapiintegration/client.test.js.map +1 -1
- package/dist/integrations/slack/client.test.js +26 -1
- package/dist/integrations/slack/client.test.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +33 -5
- package/src/integrations/anthropic/README.md +7 -0
- package/src/integrations/base/decode-worker-binary-response.test.ts +107 -0
- package/src/integrations/base/decode-worker-binary-response.ts +62 -0
- package/src/integrations/base/rest-api-client-base.ts +28 -56
- package/src/integrations/base/rest-api-integration-client.ts +14 -33
- package/src/integrations/base/types.ts +41 -45
- package/src/integrations/bigquery/README.md +1 -0
- package/src/integrations/box/README.md +3 -0
- package/src/integrations/cohere/README.md +7 -0
- package/src/integrations/documentation-resolver.test.ts +13 -6
- package/src/integrations/fireworks/README.md +7 -0
- package/src/integrations/gemini/README.md +8 -0
- package/src/integrations/groq/README.md +7 -0
- package/src/integrations/mistral/README.md +7 -0
- package/src/integrations/openai_v2/README.md +7 -0
- package/src/integrations/perplexity/README.md +7 -0
- package/src/integrations/restapiintegration/client.test.ts +207 -86
- package/src/integrations/restapiintegration/docs.manifest.json +5 -1
- package/src/integrations/restapiintegration/overlays/response-types-binary.md +51 -0
- package/src/integrations/s3/README.md +1 -0
- package/src/integrations/slack/client.test.ts +36 -1
- package/src/integrations/snowflakecortex/README.md +8 -0
- package/src/integrations/stabilityai/README.md +7 -0
|
@@ -257,6 +257,13 @@ All methods accept an optional `metadata` parameter as the last argument for dia
|
|
|
257
257
|
|
|
258
258
|
## Common Pitfalls
|
|
259
259
|
|
|
260
|
+
### Streaming Is Not Supported
|
|
261
|
+
|
|
262
|
+
`apiRequest()` does not support streaming or Server-Sent Events. Do not set
|
|
263
|
+
`stream: true` — streaming responses fail schema validation. Every call
|
|
264
|
+
returns the complete response; if a UI needs real-time token streaming,
|
|
265
|
+
handle it at the frontend layer, not through the SDK.
|
|
266
|
+
|
|
260
267
|
### No Specialized Methods
|
|
261
268
|
|
|
262
269
|
The OpenAI client only provides `apiRequest()`. There are no other specialized methods:
|
|
@@ -189,6 +189,13 @@ All methods accept an optional `metadata` parameter as the last argument for dia
|
|
|
189
189
|
|
|
190
190
|
## Common Pitfalls
|
|
191
191
|
|
|
192
|
+
### Streaming Is Not Supported
|
|
193
|
+
|
|
194
|
+
`apiRequest()` does not support streaming or Server-Sent Events. Do not set
|
|
195
|
+
`stream: true` — streaming responses fail schema validation. Every call
|
|
196
|
+
returns the complete response; if a UI needs real-time token streaming,
|
|
197
|
+
handle it at the frontend layer, not through the SDK.
|
|
198
|
+
|
|
192
199
|
### No Specialized Methods
|
|
193
200
|
|
|
194
201
|
```typescript
|
|
@@ -1,37 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
* Tests for RestApiIntegrationPluginClientImpl (generic REST API Integration).
|
|
3
|
-
*
|
|
4
|
-
* Covers the responseType passthrough:
|
|
5
|
-
* - default requests keep responseType "json"; callers that pass a
|
|
6
|
-
* response schema still get validated results
|
|
7
|
-
* - the responseType option is forwarded to the orchestrator request
|
|
8
|
-
* - non-JSON responses (e.g. XML with responseType "text") are returned
|
|
9
|
-
* raw when no response schema is provided
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, vi } from "vitest";
|
|
1
|
+
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
13
2
|
import { z } from "zod";
|
|
14
3
|
|
|
15
|
-
import { ActionResponseType } from "@superblocksteam/shared";
|
|
4
|
+
import type { ActionResponseType } from "@superblocksteam/shared";
|
|
16
5
|
|
|
17
6
|
import { RestApiValidationError } from "../../errors.js";
|
|
18
|
-
import
|
|
7
|
+
import { REST_API_RESPONSE_TYPES } from "../base/types.js";
|
|
19
8
|
import type { IntegrationConfig } from "../types.js";
|
|
20
9
|
import { RestApiIntegrationPluginClientImpl } from "./client.js";
|
|
21
10
|
|
|
22
|
-
// ── Wire-contract assertions (verified by `pnpm typecheck`) ────────
|
|
23
|
-
//
|
|
24
|
-
// The responseType strings are sent to the orchestrator as-is; the
|
|
25
|
-
// accepted values are defined by ActionResponseType. This assertion
|
|
26
|
-
// fails compilation if the SDK union drifts outside the wire values.
|
|
27
|
-
//
|
|
28
|
-
// The SDK union is a deliberate subset of the wire values: "auto" and
|
|
29
|
-
// "binary" are implemented by the orchestrator but not exposed (see the
|
|
30
|
-
// RestApiResponseType doc for why), and "raw" is streaming-only.
|
|
31
|
-
const _sdkValuesAreValidWireValues: Exclude<`${ActionResponseType}`, "raw">[] =
|
|
32
|
-
[] as RestApiResponseType[];
|
|
33
|
-
void _sdkValuesAreValidWireValues;
|
|
34
|
-
|
|
35
11
|
const TEST_CONFIG: IntegrationConfig = {
|
|
36
12
|
id: "restapi-test-id",
|
|
37
13
|
name: "Test REST API",
|
|
@@ -40,6 +16,11 @@ const TEST_CONFIG: IntegrationConfig = {
|
|
|
40
16
|
};
|
|
41
17
|
|
|
42
18
|
const XML_RESPONSE = `<?xml version="1.0"?><note><body>hi</body></note>`;
|
|
19
|
+
const PDF_BYTES = [0x25, 0x50, 0x44, 0x46];
|
|
20
|
+
|
|
21
|
+
function workerBufferPayload(data: unknown): unknown {
|
|
22
|
+
return { type: "Buffer", data };
|
|
23
|
+
}
|
|
43
24
|
|
|
44
25
|
function createClient(mockResult: unknown) {
|
|
45
26
|
const executeQuery = vi.fn().mockResolvedValue(mockResult);
|
|
@@ -50,9 +31,14 @@ function createClient(mockResult: unknown) {
|
|
|
50
31
|
return { client, executeQuery };
|
|
51
32
|
}
|
|
52
33
|
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
function callApiRequest(
|
|
35
|
+
client: RestApiIntegrationPluginClientImpl,
|
|
36
|
+
...args: unknown[]
|
|
37
|
+
): Promise<unknown> {
|
|
38
|
+
return Reflect.apply(client.apiRequest, client, args);
|
|
39
|
+
}
|
|
55
40
|
|
|
41
|
+
describe("RestApiIntegrationPluginClientImpl", () => {
|
|
56
42
|
describe("default JSON behavior", () => {
|
|
57
43
|
it("sends responseType json when the option is omitted", async () => {
|
|
58
44
|
const { client, executeQuery } = createClient({ id: "1" });
|
|
@@ -81,19 +67,26 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
81
67
|
});
|
|
82
68
|
|
|
83
69
|
it("throws RestApiValidationError when the response does not match the schema", async () => {
|
|
84
|
-
const
|
|
70
|
+
const payload = XML_RESPONSE;
|
|
71
|
+
const { client } = createClient(payload);
|
|
85
72
|
|
|
86
|
-
|
|
87
|
-
client.apiRequest(
|
|
73
|
+
try {
|
|
74
|
+
await client.apiRequest(
|
|
88
75
|
{ method: "GET", path: "/users" },
|
|
89
76
|
{ response: z.object({ id: z.string() }) },
|
|
90
|
-
)
|
|
91
|
-
|
|
77
|
+
);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
80
|
+
if (!(error instanceof RestApiValidationError)) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
expect(error.details.data).toBe(payload);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
throw new Error("Expected JSON response validation to fail");
|
|
92
87
|
});
|
|
93
88
|
});
|
|
94
89
|
|
|
95
|
-
// ── responseType passthrough ────────────────────────────────────
|
|
96
|
-
|
|
97
90
|
describe("responseType passthrough", () => {
|
|
98
91
|
it("forwards responseType text to the orchestrator request", async () => {
|
|
99
92
|
const { client, executeQuery } = createClient(XML_RESPONSE);
|
|
@@ -152,8 +145,6 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
152
145
|
});
|
|
153
146
|
|
|
154
147
|
it("returns an empty string unchanged for responseType text", async () => {
|
|
155
|
-
// An empty text body legitimately decodes to "" — the null/undefined
|
|
156
|
-
// guard must not reject it.
|
|
157
148
|
const { client } = createClient("");
|
|
158
149
|
|
|
159
150
|
const result = await client.apiRequest({
|
|
@@ -166,10 +157,6 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
166
157
|
});
|
|
167
158
|
|
|
168
159
|
it("rejects null and undefined results for every response type", async () => {
|
|
169
|
-
// Neither value is a legitimate decode result for the exposed
|
|
170
|
-
// response types: JSON parses to a value the schema sees, and an
|
|
171
|
-
// empty text body decodes to "". Either means a broken execution
|
|
172
|
-
// contract, so both throw.
|
|
173
160
|
for (const badResult of [null, undefined]) {
|
|
174
161
|
const { client: textClient } = createClient(badResult);
|
|
175
162
|
await expect(
|
|
@@ -180,8 +167,15 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
180
167
|
}),
|
|
181
168
|
).rejects.toThrow(RestApiValidationError);
|
|
182
169
|
|
|
183
|
-
|
|
184
|
-
|
|
170
|
+
const { client: binaryClient } = createClient(badResult);
|
|
171
|
+
await expect(
|
|
172
|
+
binaryClient.apiRequest({
|
|
173
|
+
method: "GET",
|
|
174
|
+
path: "/broken",
|
|
175
|
+
responseType: "binary",
|
|
176
|
+
}),
|
|
177
|
+
).rejects.toThrow(RestApiValidationError);
|
|
178
|
+
|
|
185
179
|
const { client: jsonClient } = createClient(badResult);
|
|
186
180
|
await expect(
|
|
187
181
|
jsonClient.apiRequest(
|
|
@@ -193,14 +187,109 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
193
187
|
});
|
|
194
188
|
});
|
|
195
189
|
|
|
196
|
-
|
|
190
|
+
describe("responseType binary", () => {
|
|
191
|
+
it("forwards responseType binary and returns decoded bytes", async () => {
|
|
192
|
+
const { client, executeQuery } = createClient(
|
|
193
|
+
workerBufferPayload(PDF_BYTES),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const result = await client.apiRequest({
|
|
197
|
+
method: "GET",
|
|
198
|
+
path: "/file.pdf",
|
|
199
|
+
responseType: "binary",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
203
|
+
expect.objectContaining({ responseType: "binary" }),
|
|
204
|
+
undefined,
|
|
205
|
+
undefined,
|
|
206
|
+
);
|
|
207
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
208
|
+
expect(result.constructor).toBe(Uint8Array);
|
|
209
|
+
expect(Array.from(result)).toEqual(PDF_BYTES);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("rejects malformed binary data before applying a response schema", async () => {
|
|
213
|
+
const malformed = workerBufferPayload([256]);
|
|
214
|
+
const { client, executeQuery } = createClient(malformed);
|
|
215
|
+
const workerShapeSchema = z.object({
|
|
216
|
+
type: z.literal("Buffer"),
|
|
217
|
+
data: z.array(z.number()),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await expect(
|
|
221
|
+
callApiRequest(
|
|
222
|
+
client,
|
|
223
|
+
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
224
|
+
{ response: workerShapeSchema },
|
|
225
|
+
),
|
|
226
|
+
).rejects.toThrow(RestApiValidationError);
|
|
227
|
+
expect(executeQuery).toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("accepts response schemas that transform bytes to bytes", async () => {
|
|
231
|
+
const { client } = createClient(workerBufferPayload(PDF_BYTES));
|
|
232
|
+
|
|
233
|
+
const result = await client.apiRequest(
|
|
234
|
+
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
235
|
+
{
|
|
236
|
+
response: z
|
|
237
|
+
.instanceof(Uint8Array)
|
|
238
|
+
.refine((bytes) => bytes[0] === 0x25)
|
|
239
|
+
.transform((bytes) => bytes.slice(1)),
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(Array.from(result)).toEqual(PDF_BYTES.slice(1));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("throws RestApiValidationError when the schema rejects normalized bytes", async () => {
|
|
247
|
+
const { client } = createClient(workerBufferPayload(PDF_BYTES));
|
|
248
|
+
|
|
249
|
+
await expect(
|
|
250
|
+
client.apiRequest(
|
|
251
|
+
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
252
|
+
{
|
|
253
|
+
response: z
|
|
254
|
+
.instanceof(Uint8Array)
|
|
255
|
+
.refine((bytes) => bytes.byteLength > 64),
|
|
256
|
+
},
|
|
257
|
+
),
|
|
258
|
+
).rejects.toThrow(RestApiValidationError);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("redacts binary data from response validation errors", async () => {
|
|
262
|
+
const bytes = Array.from({ length: 4096 }, () => 0);
|
|
263
|
+
const { client } = createClient(workerBufferPayload(bytes));
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await client.apiRequest(
|
|
267
|
+
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
268
|
+
{
|
|
269
|
+
response: z
|
|
270
|
+
.instanceof(Uint8Array)
|
|
271
|
+
.refine((value) => value.byteLength < 1024),
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
276
|
+
if (!(error instanceof RestApiValidationError)) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
expect(error.details.data).toEqual({
|
|
280
|
+
dataType: "binary",
|
|
281
|
+
redacted: true,
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
throw new Error("Expected binary response validation to fail");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
197
288
|
|
|
198
289
|
describe("request body validation", () => {
|
|
199
290
|
it("throws RestApiValidationError for an invalid body with a body-only schema", async () => {
|
|
200
291
|
const { client, executeQuery } = createClient({ ok: true });
|
|
201
292
|
|
|
202
|
-
// Runtime-invalid bodies reach this path via dynamic data (user
|
|
203
|
-
// input, parsed JSON) that the compiler cannot check.
|
|
204
293
|
const dynamicBody: unknown = JSON.parse('{"amount":"not-a-number"}');
|
|
205
294
|
|
|
206
295
|
await expect(
|
|
@@ -242,23 +331,12 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
242
331
|
});
|
|
243
332
|
});
|
|
244
333
|
|
|
245
|
-
// ── Runtime contract enforcement ─────────────────────────────────
|
|
246
|
-
//
|
|
247
|
-
// The overloads make these calls unrepresentable in TypeScript, but
|
|
248
|
-
// user API code executes as esbuild-bundled JS with no type
|
|
249
|
-
// enforcement — the same contract must hold at runtime.
|
|
250
|
-
|
|
251
334
|
describe("runtime contract enforcement", () => {
|
|
252
|
-
type UncheckedApiRequest = (...args: unknown[]) => Promise<unknown>;
|
|
253
|
-
|
|
254
335
|
it("rejects a schema-less call in default JSON mode without issuing the request", async () => {
|
|
255
336
|
const { client, executeQuery } = createClient({ ok: true });
|
|
256
337
|
|
|
257
338
|
await expect(
|
|
258
|
-
(client
|
|
259
|
-
method: "GET",
|
|
260
|
-
path: "/users",
|
|
261
|
-
}),
|
|
339
|
+
callApiRequest(client, { method: "GET", path: "/users" }),
|
|
262
340
|
).rejects.toThrow(RestApiValidationError);
|
|
263
341
|
expect(executeQuery).not.toHaveBeenCalled();
|
|
264
342
|
});
|
|
@@ -267,7 +345,8 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
267
345
|
const { client, executeQuery } = createClient({ ok: true });
|
|
268
346
|
|
|
269
347
|
await expect(
|
|
270
|
-
(
|
|
348
|
+
callApiRequest(
|
|
349
|
+
client,
|
|
271
350
|
{
|
|
272
351
|
method: "POST",
|
|
273
352
|
path: "/orders",
|
|
@@ -280,27 +359,32 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
280
359
|
expect(executeQuery).not.toHaveBeenCalled();
|
|
281
360
|
});
|
|
282
361
|
|
|
283
|
-
it("rejects
|
|
284
|
-
const
|
|
362
|
+
it("rejects auto, which can yield unvalidated JSON, and raw, which is streaming-only", async () => {
|
|
363
|
+
for (const responseType of ["auto", "raw"]) {
|
|
364
|
+
const { client, executeQuery } = createClient({ ok: true });
|
|
285
365
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
366
|
+
await expect(
|
|
367
|
+
callApiRequest(client, {
|
|
368
|
+
method: "GET",
|
|
369
|
+
path: "/report",
|
|
370
|
+
responseType,
|
|
371
|
+
}),
|
|
372
|
+
).rejects.toThrow(RestApiValidationError);
|
|
373
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
374
|
+
}
|
|
294
375
|
});
|
|
295
376
|
});
|
|
296
377
|
|
|
297
|
-
// ── Compile-time overload contracts (verified by `pnpm typecheck`) ──
|
|
298
|
-
|
|
299
378
|
describe("overload resolution", () => {
|
|
379
|
+
it("keeps public response types within the worker contract", () => {
|
|
380
|
+
expectTypeOf<(typeof REST_API_RESPONSE_TYPES)[number]>().toMatchTypeOf<
|
|
381
|
+
Exclude<`${ActionResponseType}`, "raw">
|
|
382
|
+
>();
|
|
383
|
+
});
|
|
384
|
+
|
|
300
385
|
it("keeps typed results for schema callers and unknown for schema-less calls", async () => {
|
|
301
386
|
const { client } = createClient({ id: "1" });
|
|
302
387
|
|
|
303
|
-
// With a response schema: resolves to the typed overload.
|
|
304
388
|
const typed: Promise<{ id: string }> = client.apiRequest(
|
|
305
389
|
{ method: "GET", path: "/users" },
|
|
306
390
|
{ response: z.object({ id: z.string() }) },
|
|
@@ -309,7 +393,6 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
309
393
|
|
|
310
394
|
const { client: rawClient } = createClient(XML_RESPONSE);
|
|
311
395
|
|
|
312
|
-
// Without a schema: resolves to the raw overload returning unknown.
|
|
313
396
|
const raw: Promise<unknown> = rawClient.apiRequest({
|
|
314
397
|
method: "GET",
|
|
315
398
|
path: "/report.xml",
|
|
@@ -325,20 +408,58 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
325
408
|
});
|
|
326
409
|
await wrong;
|
|
327
410
|
|
|
328
|
-
//
|
|
329
|
-
// the overloads (@ts-expect-error) and at runtime by the contract
|
|
330
|
-
// guards, since bundled user code carries no types.
|
|
331
|
-
|
|
332
|
-
// @ts-expect-error unvalidated JSON is unrepresentable — schema-less
|
|
333
|
-
// calls must opt into a non-JSON responseType
|
|
411
|
+
// @ts-expect-error unvalidated JSON is unrepresentable; schema-less calls must opt into a non-JSON responseType
|
|
334
412
|
const unvalidatedJson = rawClient.apiRequest({
|
|
335
413
|
method: "GET",
|
|
336
414
|
path: "/users",
|
|
337
415
|
});
|
|
338
416
|
await expect(unvalidatedJson).rejects.toThrow(RestApiValidationError);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("types schema-less binary responses as Uint8Array", async () => {
|
|
420
|
+
const { client: binaryClient } = createClient(
|
|
421
|
+
workerBufferPayload(PDF_BYTES),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const binary: Promise<Uint8Array> = binaryClient.apiRequest({
|
|
425
|
+
method: "GET",
|
|
426
|
+
path: "/file.pdf",
|
|
427
|
+
responseType: "binary",
|
|
428
|
+
});
|
|
429
|
+
expectTypeOf(binary).toEqualTypeOf<Promise<Uint8Array>>();
|
|
430
|
+
expect(Array.from(await binary)).toEqual(PDF_BYTES);
|
|
431
|
+
|
|
432
|
+
// @ts-expect-error schema-less binary apiRequest returns Uint8Array
|
|
433
|
+
const wrongBinary: Promise<string> = binaryClient.apiRequest({
|
|
434
|
+
method: "GET",
|
|
435
|
+
path: "/file.pdf",
|
|
436
|
+
responseType: "binary",
|
|
437
|
+
});
|
|
438
|
+
await wrongBinary;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("rejects binary response schemas that transform bytes to another type", () => {
|
|
442
|
+
const { client: binaryClient } = createClient(
|
|
443
|
+
workerBufferPayload(PDF_BYTES),
|
|
444
|
+
);
|
|
339
445
|
|
|
340
|
-
//
|
|
341
|
-
|
|
446
|
+
// @ts-expect-error binary response schemas must output Uint8Array
|
|
447
|
+
binaryClient.apiRequest(
|
|
448
|
+
{
|
|
449
|
+
method: "GET",
|
|
450
|
+
path: "/file.pdf",
|
|
451
|
+
responseType: "binary",
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
response: z
|
|
455
|
+
.instanceof(Uint8Array)
|
|
456
|
+
.transform((bytes) => bytes.byteLength),
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("rejects response types outside the public SDK contract", async () => {
|
|
462
|
+
const { client: rawClient } = createClient(XML_RESPONSE);
|
|
342
463
|
const autoCall = rawClient.apiRequest({
|
|
343
464
|
method: "GET",
|
|
344
465
|
path: "/report.xml",
|
|
@@ -347,13 +468,13 @@ describe("RestApiIntegrationPluginClientImpl", () => {
|
|
|
347
468
|
});
|
|
348
469
|
await expect(autoCall).rejects.toThrow(RestApiValidationError);
|
|
349
470
|
|
|
350
|
-
const
|
|
471
|
+
const rawCall = rawClient.apiRequest({
|
|
351
472
|
method: "GET",
|
|
352
473
|
path: "/report.xml",
|
|
353
|
-
// @ts-expect-error "
|
|
354
|
-
responseType: "
|
|
474
|
+
// @ts-expect-error "raw" is not an exposed responseType
|
|
475
|
+
responseType: "raw",
|
|
355
476
|
});
|
|
356
|
-
await expect(
|
|
477
|
+
await expect(rawCall).rejects.toThrow(RestApiValidationError);
|
|
357
478
|
});
|
|
358
479
|
});
|
|
359
480
|
});
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
"overlays": [
|
|
5
5
|
{
|
|
6
6
|
"file": "overlays/response-types.md",
|
|
7
|
-
"sdkVersionRange": ">=0.0.3"
|
|
7
|
+
"sdkVersionRange": ">=0.0.3 <0.0.4"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"file": "overlays/response-types-binary.md",
|
|
11
|
+
"sdkVersionRange": ">=0.0.4"
|
|
8
12
|
},
|
|
9
13
|
{
|
|
10
14
|
"file": "overlays/response-types-unsupported.md",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
## Non-JSON Responses
|
|
2
|
+
|
|
3
|
+
`apiRequest()` decodes responses as JSON by default. For endpoints that return non-JSON payloads, pass `responseType`:
|
|
4
|
+
|
|
5
|
+
- `"binary"` for PDFs and other byte payloads. The result is a `Uint8Array`
|
|
6
|
+
- `"text"` for XML, CSV, HTML, and other decoded strings
|
|
7
|
+
|
|
8
|
+
With `responseType: "text"` or `responseType: "binary"` the response schema may be omitted. Text returns the raw string typed `unknown`. Binary returns a `Uint8Array`:
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
const xml = await ctx.integrations.legacyApi.apiRequest({
|
|
12
|
+
method: "GET",
|
|
13
|
+
path: "/report.xml",
|
|
14
|
+
responseType: "text",
|
|
15
|
+
});
|
|
16
|
+
// xml is the raw XML string (typed unknown)
|
|
17
|
+
|
|
18
|
+
const pdf = await ctx.integrations.legacyApi.apiRequest({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/file.pdf",
|
|
21
|
+
responseType: "binary",
|
|
22
|
+
});
|
|
23
|
+
// pdf is a Uint8Array
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To validate after decoding, pass a schema that matches the decoded value:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const xml = await ctx.integrations.legacyApi.apiRequest(
|
|
30
|
+
{ method: "GET", path: "/report.xml", responseType: "text" },
|
|
31
|
+
{ response: z.string() },
|
|
32
|
+
);
|
|
33
|
+
// xml is typed string
|
|
34
|
+
|
|
35
|
+
const pdf = await ctx.integrations.legacyApi.apiRequest(
|
|
36
|
+
{ method: "GET", path: "/file.pdf", responseType: "binary" },
|
|
37
|
+
{ response: z.instanceof(Uint8Array) },
|
|
38
|
+
);
|
|
39
|
+
// pdf is typed Uint8Array
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Convert binary data to a JSON-safe representation before returning it from an SDK API:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
return {
|
|
46
|
+
contentBase64: Buffer.from(pdf).toString("base64"),
|
|
47
|
+
filename: "file.pdf",
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
JSON responses (the default) always require a response schema. Omitting the schema is only allowed together with an explicit `responseType: "text"` or `responseType: "binary"`, so unvalidated JSON is not representable.
|
|
@@ -12,6 +12,7 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
14
|
import { RestApiValidationError } from "../../errors.js";
|
|
15
|
+
import { REDACTED_BINARY_RESPONSE_DATA } from "../base/decode-worker-binary-response.js";
|
|
15
16
|
import type { IntegrationConfig } from "../types.js";
|
|
16
17
|
import { SlackClientImpl } from "./client.js";
|
|
17
18
|
import type { SlackErrorResponse } from "./types.js";
|
|
@@ -508,7 +509,41 @@ describe("SlackClientImpl", () => {
|
|
|
508
509
|
});
|
|
509
510
|
});
|
|
510
511
|
|
|
511
|
-
|
|
512
|
+
describe("responseType binary", () => {
|
|
513
|
+
const pdfBytes = [0x25, 0x50, 0x44, 0x46];
|
|
514
|
+
const workerPayload = { type: "Buffer", data: pdfBytes };
|
|
515
|
+
|
|
516
|
+
it("decodes worker Buffer JSON and redacts it from validation errors", async () => {
|
|
517
|
+
const { client, executeQuery } = createClient(workerPayload);
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
await client.apiRequest(
|
|
521
|
+
{
|
|
522
|
+
method: "GET",
|
|
523
|
+
path: "/files.download",
|
|
524
|
+
responseType: "binary",
|
|
525
|
+
},
|
|
526
|
+
{ response: ChannelsSchema },
|
|
527
|
+
);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
530
|
+
if (!(error instanceof RestApiValidationError)) {
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
534
|
+
expect.objectContaining({ responseType: "binary" }),
|
|
535
|
+
undefined,
|
|
536
|
+
undefined,
|
|
537
|
+
);
|
|
538
|
+
expect(error.details.data).toEqual(REDACTED_BINARY_RESPONSE_DATA);
|
|
539
|
+
expect(error.details.data).not.toEqual(workerPayload);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Expected Slack binary apiRequest to fail envelope validation",
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
512
547
|
|
|
513
548
|
describe("type narrowing", () => {
|
|
514
549
|
it("narrows to error response fields when ok is false", async () => {
|
|
@@ -108,6 +108,14 @@ All methods accept an optional `metadata` parameter as the last argument for dia
|
|
|
108
108
|
|
|
109
109
|
## Common Pitfalls
|
|
110
110
|
|
|
111
|
+
### Streaming Is Not Supported
|
|
112
|
+
|
|
113
|
+
`apiRequest()` does not support streaming or Server-Sent Events. Always set
|
|
114
|
+
`stream: false` in Cortex request bodies (as the examples do) — a streaming
|
|
115
|
+
response fails schema validation. Every call returns the complete response;
|
|
116
|
+
if a UI needs real-time token streaming, handle it at the frontend layer,
|
|
117
|
+
not through the SDK.
|
|
118
|
+
|
|
111
119
|
### No Specialized Methods
|
|
112
120
|
|
|
113
121
|
The Snowflake Cortex client only provides `apiRequest()`. There are no other specialized methods:
|
|
@@ -234,6 +234,13 @@ All methods accept an optional `metadata` parameter as the last argument for dia
|
|
|
234
234
|
|
|
235
235
|
## Common Pitfalls
|
|
236
236
|
|
|
237
|
+
### Streaming Is Not Supported
|
|
238
|
+
|
|
239
|
+
`apiRequest()` does not support streaming or Server-Sent Events. Do not set
|
|
240
|
+
`stream: true` — streaming responses fail schema validation. Every call
|
|
241
|
+
returns the complete response; if a UI needs real-time token streaming,
|
|
242
|
+
handle it at the frontend layer, not through the SDK.
|
|
243
|
+
|
|
237
244
|
### No Specialized Methods
|
|
238
245
|
|
|
239
246
|
```typescript
|