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
TypeScript
/**
*
* handler
*
*/
import { ExecutionArgs, GraphQLSchema, validate as graphqlValidate } from 'graphql';
import { ExecutionResult, ExecutionPatchResult, RequestParams } from './common';
/**
* 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;