@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.
@@ -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
+ });
@@ -1,5 +1,10 @@
1
1
  {
2
2
  "pluginId": "graphql",
3
3
  "base": "README.md",
4
- "overlays": []
4
+ "overlays": [
5
+ {
6
+ "file": "overlays/dynamic-headers.md",
7
+ "sdkVersionRange": ">=0.0.2"
8
+ }
9
+ ]
5
10
  }
@@ -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
  }