@superblocksteam/sdk-api 0.0.7 → 0.0.8

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 (43) hide show
  1. package/dist/integrations/base/index.d.ts +1 -1
  2. package/dist/integrations/base/index.d.ts.map +1 -1
  3. package/dist/integrations/base/rest-api-client-base.d.ts +1 -1
  4. package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
  5. package/dist/integrations/base/rest-api-client-base.js +23 -3
  6. package/dist/integrations/base/rest-api-client-base.js.map +1 -1
  7. package/dist/integrations/base/rest-api-integration-client.d.ts +9 -3
  8. package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
  9. package/dist/integrations/base/rest-api-integration-client.js +31 -3
  10. package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
  11. package/dist/integrations/base/types.d.ts +69 -3
  12. package/dist/integrations/base/types.d.ts.map +1 -1
  13. package/dist/integrations/base/types.js +23 -1
  14. package/dist/integrations/base/types.js.map +1 -1
  15. package/dist/integrations/documentation-resolver.test.js +167 -1
  16. package/dist/integrations/documentation-resolver.test.js.map +1 -1
  17. package/dist/integrations/documentation.d.ts +1 -0
  18. package/dist/integrations/documentation.d.ts.map +1 -1
  19. package/dist/integrations/documentation.js +31 -8
  20. package/dist/integrations/documentation.js.map +1 -1
  21. package/dist/integrations/postgres/client.d.ts +0 -7
  22. package/dist/integrations/postgres/client.d.ts.map +1 -1
  23. package/dist/integrations/restapiintegration/client.test.d.ts +12 -0
  24. package/dist/integrations/restapiintegration/client.test.d.ts.map +1 -0
  25. package/dist/integrations/restapiintegration/client.test.js +236 -0
  26. package/dist/integrations/restapiintegration/client.test.js.map +1 -0
  27. package/dist/integrations/slack/types.d.ts +2 -2
  28. package/dist/integrations/slack/types.d.ts.map +1 -1
  29. package/package.json +6 -6
  30. package/src/integrations/base/index.ts +1 -0
  31. package/src/integrations/base/rest-api-client-base.ts +27 -3
  32. package/src/integrations/base/rest-api-integration-client.ts +54 -5
  33. package/src/integrations/base/types.ts +77 -3
  34. package/src/integrations/documentation-resolver.test.ts +190 -1
  35. package/src/integrations/documentation.ts +63 -11
  36. package/src/integrations/graphql/docs.manifest.json +6 -1
  37. package/src/integrations/graphql/overlays/dynamic-headers.md +34 -0
  38. package/src/integrations/postgres/client.ts +1 -1
  39. package/src/integrations/restapiintegration/client.test.ts +359 -0
  40. package/src/integrations/restapiintegration/docs.manifest.json +10 -1
  41. package/src/integrations/restapiintegration/overlays/response-types-unsupported.md +5 -0
  42. package/src/integrations/restapiintegration/overlays/response-types.md +26 -0
  43. package/src/integrations/slack/types.ts +2 -2
@@ -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.
@@ -56,7 +56,7 @@ type SlackSuccessSchemaHasExplicitOkKey<TSchema extends z.AnyZodObject> =
56
56
  : false
57
57
  : false;
58
58
 
59
- export type SlackSuccessSchema<TSchema extends z.AnyZodObject> =
59
+ type SlackSuccessSchema<TSchema extends z.AnyZodObject> =
60
60
  SlackSuccessSchemaHasExplicitOkKey<TSchema> extends true ? never : TSchema;
61
61
 
62
62
  /**
@@ -73,7 +73,7 @@ export interface SlackApiRequestSchema<
73
73
  }
74
74
 
75
75
  /** Slack success response shape with injected discriminant. */
76
- export type SlackSuccessResponse<T extends Record<string, unknown>> = T & {
76
+ type SlackSuccessResponse<T extends Record<string, unknown>> = T & {
77
77
  readonly ok: true;
78
78
  };
79
79