Skip to content
Effect 4.0 is here. One ecosystem. Zero dependencies.

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.

8 exports Since v4.0.0 Source

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" } }
Since v4.0.0

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

Since v4.0.0

Middleware 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

interface
Since v4.0.0

The 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

interface
Since v4.0.0

Any 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;
};
}

Error

type
Since v4.0.0

Extracts the error type a middleware declared.

Signature

type Error<M> = M extends {
readonly "~effect/graphql/GraphQLMiddleware": {
readonly error: infer E;
};
} ? E : never
Since v4.0.0

Extracts the service identifier of a middleware, which is what GraphQLClient.make requires.

Signature

type Identifier<M> = M extends Context.Service<infer I, any> ? I : never
Since v4.0.0

Extracts 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