import * as Context from "../Context.ts"; import * as Duration from "../Duration.ts"; import * as Effect from "../Effect.ts"; import * as Headers from "../http/Headers.ts"; import * as HttpClient from "../http/HttpClient.ts"; import * as UrlParams from "../http/UrlParams.ts"; import * as Layer from "../Layer.ts"; import * as Schema from "../Schema.ts"; import type * as Scope from "../Scope.ts"; import * as Socket from "../socket/Socket.ts"; import * as Stream from "../Stream.ts"; import type { Kind } from "./GraphQL.ts"; import { TransportError } from "./GraphQLClientError.ts"; /** * The request a transport sends. Middleware sees and may rewrite it before it * reaches the transport. * * **Details** * * `kind` is the operation type, which the HTTP transport uses to send queries * as `GET` when `queryMethod: "GET"` is set. `variables` are already encoded * through the operation's variables Schema. * * `headers` and `urlParams` are applied by the HTTP transport, to queries, * mutations and graphql-sse subscriptions alike. A graphql-ws transport * ignores both, because graphql-ws has no per-operation headers or URL. * Request headers are normalized, immutable `Headers.Headers` values. * * @stability experimental * @category models * @since 4.0.0 */ export interface GraphQLRequest { readonly kind: Kind; readonly query: string; readonly operationName: string; readonly variables: unknown; readonly extensions?: Record | undefined; readonly headers: Headers.Headers; readonly urlParams: UrlParams.UrlParams; } /** * A raw GraphQL result before schema decoding. Middleware receive it from * `next` and can read response `extensions`, such as rate-limit costs. * * **Details** * * `status` and `headers` are transport metadata, not part of the GraphQL * response body: the client decodes the body without them and then copies * what the transport reported. Middleware may read or rewrite them, and a * failed call's `ResponseError` carries the final values. Headers use * `Headers.Headers` at runtime and encode as a plain string record. * * @stability experimental * @category schemas * @since 4.0.0 */ export declare const ExecutionResult: Schema.Struct<{ readonly data: Schema.optional; readonly errors: Schema.optional>>; readonly path: Schema.optional>>; readonly extensions: Schema.optional>>; }>, readonly [Schema.$Record>]>>>; readonly extensions: Schema.optional>>; readonly status: Schema.optional; readonly headers: Schema.optional, Schema.$Record, never, never>>; }>; /** * The decoded type of {@link ExecutionResult}. * * @stability experimental * @category models * @since 4.0.0 */ export type ExecutionResult = typeof ExecutionResult.Type; declare const GraphQLProtocol_base: Context.ServiceClass Effect.Effect<{ readonly body: unknown; readonly status?: number | undefined; readonly headers?: Headers.Headers | undefined; }, TransportError>; readonly subscribe: (request: GraphQLRequest) => Stream.Stream<{ readonly body: unknown; readonly status?: number | undefined; readonly headers?: Headers.Headers | undefined; }, TransportError>; /** * Resolves the transport URL for resolver batching. Set by `layerHttp`; * optional for custom transports. */ readonly url?: ((options: { readonly kind: Kind; readonly operationName: string; }) => Effect.Effect) | undefined; }>; /** * The transport service. `execute` answers a query or mutation with the raw * JSON body, and `subscribe` turns a subscription into a stream of raw events. * Both fail only with `TransportError`; GraphQL `errors` are left in the body * for the client to interpret. * * **Details** * * Each answer is `{ body, status?, headers? }`. The HTTP transport sets * `headers` on every answer, including graphql-sse events, and `status` on * all but graphql-sse events. Headers are `Headers.Headers` values. * graphql-ws leaves both unset. * * **Example** (A scripted transport for tests) * * ```ts import.meta.vitest * import { Effect, Layer, Schema, Stream } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * const Scripted = Layer.succeed(GraphQLProtocol.GraphQLProtocol, { * execute: () => Effect.succeed({ body: { data: { viewer: { login: "tim" } } } }), * subscribe: () => Stream.empty * }) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer() * }) * * await Effect.runPromise(program.pipe(Effect.provide(Scripted))) // => { viewer: { login: "tim" } } * ``` * * @stability experimental * @category services * @since 4.0.0 */ export declare class GraphQLProtocol extends GraphQLProtocol_base { } /** * The URL of an HTTP transport: fixed, resolved for each operation from the * calling context, `kind` and `operationName`, or an `Effect` with no errors * or requirements. See {@link layerHttp}. * * @stability experimental * @category models * @since 4.0.0 */ export type Url = string | Effect.Effect | ((options: { readonly context: Context.Context; readonly kind: Kind; readonly operationName: string; }) => string); /** * Builds the HTTP transport as an effect, for composing transports by hand. * Most code uses {@link layerHttp}. * * **Details** * * Takes the same options as {@link layerHttp}, including `queryMethod`. * Without `subscriptions`, subscriptions use graphql-sse distinct mode on * `url`. With `subscriptions.webSocket`, `url` must be a string, subscriptions * go over graphql-ws instead, and the effect also builds the graphql-ws * transport, so it needs a `Socket.WebSocketConstructor` and a `Scope`. * * @stability experimental * @category constructors * @since 4.0.0 */ export declare const makeHttp: { /** * Builds the HTTP transport as an effect, for composing transports by hand. * Most code uses {@link layerHttp}. * * **Details** * * Takes the same options as {@link layerHttp}, including `queryMethod`. * Without `subscriptions`, subscriptions use graphql-sse distinct mode on * `url`. With `subscriptions.webSocket`, `url` must be a string, subscriptions * go over graphql-ws instead, and the effect also builds the graphql-ws * transport, so it needs a `Socket.WebSocketConstructor` and a `Scope`. * * @stability experimental * @category constructors * @since 4.0.0 */ (options: { readonly url: string; readonly queryMethod?: "GET" | "POST" | undefined; readonly subscriptions: { readonly webSocket: { readonly url: string; readonly headers?: Headers.Input | undefined; readonly connectionParams?: Effect.Effect | undefined, unknown, R> | undefined; readonly connectionAckTimeout?: Duration.Input | undefined; readonly keepAlive?: Duration.Input | false | undefined; readonly idleTimeout?: Duration.Input | undefined; }; }; }): Effect.Effect; /** * Builds the HTTP transport as an effect, for composing transports by hand. * Most code uses {@link layerHttp}. * * **Details** * * Takes the same options as {@link layerHttp}, including `queryMethod`. * Without `subscriptions`, subscriptions use graphql-sse distinct mode on * `url`. With `subscriptions.webSocket`, `url` must be a string, subscriptions * go over graphql-ws instead, and the effect also builds the graphql-ws * transport, so it needs a `Socket.WebSocketConstructor` and a `Scope`. * * @stability experimental * @category constructors * @since 4.0.0 */ (options: { readonly url: Url; readonly queryMethod?: "GET" | "POST" | undefined; readonly subscriptions?: undefined; }): Effect.Effect; }; /** * The HTTP transport: queries and mutations are sent as `POST` with a JSON * body to `url`, using the `HttpClient` in context. Subscriptions use * graphql-sse distinct mode on the same URL, or graphql-ws when * `subscriptions.webSocket` is set. * * **Details** * * With `queryMethod: "GET"` (default `"POST"`), queries are sent as `GET` * with no body: `query` and `operationName` become URL params, plus * `variables` and `extensions` as JSON when set. Mutations are always * `POST`, and so are `GraphQLResolver.aliased` batches. A long query is not * retried as `POST`. * * A request's `headers` and `urlParams`, from the call options or from * middleware, are applied to every HTTP request. A query string already on * `url` is kept. On `GET`, the request's `urlParams` come before the GraphQL * params, which replace any caller param with the same name. * * Every response's HTTP `status` is passed to the client as * `ExecutionResult.status`, and a call that fails with GraphQL `errors` * reports it as `ResponseError.status`. Response headers are passed the * same way, as `ExecutionResult.headers` and `ResponseError.headers`. * * A response is a GraphQL response when its `Content-Type` is * `application/graphql-response+json`, whatever the status, or when it is * `application/json` and the body has a list of `errors` or a `data` object * (or `null`). Media types match case-insensitively. Anything else * is a `TransportError` with the response `status` and `headers`, and a * `Retry-After` header (whole seconds or an HTTP date, measured from when the * response arrived) becomes `retryAfter`. * * A subscription is a `POST` with `Accept: text/event-stream`. Each `next` * event is emitted with the response headers but no status, and the stream * ends on a `complete` event, with or without a `data:` line. A body that * fails, or ends without a `complete` event, is a retryable `TransportError` * with a server `retry:` line as its `retryAfter`, so the client's * `subscriptionRetry` resubscribes. A non-stream answer, such as a `400` with * `errors`, is read like a query's response. Interrupting the stream aborts * the request. * * With `subscriptions: { webSocket }`, subscriptions go over graphql-ws as in * {@link layerWebSocket}, and the layer needs a `Socket.WebSocketConstructor`. * * To route each query, mutation and graphql-sse subscription, `url` can be a * function of the calling fiber's context (including the call's `context` * option), `kind` and `operationName`, or an `Effect` with no errors * or requirements that runs in that context. Use a defaulted * `Context.Reference` to select the server. Reading a missing service with * `Context.get` is a defect, not a `TransportError`. Neither form can be * combined with `subscriptions.webSocket`: the socket URL is fixed at * connection time. * * **Gotchas** * * `GraphQLResolver` resolves each lookup's URL once, in the calling fiber, * before middleware run and before the batched request exists, then sends * each batch to the URL it was grouped by. A single call resolves it in the * transport, after middleware. * So the URL must depend only on the context and the operation, and * middleware must not change `kind` or `operationName` or provide services * the URL reads. * * `RequestResolver.withCache` and `RequestResolver.persisted` key on the lookup, * not the URL, so results can leak across servers. For caching, use a separate * client and resolver per server, for example with `LayerMap`. * * **Example** (Building a client over HTTP) * * ```ts import.meta.vitest * import { Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * // A stand-in for a real HttpClient layer such as FetchHttpClient.layer * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response(JSON.stringify({ data: { viewer: { login: "tim" } } }), { * headers: { "content-type": "application/graphql-response+json" } * }) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer() * }).pipe( * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => { viewer: { login: "tim" } } * ``` * * **Example** (Queries over GET with a per-call URL param) * * ```ts import.meta.vitest * import { Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * // A stand-in server that answers with the method and the `preview` param * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request, url) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response( * JSON.stringify({ data: { viewer: { login: `${request.method} ${url.searchParams.get("preview")}` } } }), * { headers: { "content-type": "application/graphql-response+json" } } * ) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer(undefined, { urlParams: { preview: "draft-token" } }) * }).pipe( * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://cms.example.com/graphql", queryMethod: "GET" })), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => { viewer: { login: "GET draft-token" } } * ``` * * **Example** (One client for github.com, GHE.com and GitHub Enterprise Server) * * ```ts import.meta.vitest * import { Context, Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * const GitHubHost = Context.Reference("GitHubHost", { defaultValue: () => "github.com" }) * * const Protocol = GraphQLProtocol.layerHttp({ * url: ({ context }) => { * const host = Context.get(context, GitHubHost) * if (host === "github.com") return "https://api.github.com/graphql" * if (host.endsWith(".ghe.com")) return `https://api.${host}/graphql` * return `https://${host}/api/graphql` * } * }) * * // Echo the request URL instead of calling GitHub. * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request, url) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response(JSON.stringify({ data: { viewer: { login: url.href } } }), { * headers: { "content-type": "application/graphql-response+json" } * }) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * const dotcom = yield* client.Viewer() * const cloud = yield* client.Viewer().pipe(Effect.provideService(GitHubHost, "acme.ghe.com")) * const server = yield* client.Viewer(undefined, { context: Context.make(GitHubHost, "github.example.com") }) * return [dotcom, cloud, server].map((result) => result.viewer.login) * }).pipe( * Effect.provide(Protocol), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => ["https://api.github.com/graphql", "https://api.acme.ghe.com/graphql", "https://github.example.com/api/graphql"] * ``` * * **Example** (Subscriptions over graphql-ws, everything else over POST) * * ```ts import.meta.vitest * import { Effect } from "effect" * import { GraphQLProtocol } from "effect/graphql" * * const token = Effect.succeed("secret") * * // Provide an HttpClient and a Socket.WebSocketConstructor, such as * // FetchHttpClient.layer and NodeSocket.layerWebSocketConstructor * export const Protocol = GraphQLProtocol.layerHttp({ * url: "https://example.com/graphql", * subscriptions: { * webSocket: { * url: "wss://example.com/graphql", * connectionParams: Effect.map(token, (token) => ({ authorization: `Bearer ${token}` })) * } * } * }) * ``` * * @stability experimental * @category layers * @since 4.0.0 */ export declare const layerHttp: { /** * The HTTP transport: queries and mutations are sent as `POST` with a JSON * body to `url`, using the `HttpClient` in context. Subscriptions use * graphql-sse distinct mode on the same URL, or graphql-ws when * `subscriptions.webSocket` is set. * * **Details** * * With `queryMethod: "GET"` (default `"POST"`), queries are sent as `GET` * with no body: `query` and `operationName` become URL params, plus * `variables` and `extensions` as JSON when set. Mutations are always * `POST`, and so are `GraphQLResolver.aliased` batches. A long query is not * retried as `POST`. * * A request's `headers` and `urlParams`, from the call options or from * middleware, are applied to every HTTP request. A query string already on * `url` is kept. On `GET`, the request's `urlParams` come before the GraphQL * params, which replace any caller param with the same name. * * Every response's HTTP `status` is passed to the client as * `ExecutionResult.status`, and a call that fails with GraphQL `errors` * reports it as `ResponseError.status`. Response headers are passed the * same way, as `ExecutionResult.headers` and `ResponseError.headers`. * * A response is a GraphQL response when its `Content-Type` is * `application/graphql-response+json`, whatever the status, or when it is * `application/json` and the body has a list of `errors` or a `data` object * (or `null`). Media types match case-insensitively. Anything else * is a `TransportError` with the response `status` and `headers`, and a * `Retry-After` header (whole seconds or an HTTP date, measured from when the * response arrived) becomes `retryAfter`. * * A subscription is a `POST` with `Accept: text/event-stream`. Each `next` * event is emitted with the response headers but no status, and the stream * ends on a `complete` event, with or without a `data:` line. A body that * fails, or ends without a `complete` event, is a retryable `TransportError` * with a server `retry:` line as its `retryAfter`, so the client's * `subscriptionRetry` resubscribes. A non-stream answer, such as a `400` with * `errors`, is read like a query's response. Interrupting the stream aborts * the request. * * With `subscriptions: { webSocket }`, subscriptions go over graphql-ws as in * {@link layerWebSocket}, and the layer needs a `Socket.WebSocketConstructor`. * * To route each query, mutation and graphql-sse subscription, `url` can be a * function of the calling fiber's context (including the call's `context` * option), `kind` and `operationName`, or an `Effect` with no errors * or requirements that runs in that context. Use a defaulted * `Context.Reference` to select the server. Reading a missing service with * `Context.get` is a defect, not a `TransportError`. Neither form can be * combined with `subscriptions.webSocket`: the socket URL is fixed at * connection time. * * **Gotchas** * * `GraphQLResolver` resolves each lookup's URL once, in the calling fiber, * before middleware run and before the batched request exists, then sends * each batch to the URL it was grouped by. A single call resolves it in the * transport, after middleware. * So the URL must depend only on the context and the operation, and * middleware must not change `kind` or `operationName` or provide services * the URL reads. * * `RequestResolver.withCache` and `RequestResolver.persisted` key on the lookup, * not the URL, so results can leak across servers. For caching, use a separate * client and resolver per server, for example with `LayerMap`. * * **Example** (Building a client over HTTP) * * ```ts import.meta.vitest * import { Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * // A stand-in for a real HttpClient layer such as FetchHttpClient.layer * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response(JSON.stringify({ data: { viewer: { login: "tim" } } }), { * headers: { "content-type": "application/graphql-response+json" } * }) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer() * }).pipe( * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => { viewer: { login: "tim" } } * ``` * * **Example** (Queries over GET with a per-call URL param) * * ```ts import.meta.vitest * import { Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * // A stand-in server that answers with the method and the `preview` param * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request, url) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response( * JSON.stringify({ data: { viewer: { login: `${request.method} ${url.searchParams.get("preview")}` } } }), * { headers: { "content-type": "application/graphql-response+json" } } * ) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer(undefined, { urlParams: { preview: "draft-token" } }) * }).pipe( * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://cms.example.com/graphql", queryMethod: "GET" })), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => { viewer: { login: "GET draft-token" } } * ``` * * **Example** (One client for github.com, GHE.com and GitHub Enterprise Server) * * ```ts import.meta.vitest * import { Context, Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * const GitHubHost = Context.Reference("GitHubHost", { defaultValue: () => "github.com" }) * * const Protocol = GraphQLProtocol.layerHttp({ * url: ({ context }) => { * const host = Context.get(context, GitHubHost) * if (host === "github.com") return "https://api.github.com/graphql" * if (host.endsWith(".ghe.com")) return `https://api.${host}/graphql` * return `https://${host}/api/graphql` * } * }) * * // Echo the request URL instead of calling GitHub. * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request, url) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response(JSON.stringify({ data: { viewer: { login: url.href } } }), { * headers: { "content-type": "application/graphql-response+json" } * }) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * const dotcom = yield* client.Viewer() * const cloud = yield* client.Viewer().pipe(Effect.provideService(GitHubHost, "acme.ghe.com")) * const server = yield* client.Viewer(undefined, { context: Context.make(GitHubHost, "github.example.com") }) * return [dotcom, cloud, server].map((result) => result.viewer.login) * }).pipe( * Effect.provide(Protocol), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => ["https://api.github.com/graphql", "https://api.acme.ghe.com/graphql", "https://github.example.com/api/graphql"] * ``` * * **Example** (Subscriptions over graphql-ws, everything else over POST) * * ```ts import.meta.vitest * import { Effect } from "effect" * import { GraphQLProtocol } from "effect/graphql" * * const token = Effect.succeed("secret") * * // Provide an HttpClient and a Socket.WebSocketConstructor, such as * // FetchHttpClient.layer and NodeSocket.layerWebSocketConstructor * export const Protocol = GraphQLProtocol.layerHttp({ * url: "https://example.com/graphql", * subscriptions: { * webSocket: { * url: "wss://example.com/graphql", * connectionParams: Effect.map(token, (token) => ({ authorization: `Bearer ${token}` })) * } * } * }) * ``` * * @stability experimental * @category layers * @since 4.0.0 */ (options: { readonly url: string; readonly queryMethod?: "GET" | "POST" | undefined; readonly subscriptions: { readonly webSocket: { readonly url: string; readonly headers?: Headers.Input | undefined; readonly connectionParams?: Effect.Effect | undefined, unknown, R> | undefined; readonly connectionAckTimeout?: Duration.Input | undefined; readonly keepAlive?: Duration.Input | false | undefined; readonly idleTimeout?: Duration.Input | undefined; }; }; }): Layer.Layer; /** * The HTTP transport: queries and mutations are sent as `POST` with a JSON * body to `url`, using the `HttpClient` in context. Subscriptions use * graphql-sse distinct mode on the same URL, or graphql-ws when * `subscriptions.webSocket` is set. * * **Details** * * With `queryMethod: "GET"` (default `"POST"`), queries are sent as `GET` * with no body: `query` and `operationName` become URL params, plus * `variables` and `extensions` as JSON when set. Mutations are always * `POST`, and so are `GraphQLResolver.aliased` batches. A long query is not * retried as `POST`. * * A request's `headers` and `urlParams`, from the call options or from * middleware, are applied to every HTTP request. A query string already on * `url` is kept. On `GET`, the request's `urlParams` come before the GraphQL * params, which replace any caller param with the same name. * * Every response's HTTP `status` is passed to the client as * `ExecutionResult.status`, and a call that fails with GraphQL `errors` * reports it as `ResponseError.status`. Response headers are passed the * same way, as `ExecutionResult.headers` and `ResponseError.headers`. * * A response is a GraphQL response when its `Content-Type` is * `application/graphql-response+json`, whatever the status, or when it is * `application/json` and the body has a list of `errors` or a `data` object * (or `null`). Media types match case-insensitively. Anything else * is a `TransportError` with the response `status` and `headers`, and a * `Retry-After` header (whole seconds or an HTTP date, measured from when the * response arrived) becomes `retryAfter`. * * A subscription is a `POST` with `Accept: text/event-stream`. Each `next` * event is emitted with the response headers but no status, and the stream * ends on a `complete` event, with or without a `data:` line. A body that * fails, or ends without a `complete` event, is a retryable `TransportError` * with a server `retry:` line as its `retryAfter`, so the client's * `subscriptionRetry` resubscribes. A non-stream answer, such as a `400` with * `errors`, is read like a query's response. Interrupting the stream aborts * the request. * * With `subscriptions: { webSocket }`, subscriptions go over graphql-ws as in * {@link layerWebSocket}, and the layer needs a `Socket.WebSocketConstructor`. * * To route each query, mutation and graphql-sse subscription, `url` can be a * function of the calling fiber's context (including the call's `context` * option), `kind` and `operationName`, or an `Effect` with no errors * or requirements that runs in that context. Use a defaulted * `Context.Reference` to select the server. Reading a missing service with * `Context.get` is a defect, not a `TransportError`. Neither form can be * combined with `subscriptions.webSocket`: the socket URL is fixed at * connection time. * * **Gotchas** * * `GraphQLResolver` resolves each lookup's URL once, in the calling fiber, * before middleware run and before the batched request exists, then sends * each batch to the URL it was grouped by. A single call resolves it in the * transport, after middleware. * So the URL must depend only on the context and the operation, and * middleware must not change `kind` or `operationName` or provide services * the URL reads. * * `RequestResolver.withCache` and `RequestResolver.persisted` key on the lookup, * not the URL, so results can leak across servers. For caching, use a separate * client and resolver per server, for example with `LayerMap`. * * **Example** (Building a client over HTTP) * * ```ts import.meta.vitest * import { Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * // A stand-in for a real HttpClient layer such as FetchHttpClient.layer * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response(JSON.stringify({ data: { viewer: { login: "tim" } } }), { * headers: { "content-type": "application/graphql-response+json" } * }) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer() * }).pipe( * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => { viewer: { login: "tim" } } * ``` * * **Example** (Queries over GET with a per-call URL param) * * ```ts import.meta.vitest * import { Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * // A stand-in server that answers with the method and the `preview` param * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request, url) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response( * JSON.stringify({ data: { viewer: { login: `${request.method} ${url.searchParams.get("preview")}` } } }), * { headers: { "content-type": "application/graphql-response+json" } } * ) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * return yield* client.Viewer(undefined, { urlParams: { preview: "draft-token" } }) * }).pipe( * Effect.provide(GraphQLProtocol.layerHttp({ url: "https://cms.example.com/graphql", queryMethod: "GET" })), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => { viewer: { login: "GET draft-token" } } * ``` * * **Example** (One client for github.com, GHE.com and GitHub Enterprise Server) * * ```ts import.meta.vitest * import { Context, Effect, Layer, Schema } from "effect" * import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol } from "effect/graphql" * import { HttpClient, HttpClientResponse } from "effect/http" * * const Viewer = GraphQL.query("Viewer", { * document: "query Viewer{viewer{login}}", * result: Schema.Struct({ viewer: Schema.Struct({ login: Schema.String }) }) * }) * * const GitHubHost = Context.Reference("GitHubHost", { defaultValue: () => "github.com" }) * * const Protocol = GraphQLProtocol.layerHttp({ * url: ({ context }) => { * const host = Context.get(context, GitHubHost) * if (host === "github.com") return "https://api.github.com/graphql" * if (host.endsWith(".ghe.com")) return `https://api.${host}/graphql` * return `https://${host}/api/graphql` * } * }) * * // Echo the request URL instead of calling GitHub. * const FakeServer = Layer.succeed( * HttpClient.HttpClient, * HttpClient.make((request, url) => * Effect.succeed(HttpClientResponse.fromWeb( * request, * new Response(JSON.stringify({ data: { viewer: { login: url.href } } }), { * headers: { "content-type": "application/graphql-response+json" } * }) * )) * ) * ) * * const program = Effect.gen(function*() { * const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer)) * const dotcom = yield* client.Viewer() * const cloud = yield* client.Viewer().pipe(Effect.provideService(GitHubHost, "acme.ghe.com")) * const server = yield* client.Viewer(undefined, { context: Context.make(GitHubHost, "github.example.com") }) * return [dotcom, cloud, server].map((result) => result.viewer.login) * }).pipe( * Effect.provide(Protocol), * Effect.provide(FakeServer) * ) * * await Effect.runPromise(program) // => ["https://api.github.com/graphql", "https://api.acme.ghe.com/graphql", "https://github.example.com/api/graphql"] * ``` * * **Example** (Subscriptions over graphql-ws, everything else over POST) * * ```ts import.meta.vitest * import { Effect } from "effect" * import { GraphQLProtocol } from "effect/graphql" * * const token = Effect.succeed("secret") * * // Provide an HttpClient and a Socket.WebSocketConstructor, such as * // FetchHttpClient.layer and NodeSocket.layerWebSocketConstructor * export const Protocol = GraphQLProtocol.layerHttp({ * url: "https://example.com/graphql", * subscriptions: { * webSocket: { * url: "wss://example.com/graphql", * connectionParams: Effect.map(token, (token) => ({ authorization: `Bearer ${token}` })) * } * } * }) * ``` * * @stability experimental * @category layers * @since 4.0.0 */ (options: { readonly url: Url; readonly queryMethod?: "GET" | "POST" | undefined; readonly subscriptions?: undefined; }): Layer.Layer; }; /** * Builds the graphql-ws transport as an effect, for composing transports by * hand. Most code uses {@link layerWebSocket}, which describes the options. * * @stability experimental * @category constructors * @since 4.0.0 */ export declare const makeWebSocket: (options: { readonly url: string; readonly headers?: Headers.Input | undefined; readonly connectionParams?: Effect.Effect | undefined, unknown, R> | undefined; readonly connectionAckTimeout?: Duration.Input | undefined; readonly keepAlive?: Duration.Input | false | undefined; readonly idleTimeout?: Duration.Input | undefined; }) => Effect.Effect; /** * The graphql-ws transport: every operation goes over one WebSocket using the * `graphql-transport-ws` subprotocol, with the `Socket.WebSocketConstructor` * in context. * * **Details** * * The socket connects lazily and multiplexes operations by id. It closes after * `idleTimeout` (default 0) with no active operations, or with code `1000` * when the layer's scope closes. * * - Operations wait for `connection_ack`. `connectionAckTimeout` (default * 10 seconds) starts after sending `connection_init`. * - `connectionParams` runs on every connection, allowing token refresh. * Failure or a socket close during evaluation is a `TransportError`. * - Handshake `headers` accept `Headers.Input` and are normalized before * reaching the socket. They require a Node or Bun WebSocket constructor; * browsers cannot set them. Per-operation `headers` and `urlParams` are ignored. * - Results carry no HTTP `status`. * - `keepAlive` (default 10 seconds, or `false` to disable) sends `ping`. * No server frame in the following interval causes a retryable failure. * Server `ping` receives `pong`. * - Lost connections fail every active operation with a `TransportError` * carrying the close code. See `GraphQLClientError.isFatalCloseCode` for * non-retryable codes. The transport never replays operations; the client * retries subscriptions and the next operation reconnects. * - Once the layer's scope has closed, every new operation fails at once * with a non-retryable `TransportError` marked `fatal`. * - Messages follow graphql-ws validation. Invalid messages close the socket * with fatal code `4400`; valid messages for unknown ids are ignored. * - Server `error` messages become `ResponseError` values. Interrupting a * subscription sends `complete`. * * **Example** (A client over graphql-ws) * * ```ts import.meta.vitest * import { Effect, Layer } from "effect" * import { GraphQLProtocol } from "effect/graphql" * import type { Socket } from "effect/socket" * * // Provide a Socket.WebSocketConstructor, such as * // Socket.layerWebSocketConstructorGlobal in a browser * export const Protocol: Layer.Layer = * GraphQLProtocol.layerWebSocket({ * url: "wss://example.com/graphql", * connectionParams: Effect.succeed({ authorization: "Bearer secret" }), * idleTimeout: "30 seconds" * }) * ``` * * @stability experimental * @category layers * @since 4.0.0 */ export declare const layerWebSocket: (options: { readonly url: string; readonly headers?: Headers.Input | undefined; readonly connectionParams?: Effect.Effect | undefined, unknown, R> | undefined; readonly connectionAckTimeout?: Duration.Input | undefined; readonly keepAlive?: Duration.Input | false | undefined; readonly idleTimeout?: Duration.Input | undefined; }) => Layer.Layer; export {}; //# sourceMappingURL=GraphQLProtocol.d.ts.map