@glubean/graphql 0.1.6 → 0.2.1
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/README.md +433 -0
- package/dist/contract/adapter.d.ts +27 -0
- package/dist/contract/adapter.d.ts.map +1 -0
- package/dist/contract/adapter.js +543 -0
- package/dist/contract/adapter.js.map +1 -0
- package/dist/contract/factory.d.ts +34 -0
- package/dist/contract/factory.d.ts.map +1 -0
- package/dist/contract/factory.js +90 -0
- package/dist/contract/factory.js.map +1 -0
- package/dist/contract/index.d.ts +37 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +55 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/matchers.d.ts +95 -0
- package/dist/contract/matchers.d.ts.map +1 -0
- package/dist/contract/matchers.js +246 -0
- package/dist/contract/matchers.js.map +1 -0
- package/dist/contract/types.d.ts +369 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +41 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/package.json +13 -3
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# @glubean/graphql
|
|
2
|
+
|
|
3
|
+
GraphQL for [Glubean](https://glubean.dev). This package owns two layers:
|
|
4
|
+
|
|
5
|
+
- **Contract** — author GraphQL API intent as a single artifact (`contract.graphql.with(...)`). Executable spec, agent-readable, fits `contract.flow()` composition. **Recommended for new work.**
|
|
6
|
+
- **Transport / test plugin** — thin wrapper over `ctx.http` with operation-name tracing, used via `configure({ plugins: { ... } })` or `createGraphQLClient(...)`. Still supported for test-after / exploratory work.
|
|
7
|
+
|
|
8
|
+
> **v0.2.0 single-package release note:** in earlier drafts, GraphQL contract was planned as a separate `@glubean/contract-graphql` package. Decision 2026-04-20: one package per protocol. Importing `@glubean/graphql` now registers the contract adapter as a side effect. No new install, no second package.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @glubean/graphql
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
No native peer dependencies — the client runs over `ctx.http` (ky).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick Start — Contract
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { contract, configure } from "@glubean/sdk";
|
|
24
|
+
import { graphql, gql } from "@glubean/graphql";
|
|
25
|
+
// ^ importing @glubean/graphql registers contract.graphql side-effect
|
|
26
|
+
|
|
27
|
+
const { api } = configure({
|
|
28
|
+
plugins: {
|
|
29
|
+
api: graphql({
|
|
30
|
+
endpoint: "{{GRAPHQL_URL}}",
|
|
31
|
+
headers: { Authorization: "Bearer {{API_TOKEN}}" },
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const userContracts = contract.graphql.with("user-api", {
|
|
37
|
+
client: api,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const getUser = userContracts("get-user", {
|
|
41
|
+
endpoint: "/graphql",
|
|
42
|
+
description: "Fetch a user by id",
|
|
43
|
+
cases: {
|
|
44
|
+
happy: {
|
|
45
|
+
description: "existing user returns name + email",
|
|
46
|
+
query: gql`
|
|
47
|
+
query GetUser($id: ID!) {
|
|
48
|
+
user(id: $id) { id name email }
|
|
49
|
+
}
|
|
50
|
+
`,
|
|
51
|
+
variables: { id: "u_123" },
|
|
52
|
+
expect: {
|
|
53
|
+
httpStatus: 200,
|
|
54
|
+
data: { user: { id: "u_123", name: "Alice" } },
|
|
55
|
+
errors: "absent",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
unauth: {
|
|
59
|
+
description: "missing token yields 401",
|
|
60
|
+
query: `query Me { me { id } }`,
|
|
61
|
+
headers: {},
|
|
62
|
+
expect: { httpStatus: 401, errors: "any" },
|
|
63
|
+
},
|
|
64
|
+
forbidden: {
|
|
65
|
+
description: "server returns FORBIDDEN on scope mismatch",
|
|
66
|
+
query: `query AdminOnly { admin { key } }`,
|
|
67
|
+
expect: {
|
|
68
|
+
httpStatus: 200,
|
|
69
|
+
errors: [{ extensions: { code: "FORBIDDEN" } }],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run with `glubean run`. Each case becomes a first-class test; failure surfaces HTTP status + GraphQL errors on the trace.
|
|
77
|
+
|
|
78
|
+
### Cases in a flow
|
|
79
|
+
|
|
80
|
+
Contract cases compose into `contract.flow()` steps — the same artifact serves both single-case and multi-step verification, **across protocols**:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { contract } from "@glubean/sdk";
|
|
84
|
+
import { createOrder } from "./orders.contract.ts"; // HTTP
|
|
85
|
+
import { completePayment } from "./payment.contract.ts"; // gRPC
|
|
86
|
+
import { notifyUser } from "./notify.contract.ts"; // GraphQL
|
|
87
|
+
|
|
88
|
+
export const checkoutFlow = contract
|
|
89
|
+
.flow("checkout-with-notify")
|
|
90
|
+
.meta({
|
|
91
|
+
description: "Create order → complete payment → notify user",
|
|
92
|
+
tags: ["e2e"],
|
|
93
|
+
})
|
|
94
|
+
// Step 1: HTTP — create order
|
|
95
|
+
.step(createOrder.case("happy"), {
|
|
96
|
+
out: (_s, res: any) => ({ orderId: res.body.id, userId: res.body.userId }),
|
|
97
|
+
})
|
|
98
|
+
// Step 2: gRPC — complete payment
|
|
99
|
+
.step(completePayment.case("happy"), {
|
|
100
|
+
in: (s: any) => ({ request: { orderId: s.orderId } }),
|
|
101
|
+
out: (s, res: any) => ({ ...s, paymentId: res.message.paymentId }),
|
|
102
|
+
})
|
|
103
|
+
// Step 3: GraphQL — notify
|
|
104
|
+
.step(notifyUser.case("orderComplete"), {
|
|
105
|
+
in: (s: any) => ({ variables: { userId: s.userId, orderId: s.orderId } }),
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Flow state threads through via typed `in` / `out` lenses.
|
|
110
|
+
|
|
111
|
+
### What you get
|
|
112
|
+
|
|
113
|
+
- **Selection-set-per-case** — each case owns its own `query` and response schema (`expect.schema` or partial `data`). Contract-level types declaration is optional (`types: { User: { id: "ID!", ... } }`, Phase 2 projection hint).
|
|
114
|
+
- **Case-level lifecycle** — mark cases `deferred` (with reason) or `deprecated` (with replacement hint).
|
|
115
|
+
- **Structured failure classification (3-layer)** — HTTP transport (4xx/5xx) → payload `errors` (with `extensions.code`) → error shape. Maps to `transient` / `client` / `semantic` / `auth` / `server` kinds; `429` / `503` / `504` marked `retryable`.
|
|
116
|
+
- **Envelope exposure** — `GraphqlCaseResult` surfaces `httpStatus`, `headers`, `rawBody` alongside `data` / `errors` for negative-case assertions and flow `out` lens inspection.
|
|
117
|
+
- **Projection to Markdown** — case inventory with operation / `operationName` / query snippets, via `glubean contracts`.
|
|
118
|
+
- **Flow composition** — mix with HTTP / gRPC cases, same artifact.
|
|
119
|
+
- **Scanner + MCP integration** — `glubean scan`, `glubean_extract_contracts` MCP tool, all work unchanged for `contract.graphql(...)`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Quick Start — Transport / test plugin (low-level)
|
|
124
|
+
|
|
125
|
+
For quick tests or exploratory work that doesn't need a declared contract:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { test, configure } from "@glubean/sdk";
|
|
129
|
+
import { graphql } from "@glubean/graphql";
|
|
130
|
+
|
|
131
|
+
const { gql } = configure({
|
|
132
|
+
plugins: {
|
|
133
|
+
gql: graphql({
|
|
134
|
+
endpoint: "{{GRAPHQL_URL}}",
|
|
135
|
+
headers: { Authorization: "Bearer {{API_TOKEN}}" },
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const getUser = test("get-user", async (ctx) => {
|
|
141
|
+
const { data, errors } = await gql.query<{ user: { name: string } }>(`
|
|
142
|
+
query GetUser($id: ID!) { user(id: $id) { name } }
|
|
143
|
+
`, { variables: { id: "u_123" } });
|
|
144
|
+
|
|
145
|
+
ctx.expect(errors).toBeUndefined();
|
|
146
|
+
ctx.expect(data?.user.name).toBe("Alice");
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Standalone (without `configure()`)
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { test } from "@glubean/sdk";
|
|
154
|
+
import { createGraphQLClient } from "@glubean/graphql";
|
|
155
|
+
|
|
156
|
+
export const quick = test("quick-gql", async (ctx) => {
|
|
157
|
+
const gql = createGraphQLClient(ctx.http, {
|
|
158
|
+
endpoint: "https://api.example.com/graphql",
|
|
159
|
+
});
|
|
160
|
+
const res = await gql.query(`{ health }`);
|
|
161
|
+
ctx.assert(res.data?.health === "ok", "Service healthy");
|
|
162
|
+
ctx.expect(res.httpStatus).toBe(200);
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## API Reference
|
|
169
|
+
|
|
170
|
+
### Contract
|
|
171
|
+
|
|
172
|
+
#### `contract.graphql.with(instanceName, defaults?)`
|
|
173
|
+
|
|
174
|
+
Returns a scoped factory. Direct `contract.graphql("id", spec)` is not supported — use `.with(...)` first.
|
|
175
|
+
|
|
176
|
+
Instance defaults (`GraphqlContractDefaults`):
|
|
177
|
+
|
|
178
|
+
| Option | Type | Description |
|
|
179
|
+
|--------|------|-------------|
|
|
180
|
+
| `client` | `GraphQLClient` | Default client (from `configure({ plugins })`) |
|
|
181
|
+
| `endpoint` | `string` | **Projection-only.** Travels on `meta.endpoint` for markdown / scanner / MCP display. Does NOT redirect the runtime call — the call goes through `client`, whose endpoint is fixed at construction. Multi-endpoint = multiple clients. |
|
|
182
|
+
| `tags` | `string[]` | Tags inherited by all contracts in this instance |
|
|
183
|
+
| `feature` | `string` | Grouping key for projection |
|
|
184
|
+
| `headers` | `Record<string, string>` | Default headers merged into every case |
|
|
185
|
+
| `extensions` | `Extensions` | Projection-level extensions (x-* keys) |
|
|
186
|
+
|
|
187
|
+
#### `contract.graphql.with(...)("contractId", spec)`
|
|
188
|
+
|
|
189
|
+
Creates one contract. Spec shape (`GraphqlContractSpec`):
|
|
190
|
+
|
|
191
|
+
| Field | Type | Description |
|
|
192
|
+
|-------|------|-------------|
|
|
193
|
+
| `endpoint` | `string` | **Projection-only.** Shown in projection meta and markdown; does not override the runtime client's endpoint. See `GraphqlContractDefaults.endpoint` above. |
|
|
194
|
+
| `description` | `string` | Contract-level description |
|
|
195
|
+
| `types` | `GraphqlTypeDefs` | Explicit type declarations (Phase 2 `.gql` projection hint; opaque in Phase 1) |
|
|
196
|
+
| `defaultOperation` | `"query" \| "mutation"` | Default operation type for cases (default: `"query"`) |
|
|
197
|
+
| `variablesSchema` | `SchemaLike<Vars>` | Contract-level variables schema |
|
|
198
|
+
| `responseSchema` | `SchemaLike<Res>` | Contract-level response schema (rare — per-case `expect.schema` is the primary home) |
|
|
199
|
+
| `defaultVariables` | `Partial<Vars>` | Deep-merged under each case's `variables` |
|
|
200
|
+
| `defaultHeaders` | `Record<string, string>` | Merged under each case's `headers` |
|
|
201
|
+
| `client` | `GraphQLClient` | Override instance client |
|
|
202
|
+
| `cases` | `Record<string, GraphqlContractCase>` | Named cases — required |
|
|
203
|
+
|
|
204
|
+
Case shape (`GraphqlContractCase<Vars, Res, S>`):
|
|
205
|
+
|
|
206
|
+
| Field | Type | Description |
|
|
207
|
+
|-------|------|-------------|
|
|
208
|
+
| `description` | `string` | Required — why this case exists |
|
|
209
|
+
| `query` | `string` | Required — GraphQL document (inline, `gql` tag, or `fromGql("./file.gql")`) |
|
|
210
|
+
| `operation` | `"query" \| "mutation"` | Override spec-level default (`subscription` is Phase 2) |
|
|
211
|
+
| `operationName` | `string` | Display hint; defaults to parse from `query` |
|
|
212
|
+
| `variables` | `Vars \| (state) => Vars` | Variables; deep-merged over `defaultVariables` |
|
|
213
|
+
| `headers` | `Record<string, string> \| fn` | Per-call headers |
|
|
214
|
+
| `expect` | `GraphqlContractExpect<Res>` | `httpStatus` / `data` / `errors` / `schema` / `headers` / `headersMatch` |
|
|
215
|
+
| `setup` / `teardown` | `(ctx, state?) => Promise<void>` | Lifecycle |
|
|
216
|
+
| `verify` | `(ctx, GraphqlCaseResult) => Promise<void>` | Business-logic check after transport + schema + data assertions |
|
|
217
|
+
| `deferred` | `string` | Skip with reason |
|
|
218
|
+
| `deprecated` | `string` | Deprecate with reason |
|
|
219
|
+
| `tags` / `severity` / `requires` / `defaultRun` | — | Standard case metadata |
|
|
220
|
+
|
|
221
|
+
`expect` fields:
|
|
222
|
+
|
|
223
|
+
| Field | Type | Description |
|
|
224
|
+
|-------|------|-------------|
|
|
225
|
+
| `httpStatus` | `number` | Expected HTTP status from the POST (default: `200`) |
|
|
226
|
+
| `schema` | `SchemaLike<Res>` | Per-case response schema (selection-set-coupled); validated via `ctx.validate` |
|
|
227
|
+
| `data` | `Partial<Res>` | Partial match on response `data` |
|
|
228
|
+
| `errors` | `GraphqlErrorsExpect` | `"absent"` (default) \| `"any"` \| `Array<Partial<GraphQLError>>` |
|
|
229
|
+
| `headers` | `SchemaLike<Record<string, string \| string[]>>` | Schema for response headers |
|
|
230
|
+
| `headersMatch` | `Record<string, string>` | Partial match on response headers |
|
|
231
|
+
|
|
232
|
+
`GraphqlCaseResult<Res>` — shape passed to `verify` and flow `out` lens:
|
|
233
|
+
|
|
234
|
+
| Field | Type | Description |
|
|
235
|
+
|-------|------|-------------|
|
|
236
|
+
| `data` | `Res \| null` | Decoded `data` field (null if all fields errored or transport failed) |
|
|
237
|
+
| `errors` | `GraphQLError[] \| undefined` | Payload errors array |
|
|
238
|
+
| `extensions` | `Record<string, unknown>` | Server-side tracing/cost/etc |
|
|
239
|
+
| `httpStatus` | `number` | HTTP status from the underlying POST |
|
|
240
|
+
| `headers` | `Record<string, string \| string[]>` | Response headers (lowercased keys) |
|
|
241
|
+
| `rawBody` | `string \| null` | Raw response body (null on network error) |
|
|
242
|
+
| `operationName` | `string` | Resolved operation name |
|
|
243
|
+
| `duration` | `number` | Call duration in ms |
|
|
244
|
+
|
|
245
|
+
### Transport
|
|
246
|
+
|
|
247
|
+
#### `graphql(options)` — Plugin Factory
|
|
248
|
+
|
|
249
|
+
For use with `configure({ plugins })`. Supports `{{template}}` placeholders in `endpoint` and `headers` values, resolved from Glubean vars and secrets.
|
|
250
|
+
|
|
251
|
+
| Option | Type | Description |
|
|
252
|
+
|--------|------|-------------|
|
|
253
|
+
| `endpoint` | `string` | GraphQL endpoint URL, supports `{{VAR}}` |
|
|
254
|
+
| `headers` | `Record<string, string>` | Default headers, supports `{{VAR}}` |
|
|
255
|
+
| `throwOnGraphQLErrors` | `boolean` | Throw `GraphQLResponseError` when the response carries `errors` (default: `false`) |
|
|
256
|
+
|
|
257
|
+
#### `createGraphQLClient(http, options)` — Standalone
|
|
258
|
+
|
|
259
|
+
Returns a `GraphQLClient` bound to `http` (typically `ctx.http`).
|
|
260
|
+
|
|
261
|
+
#### `client.query(query, options?)` / `client.mutate(mutation, options?)`
|
|
262
|
+
|
|
263
|
+
Returns `GraphQLResult<T>`:
|
|
264
|
+
|
|
265
|
+
| Field | Type | Description |
|
|
266
|
+
|-------|------|-------------|
|
|
267
|
+
| `data` | `T \| null` | Parsed response data |
|
|
268
|
+
| `errors` | `GraphQLError[] \| undefined` | Payload errors |
|
|
269
|
+
| `extensions` | `Record<string, unknown>` | Server extensions |
|
|
270
|
+
| `httpStatus` | `number` | HTTP status |
|
|
271
|
+
| `headers` | `Record<string, string \| string[]>` | Response headers |
|
|
272
|
+
| `rawBody` | `string \| null` | Raw body |
|
|
273
|
+
|
|
274
|
+
Options:
|
|
275
|
+
|
|
276
|
+
| Option | Type | Description |
|
|
277
|
+
|--------|------|-------------|
|
|
278
|
+
| `variables` | `Record<string, unknown>` | Query variables |
|
|
279
|
+
| `operationName` | `string` | Override auto-parsed name |
|
|
280
|
+
| `headers` | `Record<string, string>` | Extra per-request headers |
|
|
281
|
+
|
|
282
|
+
Errors don't throw by default — inspect `errors` / `httpStatus` for assertion-friendly testing. Opt into throws via `throwOnGraphQLErrors: true`.
|
|
283
|
+
|
|
284
|
+
#### `gql` — tagged template
|
|
285
|
+
|
|
286
|
+
Identity function; exists so IDE GraphQL extensions pick up syntax highlighting.
|
|
287
|
+
|
|
288
|
+
#### `fromGql(path)` — `.gql` file loader
|
|
289
|
+
|
|
290
|
+
Reads a GraphQL document file relative to the test file. Prefer this for full IDE support (autocomplete, schema validation) when you've got a `.graphqlrc`.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Custom matchers
|
|
295
|
+
|
|
296
|
+
`import "@glubean/graphql"` side-effect registers GraphQL matchers onto the
|
|
297
|
+
shared `ctx.expect()` surface. No extra import or configure field needed.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import "@glubean/graphql";
|
|
301
|
+
|
|
302
|
+
// Works on GraphQLResult (transport) and GraphqlCaseResult (contract verify / flow out lens)
|
|
303
|
+
ctx.expect(res).toHaveHttpStatus(200); // transport-layer
|
|
304
|
+
ctx.expect(res).toHaveGraphqlNoErrors(); // errors absent / empty
|
|
305
|
+
ctx.expect(res).toHaveGraphqlData({ user: { name: "Alice" } }); // partial data match
|
|
306
|
+
ctx.expect(res).toHaveGraphqlErrorCode("UNAUTHENTICATED"); // case-insensitive
|
|
307
|
+
ctx.expect(res).toHaveGraphqlExtension("tracing"); // extensions key present
|
|
308
|
+
ctx.expect(res).not.toHaveGraphqlNoErrors(); // negation
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
> **Why `toHaveHttpStatus` and not `toHaveStatus`?** The GraphQL envelope
|
|
312
|
+
> (CG-10) uses `httpStatus` instead of `status` so it doesn't shadow the
|
|
313
|
+
> native `Response.status` semantics. The built-in `toHaveStatus` reads
|
|
314
|
+
> `actual.status` and won't find the envelope's status; use
|
|
315
|
+
> `toHaveHttpStatus` for GraphQL responses.
|
|
316
|
+
|
|
317
|
+
All matchers inherit `.not` negation, `.orFail()` chaining, and soft-by-default
|
|
318
|
+
semantics from `@glubean/sdk`'s `Expectation`. Types come through
|
|
319
|
+
`CustomMatchers<T>` declaration merging automatically — no user-side
|
|
320
|
+
`declare module` required.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Tracing
|
|
325
|
+
|
|
326
|
+
Every GraphQL call inherits HTTP-level tracing via `ctx.http` and injects `X-Glubean-Op: <operationName>` so individual operations are distinguishable in the dashboard instead of showing a generic `POST /graphql`.
|
|
327
|
+
|
|
328
|
+
The underlying HTTP trace event already carries status, timing, and request/response bodies. At the contract layer, `classifyFailure` consumes `graphql_response` / `http_response` events and maps to the repair-loop `FailureKind` values.
|
|
329
|
+
|
|
330
|
+
## Auth
|
|
331
|
+
|
|
332
|
+
Static headers (including auth tokens) are sent with every call:
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
graphql({
|
|
336
|
+
// ...
|
|
337
|
+
headers: { Authorization: "Bearer {{API_TOKEN}}" },
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Per-call headers override static values:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
await gql.query(`{ me { id } }`, {
|
|
345
|
+
headers: { Authorization: "Bearer per-call-token" },
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
At the contract layer, headers merge in this order (right wins):
|
|
350
|
+
instance `defaults.headers` < contract `defaultHeaders` < case `headers` < flow-step `in` lens `headers`.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Migration: 0.1.x → 0.2.0
|
|
355
|
+
|
|
356
|
+
**What's new:**
|
|
357
|
+
- Contract adapter shipped inside this package. `import "@glubean/graphql"` now also registers `contract.graphql.with(...)`.
|
|
358
|
+
- Single-package model: no separate `@glubean/contract-graphql` package.
|
|
359
|
+
- `GraphQLClient.query` / `.mutate` return `GraphQLResult<T>` — additive over `GraphQLResponse<T>`: same `data` / `errors` / `extensions`, plus new `httpStatus` / `headers` / `rawBody`.
|
|
360
|
+
|
|
361
|
+
**What's not broken:**
|
|
362
|
+
- Existing `configure({ plugins: { x: graphql({ ... }) } })` usage is unchanged.
|
|
363
|
+
- Existing `createGraphQLClient(...)` usage is unchanged.
|
|
364
|
+
- Code that destructures `{ data, errors }` from query/mutation calls continues to work — new fields are additive.
|
|
365
|
+
- All 0.1.x transport tests still pass without modification.
|
|
366
|
+
|
|
367
|
+
**Only additive API changes:**
|
|
368
|
+
- `contract.graphql.with(...)` now available from `@glubean/sdk` after importing this package.
|
|
369
|
+
- `GraphQLResult<T>` is exported alongside `GraphQLResponse<T>` and is returned from client methods.
|
|
370
|
+
- Export surface gained contract types (`GraphqlContractSpec`, `GraphqlContractCase`, etc.).
|
|
371
|
+
|
|
372
|
+
If you currently use `@glubean/graphql` only as a transport plugin and do not import `contract.graphql` anywhere, you only need to rebuild; no source changes are required.
|
|
373
|
+
|
|
374
|
+
## Known Phase 1 limitations
|
|
375
|
+
|
|
376
|
+
A few edges are deliberately simple in Phase 1. Reviewed in
|
|
377
|
+
[../internal/30-execution/2026-04-20-multi-protocol-contract/request-for-review-graphql.md](../../internal/30-execution/2026-04-20-multi-protocol-contract/request-for-review-graphql.md).
|
|
378
|
+
|
|
379
|
+
1. **`endpoint` is projection-only.** Shown in meta / markdown for
|
|
380
|
+
scanner + MCP, but the adapter does not override the bound endpoint
|
|
381
|
+
of the supplied `GraphQLClient`. Express multi-endpoint via multiple
|
|
382
|
+
clients (`api_v1 = graphql({endpoint: "/v1"}); api_v2 = ...`).
|
|
383
|
+
2. **`variablesSchema` is contract-level only.** If two cases in the
|
|
384
|
+
same contract have materially different `variables` shapes (e.g.
|
|
385
|
+
different operations), a contract-level schema will lose fidelity.
|
|
386
|
+
Workaround: split into separate contracts, or run ad-hoc
|
|
387
|
+
per-case variable assertions via `verify`. Phase 2 ergonomics
|
|
388
|
+
work may add per-case `variablesSchema` override.
|
|
389
|
+
3. **Use `throwOnGraphQLErrors: false` (default) with contracts.** If
|
|
390
|
+
you construct the underlying client with `throwOnGraphQLErrors: true`
|
|
391
|
+
the adapter will observe a thrown `GraphQLResponseError` instead of
|
|
392
|
+
the normal envelope — this bypasses the 3-layer assertion path
|
|
393
|
+
(`expect.errors`, `expect.data`, `expect.schema` no longer runs).
|
|
394
|
+
The `graphql({...})` plugin factory defaults to `false`; only
|
|
395
|
+
standalone `createGraphQLClient(http, { throwOnGraphQLErrors: true })`
|
|
396
|
+
is affected.
|
|
397
|
+
4. **Scoped-style authoring (`contract.graphql.with("api", ...)(...)`)
|
|
398
|
+
requires runtime import for scanner discovery.** The static
|
|
399
|
+
extractor regex matches only `contract.graphql("id", {...})` (the
|
|
400
|
+
direct form, which the runtime rejects). Scoped authoring therefore
|
|
401
|
+
depends on `glubean scan`'s runtime-import fallback — same
|
|
402
|
+
behavior as gRPC, same caveat as `@glubean/grpc` v0.2.0.
|
|
403
|
+
|
|
404
|
+
## Scope
|
|
405
|
+
|
|
406
|
+
### Phase 1 (shipped)
|
|
407
|
+
|
|
408
|
+
- Query + mutation contracts (selection-set-per-case)
|
|
409
|
+
- 3-layer failure classification (transport / payload / error shape)
|
|
410
|
+
- Per-case schema validation (selection-set coupled)
|
|
411
|
+
- Headers + variables merge through contract → instance → case → flow-step
|
|
412
|
+
- Envelope exposure (`httpStatus` / `headers` / `rawBody`)
|
|
413
|
+
- Markdown projection (case list + operation + query snippets + lifecycle markers)
|
|
414
|
+
- Cross-protocol flow composition (HTTP + gRPC + GraphQL verified end-to-end)
|
|
415
|
+
|
|
416
|
+
### Phase 2 (planned)
|
|
417
|
+
|
|
418
|
+
- Subscription support sharing the same streaming case design as gRPC streaming
|
|
419
|
+
- `.gql` / SDL projection from `types` declaration (see proposal §7b — solvable, sequencing deferral)
|
|
420
|
+
- See `internal/40-discovery/proposals/contract-async-protocol-plugins.md`
|
|
421
|
+
|
|
422
|
+
### Out of scope (Phase 3+)
|
|
423
|
+
|
|
424
|
+
- Federated gateways / schema stitching
|
|
425
|
+
- Apollo Studio / Hasura registry integration
|
|
426
|
+
- Persistent queries
|
|
427
|
+
- Automatic `.gql` SDL generation as the only source of truth — see proposal §7b.4 for long-term framing
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in GraphQL contract adapter for @glubean/graphql 0.2.0.
|
|
3
|
+
*
|
|
4
|
+
* Shipped alongside the transport plugin in @glubean/graphql (single-package
|
|
5
|
+
* model — "contract is a first-class citizen"). Registered via
|
|
6
|
+
* `contract.register("graphql", graphqlAdapter)` on import — see ./index.ts.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities (same interface as HTTP / gRPC adapters):
|
|
9
|
+
* - execute: setup → call → expect → verify → teardown
|
|
10
|
+
* - executeCaseInFlow: deep-merge resolvedInputs, run case in flow mode
|
|
11
|
+
* - validateCaseForFlow: reject function-valued variables / headers
|
|
12
|
+
* - project: runtime ContractProjection<GraphqlPayloadSchemas>
|
|
13
|
+
* - normalize: runtime → JSON-safe ExtractedContractProjection
|
|
14
|
+
* - classifyFailure: 3-layer (transport / payload errors / data shape)
|
|
15
|
+
* - renderTarget: operationName (parsed from query if needed)
|
|
16
|
+
* - toMarkdown: case list with operation + query snippet
|
|
17
|
+
* - describePayload: high-level summary for index views
|
|
18
|
+
*
|
|
19
|
+
* Phase 1 scope: query + mutation only. Subscription deferred to Phase 2.
|
|
20
|
+
*/
|
|
21
|
+
import type { ContractProtocolAdapter } from "@glubean/sdk";
|
|
22
|
+
import type { GraphqlContractMeta, GraphqlContractSafeMeta, GraphqlContractSpec, GraphqlPayloadSchemas, GraphqlSafeSchemas } from "./types.js";
|
|
23
|
+
/** Convert a SchemaLike to a JSON Schema fragment if possible (best-effort). */
|
|
24
|
+
export declare function schemaToJsonSchema(schema: unknown): Record<string, unknown> | null;
|
|
25
|
+
export declare function validateGraphqlCaseForFlow(spec: GraphqlContractSpec, caseKey: string, contractId: string): void;
|
|
26
|
+
export declare const graphqlAdapter: ContractProtocolAdapter<GraphqlContractSpec, GraphqlPayloadSchemas, GraphqlContractMeta, GraphqlSafeSchemas, GraphqlContractSafeMeta>;
|
|
27
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/contract/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAEV,uBAAuB,EAOxB,MAAM,cAAc,CAAC;AAItB,OAAO,KAAK,EAGV,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EAEnB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAMpB,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAYlF;AAmcD,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,IAAI,CAoBN;AAwLD,eAAO,MAAM,cAAc,EAAE,uBAAuB,CAClD,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,CAmBxB,CAAC"}
|