UNPKG

graphql-sse

Version:

Zero-dependency, HTTP/1 safe, simple, GraphQL over Server-Sent Events Protocol server and client

243 lines (242 loc) 10.8 kB
/** * * handler * */ import { ExecutionArgs, GraphQLSchema, validate as graphqlValidate } from 'graphql'; import { ExecutionResult, ExecutionPatchResult, RequestParams } from './common.mjs'; /** * The incoming request headers the implementing server should provide. * * @category Server */ export interface RequestHeaders { get: (key: string) => string | null | undefined; } /** * Server agnostic request interface containing the raw request * which is server dependant. * * @category Server */ export interface Request<Raw = unknown, Context = unknown> { readonly method: string; readonly url: string; readonly headers: RequestHeaders; /** * Parsed request body or a parser function. * * If the provided function throws, the error message "Unparsable JSON body" will * be in the erroneous response. */ readonly body: string | Record<PropertyKey, unknown> | null | (() => string | Record<PropertyKey, unknown> | null | Promise<string | Record<PropertyKey, unknown> | null>); /** * The raw request itself from the implementing server. */ readonly raw: Raw; /** * Context value about the incoming request, you're free to pass any information here. * * Intentionally not readonly because you're free to mutate it whenever you want. */ context: Context; } /** * The response headers that get returned from graphql-sse. * * @category Server */ export type ResponseHeaders = { accept?: string; allow?: string; 'content-type'?: string; } & Record<string, string>; /** * Server agnostic response body returned from `graphql-sse` needing * to be coerced to the server implementation in use. * * When the body is a string, it is NOT a GraphQL response. * * @category Server */ export type ResponseBody = string | AsyncGenerator<string, void, undefined>; /** * Server agnostic response options (ex. status and headers) returned from * `graphql-sse` needing to be coerced to the server implementation in use. * * @category Server */ export interface ResponseInit { readonly status: number; readonly statusText: string; readonly headers?: ResponseHeaders; } /** * Server agnostic response returned from `graphql-sse` containing the * body and init options needing to be coerced to the server implementation in use. * * @category Server */ export type Response = readonly [body: ResponseBody | null, init: ResponseInit]; /** * A concrete GraphQL execution context value type. * * Mainly used because TypeScript collapses unions * with `any` or `unknown` to `any` or `unknown`. So, * we use a custom type to allow definitions such as * the `context` server option. * * @category Server */ export type OperationContext = Record<PropertyKey, unknown> | symbol | number | string | boolean | undefined | null; /** @category Server */ export type OperationArgs<Context extends OperationContext = undefined> = ExecutionArgs & { contextValue: Context; }; /** @category Server */ export type OperationResult = Promise<AsyncGenerator<ExecutionResult | ExecutionPatchResult> | AsyncIterable<ExecutionResult | ExecutionPatchResult> | ExecutionResult> | AsyncGenerator<ExecutionResult | ExecutionPatchResult> | AsyncIterable<ExecutionResult | ExecutionPatchResult> | ExecutionResult; /** @category Server */ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown, Context extends OperationContext = undefined> { /** * A custom GraphQL validate function allowing you to apply your * own validation rules. */ validate?: typeof graphqlValidate; /** * Is the `execute` function from GraphQL which is * used to execute the query and mutation operations. */ execute?: (args: OperationArgs<Context>) => OperationResult; /** * Is the `subscribe` function from GraphQL which is * used to execute the subscription operation. */ subscribe?: (args: OperationArgs<Context>) => OperationResult; /** * The GraphQL schema on which the operations will * be executed and validated against. * * If a function is provided, it will be called on every * subscription request allowing you to manipulate schema * dynamically. * * If the schema is left undefined, you're trusted to * provide one in the returned `ExecutionArgs` from the * `onSubscribe` callback. */ schema?: GraphQLSchema | ((req: Request<RequestRaw, RequestContext>, args: Pick<OperationArgs<Context>, 'contextValue' | 'operationName' | 'document' | 'variableValues'>) => Promise<GraphQLSchema> | GraphQLSchema); /** * Authenticate the client. Returning a string indicates that the client * is authenticated and the request is ready to be processed. * * A distinct token of type string must be supplied to enable the "single connection mode". * * Providing `null` as the token will completely disable the "single connection mode" * and all incoming requests will always use the "distinct connection mode". * * @default 'req.headers["x-graphql-event-stream-token"] || req.url.searchParams["token"] || generateRandomUUID()' // https://gist.github.com/jed/982883 */ authenticate?: (req: Request<RequestRaw, RequestContext>) => Promise<Response | string | undefined | null> | Response | string | undefined | null; /** * Called when a new event stream is connecting BEFORE it is accepted. * By accepted, its meant the server processed the request and responded * with a 200 (OK), alongside flushing the necessary event stream headers. */ onConnect?: (req: Request<RequestRaw, RequestContext>) => Promise<Response | null | undefined | void> | Response | null | undefined | void; /** * A value which is provided to every resolver and holds * important contextual information like the currently * logged in user, or access to a database. * * Note that the context function is invoked on each operation only once. * Meaning, for subscriptions, only at the point of initialising the subscription; * not on every subscription event emission. Read more about the context lifecycle * in subscriptions here: https://github.com/graphql/graphql-js/issues/894. * * If you don't provide the context context field, but have a context - you're trusted to * provide one in `onSubscribe`. */ context?: Context | ((req: Request<RequestRaw, RequestContext>, params: RequestParams) => Promise<Context> | Context); /** * The subscribe callback executed right after processing the request * before proceeding with the GraphQL operation execution. * * If you return `ExecutionArgs` from the callback, it will be used instead of * trying to build one internally. In this case, you are responsible for providing * a ready set of arguments which will be directly plugged in the operation execution. * * Omitting the fields `contextValue` from the returned `ExecutionArgs` will use the * provided `context` option, if available. * * If you want to respond to the client with a custom status or body, * you should do so using the provided `res` argument which will stop * further execution. * * Useful for preparing the execution arguments following a custom logic. A typical * use-case is persisted queries. You can identify the query from the request parameters * and supply the appropriate GraphQL operation execution arguments. */ onSubscribe?: (req: Request<RequestRaw, RequestContext>, params: RequestParams) => Promise<Response | OperationResult | OperationArgs<Context> | void> | Response | OperationResult | OperationArgs<Context> | void; /** * Executed after the operation call resolves. For streaming * operations, triggering this callback does not necessarily * mean that there is already a result available - it means * that the subscription process for the stream has resolved * and that the client is now subscribed. * * The `OperationResult` argument is the result of operation * execution. It can be an iterator or already a value. * * If you want the single result and the events from a streaming * operation, use the `onNext` callback. * * If `onSubscribe` returns an `OperationResult`, this hook * will NOT be called. */ onOperation?: (ctx: Context, req: Request<RequestRaw, RequestContext>, args: ExecutionArgs, result: OperationResult) => Promise<OperationResult | void> | OperationResult | void; /** * Executed after an operation has emitted a result right before * that result has been sent to the client. * * Results from both single value and streaming operations will * invoke this callback. * * Use this callback if you want to format the execution result * before it reaches the client. * * @param req - Always the request that contains the GraphQL operation. */ onNext?: (ctx: Context, req: Request<RequestRaw, RequestContext>, result: ExecutionResult | ExecutionPatchResult) => Promise<ExecutionResult | ExecutionPatchResult | void> | ExecutionResult | ExecutionPatchResult | void; /** * The complete callback is executed after the operation * has completed and the client has been notified. * * Since the library makes sure to complete streaming * operations even after an abrupt closure, this callback * will always be called. * * @param req - Always the request that contains the GraphQL operation. */ onComplete?: (ctx: Context, req: Request<RequestRaw, RequestContext>) => Promise<void> | void; } /** * The ready-to-use handler. Simply plug it in your favourite fetch-enabled HTTP * framework and enjoy. * * Errors thrown from **any** of the provided options or callbacks (or even due to * library misuse or potential bugs) will reject the handler's promise. They are * considered internal errors and you should take care of them accordingly. * * @category Server */ export type Handler<RequestRaw = unknown, RequestContext = unknown> = (req: Request<RequestRaw, RequestContext>) => Promise<Response>; /** * Makes a Protocol compliant HTTP GraphQL server handler. The handler can * be used with your favourite server library. * * Read more about the Protocol in the PROTOCOL.md documentation file. * * @category Server */ export declare function createHandler<RequestRaw = unknown, RequestContext = unknown, Context extends OperationContext = undefined>(options: HandlerOptions<RequestRaw, RequestContext, Context>): Handler<RequestRaw, RequestContext>; export declare function isExecutionResult(val: unknown): val is ExecutionResult;