@glubean/graphql 0.1.3

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/data.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Data loaders for GraphQL files.
3
+ *
4
+ * @module data
5
+ */
6
+ /**
7
+ * Load a GraphQL query from a `.gql` or `.graphql` file.
8
+ *
9
+ * Using external `.gql` files instead of inline strings enables full IDE
10
+ * support: syntax highlighting, field autocomplete, and schema validation
11
+ * (when a `.graphqlrc` config points to your schema).
12
+ *
13
+ * @param path Path to the `.gql` / `.graphql` file, relative to project root
14
+ * @returns The query string (trimmed)
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { fromGql } from "@glubean/graphql/data";
19
+ * import { graphql } from "@glubean/graphql";
20
+ * import { test, configure } from "@glubean/sdk";
21
+ *
22
+ * const GetUser = await fromGql("./queries/getUser.gql");
23
+ *
24
+ * const { gql } = configure({
25
+ * plugins: {
26
+ * gql: graphql({ endpoint: "{{graphql_url}}" }),
27
+ * },
28
+ * });
29
+ *
30
+ * export const getUser = test("get-user", async (ctx) => {
31
+ * const { data } = await gql.query(GetUser, { variables: { id: "1" } });
32
+ * ctx.expect(data?.user.name).toBe("Alice");
33
+ * });
34
+ * ```
35
+ */
36
+ export declare function fromGql(path: string): Promise<string>;
37
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG3D"}
package/dist/data.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Data loaders for GraphQL files.
3
+ *
4
+ * @module data
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ /**
8
+ * Load a GraphQL query from a `.gql` or `.graphql` file.
9
+ *
10
+ * Using external `.gql` files instead of inline strings enables full IDE
11
+ * support: syntax highlighting, field autocomplete, and schema validation
12
+ * (when a `.graphqlrc` config points to your schema).
13
+ *
14
+ * @param path Path to the `.gql` / `.graphql` file, relative to project root
15
+ * @returns The query string (trimmed)
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { fromGql } from "@glubean/graphql/data";
20
+ * import { graphql } from "@glubean/graphql";
21
+ * import { test, configure } from "@glubean/sdk";
22
+ *
23
+ * const GetUser = await fromGql("./queries/getUser.gql");
24
+ *
25
+ * const { gql } = configure({
26
+ * plugins: {
27
+ * gql: graphql({ endpoint: "{{graphql_url}}" }),
28
+ * },
29
+ * });
30
+ *
31
+ * export const getUser = test("get-user", async (ctx) => {
32
+ * const { data } = await gql.query(GetUser, { variables: { id: "1" } });
33
+ * ctx.expect(data?.user.name).toBe("Alice");
34
+ * });
35
+ * ```
36
+ */
37
+ export async function fromGql(path) {
38
+ const content = await readFile(path, "utf-8");
39
+ return content.trim();
40
+ }
41
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.js","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC9C,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,325 @@
1
+ /**
2
+ * GraphQL plugin for Glubean tests.
3
+ *
4
+ * Provides a thin wrapper over `ctx.http` (or any `HttpClient`) that simplifies
5
+ * GraphQL query/mutation execution with auto-tracing, operationName extraction,
6
+ * and optional error-throwing behavior.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * ### As a plugin via `configure()` (recommended)
11
+ *
12
+ * ```ts
13
+ * import { test, configure } from "@glubean/sdk";
14
+ * import { graphql } from "@glubean/graphql";
15
+ *
16
+ * const { gql } = configure({
17
+ * plugins: {
18
+ * gql: graphql({
19
+ * endpoint: "{{graphql_url}}",
20
+ * headers: { Authorization: "Bearer {{api_key}}" },
21
+ * }),
22
+ * },
23
+ * });
24
+ *
25
+ * export const getUser = test("get-user", async (ctx) => {
26
+ * const { data, errors } = await gql.query<{ user: { name: string } }>(`
27
+ * query GetUser($id: ID!) {
28
+ * user(id: $id) { name }
29
+ * }
30
+ * `, { variables: { id: "1" } });
31
+ *
32
+ * ctx.expect(errors).toBeUndefined();
33
+ * ctx.expect(data?.user.name).toBe("Alice");
34
+ * });
35
+ * ```
36
+ *
37
+ * ### Standalone (without `configure()`)
38
+ *
39
+ * ```ts
40
+ * import { test } from "@glubean/sdk";
41
+ * import { createGraphQLClient } from "@glubean/graphql";
42
+ *
43
+ * export const quick = test("quick-gql", async (ctx) => {
44
+ * const gql = createGraphQLClient(ctx.http, {
45
+ * endpoint: "https://api.example.com/graphql",
46
+ * });
47
+ * const { data } = await gql.query(`{ health }`);
48
+ * ctx.assert(data?.health === "ok", "Service healthy");
49
+ * });
50
+ * ```
51
+ *
52
+ * @module graphql
53
+ */
54
+ import type { HttpClient, PluginFactory } from "@glubean/sdk";
55
+ /**
56
+ * A single GraphQL error as defined by the
57
+ * [GraphQL spec](https://spec.graphql.org/October2021/#sec-Errors).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // Typical error shape from a GraphQL server
62
+ * {
63
+ * message: "User not found",
64
+ * locations: [{ line: 2, column: 3 }],
65
+ * path: ["user"],
66
+ * extensions: { code: "NOT_FOUND" }
67
+ * }
68
+ * ```
69
+ */
70
+ export interface GraphQLError {
71
+ /** Human-readable error description */
72
+ message: string;
73
+ /** Source locations in the query that caused this error */
74
+ locations?: {
75
+ line: number;
76
+ column: number;
77
+ }[];
78
+ /** Path to the field that produced the error */
79
+ path?: (string | number)[];
80
+ /** Server-defined extension data (e.g., error codes) */
81
+ extensions?: Record<string, unknown>;
82
+ }
83
+ /**
84
+ * Standard GraphQL response envelope.
85
+ *
86
+ * Per the spec, `data` is `null` when all requested fields errored,
87
+ * and `errors` is absent when there are no errors.
88
+ *
89
+ * @template T Shape of the `data` field
90
+ */
91
+ export interface GraphQLResponse<T = unknown> {
92
+ /** The result of the query/mutation. `null` if all fields errored. */
93
+ data: T | null;
94
+ /** Array of errors, if any. Absent when no errors. */
95
+ errors?: GraphQLError[];
96
+ /** Optional server extensions (e.g., tracing, cost). */
97
+ extensions?: Record<string, unknown>;
98
+ }
99
+ /**
100
+ * Options for a single GraphQL request.
101
+ */
102
+ export interface GraphQLRequestOptions {
103
+ /** Variables to pass to the query/mutation */
104
+ variables?: Record<string, unknown>;
105
+ /**
106
+ * Explicit operationName. If omitted, the client parses the first named
107
+ * operation from the query string (e.g., `query GetUser` -> `"GetUser"`).
108
+ */
109
+ operationName?: string;
110
+ /** Additional headers for this request only */
111
+ headers?: Record<string, string>;
112
+ }
113
+ /**
114
+ * Options for creating a GraphQL client.
115
+ */
116
+ export interface GraphQLClientOptions {
117
+ /** The GraphQL endpoint URL (e.g., "https://api.example.com/graphql") */
118
+ endpoint: string;
119
+ /** Default headers sent with every request */
120
+ headers?: Record<string, string>;
121
+ /**
122
+ * If `true`, the client throws a `GraphQLResponseError` when the response
123
+ * contains `errors`, even though the HTTP status is 200.
124
+ *
125
+ * Default: `false` -- errors are returned in the response object.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const gql = createGraphQLClient(ctx.http, {
130
+ * endpoint: "https://api.example.com/graphql",
131
+ * throwOnGraphQLErrors: true,
132
+ * });
133
+ *
134
+ * try {
135
+ * const { data } = await gql.query(`{ me { name } }`);
136
+ * } catch (err) {
137
+ * if (err instanceof GraphQLResponseError) {
138
+ * console.log(err.errors); // GraphQLError[]
139
+ * }
140
+ * }
141
+ * ```
142
+ */
143
+ throwOnGraphQLErrors?: boolean;
144
+ }
145
+ /**
146
+ * Error thrown when a GraphQL response contains errors and
147
+ * `throwOnGraphQLErrors` is enabled.
148
+ */
149
+ export declare class GraphQLResponseError extends Error {
150
+ /** The GraphQL errors from the response */
151
+ readonly errors: GraphQLError[];
152
+ /** The original response (may contain partial `data`) */
153
+ readonly response: GraphQLResponse;
154
+ constructor(errors: GraphQLError[], response: GraphQLResponse);
155
+ }
156
+ /**
157
+ * A GraphQL client bound to a specific endpoint.
158
+ *
159
+ * All requests are auto-traced via the underlying `HttpClient`.
160
+ * The operation name is injected into traces via the `X-Glubean-Op` header,
161
+ * so the dashboard can distinguish between different GraphQL operations
162
+ * instead of showing a generic `POST /graphql`.
163
+ */
164
+ export interface GraphQLClient {
165
+ /**
166
+ * Execute a GraphQL query.
167
+ *
168
+ * @template T Shape of the `data` field
169
+ * @param query The GraphQL query string
170
+ * @param options Variables, operationName, and extra headers
171
+ * @returns The parsed GraphQL response
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const { data, errors } = await gql.query<{ users: User[] }>(`
176
+ * query ListUsers($limit: Int) {
177
+ * users(limit: $limit) { id name }
178
+ * }
179
+ * `, { variables: { limit: 10 } });
180
+ * ```
181
+ */
182
+ query<T = unknown>(query: string, options?: GraphQLRequestOptions): Promise<GraphQLResponse<T>>;
183
+ /**
184
+ * Execute a GraphQL mutation.
185
+ *
186
+ * Functionally identical to `query()` -- the distinction is purely semantic
187
+ * to improve readability in test code.
188
+ *
189
+ * @template T Shape of the `data` field
190
+ * @param mutation The GraphQL mutation string
191
+ * @param options Variables, operationName, and extra headers
192
+ * @returns The parsed GraphQL response
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * const { data } = await gql.mutate<{ createUser: { id: string } }>(`
197
+ * mutation CreateUser($input: CreateUserInput!) {
198
+ * createUser(input: $input) { id }
199
+ * }
200
+ * `, { variables: { input: { name: "Alice" } } });
201
+ * ```
202
+ */
203
+ mutate<T = unknown>(mutation: string, options?: GraphQLRequestOptions): Promise<GraphQLResponse<T>>;
204
+ }
205
+ /**
206
+ * Extract the first named operation from a GraphQL query string.
207
+ *
208
+ * Matches patterns like:
209
+ * - `query GetUser` -> "GetUser"
210
+ * - `mutation CreateUser` -> "CreateUser"
211
+ * - `subscription OnMessage` -> "OnMessage"
212
+ * - `query GetUser($id: ID!)` -> "GetUser"
213
+ *
214
+ * Returns `undefined` for anonymous operations (e.g., `{ users { id } }`).
215
+ *
216
+ * @internal
217
+ */
218
+ export declare function parseOperationName(query: string): string | undefined;
219
+ /**
220
+ * Tagged template literal for GraphQL queries.
221
+ *
222
+ * This is an identity function -- it returns the query string as-is.
223
+ * Its purpose is purely for IDE integration: the VSCode GraphQL extension
224
+ * recognizes the `gql` tag and enables syntax highlighting (and autocomplete
225
+ * when a `.graphqlrc` schema is configured).
226
+ *
227
+ * For full IDE support (autocomplete, validation), prefer `.gql` files
228
+ * loaded via `fromGql()`. Use `gql` for quick inline queries where a
229
+ * separate file would be overkill.
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * import { gql } from "@glubean/graphql";
234
+ *
235
+ * const GET_USER = gql`
236
+ * query GetUser($id: ID!) {
237
+ * user(id: $id) { name email }
238
+ * }
239
+ * `;
240
+ *
241
+ * const { data } = await client.query(GET_USER, { variables: { id: "1" } });
242
+ * ```
243
+ */
244
+ export declare function gql(strings: TemplateStringsArray, ...values: unknown[]): string;
245
+ /**
246
+ * Create a GraphQL client bound to a specific endpoint.
247
+ *
248
+ * The client wraps the provided `HttpClient` (typically `ctx.http`), so all
249
+ * requests inherit auto-tracing, auto-metrics, and retry behavior.
250
+ *
251
+ * The operation name is injected via the `X-Glubean-Op` request header.
252
+ * The runner's harness reads this header to set `trace.name`, making
253
+ * individual GraphQL operations distinguishable in the dashboard.
254
+ *
255
+ * @param http The base HTTP client (e.g., `ctx.http`)
256
+ * @param options Endpoint URL, default headers, error handling
257
+ * @returns A bound `GraphQLClient` instance
258
+ *
259
+ * @example Basic usage
260
+ * ```ts
261
+ * import { createGraphQLClient } from "@glubean/graphql";
262
+ *
263
+ * export const myTest = test("gql-test", async (ctx) => {
264
+ * const gql = createGraphQLClient(ctx.http, {
265
+ * endpoint: ctx.vars.require("GQL_URL"),
266
+ * headers: { Authorization: `Bearer ${ctx.secrets.require("TOKEN")}` },
267
+ * });
268
+ *
269
+ * const { data } = await gql.query<{ user: { name: string } }>(`
270
+ * query GetUser($id: ID!) { user(id: $id) { name } }
271
+ * `, { variables: { id: "1" } });
272
+ *
273
+ * ctx.expect(data?.user.name).toBe("Alice");
274
+ * });
275
+ * ```
276
+ *
277
+ * @example With throwOnGraphQLErrors
278
+ * ```ts
279
+ * const gql = createGraphQLClient(ctx.http, {
280
+ * endpoint: ctx.vars.require("GQL_URL"),
281
+ * throwOnGraphQLErrors: true,
282
+ * });
283
+ *
284
+ * // This will throw GraphQLResponseError if the response contains errors
285
+ * const { data } = await gql.query(`{ me { name } }`);
286
+ * ```
287
+ */
288
+ export declare function createGraphQLClient(http: HttpClient, options: GraphQLClientOptions): GraphQLClient;
289
+ /**
290
+ * Create a GraphQL plugin for use with `configure({ plugins })`.
291
+ *
292
+ * Resolves `{{template}}` placeholders in `endpoint` and `headers` using
293
+ * the Glubean runtime (vars and secrets). The returned `GraphQLClient` is
294
+ * lazily created on first access.
295
+ *
296
+ * @param options GraphQL client options (endpoint may use `{{var_name}}` templates)
297
+ * @returns A `PluginFactory` that produces a `GraphQLClient`
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * import { test, configure } from "@glubean/sdk";
302
+ * import { graphql } from "@glubean/graphql";
303
+ *
304
+ * const { gql } = configure({
305
+ * plugins: {
306
+ * gql: graphql({
307
+ * endpoint: "{{graphql_url}}",
308
+ * headers: { Authorization: "Bearer {{api_key}}" },
309
+ * throwOnGraphQLErrors: true,
310
+ * }),
311
+ * },
312
+ * });
313
+ *
314
+ * export const getUser = test("get-user", async (ctx) => {
315
+ * const { data } = await gql.query<{ user: { name: string } }>(
316
+ * `query GetUser($id: ID!) { user(id: $id) { name } }`,
317
+ * { variables: { id: "1" } },
318
+ * );
319
+ * ctx.expect(data?.user.name).toBe("Alice");
320
+ * });
321
+ * ```
322
+ */
323
+ export declare function graphql(options: GraphQLClientOptions): PluginFactory<GraphQLClient>;
324
+ export { fromGql } from "./data.js";
325
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * GraphQL plugin for Glubean tests.
3
+ *
4
+ * Provides a thin wrapper over `ctx.http` (or any `HttpClient`) that simplifies
5
+ * GraphQL query/mutation execution with auto-tracing, operationName extraction,
6
+ * and optional error-throwing behavior.
7
+ *
8
+ * ## Usage
9
+ *
10
+ * ### As a plugin via `configure()` (recommended)
11
+ *
12
+ * ```ts
13
+ * import { test, configure } from "@glubean/sdk";
14
+ * import { graphql } from "@glubean/graphql";
15
+ *
16
+ * const { gql } = configure({
17
+ * plugins: {
18
+ * gql: graphql({
19
+ * endpoint: "{{graphql_url}}",
20
+ * headers: { Authorization: "Bearer {{api_key}}" },
21
+ * }),
22
+ * },
23
+ * });
24
+ *
25
+ * export const getUser = test("get-user", async (ctx) => {
26
+ * const { data, errors } = await gql.query<{ user: { name: string } }>(`
27
+ * query GetUser($id: ID!) {
28
+ * user(id: $id) { name }
29
+ * }
30
+ * `, { variables: { id: "1" } });
31
+ *
32
+ * ctx.expect(errors).toBeUndefined();
33
+ * ctx.expect(data?.user.name).toBe("Alice");
34
+ * });
35
+ * ```
36
+ *
37
+ * ### Standalone (without `configure()`)
38
+ *
39
+ * ```ts
40
+ * import { test } from "@glubean/sdk";
41
+ * import { createGraphQLClient } from "@glubean/graphql";
42
+ *
43
+ * export const quick = test("quick-gql", async (ctx) => {
44
+ * const gql = createGraphQLClient(ctx.http, {
45
+ * endpoint: "https://api.example.com/graphql",
46
+ * });
47
+ * const { data } = await gql.query(`{ health }`);
48
+ * ctx.assert(data?.health === "ok", "Service healthy");
49
+ * });
50
+ * ```
51
+ *
52
+ * @module graphql
53
+ */
54
+ import { definePlugin } from "@glubean/sdk/plugin";
55
+ /**
56
+ * Error thrown when a GraphQL response contains errors and
57
+ * `throwOnGraphQLErrors` is enabled.
58
+ */
59
+ export class GraphQLResponseError extends Error {
60
+ /** The GraphQL errors from the response */
61
+ errors;
62
+ /** The original response (may contain partial `data`) */
63
+ response;
64
+ constructor(errors, response) {
65
+ const summary = errors.map((e) => e.message).join("; ");
66
+ super(`GraphQL errors: ${summary}`);
67
+ this.name = "GraphQLResponseError";
68
+ this.errors = errors;
69
+ this.response = response;
70
+ }
71
+ }
72
+ // =============================================================================
73
+ // Helpers
74
+ // =============================================================================
75
+ /**
76
+ * Extract the first named operation from a GraphQL query string.
77
+ *
78
+ * Matches patterns like:
79
+ * - `query GetUser` -> "GetUser"
80
+ * - `mutation CreateUser` -> "CreateUser"
81
+ * - `subscription OnMessage` -> "OnMessage"
82
+ * - `query GetUser($id: ID!)` -> "GetUser"
83
+ *
84
+ * Returns `undefined` for anonymous operations (e.g., `{ users { id } }`).
85
+ *
86
+ * @internal
87
+ */
88
+ export function parseOperationName(query) {
89
+ const match = query.match(/(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/);
90
+ return match?.[1];
91
+ }
92
+ // =============================================================================
93
+ // Tagged template
94
+ // =============================================================================
95
+ /**
96
+ * Tagged template literal for GraphQL queries.
97
+ *
98
+ * This is an identity function -- it returns the query string as-is.
99
+ * Its purpose is purely for IDE integration: the VSCode GraphQL extension
100
+ * recognizes the `gql` tag and enables syntax highlighting (and autocomplete
101
+ * when a `.graphqlrc` schema is configured).
102
+ *
103
+ * For full IDE support (autocomplete, validation), prefer `.gql` files
104
+ * loaded via `fromGql()`. Use `gql` for quick inline queries where a
105
+ * separate file would be overkill.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { gql } from "@glubean/graphql";
110
+ *
111
+ * const GET_USER = gql`
112
+ * query GetUser($id: ID!) {
113
+ * user(id: $id) { name email }
114
+ * }
115
+ * `;
116
+ *
117
+ * const { data } = await client.query(GET_USER, { variables: { id: "1" } });
118
+ * ```
119
+ */
120
+ export function gql(strings, ...values) {
121
+ let result = strings[0];
122
+ for (let i = 0; i < values.length; i++) {
123
+ result += String(values[i]) + strings[i + 1];
124
+ }
125
+ return result.replace(/\s+/g, " ").trim();
126
+ }
127
+ // =============================================================================
128
+ // Client implementation
129
+ // =============================================================================
130
+ /**
131
+ * Create a GraphQL client bound to a specific endpoint.
132
+ *
133
+ * The client wraps the provided `HttpClient` (typically `ctx.http`), so all
134
+ * requests inherit auto-tracing, auto-metrics, and retry behavior.
135
+ *
136
+ * The operation name is injected via the `X-Glubean-Op` request header.
137
+ * The runner's harness reads this header to set `trace.name`, making
138
+ * individual GraphQL operations distinguishable in the dashboard.
139
+ *
140
+ * @param http The base HTTP client (e.g., `ctx.http`)
141
+ * @param options Endpoint URL, default headers, error handling
142
+ * @returns A bound `GraphQLClient` instance
143
+ *
144
+ * @example Basic usage
145
+ * ```ts
146
+ * import { createGraphQLClient } from "@glubean/graphql";
147
+ *
148
+ * export const myTest = test("gql-test", async (ctx) => {
149
+ * const gql = createGraphQLClient(ctx.http, {
150
+ * endpoint: ctx.vars.require("GQL_URL"),
151
+ * headers: { Authorization: `Bearer ${ctx.secrets.require("TOKEN")}` },
152
+ * });
153
+ *
154
+ * const { data } = await gql.query<{ user: { name: string } }>(`
155
+ * query GetUser($id: ID!) { user(id: $id) { name } }
156
+ * `, { variables: { id: "1" } });
157
+ *
158
+ * ctx.expect(data?.user.name).toBe("Alice");
159
+ * });
160
+ * ```
161
+ *
162
+ * @example With throwOnGraphQLErrors
163
+ * ```ts
164
+ * const gql = createGraphQLClient(ctx.http, {
165
+ * endpoint: ctx.vars.require("GQL_URL"),
166
+ * throwOnGraphQLErrors: true,
167
+ * });
168
+ *
169
+ * // This will throw GraphQLResponseError if the response contains errors
170
+ * const { data } = await gql.query(`{ me { name } }`);
171
+ * ```
172
+ */
173
+ export function createGraphQLClient(http, options) {
174
+ const { endpoint, headers: defaultHeaders, throwOnGraphQLErrors } = options;
175
+ async function execute(query, requestOptions) {
176
+ const opName = requestOptions?.operationName ?? parseOperationName(query) ?? "anonymous";
177
+ const mergedHeaders = {
178
+ ...defaultHeaders,
179
+ ...requestOptions?.headers,
180
+ "X-Glubean-Op": opName,
181
+ };
182
+ const body = { query };
183
+ if (requestOptions?.variables) {
184
+ body.variables = requestOptions.variables;
185
+ }
186
+ if (opName !== "anonymous") {
187
+ body.operationName = opName;
188
+ }
189
+ const response = await http
190
+ .post(endpoint, {
191
+ json: body,
192
+ headers: mergedHeaders,
193
+ throwHttpErrors: false,
194
+ })
195
+ .json();
196
+ if (throwOnGraphQLErrors && response.errors && response.errors.length > 0) {
197
+ throw new GraphQLResponseError(response.errors, response);
198
+ }
199
+ return response;
200
+ }
201
+ return {
202
+ query: (query, options) => execute(query, options),
203
+ mutate: (mutation, options) => execute(mutation, options),
204
+ };
205
+ }
206
+ // =============================================================================
207
+ // Plugin factory
208
+ // =============================================================================
209
+ /**
210
+ * Create a GraphQL plugin for use with `configure({ plugins })`.
211
+ *
212
+ * Resolves `{{template}}` placeholders in `endpoint` and `headers` using
213
+ * the Glubean runtime (vars and secrets). The returned `GraphQLClient` is
214
+ * lazily created on first access.
215
+ *
216
+ * @param options GraphQL client options (endpoint may use `{{var_name}}` templates)
217
+ * @returns A `PluginFactory` that produces a `GraphQLClient`
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * import { test, configure } from "@glubean/sdk";
222
+ * import { graphql } from "@glubean/graphql";
223
+ *
224
+ * const { gql } = configure({
225
+ * plugins: {
226
+ * gql: graphql({
227
+ * endpoint: "{{graphql_url}}",
228
+ * headers: { Authorization: "Bearer {{api_key}}" },
229
+ * throwOnGraphQLErrors: true,
230
+ * }),
231
+ * },
232
+ * });
233
+ *
234
+ * export const getUser = test("get-user", async (ctx) => {
235
+ * const { data } = await gql.query<{ user: { name: string } }>(
236
+ * `query GetUser($id: ID!) { user(id: $id) { name } }`,
237
+ * { variables: { id: "1" } },
238
+ * );
239
+ * ctx.expect(data?.user.name).toBe("Alice");
240
+ * });
241
+ * ```
242
+ */
243
+ export function graphql(options) {
244
+ return definePlugin((runtime) => {
245
+ const resolved = {
246
+ ...options,
247
+ endpoint: runtime.resolveTemplate(options.endpoint),
248
+ };
249
+ if (options.headers) {
250
+ resolved.headers = {};
251
+ for (const [k, v] of Object.entries(options.headers)) {
252
+ resolved.headers[k] = runtime.resolveTemplate(v);
253
+ }
254
+ }
255
+ return createGraphQLClient(runtime.http, resolved);
256
+ });
257
+ }
258
+ // Re-export data loader for convenience (spec: `import { fromGql } from "@glubean/graphql"`)
259
+ export { fromGql } from "./data.js";
260
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@glubean/graphql",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ },
10
+ "./data": {
11
+ "types": "./dist/data.d.ts",
12
+ "import": "./dist/data.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@glubean/sdk": "0.1.3"
20
+ },
21
+ "devDependencies": {
22
+ "vitest": "^3.2.1",
23
+ "typescript": "^5.8.3"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "test": "vitest run"
28
+ }
29
+ }