@superblocksteam/sdk-api 2.0.142-next.0 → 2.0.142-next.2
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/integrations/base/index.d.ts +1 -1
- package/dist/integrations/base/index.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.d.ts +1 -1
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +23 -3
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts +9 -3
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +31 -3
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +69 -3
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/base/types.js +23 -1
- package/dist/integrations/base/types.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +24 -0
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/restapiintegration/client.test.d.ts +12 -0
- package/dist/integrations/restapiintegration/client.test.d.ts.map +1 -0
- package/dist/integrations/restapiintegration/client.test.js +236 -0
- package/dist/integrations/restapiintegration/client.test.js.map +1 -0
- package/package.json +3 -2
- package/src/integrations/base/index.ts +1 -0
- package/src/integrations/base/rest-api-client-base.ts +27 -3
- package/src/integrations/base/rest-api-integration-client.ts +54 -5
- package/src/integrations/base/types.ts +77 -3
- package/src/integrations/documentation-resolver.test.ts +31 -0
- package/src/integrations/restapiintegration/client.test.ts +359 -0
- package/src/integrations/restapiintegration/docs.manifest.json +10 -1
- package/src/integrations/restapiintegration/overlays/response-types-unsupported.md +5 -0
- package/src/integrations/restapiintegration/overlays/response-types.md +26 -0
|
@@ -0,0 +1,359 @@
|
|
|
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";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
import { ActionResponseType } from "@superblocksteam/shared";
|
|
16
|
+
|
|
17
|
+
import { RestApiValidationError } from "../../errors.js";
|
|
18
|
+
import type { RestApiResponseType } from "../base/types.js";
|
|
19
|
+
import type { IntegrationConfig } from "../types.js";
|
|
20
|
+
import { RestApiIntegrationPluginClientImpl } from "./client.js";
|
|
21
|
+
|
|
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
|
+
const TEST_CONFIG: IntegrationConfig = {
|
|
36
|
+
id: "restapi-test-id",
|
|
37
|
+
name: "Test REST API",
|
|
38
|
+
pluginId: "restapiintegration",
|
|
39
|
+
configuration: {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const XML_RESPONSE = `<?xml version="1.0"?><note><body>hi</body></note>`;
|
|
43
|
+
|
|
44
|
+
function createClient(mockResult: unknown) {
|
|
45
|
+
const executeQuery = vi.fn().mockResolvedValue(mockResult);
|
|
46
|
+
const client = new RestApiIntegrationPluginClientImpl(
|
|
47
|
+
TEST_CONFIG,
|
|
48
|
+
executeQuery,
|
|
49
|
+
);
|
|
50
|
+
return { client, executeQuery };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("RestApiIntegrationPluginClientImpl", () => {
|
|
54
|
+
// ── Default JSON behavior (must not regress) ────────────────────
|
|
55
|
+
|
|
56
|
+
describe("default JSON behavior", () => {
|
|
57
|
+
it("sends responseType json when the option is omitted", async () => {
|
|
58
|
+
const { client, executeQuery } = createClient({ id: "1" });
|
|
59
|
+
|
|
60
|
+
await client.apiRequest(
|
|
61
|
+
{ method: "GET", path: "/users" },
|
|
62
|
+
{ response: z.object({ id: z.string() }) },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
66
|
+
expect.objectContaining({ responseType: "json" }),
|
|
67
|
+
undefined,
|
|
68
|
+
undefined,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("validates the response against the provided schema", async () => {
|
|
73
|
+
const { client } = createClient({ id: "1", extra: "stripped" });
|
|
74
|
+
|
|
75
|
+
const result = await client.apiRequest(
|
|
76
|
+
{ method: "GET", path: "/users" },
|
|
77
|
+
{ response: z.object({ id: z.string() }) },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result).toEqual({ id: "1" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws RestApiValidationError when the response does not match the schema", async () => {
|
|
84
|
+
const { client } = createClient(XML_RESPONSE);
|
|
85
|
+
|
|
86
|
+
await expect(
|
|
87
|
+
client.apiRequest(
|
|
88
|
+
{ method: "GET", path: "/users" },
|
|
89
|
+
{ response: z.object({ id: z.string() }) },
|
|
90
|
+
),
|
|
91
|
+
).rejects.toThrow(RestApiValidationError);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── responseType passthrough ────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("responseType passthrough", () => {
|
|
98
|
+
it("forwards responseType text to the orchestrator request", async () => {
|
|
99
|
+
const { client, executeQuery } = createClient(XML_RESPONSE);
|
|
100
|
+
|
|
101
|
+
await client.apiRequest({
|
|
102
|
+
method: "GET",
|
|
103
|
+
path: "/report.xml",
|
|
104
|
+
responseType: "text",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
108
|
+
expect.objectContaining({ responseType: "text" }),
|
|
109
|
+
undefined,
|
|
110
|
+
undefined,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns a raw XML string with responseType text and no response schema", async () => {
|
|
115
|
+
const { client } = createClient(XML_RESPONSE);
|
|
116
|
+
|
|
117
|
+
const result = await client.apiRequest({
|
|
118
|
+
method: "GET",
|
|
119
|
+
path: "/report.xml",
|
|
120
|
+
responseType: "text",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result).toBe(XML_RESPONSE);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("still validates when a response schema is provided alongside responseType", async () => {
|
|
127
|
+
const { client, executeQuery } = createClient(XML_RESPONSE);
|
|
128
|
+
|
|
129
|
+
const result = await client.apiRequest(
|
|
130
|
+
{ method: "GET", path: "/report.xml", responseType: "text" },
|
|
131
|
+
{ response: z.string() },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
135
|
+
expect.objectContaining({ responseType: "text" }),
|
|
136
|
+
undefined,
|
|
137
|
+
undefined,
|
|
138
|
+
);
|
|
139
|
+
expect(result).toBe(XML_RESPONSE);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns the raw response without validation when no schema is given", async () => {
|
|
143
|
+
const { client } = createClient({ ok: true, rows: [1, 2, 3] });
|
|
144
|
+
|
|
145
|
+
const result = await client.apiRequest({
|
|
146
|
+
method: "GET",
|
|
147
|
+
path: "/data",
|
|
148
|
+
responseType: "text",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result).toEqual({ ok: true, rows: [1, 2, 3] });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
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
|
+
const { client } = createClient("");
|
|
158
|
+
|
|
159
|
+
const result = await client.apiRequest({
|
|
160
|
+
method: "GET",
|
|
161
|
+
path: "/empty",
|
|
162
|
+
responseType: "text",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(result).toBe("");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
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
|
+
for (const badResult of [null, undefined]) {
|
|
174
|
+
const { client: textClient } = createClient(badResult);
|
|
175
|
+
await expect(
|
|
176
|
+
textClient.apiRequest({
|
|
177
|
+
method: "GET",
|
|
178
|
+
path: "/broken",
|
|
179
|
+
responseType: "text",
|
|
180
|
+
}),
|
|
181
|
+
).rejects.toThrow(RestApiValidationError);
|
|
182
|
+
|
|
183
|
+
// JSON mode always goes through the schema overload; the bad
|
|
184
|
+
// result is rejected before validation runs.
|
|
185
|
+
const { client: jsonClient } = createClient(badResult);
|
|
186
|
+
await expect(
|
|
187
|
+
jsonClient.apiRequest(
|
|
188
|
+
{ method: "GET", path: "/broken" },
|
|
189
|
+
{ response: z.object({}) },
|
|
190
|
+
),
|
|
191
|
+
).rejects.toThrow(RestApiValidationError);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── Request-body validation via the schema-less overload ────────
|
|
197
|
+
|
|
198
|
+
describe("request body validation", () => {
|
|
199
|
+
it("throws RestApiValidationError for an invalid body with a body-only schema", async () => {
|
|
200
|
+
const { client, executeQuery } = createClient({ ok: true });
|
|
201
|
+
|
|
202
|
+
// Runtime-invalid bodies reach this path via dynamic data (user
|
|
203
|
+
// input, parsed JSON) that the compiler cannot check.
|
|
204
|
+
const dynamicBody: unknown = JSON.parse('{"amount":"not-a-number"}');
|
|
205
|
+
|
|
206
|
+
await expect(
|
|
207
|
+
client.apiRequest(
|
|
208
|
+
{
|
|
209
|
+
method: "POST",
|
|
210
|
+
path: "/orders",
|
|
211
|
+
body: dynamicBody,
|
|
212
|
+
responseType: "text",
|
|
213
|
+
},
|
|
214
|
+
{ body: z.object({ amount: z.number() }) },
|
|
215
|
+
),
|
|
216
|
+
).rejects.toThrow(RestApiValidationError);
|
|
217
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("sends a valid body and returns the raw response with a body-only schema", async () => {
|
|
221
|
+
const { client, executeQuery } = createClient({ ok: true });
|
|
222
|
+
|
|
223
|
+
const result = await client.apiRequest(
|
|
224
|
+
{
|
|
225
|
+
method: "POST",
|
|
226
|
+
path: "/orders",
|
|
227
|
+
body: { amount: 42 },
|
|
228
|
+
responseType: "text",
|
|
229
|
+
},
|
|
230
|
+
{ body: z.object({ amount: z.number() }) },
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(executeQuery).toHaveBeenCalledWith(
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
body: JSON.stringify({ amount: 42 }),
|
|
236
|
+
bodyType: "jsonBody",
|
|
237
|
+
}),
|
|
238
|
+
undefined,
|
|
239
|
+
undefined,
|
|
240
|
+
);
|
|
241
|
+
expect(result).toEqual({ ok: true });
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
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
|
+
describe("runtime contract enforcement", () => {
|
|
252
|
+
type UncheckedApiRequest = (...args: unknown[]) => Promise<unknown>;
|
|
253
|
+
|
|
254
|
+
it("rejects a schema-less call in default JSON mode without issuing the request", async () => {
|
|
255
|
+
const { client, executeQuery } = createClient({ ok: true });
|
|
256
|
+
|
|
257
|
+
await expect(
|
|
258
|
+
(client.apiRequest as UncheckedApiRequest)({
|
|
259
|
+
method: "GET",
|
|
260
|
+
path: "/users",
|
|
261
|
+
}),
|
|
262
|
+
).rejects.toThrow(RestApiValidationError);
|
|
263
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("rejects a body-only schema in explicit JSON mode without issuing the request", async () => {
|
|
267
|
+
const { client, executeQuery } = createClient({ ok: true });
|
|
268
|
+
|
|
269
|
+
await expect(
|
|
270
|
+
(client.apiRequest as UncheckedApiRequest)(
|
|
271
|
+
{
|
|
272
|
+
method: "POST",
|
|
273
|
+
path: "/orders",
|
|
274
|
+
body: { amount: 42 },
|
|
275
|
+
responseType: "json",
|
|
276
|
+
},
|
|
277
|
+
{ body: z.object({ amount: z.number() }) },
|
|
278
|
+
),
|
|
279
|
+
).rejects.toThrow(RestApiValidationError);
|
|
280
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("rejects an unexposed responseType value without issuing the request", async () => {
|
|
284
|
+
const { client, executeQuery } = createClient({ ok: true });
|
|
285
|
+
|
|
286
|
+
await expect(
|
|
287
|
+
(client.apiRequest as UncheckedApiRequest)({
|
|
288
|
+
method: "GET",
|
|
289
|
+
path: "/report",
|
|
290
|
+
responseType: "auto",
|
|
291
|
+
}),
|
|
292
|
+
).rejects.toThrow(RestApiValidationError);
|
|
293
|
+
expect(executeQuery).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ── Compile-time overload contracts (verified by `pnpm typecheck`) ──
|
|
298
|
+
|
|
299
|
+
describe("overload resolution", () => {
|
|
300
|
+
it("keeps typed results for schema callers and unknown for schema-less calls", async () => {
|
|
301
|
+
const { client } = createClient({ id: "1" });
|
|
302
|
+
|
|
303
|
+
// With a response schema: resolves to the typed overload.
|
|
304
|
+
const typed: Promise<{ id: string }> = client.apiRequest(
|
|
305
|
+
{ method: "GET", path: "/users" },
|
|
306
|
+
{ response: z.object({ id: z.string() }) },
|
|
307
|
+
);
|
|
308
|
+
expect(await typed).toEqual({ id: "1" });
|
|
309
|
+
|
|
310
|
+
const { client: rawClient } = createClient(XML_RESPONSE);
|
|
311
|
+
|
|
312
|
+
// Without a schema: resolves to the raw overload returning unknown.
|
|
313
|
+
const raw: Promise<unknown> = rawClient.apiRequest({
|
|
314
|
+
method: "GET",
|
|
315
|
+
path: "/report.xml",
|
|
316
|
+
responseType: "text",
|
|
317
|
+
});
|
|
318
|
+
expect(await raw).toBe(XML_RESPONSE);
|
|
319
|
+
|
|
320
|
+
// @ts-expect-error schema-less apiRequest returns unknown, not a typed shape
|
|
321
|
+
const wrong: Promise<{ id: string }> = rawClient.apiRequest({
|
|
322
|
+
method: "GET",
|
|
323
|
+
path: "/report.xml",
|
|
324
|
+
responseType: "text",
|
|
325
|
+
});
|
|
326
|
+
await wrong;
|
|
327
|
+
|
|
328
|
+
// The remaining cases are rejected twice over: at compile time by
|
|
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
|
|
334
|
+
const unvalidatedJson = rawClient.apiRequest({
|
|
335
|
+
method: "GET",
|
|
336
|
+
path: "/users",
|
|
337
|
+
});
|
|
338
|
+
await expect(unvalidatedJson).rejects.toThrow(RestApiValidationError);
|
|
339
|
+
|
|
340
|
+
// "auto" and "binary" are valid wire values but deliberately not
|
|
341
|
+
// exposed in the SDK union (see RestApiResponseType).
|
|
342
|
+
const autoCall = rawClient.apiRequest({
|
|
343
|
+
method: "GET",
|
|
344
|
+
path: "/report.xml",
|
|
345
|
+
// @ts-expect-error "auto" is not an exposed responseType
|
|
346
|
+
responseType: "auto",
|
|
347
|
+
});
|
|
348
|
+
await expect(autoCall).rejects.toThrow(RestApiValidationError);
|
|
349
|
+
|
|
350
|
+
const binaryCall = rawClient.apiRequest({
|
|
351
|
+
method: "GET",
|
|
352
|
+
path: "/report.xml",
|
|
353
|
+
// @ts-expect-error "binary" is not an exposed responseType
|
|
354
|
+
responseType: "binary",
|
|
355
|
+
});
|
|
356
|
+
await expect(binaryCall).rejects.toThrow(RestApiValidationError);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginId": "restapiintegration",
|
|
3
3
|
"base": "README.md",
|
|
4
|
-
"overlays": [
|
|
4
|
+
"overlays": [
|
|
5
|
+
{
|
|
6
|
+
"file": "overlays/response-types.md",
|
|
7
|
+
"sdkVersionRange": ">=0.0.3"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"file": "overlays/response-types-unsupported.md",
|
|
11
|
+
"sdkVersionRange": "<0.0.3"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
5
14
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
## Non-JSON Responses
|
|
2
|
+
|
|
3
|
+
This organization's agent does not support the `responseType` option on `apiRequest()` — its worker runs an sdk-api version that decodes every response as JSON and always requires a response schema. Endpoints returning XML, CSV, or other non-JSON payloads cannot be consumed from SDK APIs until the agent is upgraded.
|
|
4
|
+
|
|
5
|
+
Do not generate code that passes a `responseType` value: it may typecheck against a newer local copy of the SDK but fails at runtime on this agent with a `RestApiValidationError` (for example, "Integration query returned ... — expected a JSON response object"). If the user needs to consume a non-JSON endpoint, tell them their Superblocks agent must be upgraded first.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Non-JSON Responses
|
|
2
|
+
|
|
3
|
+
`apiRequest()` decodes responses as JSON by default. For endpoints that return non-JSON payloads (XML, CSV, HTML, plain text), pass `responseType: "text"` to receive the decoded body as a string.
|
|
4
|
+
|
|
5
|
+
With `responseType: "text"` the response schema may be omitted — the raw string is returned typed `unknown`:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const xml = await ctx.integrations.legacyApi.apiRequest({
|
|
9
|
+
method: "GET",
|
|
10
|
+
path: "/report.xml",
|
|
11
|
+
responseType: "text",
|
|
12
|
+
});
|
|
13
|
+
// xml is the raw XML string (typed unknown)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
To get a typed string instead, validate with a schema matching the decoded value:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
const xml = await ctx.integrations.legacyApi.apiRequest(
|
|
20
|
+
{ method: "GET", path: "/report.xml", responseType: "text" },
|
|
21
|
+
{ response: z.string() },
|
|
22
|
+
);
|
|
23
|
+
// xml is typed string
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
JSON responses (the default) always require a response schema. Omitting the schema is only allowed together with an explicit `responseType: "text"`, so unvalidated JSON is not representable.
|