@superblocksteam/sdk-api 0.0.7 → 0.0.9
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/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 +0 -27
- package/dist/integrations/base/rest-api-client-base.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-client-base.js +34 -37
- package/dist/integrations/base/rest-api-client-base.js.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.d.ts +11 -17
- package/dist/integrations/base/rest-api-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/rest-api-integration-client.js +18 -17
- package/dist/integrations/base/rest-api-integration-client.js.map +1 -1
- package/dist/integrations/base/types.d.ts +72 -13
- package/dist/integrations/base/types.d.ts.map +1 -1
- package/dist/integrations/base/types.js +1 -4
- package/dist/integrations/base/types.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +173 -1
- package/dist/integrations/documentation-resolver.test.js.map +1 -1
- package/dist/integrations/documentation.d.ts +1 -0
- package/dist/integrations/documentation.d.ts.map +1 -1
- package/dist/integrations/documentation.js +31 -8
- package/dist/integrations/documentation.js.map +1 -1
- package/dist/integrations/postgres/client.d.ts +0 -7
- package/dist/integrations/postgres/client.d.ts.map +1 -1
- package/dist/integrations/restapiintegration/client.test.d.ts +2 -0
- package/dist/integrations/restapiintegration/client.test.d.ts.map +1 -0
- package/dist/integrations/restapiintegration/client.test.js +313 -0
- package/dist/integrations/restapiintegration/client.test.js.map +1 -0
- package/dist/integrations/slack/client.test.js +26 -1
- package/dist/integrations/slack/client.test.js.map +1 -1
- package/dist/integrations/slack/types.d.ts +2 -2
- package/dist/integrations/slack/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/errors.ts +33 -5
- 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/index.ts +1 -0
- package/src/integrations/base/rest-api-client-base.ts +42 -46
- package/src/integrations/base/rest-api-integration-client.ts +51 -21
- package/src/integrations/base/types.ts +85 -15
- package/src/integrations/documentation-resolver.test.ts +197 -1
- package/src/integrations/documentation.ts +63 -11
- package/src/integrations/graphql/docs.manifest.json +6 -1
- package/src/integrations/graphql/overlays/dynamic-headers.md +34 -0
- package/src/integrations/postgres/client.ts +1 -1
- package/src/integrations/restapiintegration/client.test.ts +480 -0
- package/src/integrations/restapiintegration/docs.manifest.json +14 -1
- package/src/integrations/restapiintegration/overlays/response-types-binary.md +51 -0
- package/src/integrations/restapiintegration/overlays/response-types-unsupported.md +7 -0
- package/src/integrations/restapiintegration/overlays/response-types.md +26 -0
- package/src/integrations/slack/client.test.ts +36 -1
- package/src/integrations/slack/types.ts +2 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { RestApiValidationError } from "../../errors.js";
|
|
4
|
+
import { decodeWorkerBinaryResponse } from "./decode-worker-binary-response.js";
|
|
5
|
+
|
|
6
|
+
describe("decodeWorkerBinaryResponse", () => {
|
|
7
|
+
it("copies a Uint8Array into a plain Uint8Array with the same bytes", () => {
|
|
8
|
+
const source = new Uint8Array([1, 2, 255]);
|
|
9
|
+
|
|
10
|
+
const result = decodeWorkerBinaryResponse(source);
|
|
11
|
+
|
|
12
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
13
|
+
expect(result.constructor).toBe(Uint8Array);
|
|
14
|
+
expect(Array.from(result)).toEqual([1, 2, 255]);
|
|
15
|
+
source[0] = 9;
|
|
16
|
+
expect(result[0]).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("converts worker Buffer JSON into a plain Uint8Array", () => {
|
|
20
|
+
const result = decodeWorkerBinaryResponse({
|
|
21
|
+
type: "Buffer",
|
|
22
|
+
data: [37, 80, 68, 70],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(result.constructor).toBe(Uint8Array);
|
|
26
|
+
expect(Array.from(result)).toEqual([37, 80, 68, 70]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("converts empty Buffer JSON into a zero-length Uint8Array", () => {
|
|
30
|
+
const result = decodeWorkerBinaryResponse({ type: "Buffer", data: [] });
|
|
31
|
+
|
|
32
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
33
|
+
expect(result.byteLength).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects a missing discriminant", () => {
|
|
37
|
+
expect(() => decodeWorkerBinaryResponse({ data: [1] })).toThrow(
|
|
38
|
+
RestApiValidationError,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects a non-Buffer discriminant", () => {
|
|
43
|
+
expect(() =>
|
|
44
|
+
decodeWorkerBinaryResponse({ type: "Array", data: [1] }),
|
|
45
|
+
).toThrow(RestApiValidationError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects a non-array data field", () => {
|
|
49
|
+
expect(() =>
|
|
50
|
+
decodeWorkerBinaryResponse({ type: "Buffer", data: "00ff" }),
|
|
51
|
+
).toThrow(RestApiValidationError);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rejects a byte outside 0 through 255", () => {
|
|
55
|
+
expect(() =>
|
|
56
|
+
decodeWorkerBinaryResponse({ type: "Buffer", data: [256] }),
|
|
57
|
+
).toThrow(RestApiValidationError);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects a negative byte", () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
decodeWorkerBinaryResponse({ type: "Buffer", data: [-1] }),
|
|
63
|
+
).toThrow(RestApiValidationError);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reports the index of a non-integer byte", () => {
|
|
67
|
+
try {
|
|
68
|
+
decodeWorkerBinaryResponse({ type: "Buffer", data: [1, 1.5] });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
71
|
+
if (!(error instanceof RestApiValidationError)) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
expect(error.details.zodError.issues).toEqual([
|
|
75
|
+
expect.objectContaining({ path: ["data", 1] }),
|
|
76
|
+
]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw new Error("Expected binary response validation to fail");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("redacts malformed binary data from validation errors", () => {
|
|
83
|
+
const data = Array.from({ length: 4096 }, () => 0);
|
|
84
|
+
data[4095] = 256;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
decodeWorkerBinaryResponse({ type: "Buffer", data });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
expect(error).toBeInstanceOf(RestApiValidationError);
|
|
90
|
+
if (!(error instanceof RestApiValidationError)) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
expect(error.details.data).toEqual({
|
|
94
|
+
dataType: "binary",
|
|
95
|
+
redacted: true,
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw new Error("Expected malformed binary response to fail");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects a string payload", () => {
|
|
103
|
+
expect(() => decodeWorkerBinaryResponse("not-binary")).toThrow(
|
|
104
|
+
RestApiValidationError,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
REDACTED_BINARY_RESPONSE_DATA,
|
|
5
|
+
RestApiValidationError,
|
|
6
|
+
} from "../../errors.js";
|
|
7
|
+
|
|
8
|
+
export { REDACTED_BINARY_RESPONSE_DATA };
|
|
9
|
+
|
|
10
|
+
const workerBinaryResponseSchema = z.union([
|
|
11
|
+
z.instanceof(Uint8Array).transform((value) => new Uint8Array(value)),
|
|
12
|
+
z
|
|
13
|
+
.object({
|
|
14
|
+
data: z.unknown(),
|
|
15
|
+
type: z.literal("Buffer"),
|
|
16
|
+
})
|
|
17
|
+
.transform((value, context) => {
|
|
18
|
+
if (!Array.isArray(value.data)) {
|
|
19
|
+
context.addIssue({
|
|
20
|
+
code: z.ZodIssueCode.custom,
|
|
21
|
+
message: "data must be an array of bytes",
|
|
22
|
+
path: ["data"],
|
|
23
|
+
});
|
|
24
|
+
return z.NEVER;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bytes = new Uint8Array(value.data.length);
|
|
28
|
+
for (let index = 0; index < value.data.length; index += 1) {
|
|
29
|
+
const item = value.data[index];
|
|
30
|
+
if (
|
|
31
|
+
typeof item !== "number" ||
|
|
32
|
+
!Number.isInteger(item) ||
|
|
33
|
+
item < 0 ||
|
|
34
|
+
item > 255
|
|
35
|
+
) {
|
|
36
|
+
context.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
message: "byte must be an integer from 0 through 255",
|
|
39
|
+
path: ["data", index],
|
|
40
|
+
});
|
|
41
|
+
return z.NEVER;
|
|
42
|
+
}
|
|
43
|
+
bytes[index] = item;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return bytes;
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function decodeWorkerBinaryResponse(value: unknown): Uint8Array {
|
|
51
|
+
const result = workerBinaryResponseSchema.safeParse(value);
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
throw new RestApiValidationError(
|
|
54
|
+
`Binary response is malformed: ${result.error.message}`,
|
|
55
|
+
{
|
|
56
|
+
data: REDACTED_BINARY_RESPONSE_DATA,
|
|
57
|
+
zodError: result.error,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return result.data;
|
|
62
|
+
}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abstract base for REST API Integration clients.
|
|
3
|
-
*
|
|
4
|
-
* Owns the shared infrastructure that every REST-API-backed integration
|
|
5
|
-
* needs: config storage, parameter helpers, body validation, request
|
|
6
|
-
* construction, and query execution. Subclasses add their own
|
|
7
|
-
* `apiRequest` with whatever response-handling strategy they need
|
|
8
|
-
* (e.g. direct Zod validation, discriminated-union wrapping).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
import type { PartialMessage } from "@bufbuild/protobuf";
|
|
12
2
|
import { z } from "zod";
|
|
13
3
|
|
|
@@ -17,17 +7,12 @@ import type { Plugin as RestApiIntegrationPlugin } from "@superblocksteam/types/
|
|
|
17
7
|
import { RestApiValidationError } from "../../errors.js";
|
|
18
8
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
19
9
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
10
|
+
import { decodeWorkerBinaryResponse } from "./decode-worker-binary-response.js";
|
|
11
|
+
import { REST_API_RESPONSE_TYPES } from "./types.js";
|
|
20
12
|
import type { ApiRequestOptions } from "./types.js";
|
|
21
13
|
|
|
22
14
|
export type RestApiRequest = PartialMessage<RestApiIntegrationPlugin>;
|
|
23
15
|
|
|
24
|
-
/**
|
|
25
|
-
* Shared base for all REST API Integration clients.
|
|
26
|
-
*
|
|
27
|
-
* Provides config fields, parameter helpers, body validation, request
|
|
28
|
-
* building, and query execution. Does NOT define `apiRequest` — each
|
|
29
|
-
* concrete subclass supplies its own return-type contract.
|
|
30
|
-
*/
|
|
31
16
|
export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
32
17
|
readonly name: string;
|
|
33
18
|
readonly pluginId: string;
|
|
@@ -52,23 +37,11 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
|
52
37
|
};
|
|
53
38
|
}
|
|
54
39
|
|
|
55
|
-
/**
|
|
56
|
-
* Validate the request body, build the proto request, and execute it.
|
|
57
|
-
*
|
|
58
|
-
* Returns the raw (unvalidated) response from the orchestrator.
|
|
59
|
-
* Subclasses call this, then apply their own response handling.
|
|
60
|
-
*
|
|
61
|
-
* @param options - HTTP method, path, body, params, headers
|
|
62
|
-
* @param bodySchema - Optional Zod schema for body validation
|
|
63
|
-
* @param metadata - Optional trace metadata for observability
|
|
64
|
-
* @returns Raw response from the orchestrator
|
|
65
|
-
*/
|
|
66
40
|
protected async executeApiRequest<TBody>(
|
|
67
41
|
options: ApiRequestOptions<TBody>,
|
|
68
42
|
bodySchema?: z.ZodSchema<TBody>,
|
|
69
43
|
metadata?: TraceMetadata,
|
|
70
44
|
): Promise<unknown> {
|
|
71
|
-
// Validate request body if both body and schema are present.
|
|
72
45
|
if (options.body !== undefined && bodySchema) {
|
|
73
46
|
const bodyParseResult = bodySchema.safeParse(options.body);
|
|
74
47
|
if (!bodyParseResult.success) {
|
|
@@ -96,13 +69,27 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
|
96
69
|
}
|
|
97
70
|
}
|
|
98
71
|
|
|
72
|
+
const responseTypeResult = z
|
|
73
|
+
.enum(REST_API_RESPONSE_TYPES)
|
|
74
|
+
.safeParse(options.responseType ?? "json");
|
|
75
|
+
if (!responseTypeResult.success) {
|
|
76
|
+
throw new RestApiValidationError(
|
|
77
|
+
`Unsupported responseType ${JSON.stringify(options.responseType)} - expected one of: ${REST_API_RESPONSE_TYPES.join(", ")}`,
|
|
78
|
+
{
|
|
79
|
+
zodError: responseTypeResult.error,
|
|
80
|
+
data: options.responseType,
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const responseType = responseTypeResult.data;
|
|
85
|
+
|
|
99
86
|
const request: RestApiRequest = {
|
|
100
87
|
openApiAction: "genericHttpRequest",
|
|
101
88
|
httpMethod: options.method.toUpperCase(),
|
|
102
89
|
urlPath: options.path,
|
|
103
90
|
headers,
|
|
104
91
|
params,
|
|
105
|
-
responseType
|
|
92
|
+
responseType,
|
|
106
93
|
};
|
|
107
94
|
|
|
108
95
|
if (options.body !== undefined) {
|
|
@@ -110,25 +97,34 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
|
110
97
|
request.bodyType = "jsonBody";
|
|
111
98
|
}
|
|
112
99
|
|
|
113
|
-
const result = await this.executeQuery(
|
|
114
|
-
request as Record<string, unknown>,
|
|
115
|
-
undefined,
|
|
116
|
-
metadata,
|
|
117
|
-
);
|
|
100
|
+
const result = await this.executeQuery({ ...request }, undefined, metadata);
|
|
118
101
|
|
|
119
102
|
if (result === null || result === undefined) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
103
|
+
throw new RestApiValidationError(
|
|
104
|
+
`Integration query returned ${String(result)} for responseType "${responseType}" - expected a response value`,
|
|
105
|
+
{
|
|
106
|
+
zodError: new z.ZodError([
|
|
107
|
+
{
|
|
108
|
+
code: z.ZodIssueCode.custom,
|
|
109
|
+
message: "response value is required",
|
|
110
|
+
path: [],
|
|
111
|
+
},
|
|
112
|
+
]),
|
|
113
|
+
data: result,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
130
116
|
}
|
|
131
117
|
|
|
132
|
-
|
|
118
|
+
switch (responseType) {
|
|
119
|
+
case "binary":
|
|
120
|
+
return decodeWorkerBinaryResponse(result);
|
|
121
|
+
case "json":
|
|
122
|
+
case "text":
|
|
123
|
+
return result;
|
|
124
|
+
default: {
|
|
125
|
+
const _exhaustive: never = responseType;
|
|
126
|
+
return _exhaustive;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
133
129
|
}
|
|
134
130
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Generic REST API Integration client with Zod response validation.
|
|
3
|
-
*
|
|
4
|
-
* Extends RestApiClientBase with an apiRequest() that validates the
|
|
5
|
-
* full response against a caller-supplied Zod schema and throws
|
|
6
|
-
* RestApiValidationError on mismatch.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { z } from "zod";
|
|
1
|
+
import { z } from "zod";
|
|
10
2
|
|
|
11
3
|
import { RestApiValidationError } from "../../errors.js";
|
|
12
4
|
import type { TraceMetadata } from "../registry.js";
|
|
@@ -14,38 +6,76 @@ import { RestApiClientBase } from "./rest-api-client-base.js";
|
|
|
14
6
|
import type {
|
|
15
7
|
ApiRequestOptions,
|
|
16
8
|
ApiRequestSchema,
|
|
9
|
+
RestApiResponseType,
|
|
17
10
|
SupportsApiRequest,
|
|
18
11
|
} from "./types.js";
|
|
19
12
|
|
|
20
|
-
/**
|
|
21
|
-
* Base implementation for REST API Integration clients.
|
|
22
|
-
*
|
|
23
|
-
* All OpenAPI-based integration clients (except those with
|
|
24
|
-
* integration-specific response handling) extend this class to
|
|
25
|
-
* inherit the generic apiRequest() method with runtime Zod validation.
|
|
26
|
-
*/
|
|
27
13
|
export abstract class RestApiIntegrationClient
|
|
28
14
|
extends RestApiClientBase
|
|
29
15
|
implements SupportsApiRequest
|
|
30
16
|
{
|
|
31
17
|
async apiRequest<TBody, TResponse>(
|
|
32
|
-
options: ApiRequestOptions<TBody
|
|
18
|
+
options: ApiRequestOptions<TBody> & {
|
|
19
|
+
responseType?: Exclude<RestApiResponseType, "binary">;
|
|
20
|
+
},
|
|
33
21
|
schema: ApiRequestSchema<TBody, TResponse> & {
|
|
34
22
|
response: z.ZodSchema<TResponse>;
|
|
35
23
|
},
|
|
36
24
|
metadata?: TraceMetadata,
|
|
37
|
-
): Promise<TResponse
|
|
38
|
-
|
|
25
|
+
): Promise<TResponse>;
|
|
26
|
+
async apiRequest<TBody>(
|
|
27
|
+
options: ApiRequestOptions<TBody> & {
|
|
28
|
+
responseType: "binary";
|
|
29
|
+
},
|
|
30
|
+
schema?: ApiRequestSchema<TBody, Uint8Array>,
|
|
31
|
+
metadata?: TraceMetadata,
|
|
32
|
+
): Promise<Uint8Array>;
|
|
33
|
+
async apiRequest<TBody>(
|
|
34
|
+
options: ApiRequestOptions<TBody> & {
|
|
35
|
+
responseType: "text";
|
|
36
|
+
},
|
|
37
|
+
schema?: ApiRequestSchema<TBody, unknown>,
|
|
38
|
+
metadata?: TraceMetadata,
|
|
39
|
+
): Promise<unknown>;
|
|
40
|
+
async apiRequest<TBody, TResponse>(
|
|
41
|
+
options: ApiRequestOptions<TBody>,
|
|
42
|
+
schema?: ApiRequestSchema<TBody, TResponse>,
|
|
43
|
+
metadata?: TraceMetadata,
|
|
44
|
+
): Promise<TResponse | Uint8Array | unknown> {
|
|
45
|
+
if ((options.responseType ?? "json") === "json" && !schema?.response) {
|
|
46
|
+
throw new RestApiValidationError(
|
|
47
|
+
'apiRequest() with responseType "json" requires a response schema. Provide one, or set responseType to "text" or "binary"',
|
|
48
|
+
{
|
|
49
|
+
zodError: new z.ZodError([
|
|
50
|
+
{
|
|
51
|
+
code: z.ZodIssueCode.custom,
|
|
52
|
+
message: "response schema is required for JSON responses",
|
|
53
|
+
path: ["response"],
|
|
54
|
+
},
|
|
55
|
+
]),
|
|
56
|
+
data: undefined,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = await this.executeApiRequest(
|
|
62
|
+
options,
|
|
63
|
+
schema?.body,
|
|
64
|
+
metadata,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (!schema?.response) {
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
39
70
|
|
|
40
|
-
// Response schema is REQUIRED - always validate
|
|
41
71
|
const responseParseResult = schema.response.safeParse(result);
|
|
42
72
|
|
|
43
73
|
if (!responseParseResult.success) {
|
|
44
74
|
throw new RestApiValidationError(
|
|
45
75
|
`Response validation failed: ${responseParseResult.error.message}`,
|
|
46
76
|
{
|
|
47
|
-
zodError: responseParseResult.error,
|
|
48
77
|
data: result,
|
|
78
|
+
zodError: responseParseResult.error,
|
|
49
79
|
},
|
|
50
80
|
);
|
|
51
81
|
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for REST API Integration (OpenAPI) based clients.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import type { z } from "zod";
|
|
6
2
|
|
|
7
3
|
import type { TraceMetadata } from "../registry.js";
|
|
8
4
|
|
|
9
|
-
// Re-export for backwards compatibility
|
|
10
5
|
export type { TraceMetadata };
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
export const REST_API_RESPONSE_TYPES = ["binary", "json", "text"] as const;
|
|
8
|
+
|
|
9
|
+
export type RestApiResponseType = (typeof REST_API_RESPONSE_TYPES)[number];
|
|
10
|
+
|
|
15
11
|
export interface ApiRequestOptions<TBody = unknown> {
|
|
16
12
|
/**
|
|
17
13
|
* HTTP method for the request.
|
|
@@ -38,15 +34,23 @@ export interface ApiRequestOptions<TBody = unknown> {
|
|
|
38
34
|
* Optional HTTP headers
|
|
39
35
|
*/
|
|
40
36
|
headers?: Record<string, string>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How the response body should be decoded. Defaults to `"json"`.
|
|
40
|
+
*
|
|
41
|
+
* Use `"text"` for endpoints that return non-JSON payloads such as
|
|
42
|
+
* XML. Use `"binary"` for PDFs and other byte payloads. When
|
|
43
|
+
* requesting a text or binary response, omit the response schema (or
|
|
44
|
+
* use one matching the decoded value, e.g. `z.string()` or
|
|
45
|
+
* `z.instanceof(Uint8Array)`).
|
|
46
|
+
*/
|
|
47
|
+
responseType?: RestApiResponseType;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
/**
|
|
44
|
-
* Schema configuration for API request validation.
|
|
45
|
-
*/
|
|
46
50
|
export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
|
|
47
51
|
/**
|
|
48
52
|
* Optional Zod schema for request body validation.
|
|
49
|
-
*
|
|
53
|
+
* When omitted, `options.body` is sent without validation.
|
|
50
54
|
*/
|
|
51
55
|
body?: z.ZodSchema<TBody>;
|
|
52
56
|
|
|
@@ -60,14 +64,17 @@ export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
|
|
|
60
64
|
/**
|
|
61
65
|
* Interface for integration clients that support generic API requests.
|
|
62
66
|
*
|
|
63
|
-
*
|
|
67
|
+
* Providing a response schema gives type-safe, validated results. Omitting
|
|
68
|
+
* it returns the decoded response: `Uint8Array` for `responseType: "binary"`,
|
|
69
|
+
* or `unknown` for `responseType: "text"`. JSON still requires a schema.
|
|
70
|
+
* Object schemas only make sense for JSON-shaped results.
|
|
64
71
|
*/
|
|
65
72
|
export interface SupportsApiRequest {
|
|
66
73
|
/**
|
|
67
74
|
* Execute a generic API request with type-safe validation.
|
|
68
75
|
*
|
|
69
76
|
* @param options - Request configuration including method, path, params, and body
|
|
70
|
-
* @param schema - Zod schemas for request body and response validation
|
|
77
|
+
* @param schema - Zod schemas for request body and response validation
|
|
71
78
|
* @param metadata - Optional trace metadata for observability (label, description)
|
|
72
79
|
* @returns Validated response data
|
|
73
80
|
*
|
|
@@ -97,10 +104,73 @@ export interface SupportsApiRequest {
|
|
|
97
104
|
* ```
|
|
98
105
|
*/
|
|
99
106
|
apiRequest<TBody, TResponse>(
|
|
100
|
-
options: ApiRequestOptions<TBody
|
|
107
|
+
options: ApiRequestOptions<TBody> & {
|
|
108
|
+
responseType?: Exclude<RestApiResponseType, "binary">;
|
|
109
|
+
},
|
|
101
110
|
schema: ApiRequestSchema<TBody, TResponse> & {
|
|
102
111
|
response: z.ZodSchema<TResponse>;
|
|
103
112
|
},
|
|
104
113
|
metadata?: TraceMetadata,
|
|
105
114
|
): Promise<TResponse>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute a generic API request that returns binary data.
|
|
118
|
+
*
|
|
119
|
+
* @param options - Request configuration; `responseType` must be "binary"
|
|
120
|
+
* @param schema - Optional Zod schemas for request body and decoded response validation
|
|
121
|
+
* @param metadata - Optional trace metadata for observability (label, description)
|
|
122
|
+
* @returns The decoded body as a `Uint8Array`
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const pdf = await ctx.integrations.legacyApi.apiRequest({
|
|
127
|
+
* method: 'GET',
|
|
128
|
+
* path: '/file.pdf',
|
|
129
|
+
* responseType: 'binary',
|
|
130
|
+
* });
|
|
131
|
+
* // pdf is a Uint8Array
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
apiRequest<TBody>(
|
|
135
|
+
options: ApiRequestOptions<TBody> & {
|
|
136
|
+
responseType: "binary";
|
|
137
|
+
},
|
|
138
|
+
schema?: ApiRequestSchema<TBody, Uint8Array>,
|
|
139
|
+
metadata?: TraceMetadata,
|
|
140
|
+
): Promise<Uint8Array>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Execute a generic API request without response validation.
|
|
144
|
+
*
|
|
145
|
+
* This overload requires an explicit `responseType: "text"` - JSON
|
|
146
|
+
* responses must always be consumed through the schema overload above,
|
|
147
|
+
* so "unvalidated JSON" is unrepresentable. The decoded text
|
|
148
|
+
* response is returned as-is, typed `unknown`.
|
|
149
|
+
*
|
|
150
|
+
* Note: request-body validation is opt-in - it runs only when
|
|
151
|
+
* `schema.body` is provided. Omitting `schema` sends `options.body`
|
|
152
|
+
* without validation.
|
|
153
|
+
*
|
|
154
|
+
* @param options - Request configuration; `responseType` must be "text"
|
|
155
|
+
* @param schema - Optional Zod schema for request body validation
|
|
156
|
+
* @param metadata - Optional trace metadata for observability (label, description)
|
|
157
|
+
* @returns The raw response from the integration
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const xml = await ctx.integrations.legacyApi.apiRequest({
|
|
162
|
+
* method: 'GET',
|
|
163
|
+
* path: '/report.xml',
|
|
164
|
+
* responseType: 'text',
|
|
165
|
+
* });
|
|
166
|
+
* // xml is the raw XML string
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
apiRequest<TBody>(
|
|
170
|
+
options: ApiRequestOptions<TBody> & {
|
|
171
|
+
responseType: "text";
|
|
172
|
+
},
|
|
173
|
+
schema?: ApiRequestSchema<TBody, unknown>,
|
|
174
|
+
metadata?: TraceMetadata,
|
|
175
|
+
): Promise<unknown>;
|
|
106
176
|
}
|