@superblocksteam/sdk-api 2.0.119 → 2.0.120-next.0
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/graphql-integration-client.d.ts +6 -2
- package/dist/integrations/base/graphql-integration-client.d.ts.map +1 -1
- package/dist/integrations/base/graphql-integration-client.js +18 -5
- package/dist/integrations/base/graphql-integration-client.js.map +1 -1
- package/dist/integrations/documentation-resolver.test.js +143 -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/graphql/client.test.d.ts +15 -0
- package/dist/integrations/graphql/client.test.d.ts.map +1 -0
- package/dist/integrations/graphql/client.test.js +148 -0
- package/dist/integrations/graphql/client.test.js.map +1 -0
- package/dist/integrations/graphql/types.d.ts +21 -2
- package/dist/integrations/graphql/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/integrations/base/graphql-integration-client.ts +24 -3
- package/src/integrations/documentation-resolver.test.ts +159 -1
- package/src/integrations/documentation.ts +63 -11
- package/src/integrations/graphql/client.test.ts +220 -0
- package/src/integrations/graphql/docs.manifest.json +6 -1
- package/src/integrations/graphql/overlays/dynamic-headers.md +34 -0
- package/src/integrations/graphql/types.ts +21 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for GraphQLClientImpl.
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - The proto request built by query()/mutation() matches the
|
|
6
|
+
* graphql.v1.Plugin shape expected by the orchestrator.
|
|
7
|
+
* - Optional per-request headers are forwarded as the proto's repeated
|
|
8
|
+
* `headers` field (key/value Property entries) so dynamic values like
|
|
9
|
+
* Authorization tokens can be sent from API code.
|
|
10
|
+
* - Variables are serialized into the proto `custom.variables` Property.
|
|
11
|
+
* - Trace metadata is passed through to the executeQuery callback.
|
|
12
|
+
* - Response Zod validation throws RestApiValidationError on mismatch.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, vi } from "vitest";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import { RestApiValidationError } from "../../errors.js";
|
|
19
|
+
import type { IntegrationConfig } from "../types.js";
|
|
20
|
+
import { GraphQLClientImpl } from "./client.js";
|
|
21
|
+
|
|
22
|
+
const TEST_CONFIG: IntegrationConfig = {
|
|
23
|
+
id: "graphql-test-id",
|
|
24
|
+
name: "Test GraphQL",
|
|
25
|
+
pluginId: "graphqlintegration",
|
|
26
|
+
configuration: {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function createClient(mockResult: unknown) {
|
|
30
|
+
const executeQuery = vi.fn().mockResolvedValue(mockResult);
|
|
31
|
+
const client = new GraphQLClientImpl(TEST_CONFIG, executeQuery);
|
|
32
|
+
return { client, executeQuery };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const UserResponseSchema = z.object({
|
|
36
|
+
data: z.object({
|
|
37
|
+
user: z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
name: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const SAMPLE_QUERY = `query GetUser($id: ID!) {
|
|
45
|
+
user(id: $id) { id name }
|
|
46
|
+
}`;
|
|
47
|
+
|
|
48
|
+
const SAMPLE_MUTATION = `mutation CreateUser($name: String!) {
|
|
49
|
+
createUser(name: $name) { id name }
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
describe("GraphQLClientImpl", () => {
|
|
53
|
+
describe("query()", () => {
|
|
54
|
+
it("returns validated data on a successful response", async () => {
|
|
55
|
+
const { client } = createClient({
|
|
56
|
+
data: { user: { id: "u1", name: "Alice" } },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = await client.query(
|
|
60
|
+
SAMPLE_QUERY,
|
|
61
|
+
{ response: UserResponseSchema },
|
|
62
|
+
{ id: "u1" },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(result.data.user).toEqual({ id: "u1", name: "Alice" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("builds the proto request with body, variables, and defaults", async () => {
|
|
69
|
+
const { client, executeQuery } = createClient({
|
|
70
|
+
data: { user: { id: "u1", name: "Alice" } },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await client.query(
|
|
74
|
+
SAMPLE_QUERY,
|
|
75
|
+
{ response: UserResponseSchema },
|
|
76
|
+
{ id: "u1" },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(executeQuery).toHaveBeenCalledOnce();
|
|
80
|
+
const request = executeQuery.mock.calls[0][0];
|
|
81
|
+
expect(request.body).toBe(SAMPLE_QUERY);
|
|
82
|
+
expect(request.verboseHttpOutput).toBe(false);
|
|
83
|
+
expect(request.failOnGraphqlErrors).toBe(true);
|
|
84
|
+
expect(request.custom).toEqual({
|
|
85
|
+
variables: {
|
|
86
|
+
key: "variables",
|
|
87
|
+
value: JSON.stringify({ id: "u1" }),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
expect(request.headers).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("omits custom when no variables are provided", async () => {
|
|
94
|
+
const { client, executeQuery } = createClient({
|
|
95
|
+
data: { user: { id: "u1", name: "Alice" } },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await client.query(SAMPLE_QUERY, { response: UserResponseSchema });
|
|
99
|
+
|
|
100
|
+
const request = executeQuery.mock.calls[0][0];
|
|
101
|
+
expect(request.custom).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("forwards per-request headers into the proto headers field", async () => {
|
|
105
|
+
const { client, executeQuery } = createClient({
|
|
106
|
+
data: { user: { id: "u1", name: "Alice" } },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await client.query(
|
|
110
|
+
SAMPLE_QUERY,
|
|
111
|
+
{ response: UserResponseSchema },
|
|
112
|
+
{ id: "u1" },
|
|
113
|
+
undefined,
|
|
114
|
+
{
|
|
115
|
+
Authorization: "Bearer abc123",
|
|
116
|
+
"X-Trace-Id": "trace-1",
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const request = executeQuery.mock.calls[0][0];
|
|
121
|
+
expect(request.headers).toEqual([
|
|
122
|
+
{ key: "Authorization", value: "Bearer abc123" },
|
|
123
|
+
{ key: "X-Trace-Id", value: "trace-1" },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("does not set headers when an empty headers object is passed", async () => {
|
|
128
|
+
const { client, executeQuery } = createClient({
|
|
129
|
+
data: { user: { id: "u1", name: "Alice" } },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await client.query(
|
|
133
|
+
SAMPLE_QUERY,
|
|
134
|
+
{ response: UserResponseSchema },
|
|
135
|
+
undefined,
|
|
136
|
+
undefined,
|
|
137
|
+
{},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const request = executeQuery.mock.calls[0][0];
|
|
141
|
+
expect(request.headers).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("passes trace metadata through to executeQuery", async () => {
|
|
145
|
+
const { client, executeQuery } = createClient({
|
|
146
|
+
data: { user: { id: "u1", name: "Alice" } },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await client.query(
|
|
150
|
+
SAMPLE_QUERY,
|
|
151
|
+
{ response: UserResponseSchema },
|
|
152
|
+
undefined,
|
|
153
|
+
{ label: "graphql.getUser", description: "Fetch a user by id" },
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(executeQuery).toHaveBeenCalledWith(expect.any(Object), undefined, {
|
|
157
|
+
label: "graphql.getUser",
|
|
158
|
+
description: "Fetch a user by id",
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("throws RestApiValidationError when the response fails schema validation", async () => {
|
|
163
|
+
const { client } = createClient({
|
|
164
|
+
data: { user: { id: "u1" /* missing name */ } },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await expect(
|
|
168
|
+
client.query(SAMPLE_QUERY, { response: UserResponseSchema }),
|
|
169
|
+
).rejects.toThrow(RestApiValidationError);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("mutation()", () => {
|
|
174
|
+
it("returns validated data on a successful response", async () => {
|
|
175
|
+
const { client } = createClient({
|
|
176
|
+
data: { createUser: { id: "u2", name: "Bob" } },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const Schema = z.object({
|
|
180
|
+
data: z.object({
|
|
181
|
+
createUser: z.object({ id: z.string(), name: z.string() }),
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = await client.mutation(
|
|
186
|
+
SAMPLE_MUTATION,
|
|
187
|
+
{ response: Schema },
|
|
188
|
+
{ name: "Bob" },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(result.data.createUser).toEqual({ id: "u2", name: "Bob" });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("forwards per-request headers into the proto headers field", async () => {
|
|
195
|
+
const { client, executeQuery } = createClient({
|
|
196
|
+
data: { createUser: { id: "u2", name: "Bob" } },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const Schema = z.object({
|
|
200
|
+
data: z.object({
|
|
201
|
+
createUser: z.object({ id: z.string(), name: z.string() }),
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await client.mutation(
|
|
206
|
+
SAMPLE_MUTATION,
|
|
207
|
+
{ response: Schema },
|
|
208
|
+
{ name: "Bob" },
|
|
209
|
+
undefined,
|
|
210
|
+
{ Authorization: "Bearer xyz" },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const request = executeQuery.mock.calls[0][0];
|
|
214
|
+
expect(request.body).toBe(SAMPLE_MUTATION);
|
|
215
|
+
expect(request.headers).toEqual([
|
|
216
|
+
{ key: "Authorization", value: "Bearer xyz" },
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -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.
|
|
@@ -62,6 +62,17 @@ import type { TraceMetadata } from "../registry.js";
|
|
|
62
62
|
* { response: MutationResponseSchema },
|
|
63
63
|
* { name: 'John Doe' }
|
|
64
64
|
* );
|
|
65
|
+
*
|
|
66
|
+
* // Send per-request headers (e.g. a dynamic Authorization token).
|
|
67
|
+
* // Static headers and auth configured on the integration apply automatically;
|
|
68
|
+
* // use this parameter only for values that vary per request.
|
|
69
|
+
* await graphql.query(
|
|
70
|
+
* `query { me { id } }`,
|
|
71
|
+
* { response: z.object({ data: z.object({ me: z.object({ id: z.string() }) }) }) },
|
|
72
|
+
* undefined,
|
|
73
|
+
* undefined,
|
|
74
|
+
* { Authorization: `Bearer ${token}` },
|
|
75
|
+
* );
|
|
65
76
|
* ```
|
|
66
77
|
*/
|
|
67
78
|
export interface GraphQLClient extends BaseIntegrationClient {
|
|
@@ -74,6 +85,10 @@ export interface GraphQLClient extends BaseIntegrationClient {
|
|
|
74
85
|
* `metadata` accept plain objects, pass `undefined` for variables when you only need metadata:
|
|
75
86
|
* `query(q, schema, undefined, { label: "..." })`
|
|
76
87
|
* @param metadata - Optional trace metadata for observability
|
|
88
|
+
* @param headers - Optional HTTP headers to include on this request. Static
|
|
89
|
+
* headers and auth configured on the integration apply automatically; use
|
|
90
|
+
* this parameter only for values that vary per request (e.g. a bearer token
|
|
91
|
+
* derived from the API's input).
|
|
77
92
|
* @returns Validated query result
|
|
78
93
|
*
|
|
79
94
|
* @example
|
|
@@ -95,6 +110,7 @@ export interface GraphQLClient extends BaseIntegrationClient {
|
|
|
95
110
|
schema: { response: z.ZodSchema<TResponse> },
|
|
96
111
|
variables?: Record<string, unknown>,
|
|
97
112
|
metadata?: TraceMetadata,
|
|
113
|
+
headers?: Record<string, string>,
|
|
98
114
|
): Promise<TResponse>;
|
|
99
115
|
|
|
100
116
|
/**
|
|
@@ -106,6 +122,10 @@ export interface GraphQLClient extends BaseIntegrationClient {
|
|
|
106
122
|
* `metadata` accept plain objects, pass `undefined` for variables when you only need metadata:
|
|
107
123
|
* `mutation(m, schema, undefined, { label: "..." })`
|
|
108
124
|
* @param metadata - Optional trace metadata for observability
|
|
125
|
+
* @param headers - Optional HTTP headers to include on this request. Static
|
|
126
|
+
* headers and auth configured on the integration apply automatically; use
|
|
127
|
+
* this parameter only for values that vary per request (e.g. a bearer token
|
|
128
|
+
* derived from the API's input).
|
|
109
129
|
* @returns Validated mutation result
|
|
110
130
|
*
|
|
111
131
|
* @example
|
|
@@ -128,5 +148,6 @@ export interface GraphQLClient extends BaseIntegrationClient {
|
|
|
128
148
|
schema: { response: z.ZodSchema<TResponse> },
|
|
129
149
|
variables?: Record<string, unknown>,
|
|
130
150
|
metadata?: TraceMetadata,
|
|
151
|
+
headers?: Record<string, string>,
|
|
131
152
|
): Promise<TResponse>;
|
|
132
153
|
}
|