@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.
- 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 +167 -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 +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/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/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 +190 -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 +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
- package/src/integrations/slack/types.ts +2 -2
|
@@ -17,6 +17,7 @@ import type { Plugin as RestApiIntegrationPlugin } from "@superblocksteam/types/
|
|
|
17
17
|
import { RestApiValidationError } from "../../errors.js";
|
|
18
18
|
import type { QueryExecutor, TraceMetadata } from "../registry.js";
|
|
19
19
|
import type { IntegrationConfig, IntegrationClientImpl } from "../types.js";
|
|
20
|
+
import { REST_API_RESPONSE_TYPES } from "./types.js";
|
|
20
21
|
import type { ApiRequestOptions } from "./types.js";
|
|
21
22
|
|
|
22
23
|
export type RestApiRequest = PartialMessage<RestApiIntegrationPlugin>;
|
|
@@ -58,7 +59,7 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
|
58
59
|
* Returns the raw (unvalidated) response from the orchestrator.
|
|
59
60
|
* Subclasses call this, then apply their own response handling.
|
|
60
61
|
*
|
|
61
|
-
* @param options -
|
|
62
|
+
* @param options - Request configuration (see {@link ApiRequestOptions})
|
|
62
63
|
* @param bodySchema - Optional Zod schema for body validation
|
|
63
64
|
* @param metadata - Optional trace metadata for observability
|
|
64
65
|
* @returns Raw response from the orchestrator
|
|
@@ -96,13 +97,31 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// The RestApiResponseType union is erased in the bundled JS user APIs
|
|
101
|
+
// run as, so values outside it (including wire values the orchestrator
|
|
102
|
+
// implements but the SDK deliberately does not expose, like "auto")
|
|
103
|
+
// must also be rejected at runtime, before the request is issued.
|
|
104
|
+
const responseTypeResult = z
|
|
105
|
+
.enum(REST_API_RESPONSE_TYPES)
|
|
106
|
+
.safeParse(options.responseType ?? "json");
|
|
107
|
+
if (!responseTypeResult.success) {
|
|
108
|
+
throw new RestApiValidationError(
|
|
109
|
+
`Unsupported responseType ${JSON.stringify(options.responseType)} — expected one of: ${REST_API_RESPONSE_TYPES.join(", ")}`,
|
|
110
|
+
{
|
|
111
|
+
zodError: responseTypeResult.error,
|
|
112
|
+
data: options.responseType,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const responseType = responseTypeResult.data;
|
|
117
|
+
|
|
99
118
|
const request: RestApiRequest = {
|
|
100
119
|
openApiAction: "genericHttpRequest",
|
|
101
120
|
httpMethod: options.method.toUpperCase(),
|
|
102
121
|
urlPath: options.path,
|
|
103
122
|
headers,
|
|
104
123
|
params,
|
|
105
|
-
responseType
|
|
124
|
+
responseType,
|
|
106
125
|
};
|
|
107
126
|
|
|
108
127
|
if (options.body !== undefined) {
|
|
@@ -116,11 +135,16 @@ export abstract class RestApiClientBase implements IntegrationClientImpl {
|
|
|
116
135
|
metadata,
|
|
117
136
|
);
|
|
118
137
|
|
|
138
|
+
// Neither `null` nor `undefined` is a legitimate decode result for the
|
|
139
|
+
// exposed response types: JSON parses to a value the schema sees, and
|
|
140
|
+
// an empty text body decodes to "". Either value only arises from a
|
|
141
|
+
// broken execution contract, so both throw. (If "auto" is ever exposed,
|
|
142
|
+
// `null` becomes legitimate — it parses a literal `null` JSON body.)
|
|
119
143
|
if (result === null || result === undefined) {
|
|
120
144
|
const nonNullResult = z.object({}).safeParse(result);
|
|
121
145
|
if (!nonNullResult.success) {
|
|
122
146
|
throw new RestApiValidationError(
|
|
123
|
-
`Integration query returned ${String(result)} — expected a
|
|
147
|
+
`Integration query returned ${String(result)} for responseType "${responseType}" — expected a response value`,
|
|
124
148
|
{
|
|
125
149
|
zodError: nonNullResult.error,
|
|
126
150
|
data: result,
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extends RestApiClientBase with an apiRequest() that validates the
|
|
5
5
|
* full response against a caller-supplied Zod schema and throws
|
|
6
|
-
* RestApiValidationError on mismatch.
|
|
6
|
+
* RestApiValidationError on mismatch. When no response schema is
|
|
7
|
+
* provided, the raw result is returned without validation — useful for
|
|
8
|
+
* non-JSON responses (e.g. XML), which can alternatively be validated
|
|
9
|
+
* with a schema matching the decoded value (e.g. z.string()).
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import
|
|
12
|
+
import { z } from "zod";
|
|
10
13
|
|
|
11
14
|
import { RestApiValidationError } from "../../errors.js";
|
|
12
15
|
import type { TraceMetadata } from "../registry.js";
|
|
@@ -14,6 +17,7 @@ import { RestApiClientBase } from "./rest-api-client-base.js";
|
|
|
14
17
|
import type {
|
|
15
18
|
ApiRequestOptions,
|
|
16
19
|
ApiRequestSchema,
|
|
20
|
+
RestApiResponseType,
|
|
17
21
|
SupportsApiRequest,
|
|
18
22
|
} from "./types.js";
|
|
19
23
|
|
|
@@ -34,10 +38,55 @@ export abstract class RestApiIntegrationClient
|
|
|
34
38
|
response: z.ZodSchema<TResponse>;
|
|
35
39
|
},
|
|
36
40
|
metadata?: TraceMetadata,
|
|
37
|
-
): Promise<TResponse
|
|
38
|
-
|
|
41
|
+
): Promise<TResponse>;
|
|
42
|
+
async apiRequest<TBody>(
|
|
43
|
+
options: ApiRequestOptions<TBody> & {
|
|
44
|
+
responseType: Exclude<RestApiResponseType, "json">;
|
|
45
|
+
},
|
|
46
|
+
schema?: ApiRequestSchema<TBody, unknown>,
|
|
47
|
+
metadata?: TraceMetadata,
|
|
48
|
+
): Promise<unknown>;
|
|
49
|
+
async apiRequest<TBody, TResponse>(
|
|
50
|
+
options: ApiRequestOptions<TBody>,
|
|
51
|
+
schema?: ApiRequestSchema<TBody, TResponse>,
|
|
52
|
+
metadata?: TraceMetadata,
|
|
53
|
+
): Promise<TResponse | unknown> {
|
|
54
|
+
// The overloads make schema-less JSON unrepresentable in TypeScript,
|
|
55
|
+
// but user API code executes as bundled JS with no type enforcement —
|
|
56
|
+
// enforce the same contract at runtime, before issuing the request.
|
|
57
|
+
if ((options.responseType ?? "json") === "json" && !schema?.response) {
|
|
58
|
+
throw new RestApiValidationError(
|
|
59
|
+
'apiRequest() with responseType "json" requires a response schema — provide one, or set responseType: "text" to receive a non-JSON payload unvalidated',
|
|
60
|
+
{
|
|
61
|
+
zodError: new z.ZodError([
|
|
62
|
+
{
|
|
63
|
+
code: z.ZodIssueCode.custom,
|
|
64
|
+
message: "response schema is required for JSON responses",
|
|
65
|
+
path: ["response"],
|
|
66
|
+
},
|
|
67
|
+
]),
|
|
68
|
+
data: undefined,
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await this.executeApiRequest(
|
|
74
|
+
options,
|
|
75
|
+
schema?.body,
|
|
76
|
+
metadata,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Without a response schema the raw result is returned as-is (typed
|
|
80
|
+
// unknown). Non-JSON responses (e.g. responseType "text" returning
|
|
81
|
+
// XML) can either omit the schema or pass one matching the decoded
|
|
82
|
+
// value, such as z.string(). Request-body validation is opt-in:
|
|
83
|
+
// executeApiRequest only validates options.body when schema.body is
|
|
84
|
+
// provided (unchanged from the previous contract, where schema.body
|
|
85
|
+
// was already optional).
|
|
86
|
+
if (!schema?.response) {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
39
89
|
|
|
40
|
-
// Response schema is REQUIRED - always validate
|
|
41
90
|
const responseParseResult = schema.response.safeParse(result);
|
|
42
91
|
|
|
43
92
|
if (!responseParseResult.success) {
|
|
@@ -9,6 +9,32 @@ import type { TraceMetadata } from "../registry.js";
|
|
|
9
9
|
// Re-export for backwards compatibility
|
|
10
10
|
export type { TraceMetadata };
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* How the orchestrator should decode the HTTP response body.
|
|
14
|
+
*
|
|
15
|
+
* - `"json"` (default): parse the response as JSON; fails on non-JSON bodies
|
|
16
|
+
* - `"text"`: return the decoded response body as a string (XML, CSV, HTML, ...)
|
|
17
|
+
*
|
|
18
|
+
* The orchestrator also implements `"auto"`, `"binary"`, and `"raw"` wire
|
|
19
|
+
* values; they are deliberately not exposed here (widening this union later
|
|
20
|
+
* is backward-compatible, narrowing it is not):
|
|
21
|
+
*
|
|
22
|
+
* - `"auto"` returns parsed JSON whenever the body happens to parse, which
|
|
23
|
+
* would let JSON responses bypass schema validation via the schema-less
|
|
24
|
+
* overload
|
|
25
|
+
* - `"binary"` would freeze the worker's internal Buffer JSON encoding
|
|
26
|
+
* (`{ type: "Buffer", data: number[] }`, ~4x payload inflation) into the
|
|
27
|
+
* public SDK contract
|
|
28
|
+
* - `"raw"` is rejected by the orchestrator for non-streaming requests
|
|
29
|
+
*
|
|
30
|
+
* The runtime tuple backs the request-time guard in `executeApiRequest`:
|
|
31
|
+
* the type-level union is erased in the bundled JS user APIs run as, so
|
|
32
|
+
* values outside it must also be rejected at runtime.
|
|
33
|
+
*/
|
|
34
|
+
export const REST_API_RESPONSE_TYPES = ["json", "text"] as const;
|
|
35
|
+
|
|
36
|
+
export type RestApiResponseType = (typeof REST_API_RESPONSE_TYPES)[number];
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Options for making a generic REST API request.
|
|
14
40
|
*/
|
|
@@ -38,6 +64,15 @@ export interface ApiRequestOptions<TBody = unknown> {
|
|
|
38
64
|
* Optional HTTP headers
|
|
39
65
|
*/
|
|
40
66
|
headers?: Record<string, string>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How the response body should be decoded. Defaults to `"json"`.
|
|
70
|
+
*
|
|
71
|
+
* Use `"text"` for endpoints that return non-JSON payloads such as
|
|
72
|
+
* XML. When requesting a text response, omit the response schema (or
|
|
73
|
+
* use one matching the decoded string, e.g. `z.string()`).
|
|
74
|
+
*/
|
|
75
|
+
responseType?: RestApiResponseType;
|
|
41
76
|
}
|
|
42
77
|
|
|
43
78
|
/**
|
|
@@ -46,7 +81,7 @@ export interface ApiRequestOptions<TBody = unknown> {
|
|
|
46
81
|
export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
|
|
47
82
|
/**
|
|
48
83
|
* Optional Zod schema for request body validation.
|
|
49
|
-
*
|
|
84
|
+
* When omitted, `options.body` is sent without validation.
|
|
50
85
|
*/
|
|
51
86
|
body?: z.ZodSchema<TBody>;
|
|
52
87
|
|
|
@@ -60,14 +95,18 @@ export interface ApiRequestSchema<TBody = unknown, TResponse = unknown> {
|
|
|
60
95
|
/**
|
|
61
96
|
* Interface for integration clients that support generic API requests.
|
|
62
97
|
*
|
|
63
|
-
*
|
|
98
|
+
* Providing a response schema gives type-safe, validated results. Omitting
|
|
99
|
+
* it returns the raw response as `unknown`. For non-JSON payloads (e.g.
|
|
100
|
+
* `responseType: "text"` for XML), either omit the schema or supply one
|
|
101
|
+
* matching the decoded value (e.g. `z.string()` for text) — object schemas
|
|
102
|
+
* only make sense for JSON-shaped results.
|
|
64
103
|
*/
|
|
65
104
|
export interface SupportsApiRequest {
|
|
66
105
|
/**
|
|
67
106
|
* Execute a generic API request with type-safe validation.
|
|
68
107
|
*
|
|
69
108
|
* @param options - Request configuration including method, path, params, and body
|
|
70
|
-
* @param schema - Zod schemas for request body and response validation
|
|
109
|
+
* @param schema - Zod schemas for request body and response validation
|
|
71
110
|
* @param metadata - Optional trace metadata for observability (label, description)
|
|
72
111
|
* @returns Validated response data
|
|
73
112
|
*
|
|
@@ -103,4 +142,39 @@ export interface SupportsApiRequest {
|
|
|
103
142
|
},
|
|
104
143
|
metadata?: TraceMetadata,
|
|
105
144
|
): Promise<TResponse>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Execute a generic API request without response validation.
|
|
148
|
+
*
|
|
149
|
+
* This overload requires an explicit non-JSON `responseType` — JSON
|
|
150
|
+
* responses must always be consumed through the schema overload above,
|
|
151
|
+
* so "unvalidated JSON" is unrepresentable. The decoded non-JSON
|
|
152
|
+
* response is returned as-is, typed `unknown`.
|
|
153
|
+
*
|
|
154
|
+
* Note: request-body validation is opt-in — it runs only when
|
|
155
|
+
* `schema.body` is provided. Omitting `schema` sends `options.body`
|
|
156
|
+
* without validation.
|
|
157
|
+
*
|
|
158
|
+
* @param options - Request configuration; `responseType` must be "text"
|
|
159
|
+
* @param schema - Optional Zod schema for request body validation
|
|
160
|
+
* @param metadata - Optional trace metadata for observability (label, description)
|
|
161
|
+
* @returns The raw response from the integration
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* const xml = await ctx.integrations.legacyApi.apiRequest({
|
|
166
|
+
* method: 'GET',
|
|
167
|
+
* path: '/report.xml',
|
|
168
|
+
* responseType: 'text',
|
|
169
|
+
* });
|
|
170
|
+
* // xml is the raw XML string
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
apiRequest<TBody>(
|
|
174
|
+
options: ApiRequestOptions<TBody> & {
|
|
175
|
+
responseType: Exclude<RestApiResponseType, "json">;
|
|
176
|
+
},
|
|
177
|
+
schema?: ApiRequestSchema<TBody, unknown>,
|
|
178
|
+
metadata?: TraceMetadata,
|
|
179
|
+
): Promise<unknown>;
|
|
106
180
|
}
|
|
@@ -659,7 +659,27 @@ describe("resolveIntegrationDocumentation", () => {
|
|
|
659
659
|
}
|
|
660
660
|
});
|
|
661
661
|
|
|
662
|
-
it("throws when overlay entry has
|
|
662
|
+
it("throws when overlay entry has neither versionRange nor sdkVersionRange", async () => {
|
|
663
|
+
const integrationsDirectory = createPluginDocsFixture("dropbox", {
|
|
664
|
+
"README.md": "base",
|
|
665
|
+
"docs.manifest.json": JSON.stringify({
|
|
666
|
+
overlays: [{ file: "overlays/01.md" }],
|
|
667
|
+
}),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
await expect(
|
|
672
|
+
resolveIntegrationDocumentation("dropbox", {
|
|
673
|
+
pluginVersion: "0.4.0",
|
|
674
|
+
integrationsDirectory,
|
|
675
|
+
}),
|
|
676
|
+
).rejects.toThrowError(/Invalid overlay entry/);
|
|
677
|
+
} finally {
|
|
678
|
+
rmSync(integrationsDirectory, { recursive: true, force: true });
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("throws when overlay entry has empty versionRange and no sdkVersionRange", async () => {
|
|
663
683
|
const integrationsDirectory = createPluginDocsFixture("dropbox", {
|
|
664
684
|
"README.md": "base",
|
|
665
685
|
"docs.manifest.json": JSON.stringify({
|
|
@@ -680,6 +700,175 @@ describe("resolveIntegrationDocumentation", () => {
|
|
|
680
700
|
});
|
|
681
701
|
});
|
|
682
702
|
|
|
703
|
+
describe("sdkVersionRange", () => {
|
|
704
|
+
it("applies overlay when sdkVersion matches sdkVersionRange", async () => {
|
|
705
|
+
const integrationsDirectory = createPluginDocsFixture("graphql", {
|
|
706
|
+
"README.md": "base-graphql-docs",
|
|
707
|
+
"docs.manifest.json": JSON.stringify({
|
|
708
|
+
overlays: [
|
|
709
|
+
{ file: "overlays/headers.md", sdkVersionRange: ">=0.0.2" },
|
|
710
|
+
],
|
|
711
|
+
}),
|
|
712
|
+
"overlays/headers.md": "dynamic-headers-docs",
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
const docs = await resolveIntegrationDocumentation("graphql", {
|
|
717
|
+
sdkVersion: "0.0.2",
|
|
718
|
+
integrationsDirectory,
|
|
719
|
+
});
|
|
720
|
+
expect(docs).toBe("base-graphql-docs\n\ndynamic-headers-docs");
|
|
721
|
+
} finally {
|
|
722
|
+
rmSync(integrationsDirectory, { recursive: true, force: true });
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("skips overlay when sdkVersion does not match sdkVersionRange", async () => {
|
|
727
|
+
const integrationsDirectory = createPluginDocsFixture("graphql", {
|
|
728
|
+
"README.md": "base-graphql-docs",
|
|
729
|
+
"docs.manifest.json": JSON.stringify({
|
|
730
|
+
overlays: [
|
|
731
|
+
{ file: "overlays/headers.md", sdkVersionRange: ">=0.0.2" },
|
|
732
|
+
],
|
|
733
|
+
}),
|
|
734
|
+
"overlays/headers.md": "dynamic-headers-docs",
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const docs = await resolveIntegrationDocumentation("graphql", {
|
|
739
|
+
sdkVersion: "0.0.1",
|
|
740
|
+
integrationsDirectory,
|
|
741
|
+
});
|
|
742
|
+
expect(docs).toBe("base-graphql-docs");
|
|
743
|
+
} finally {
|
|
744
|
+
rmSync(integrationsDirectory, { recursive: true, force: true });
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("skips overlay when sdkVersion is not provided", async () => {
|
|
749
|
+
const integrationsDirectory = createPluginDocsFixture("graphql", {
|
|
750
|
+
"README.md": "base-graphql-docs",
|
|
751
|
+
"docs.manifest.json": JSON.stringify({
|
|
752
|
+
overlays: [
|
|
753
|
+
{ file: "overlays/headers.md", sdkVersionRange: ">=0.0.2" },
|
|
754
|
+
],
|
|
755
|
+
}),
|
|
756
|
+
"overlays/headers.md": "dynamic-headers-docs",
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
const docs = await resolveIntegrationDocumentation("graphql", {
|
|
761
|
+
pluginVersion: "0.0.10",
|
|
762
|
+
integrationsDirectory,
|
|
763
|
+
});
|
|
764
|
+
expect(docs).toBe("base-graphql-docs");
|
|
765
|
+
} finally {
|
|
766
|
+
rmSync(integrationsDirectory, { recursive: true, force: true });
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("requires both versionRange and sdkVersionRange to match when both are specified", async () => {
|
|
771
|
+
const integrationsDirectory = createPluginDocsFixture("graphql", {
|
|
772
|
+
"README.md": "base",
|
|
773
|
+
"docs.manifest.json": JSON.stringify({
|
|
774
|
+
overlays: [
|
|
775
|
+
{
|
|
776
|
+
file: "overlays/both.md",
|
|
777
|
+
versionRange: ">=0.0.10",
|
|
778
|
+
sdkVersionRange: ">=0.0.2",
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
}),
|
|
782
|
+
"overlays/both.md": "both-match-overlay",
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
// Both match
|
|
787
|
+
expect(
|
|
788
|
+
await resolveIntegrationDocumentation("graphql", {
|
|
789
|
+
pluginVersion: "0.0.10",
|
|
790
|
+
sdkVersion: "0.0.2",
|
|
791
|
+
integrationsDirectory,
|
|
792
|
+
}),
|
|
793
|
+
).toBe("base\n\nboth-match-overlay");
|
|
794
|
+
|
|
795
|
+
// Only plugin matches
|
|
796
|
+
expect(
|
|
797
|
+
await resolveIntegrationDocumentation("graphql", {
|
|
798
|
+
pluginVersion: "0.0.10",
|
|
799
|
+
sdkVersion: "0.0.1",
|
|
800
|
+
integrationsDirectory,
|
|
801
|
+
}),
|
|
802
|
+
).toBe("base");
|
|
803
|
+
|
|
804
|
+
// Only sdk matches
|
|
805
|
+
expect(
|
|
806
|
+
await resolveIntegrationDocumentation("graphql", {
|
|
807
|
+
pluginVersion: "0.0.9",
|
|
808
|
+
sdkVersion: "0.0.2",
|
|
809
|
+
integrationsDirectory,
|
|
810
|
+
}),
|
|
811
|
+
).toBe("base");
|
|
812
|
+
} finally {
|
|
813
|
+
rmSync(integrationsDirectory, { recursive: true, force: true });
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("allows overlay with only sdkVersionRange (no versionRange)", async () => {
|
|
818
|
+
const integrationsDirectory = createPluginDocsFixture("graphql", {
|
|
819
|
+
"README.md": "base",
|
|
820
|
+
"docs.manifest.json": JSON.stringify({
|
|
821
|
+
overlays: [
|
|
822
|
+
{ file: "overlays/sdk-only.md", sdkVersionRange: ">=0.0.2" },
|
|
823
|
+
],
|
|
824
|
+
}),
|
|
825
|
+
"overlays/sdk-only.md": "sdk-gated-overlay",
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
// No pluginVersion needed — only sdkVersion matters
|
|
830
|
+
const docs = await resolveIntegrationDocumentation("graphql", {
|
|
831
|
+
sdkVersion: "0.0.3",
|
|
832
|
+
integrationsDirectory,
|
|
833
|
+
});
|
|
834
|
+
expect(docs).toBe("base\n\nsdk-gated-overlay");
|
|
835
|
+
} finally {
|
|
836
|
+
rmSync(integrationsDirectory, { recursive: true, force: true });
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
describe("restapiintegration responseType gating (real docs)", () => {
|
|
842
|
+
// These resolve the actual shipped docs, not a fixture: the overlay
|
|
843
|
+
// gating is the discoverability guard for the responseType feature
|
|
844
|
+
// (agents whose worker sdk-api predates it must never be told the
|
|
845
|
+
// option exists), so the real manifest content is the contract.
|
|
846
|
+
|
|
847
|
+
it("documents responseType for agents whose sdk-api supports it", async () => {
|
|
848
|
+
const docs = await resolveIntegrationDocumentation("restapiintegration", {
|
|
849
|
+
sdkVersion: "0.0.3",
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
expect(docs).toContain('responseType: "text"');
|
|
853
|
+
expect(docs).not.toContain("does not support the `responseType`");
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("notes non-support instead of documenting responseType on older agents", async () => {
|
|
857
|
+
const docs = await resolveIntegrationDocumentation("restapiintegration", {
|
|
858
|
+
sdkVersion: "0.0.2",
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
expect(docs).toContain("does not support the `responseType`");
|
|
862
|
+
expect(docs).not.toContain('responseType: "text"');
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it("stays silent about responseType when the agent reports no sdk-api version", async () => {
|
|
866
|
+
const docs = await resolveIntegrationDocumentation("restapiintegration");
|
|
867
|
+
|
|
868
|
+
expect(docs).not.toContain("responseType");
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
683
872
|
it("blocks overlay paths outside the plugin directory", async () => {
|
|
684
873
|
const integrationsDirectory = createPluginDocsFixture("dropbox", {
|
|
685
874
|
"README.md": "base",
|
|
@@ -11,7 +11,13 @@ const DOC_DIRECTORY_ALIASES = {
|
|
|
11
11
|
|
|
12
12
|
interface DocumentationOverlay {
|
|
13
13
|
file: string;
|
|
14
|
-
versionRange
|
|
14
|
+
versionRange?: string;
|
|
15
|
+
/** When set, checks against the sdk-api version ("javascriptsdkapi") running
|
|
16
|
+
* in the orchestrator instead of the plugin-specific version. Use this to
|
|
17
|
+
* gate overlays on sdk-api features that ship independently of plugin
|
|
18
|
+
* version bumps. Both versionRange and sdkVersionRange can be specified
|
|
19
|
+
* together — the overlay is applied only when all specified ranges match. */
|
|
20
|
+
sdkVersionRange?: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
interface DocumentationManifest {
|
|
@@ -32,6 +38,7 @@ type OverlayOperation =
|
|
|
32
38
|
|
|
33
39
|
export interface ResolveIntegrationDocumentationOptions {
|
|
34
40
|
pluginVersion?: SemVer;
|
|
41
|
+
sdkVersion?: SemVer;
|
|
35
42
|
integrationsDirectory?: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -82,16 +89,17 @@ async function parseDocumentationManifest(
|
|
|
82
89
|
typeof overlay !== "object" ||
|
|
83
90
|
overlay === null ||
|
|
84
91
|
!("file" in overlay) ||
|
|
85
|
-
!("versionRange" in overlay)
|
|
92
|
+
(!("versionRange" in overlay) && !("sdkVersionRange" in overlay))
|
|
86
93
|
) {
|
|
87
94
|
throw new Error(
|
|
88
95
|
`Invalid overlay entry at index ${index} in ${manifestPath}.`,
|
|
89
96
|
);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
const { file, versionRange } = overlay as {
|
|
99
|
+
const { file, versionRange, sdkVersionRange } = overlay as {
|
|
93
100
|
file: unknown;
|
|
94
101
|
versionRange: unknown;
|
|
102
|
+
sdkVersionRange: unknown;
|
|
95
103
|
};
|
|
96
104
|
|
|
97
105
|
if (typeof file !== "string" || file.trim().length === 0) {
|
|
@@ -99,18 +107,35 @@ async function parseDocumentationManifest(
|
|
|
99
107
|
`Invalid "file" for overlay index ${index} in ${manifestPath}.`,
|
|
100
108
|
);
|
|
101
109
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
versionRange.trim().length
|
|
105
|
-
|
|
110
|
+
|
|
111
|
+
const hasVersionRange =
|
|
112
|
+
typeof versionRange === "string" && versionRange.trim().length > 0;
|
|
113
|
+
const hasSdkVersionRange =
|
|
114
|
+
typeof sdkVersionRange === "string" &&
|
|
115
|
+
sdkVersionRange.trim().length > 0;
|
|
116
|
+
|
|
117
|
+
if ("versionRange" in overlay && !hasVersionRange) {
|
|
106
118
|
throw new Error(
|
|
107
119
|
`Invalid "versionRange" for overlay index ${index} in ${manifestPath}.`,
|
|
108
120
|
);
|
|
109
121
|
}
|
|
122
|
+
if ("sdkVersionRange" in overlay && !hasSdkVersionRange) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Invalid "sdkVersionRange" for overlay index ${index} in ${manifestPath}.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!hasVersionRange && !hasSdkVersionRange) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Overlay at index ${index} in ${manifestPath} must specify at least one of "versionRange" or "sdkVersionRange".`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
110
132
|
|
|
111
133
|
return {
|
|
112
134
|
file,
|
|
113
|
-
versionRange,
|
|
135
|
+
...(hasVersionRange ? { versionRange: versionRange as string } : {}),
|
|
136
|
+
...(hasSdkVersionRange
|
|
137
|
+
? { sdkVersionRange: sdkVersionRange as string }
|
|
138
|
+
: {}),
|
|
114
139
|
};
|
|
115
140
|
},
|
|
116
141
|
);
|
|
@@ -380,11 +405,38 @@ function ensureOverlayPathIsWithinPluginDir(
|
|
|
380
405
|
}
|
|
381
406
|
}
|
|
382
407
|
|
|
408
|
+
function overlayMatchesVersions(
|
|
409
|
+
overlay: DocumentationOverlay,
|
|
410
|
+
pluginVersion: SemVer | undefined,
|
|
411
|
+
sdkVersion: SemVer | undefined,
|
|
412
|
+
): boolean {
|
|
413
|
+
if (
|
|
414
|
+
overlay.versionRange &&
|
|
415
|
+
(!pluginVersion ||
|
|
416
|
+
!versionMatchesRange(pluginVersion, overlay.versionRange))
|
|
417
|
+
) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
overlay.sdkVersionRange &&
|
|
423
|
+
(!sdkVersion || !versionMatchesRange(sdkVersion, overlay.sdkVersionRange))
|
|
424
|
+
) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
383
431
|
export async function resolveIntegrationDocumentation(
|
|
384
432
|
pluginId: string,
|
|
385
433
|
options: ResolveIntegrationDocumentationOptions = {},
|
|
386
434
|
): Promise<string> {
|
|
387
|
-
const {
|
|
435
|
+
const {
|
|
436
|
+
pluginVersion,
|
|
437
|
+
sdkVersion,
|
|
438
|
+
integrationsDirectory = INTEGRATIONS_DIR,
|
|
439
|
+
} = options;
|
|
388
440
|
|
|
389
441
|
const pluginDirectory = resolve(
|
|
390
442
|
integrationsDirectory,
|
|
@@ -397,13 +449,13 @@ export async function resolveIntegrationDocumentation(
|
|
|
397
449
|
const baseDocumentationPath = resolve(pluginDirectory, baseFileName);
|
|
398
450
|
const baseDocumentation = await readFile(baseDocumentationPath, "utf8");
|
|
399
451
|
|
|
400
|
-
if (!manifest
|
|
452
|
+
if (!manifest) {
|
|
401
453
|
return baseDocumentation;
|
|
402
454
|
}
|
|
403
455
|
|
|
404
456
|
let resolvedDocumentation = baseDocumentation;
|
|
405
457
|
for (const overlay of manifest.overlays) {
|
|
406
|
-
if (!
|
|
458
|
+
if (!overlayMatchesVersions(overlay, pluginVersion, sdkVersion)) {
|
|
407
459
|
continue;
|
|
408
460
|
}
|
|
409
461
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## @replace: Methods
|
|
2
|
+
|
|
3
|
+
| Method | Description |
|
|
4
|
+
| ---------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
5
|
+
| `query<T>(query, schema, variables?, metadata?, headers?)` | Execute a GraphQL query with required schema validation |
|
|
6
|
+
| `mutation<T>(mutation, schema, variables?, metadata?, headers?)` | Execute a GraphQL mutation with required schema validation |
|
|
7
|
+
|
|
8
|
+
## @replace: Trace Metadata
|
|
9
|
+
|
|
10
|
+
All methods accept an optional `metadata` parameter for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.
|
|
11
|
+
|
|
12
|
+
## Dynamic Headers
|
|
13
|
+
|
|
14
|
+
Static headers (e.g. a fixed `X-API-Version`) and auth headers (e.g. Bearer tokens, API keys) should be configured on the GraphQL integration in the Superblocks UI so they apply to every call automatically.
|
|
15
|
+
|
|
16
|
+
For values that change per request — for example a bearer token derived from the API's input or from `ctx.env` — pass an optional `headers` map as the final argument to `query()` or `mutation()`:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
const MeResponseSchema = z.object({
|
|
20
|
+
data: z.object({
|
|
21
|
+
me: z.object({ id: z.string(), email: z.string() }),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const result = await ctx.integrations.graphql.query(
|
|
26
|
+
`query { me { id email } }`,
|
|
27
|
+
{ response: MeResponseSchema },
|
|
28
|
+
undefined, // no variables
|
|
29
|
+
undefined, // no trace metadata
|
|
30
|
+
{ Authorization: `Bearer ${ctx.env.UPSTREAM_TOKEN}` },
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Since both `variables` and `metadata` accept plain objects, pass `undefined` for any of them that you do not need.
|
|
@@ -20,7 +20,7 @@ import type { PostgresClient } from "./types.js";
|
|
|
20
20
|
* PostgreSQL request type derived from proto definition.
|
|
21
21
|
* Using PartialMessage allows optional fields.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
type PostgresRequest = PartialMessage<PostgresPlugin>;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Internal implementation of PostgresClient.
|