GraphQLMiddleware
GraphQL-level middleware provided as Context.Service implementations.
Middleware can rewrite requests and inspect raw results, including response
extensions and HTTP headers. Provide implementations with Layer.succeed
or Layer.effect.
Constructors
Builds an Implementation that transforms requests for queries, mutations and subscriptions. Use it to set headers, auth or extensions.
Signature
declare function mapRequest<E = never, R = never>(f: (request: GraphQLRequest, operation: Any) => Effect<GraphQLRequest, E, R>): Implementation<E, R>Example Adding a header to every request
import { Effect, Layer, Schema, Stream } from "effect"import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLMiddleware, GraphQLProtocol } from "effect/graphql"import { Headers } from "effect/http"
class RequestId extends GraphQLMiddleware.Service<RequestId>()("app/RequestId") {}
const RequestIdLive = Layer.succeed( RequestId, GraphQLMiddleware.mapRequest((request) => Effect.succeed({ ...request, headers: Headers.set(request.headers, "x-request-id", "abc") }) ))
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: (request) => Effect.succeed({ body: { data: { viewer: { login: request.headers["x-request-id"] } } } }), subscribe: () => Stream.empty})
const program = Effect.gen(function*() { const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer).middleware(RequestId)) return yield* client.Viewer()})
await Effect.runPromise(program.pipe(Effect.provide([Scripted, RequestIdLive]))) // => { viewer: { login: "abc" } }Declares a middleware tag. requires are services the implementation needs
on every call, and error is what it can fail with; both show up on the
requirements and error type of every client method the middleware is
attached to.
Signature
declare function Service<Self, Config extends { readonly error?: any; readonly requires?: any;} = {}>(): <Key extends string>(key: Key) => ServiceClass<Self, Key, Config extends { readonly error: E;} ? E : never, Config extends { readonly requires: R;} ? R : never>Example An auth middleware that needs the current user
import { Context, Effect, Layer, Schema, Stream } from "effect"import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLMiddleware, GraphQLProtocol } from "effect/graphql"import { Headers } from "effect/http"
class CurrentUser extends Context.Service<CurrentUser, { readonly token: string }>()("CurrentUser") {}class TokenExpired extends Schema.TaggedError<TokenExpired>()("TokenExpired", {}) {}
class Auth extends GraphQLMiddleware.Service<Auth, { requires: CurrentUser error: TokenExpired}>()("app/Auth") {}
const AuthLive = Layer.succeed( Auth, GraphQLMiddleware.mapRequest(Effect.fnUntraced(function*(request) { const user = yield* CurrentUser if (user.token === "") return yield* new TokenExpired() return { ...request, headers: Headers.set(request.headers, "authorization", `Bearer ${user.token}`) } })))
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: (request) => Effect.succeed({ body: { data: { viewer: { login: request.headers.authorization } } } }), subscribe: () => Stream.empty})
const program = Effect.gen(function*() { const client = yield* GraphQLClient.make(GraphQLGroup.make(Viewer).middleware(Auth)) // CurrentUser is required by the method because Auth is attached return yield* client.Viewer().pipe(Effect.provideService(CurrentUser, { token: "abc" }))})
await Effect.runPromise(program.pipe(Effect.provide([Scripted, AuthLive]))) // => { viewer: { login: "Bearer abc" } }Models
Implementation
interfaceMiddleware handlers for queries, mutations and subscriptions. Both
execute and subscribe are required.
Details
execute runs once per query or mutation. subscribe runs once per
subscription attempt (the client re-runs the whole chain when it retries a
subscription) and can map or tap the event stream.
Both receive the operation being run, the request so far, and next, which
runs the rest of the chain with the given request and returns the raw
ExecutionResult (or, for subscribe, the stream of them). Its status
and headers are the HTTP status and response headers the transport
reported, if any; a middleware may rewrite them, and a failed call's
ResponseError carries the final values. Request and response headers
are Headers.Headers values; use Headers.set to rewrite them.
next fails with GraphQLClientError, or with the error of a middleware
further in, which is typed as Types.unhandled, as in RpcMiddleware.
Narrow it, for example with Effect.catchTag("GraphQLClientError", ...),
before reading reason.
Use mapRequest when the middleware only needs to transform the request.
Signature
interface Implementation<E = never, R = never> { readonly execute: (options: { readonly next: (request: GraphQLRequest) => Effect<{ readonly data?: unknown; readonly errors?: readonly Array<{ [key: string]: Json; readonly extensions?: { [key: string]: Json; }; readonly locations?: readonly Array<{ readonly column: ...; readonly line: ...; }>; readonly message: string; readonly path?: readonly Array<... | ...>; }>; readonly extensions?: { [key: string]: Json; }; readonly headers?: Headers; readonly status?: number; }, unhandled | GraphQLClientError>; readonly operation: Any; readonly request: GraphQLRequest; }) => Effect<{ readonly data?: unknown; readonly errors?: readonly Array<{ [key: string]: Json; readonly extensions?: { [key: string]: Json; }; readonly locations?: readonly Array<{ readonly column: number; readonly line: number; }>; readonly message: string; readonly path?: readonly Array<string | number>; }>; readonly extensions?: { [key: string]: Json; }; readonly headers?: Headers; readonly status?: number; }, unhandled | GraphQLClientError | E, R>; readonly subscribe: (options: { readonly next: (request: GraphQLRequest) => Stream<{ readonly data?: unknown; readonly errors?: readonly Array<{ [key: string]: Json; readonly extensions?: { [key: string]: Json; }; readonly locations?: readonly Array<{ readonly column: ...; readonly line: ...; }>; readonly message: string; readonly path?: readonly Array<... | ...>; }>; readonly extensions?: { [key: string]: Json; }; readonly headers?: Headers; readonly status?: number; }, unhandled | GraphQLClientError>; readonly operation: Any; readonly request: GraphQLRequest; }) => Stream<{ readonly data?: unknown; readonly errors?: readonly Array<{ [key: string]: Json; readonly extensions?: { [key: string]: Json; }; readonly locations?: readonly Array<{ readonly column: number; readonly line: number; }>; readonly message: string; readonly path?: readonly Array<string | number>; }>; readonly extensions?: { [key: string]: Json; }; readonly headers?: Headers; readonly status?: number; }, unhandled | GraphQLClientError | E, R>;}Example Reading a rate-limit header
import { Effect } from "effect"import { GraphQLMiddleware } from "effect/graphql"import { Headers } from "effect/http"
const rateLimit: GraphQLMiddleware.Implementation = { execute: ({ next, request }) => Effect.tap(next(request), (result) => Effect.log(result.headers === undefined ? undefined : Headers.get(result.headers, "x-ratelimit-remaining")) ), subscribe: ({ next, request }) => next(request)}Services
ServiceClass
interfaceThe class returned by Service: a Context.Service for an
Implementation, carrying the declared error and requires types.
Signature
interface ServiceClass<Self, Key extends string, E, R> extends Service<Self, Implementation<E, R>> { constructor(_: never); readonly "~effect/graphql/GraphQLMiddleware": { readonly error: E; readonly requires: R; }; readonly key: Key;}Utility Types
AnyService
interfaceAny middleware service class, with its error and requirement types erased.
Signature
interface AnyService extends Service<any, Implementation<any, any>> { readonly "~effect/graphql/GraphQLMiddleware": { readonly error: any; readonly requires: any; };}Extracts the error type a middleware declared.
Signature
type Error<M> = M extends { readonly "~effect/graphql/GraphQLMiddleware": { readonly error: infer E; };} ? E : neverIdentifier
typeExtracts the service identifier of a middleware, which is what
GraphQLClient.make requires.
Signature
type Identifier<M> = M extends Context.Service<infer I, any> ? I : neverExtracts the services a middleware declared it requires per call.
Signature
type Requires<M> = M extends { readonly "~effect/graphql/GraphQLMiddleware": { readonly requires: infer R; };} ? R : never