From 22cf03d3c214019a3bf538742bbceac766c17353 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Tue, 20 Dec 2022 15:20:49 +0100 Subject: [PATCH] feat(handler): Server and environment agnostic handler (#37) BREAKING CHANGE: The handler is now server agnostic and can run _anywhere_ - Core of `graphql-sse` is now server agnostic and as such offers a handler that implements a generic request/response model - Handler does not await for whole operation to complete anymore. Only the processing part (parsing, validating and executing) - GraphQL context is now typed - Hook arguments have been changed, they're not providing the Node native req/res anymore - they instead provide the generic request/response - `onSubscribe` hook can now return an execution result too (useful for caching for example) - Throwing in `onNext` and `onComplete` hooks will bubble the error to the returned iterator ### Migration Even though the core of graphql-sse is now completely server agnostic, there are adapters to ease the integration with existing solutions. Migrating is actually not a headache! Beware that the adapters **don't** handle internal errors, it's your responsibility to take care of that and behave accordingly. #### [`http`](https://nodejs.org/api/http.html) ```diff import http from 'http'; - import { createHandler } from 'graphql-sse'; + import { createHandler } from 'graphql-sse/lib/use/http'; // Create the GraphQL over SSE handler const handler = createHandler({ schema }); // Create an HTTP server using the handler on `/graphql/stream` const server = http.createServer((req, res) => { if (req.url.startsWith('/graphql/stream')) { return handler(req, res); } res.writeHead(404).end(); }); server.listen(4000); console.log('Listening to port 4000'); ``` #### [`http2`](https://nodejs.org/api/http2.html) ```diff import fs from 'fs'; import http2 from 'http2'; - import { createHandler } from 'graphql-sse'; + import { createHandler } from 'graphql-sse/lib/use/http2'; // Create the GraphQL over SSE handler const handler = createHandler({ schema }); // Create an HTTP server using the handler on `/graphql/stream` const server = http.createServer((req, res) => { if (req.url.startsWith('/graphql/stream')) { return handler(req, res); } res.writeHead(404).end(); }); server.listen(4000); console.log('Listening to port 4000'); ``` #### [`express`](https://expressjs.com/) ```diff import express from 'express'; // yarn add express - import { createHandler } from 'graphql-sse'; + import { createHandler } from 'graphql-sse/lib/use/express'; // Create the GraphQL over SSE handler const handler = createHandler({ schema }); // Create an express app const app = express(); // Serve all methods on `/graphql/stream` app.use('/graphql/stream', handler); server.listen(4000); console.log('Listening to port 4000'); ``` #### [`fastify`](https://www.fastify.io/) ```diff import Fastify from 'fastify'; // yarn add fastify - import { createHandler } from 'graphql-sse'; + import { createHandler } from 'graphql-sse/lib/use/fastify'; // Create the GraphQL over SSE handler const handler = createHandler({ schema }); // Create a fastify app const fastify = Fastify(); // Serve all methods on `/graphql/stream` fastify.all('/graphql/stream', handler); fastify.listen({ port: 4000 }); console.log('Listening to port 4000'); ``` --- .github/workflows/continuous-integration.yml | 3 +- README.md | 286 ++-- docs/README.md | 324 +---- ...NetworkError.md => client.NetworkError.md} | 8 +- docs/interfaces/HandlerOptions.md | 406 ------ .../{Client.md => client.Client.md} | 12 +- ...ientOptions.md => client.ClientOptions.md} | 42 +- ...sult.md => common.ExecutionPatchResult.md} | 16 +- ...ionResult.md => common.ExecutionResult.md} | 12 +- ...questParams.md => common.RequestParams.md} | 12 +- docs/interfaces/{Sink.md => common.Sink.md} | 10 +- ...reamMessage.md => common.StreamMessage.md} | 12 +- docs/interfaces/handler.HandlerOptions.md | 345 +++++ docs/interfaces/handler.Request.md | 73 + docs/interfaces/handler.RequestHeaders.md | 33 + docs/interfaces/handler.ResponseInit.md | 34 + docs/interfaces/use_express.RequestContext.md | 17 + docs/interfaces/use_fastify.RequestContext.md | 17 + docs/interfaces/use_fetch.RequestContext.md | 107 ++ docs/interfaces/use_http.RequestContext.md | 17 + docs/interfaces/use_http2.RequestContext.md | 17 + docs/modules/client.md | 159 +++ docs/modules/common.md | 195 +++ docs/modules/handler.md | 148 ++ docs/modules/use_express.md | 81 ++ docs/modules/use_fastify.md | 80 ++ docs/modules/use_fetch.md | 76 ++ docs/modules/use_http.md | 79 ++ docs/modules/use_http2.md | 79 ++ jest.config.js | 8 +- package.json | 36 +- src/__tests__/__snapshots__/client.ts.snap | 23 - src/__tests__/__snapshots__/handler.ts.snap | 336 ++--- src/__tests__/__snapshots__/parser.ts.snap | 64 +- src/__tests__/client.ts | 613 ++++----- src/__tests__/fixtures/simple.ts | 6 +- src/__tests__/handler.ts | 849 ++++++------ src/__tests__/jest.d.ts | 6 - src/__tests__/utils/eventStream.ts | 45 - src/__tests__/utils/testkit.ts | 73 + src/__tests__/utils/tfetch.ts | 49 + src/__tests__/utils/thandler.ts | 86 ++ src/__tests__/utils/tserver.ts | 259 ---- src/__tests__/utils/tsubscribe.ts | 79 +- src/common.ts | 35 + src/handler.ts | 1206 ++++++++++------- src/use/express.ts | 99 ++ src/use/fastify.ts | 100 ++ src/use/fetch.ts | 105 ++ src/use/http.ts | 94 ++ src/use/http2.ts | 96 ++ tsconfig.json | 1 - typedoc.js | 1 + yarn.lock | 911 ++++++------- 54 files changed, 4633 insertions(+), 3247 deletions(-) rename docs/classes/{NetworkError.md => client.NetworkError.md} (84%) delete mode 100644 docs/interfaces/HandlerOptions.md rename docs/interfaces/{Client.md => client.Client.md} (73%) rename docs/interfaces/{ClientOptions.md => client.ClientOptions.md} (84%) rename docs/interfaces/{ExecutionPatchResult.md => common.ExecutionPatchResult.md} (58%) rename docs/interfaces/{ExecutionResult.md => common.ExecutionResult.md} (61%) rename docs/interfaces/{RequestParams.md => common.RequestParams.md} (61%) rename docs/interfaces/{Sink.md => common.Sink.md} (77%) rename docs/interfaces/{StreamMessage.md => common.StreamMessage.md} (55%) create mode 100644 docs/interfaces/handler.HandlerOptions.md create mode 100644 docs/interfaces/handler.Request.md create mode 100644 docs/interfaces/handler.RequestHeaders.md create mode 100644 docs/interfaces/handler.ResponseInit.md create mode 100644 docs/interfaces/use_express.RequestContext.md create mode 100644 docs/interfaces/use_fastify.RequestContext.md create mode 100644 docs/interfaces/use_fetch.RequestContext.md create mode 100644 docs/interfaces/use_http.RequestContext.md create mode 100644 docs/interfaces/use_http2.RequestContext.md create mode 100644 docs/modules/client.md create mode 100644 docs/modules/common.md create mode 100644 docs/modules/handler.md create mode 100644 docs/modules/use_express.md create mode 100644 docs/modules/use_fastify.md create mode 100644 docs/modules/use_fetch.md create mode 100644 docs/modules/use_http.md create mode 100644 docs/modules/use_http2.md delete mode 100644 src/__tests__/__snapshots__/client.ts.snap delete mode 100644 src/__tests__/jest.d.ts delete mode 100644 src/__tests__/utils/eventStream.ts create mode 100644 src/__tests__/utils/testkit.ts create mode 100644 src/__tests__/utils/tfetch.ts create mode 100644 src/__tests__/utils/thandler.ts delete mode 100644 src/__tests__/utils/tserver.ts create mode 100644 src/use/express.ts create mode 100644 src/use/fastify.ts create mode 100644 src/use/fetch.ts create mode 100644 src/use/http.ts create mode 100644 src/use/http2.ts diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 567a03b6..277747ff 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -59,4 +59,5 @@ jobs: - name: Install run: yarn install --immutable - name: Test - run: yarn test --forceExit + timeout-minutes: 1 # detectOpenHandles will probably hang if leaking + run: yarn test --detectOpenHandles diff --git a/README.md b/README.md index 37436c68..c5f0b144 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ yarn add graphql-sse #### Create a GraphQL schema -```ts +```js import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; /** @@ -65,19 +65,20 @@ const schema = new GraphQLSchema({ ##### With [`http`](https://nodejs.org/api/http.html) -```ts +```js import http from 'http'; -import { createHandler } from 'graphql-sse'; +import { createHandler } from 'graphql-sse/lib/use/http'; +import { schema } from './previous-step'; // Create the GraphQL over SSE handler -const handler = createHandler({ - schema, // from the previous step -}); +const handler = createHandler({ schema }); -// Create a HTTP server using the handler on `/graphql/stream` +// Create an HTTP server using the handler on `/graphql/stream` const server = http.createServer((req, res) => { - if (req.url.startsWith('/graphql/stream')) return handler(req, res); - return res.writeHead(404).end(); + if (req.url.startsWith('/graphql/stream')) { + return handler(req, res); + } + res.writeHead(404).end(); }); server.listen(4000); @@ -93,75 +94,116 @@ $ openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \ -keyout localhost-privkey.pem -out localhost-cert.pem ``` -```ts +```js import fs from 'fs'; import http2 from 'http2'; -import { createHandler } from 'graphql-sse'; +import { createHandler } from 'graphql-sse/lib/use/http2'; +import { schema } from './previous-step'; // Create the GraphQL over SSE handler -const handler = createHandler({ - schema, // from the previous step -}); +const handler = createHandler({ schema }); -// Create a HTTP/2 server using the handler on `/graphql/stream` -const server = http2.createSecureServer( - { - key: fs.readFileSync('localhost-privkey.pem'), - cert: fs.readFileSync('localhost-cert.pem'), - }, - (req, res) => { - if (req.url.startsWith('/graphql/stream')) return handler(req, res); - return res.writeHead(404).end(); - }, -); +// Create an HTTP server using the handler on `/graphql/stream` +const server = http.createServer((req, res) => { + if (req.url.startsWith('/graphql/stream')) { + return handler(req, res); + } + res.writeHead(404).end(); +}); server.listen(4000); console.log('Listening to port 4000'); ``` -##### With [`express`](https://expressjs.com/) +##### With [`express`](https://expressjs.com) -```ts +```js import express from 'express'; // yarn add express -import { createHandler } from 'graphql-sse'; +import { createHandler } from 'graphql-sse/lib/use/express'; +import { schema } from './previous-step'; // Create the GraphQL over SSE handler const handler = createHandler({ schema }); -// Create an express app serving all methods on `/graphql/stream` +// Create an express app const app = express(); + +// Serve all methods on `/graphql/stream` app.use('/graphql/stream', handler); -app.listen(4000); +server.listen(4000); console.log('Listening to port 4000'); ``` -##### With [`fastify`](https://www.fastify.io/) +##### With [`fastify`](https://www.fastify.io) -```ts +```js import Fastify from 'fastify'; // yarn add fastify -import { createHandler } from 'graphql-sse'; +import { createHandler } from 'graphql-sse/lib/use/fastify'; // Create the GraphQL over SSE handler const handler = createHandler({ schema }); -// Create a fastify instance serving all methods on `/graphql/stream` +// Create a fastify app const fastify = Fastify(); -fastify.all('/graphql/stream', (req, res) => - handler( - req.raw, - res.raw, - req.body, // fastify reads the body for you - ), -); -fastify.listen(4000); +// Serve all methods on `/graphql/stream` +fastify.all('/graphql/stream', handler); + +fastify.listen({ port: 4000 }); console.log('Listening to port 4000'); ``` -#### Use the client +##### With [`Deno`](https://deno.land/) ```ts +import { serve } from 'https://deno.land/std/http/server.ts'; +import { createHandler } from 'https://esm.sh/graphql-sse/lib/use/fetch'; +import { schema } from './previous-step'; + +// Create the GraphQL over SSE native fetch handler +const handler = createHandler({ schema }); + +// Serve on `/graphql/stream` using the handler +await serve( + (req: Request) => { + const [path, _search] = req.url.split('?'); + if (path.endsWith('/graphql/stream')) { + return await handler(req); + } + return new Response(null, { status: 404 }); + }, + { + port: 4000, // Listening to port 4000 + }, +); +``` + +##### With [`Bun`](https://bun.sh/) + +```js +import { createHandler } from 'graphql-sse/lib/use/fetch'; // bun install graphql-sse +import { schema } from './previous-step'; + +// Create the GraphQL over SSE native fetch handler +const handler = createHandler({ schema }); + +// Serve on `/graphql/stream` using the handler +export default { + port: 4000, // Listening to port 4000 + async fetch(req) { + const [path, _search] = req.url.split('?'); + if (path.endsWith('/graphql/stream')) { + return await handler(req); + } + return new Response(null, { status: 404 }); + }, +}; +``` + +#### Use the client + +```js import { createClient } from 'graphql-sse'; const client = createClient({ @@ -221,13 +263,13 @@ const client = createClient({ 🔗 Client usage with Promise ```ts -import { createClient, SubscribePayload } from 'graphql-sse'; +import { createClient, RequestParams } from 'graphql-sse'; const client = createClient({ url: 'http://hey.there:4000/graphql/stream', }); -async function execute(payload: SubscribePayload) { +export async function execute(payload: RequestParams) { return new Promise((resolve, reject) => { let result: T; client.subscribe(payload, { @@ -257,22 +299,19 @@ async function execute(payload: SubscribePayload) {
🔗 Client usage with AsyncIterator -```ts -import { createClient, SubscribePayload } from 'graphql-sse'; +```js +import { createClient, RequestParams } from 'graphql-sse'; const client = createClient({ url: 'http://iterators.ftw:4000/graphql/stream', }); -function subscribe(payload: SubscribePayload): AsyncGenerator { - let deferred: { - resolve: (done: boolean) => void; - reject: (err: unknown) => void; - } | null = null; - const pending: T[] = []; - let throwMe: unknown = null, +export function subscribe(payload) { + let deferred = null; + const pending = []; + let throwMe = null, done = false; - const dispose = client.subscribe(payload, { + const dispose = client.subscribe(payload, { next: (data) => { pending.push(data); deferred?.resolve(false); @@ -293,17 +332,21 @@ function subscribe(payload: SubscribePayload): AsyncGenerator { async next() { if (done) return { done: true, value: undefined }; if (throwMe) throw throwMe; - if (pending.length) return { value: pending.shift()! }; - return (await new Promise( + if (pending.length) return { value: pending.shift() }; + return (await new Promise( (resolve, reject) => (deferred = { resolve, reject }), )) ? { done: true, value: undefined } - : { value: pending.shift()! }; + : { value: pending.shift() }; }, async throw(err) { - throw err; + throwMe = err; + deferred?.reject(throwMe); + return { done: true, value: undefined }; }, async return() { + done = true; + deferred?.resolve(true); dispose(); return { done: true, value: undefined }; }, @@ -328,7 +371,7 @@ function subscribe(payload: SubscribePayload): AsyncGenerator {
🔗 Client usage with Observable -```ts +```js import { Observable } from 'relay-runtime'; // or import { Observable } from '@apollo/client/core'; @@ -342,7 +385,7 @@ const client = createClient({ url: 'http://graphql.loves:4000/observables', }); -function toObservable(operation) { +export function toObservable(operation) { return new Observable((observer) => client.subscribe(operation, { next: (data) => observer.next(data), @@ -370,7 +413,7 @@ subscription.unsubscribe();
🔗 Client usage with Relay -```ts +```js import { GraphQLError } from 'graphql'; import { Network, @@ -416,15 +459,15 @@ export const network = Network.create(fetchOrSubscribe, fetchOrSubscribe);
🔗 Client usage with urql -```ts +```js import { createClient, defaultExchanges, subscriptionExchange } from 'urql'; -import { createClient as createWSClient } from 'graphql-sse'; +import { createClient as createSSEClient } from 'graphql-sse'; -const sseClient = createWSClient({ +const sseClient = createSSEClient({ url: 'http://its.urql:4000/graphql/stream', }); -const client = createClient({ +export const client = createClient({ url: '/graphql/stream', exchanges: [ ...defaultExchanges, @@ -449,7 +492,7 @@ const client = createClient({
🔗 Client usage with Apollo -```typescript +```ts import { ApolloLink, Operation, @@ -481,7 +524,7 @@ class SSELink extends ApolloLink { } } -const link = new SSELink({ +export const link = new SSELink({ url: 'http://where.is:4000/graphql/stream', headers: () => { const session = getSession(); @@ -498,10 +541,10 @@ const link = new SSELink({
🔗 Client usage for HTTP/1 (aka. single connection mode) -```typescript +```js import { createClient } from 'graphql-sse'; -const client = createClient({ +export const client = createClient({ singleConnection: true, // this is literally it 😄 url: 'http://use.single:4000/connection/graphql/stream', // lazy: true (default) -> connect on first subscribe and disconnect on last unsubscribe @@ -518,13 +561,13 @@ const client = createClient({
🔗 Client usage with custom retry timeout strategy -```typescript +```js import { createClient } from 'graphql-sse'; import { waitForHealthy } from './my-servers'; const url = 'http://i.want.retry:4000/control/graphql/stream'; -const client = createClient({ +export const client = createClient({ url, retryWait: async function waitForServerHealthyBeforeRetry() { // if you have a server healthcheck, you can wait for it to become @@ -545,10 +588,10 @@ const client = createClient({
🔗 Client usage with logging of incoming messages (browsers don't show them in the DevTools) -```typescript +```js import { createClient } from 'graphql-sse'; -const client = createClient({ +export const client = createClient({ url: 'http://let-me-see.messages:4000/graphql/stream', onMessage: console.log, }); @@ -587,14 +630,14 @@ const client = createClient({
🔗 Client usage in Node -```ts +```js const ws = require('ws'); // yarn add ws const fetch = require('node-fetch'); // yarn add node-fetch const { AbortController } = require('node-abort-controller'); // (node < v15) yarn add node-abort-controller const Crypto = require('crypto'); const { createClient } = require('graphql-sse'); -const client = createClient({ +export const client = createClient({ url: 'http://no.browser:4000/graphql/stream', fetchFn: fetch, abortControllerImpl: AbortController, // node < v15 @@ -607,8 +650,6 @@ const client = createClient({ (c ^ (Crypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16), ), }); - -// consider other recipes for usage inspiration ```
@@ -616,7 +657,7 @@ const client = createClient({
🔗 Server handler usage with custom authentication -```typescript +```js import { createHandler } from 'graphql-sse'; import { schema, @@ -625,10 +666,10 @@ import { processAuthorizationHeader, } from './my-graphql'; -const handler = createHandler({ +export const handler = createHandler({ schema, - authenticate: async (req, res) => { - let token = req.headers['x-graphql-event-stream-token']; + authenticate: async (req) => { + let token = req.headers.get('x-graphql-event-stream-token'); if (token) { // When the client is working in a "single connection mode" // all subsequent requests for operations will have the @@ -647,20 +688,25 @@ const handler = createHandler({ // of generating the token is completely up to the implementor. token = getOrCreateTokenFromCookies(req); // or - token = processAuthorizationHeader(req.headers['authorization']); + token = processAuthorizationHeader(req.headers.get('authorization')); // or token = await customAuthenticationTokenDiscovery(req); // Using the response argument the implementor may respond to // authentication issues however he sees fit. - if (!token) return res.writeHead(401, 'Unauthorized').end(); + if (!token) { + return [null, { status: 401, statusText: 'Unauthorized' }]; + } // Clients that operate in "distinct connections mode" dont // need a unique stream token. It is completely ok to simply // return an empty string for authenticated clients. // // Read more: https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#distinct-connections-mode - if (req.method === 'POST' && req.headers.accept === 'text/event-stream') { + if ( + req.method === 'POST' && + req.headers.get('accept') === 'text/event-stream' + ) { // "distinct connections mode" requests an event-stream with a POST // method. These two checks, together with the lack of `X-GraphQL-Event-Stream-Token` // header, are sufficient for accurate detection. @@ -675,8 +721,6 @@ const handler = createHandler({ return token; }, }); - -// use `handler` with your favourite http library ```
@@ -684,11 +728,11 @@ const handler = createHandler({
🔗 Server handler usage with dynamic schema -```typescript +```js import { createHandler } from 'graphql-sse'; import { schema, checkIsAdmin, getDebugSchema } from './my-graphql'; -const handler = createHandler({ +export const handler = createHandler({ schema: async (req, executionArgsWithoutSchema) => { // will be called on every subscribe request // allowing you to dynamically supply the schema @@ -698,8 +742,6 @@ const handler = createHandler({ return schema; }, }); - -// use `handler` with your favourite http library ```
@@ -707,19 +749,17 @@ const handler = createHandler({
🔗 Server handler usage with custom context value -```typescript +```js import { createHandler } from 'graphql-sse'; import { schema, getDynamicContext } from './my-graphql'; -const handler = createHandler({ +export const handler = createHandler({ schema, // or static context by supplying the value direcly - context: async (req, args) => { + context: (req, args) => { return getDynamicContext(req, args); }, }); - -// use `handler` with your favourite http library ```
@@ -727,27 +767,24 @@ const handler = createHandler({
🔗 Server handler usage with custom execution arguments -```typescript +```js import { parse } from 'graphql'; import { createHandler } from 'graphql-sse'; import { getSchema, myValidationRules } from './my-graphql'; -const handler = createHandler({ - onSubscribe: async (req, _res, params) => { +export const handler = createHandler({ + onSubscribe: async (req, params) => { const schema = await getSchema(req); - - const args = { + return { schema, operationName: params.operationName, - document: parse(params.query), + document: + typeof params.query === 'string' ? parse(params.query) : params.query, variableValues: params.variables, + contextValue: undefined, }; - - return args; }, }); - -// use `handler` with your favourite http library ```
@@ -755,12 +792,12 @@ const handler = createHandler({
🔗 Server handler and client usage with persisted queries -```typescript +```ts // 🛸 server import { parse, ExecutionArgs } from 'graphql'; import { createHandler } from 'graphql-sse'; -import { schema } from './my-graphql-schema'; +import { schema } from './my-graphql'; // a unique GraphQL execution ID used for representing // a query in the persisted queries store. when subscribing @@ -774,28 +811,25 @@ const queriesStore: Record = { }, }; -const handler = createHandler( - { - onSubscribe: (req, res, params) => { - const persistedQuery = queriesStore[params.extensions?.persistedQuery]; - if (persistedQuery) { - return { - ...persistedQuery, - variableValues: params.variables, // use the variables from the client - }; - } +export const handler = createHandler({ + onSubscribe: (_req, params) => { + const persistedQuery = + queriesStore[String(params.extensions?.persistedQuery)]; + if (persistedQuery) { + return { + ...persistedQuery, + variableValues: params.variables, // use the variables from the client + contextValue: undefined, + }; + } - // for extra security only allow the queries from the store - return res.writeHead(404, 'Query Not Found').end(); - }, + // for extra security only allow the queries from the store + return [null, { status: 404, statusText: 'Not Found' }]; }, - wsServer, -); - -// use `handler` with your favourite http library +}); ``` -```typescript +```ts // 📺 client import { createClient } from 'graphql-sse'; diff --git a/docs/README.md b/docs/README.md index c9f7547a..02cf0ffe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,317 +4,13 @@ graphql-sse ## Table of contents -### Classes - -- [NetworkError](classes/NetworkError.md) - -### Interfaces - -- [Client](interfaces/Client.md) -- [ClientOptions](interfaces/ClientOptions.md) -- [ExecutionPatchResult](interfaces/ExecutionPatchResult.md) -- [ExecutionResult](interfaces/ExecutionResult.md) -- [HandlerOptions](interfaces/HandlerOptions.md) -- [RequestParams](interfaces/RequestParams.md) -- [Sink](interfaces/Sink.md) -- [StreamMessage](interfaces/StreamMessage.md) - -### Type Aliases - -- [ExecutionContext](README.md#executioncontext) -- [Handler](README.md#handler) -- [NodeRequest](README.md#noderequest) -- [NodeResponse](README.md#noderesponse) -- [OperationResult](README.md#operationresult) -- [StreamData](README.md#streamdata) -- [StreamDataForID](README.md#streamdataforid) -- [StreamEvent](README.md#streamevent) - -### Variables - -- [TOKEN\_HEADER\_KEY](README.md#token_header_key) -- [TOKEN\_QUERY\_KEY](README.md#token_query_key) - -### Functions - -- [createClient](README.md#createclient) -- [createHandler](README.md#createhandler) -- [isAsyncGenerator](README.md#isasyncgenerator) -- [parseStreamData](README.md#parsestreamdata) -- [validateStreamEvent](README.md#validatestreamevent) - -## Client - -### createClient - -▸ **createClient**<`SingleConnection`\>(`options`): [`Client`](interfaces/Client.md) - -Creates a disposable GraphQL over SSE client to transmit -GraphQL operation results. - -If you have an HTTP/2 server, it is recommended to use the client -in "distinct connections mode" (`singleConnection = false`) which will -create a new SSE connection for each subscribe. This is the default. - -However, when dealing with HTTP/1 servers from a browser, consider using -the "single connection mode" (`singleConnection = true`) which will -use only one SSE connection. - -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `SingleConnection` | extends `boolean` = ``false`` | - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `options` | [`ClientOptions`](interfaces/ClientOptions.md)<`SingleConnection`\> | - -#### Returns - -[`Client`](interfaces/Client.md) - -## Common - -### StreamData - -Ƭ **StreamData**<`E`\>: `E` extends ``"next"`` ? [`ExecutionResult`](interfaces/ExecutionResult.md) \| [`ExecutionPatchResult`](interfaces/ExecutionPatchResult.md) : `E` extends ``"complete"`` ? ``null`` : `never` - -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `E` | extends [`StreamEvent`](README.md#streamevent) | - -___ - -### StreamDataForID - -Ƭ **StreamDataForID**<`E`\>: `E` extends ``"next"`` ? { `id`: `string` ; `payload`: [`ExecutionResult`](interfaces/ExecutionResult.md) \| [`ExecutionPatchResult`](interfaces/ExecutionPatchResult.md) } : `E` extends ``"complete"`` ? { `id`: `string` } : `never` - -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `E` | extends [`StreamEvent`](README.md#streamevent) | - -___ - -### StreamEvent - -Ƭ **StreamEvent**: ``"next"`` \| ``"complete"`` - -___ - -### TOKEN\_HEADER\_KEY - -• `Const` **TOKEN\_HEADER\_KEY**: ``"x-graphql-event-stream-token"`` - -Header key through which the event stream token is transmitted -when using the client in "single connection mode". - -Read more: https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#single-connection-mode - -___ - -### TOKEN\_QUERY\_KEY - -• `Const` **TOKEN\_QUERY\_KEY**: ``"token"`` - -URL query parameter key through which the event stream token is transmitted -when using the client in "single connection mode". - -Read more: https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#single-connection-mode - -___ - -### parseStreamData - -▸ **parseStreamData**<`ForID`, `E`\>(`e`, `data`): `ForID` extends ``true`` ? [`StreamDataForID`](README.md#streamdataforid)<`E`\> : [`StreamData`](README.md#streamdata)<`E`\> - -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `ForID` | extends `boolean` | -| `E` | extends [`StreamEvent`](README.md#streamevent) | - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `e` | `E` | -| `data` | `string` | - -#### Returns - -`ForID` extends ``true`` ? [`StreamDataForID`](README.md#streamdataforid)<`E`\> : [`StreamData`](README.md#streamdata)<`E`\> - -___ - -### validateStreamEvent - -▸ **validateStreamEvent**(`e`): [`StreamEvent`](README.md#streamevent) - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `e` | `unknown` | - -#### Returns - -[`StreamEvent`](README.md#streamevent) - -## Other - -### isAsyncGenerator - -▸ **isAsyncGenerator**<`T`\>(`val`): val is AsyncGenerator - -#### Type parameters - -| Name | -| :------ | -| `T` | - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `val` | `unknown` | - -#### Returns - -val is AsyncGenerator - -## Server - -### ExecutionContext - -Ƭ **ExecutionContext**: `object` \| `symbol` \| `number` \| `string` \| `boolean` \| `undefined` \| ``null`` - -A concrete GraphQL execution context value type. - -Mainly used because TypeScript collapes unions -with `any` or `unknown` to `any` or `unknown`. So, -we use a custom type to allow definitions such as -the `context` server option. - -___ - -### Handler - -Ƭ **Handler**<`Request`, `Response`\>: (`req`: `Request`, `res`: `Response`, `body?`: `unknown`) => `Promise`<`void`\> - -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `Request` | extends [`NodeRequest`](README.md#noderequest) = [`NodeRequest`](README.md#noderequest) | -| `Response` | extends [`NodeResponse`](README.md#noderesponse) = [`NodeResponse`](README.md#noderesponse) | - -#### Type declaration - -▸ (`req`, `res`, `body?`): `Promise`<`void`\> - -The ready-to-use handler. Simply plug it in your favourite HTTP framework -and enjoy. - -Beware that the handler resolves only after the whole operation completes. -- If query/mutation, waits for result -- If subscription, waits for complete - -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. - -For production environments, its recommended not to transmit the exact internal -error details to the client, but instead report to an error logging tool or simply -the console. Roughly: - -```ts -import http from 'http'; -import { createHandler } from 'graphql-sse'; - -const handler = createHandler({ ... }); - -http.createServer(async (req, res) => { - try { - await handler(req, res); - } catch (err) { - console.error(err); - // or - Sentry.captureException(err); - - if (!res.headersSent) { - res.writeHead(500, 'Internal Server Error').end(); - } - } -}); -``` - -Note that some libraries, like fastify, parse the body before reaching the handler. -In such cases all request 'data' events are already consumed. Use this `body` argument -too pass in the read body and avoid listening for the 'data' events internally. Do -beware that the `body` argument will be consumed **only** if it's an object. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `res` | `Response` | -| `body?` | `unknown` | - -##### Returns - -`Promise`<`void`\> - -___ - -### NodeRequest - -Ƭ **NodeRequest**: `IncomingMessage` \| `Http2ServerRequest` - -___ - -### NodeResponse - -Ƭ **NodeResponse**: `ServerResponse` \| `Http2ServerResponse` - -___ - -### OperationResult - -Ƭ **OperationResult**: `Promise`<`AsyncGenerator`<[`ExecutionResult`](interfaces/ExecutionResult.md) \| [`ExecutionPatchResult`](interfaces/ExecutionPatchResult.md)\> \| `AsyncIterable`<[`ExecutionResult`](interfaces/ExecutionResult.md) \| [`ExecutionPatchResult`](interfaces/ExecutionPatchResult.md)\> \| [`ExecutionResult`](interfaces/ExecutionResult.md)\> \| `AsyncGenerator`<[`ExecutionResult`](interfaces/ExecutionResult.md) \| [`ExecutionPatchResult`](interfaces/ExecutionPatchResult.md)\> \| `AsyncIterable`<[`ExecutionResult`](interfaces/ExecutionResult.md) \| [`ExecutionPatchResult`](interfaces/ExecutionPatchResult.md)\> \| [`ExecutionResult`](interfaces/ExecutionResult.md) - -___ - -### createHandler - -▸ **createHandler**<`Request`, `Response`\>(`options`): [`Handler`](README.md#handler)<`Request`, `Response`\> - -Makes a Protocol complient 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. - -#### Type parameters - -| Name | Type | -| :------ | :------ | -| `Request` | extends [`NodeRequest`](README.md#noderequest) = [`NodeRequest`](README.md#noderequest) | -| `Response` | extends [`NodeResponse`](README.md#noderesponse) = [`NodeResponse`](README.md#noderesponse) | - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `options` | [`HandlerOptions`](interfaces/HandlerOptions.md)<`Request`, `Response`\> | - -#### Returns - -[`Handler`](README.md#handler)<`Request`, `Response`\> +### Modules + +- [client](modules/client.md) +- [common](modules/common.md) +- [handler](modules/handler.md) +- [use/express](modules/use_express.md) +- [use/fastify](modules/use_fastify.md) +- [use/fetch](modules/use_fetch.md) +- [use/http](modules/use_http.md) +- [use/http2](modules/use_http2.md) diff --git a/docs/classes/NetworkError.md b/docs/classes/client.NetworkError.md similarity index 84% rename from docs/classes/NetworkError.md rename to docs/classes/client.NetworkError.md index 5f3e35f1..edd07e60 100644 --- a/docs/classes/NetworkError.md +++ b/docs/classes/client.NetworkError.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / NetworkError +[graphql-sse](../README.md) / [client](../modules/client.md) / NetworkError # Class: NetworkError +[client](../modules/client.md).NetworkError + A network error caused by the client or an unexpected response from the server. Network errors are considered retryable, all others error types will be reported @@ -26,11 +28,11 @@ you should supply the `Response` generic depending on your Fetch implementation. ### Constructors -- [constructor](NetworkError.md#constructor) +- [constructor](client.NetworkError.md#constructor) ### Properties -- [response](NetworkError.md#response) +- [response](client.NetworkError.md#response) ## Constructors diff --git a/docs/interfaces/HandlerOptions.md b/docs/interfaces/HandlerOptions.md deleted file mode 100644 index 58b7297f..00000000 --- a/docs/interfaces/HandlerOptions.md +++ /dev/null @@ -1,406 +0,0 @@ -[graphql-sse](../README.md) / HandlerOptions - -# Interface: HandlerOptions - -## Type parameters - -| Name | Type | -| :------ | :------ | -| `Request` | extends [`NodeRequest`](../README.md#noderequest) = [`NodeRequest`](../README.md#noderequest) | -| `Response` | extends [`NodeResponse`](../README.md#noderesponse) = [`NodeResponse`](../README.md#noderesponse) | - -## Table of contents - -### Properties - -- [authenticate](HandlerOptions.md#authenticate) -- [context](HandlerOptions.md#context) -- [execute](HandlerOptions.md#execute) -- [onComplete](HandlerOptions.md#oncomplete) -- [onConnected](HandlerOptions.md#onconnected) -- [onConnecting](HandlerOptions.md#onconnecting) -- [onDisconnect](HandlerOptions.md#ondisconnect) -- [onNext](HandlerOptions.md#onnext) -- [onOperation](HandlerOptions.md#onoperation) -- [onSubscribe](HandlerOptions.md#onsubscribe) -- [schema](HandlerOptions.md#schema) -- [subscribe](HandlerOptions.md#subscribe) -- [validate](HandlerOptions.md#validate) - -## Properties - -### authenticate - -• `Optional` **authenticate**: (`req`: `Request`, `res`: `Response`) => `undefined` \| `string` \| `void` \| `Promise`<`undefined` \| `string` \| `void`\> - -#### Type declaration - -▸ (`req`, `res`): `undefined` \| `string` \| `void` \| `Promise`<`undefined` \| `string` \| `void`\> - -Authenticate the client. Returning a string indicates that the client -is authenticated and the request is ready to be processed. - -A token of type string MUST be supplied; if there is no token, you may -return an empty string (`''`); - -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. - -**`Default`** - -'req.headers["x-graphql-event-stream-token"] || req.url.searchParams["token"] || generateRandomUUID()' // https://gist.github.com/jed/982883 - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `res` | `Response` | - -##### Returns - -`undefined` \| `string` \| `void` \| `Promise`<`undefined` \| `string` \| `void`\> - -___ - -### context - -• `Optional` **context**: [`ExecutionContext`](../README.md#executioncontext) \| (`req`: `Request`, `args`: `ExecutionArgs`) => [`ExecutionContext`](../README.md#executioncontext) \| `Promise`<[`ExecutionContext`](../README.md#executioncontext)\> - -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. - -___ - -### execute - -• `Optional` **execute**: (`args`: `ExecutionArgs`) => [`OperationResult`](../README.md#operationresult) - -#### Type declaration - -▸ (`args`): [`OperationResult`](../README.md#operationresult) - -Is the `execute` function from GraphQL which is -used to execute the query and mutation operations. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `args` | `ExecutionArgs` | - -##### Returns - -[`OperationResult`](../README.md#operationresult) - -___ - -### onComplete - -• `Optional` **onComplete**: (`req`: `Request`, `args`: `ExecutionArgs`) => `void` \| `Promise`<`void`\> - -#### Type declaration - -▸ (`req`, `args`): `void` \| `Promise`<`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. - -First argument, the request, is always the GraphQL operation -request. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `args` | `ExecutionArgs` | - -##### Returns - -`void` \| `Promise`<`void`\> - -___ - -### onConnected - -• `Optional` **onConnected**: (`req`: `Request`) => `void` \| `Promise`<`void`\> - -#### Type declaration - -▸ (`req`): `void` \| `Promise`<`void`\> - -Called when a new event stream has been succesfully connected and -accepted, and after all pending messages have been flushed. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | - -##### Returns - -`void` \| `Promise`<`void`\> - -___ - -### onConnecting - -• `Optional` **onConnecting**: (`req`: `Request`, `res`: `Response`) => `void` \| `Promise`<`void`\> - -#### Type declaration - -▸ (`req`, `res`): `void` \| `Promise`<`void`\> - -Called when a new event stream is connecting BEFORE it is accepted. -By accepted, its meant the server responded with a 200 (OK), alongside -flushing the necessary event stream headers. - -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. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `res` | `Response` | - -##### Returns - -`void` \| `Promise`<`void`\> - -___ - -### onDisconnect - -• `Optional` **onDisconnect**: (`req`: `Request`) => `void` \| `Promise`<`void`\> - -#### Type declaration - -▸ (`req`): `void` \| `Promise`<`void`\> - -Called when an event stream has disconnected right before the -accepting the stream. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | - -##### Returns - -`void` \| `Promise`<`void`\> - -___ - -### onNext - -• `Optional` **onNext**: (`req`: `Request`, `args`: `ExecutionArgs`, `result`: [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>) => `void` \| [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> \| `Promise`<`void` \| [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>\> - -#### Type declaration - -▸ (`req`, `args`, `result`): `void` \| [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> \| `Promise`<`void` \| [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>\> - -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. - -First argument, the request, is always the GraphQL operation -request. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `args` | `ExecutionArgs` | -| `result` | [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> | - -##### Returns - -`void` \| [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> \| `Promise`<`void` \| [`ExecutionResult`](ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>\> - -___ - -### onOperation - -• `Optional` **onOperation**: (`req`: `Request`, `res`: `Response`, `args`: `ExecutionArgs`, `result`: [`OperationResult`](../README.md#operationresult)) => `void` \| [`OperationResult`](../README.md#operationresult) \| `Promise`<`void` \| [`OperationResult`](../README.md#operationresult)\> - -#### Type declaration - -▸ (`req`, `res`, `args`, `result`): `void` \| [`OperationResult`](../README.md#operationresult) \| `Promise`<`void` \| [`OperationResult`](../README.md#operationresult)\> - -Executed after the operation call resolves. For streaming -operations, triggering this callback does not necessarely -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. - -Use this callback to listen for GraphQL operations and -execution result manipulation. - -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. - -First argument, the request, is always the GraphQL operation -request. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `res` | `Response` | -| `args` | `ExecutionArgs` | -| `result` | [`OperationResult`](../README.md#operationresult) | - -##### Returns - -`void` \| [`OperationResult`](../README.md#operationresult) \| `Promise`<`void` \| [`OperationResult`](../README.md#operationresult)\> - -___ - -### onSubscribe - -• `Optional` **onSubscribe**: (`req`: `Request`, `res`: `Response`, `params`: [`RequestParams`](RequestParams.md)) => `void` \| `ExecutionArgs` \| `Promise`<`void` \| `ExecutionArgs`\> - -#### Type declaration - -▸ (`req`, `res`, `params`): `void` \| `ExecutionArgs` \| `Promise`<`void` \| `ExecutionArgs`\> - -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. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `req` | `Request` | -| `res` | `Response` | -| `params` | [`RequestParams`](RequestParams.md) | - -##### Returns - -`void` \| `ExecutionArgs` \| `Promise`<`void` \| `ExecutionArgs`\> - -___ - -### schema - -• `Optional` **schema**: `GraphQLSchema` \| (`req`: `Request`, `args`: `Omit`<`ExecutionArgs`, ``"schema"``\>) => `GraphQLSchema` \| `Promise`<`GraphQLSchema`\> - -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. - -___ - -### subscribe - -• `Optional` **subscribe**: (`args`: `ExecutionArgs`) => [`OperationResult`](../README.md#operationresult) - -#### Type declaration - -▸ (`args`): [`OperationResult`](../README.md#operationresult) - -Is the `subscribe` function from GraphQL which is -used to execute the subscription operation. - -##### Parameters - -| Name | Type | -| :------ | :------ | -| `args` | `ExecutionArgs` | - -##### Returns - -[`OperationResult`](../README.md#operationresult) - -___ - -### validate - -• `Optional` **validate**: (`schema`: `GraphQLSchema`, `documentAST`: `DocumentNode`, `rules?`: readonly `ValidationRule`[], `options?`: {}, `typeInfo?`: `TypeInfo`) => `ReadonlyArray`<`GraphQLError`\> - -#### Type declaration - -▸ (`schema`, `documentAST`, `rules?`, `options?`, `typeInfo?`): `ReadonlyArray`<`GraphQLError`\> - -Implements the "Validation" section of the spec. - -Validation runs synchronously, returning an array of encountered errors, or -an empty array if no errors were encountered and the document is valid. - -A list of specific validation rules may be provided. If not provided, the -default list of rules defined by the GraphQL specification will be used. - -Each validation rules is a function which returns a visitor -(see the language/visitor API). Visitor methods are expected to return -GraphQLErrors, or Arrays of GraphQLErrors when invalid. - -Validate will stop validation after a `maxErrors` limit has been reached. -Attackers can send pathologically invalid queries to induce a DoS attack, -so by default `maxErrors` set to 100 errors. - -Optionally a custom TypeInfo instance may be provided. If not provided, one -will be created from the provided schema. - -##### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `schema` | `GraphQLSchema` | - | -| `documentAST` | `DocumentNode` | - | -| `rules?` | readonly `ValidationRule`[] | - | -| `options?` | `Object` | - | -| `typeInfo?` | `TypeInfo` | **`Deprecated`** will be removed in 17.0.0 | - -##### Returns - -`ReadonlyArray`<`GraphQLError`\> diff --git a/docs/interfaces/Client.md b/docs/interfaces/client.Client.md similarity index 73% rename from docs/interfaces/Client.md rename to docs/interfaces/client.Client.md index 22ee46b8..ce3c6251 100644 --- a/docs/interfaces/Client.md +++ b/docs/interfaces/client.Client.md @@ -1,16 +1,18 @@ -[graphql-sse](../README.md) / Client +[graphql-sse](../README.md) / [client](../modules/client.md) / Client # Interface: Client +[client](../modules/client.md).Client + ## Table of contents ### Properties -- [dispose](Client.md#dispose) +- [dispose](client.Client.md#dispose) ### Methods -- [subscribe](Client.md#subscribe) +- [subscribe](client.Client.md#subscribe) ## Properties @@ -50,8 +52,8 @@ function used for dropping the subscription and cleaning up. | Name | Type | | :------ | :------ | -| `request` | [`RequestParams`](RequestParams.md) | -| `sink` | [`Sink`](Sink.md)<[`ExecutionResult`](ExecutionResult.md)<`Data`, `Extensions`\>\> | +| `request` | [`RequestParams`](common.RequestParams.md) | +| `sink` | [`Sink`](common.Sink.md)<[`ExecutionResult`](common.ExecutionResult.md)<`Data`, `Extensions`\>\> | #### Returns diff --git a/docs/interfaces/ClientOptions.md b/docs/interfaces/client.ClientOptions.md similarity index 84% rename from docs/interfaces/ClientOptions.md rename to docs/interfaces/client.ClientOptions.md index 2890eb4b..5d7640b0 100644 --- a/docs/interfaces/ClientOptions.md +++ b/docs/interfaces/client.ClientOptions.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / ClientOptions +[graphql-sse](../README.md) / [client](../modules/client.md) / ClientOptions # Interface: ClientOptions +[client](../modules/client.md).ClientOptions + ## Type parameters | Name | Type | @@ -12,21 +14,21 @@ ### Properties -- [abortControllerImpl](ClientOptions.md#abortcontrollerimpl) -- [credentials](ClientOptions.md#credentials) -- [fetchFn](ClientOptions.md#fetchfn) -- [generateID](ClientOptions.md#generateid) -- [headers](ClientOptions.md#headers) -- [lazy](ClientOptions.md#lazy) -- [lazyCloseTimeout](ClientOptions.md#lazyclosetimeout) -- [onMessage](ClientOptions.md#onmessage) -- [onNonLazyError](ClientOptions.md#onnonlazyerror) -- [referrer](ClientOptions.md#referrer) -- [referrerPolicy](ClientOptions.md#referrerpolicy) -- [retry](ClientOptions.md#retry) -- [retryAttempts](ClientOptions.md#retryattempts) -- [singleConnection](ClientOptions.md#singleconnection) -- [url](ClientOptions.md#url) +- [abortControllerImpl](client.ClientOptions.md#abortcontrollerimpl) +- [credentials](client.ClientOptions.md#credentials) +- [fetchFn](client.ClientOptions.md#fetchfn) +- [generateID](client.ClientOptions.md#generateid) +- [headers](client.ClientOptions.md#headers) +- [lazy](client.ClientOptions.md#lazy) +- [lazyCloseTimeout](client.ClientOptions.md#lazyclosetimeout) +- [onMessage](client.ClientOptions.md#onmessage) +- [onNonLazyError](client.ClientOptions.md#onnonlazyerror) +- [referrer](client.ClientOptions.md#referrer) +- [referrerPolicy](client.ClientOptions.md#referrerpolicy) +- [retry](client.ClientOptions.md#retry) +- [retryAttempts](client.ClientOptions.md#retryattempts) +- [singleConnection](client.ClientOptions.md#singleconnection) +- [url](client.ClientOptions.md#url) ## Properties @@ -46,7 +48,7 @@ ___ ### credentials -• `Optional` **credentials**: ``"omit"`` \| ``"same-origin"`` \| ``"include"`` +• `Optional` **credentials**: ``"include"`` \| ``"omit"`` \| ``"same-origin"`` Indicates whether the user agent should send cookies from the other domain in the case of cross-origin requests. @@ -153,7 +155,7 @@ ___ ### onMessage -• `Optional` **onMessage**: (`message`: [`StreamMessage`](StreamMessage.md)<`SingleConnection`, [`StreamEvent`](../README.md#streamevent)\>) => `void` +• `Optional` **onMessage**: (`message`: [`StreamMessage`](common.StreamMessage.md)<`SingleConnection`, [`StreamEvent`](../modules/common.md#streamevent)\>) => `void` #### Type declaration @@ -168,7 +170,7 @@ Use this function if you want to inspect valid messages received through the act | Name | Type | | :------ | :------ | -| `message` | [`StreamMessage`](StreamMessage.md)<`SingleConnection`, [`StreamEvent`](../README.md#streamevent)\> | +| `message` | [`StreamMessage`](common.StreamMessage.md)<`SingleConnection`, [`StreamEvent`](../modules/common.md#streamevent)\> | ##### Returns @@ -208,7 +210,7 @@ ___ ### referrerPolicy -• `Optional` **referrerPolicy**: ``"same-origin"`` \| ``"no-referrer"`` \| ``"no-referrer-when-downgrade"`` \| ``"origin"`` \| ``"strict-origin"`` \| ``"origin-when-cross-origin"`` \| ``"strict-origin-when-cross-origin"`` \| ``"unsafe-url"`` +• `Optional` **referrerPolicy**: ``"origin"`` \| ``"same-origin"`` \| ``"no-referrer"`` \| ``"no-referrer-when-downgrade"`` \| ``"origin-when-cross-origin"`` \| ``"strict-origin"`` \| ``"strict-origin-when-cross-origin"`` \| ``"unsafe-url"`` Specifies the referrer policy to use for the request. diff --git a/docs/interfaces/ExecutionPatchResult.md b/docs/interfaces/common.ExecutionPatchResult.md similarity index 58% rename from docs/interfaces/ExecutionPatchResult.md rename to docs/interfaces/common.ExecutionPatchResult.md index f1c0d889..d443736a 100644 --- a/docs/interfaces/ExecutionPatchResult.md +++ b/docs/interfaces/common.ExecutionPatchResult.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / ExecutionPatchResult +[graphql-sse](../README.md) / [common](../modules/common.md) / ExecutionPatchResult # Interface: ExecutionPatchResult +[common](../modules/common.md).ExecutionPatchResult + ## Type parameters | Name | Type | @@ -13,12 +15,12 @@ ### Properties -- [data](ExecutionPatchResult.md#data) -- [errors](ExecutionPatchResult.md#errors) -- [extensions](ExecutionPatchResult.md#extensions) -- [hasNext](ExecutionPatchResult.md#hasnext) -- [label](ExecutionPatchResult.md#label) -- [path](ExecutionPatchResult.md#path) +- [data](common.ExecutionPatchResult.md#data) +- [errors](common.ExecutionPatchResult.md#errors) +- [extensions](common.ExecutionPatchResult.md#extensions) +- [hasNext](common.ExecutionPatchResult.md#hasnext) +- [label](common.ExecutionPatchResult.md#label) +- [path](common.ExecutionPatchResult.md#path) ## Properties diff --git a/docs/interfaces/ExecutionResult.md b/docs/interfaces/common.ExecutionResult.md similarity index 61% rename from docs/interfaces/ExecutionResult.md rename to docs/interfaces/common.ExecutionResult.md index c1c70f7d..6f655e17 100644 --- a/docs/interfaces/ExecutionResult.md +++ b/docs/interfaces/common.ExecutionResult.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / ExecutionResult +[graphql-sse](../README.md) / [common](../modules/common.md) / ExecutionResult # Interface: ExecutionResult +[common](../modules/common.md).ExecutionResult + ## Type parameters | Name | Type | @@ -13,10 +15,10 @@ ### Properties -- [data](ExecutionResult.md#data) -- [errors](ExecutionResult.md#errors) -- [extensions](ExecutionResult.md#extensions) -- [hasNext](ExecutionResult.md#hasnext) +- [data](common.ExecutionResult.md#data) +- [errors](common.ExecutionResult.md#errors) +- [extensions](common.ExecutionResult.md#extensions) +- [hasNext](common.ExecutionResult.md#hasnext) ## Properties diff --git a/docs/interfaces/RequestParams.md b/docs/interfaces/common.RequestParams.md similarity index 61% rename from docs/interfaces/RequestParams.md rename to docs/interfaces/common.RequestParams.md index b979996d..0df0b660 100644 --- a/docs/interfaces/RequestParams.md +++ b/docs/interfaces/common.RequestParams.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / RequestParams +[graphql-sse](../README.md) / [common](../modules/common.md) / RequestParams # Interface: RequestParams +[common](../modules/common.md).RequestParams + Parameters for GraphQL's request for execution. Reference: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request @@ -10,10 +12,10 @@ Reference: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOv ### Properties -- [extensions](RequestParams.md#extensions) -- [operationName](RequestParams.md#operationname) -- [query](RequestParams.md#query) -- [variables](RequestParams.md#variables) +- [extensions](common.RequestParams.md#extensions) +- [operationName](common.RequestParams.md#operationname) +- [query](common.RequestParams.md#query) +- [variables](common.RequestParams.md#variables) ## Properties diff --git a/docs/interfaces/Sink.md b/docs/interfaces/common.Sink.md similarity index 77% rename from docs/interfaces/Sink.md rename to docs/interfaces/common.Sink.md index d85d1f61..1388e4e5 100644 --- a/docs/interfaces/Sink.md +++ b/docs/interfaces/common.Sink.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / Sink +[graphql-sse](../README.md) / [common](../modules/common.md) / Sink # Interface: Sink +[common](../modules/common.md).Sink + A representation of any set of values over any amount of time. ## Type parameters @@ -14,9 +16,9 @@ A representation of any set of values over any amount of time. ### Methods -- [complete](Sink.md#complete) -- [error](Sink.md#error) -- [next](Sink.md#next) +- [complete](common.Sink.md#complete) +- [error](common.Sink.md#error) +- [next](common.Sink.md#next) ## Methods diff --git a/docs/interfaces/StreamMessage.md b/docs/interfaces/common.StreamMessage.md similarity index 55% rename from docs/interfaces/StreamMessage.md rename to docs/interfaces/common.StreamMessage.md index e0d29541..984caadd 100644 --- a/docs/interfaces/StreamMessage.md +++ b/docs/interfaces/common.StreamMessage.md @@ -1,7 +1,9 @@ -[graphql-sse](../README.md) / StreamMessage +[graphql-sse](../README.md) / [common](../modules/common.md) / StreamMessage # Interface: StreamMessage +[common](../modules/common.md).StreamMessage + Represents a message in an event stream. Read more: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format @@ -11,20 +13,20 @@ Read more: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/U | Name | Type | | :------ | :------ | | `ForID` | extends `boolean` | -| `E` | extends [`StreamEvent`](../README.md#streamevent) | +| `E` | extends [`StreamEvent`](../modules/common.md#streamevent) | ## Table of contents ### Properties -- [data](StreamMessage.md#data) -- [event](StreamMessage.md#event) +- [data](common.StreamMessage.md#data) +- [event](common.StreamMessage.md#event) ## Properties ### data -• **data**: `ForID` extends ``true`` ? [`StreamDataForID`](../README.md#streamdataforid)<`E`\> : [`StreamData`](../README.md#streamdata)<`E`\> +• **data**: `ForID` extends ``true`` ? [`StreamDataForID`](../modules/common.md#streamdataforid)<`E`\> : [`StreamData`](../modules/common.md#streamdata)<`E`\> ___ diff --git a/docs/interfaces/handler.HandlerOptions.md b/docs/interfaces/handler.HandlerOptions.md new file mode 100644 index 00000000..dc8a1df9 --- /dev/null +++ b/docs/interfaces/handler.HandlerOptions.md @@ -0,0 +1,345 @@ +[graphql-sse](../README.md) / [handler](../modules/handler.md) / HandlerOptions + +# Interface: HandlerOptions + +[handler](../modules/handler.md).HandlerOptions + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `RequestRaw` | `unknown` | +| `RequestContext` | `unknown` | +| `Context` | extends [`OperationContext`](../modules/handler.md#operationcontext) = `undefined` | + +## Table of contents + +### Properties + +- [authenticate](handler.HandlerOptions.md#authenticate) +- [context](handler.HandlerOptions.md#context) +- [execute](handler.HandlerOptions.md#execute) +- [onComplete](handler.HandlerOptions.md#oncomplete) +- [onConnect](handler.HandlerOptions.md#onconnect) +- [onNext](handler.HandlerOptions.md#onnext) +- [onOperation](handler.HandlerOptions.md#onoperation) +- [onSubscribe](handler.HandlerOptions.md#onsubscribe) +- [schema](handler.HandlerOptions.md#schema) +- [subscribe](handler.HandlerOptions.md#subscribe) +- [validate](handler.HandlerOptions.md#validate) + +## Properties + +### authenticate + +• `Optional` **authenticate**: (`req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>) => `undefined` \| ``null`` \| `string` \| [`Response`](../modules/handler.md#response) \| `Promise`<`undefined` \| ``null`` \| `string` \| [`Response`](../modules/handler.md#response)\> + +#### Type declaration + +▸ (`req`): `undefined` \| ``null`` \| `string` \| [`Response`](../modules/handler.md#response) \| `Promise`<`undefined` \| ``null`` \| `string` \| [`Response`](../modules/handler.md#response)\> + +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 + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\> | + +##### Returns + +`undefined` \| ``null`` \| `string` \| [`Response`](../modules/handler.md#response) \| `Promise`<`undefined` \| ``null`` \| `string` \| [`Response`](../modules/handler.md#response)\> + +___ + +### context + +• `Optional` **context**: `Context` \| (`req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>, `params`: [`RequestParams`](common.RequestParams.md)) => `Context` \| `Promise`<`Context`\> + +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`. + +___ + +### execute + +• `Optional` **execute**: (`args`: [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\>) => [`OperationResult`](../modules/handler.md#operationresult) + +#### Type declaration + +▸ (`args`): [`OperationResult`](../modules/handler.md#operationresult) + +Is the `execute` function from GraphQL which is +used to execute the query and mutation operations. + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `args` | [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\> | + +##### Returns + +[`OperationResult`](../modules/handler.md#operationresult) + +___ + +### onComplete + +• `Optional` **onComplete**: (`ctx`: `Context`, `req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>) => `void` \| `Promise`<`void`\> + +#### Type declaration + +▸ (`ctx`, `req`): `void` \| `Promise`<`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. + +##### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `ctx` | `Context` | - | +| `req` | [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\> | Always the request that contains the GraphQL operation. | + +##### Returns + +`void` \| `Promise`<`void`\> + +___ + +### onConnect + +• `Optional` **onConnect**: (`req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>) => `undefined` \| ``null`` \| `void` \| [`Response`](../modules/handler.md#response) \| `Promise`<`undefined` \| ``null`` \| `void` \| [`Response`](../modules/handler.md#response)\> + +#### Type declaration + +▸ (`req`): `undefined` \| ``null`` \| `void` \| [`Response`](../modules/handler.md#response) \| `Promise`<`undefined` \| ``null`` \| `void` \| [`Response`](../modules/handler.md#response)\> + +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. + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\> | + +##### Returns + +`undefined` \| ``null`` \| `void` \| [`Response`](../modules/handler.md#response) \| `Promise`<`undefined` \| ``null`` \| `void` \| [`Response`](../modules/handler.md#response)\> + +___ + +### onNext + +• `Optional` **onNext**: (`ctx`: `Context`, `req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>, `result`: [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>) => `void` \| [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> \| `Promise`<`void` \| [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>\> + +#### Type declaration + +▸ (`ctx`, `req`, `result`): `void` \| [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> \| `Promise`<`void` \| [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>\> + +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. + +##### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `ctx` | `Context` | - | +| `req` | [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\> | Always the request that contains the GraphQL operation. | +| `result` | [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> | - | + +##### Returns + +`void` \| [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\> \| `Promise`<`void` \| [`ExecutionResult`](common.ExecutionResult.md)<`Record`<`string`, `unknown`\>, `Record`<`string`, `unknown`\>\> \| [`ExecutionPatchResult`](common.ExecutionPatchResult.md)<`unknown`, `Record`<`string`, `unknown`\>\>\> + +___ + +### onOperation + +• `Optional` **onOperation**: (`ctx`: `Context`, `req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>, `args`: `ExecutionArgs`, `result`: [`OperationResult`](../modules/handler.md#operationresult)) => `void` \| [`OperationResult`](../modules/handler.md#operationresult) \| `Promise`<`void` \| [`OperationResult`](../modules/handler.md#operationresult)\> + +#### Type declaration + +▸ (`ctx`, `req`, `args`, `result`): `void` \| [`OperationResult`](../modules/handler.md#operationresult) \| `Promise`<`void` \| [`OperationResult`](../modules/handler.md#operationresult)\> + +Executed after the operation call resolves. For streaming +operations, triggering this callback does not necessarely +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. + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `ctx` | `Context` | +| `req` | [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\> | +| `args` | `ExecutionArgs` | +| `result` | [`OperationResult`](../modules/handler.md#operationresult) | + +##### Returns + +`void` \| [`OperationResult`](../modules/handler.md#operationresult) \| `Promise`<`void` \| [`OperationResult`](../modules/handler.md#operationresult)\> + +___ + +### onSubscribe + +• `Optional` **onSubscribe**: (`req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>, `params`: [`RequestParams`](common.RequestParams.md)) => `void` \| [`Response`](../modules/handler.md#response) \| [`OperationResult`](../modules/handler.md#operationresult) \| [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\> \| `Promise`<`void` \| [`Response`](../modules/handler.md#response) \| [`OperationResult`](../modules/handler.md#operationresult) \| [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\>\> + +#### Type declaration + +▸ (`req`, `params`): `void` \| [`Response`](../modules/handler.md#response) \| [`OperationResult`](../modules/handler.md#operationresult) \| [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\> \| `Promise`<`void` \| [`Response`](../modules/handler.md#response) \| [`OperationResult`](../modules/handler.md#operationresult) \| [`OperationArgs`](../modules/handler.md#operationargs)<`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. + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\> | +| `params` | [`RequestParams`](common.RequestParams.md) | + +##### Returns + +`void` \| [`Response`](../modules/handler.md#response) \| [`OperationResult`](../modules/handler.md#operationresult) \| [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\> \| `Promise`<`void` \| [`Response`](../modules/handler.md#response) \| [`OperationResult`](../modules/handler.md#operationresult) \| [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\>\> + +___ + +### schema + +• `Optional` **schema**: `GraphQLSchema` \| (`req`: [`Request`](handler.Request.md)<`RequestRaw`, `RequestContext`\>, `args`: `Pick`<[`OperationArgs`](../modules/handler.md#operationargs)<`Context`\>, ``"document"`` \| ``"contextValue"`` \| ``"operationName"`` \| ``"variableValues"``\>) => `GraphQLSchema` \| `Promise`<`GraphQLSchema`\> + +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. + +___ + +### subscribe + +• `Optional` **subscribe**: (`args`: [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\>) => [`OperationResult`](../modules/handler.md#operationresult) + +#### Type declaration + +▸ (`args`): [`OperationResult`](../modules/handler.md#operationresult) + +Is the `subscribe` function from GraphQL which is +used to execute the subscription operation. + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `args` | [`OperationArgs`](../modules/handler.md#operationargs)<`Context`\> | + +##### Returns + +[`OperationResult`](../modules/handler.md#operationresult) + +___ + +### validate + +• `Optional` **validate**: (`schema`: `GraphQLSchema`, `documentAST`: `DocumentNode`, `rules?`: readonly `ValidationRule`[], `options?`: {}, `typeInfo?`: `TypeInfo`) => `ReadonlyArray`<`GraphQLError`\> + +#### Type declaration + +▸ (`schema`, `documentAST`, `rules?`, `options?`, `typeInfo?`): `ReadonlyArray`<`GraphQLError`\> + +Implements the "Validation" section of the spec. + +Validation runs synchronously, returning an array of encountered errors, or +an empty array if no errors were encountered and the document is valid. + +A list of specific validation rules may be provided. If not provided, the +default list of rules defined by the GraphQL specification will be used. + +Each validation rules is a function which returns a visitor +(see the language/visitor API). Visitor methods are expected to return +GraphQLErrors, or Arrays of GraphQLErrors when invalid. + +Validate will stop validation after a `maxErrors` limit has been reached. +Attackers can send pathologically invalid queries to induce a DoS attack, +so by default `maxErrors` set to 100 errors. + +Optionally a custom TypeInfo instance may be provided. If not provided, one +will be created from the provided schema. + +##### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `schema` | `GraphQLSchema` | - | +| `documentAST` | `DocumentNode` | - | +| `rules?` | readonly `ValidationRule`[] | - | +| `options?` | `Object` | - | +| `typeInfo?` | `TypeInfo` | **`Deprecated`** will be removed in 17.0.0 | + +##### Returns + +`ReadonlyArray`<`GraphQLError`\> diff --git a/docs/interfaces/handler.Request.md b/docs/interfaces/handler.Request.md new file mode 100644 index 00000000..1d43ac41 --- /dev/null +++ b/docs/interfaces/handler.Request.md @@ -0,0 +1,73 @@ +[graphql-sse](../README.md) / [handler](../modules/handler.md) / Request + +# Interface: Request + +[handler](../modules/handler.md).Request + +Server agnostic request interface containing the raw request +which is server dependant. + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `Raw` | `unknown` | +| `Context` | `unknown` | + +## Table of contents + +### Properties + +- [body](handler.Request.md#body) +- [context](handler.Request.md#context) +- [headers](handler.Request.md#headers) +- [method](handler.Request.md#method) +- [raw](handler.Request.md#raw) +- [url](handler.Request.md#url) + +## Properties + +### body + +• `Readonly` **body**: ``null`` \| `string` \| `Record`<`PropertyKey`, `unknown`\> \| () => ``null`` \| `string` \| `Record`<`PropertyKey`, `unknown`\> \| `Promise`<``null`` \| `string` \| `Record`<`PropertyKey`, `unknown`\>\> + +Parsed request body or a parser function. + +If the provided function throws, the error message "Unparsable JSON body" will +be in the erroneous response. + +___ + +### context + +• **context**: `Context` + +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. + +___ + +### headers + +• `Readonly` **headers**: [`RequestHeaders`](handler.RequestHeaders.md) + +___ + +### method + +• `Readonly` **method**: `string` + +___ + +### raw + +• `Readonly` **raw**: `Raw` + +The raw request itself from the implementing server. + +___ + +### url + +• `Readonly` **url**: `string` diff --git a/docs/interfaces/handler.RequestHeaders.md b/docs/interfaces/handler.RequestHeaders.md new file mode 100644 index 00000000..b86e090f --- /dev/null +++ b/docs/interfaces/handler.RequestHeaders.md @@ -0,0 +1,33 @@ +[graphql-sse](../README.md) / [handler](../modules/handler.md) / RequestHeaders + +# Interface: RequestHeaders + +[handler](../modules/handler.md).RequestHeaders + +The incoming request headers the implementing server should provide. + +## Table of contents + +### Properties + +- [get](handler.RequestHeaders.md#get) + +## Properties + +### get + +• **get**: (`key`: `string`) => `undefined` \| ``null`` \| `string` + +#### Type declaration + +▸ (`key`): `undefined` \| ``null`` \| `string` + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `key` | `string` | + +##### Returns + +`undefined` \| ``null`` \| `string` diff --git a/docs/interfaces/handler.ResponseInit.md b/docs/interfaces/handler.ResponseInit.md new file mode 100644 index 00000000..302ffd5b --- /dev/null +++ b/docs/interfaces/handler.ResponseInit.md @@ -0,0 +1,34 @@ +[graphql-sse](../README.md) / [handler](../modules/handler.md) / ResponseInit + +# Interface: ResponseInit + +[handler](../modules/handler.md).ResponseInit + +Server agnostic response options (ex. status and headers) returned from +`graphql-sse` needing to be coerced to the server implementation in use. + +## Table of contents + +### Properties + +- [headers](handler.ResponseInit.md#headers) +- [status](handler.ResponseInit.md#status) +- [statusText](handler.ResponseInit.md#statustext) + +## Properties + +### headers + +• `Optional` `Readonly` **headers**: [`ResponseHeaders`](../modules/handler.md#responseheaders) + +___ + +### status + +• `Readonly` **status**: `number` + +___ + +### statusText + +• `Readonly` **statusText**: `string` diff --git a/docs/interfaces/use_express.RequestContext.md b/docs/interfaces/use_express.RequestContext.md new file mode 100644 index 00000000..aeded650 --- /dev/null +++ b/docs/interfaces/use_express.RequestContext.md @@ -0,0 +1,17 @@ +[graphql-sse](../README.md) / [use/express](../modules/use_express.md) / RequestContext + +# Interface: RequestContext + +[use/express](../modules/use_express.md).RequestContext + +## Table of contents + +### Properties + +- [res](use_express.RequestContext.md#res) + +## Properties + +### res + +• **res**: `Response`<`any`, `Record`<`string`, `any`\>\> diff --git a/docs/interfaces/use_fastify.RequestContext.md b/docs/interfaces/use_fastify.RequestContext.md new file mode 100644 index 00000000..1eec27b6 --- /dev/null +++ b/docs/interfaces/use_fastify.RequestContext.md @@ -0,0 +1,17 @@ +[graphql-sse](../README.md) / [use/fastify](../modules/use_fastify.md) / RequestContext + +# Interface: RequestContext + +[use/fastify](../modules/use_fastify.md).RequestContext + +## Table of contents + +### Properties + +- [reply](use_fastify.RequestContext.md#reply) + +## Properties + +### reply + +• **reply**: `FastifyReply`<`RawServerDefault`, `IncomingMessage`, `ServerResponse`<`IncomingMessage`\>, `RouteGenericInterface`, `unknown`, `FastifySchema`, `FastifyTypeProviderDefault`, `unknown`\> diff --git a/docs/interfaces/use_fetch.RequestContext.md b/docs/interfaces/use_fetch.RequestContext.md new file mode 100644 index 00000000..63e7049a --- /dev/null +++ b/docs/interfaces/use_fetch.RequestContext.md @@ -0,0 +1,107 @@ +[graphql-sse](../README.md) / [use/fetch](../modules/use_fetch.md) / RequestContext + +# Interface: RequestContext + +[use/fetch](../modules/use_fetch.md).RequestContext + +## Table of contents + +### Properties + +- [ReadableStream](use_fetch.RequestContext.md#readablestream) +- [Response](use_fetch.RequestContext.md#response) +- [TextEncoder](use_fetch.RequestContext.md#textencoder) + +## Properties + +### ReadableStream + +• **ReadableStream**: (`underlyingSource`: `UnderlyingByteSource`, `strategy?`: {}) => `ReadableStream`<`Uint8Array`\>(`underlyingSource`: `UnderlyingDefaultSource`<`R`\>, `strategy?`: `QueuingStrategy`<`R`\>) => `ReadableStream`<`R`\>(`underlyingSource?`: `UnderlyingSource`<`R`\>, `strategy?`: `QueuingStrategy`<`R`\>) => `ReadableStream`<`R`\> + +#### Type declaration + +• **new RequestContext**(`underlyingSource`, `strategy?`): `ReadableStream`<`Uint8Array`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `underlyingSource` | `UnderlyingByteSource` | +| `strategy?` | `Object` | + +##### Returns + +`ReadableStream`<`Uint8Array`\> + +• **new RequestContext**<`R`\>(`underlyingSource`, `strategy?`): `ReadableStream`<`R`\> + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `R` | `any` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `underlyingSource` | `UnderlyingDefaultSource`<`R`\> | +| `strategy?` | `QueuingStrategy`<`R`\> | + +##### Returns + +`ReadableStream`<`R`\> + +• **new RequestContext**<`R`\>(`underlyingSource?`, `strategy?`): `ReadableStream`<`R`\> + +##### Type parameters + +| Name | Type | +| :------ | :------ | +| `R` | `any` | + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `underlyingSource?` | `UnderlyingSource`<`R`\> | +| `strategy?` | `QueuingStrategy`<`R`\> | + +##### Returns + +`ReadableStream`<`R`\> + +___ + +### Response + +• **Response**: (`body?`: ``null`` \| `BodyInit`, `init?`: `ResponseInit`) => `Response` + +#### Type declaration + +• **new RequestContext**(`body?`, `init?`): `Response` + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `body?` | ``null`` \| `BodyInit` | +| `init?` | `ResponseInit` | + +##### Returns + +`Response` + +___ + +### TextEncoder + +• **TextEncoder**: () => `TextEncoder` + +#### Type declaration + +• **new RequestContext**(): `TextEncoder` + +##### Returns + +`TextEncoder` diff --git a/docs/interfaces/use_http.RequestContext.md b/docs/interfaces/use_http.RequestContext.md new file mode 100644 index 00000000..085688ea --- /dev/null +++ b/docs/interfaces/use_http.RequestContext.md @@ -0,0 +1,17 @@ +[graphql-sse](../README.md) / [use/http](../modules/use_http.md) / RequestContext + +# Interface: RequestContext + +[use/http](../modules/use_http.md).RequestContext + +## Table of contents + +### Properties + +- [res](use_http.RequestContext.md#res) + +## Properties + +### res + +• **res**: `ServerResponse`<`IncomingMessage`\> diff --git a/docs/interfaces/use_http2.RequestContext.md b/docs/interfaces/use_http2.RequestContext.md new file mode 100644 index 00000000..eca9b0fe --- /dev/null +++ b/docs/interfaces/use_http2.RequestContext.md @@ -0,0 +1,17 @@ +[graphql-sse](../README.md) / [use/http2](../modules/use_http2.md) / RequestContext + +# Interface: RequestContext + +[use/http2](../modules/use_http2.md).RequestContext + +## Table of contents + +### Properties + +- [res](use_http2.RequestContext.md#res) + +## Properties + +### res + +• **res**: `Http2ServerResponse` diff --git a/docs/modules/client.md b/docs/modules/client.md new file mode 100644 index 00000000..c6dfa8a3 --- /dev/null +++ b/docs/modules/client.md @@ -0,0 +1,159 @@ +[graphql-sse](../README.md) / client + +# Module: client + +## Table of contents + +### References + +- [ExecutionPatchResult](client.md#executionpatchresult) +- [ExecutionResult](client.md#executionresult) +- [RequestParams](client.md#requestparams) +- [Sink](client.md#sink) +- [StreamData](client.md#streamdata) +- [StreamDataForID](client.md#streamdataforid) +- [StreamEvent](client.md#streamevent) +- [StreamMessage](client.md#streammessage) +- [TOKEN\_HEADER\_KEY](client.md#token_header_key) +- [TOKEN\_QUERY\_KEY](client.md#token_query_key) +- [isAsyncGenerator](client.md#isasyncgenerator) +- [isAsyncIterable](client.md#isasynciterable) +- [parseStreamData](client.md#parsestreamdata) +- [print](client.md#print) +- [validateStreamEvent](client.md#validatestreamevent) + +### Classes + +- [NetworkError](../classes/client.NetworkError.md) + +### Interfaces + +- [Client](../interfaces/client.Client.md) +- [ClientOptions](../interfaces/client.ClientOptions.md) + +### Functions + +- [createClient](client.md#createclient) + +## Client + +### createClient + +▸ **createClient**<`SingleConnection`\>(`options`): [`Client`](../interfaces/client.Client.md) + +Creates a disposable GraphQL over SSE client to transmit +GraphQL operation results. + +If you have an HTTP/2 server, it is recommended to use the client +in "distinct connections mode" (`singleConnection = false`) which will +create a new SSE connection for each subscribe. This is the default. + +However, when dealing with HTTP/1 servers from a browser, consider using +the "single connection mode" (`singleConnection = true`) which will +use only one SSE connection. + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `SingleConnection` | extends `boolean` = ``false`` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`ClientOptions`](../interfaces/client.ClientOptions.md)<`SingleConnection`\> | + +#### Returns + +[`Client`](../interfaces/client.Client.md) + +## Other + +### ExecutionPatchResult + +Re-exports [ExecutionPatchResult](../interfaces/common.ExecutionPatchResult.md) + +___ + +### ExecutionResult + +Re-exports [ExecutionResult](../interfaces/common.ExecutionResult.md) + +___ + +### RequestParams + +Re-exports [RequestParams](../interfaces/common.RequestParams.md) + +___ + +### Sink + +Re-exports [Sink](../interfaces/common.Sink.md) + +___ + +### StreamData + +Re-exports [StreamData](common.md#streamdata) + +___ + +### StreamDataForID + +Re-exports [StreamDataForID](common.md#streamdataforid) + +___ + +### StreamEvent + +Re-exports [StreamEvent](common.md#streamevent) + +___ + +### StreamMessage + +Re-exports [StreamMessage](../interfaces/common.StreamMessage.md) + +___ + +### TOKEN\_HEADER\_KEY + +Re-exports [TOKEN_HEADER_KEY](common.md#token_header_key) + +___ + +### TOKEN\_QUERY\_KEY + +Re-exports [TOKEN_QUERY_KEY](common.md#token_query_key) + +___ + +### isAsyncGenerator + +Re-exports [isAsyncGenerator](common.md#isasyncgenerator) + +___ + +### isAsyncIterable + +Re-exports [isAsyncIterable](common.md#isasynciterable) + +___ + +### parseStreamData + +Re-exports [parseStreamData](common.md#parsestreamdata) + +___ + +### print + +Re-exports [print](common.md#print) + +___ + +### validateStreamEvent + +Re-exports [validateStreamEvent](common.md#validatestreamevent) diff --git a/docs/modules/common.md b/docs/modules/common.md new file mode 100644 index 00000000..bc4f21e8 --- /dev/null +++ b/docs/modules/common.md @@ -0,0 +1,195 @@ +[graphql-sse](../README.md) / common + +# Module: common + +## Table of contents + +### Interfaces + +- [ExecutionPatchResult](../interfaces/common.ExecutionPatchResult.md) +- [ExecutionResult](../interfaces/common.ExecutionResult.md) +- [RequestParams](../interfaces/common.RequestParams.md) +- [Sink](../interfaces/common.Sink.md) +- [StreamMessage](../interfaces/common.StreamMessage.md) + +### Type Aliases + +- [StreamData](common.md#streamdata) +- [StreamDataForID](common.md#streamdataforid) +- [StreamEvent](common.md#streamevent) + +### Variables + +- [TOKEN\_HEADER\_KEY](common.md#token_header_key) +- [TOKEN\_QUERY\_KEY](common.md#token_query_key) + +### Functions + +- [isAsyncGenerator](common.md#isasyncgenerator) +- [isAsyncIterable](common.md#isasynciterable) +- [parseStreamData](common.md#parsestreamdata) +- [print](common.md#print) +- [validateStreamEvent](common.md#validatestreamevent) + +## Common + +### StreamData + +Ƭ **StreamData**<`E`\>: `E` extends ``"next"`` ? [`ExecutionResult`](../interfaces/common.ExecutionResult.md) \| [`ExecutionPatchResult`](../interfaces/common.ExecutionPatchResult.md) : `E` extends ``"complete"`` ? ``null`` : `never` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `E` | extends [`StreamEvent`](common.md#streamevent) | + +___ + +### StreamDataForID + +Ƭ **StreamDataForID**<`E`\>: `E` extends ``"next"`` ? { `id`: `string` ; `payload`: [`ExecutionResult`](../interfaces/common.ExecutionResult.md) \| [`ExecutionPatchResult`](../interfaces/common.ExecutionPatchResult.md) } : `E` extends ``"complete"`` ? { `id`: `string` } : `never` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `E` | extends [`StreamEvent`](common.md#streamevent) | + +___ + +### StreamEvent + +Ƭ **StreamEvent**: ``"next"`` \| ``"complete"`` + +___ + +### TOKEN\_HEADER\_KEY + +• `Const` **TOKEN\_HEADER\_KEY**: ``"x-graphql-event-stream-token"`` + +Header key through which the event stream token is transmitted +when using the client in "single connection mode". + +Read more: https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#single-connection-mode + +___ + +### TOKEN\_QUERY\_KEY + +• `Const` **TOKEN\_QUERY\_KEY**: ``"token"`` + +URL query parameter key through which the event stream token is transmitted +when using the client in "single connection mode". + +Read more: https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#single-connection-mode + +___ + +### isAsyncGenerator + +▸ **isAsyncGenerator**<`T`\>(`val`): val is AsyncGenerator + +Checkes whether the provided value is an async generator. + +#### Type parameters + +| Name | +| :------ | +| `T` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `val` | `unknown` | + +#### Returns + +val is AsyncGenerator + +___ + +### isAsyncIterable + +▸ **isAsyncIterable**<`T`\>(`val`): val is AsyncIterable + +Checkes whether the provided value is an async iterable. + +#### Type parameters + +| Name | +| :------ | +| `T` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `val` | `unknown` | + +#### Returns + +val is AsyncIterable + +___ + +### parseStreamData + +▸ **parseStreamData**<`ForID`, `E`\>(`e`, `data`): `ForID` extends ``true`` ? [`StreamDataForID`](common.md#streamdataforid)<`E`\> : [`StreamData`](common.md#streamdata)<`E`\> + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `ForID` | extends `boolean` | +| `E` | extends [`StreamEvent`](common.md#streamevent) | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `e` | `E` | +| `data` | `string` | + +#### Returns + +`ForID` extends ``true`` ? [`StreamDataForID`](common.md#streamdataforid)<`E`\> : [`StreamData`](common.md#streamdata)<`E`\> + +___ + +### print + +▸ **print**<`ForID`, `E`\>(`msg`): `string` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `ForID` | extends `boolean` | +| `E` | extends [`StreamEvent`](common.md#streamevent) | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `msg` | [`StreamMessage`](../interfaces/common.StreamMessage.md)<`ForID`, `E`\> | + +#### Returns + +`string` + +___ + +### validateStreamEvent + +▸ **validateStreamEvent**(`e`): [`StreamEvent`](common.md#streamevent) + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `e` | `unknown` | + +#### Returns + +[`StreamEvent`](common.md#streamevent) diff --git a/docs/modules/handler.md b/docs/modules/handler.md new file mode 100644 index 00000000..3a5af2da --- /dev/null +++ b/docs/modules/handler.md @@ -0,0 +1,148 @@ +[graphql-sse](../README.md) / handler + +# Module: handler + +## Table of contents + +### Interfaces + +- [HandlerOptions](../interfaces/handler.HandlerOptions.md) +- [Request](../interfaces/handler.Request.md) +- [RequestHeaders](../interfaces/handler.RequestHeaders.md) +- [ResponseInit](../interfaces/handler.ResponseInit.md) + +### Type Aliases + +- [Handler](handler.md#handler) +- [OperationArgs](handler.md#operationargs) +- [OperationContext](handler.md#operationcontext) +- [OperationResult](handler.md#operationresult) +- [Response](handler.md#response) +- [ResponseBody](handler.md#responsebody) +- [ResponseHeaders](handler.md#responseheaders) + +### Functions + +- [createHandler](handler.md#createhandler) + +## Server + +### Handler + +Ƭ **Handler**<`RequestRaw`, `RequestContext`\>: (`req`: [`Request`](../interfaces/handler.Request.md)<`RequestRaw`, `RequestContext`\>) => `Promise`<[`Response`](handler.md#response)\> + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `RequestRaw` | `unknown` | +| `RequestContext` | `unknown` | + +#### Type declaration + +▸ (`req`): `Promise`<[`Response`](handler.md#response)\> + +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. + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | [`Request`](../interfaces/handler.Request.md)<`RequestRaw`, `RequestContext`\> | + +##### Returns + +`Promise`<[`Response`](handler.md#response)\> + +___ + +### OperationArgs + +Ƭ **OperationArgs**<`Context`\>: `ExecutionArgs` & { `contextValue`: `Context` } + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +___ + +### OperationContext + +Ƭ **OperationContext**: `Record`<`PropertyKey`, `unknown`\> \| `symbol` \| `number` \| `string` \| `boolean` \| `undefined` \| ``null`` + +A concrete GraphQL execution context value type. + +Mainly used because TypeScript collapes unions +with `any` or `unknown` to `any` or `unknown`. So, +we use a custom type to allow definitions such as +the `context` server option. + +___ + +### OperationResult + +Ƭ **OperationResult**: `Promise`<`AsyncGenerator`<[`ExecutionResult`](../interfaces/common.ExecutionResult.md) \| [`ExecutionPatchResult`](../interfaces/common.ExecutionPatchResult.md)\> \| `AsyncIterable`<[`ExecutionResult`](../interfaces/common.ExecutionResult.md) \| [`ExecutionPatchResult`](../interfaces/common.ExecutionPatchResult.md)\> \| [`ExecutionResult`](../interfaces/common.ExecutionResult.md)\> \| `AsyncGenerator`<[`ExecutionResult`](../interfaces/common.ExecutionResult.md) \| [`ExecutionPatchResult`](../interfaces/common.ExecutionPatchResult.md)\> \| `AsyncIterable`<[`ExecutionResult`](../interfaces/common.ExecutionResult.md) \| [`ExecutionPatchResult`](../interfaces/common.ExecutionPatchResult.md)\> \| [`ExecutionResult`](../interfaces/common.ExecutionResult.md) + +___ + +### Response + +Ƭ **Response**: readonly [body: ResponseBody \| null, init: ResponseInit] + +Server agnostic response returned from `graphql-sse` containing the +body and init options needing to be coerced to the server implementation in use. + +___ + +### ResponseBody + +Ƭ **ResponseBody**: `string` \| `AsyncGenerator`<`string`, `void`, `undefined`\> + +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. + +___ + +### ResponseHeaders + +Ƭ **ResponseHeaders**: { `accept?`: `string` ; `allow?`: `string` ; `content-type?`: `string` } & `Record`<`string`, `string`\> + +The response headers that get returned from graphql-sse. + +___ + +### createHandler + +▸ **createHandler**<`RequestRaw`, `RequestContext`, `Context`\>(`options`): [`Handler`](handler.md#handler)<`RequestRaw`, `RequestContext`\> + +Makes a Protocol complient 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. + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `RequestRaw` | `unknown` | +| `RequestContext` | `unknown` | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`RequestRaw`, `RequestContext`, `Context`\> | + +#### Returns + +[`Handler`](handler.md#handler)<`RequestRaw`, `RequestContext`\> diff --git a/docs/modules/use_express.md b/docs/modules/use_express.md new file mode 100644 index 00000000..e9280d1f --- /dev/null +++ b/docs/modules/use_express.md @@ -0,0 +1,81 @@ +[graphql-sse](../README.md) / use/express + +# Module: use/express + +## Table of contents + +### Interfaces + +- [RequestContext](../interfaces/use_express.RequestContext.md) + +### Functions + +- [createHandler](use_express.md#createhandler) + +## Server/express + +### createHandler + +▸ **createHandler**<`Context`\>(`options`): (`req`: `Request`, `res`: `Response`) => `Promise`<`void`\> + +The ready-to-use handler for [express](https://expressjs.com). + +Errors thrown from the provided options or callbacks (or even due to +library misuse or potential bugs) will reject the handler or bubble to the +returned iterator. They are considered internal errors and you should take care +of them accordingly. + +For production environments, its recommended not to transmit the exact internal +error details to the client, but instead report to an error logging tool or simply +the console. + +```ts +import express from 'express'; // yarn add express +import { createHandler } from 'graphql-sse/lib/use/express'; +import { schema } from './my-graphql'; + +const handler = createHandler({ schema }); + +const app = express(); + +app.use('/graphql/stream', async (req, res) => { + try { + await handler(req, res); + } catch (err) { + console.error(err); + res.writeHead(500).end(); + } +}); + +server.listen(4000); +console.log('Listening to port 4000'); +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`Request`<`ParamsDictionary`, `any`, `any`, `ParsedQs`, `Record`<`string`, `any`\>\>, [`RequestContext`](../interfaces/use_express.RequestContext.md), `Context`\> | + +#### Returns + +`fn` + +▸ (`req`, `res`): `Promise`<`void`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | `Request` | +| `res` | `Response` | + +##### Returns + +`Promise`<`void`\> diff --git a/docs/modules/use_fastify.md b/docs/modules/use_fastify.md new file mode 100644 index 00000000..3fbec714 --- /dev/null +++ b/docs/modules/use_fastify.md @@ -0,0 +1,80 @@ +[graphql-sse](../README.md) / use/fastify + +# Module: use/fastify + +## Table of contents + +### Interfaces + +- [RequestContext](../interfaces/use_fastify.RequestContext.md) + +### Functions + +- [createHandler](use_fastify.md#createhandler) + +## Server/fastify + +### createHandler + +▸ **createHandler**<`Context`\>(`options`): (`req`: `FastifyRequest`, `reply`: `FastifyReply`) => `Promise`<`void`\> + +The ready-to-use handler for [fastify](https://www.fastify.io). + +Errors thrown from the provided options or callbacks (or even due to +library misuse or potential bugs) will reject the handler or bubble to the +returned iterator. They are considered internal errors and you should take care +of them accordingly. + +For production environments, its recommended not to transmit the exact internal +error details to the client, but instead report to an error logging tool or simply +the console. + +```ts +import Fastify from 'fastify'; // yarn add fastify +import { createHandler } from 'graphql-sse/lib/use/fastify'; + +const handler = createHandler({ schema }); + +const fastify = Fastify(); + +fastify.all('/graphql/stream', async (req, reply) => { + try { + await handler(req, reply); + } catch (err) { + console.error(err); + reply.code(500).send(); + } +}); + +fastify.listen({ port: 4000 }); +console.log('Listening to port 4000'); +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`FastifyRequest`<`RouteGenericInterface`, `RawServerDefault`, `IncomingMessage`, `FastifySchema`, `FastifyTypeProviderDefault`, `unknown`, `FastifyBaseLogger`, `ResolveFastifyRequestType`<`FastifyTypeProviderDefault`, `FastifySchema`, `RouteGenericInterface`\>\>, [`RequestContext`](../interfaces/use_fastify.RequestContext.md), `Context`\> | + +#### Returns + +`fn` + +▸ (`req`, `reply`): `Promise`<`void`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | `FastifyRequest` | +| `reply` | `FastifyReply` | + +##### Returns + +`Promise`<`void`\> diff --git a/docs/modules/use_fetch.md b/docs/modules/use_fetch.md new file mode 100644 index 00000000..91133cd5 --- /dev/null +++ b/docs/modules/use_fetch.md @@ -0,0 +1,76 @@ +[graphql-sse](../README.md) / use/fetch + +# Module: use/fetch + +## Table of contents + +### Interfaces + +- [RequestContext](../interfaces/use_fetch.RequestContext.md) + +### Functions + +- [createHandler](use_fetch.md#createhandler) + +## Server/fetch + +### createHandler + +▸ **createHandler**<`Context`\>(`options`, `reqCtx?`): (`req`: `Request`) => `Promise`<`Response`\> + +The ready-to-use fetch handler. To be used with your favourite fetch +framework, in a lambda function, or have deploy to the edge. + +Errors thrown from the provided options or callbacks (or even due to +library misuse or potential bugs) will reject the handler or bubble to the +returned iterator. They are considered internal errors and you should take care +of them accordingly. + +For production environments, its recommended not to transmit the exact internal +error details to the client, but instead report to an error logging tool or simply +the console. + +```ts +import { createHandler } from 'graphql-sse/lib/use/fetch'; +import { schema } from './my-graphql'; + +const handler = createHandler({ schema }); + +export async function fetch(req: Request): Promise { + try { + return await handler(req); + } catch (err) { + console.error(err); + return new Response(null, { status: 500 }); + } +} +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`Request`, [`RequestContext`](../interfaces/use_fetch.RequestContext.md), `Context`\> | +| `reqCtx` | `Partial`<[`RequestContext`](../interfaces/use_fetch.RequestContext.md)\> | + +#### Returns + +`fn` + +▸ (`req`): `Promise`<`Response`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | `Request` | + +##### Returns + +`Promise`<`Response`\> diff --git a/docs/modules/use_http.md b/docs/modules/use_http.md new file mode 100644 index 00000000..61ad33cf --- /dev/null +++ b/docs/modules/use_http.md @@ -0,0 +1,79 @@ +[graphql-sse](../README.md) / use/http + +# Module: use/http + +## Table of contents + +### Interfaces + +- [RequestContext](../interfaces/use_http.RequestContext.md) + +### Functions + +- [createHandler](use_http.md#createhandler) + +## Server/http + +### createHandler + +▸ **createHandler**<`Context`\>(`options`): (`req`: `IncomingMessage`, `res`: `ServerResponse`) => `Promise`<`void`\> + +The ready-to-use handler for Node's [http](https://nodejs.org/api/http.html). + +Errors thrown from the provided options or callbacks (or even due to +library misuse or potential bugs) will reject the handler or bubble to the +returned iterator. They are considered internal errors and you should take care +of them accordingly. + +For production environments, its recommended not to transmit the exact internal +error details to the client, but instead report to an error logging tool or simply +the console. + +```ts +import http from 'http'; +import { createHandler } from 'graphql-sse/lib/use/http'; +import { schema } from './my-graphql'; + +const handler = createHandler({ schema }); + +const server = http.createServer(async (req, res) => { + try { + await handler(req, res); + } catch (err) { + console.error(err); + res.writeHead(500).end(); + } +}); + +server.listen(4000); +console.log('Listening to port 4000'); +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`IncomingMessage`, [`RequestContext`](../interfaces/use_http.RequestContext.md), `Context`\> | + +#### Returns + +`fn` + +▸ (`req`, `res`): `Promise`<`void`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | `IncomingMessage` | +| `res` | `ServerResponse` | + +##### Returns + +`Promise`<`void`\> diff --git a/docs/modules/use_http2.md b/docs/modules/use_http2.md new file mode 100644 index 00000000..47ce4dc0 --- /dev/null +++ b/docs/modules/use_http2.md @@ -0,0 +1,79 @@ +[graphql-sse](../README.md) / use/http2 + +# Module: use/http2 + +## Table of contents + +### Interfaces + +- [RequestContext](../interfaces/use_http2.RequestContext.md) + +### Functions + +- [createHandler](use_http2.md#createhandler) + +## Server/http2 + +### createHandler + +▸ **createHandler**<`Context`\>(`options`): (`req`: `Http2ServerRequest`, `res`: `Http2ServerResponse`) => `Promise`<`void`\> + +The ready-to-use handler for Node's [http](https://nodejs.org/api/http2.html). + +Errors thrown from the provided options or callbacks (or even due to +library misuse or potential bugs) will reject the handler or bubble to the +returned iterator. They are considered internal errors and you should take care +of them accordingly. + +For production environments, its recommended not to transmit the exact internal +error details to the client, but instead report to an error logging tool or simply +the console. + +```ts +import http from 'http2'; +import { createHandler } from 'graphql-sse/lib/use/http2'; +import { schema } from './my-graphql'; + +const handler = createHandler({ schema }); + +const server = http.createServer(async (req, res) => { + try { + await handler(req, res); + } catch (err) { + console.error(err); + res.writeHead(500).end(); + } +}); + +server.listen(4000); +console.log('Listening to port 4000'); +``` + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `Context` | extends [`OperationContext`](handler.md#operationcontext) = `undefined` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `options` | [`HandlerOptions`](../interfaces/handler.HandlerOptions.md)<`Http2ServerRequest`, [`RequestContext`](../interfaces/use_http2.RequestContext.md), `Context`\> | + +#### Returns + +`fn` + +▸ (`req`, `res`): `Promise`<`void`\> + +##### Parameters + +| Name | Type | +| :------ | :------ | +| `req` | `Http2ServerRequest` | +| `res` | `Http2ServerResponse` | + +##### Returns + +`Promise`<`void`\> diff --git a/jest.config.js b/jest.config.js index bd16c339..afac4db4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,14 +3,8 @@ */ const opts = { testEnvironment: 'node', - testRunner: 'jest-jasmine2', // until https://github.com/facebook/jest/issues/11698 and hopefully https://github.com/facebook/jest/issues/10529 moduleFileExtensions: ['ts', 'js'], extensionsToTreatAsEsm: ['.ts'], - testPathIgnorePatterns: [ - '/__tests__/jest.d.ts', // augments some jest types - '/node_modules/', - '/fixtures/', - '/utils/', - ], + testPathIgnorePatterns: ['/node_modules/', '/fixtures/', '/utils/'], }; module.exports = opts; diff --git a/package.json b/package.json index 3476d5b2..edbe6513 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,31 @@ "types": "./lib/index.d.ts", "browser": "./umd/graphql-sse.js" }, + "./lib/use/fetch": { + "types": "./lib/use/fetch.d.ts", + "require": "./lib/use/fetch.js", + "import": "./lib/use/fetch.mjs" + }, + "./lib/use/http": { + "types": "./lib/use/http.d.ts", + "require": "./lib/use/http.js", + "import": "./lib/use/http.mjs" + }, + "./lib/use/http2": { + "types": "./lib/use/http2.d.ts", + "require": "./lib/use/http2.js", + "import": "./lib/use/http2.mjs" + }, + "./lib/use/express": { + "types": "./lib/use/express.d.ts", + "require": "./lib/use/express.js", + "import": "./lib/use/express.mjs" + }, + "./lib/use/fastify": { + "types": "./lib/use/fastify.d.ts", + "require": "./lib/use/fastify.js", + "import": "./lib/use/fastify.mjs" + }, "./package.json": "./package.json" }, "types": "lib/index.d.ts", @@ -83,22 +108,19 @@ "@semantic-release/git": "^10.0.1", "@types/eslint": "^8.4.10", "@types/eventsource": "^1.1.10", - "@types/express": "^4.17.14", + "@types/express": "^4.17.15", "@types/glob": "^8.0.0", - "@types/jest": "^28.1.8", + "@types/jest": "^29.2.4", "@typescript-eslint/eslint-plugin": "^5.45.1", "@typescript-eslint/parser": "^5.45.1", - "babel-jest": "^28.1.3", + "babel-jest": "^29.3.1", "eslint": "^8.29.0", "eslint-config-prettier": "^8.5.0", - "eventsource": "^2.0.2", "express": "^4.18.2", "fastify": "^4.10.2", "glob": "^8.0.3", "graphql": "^16.6.0", - "jest": "^28.1.3", - "jest-jasmine2": "^28.1.3", - "node-fetch": "^3.3.0", + "jest": "^29.3.1", "prettier": "^2.8.0", "rollup": "^3.6.0", "semantic-release": "^19.0.5", diff --git a/src/__tests__/__snapshots__/client.ts.snap b/src/__tests__/__snapshots__/client.ts.snap deleted file mode 100644 index b925a969..00000000 --- a/src/__tests__/__snapshots__/client.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`retries should keep retrying network errors until the retry attempts are exceeded 1`] = `[NetworkError: Server responded with 403: Forbidden]`; - -exports[`retries should keep retrying network errors until the retry attempts are exceeded 2`] = `[NetworkError: Server responded with 403: Forbidden]`; - -exports[`retries should keep retrying network errors until the retry attempts are exceeded 3`] = `[NetworkError: Server responded with 403: Forbidden]`; - -exports[`single connection mode should complete subscriptions when disposing them 1`] = ` -Object { - "data": Object { - "ping": "pong", - }, -} -`; - -exports[`single connection mode should execute a simple query 1`] = ` -Object { - "data": Object { - "getValue": "value", - }, -} -`; diff --git a/src/__tests__/__snapshots__/handler.ts.snap b/src/__tests__/__snapshots__/handler.ts.snap index d3444083..ff7f55b7 100644 --- a/src/__tests__/__snapshots__/handler.ts.snap +++ b/src/__tests__/__snapshots__/handler.ts.snap @@ -1,281 +1,175 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`distinct connections mode should report operation validation issues by streaming them 1`] = ` -Object { - "data": Object { - "errors": Array [ - Object { - "locations": Array [ - Object { - "column": 3, - "line": 1, - }, - ], - "message": "Cannot query field \\"notExists\\" on type \\"Query\\".", - }, - ], - }, - "event": "next", -} -`; - -exports[`distinct connections mode should report operation validation issues by streaming them 2`] = ` -Object { - "data": null, - "event": "complete", -} -`; - exports[`distinct connections mode should stream query operations to connected event stream and then disconnect 1`] = ` -Object { - "data": Object { - "data": Object { - "getValue": "value", - }, - }, - "event": "next", -} +": + +" `; exports[`distinct connections mode should stream query operations to connected event stream and then disconnect 2`] = ` -Object { - "data": null, - "event": "complete", -} +"event: next +data: {"data":{"getValue":"value"}} + +" `; exports[`distinct connections mode should stream query operations to connected event stream and then disconnect 3`] = ` -Object { - "data": Object { - "data": Object { - "getValue": "value", - }, - }, - "event": "next", -} +"event: complete + +" `; exports[`distinct connections mode should stream query operations to connected event stream and then disconnect 4`] = ` -Object { - "data": null, - "event": "complete", -} +": + +" +`; + +exports[`distinct connections mode should stream query operations to connected event stream and then disconnect 5`] = ` +"event: next +data: {"data":{"getValue":"value"}} + +" +`; + +exports[`distinct connections mode should stream query operations to connected event stream and then disconnect 6`] = ` +"event: complete + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 1`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Hi", - }, - }, - "event": "next", -} +": + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 2`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Bonjour", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Hi"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 3`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Hola", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Bonjour"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 4`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Ciao", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Hola"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 5`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Zdravo", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Ciao"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 6`] = ` -Object { - "data": null, - "event": "complete", -} +"event: next +data: {"data":{"greetings":"Zdravo"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 7`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Hi", - }, - }, - "event": "next", -} +"event: complete + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 8`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Bonjour", - }, - }, - "event": "next", -} +": + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 9`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Hola", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Hi"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 10`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Ciao", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Bonjour"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 11`] = ` -Object { - "data": Object { - "data": Object { - "greetings": "Zdravo", - }, - }, - "event": "next", -} +"event: next +data: {"data":{"greetings":"Hola"}} + +" `; exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 12`] = ` -Object { - "data": null, - "event": "complete", -} +"event: next +data: {"data":{"greetings":"Ciao"}} + +" `; -exports[`express should work as advertised in the readme 1`] = ` -Array [ - Array [ - Object { - "data": Object { - "greetings": "Hi", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Bonjour", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Hola", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Ciao", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Zdravo", - }, - }, - ], -] +exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 13`] = ` +"event: next +data: {"data":{"greetings":"Zdravo"}} + +" `; -exports[`fastify should work as advertised in the readme 1`] = ` -Array [ - Array [ - Object { - "data": Object { - "greetings": "Hi", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Bonjour", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Hola", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Ciao", - }, - }, - ], - Array [ - Object { - "data": Object { - "greetings": "Zdravo", - }, - }, - ], -] +exports[`distinct connections mode should stream subscription operations to connected event stream and then disconnect 14`] = ` +"event: complete + +" `; -exports[`single connection mode should report operation validation issues to request 1`] = `"{\\"errors\\":[{\\"message\\":\\"Cannot query field \\\\\\"notExists\\\\\\" on type \\\\\\"Query\\\\\\".\\",\\"locations\\":[{\\"line\\":1,\\"column\\":3}]}]}"`; +exports[`single connection mode should stream subscription operations to connected event stream 2`] = ` +"event: next +data: {"id":"1","payload":{"data":{"greetings":"Hi"}}} -exports[`single connection mode should stream query operations to connected event stream 1`] = `"{\\"id\\":\\"1\\",\\"payload\\":{\\"data\\":{\\"getValue\\":\\"value\\"}}}"`; +" +`; -exports[`single connection mode should stream subscription operations to connected event stream 1`] = `"{\\"id\\":\\"1\\",\\"payload\\":{\\"data\\":{\\"greetings\\":\\"Hi\\"}}}"`; +exports[`single connection mode should stream subscription operations to connected event stream 3`] = ` +"event: next +data: {"id":"1","payload":{"data":{"greetings":"Bonjour"}}} -exports[`single connection mode should stream subscription operations to connected event stream 2`] = `"{\\"id\\":\\"1\\",\\"payload\\":{\\"data\\":{\\"greetings\\":\\"Bonjour\\"}}}"`; +" +`; -exports[`single connection mode should stream subscription operations to connected event stream 3`] = `"{\\"id\\":\\"1\\",\\"payload\\":{\\"data\\":{\\"greetings\\":\\"Hola\\"}}}"`; +exports[`single connection mode should stream subscription operations to connected event stream 4`] = ` +"event: next +data: {"id":"1","payload":{"data":{"greetings":"Hola"}}} -exports[`single connection mode should stream subscription operations to connected event stream 4`] = `"{\\"id\\":\\"1\\",\\"payload\\":{\\"data\\":{\\"greetings\\":\\"Ciao\\"}}}"`; +" +`; + +exports[`single connection mode should stream subscription operations to connected event stream 5`] = ` +"event: next +data: {"id":"1","payload":{"data":{"greetings":"Ciao"}}} + +" +`; + +exports[`single connection mode should stream subscription operations to connected event stream 6`] = ` +"event: next +data: {"id":"1","payload":{"data":{"greetings":"Zdravo"}}} -exports[`single connection mode should stream subscription operations to connected event stream 5`] = `"{\\"id\\":\\"1\\",\\"payload\\":{\\"data\\":{\\"greetings\\":\\"Zdravo\\"}}}"`; +" +`; + +exports[`single connection mode should stream subscription operations to connected event stream 7`] = ` +"event: complete +data: {"id":"1"} + +" +`; diff --git a/src/__tests__/__snapshots__/parser.ts.snap b/src/__tests__/__snapshots__/parser.ts.snap index cef8b68e..73630838 100644 --- a/src/__tests__/__snapshots__/parser.ts.snap +++ b/src/__tests__/__snapshots__/parser.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should accept valid events only 1`] = `"Invalid stream event \\"done\\""`; +exports[`should accept valid events only 1`] = `"Invalid stream event "done""`; -exports[`should accept valid events only 2`] = `"Invalid stream event \\"value\\""`; +exports[`should accept valid events only 2`] = `"Invalid stream event "value""`; exports[`should ignore comments 1`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "iAm": "data", }, "event": "next", @@ -16,9 +16,9 @@ Array [ `; exports[`should parse chunked message 1`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "iAm": "data", }, "event": "next", @@ -27,9 +27,9 @@ Array [ `; exports[`should parse message whose lines are separated by \\r\\n 1`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "iAm": "data", }, "event": "next", @@ -38,9 +38,9 @@ Array [ `; exports[`should parse message with prepended ping 1`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "iAm": "data", }, "event": "next", @@ -49,13 +49,13 @@ Array [ `; exports[`should parse multiple messages from one chunk 1`] = ` -Array [ - Object { - "data": Object {}, +[ + { + "data": {}, "event": "next", }, - Object { - "data": Object { + { + "data": { "no": "data", }, "event": "next", @@ -64,24 +64,24 @@ Array [ `; exports[`should parse multiple messages from one chunk 2`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "almost": "done", }, "event": "next", }, - Object { - "data": Object {}, + { + "data": {}, "event": "complete", }, ] `; exports[`should parse whole message 1`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "iAm": "data", }, "event": "next", @@ -90,9 +90,9 @@ Array [ `; exports[`should parse whole message 2`] = ` -Array [ - Object { - "data": Object { +[ + { + "data": { "iAm": "data", }, "event": "next", @@ -101,8 +101,8 @@ Array [ `; exports[`should parse whole message 3`] = ` -Array [ - Object { +[ + { "data": null, "event": "complete", }, diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index f6b79333..5ad10026 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -1,184 +1,166 @@ -import fetch from 'node-fetch'; -import { startTServer } from './utils/tserver'; +import { jest } from '@jest/globals'; +import { createClient, StreamMessage, StreamEvent } from '../client'; +import { createTFetch } from './utils/tfetch'; import { tsubscribe } from './utils/tsubscribe'; -import { createClient } from '../client'; +import { pong } from './fixtures/simple'; +import { sleep } from './utils/testkit'; -// just does nothing -function noop(): void { - /**/ +function noop() { + // do nothing } -it('should use the provided headers', async (done) => { - expect.assertions(4); - +it('should use the provided headers', async () => { // single connection mode - - const singleConnServer = await startTServer({ + let headers!: Headers; + let { fetch } = createTFetch({ authenticate: (req) => { - expect(req.headers['x-single']).toBe('header'); + headers = req.raw.headers; return ''; }, }); const singleConnClient = createClient({ singleConnection: true, - url: singleConnServer.url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, - headers: async () => { + headers: () => { return { 'x-single': 'header' }; }, }); - await new Promise((resolve) => { - singleConnClient.subscribe( - { - query: '{ getValue }', - }, - { - next: noop, - error: fail, - complete: () => { - singleConnClient.dispose(); - singleConnServer.dispose(); - resolve(); - }, - }, - ); + let client = tsubscribe(singleConnClient, { + query: '{ getValue }', }); + await Promise.race([client.throwOnError(), client.waitForComplete()]); + client.dispose(); - // distinct connections mode + expect(headers.get('x-single')).toBe('header'); - const distinctConnServer = await startTServer({ + // distinct connections mode + ({ fetch } = createTFetch({ authenticate: (req) => { - distinctConnClient.dispose(); - distinctConnServer.dispose(); - expect(req.headers['x-distinct']).toBe('header'); - done(); + headers = req.raw.headers; return ''; }, - }); + })); const distinctConnClient = createClient({ singleConnection: false, - url: distinctConnServer.url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, - headers: async () => { + headers: () => { return { 'x-distinct': 'header' }; }, }); - distinctConnClient.subscribe( - { - query: '{ getValue }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); + client = tsubscribe(distinctConnClient, { + query: '{ getValue }', + }); + await Promise.race([client.throwOnError(), client.waitForComplete()]); + client.dispose(); + + expect(headers.get('x-distinct')).toBe('header'); }); it('should supply all valid messages received to onMessage', async () => { - expect.assertions(4); - - const { url } = await startTServer(); + const { fetch } = createTFetch(); // single connection mode - let i = 0; + let msgs: StreamMessage[] = []; let client = createClient({ singleConnection: true, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, generateID: () => 'veryunique', - onMessage: (msg) => { - switch (++i) { - case 1: - expect(msg).toEqual({ - event: 'next', - data: { - id: 'veryunique', - payload: { data: { getValue: 'value' } }, - }, - }); - return; - case 2: - expect(msg).toEqual({ - event: 'complete', - data: { id: 'veryunique' }, - }); - return; - default: - fail('Unexpected message receieved'); - } - }, + onMessage: (msg) => msgs.push(msg), }); let sub = tsubscribe(client, { query: '{ getValue }', }); - await sub.waitForComplete(); + await Promise.race([sub.throwOnError(), sub.waitForComplete()]); + expect(msgs).toMatchInlineSnapshot(` + [ + { + "data": { + "id": "veryunique", + "payload": { + "data": { + "getValue": "value", + }, + }, + }, + "event": "next", + }, + { + "data": { + "id": "veryunique", + }, + "event": "complete", + }, + ] + `); // distinct connection mode - i = 0; + msgs = []; client = createClient({ singleConnection: false, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, generateID: () => 'veryunique', - onMessage: (msg) => { - switch (++i) { - case 1: - expect(msg).toEqual({ - event: 'next', - data: { data: { getValue: 'value' } }, - }); - return; - case 2: - expect(msg).toEqual({ - event: 'complete', - data: null, - }); - return; - default: - fail('Unexpected message receieved'); - } - }, + onMessage: (msg) => msgs.push(msg), }); sub = tsubscribe(client, { query: '{ getValue }', }); - await sub.waitForComplete(); + await Promise.race([sub.throwOnError(), sub.waitForComplete()]); + expect(msgs).toMatchInlineSnapshot(` + [ + { + "data": { + "data": { + "getValue": "value", + }, + }, + "event": "next", + }, + { + "data": null, + "event": "complete", + }, + ] + `); }); it('should report error to sink if server goes away', async () => { - const server = await startTServer(); + const { fetch, dispose } = createTFetch(); const client = createClient({ - url: server.url, fetchFn: fetch, + url: 'http://localhost', retryAttempts: 0, }); const sub = tsubscribe(client, { - query: 'subscription { ping }', + query: `subscription { ping(key: "${Math.random()}") }`, }); - await server.waitForOperation(); - await server.dispose(); + await dispose(); - await sub.waitForError(); + await expect(sub.waitForError()).resolves.toMatchInlineSnapshot( + `[NetworkError: Connection closed while having active streams]`, + ); }); it('should report error to sink if server goes away during generator emission', async () => { - const server = await startTServer(); + const { fetch, dispose } = createTFetch(); const client = createClient({ - url: server.url, fetchFn: fetch, + url: 'http://localhost', retryAttempts: 0, }); @@ -187,18 +169,20 @@ it('should report error to sink if server goes away during generator emission', }); await sub.waitForNext(); - await server.dispose(); + await dispose(); - await sub.waitForError(); + await expect(sub.waitForError()).resolves.toMatchInlineSnapshot( + `[NetworkError: Connection closed while having active streams]`, + ); }); describe('single connection mode', () => { it('should not call complete after subscription error', async () => { - const { url } = await startTServer(); + const { fetch } = createTFetch(); const client = createClient({ singleConnection: true, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, }); @@ -207,187 +191,173 @@ describe('single connection mode', () => { query: '}}', }); - await sub.waitForError(); - - await sub.waitForComplete(() => { - fail("shouldn't have completed"); - }, 20); + await expect( + Promise.race([sub.waitForError(), sub.waitForComplete()]), + ).resolves.toMatchInlineSnapshot( + `[NetworkError: Server responded with 400: Bad Request]`, + ); }); - it('should execute a simple query', async (done) => { - expect.hasAssertions(); - - const { url } = await startTServer(); + it('should execute a simple query', async () => { + const { fetch } = createTFetch(); const client = createClient({ singleConnection: true, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, }); - client.subscribe( - { - query: '{ getValue }', - }, + const sub = tsubscribe(client, { + query: '{ getValue }', + }); + + await expect(sub.waitForNext()).resolves.toMatchInlineSnapshot(` { - next: (val) => expect(val).toMatchSnapshot(), - error: fail, - complete: done, - }, - ); - }); + "data": { + "getValue": "value", + }, + } + `); - it('should complete subscriptions when disposing them', async (done) => { - expect.hasAssertions(); + await sub.waitForComplete(); + }); - const { url, waitForOperation, pong } = await startTServer(); + it('should complete subscriptions when disposing them', async () => { + const { fetch } = createTFetch(); const client = createClient({ singleConnection: true, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, lazy: true, }); - const dispose = client.subscribe( - { - query: 'subscription { ping }', - }, + const key = Math.random().toString(); + const sub = tsubscribe(client, { + query: `subscription { ping(key: "${key}") }`, + }); + + setTimeout(() => pong(key), 0); + + await expect(sub.waitForNext()).resolves.toMatchInlineSnapshot(` { - next: (val) => { - expect(val).toMatchSnapshot(); - dispose(); + "data": { + "ping": "pong", }, - error: fail, - complete: done, - }, - ); + } + `); - await waitForOperation(); + sub.dispose(); - pong(); + await sub.waitForComplete(); }); describe('lazy', () => { it('should connect on first subscribe and disconnect on last complete', async () => { - const { url, waitForOperation, waitForDisconnect, waitForComplete } = - await startTServer(); + const { fetch, waitForOperation, waitForRequest } = createTFetch(); const client = createClient({ singleConnection: true, - lazy: true, // default - url, + lazy: true, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, }); - const dispose1 = client.subscribe( - { - query: 'subscription { ping(key: "1") }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); + const sub1 = tsubscribe(client, { + query: `subscription { ping(key: "${Math.random()}") }`, + }); await waitForOperation(); - const dispose2 = client.subscribe( - { - query: 'subscription { ping(key: "2") }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); + // put + await waitForRequest(); + // stream + const streamReq = await waitForRequest(); + + const sub2 = tsubscribe(client, { + query: `subscription { ping(key: "${Math.random()}") }`, + }); await waitForOperation(); - dispose1(); - await waitForComplete(); - await waitForDisconnect(() => fail("Shouldn't have disconnected"), 30); + sub1.dispose(); + await sub1.waitForComplete(); + await sleep(20); + expect(streamReq.signal.aborted).toBeFalsy(); - dispose2(); - await waitForComplete(); - await waitForDisconnect(); + sub2.dispose(); + await sub2.waitForComplete(); + await sleep(20); + expect(streamReq.signal.aborted).toBeTruthy(); }); it('should disconnect after the lazyCloseTimeout has passed after last unsubscribe', async () => { - const { url, waitForOperation, waitForDisconnect } = await startTServer(); + const { fetch, waitForOperation, waitForRequest } = createTFetch(); const client = createClient({ singleConnection: true, lazy: true, // default lazyCloseTimeout: 20, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, }); const sub = tsubscribe(client, { - query: 'subscription { ping }', + query: `subscription { ping(key: "${Math.random()}") }`, }); await waitForOperation(); - sub.dispose(); + // put + await waitForRequest(); + // stream + const streamReq = await waitForRequest(); + sub.dispose(); await sub.waitForComplete(); + await sleep(10); // still connected due to timeout - await waitForDisconnect(() => fail("Shouldn't have disconnected"), 10); + expect(streamReq.signal.aborted).toBeFalsy(); + await sleep(10); // but will disconnect after timeout - await waitForDisconnect(); + expect(streamReq.signal.aborted).toBeTruthy(); }); }); describe('non-lazy', () => { - it('should connect as soon as the client is created', async () => { - const { url, waitForConnected } = await startTServer(); - - createClient({ - singleConnection: true, - url, - fetchFn: fetch, - retryAttempts: 0, - lazy: false, - onNonLazyError: noop, // avoiding premature close errors - }); - - await waitForConnected(); - }); - - it('should disconnect when the client gets disposed', async () => { - const { url, waitForConnected, waitForDisconnect } = await startTServer(); + it('should connect as soon as the client is created and disconnect when disposed', async () => { + const { fetch, waitForRequest } = createTFetch(); const client = createClient({ singleConnection: true, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, lazy: false, - onNonLazyError: fail, + onNonLazyError: noop, // avoiding premature close errors }); - await waitForConnected(); + // put + await waitForRequest(); + // stream + const stream = await waitForRequest(); client.dispose(); - await waitForDisconnect(); + expect(stream.signal.aborted).toBeTruthy(); }); }); }); describe('distinct connections mode', () => { it('should not call complete after subscription error', async () => { - const { url } = await startTServer(); + const { fetch } = createTFetch(); const client = createClient({ - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 0, }); @@ -398,188 +368,149 @@ describe('distinct connections mode', () => { await sub.waitForError(); - await sub.waitForComplete(() => { - fail("shouldn't have completed"); - }, 20); + await Promise.race([ + sleep(20), + sub.waitForComplete().then(() => { + throw new Error("Shouldn't have completed"); + }), + ]); }); it('should establish separate connections for each subscribe', async () => { - const { url, waitForConnected, waitForDisconnect } = await startTServer(); + const { fetch, waitForRequest, waitForOperation } = createTFetch(); const client = createClient({ singleConnection: false, - url, + url: 'http://localhost', retryAttempts: 0, fetchFn: fetch, }); - const dispose1 = client.subscribe( - { - query: 'subscription { ping(key: "1") }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); - await waitForConnected(); + const sub1 = tsubscribe(client, { + query: `subscription { ping(key: "${Math.random()}") }`, + }); + await waitForOperation(); + const stream1 = await waitForRequest(); - const dispose2 = client.subscribe( - { - query: 'subscription { ping(key: "2") }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); - await waitForConnected(); + const sub2 = tsubscribe(client, { + query: `subscription { ping(key: "${Math.random()}") }`, + }); + await waitForOperation(); + const stream2 = await waitForRequest(); - dispose1(); - await waitForDisconnect(); + sub1.dispose(); + await Promise.race([sub1.throwOnError(), sub1.waitForComplete()]); + expect(stream1.signal.aborted).toBeTruthy(); - dispose2(); - await waitForDisconnect(); + sub2.dispose(); + await Promise.race([sub2.throwOnError(), sub2.waitForComplete()]); + expect(stream2.signal.aborted).toBeTruthy(); }); it('should complete all connections when client disposes', async () => { - const { url, waitForConnected, waitForDisconnect } = await startTServer(); + const { fetch, waitForRequest, waitForOperation } = createTFetch(); const client = createClient({ singleConnection: false, - url, + url: 'http://localhost', retryAttempts: 0, fetchFn: fetch, }); - client.subscribe( - { - query: 'subscription { ping(key: "1") }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); - await waitForConnected(); + tsubscribe(client, { + query: `subscription { ping(key: "${Math.random()}") }`, + }); + await waitForOperation(); + const stream1 = await waitForRequest(); - client.subscribe( - { - query: 'subscription { ping(key: "2") }', - }, - { - next: noop, - error: fail, - complete: noop, - }, - ); - await waitForConnected(); + tsubscribe(client, { + query: `subscription { ping(key: "${Math.random()}") }`, + }); + await waitForOperation(); + const stream2 = await waitForRequest(); client.dispose(); - await waitForDisconnect(); - await waitForDisconnect(); + + expect(stream1.signal.aborted).toBeTruthy(); + expect(stream2.signal.aborted).toBeTruthy(); }); }); describe('retries', () => { it('should keep retrying network errors until the retry attempts are exceeded', async () => { let tried = 0; - const { url } = await startTServer({ - authenticate: (_, res) => { + const { fetch } = createTFetch({ + authenticate() { tried++; - res.writeHead(403).end(); + return [null, { status: 403, statusText: 'Forbidden' }]; }, }); + // non-lazy + tried = 0; await new Promise((resolve) => { - // non-lazy - - createClient({ + const client = createClient({ singleConnection: true, - url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 2, retry: () => Promise.resolve(), lazy: false, onNonLazyError: (err) => { - expect(err).toMatchSnapshot(); + expect(err).toMatchInlineSnapshot( + `[NetworkError: Server responded with 403: Forbidden]`, + ); expect(tried).toBe(3); // initial + 2 retries resolve(); + client.dispose(); }, }); }); - await new Promise((resolve) => { - // lazy - - tried = 0; - const client = createClient({ - singleConnection: true, - url, - fetchFn: fetch, - retryAttempts: 2, - retry: () => Promise.resolve(), - }); - - client.subscribe( - { - query: '{ getValue }', - }, - { - next: noop, - error: (err) => { - expect(err).toMatchSnapshot(); - expect(tried).toBe(3); // initial + 2 retries - resolve(); - }, - complete: noop, - }, - ); + // lazy + tried = 0; + let client = createClient({ + singleConnection: true, + url: 'http://localhost', + fetchFn: fetch, + retryAttempts: 2, + retry: () => Promise.resolve(), }); + let sub = tsubscribe(client, { query: '{ getValue }' }); + await expect(sub.waitForError()).resolves.toMatchInlineSnapshot( + `[NetworkError: Server responded with 403: Forbidden]`, + ); + expect(tried).toBe(3); // initial + 2 retries + client.dispose(); - await new Promise((resolve) => { - // distinct connections mode - - tried = 0; - const client = createClient({ - singleConnection: false, - url, - fetchFn: fetch, - retryAttempts: 2, - retry: () => Promise.resolve(), - }); - - client.subscribe( - { - query: '{ getValue }', - }, - { - next: noop, - error: (err) => { - expect(err).toMatchSnapshot(); - expect(tried).toBe(3); // initial + 2 retries - resolve(); - }, - complete: noop, - }, - ); + // distinct connections mode + tried = 0; + client = createClient({ + singleConnection: false, + url: 'http://localhost', + fetchFn: fetch, + retryAttempts: 2, + retry: () => Promise.resolve(), }); + sub = tsubscribe(client, { query: '{ getValue }' }); + await expect(sub.waitForError()).resolves.toMatchInlineSnapshot( + `[NetworkError: Server responded with 403: Forbidden]`, + ); + expect(tried).toBe(3); // initial + 2 retries + client.dispose(); }); - it('should retry network errors even if they occur during event emission', async (done) => { - const server = await startTServer(); + it('should retry network errors even if they occur during event emission', async () => { + const { fetch, dispose } = createTFetch(); + const retryFn = jest.fn(async () => { + // noop + }); const client = createClient({ - url: server.url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 1, - retry: async () => { - client.dispose(); - done(); - }, + retry: retryFn, }); const sub = tsubscribe(client, { @@ -588,22 +519,27 @@ describe('retries', () => { await sub.waitForNext(); - await server.dispose(); + await dispose(); + + expect(retryFn).toHaveBeenCalled(); + + client.dispose(); }); - it('should not retry fatal errors occurring during event emission', async (done) => { - const server = await startTServer(); + it('should not retry fatal errors occurring during event emission', async () => { + const { fetch } = createTFetch(); let msgsCount = 0; const fatalErr = new Error('Boom, I am fatal'); + const retryFn = jest.fn(async () => { + // noop + }); const client = createClient({ - url: server.url, + url: 'http://localhost', fetchFn: fetch, retryAttempts: 1, - retry: async () => { - done(new Error("Shouldnt've retried")); - }, + retry: retryFn, onMessage: () => { // onMessage is in the middle of stream processing, throwing from it is considered fatal msgsCount++; @@ -619,9 +555,8 @@ describe('retries', () => { await sub.waitForNext(); - await sub.waitForError((err) => { - expect(err).toBe(fatalErr); - done(); - }); + await expect(sub.waitForError()).resolves.toBe(fatalErr); + + expect(retryFn).not.toHaveBeenCalled(); }); }); diff --git a/src/__tests__/fixtures/simple.ts b/src/__tests__/fixtures/simple.ts index a86cd2c8..1949f0c6 100644 --- a/src/__tests__/fixtures/simple.ts +++ b/src/__tests__/fixtures/simple.ts @@ -9,7 +9,7 @@ import { // use for dispatching a `pong` to the `ping` subscription const pendingPongs: Record = {}; const pongListeners: Record void) | undefined> = {}; -export function pong(key = 'global'): void { +export function pong(key: string): void { if (pongListeners[key]) { pongListeners[key]?.(false); } else { @@ -52,11 +52,11 @@ export const schemaConfig: GraphQLSchemaConfig = { type: new GraphQLNonNull(GraphQLString), args: { key: { - type: GraphQLString, + type: new GraphQLNonNull(GraphQLString), }, }, subscribe: function (_src, args) { - const key = args.key ? args.key : 'global'; + const key = args.key; return { [Symbol.asyncIterator]() { return this; diff --git a/src/__tests__/handler.ts b/src/__tests__/handler.ts index fbb4cd68..caddc786 100644 --- a/src/__tests__/handler.ts +++ b/src/__tests__/handler.ts @@ -1,497 +1,590 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { jest } from '@jest/globals'; -import EventSource from 'eventsource'; -import { startTServer, startDisposableServer } from './utils/tserver'; -import { eventStream } from './utils/eventStream'; -import { createClient, createHandler } from '../index'; -import { TOKEN_HEADER_KEY, TOKEN_QUERY_KEY } from '../common'; -import http from 'http'; -import http2 from 'http2'; -import { schema } from './fixtures/simple'; -import fetch from 'node-fetch'; -import express from 'express'; -import Fastify from 'fastify'; - -// just does nothing -function noop(): void { - /**/ -} +import { + createTHandler, + assertString, + assertAsyncGenerator, +} from './utils/thandler'; +import { TOKEN_HEADER_KEY } from '../common'; it('should only accept valid accept headers', async () => { - const { request } = await startTServer(); + const { handler } = createTHandler(); - const { data: token } = await request('PUT'); + let [body, init] = await handler('PUT'); + assertString(body); + const token = body; - let res = await request('GET', { - accept: 'gibberish', - [TOKEN_HEADER_KEY]: token, + [body, init] = await handler('GET', { + headers: { + accept: 'gibberish', + [TOKEN_HEADER_KEY]: token, + }, }); - expect(res.statusCode).toBe(406); + expect(init.status).toBe(406); - res = await request('GET', { - accept: 'application/graphql+json', - [TOKEN_HEADER_KEY]: token, + [body, init] = await handler('GET', { + headers: { + accept: 'application/json', + [TOKEN_HEADER_KEY]: token, + }, }); - expect(res.statusCode).toBe(400); - expect(res.statusMessage).toBe('Missing query'); - - res = await request('GET', { - accept: 'application/json', - [TOKEN_HEADER_KEY]: token, + expect(init.status).toBe(400); + expect(init.headers?.['content-type']).toBe( + 'application/json; charset=utf-8', + ); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Missing query"}]}"`, + ); + + [body, init] = await handler('GET', { + headers: { + accept: 'text/event-stream', + }, }); - expect(res.statusCode).toBe(400); - expect(res.statusMessage).toBe('Missing query'); + expect(init.status).toBe(400); + expect(init.headers?.['content-type']).toBe( + 'application/json; charset=utf-8', + ); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Missing query"}]}"`, + ); +}); - res = await request('GET', { accept: 'text/event-stream' }); - expect(res.statusCode).toBe(400); - expect(res.statusMessage).toBe('Missing query'); +it.each(['authenticate', 'onConnect', 'onSubscribe', 'context', 'onOperation'])( + 'should bubble %s errors to the handler', + async (hook) => { + const err = new Error('hang hang'); + const { handler } = createTHandler({ + [hook]() { + throw err; + }, + }); - res = await request('POST', { accept: 'text/event-stream' }, { query: '' }); - expect(res.statusCode).toBe(400); - expect(res.statusMessage).toBe('Missing query'); -}); + await expect( + handler('POST', { + headers: { + accept: 'text/event-stream', + }, + body: { query: '{ getValue }' }, + }), + ).rejects.toBe(err); + }, +); + +it.each(['onNext', 'onComplete'])( + 'should bubble %s errors to the response body iterator', + async (hook) => { + const err = new Error('hang hang'); + const { handler } = createTHandler({ + [hook]() { + throw err; + }, + }); -it.todo('should throw all unexpected errors from the handler'); + const [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', + }, + body: { query: '{ getValue }' }, + }); + expect(init.status).toBe(200); + assertAsyncGenerator(stream); -it.todo('should use the string body argument from the handler'); + await expect( + (async () => { + for await (const _ of stream) { + // wait + } + })(), + ).rejects.toBe(err); + }, +); + +it('should bubble onNext errors to the response body iterator even if late', async () => { + const err = new Error('hang hang'); + let i = 0; + const { handler } = createTHandler({ + onNext() { + i++; + if (i > 3) { + throw err; + } + }, + }); -it.todo('should use the object body argument from the handler'); + const [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', + }, + body: { query: 'subscription { greetings }' }, + }); + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + + await expect( + (async () => { + for await (const _ of stream) { + // wait + } + })(), + ).rejects.toBe(err); +}); describe('single connection mode', () => { it('should respond with 404s when token was not previously registered', async () => { - const { request } = await startTServer(); - - // maybe POST gql request - let res = await request('POST'); - expect(res.statusCode).toBe(404); - expect(res.statusMessage).toBe('Stream not found'); - - // maybe GET gql request - res = await request('GET'); - expect(res.statusCode).toBe(404); - expect(res.statusMessage).toBe('Stream not found'); - - // completing/ending an operation - res = await request('DELETE'); - expect(res.statusCode).toBe(404); - expect(res.statusMessage).toBe('Stream not found'); + const { handler } = createTHandler(); + + let [body, init] = await handler('POST', { + headers: { + [TOKEN_HEADER_KEY]: '0', + }, + }); + expect(init.status).toBe(404); + expect(init.headers?.['content-type']).toBe( + 'application/json; charset=utf-8', + ); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Stream not found"}]}"`, + ); + + const search = new URLSearchParams(); + search.set('token', '0'); + + [body, init] = await handler('GET', { search }); + expect(init.status).toBe(404); + expect(init.headers?.['content-type']).toBe( + 'application/json; charset=utf-8', + ); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Stream not found"}]}"`, + ); + + [body, init] = await handler('DELETE', { search }); + expect(init.status).toBe(404); + expect(init.headers?.['content-type']).toBe( + 'application/json; charset=utf-8', + ); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Stream not found"}]}"`, + ); }); it('should get a token with PUT request', async () => { - const { request } = await startTServer({ authenticate: () => 'token' }); - - const { statusCode, headers, data } = await request('PUT'); + const { handler } = createTHandler({ + authenticate() { + return 'token'; + }, + }); - expect(statusCode).toBe(201); - expect(headers['content-type']).toBe('text/plain; charset=utf-8'); - expect(data).toBe('token'); + const [body, init] = await handler('PUT'); + expect(init.status).toBe(201); + expect(init.headers?.['content-type']).toBe('text/plain; charset=utf-8'); + expect(body).toBe('token'); }); - it('should allow event streams on reservations only', async () => { - const { url, request } = await startTServer(); + it('should treat event streams without reservations as regular requests', async () => { + const { handler } = createTHandler(); - // no reservation no connect - let es = new EventSource(url); - await new Promise((resolve) => { - es.onerror = () => { - resolve(); - es.close(); // no retry - }; + const [body, init] = await handler('GET', { + headers: { + [TOKEN_HEADER_KEY]: '0', + accept: 'text/event-stream', + }, }); + expect(init.status).toBe(400); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Missing query"}]}"`, + ); + }); - // token can be sent through the header - let res = await request('PUT'); - es = new EventSource(url, { - headers: { [TOKEN_HEADER_KEY]: res.data }, - }); - await new Promise((resolve, reject) => { - es.onopen = () => resolve(); - es.onerror = (e) => { - reject(e); - es.close(); // no retry - }; + it('should allow event streams on reservations', async () => { + const { handler } = createTHandler(); + + // token can be sent through headers + let [token] = await handler('PUT'); + assertString(token); + let [stream, init] = await handler('GET', { + headers: { + [TOKEN_HEADER_KEY]: token, + accept: 'text/event-stream', + }, }); - es.close(); - - // token can be sent through the url - res = await request('PUT'); - es = new EventSource(url + '?' + TOKEN_QUERY_KEY + '=' + res.data); - await new Promise((resolve, reject) => { - es.onopen = () => resolve(); - es.onerror = (e) => { - reject(e); - es.close(); // no retry - }; + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + stream.return(); + + // token can be sent through url search param + [token] = await handler('PUT'); + assertString(token); + const search = new URLSearchParams(); + search.set('token', token); + [stream, init] = await handler('GET', { + search, + headers: { + accept: 'text/event-stream', + }, }); - es.close(); + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + stream.return(); }); it('should not allow operations without providing an operation id', async () => { - const { request } = await startTServer(); + const { handler } = createTHandler(); - const { data: token } = await request('PUT'); + const [token] = await handler('PUT'); + assertString(token); - const { statusCode, statusMessage } = await request( - 'POST', - { [TOKEN_HEADER_KEY]: token }, - { query: '{ getValue }' }, - ); + const [body, init] = await handler('POST', { + headers: { [TOKEN_HEADER_KEY]: token }, + body: { query: '{ getValue }' }, + }); - expect(statusCode).toBe(400); - expect(statusMessage).toBe('Operation ID is missing'); + expect(init.status).toBe(400); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Operation ID is missing"}]}"`, + ); }); - it('should stream query operations to connected event stream', async (done) => { - const { url, request } = await startTServer(); + it('should stream query operations to connected event stream', async () => { + const { handler } = createTHandler(); - const { data: token } = await request('PUT'); + const [token] = await handler('PUT'); + assertString(token); - const es = new EventSource(url + '?' + TOKEN_QUERY_KEY + '=' + token); - es.addEventListener('next', (event) => { - expect((event as any).data).toMatchSnapshot(); + const [stream] = await handler('POST', { + headers: { + [TOKEN_HEADER_KEY]: token, + accept: 'text/event-stream', + }, }); - es.addEventListener('complete', () => { - es.close(); - done(); + assertAsyncGenerator(stream); + + const [body, init] = await handler('POST', { + headers: { [TOKEN_HEADER_KEY]: token }, + body: { query: '{ getValue }', extensions: { operationId: '1' } }, }); + expect(init.status).toBe(202); + expect(body).toBeNull(); - const { statusCode } = await request( - 'POST', - { [TOKEN_HEADER_KEY]: token }, - { query: '{ getValue }', extensions: { operationId: '1' } }, - ); - expect(statusCode).toBe(202); - }); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": ": - it.todo('should stream query operations even if event stream connects later'); + ", + } + `); // ping - it('should stream subscription operations to connected event stream', async (done) => { - const { url, request } = await startTServer(); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": "event: next + data: {"id":"1","payload":{"data":{"getValue":"value"}}} - const { data: token } = await request('PUT'); + ", + } + `); - const es = new EventSource(url + '?' + TOKEN_QUERY_KEY + '=' + token); - es.addEventListener('next', (event) => { - // called 5 times - expect((event as any).data).toMatchSnapshot(); - }); - es.addEventListener('complete', () => { - es.close(); - done(); - }); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": "event: complete + data: {"id":"1"} - const { statusCode } = await request( - 'POST', - { [TOKEN_HEADER_KEY]: token }, - { query: 'subscription { greetings }', extensions: { operationId: '1' } }, - ); - expect(statusCode).toBe(202); + ", + } + `); + + stream.return(); }); - it('should report operation validation issues to request', async () => { - const { url, request } = await startTServer(); + it('should stream subscription operations to connected event stream', async () => { + const { handler } = createTHandler(); - const { data: token } = await request('PUT'); + const [token] = await handler('PUT'); + assertString(token); - const es = new EventSource(url + '?' + TOKEN_QUERY_KEY + '=' + token); - es.addEventListener('next', () => { - fail('Shouldnt have omitted'); + const search = new URLSearchParams(); + search.set('token', token); + const [stream] = await handler('GET', { + search, + headers: { + accept: 'text/event-stream', + }, }); - es.addEventListener('complete', () => { - fail('Shouldnt have omitted'); + assertAsyncGenerator(stream); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": ": + + ", + } + `); // ping + + const [_0, init] = await handler('POST', { + headers: { + [TOKEN_HEADER_KEY]: token, + }, + body: { + query: 'subscription { greetings }', + extensions: { operationId: '1' }, + }, }); + expect(init.status).toBe(202); - const { statusCode, data } = await request( - 'POST', - { [TOKEN_HEADER_KEY]: token }, - { query: '{ notExists }', extensions: { operationId: '1' } }, - ); - expect(statusCode).toBe(400); - expect(data).toMatchSnapshot(); + for await (const msg of stream) { + expect(msg).toMatchSnapshot(); - es.close(); - }); + if (msg.startsWith('event: complete')) { + break; + } + } - it('should bubble errors thrown in onNext to the handler', async (done) => { - const onNextErr = new Error('Woops!'); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": true, + "value": undefined, + } + `); + }); - const handler = createHandler({ - schema, - onNext: () => { - throw onNextErr; - }, - }); + it.todo('should stream operations even if event stream connects late'); - const [, url, dispose] = await startDisposableServer( - http.createServer(async (req, res) => { - try { - await handler(req, res); - } catch (err) { - expect(err).toBe(onNextErr); + it('should report validation issues to operation request', async () => { + const { handler } = createTHandler(); - await dispose(); - done(); - } - }), - ); + const [token] = await handler('PUT'); + assertString(token); - const client = createClient({ - singleConnection: true, - url, - fetchFn: fetch, - retryAttempts: 0, + const search = new URLSearchParams(); + search.set('token', token); + const [stream] = await handler('GET', { + search, + headers: { + accept: 'text/event-stream', + }, }); - - client.subscribe( + assertAsyncGenerator(stream); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` { - query: '{ getValue }', + "done": false, + "value": ": + + ", + } + `); // ping + + const [body, init] = await handler('POST', { + headers: { + [TOKEN_HEADER_KEY]: token, }, - { - next: noop, - error: noop, - complete: noop, + body: { + query: 'subscription { notExists }', + extensions: { operationId: '1' }, }, + }); + expect(init.status).toBe(400); + expect(body).toMatchInlineSnapshot( + `"{"errors":[{"message":"Cannot query field \\"notExists\\" on type \\"Subscription\\".","locations":[{"line":1,"column":16}]}]}"`, ); + + // stream remains open + await expect( + Promise.race([ + stream.next(), + await new Promise((resolve) => setTimeout(resolve, 20)), + ]), + ).resolves.toBeUndefined(); + stream.return(); }); }); describe('distinct connections mode', () => { it('should stream query operations to connected event stream and then disconnect', async () => { - const { url, waitForDisconnect } = await startTServer(); - - const control = new AbortController(); - - // POST + const { handler } = createTHandler(); - let msgs = await eventStream({ - signal: control.signal, - url, - body: { query: '{ getValue }' }, + // GET + const search = new URLSearchParams(); + search.set('query', '{ getValue }'); + let [stream, init] = await handler('GET', { + search, + headers: { + accept: 'text/event-stream', + }, }); - - for await (const msg of msgs) { + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + for await (const msg of stream) { expect(msg).toMatchSnapshot(); } - await waitForDisconnect(); - - // GET - - const urlQuery = new URL(url); - urlQuery.searchParams.set('query', '{ getValue }'); - - msgs = await eventStream({ - signal: control.signal, - url: urlQuery.toString(), + // POST + [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', + }, + body: { query: '{ getValue }' }, }); - - for await (const msg of msgs) { + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + for await (const msg of stream) { expect(msg).toMatchSnapshot(); } - - await waitForDisconnect(); }); it('should stream subscription operations to connected event stream and then disconnect', async () => { - const { url, waitForDisconnect } = await startTServer(); - - const control = new AbortController(); - - // POST + const { handler } = createTHandler(); - let msgs = await eventStream({ - signal: control.signal, - url, - body: { query: 'subscription { greetings }' }, + // GET + const search = new URLSearchParams(); + search.set('query', 'subscription { greetings }'); + let [stream, init] = await handler('GET', { + search, + headers: { + accept: 'text/event-stream', + }, }); - - for await (const msg of msgs) { + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + for await (const msg of stream) { expect(msg).toMatchSnapshot(); } - await waitForDisconnect(); - - // GET - - const urlQuery = new URL(url); - urlQuery.searchParams.set('query', 'subscription { greetings }'); - - msgs = await eventStream({ - signal: control.signal, - url: urlQuery.toString(), + // POST + [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', + }, + body: { query: 'subscription { greetings }' }, }); - - for await (const msg of msgs) { + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + for await (const msg of stream) { expect(msg).toMatchSnapshot(); } - - await waitForDisconnect(); }); it('should report operation validation issues by streaming them', async () => { - const { url, waitForDisconnect } = await startTServer(); - - const control = new AbortController(); + const { handler } = createTHandler(); - const msgs = await eventStream({ - signal: control.signal, - url, + const [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', + }, body: { query: '{ notExists }' }, }); + expect(init.status).toBe(200); + assertAsyncGenerator(stream); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": ": - for await (const msg of msgs) { - expect(msg).toMatchSnapshot(); - } + ", + } + `); // ping + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": "event: next + data: {"errors":[{"message":"Cannot query field \\"notExists\\" on type \\"Query\\".","locations":[{"line":1,"column":3}]}]} + + ", + } + `); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": "event: complete - await waitForDisconnect(); + ", + } + `); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": true, + "value": undefined, + } + `); }); it('should complete subscription operations after client disconnects', async () => { - const { url, waitForOperation, waitForComplete } = await startTServer(); - - const control = new AbortController(); + const { handler } = createTHandler(); - await eventStream({ - signal: control.signal, - url, - body: { query: 'subscription { ping }' }, + const [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', + }, + body: { query: `subscription { ping(key: "${Math.random()}") }` }, }); + expect(init.status).toBe(200); + assertAsyncGenerator(stream); - await waitForOperation(); - - control.abort(); + // simulate client disconnect in next tick + setTimeout(() => stream.return(), 0); - await waitForComplete(); + for await (const _ of stream) { + // loop must break for test to pass + } }); - it('should bubble errors thrown in onNext to the handler', async (done) => { - const onNextErr = new Error('Woops!'); + it('should complete when stream ends before the subscription sent all events', async () => { + const { handler } = createTHandler(); - const handler = createHandler({ - schema, - onNext: () => { - throw onNextErr; + const [stream, init] = await handler('POST', { + headers: { + accept: 'text/event-stream', }, + body: { query: `subscription { greetings }` }, }); + expect(init.status).toBe(200); + assertAsyncGenerator(stream); - const [, url, dispose] = await startDisposableServer( - http.createServer(async (req, res) => { - try { - await handler(req, res); - } catch (err) { - expect(err).toBe(onNextErr); - - await dispose(); - done(); - } - }), - ); - - const client = createClient({ - url, - fetchFn: fetch, - retryAttempts: 0, - }); - - client.subscribe( - { - query: '{ getValue }', - }, + await expect(stream.next()).resolves.toMatchInlineSnapshot(` { - next: noop, - error: noop, - complete: noop, - }, - ); - }); -}); + "done": false, + "value": ": -describe('http2', () => { - // ts-only-test - it.skip('should work as advertised in the readme', async () => { - const handler = createHandler({ schema }); - http2.createSecureServer( - { - key: 'localhost-privkey.pem', - cert: 'localhost-cert.pem', - }, - (req, res) => { - if (req.url.startsWith('/graphql/stream')) return handler(req, res); - return res.writeHead(404).end(); - }, - ); - }); -}); + ", + } + `); // ping -describe('express', () => { - it('should work as advertised in the readme', async () => { - const app = express(); - const handler = createHandler({ schema }); - app.use('/graphql/stream', handler); - const [, url, dispose] = await startDisposableServer( - http.createServer(app), - ); - - const client = createClient({ - url: url + '/graphql/stream', - fetchFn: fetch, - retryAttempts: 0, - }); - - const next = jest.fn(); - await new Promise((resolve, reject) => { - client.subscribe( - { - query: 'subscription { greetings }', - }, - { - next: next, - error: reject, - complete: resolve, - }, - ); - }); - - expect(next).toBeCalledTimes(5); - expect(next.mock.calls).toMatchSnapshot(); + for await (const msg of stream) { + expect(msg).toMatchInlineSnapshot(` + "event: next + data: {"data":{"greetings":"Hi"}} - await dispose(); - }); -}); + " + `); -describe('fastify', () => { - it('should work as advertised in the readme', async () => { - const handler = createHandler({ schema }); - const fastify = Fastify(); - fastify.all('/graphql/stream', (req, res) => - handler(req.raw, res.raw, req.body), - ); - const url = await fastify.listen({ port: 0 }); + // return after first message (there are more) + break; + } - const client = createClient({ - url: url + '/graphql/stream', - fetchFn: fetch, - retryAttempts: 0, - }); + // message was already queued up (pending), it's ok to have it + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": "event: next + data: {"data":{"greetings":"Bonjour"}} - const next = jest.fn(); - await new Promise((resolve, reject) => { - client.subscribe( - { - query: 'subscription { greetings }', - }, - { - next: next, - error: reject, - complete: resolve, - }, - ); - }); + ", + } + `); - expect(next).toBeCalledTimes(5); - expect(next.mock.calls).toMatchSnapshot(); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": false, + "value": "event: complete - await fastify.close(); + ", + } + `); + await expect(stream.next()).resolves.toMatchInlineSnapshot(` + { + "done": true, + "value": undefined, + } + `); }); }); diff --git a/src/__tests__/jest.d.ts b/src/__tests__/jest.d.ts deleted file mode 100644 index f27ad38e..00000000 --- a/src/__tests__/jest.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace jest { - // jest-jasmine2 runner (see jest.config.js) allows done callback with promises - type ProvidesCallback = ( - cb: DoneCallback, - ) => void | undefined | Promise; -} diff --git a/src/__tests__/utils/eventStream.ts b/src/__tests__/utils/eventStream.ts deleted file mode 100644 index a791eea6..00000000 --- a/src/__tests__/utils/eventStream.ts +++ /dev/null @@ -1,45 +0,0 @@ -import fetch from 'node-fetch'; -import { RequestParams, StreamEvent, StreamMessage } from '../../common'; -import { createParser } from '../../parser'; - -export async function eventStream(options: { - signal: AbortSignal; - url: string; - headers?: Record | undefined; - body?: RequestParams; -}): Promise>> { - const { signal, url, headers, body } = options; - - const res = await fetch(url, { - signal, - method: body ? 'POST' : 'GET', - headers: { - ...headers, - accept: 'text/event-stream', - }, - body: body ? JSON.stringify(body) : undefined, - }); - if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`); - - return (async function* messages() { - if (!res.body) throw new Error('Missing response body'); - const parse = createParser(); - try { - for await (const chunk of res.body) { - if (typeof chunk === 'string') - throw new Error(`Unexpected string chunk "${chunk}"`); - - // read chunk and if messages are ready, yield them - const msgs = parse(chunk); - if (!msgs) continue; - - for (const msg of msgs) { - yield msg; - } - } - } catch (err) { - if (signal.aborted) return; - throw err; - } - })(); -} diff --git a/src/__tests__/utils/testkit.ts b/src/__tests__/utils/testkit.ts new file mode 100644 index 00000000..36e0a444 --- /dev/null +++ b/src/__tests__/utils/testkit.ts @@ -0,0 +1,73 @@ +import { EventEmitter } from 'events'; +import { HandlerOptions, OperationContext } from '../../handler'; + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export type OnOpeartionArgs< + RequestRaw = unknown, + RequestContext = unknown, + Context extends OperationContext = undefined, +> = Parameters< + NonNullable< + HandlerOptions['onOperation'] + > +>; + +export interface TestKit< + RequestRaw = unknown, + RequestContext = unknown, + Context extends OperationContext = undefined, +> { + waitForOperation(): Promise< + OnOpeartionArgs + >; +} + +export function injectTestKit< + RequestRaw = unknown, + RequestContext = unknown, + Context extends OperationContext = undefined, +>( + opts: Partial> = {}, +): TestKit { + const onOperation = + queue>(); + const origOnOperation = opts.onOperation; + opts.onOperation = async (...args) => { + onOperation.add(args); + return origOnOperation?.(...args); + }; + + return { + waitForOperation() { + return onOperation.next(); + }, + }; +} + +export function queue(): { + next(): Promise; + add(val: T): void; +} { + const sy = Symbol(); + const emitter = new EventEmitter(); + const queue: T[] = []; + return { + async next() { + while (queue.length) { + return queue.shift()!; + } + return new Promise((resolve) => { + emitter.once(sy, () => { + resolve(queue.shift()!); + }); + }); + }, + add(val) { + queue.push(val); + emitter.emit(sy); + }, + }; +} diff --git a/src/__tests__/utils/tfetch.ts b/src/__tests__/utils/tfetch.ts new file mode 100644 index 00000000..acd3a950 --- /dev/null +++ b/src/__tests__/utils/tfetch.ts @@ -0,0 +1,49 @@ +import { schema } from '../fixtures/simple'; +import { HandlerOptions } from '../../handler'; +import { createHandler, RequestContext } from '../../use/fetch'; +import { injectTestKit, queue, TestKit } from './testkit'; + +export interface TFetch extends TestKit { + fetch: typeof fetch; + waitForRequest(): Promise; + dispose(): Promise; +} + +export function createTFetch( + opts: Partial> = {}, +): TFetch { + const testkit = injectTestKit(opts); + const onRequest = queue(); + const handler = createHandler({ + schema, + ...opts, + }); + const ctrls: AbortController[] = []; + return { + ...testkit, + fetch: (input, init) => { + const ctrl = new AbortController(); + ctrls.push(ctrl); + init?.signal?.addEventListener('abort', () => ctrl.abort()); + const req = new Request(input, { + ...init, + signal: ctrl.signal, + }); + onRequest.add(req); + return handler(req); + }, + waitForRequest() { + return onRequest.next(); + }, + async dispose() { + return new Promise((resolve) => { + // dispose in next tick to allow pending fetches to complete + setTimeout(() => { + ctrls.forEach((ctrl) => ctrl.abort()); + // finally resolve in next tick to flush the aborts + setTimeout(resolve, 0); + }, 0); + }); + }, + }; +} diff --git a/src/__tests__/utils/thandler.ts b/src/__tests__/utils/thandler.ts new file mode 100644 index 00000000..68b43b64 --- /dev/null +++ b/src/__tests__/utils/thandler.ts @@ -0,0 +1,86 @@ +import { schema } from '../fixtures/simple'; +import { + createHandler, + HandlerOptions, + Response, + Request, +} from '../../handler'; +import { isAsyncGenerator, RequestParams } from '../../common'; +import { TestKit, injectTestKit, queue } from './testkit'; + +export interface THandler extends TestKit { + handler( + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + req?: { + search?: URLSearchParams; + headers?: Record; + body?: RequestParams; + }, + ): Promise; + waitForRequest(): Promise; +} + +export function createTHandler(opts: Partial = {}): THandler { + const testkit = injectTestKit(opts); + const onRequest = queue(); + const handler = createHandler({ + schema, + ...opts, + }); + return { + ...testkit, + handler: (method, treq) => { + let url = 'http://localhost'; + + const search = treq?.search?.toString(); + if (search) url += `?${search}`; + + const headers: Record = { + 'content-type': treq?.body + ? 'application/json; charset=utf-8' + : undefined, + ...treq?.headers, + }; + + const body = (treq?.body as unknown as Record) || null; + + const req = { + method, + url, + headers: { + get(key: string) { + return headers[key] || null; + }, + }, + body, + raw: null, + context: null, + }; + onRequest.add(req); + return handler(req); + }, + waitForRequest() { + return onRequest.next(); + }, + }; +} + +export function assertString(val: unknown): asserts val is string { + if (typeof val !== 'string') { + throw new Error( + `Expected val to be a "string", got "${JSON.stringify(val)}"`, + ); + } +} + +export function assertAsyncGenerator( + val: unknown, +): asserts val is AsyncGenerator { + if (!isAsyncGenerator(val)) { + throw new Error( + `Expected val to be an "AsyncGenerator", got "${JSON.stringify( + val, + )}"`, + ); + } +} diff --git a/src/__tests__/utils/tserver.ts b/src/__tests__/utils/tserver.ts deleted file mode 100644 index 3f7f8219..00000000 --- a/src/__tests__/utils/tserver.ts +++ /dev/null @@ -1,259 +0,0 @@ -import http from 'http'; -import net from 'net'; -import { EventEmitter } from 'events'; -import { schema, pong } from '../fixtures/simple'; -import { createHandler, HandlerOptions } from '../../handler'; - -type Dispose = () => Promise; - -// distinct server for each test; if you forget to dispose, the fixture wont -const leftovers: Dispose[] = []; -afterAll(async () => { - while (leftovers.length > 0) { - // if not disposed by test, cleanup - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const dispose = leftovers.pop()!; - await dispose(); - } -}); - -export interface TServer { - url: string; - server: http.Server; - request( - method: 'GET' | 'POST' | 'PUT' | 'DELETE', - headers?: http.IncomingHttpHeaders, - params?: Record, - ): Promise<{ - statusCode: number; - statusMessage: string; - headers: http.IncomingHttpHeaders; - data: string; - }>; - pong: typeof pong; - waitForConnecting( - test?: (req: http.IncomingMessage, res: http.ServerResponse) => void, - expire?: number, - ): Promise; - waitForConnected( - test?: (req: http.IncomingMessage) => void, - expire?: number, - ): Promise; - waitForOperation(test?: () => void, expire?: number): Promise; - waitForComplete(test?: () => void, expire?: number): Promise; - waitForDisconnect(test?: () => void, expire?: number): Promise; - dispose: Dispose; -} - -export async function startTServer( - options: Partial< - HandlerOptions - > = {}, -): Promise { - const emitter = new EventEmitter(); - - const pendingConnectings: [ - req: http.IncomingMessage, - res: http.ServerResponse, - ][] = []; - const pendingConnecteds: http.IncomingMessage[] = []; - let pendingOperations = 0, - pendingCompletes = 0, - pendingDisconnects = 0; - const handler = createHandler({ - schema, - ...options, - onConnecting: async (...args) => { - pendingConnectings.push([args[0], args[1]]); - await options?.onConnecting?.(...args); - emitter.emit('connecting'); - }, - onConnected: async (...args) => { - pendingConnecteds.push(args[0]); - await options?.onConnected?.(...args); - emitter.emit('connected'); - }, - onOperation: async (...args) => { - pendingOperations++; - const maybeResult = await options?.onOperation?.(...args); - emitter.emit('operation'); - return maybeResult; - }, - onComplete: async (...args) => { - pendingCompletes++; - await options?.onComplete?.(...args); - emitter.emit('complete'); - }, - onDisconnect: async (...args) => { - pendingDisconnects++; - await options?.onDisconnect?.(...args); - emitter.emit('disconn'); - }, - }); - - const [server, url, dispose] = await startDisposableServer( - http.createServer(async (req, res) => { - try { - await handler(req, res); - } catch (err) { - fail(err); - } - }), - ); - - return { - url, - server, - request(method, headers = {}, params = {}) { - const u = new URL(url); - - if (method !== 'POST') - for (const [key, val] of Object.entries(params)) { - u.searchParams.set(key, String(val ?? '')); - } - - return new Promise((resolve, reject) => { - const req = http - .request(url, { method, headers }, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - if (!res.statusCode) - return reject(new Error('No status code in response')); - if (!res.statusMessage) - return reject(new Error('No status message in response')); - resolve({ - statusCode: res.statusCode, - statusMessage: res.statusMessage, - headers: res.headers, - data, - }); - }); - }) - .on('error', reject); - if (method === 'POST' && Object.keys(params).length) - req.write(JSON.stringify(params)); - req.end(); - }); - }, - pong, - waitForConnecting(test, expire) { - return new Promise((resolve) => { - function done() { - // the on connect listener below will be called before our listener, populating the queue - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const args = pendingConnectings.shift()!; - test?.(...args); - resolve(); - } - if (pendingConnectings.length > 0) return done(); - emitter.once('connecting', done); - if (expire) - setTimeout(() => { - emitter.off('connecting', done); // expired - resolve(); - }, expire); - }); - }, - waitForConnected(test, expire) { - return new Promise((resolve) => { - function done() { - // the on connect listener below will be called before our listener, populating the queue - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const arg = pendingConnecteds.shift()!; - test?.(arg); - resolve(); - } - if (pendingConnecteds.length > 0) return done(); - emitter.once('connected', done); - if (expire) - setTimeout(() => { - emitter.off('connected', done); // expired - resolve(); - }, expire); - }); - }, - waitForOperation(test, expire) { - return new Promise((resolve) => { - function done() { - pendingOperations--; - test?.(); - resolve(); - } - if (pendingOperations > 0) return done(); - emitter.once('operation', done); - if (expire) - setTimeout(() => { - emitter.off('operation', done); // expired - resolve(); - }, expire); - }); - }, - waitForComplete(test, expire) { - return new Promise((resolve) => { - function done() { - pendingCompletes--; - test?.(); - resolve(); - } - if (pendingCompletes > 0) return done(); - emitter.once('complete', done); - if (expire) - setTimeout(() => { - emitter.off('complete', done); // expired - resolve(); - }, expire); - }); - }, - waitForDisconnect(test, expire) { - return new Promise((resolve) => { - function done() { - pendingDisconnects--; - test?.(); - resolve(); - } - if (pendingDisconnects > 0) return done(); - emitter.once('disconn', done); - if (expire) - setTimeout(() => { - emitter.off('disconn', done); // expired - resolve(); - }, expire); - }); - }, - dispose, - }; -} - -/** - * Starts a disposable server thet is really stopped when the dispose func resolves. - * - * Additionally adds the server kill function to the post tests `leftovers` - * to be invoked after each test. - */ -export async function startDisposableServer( - server: http.Server, -): Promise<[server: http.Server, url: string, dispose: () => Promise]> { - const sockets = new Set(); - server.on('connection', (socket) => { - sockets.add(socket); - socket.once('close', () => sockets.delete(socket)); - }); - - const kill = async () => { - for (const socket of sockets) { - socket.destroy(); - } - await new Promise((resolve) => server.close(() => resolve())); - }; - leftovers.push(kill); - - await new Promise((resolve) => server.listen(0, resolve)); - - const { port } = server.address() as net.AddressInfo; - const url = `http://localhost:${port}`; - - return [server, url, kill]; -} diff --git a/src/__tests__/utils/tsubscribe.ts b/src/__tests__/utils/tsubscribe.ts index e1fdf9a9..2a7bd0ee 100644 --- a/src/__tests__/utils/tsubscribe.ts +++ b/src/__tests__/utils/tsubscribe.ts @@ -4,15 +4,10 @@ import { Client } from '../../client'; import { RequestParams } from '../../common'; interface TSubscribe { - waitForNext: ( - test?: (value: ExecutionResult) => void, - expire?: number, - ) => Promise; - waitForError: ( - test?: (error: unknown) => void, - expire?: number, - ) => Promise; - waitForComplete: (test?: () => void, expire?: number) => Promise; + waitForNext: () => Promise>; + waitForError: () => Promise; + throwOnError: () => Promise; + waitForComplete: () => Promise; dispose: () => void; } @@ -40,55 +35,49 @@ export function tsubscribe( emitter.removeAllListeners(); }, }); - + function waitForError() { + return new Promise((resolve) => { + function done() { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolve(error); + } + if (error) { + done(); + } else { + emitter.once('err', done); + } + }); + } return { - waitForNext: (test, expire) => { + waitForNext() { return new Promise((resolve) => { function done() { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const result = results.shift()!; - test?.(result); - resolve(); + resolve(results.shift()!); } - if (results.length > 0) return done(); - emitter.once('next', done); - if (expire) - setTimeout(() => { - emitter.off('next', done); // expired - resolve(); - }, expire); - }); - }, - waitForError: (test, expire) => { - return new Promise((resolve) => { - function done() { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - test?.(error); - resolve(); + if (results.length) { + done(); + } else { + emitter.once('next', done); } - if (error) return done(); - emitter.once('err', done); - if (expire) - setTimeout(() => { - emitter.off('err', done); // expired - resolve(); - }, expire); }); }, - waitForComplete: (test, expire) => { + waitForError, + throwOnError: () => + waitForError().then((err) => { + throw err; + }), + waitForComplete() { return new Promise((resolve) => { function done() { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - test?.(); resolve(); } - if (completed) return done(); - emitter.once('complete', done); - if (expire) - setTimeout(() => { - emitter.off('complete', done); // expired - resolve(); - }, expire); + if (completed) { + done(); + } else { + emitter.once('complete', done); + } }); }, dispose, diff --git a/src/common.ts b/src/common.ts index 89384a27..192288dc 100644 --- a/src/common.ts +++ b/src/common.ts @@ -5,6 +5,7 @@ */ import type { DocumentNode, GraphQLError } from 'graphql'; +import { isObject } from './utils'; /** * Header key through which the event stream token is transmitted @@ -65,6 +66,16 @@ export function validateStreamEvent(e: unknown): StreamEvent { return e; } +/** @category Common */ +export function print( + msg: StreamMessage, +): string { + let str = `event: ${msg.event}`; + if (msg.data) str += `\ndata: ${JSON.stringify(msg.data)}`; + str += '\n\n'; + return str; +} + /** @category Common */ export interface ExecutionResult< Data = Record, @@ -137,3 +148,27 @@ export interface Sink { /** The sink has completed. This function "closes" the sink. */ complete(): void; } + +/** + * Checkes whether the provided value is an async iterable. + * + * @category Common + */ +export function isAsyncIterable(val: unknown): val is AsyncIterable { + return typeof Object(val)[Symbol.asyncIterator] === 'function'; +} + +/** + * Checkes whether the provided value is an async generator. + * + * @category Common + */ +export function isAsyncGenerator(val: unknown): val is AsyncGenerator { + return ( + isObject(val) && + typeof Object(val)[Symbol.asyncIterator] === 'function' && + typeof val.return === 'function' && + typeof val.throw === 'function' && + typeof val.next === 'function' + ); +} diff --git a/src/handler.ts b/src/handler.ts index 871aa65e..49cb299c 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -4,8 +4,6 @@ * */ -import type { IncomingMessage, ServerResponse } from 'http'; -import type { Http2ServerRequest, Http2ServerResponse } from 'http2'; import { ExecutionArgs, getOperationAST, @@ -18,25 +16,102 @@ import { } from 'graphql'; import { isObject } from './utils'; import { - RequestParams, - StreamEvent, - StreamData, - StreamDataForID, ExecutionResult, ExecutionPatchResult, + RequestParams, TOKEN_HEADER_KEY, TOKEN_QUERY_KEY, + print, + isAsyncGenerator, + isAsyncIterable, } from './common'; /** + * The incoming request headers the implementing server should provide. + * * @category Server */ -export type NodeRequest = IncomingMessage | Http2ServerRequest; +export interface RequestHeaders { + get: (key: string) => string | null | undefined; +} /** + * Server agnostic request interface containing the raw request + * which is server dependant. + * * @category Server */ -export type NodeResponse = ServerResponse | Http2ServerResponse; +export interface Request { + 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 + | null + | (() => + | string + | Record + | null + | Promise | 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; + +/** + * 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; + +/** + * 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. @@ -48,8 +123,8 @@ export type NodeResponse = ServerResponse | Http2ServerResponse; * * @category Server */ -export type ExecutionContext = - | object // you can literally pass "any" JS object as the context value +export type OperationContext = + | Record | symbol | number | string @@ -57,6 +132,10 @@ export type ExecutionContext = | undefined | null; +/** @category Server */ +export type OperationArgs = + ExecutionArgs & { contextValue: Context }; + /** @category Server */ export type OperationResult = | Promise< @@ -70,9 +149,25 @@ export type OperationResult = /** @category Server */ export interface HandlerOptions< - Request extends NodeRequest = NodeRequest, - Response extends NodeResponse = NodeResponse, + 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) => OperationResult; + /** + * Is the `subscribe` function from GraphQL which is + * used to execute the subscription operation. + */ + subscribe?: (args: OperationArgs) => OperationResult; /** * The GraphQL schema on which the operations will * be executed and validated against. @@ -88,72 +183,63 @@ export interface HandlerOptions< schema?: | GraphQLSchema | (( - req: Request, - args: Omit, + req: Request, + args: Pick< + OperationArgs, + 'contextValue' | 'operationName' | 'document' | 'variableValues' + >, ) => Promise | GraphQLSchema); - /** - * 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. - */ - context?: - | ExecutionContext - | (( - req: Request, - args: ExecutionArgs, - ) => Promise | ExecutionContext); - /** - * 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: ExecutionArgs) => OperationResult; - /** - * Is the `subscribe` function from GraphQL which is - * used to execute the subscription operation. - */ - subscribe?: (args: ExecutionArgs) => OperationResult; /** * Authenticate the client. Returning a string indicates that the client * is authenticated and the request is ready to be processed. * - * A token of type string MUST be supplied; if there is no token, you may - * return an empty string (`''`); + * A distinct token of type string must be supplied to enable the "single connection mode". * - * 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. + * 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, - res: Response, - ) => Promise | string | undefined | void; + req: Request, + ) => + | Promise + | Response + | string + | undefined + | null; /** * Called when a new event stream is connecting BEFORE it is accepted. - * By accepted, its meant the server responded with a 200 (OK), alongside - * flushing the necessary event stream headers. - * - * 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. + * By accepted, its meant the server processed the request and responded + * with a 200 (OK), alongside flushing the necessary event stream headers. */ - onConnecting?: (req: Request, res: Response) => Promise | void; + onConnect?: ( + req: Request, + ) => + | Promise + | Response + | null + | undefined + | void; /** - * Called when a new event stream has been succesfully connected and - * accepted, and after all pending messages have been flushed. + * 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`. */ - onConnected?: (req: Request) => Promise | void; + context?: + | Context + | (( + req: Request, + params: RequestParams, + ) => Promise | Context); /** * The subscribe callback executed right after processing the request * before proceeding with the GraphQL operation execution. @@ -174,10 +260,14 @@ export interface HandlerOptions< * and supply the appropriate GraphQL operation execution arguments. */ onSubscribe?: ( - req: Request, - res: Response, + req: Request, params: RequestParams, - ) => Promise | ExecutionArgs | void; + ) => + | Promise | void> + | Response + | OperationResult + | OperationArgs + | void; /** * Executed after the operation call resolves. For streaming * operations, triggering this callback does not necessarely @@ -188,19 +278,15 @@ export interface HandlerOptions< * The `OperationResult` argument is the result of operation * execution. It can be an iterator or already a value. * - * Use this callback to listen for GraphQL operations and - * execution result manipulation. + * If you want the single result and the events from a streaming + * operation, use the `onNext` callback. * - * 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. - * - * First argument, the request, is always the GraphQL operation - * request. + * If `onSubscribe` returns an `OperationResult`, this hook + * will NOT be called. */ onOperation?: ( - req: Request, - res: Response, + ctx: Context, + req: Request, args: ExecutionArgs, result: OperationResult, ) => Promise | OperationResult | void; @@ -214,12 +300,11 @@ export interface HandlerOptions< * Use this callback if you want to format the execution result * before it reaches the client. * - * First argument, the request, is always the GraphQL operation - * request. + * @param req - Always the request that contains the GraphQL operation. */ onNext?: ( - req: Request, - args: ExecutionArgs, + ctx: Context, + req: Request, result: ExecutionResult | ExecutionPatchResult, ) => | Promise @@ -234,102 +319,30 @@ export interface HandlerOptions< * operations even after an abrupt closure, this callback * will always be called. * - * First argument, the request, is always the GraphQL operation - * request. + * @param req - Always the request that contains the GraphQL operation. */ - onComplete?: (req: Request, args: ExecutionArgs) => Promise | void; - /** - * Called when an event stream has disconnected right before the - * accepting the stream. - */ - onDisconnect?: (req: Request) => Promise | void; + onComplete?: ( + ctx: Context, + req: Request, + ) => Promise | void; } /** - * The ready-to-use handler. Simply plug it in your favourite HTTP framework - * and enjoy. - * - * Beware that the handler resolves only after the whole operation completes. - * - If query/mutation, waits for result - * - If subscription, waits for complete + * 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. * - * For production environments, its recommended not to transmit the exact internal - * error details to the client, but instead report to an error logging tool or simply - * the console. Roughly: - * - * ```ts - * import http from 'http'; - * import { createHandler } from 'graphql-sse'; - * - * const handler = createHandler({ ... }); - * - * http.createServer(async (req, res) => { - * try { - * await handler(req, res); - * } catch (err) { - * console.error(err); - * // or - * Sentry.captureException(err); - * - * if (!res.headersSent) { - * res.writeHead(500, 'Internal Server Error').end(); - * } - * } - * }); - * ``` - * - * Note that some libraries, like fastify, parse the body before reaching the handler. - * In such cases all request 'data' events are already consumed. Use this `body` argument - * too pass in the read body and avoid listening for the 'data' events internally. Do - * beware that the `body` argument will be consumed **only** if it's an object. - * * @category Server */ -export type Handler< - Request extends NodeRequest = NodeRequest, - Response extends NodeResponse = NodeResponse, -> = (req: Request, res: Response, body?: unknown) => Promise; - -interface Stream< - Request extends NodeRequest = NodeRequest, - Response extends NodeResponse = NodeResponse, -> { - /** - * Does the stream have an open connection to some client. - */ - readonly open: boolean; - /** - * If the operation behind an ID is an `AsyncIterator` - the operation - * is streaming; on the contrary, if the operation is `null` - it is simply - * a reservation, meaning - the operation resolves to a single result or is still - * pending/being prepared. - */ - ops: Record | AsyncIterable | null>; - /** - * Use this connection for streaming. - */ - use(req: Request, res: Response): Promise; - /** - * Stream from provided execution result to used connection. - */ - from( - operationReq: Request, // holding the operation request (not necessarily the event stream) - args: ExecutionArgs, - result: - | AsyncGenerator - | AsyncIterable - | ExecutionResult - | ExecutionPatchResult, - opId?: string, - ): Promise; -} +export type Handler = ( + req: Request, +) => Promise; /** - * Makes a Protocol complient HTTP GraphQL server handler. The handler can + * Makes a Protocol complient 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. @@ -337,18 +350,19 @@ interface Stream< * @category Server */ export function createHandler< - Request extends NodeRequest = NodeRequest, - Response extends NodeResponse = NodeResponse, ->(options: HandlerOptions): Handler { + RequestRaw = unknown, + RequestContext = unknown, + Context extends OperationContext = undefined, +>( + options: HandlerOptions, +): Handler { const { - schema, - context, validate = graphqlValidate, execute = graphqlExecute, subscribe = graphqlSubscribe, + schema, authenticate = function extractOrCreateStreamToken(req) { - const headerToken = - req.headers[TOKEN_HEADER_KEY] || req.headers['x-graphql-stream-token']; // @deprecated >v1.0.0 + const headerToken = req.headers.get(TOKEN_HEADER_KEY); if (headerToken) return Array.isArray(headerToken) ? headerToken.join('') : headerToken; @@ -364,169 +378,227 @@ export function createHandler< return v.toString(16); }); }, - onConnecting, - onConnected, + onConnect, + context, onSubscribe, onOperation, onNext, onComplete, - onDisconnect, } = options; - const streams: Record> = {}; - - function createStream(token: string | null): Stream { - let request: Request | null = null, - response: Response | null = null, - pinger: ReturnType, - disposed = false; - const pendingMsgs: string[] = []; + interface Stream { + /** + * Does the stream have an open connection to some client. + */ + readonly open: boolean; + /** + * If the operation behind an ID is an `AsyncIterator` - the operation + * is streaming; on the contrary, if the operation is `null` - it is simply + * a reservation, meaning - the operation resolves to a single result or is still + * pending/being prepared. + */ + ops: Record< + string, + AsyncGenerator | AsyncIterable | null + >; + /** + * Use this connection for streaming. + */ + subscribe(): AsyncGenerator; + /** + * Stream from provided execution result to used connection. + */ + from( + ctx: Context, + req: Request, + result: + | AsyncGenerator + | AsyncIterable + | ExecutionResult + | ExecutionPatchResult, + opId: string | null, + ): void; + } + const streams: Record = {}; + function createStream(token: string | null): Stream { const ops: Record< string, AsyncGenerator | AsyncIterable | null > = {}; - function write(msg: string) { - return new Promise((resolve, reject) => { - if (disposed || !response || !response.writable) return resolve(false); - // @ts-expect-error both ServerResponse and Http2ServerResponse have this write signature - response.write(msg, 'utf-8', (err) => { - if (err) return reject(err); - resolve(true); - }); - }); - } - - async function emit( - event: E, - data: StreamData | StreamDataForID, - ): Promise { - let msg = `event: ${event}`; - if (data) msg += `\ndata: ${JSON.stringify(data)}`; - msg += '\n\n'; - - const wrote = await write(msg); - if (!wrote) pendingMsgs.push(msg); - } + let pinger: ReturnType; + const msgs = (() => { + const pending: string[] = []; + const deferred = { + done: false, + error: null as unknown, + resolve: () => { + // noop + }, + }; - async function dispose() { - if (disposed) return; - disposed = true; + async function dispose() { + clearInterval(pinger); - // make room for another potential stream while this one is being disposed - if (typeof token === 'string') delete streams[token]; + // make room for another potential stream while this one is being disposed + if (typeof token === 'string') delete streams[token]; - // complete all operations and flush messages queue before ending the stream - for (const op of Object.values(ops)) { - if (isAsyncGenerator(op)) await op.return(undefined); - } - while (pendingMsgs.length) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const msg = pendingMsgs.shift()!; - await write(msg); + // complete all operations and flush messages queue before ending the stream + for (const op of Object.values(ops)) { + if (isAsyncGenerator(op)) { + await op.return(undefined); + } + } } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - response!.end(); // response must exist at this point - response = null; - clearInterval(pinger); + const iterator = (async function* iterator() { + for (;;) { + if (!pending.length) { + // only wait if there are no pending messages available + await new Promise((resolve) => (deferred.resolve = resolve)); + } + // first flush + while (pending.length) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + yield pending.shift()!; + } + // then error + if (deferred.error) { + throw deferred.error; + } + // or complete + if (deferred.done) { + return; + } + } + })(); + + iterator.throw = async (err) => { + if (!deferred.done) { + deferred.done = true; + deferred.error = err; + deferred.resolve(); + await dispose(); + } + return { done: true, value: undefined }; + }; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onDisconnect?.(request!); // request must exist at this point - request = null; - } + iterator.return = async () => { + if (!deferred.done) { + deferred.done = true; + deferred.resolve(); + await dispose(); + } + return { done: true, value: undefined }; + }; + + return { + next(msg: string) { + pending.push(msg); + deferred.resolve(); + }, + iterator, + }; + })(); + let subscribed = false; return { get open() { - return disposed || Boolean(response); + return subscribed; }, ops, - async use(req, res) { - request = req; - response = res; - - req.socket.setTimeout(0); - req.socket.setNoDelay(true); - req.socket.setKeepAlive(true); - - res.once('close', dispose); - res.statusCode = 200; - res.setHeader('Content-Type', 'text/event-stream; charset=utf-8'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('X-Accel-Buffering', 'no'); - if (req.httpVersionMajor < 2) res.setHeader('Connection', 'keep-alive'); - if ('flushHeaders' in res) res.flushHeaders(); + subscribe() { + subscribed = true; // write an empty message because some browsers (like Firefox and Safari) // dont accept the header flush - await write(':\n\n'); + msgs.next(':\n\n'); // ping client every 12 seconds to keep the connection alive - pinger = setInterval(() => write(':\n\n'), 12_000); - - while (pendingMsgs.length) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const msg = pendingMsgs.shift()!; - const wrote = await write(msg); - if (!wrote) throw new Error('Unable to flush messages'); - } + pinger = setInterval(() => msgs.next(':\n\n'), 12_000); - await onConnected?.(req); + return msgs.iterator; }, - async from(operationReq, args, result, opId) { - if (isAsyncIterable(result)) { - /** multiple emitted results */ - for await (let part of result) { - const maybeResult = await onNext?.(operationReq, args, part); - if (maybeResult) part = maybeResult; - - await emit( - 'next', - opId - ? { - id: opId, - payload: part, - } - : part, + from(ctx, req, result, opId) { + (async () => { + if (isAsyncIterable(result)) { + /** multiple emitted results */ + for await (let part of result) { + const maybeResult = await onNext?.(ctx, req, part); + if (maybeResult) part = maybeResult; + msgs.next( + print({ + event: 'next', + data: opId + ? { + id: opId, + payload: part, + } + : part, + }), + ); + } + } else { + /** single emitted result */ + const maybeResult = await onNext?.(ctx, req, result); + if (maybeResult) result = maybeResult; + msgs.next( + print({ + event: 'next', + data: opId + ? { + id: opId, + payload: result, + } + : result, + }), ); } - } else { - /** single emitted result */ - const maybeResult = await onNext?.(operationReq, args, result); - if (maybeResult) result = maybeResult; - - await emit( - 'next', - opId - ? { - id: opId, - payload: result, - } - : result, - ); - } - await emit('complete', opId ? { id: opId } : null); + msgs.next( + print({ + event: 'complete', + data: opId ? { id: opId } : null, + }), + ); - // end on complete when no operation id is present - // because distinct event streams are used for each operation - if (!opId) await dispose(); - else delete ops[opId]; + await onComplete?.(ctx, req); - await onComplete?.(operationReq, args); + if (!opId) { + // end on complete when no operation id is present + // because distinct event streams are used for each operation + await msgs.iterator.return(); + } else { + delete ops[opId]; + } + })().catch(msgs.iterator.throw); }, }; } async function prepare( - req: Request, - res: Response, + req: Request, params: RequestParams, - ): Promise<[args: ExecutionArgs, perform: () => OperationResult] | void> { - let args: ExecutionArgs, operation: OperationTypeNode; - - const maybeExecArgs = await onSubscribe?.(req, res, params); - if (maybeExecArgs) args = maybeExecArgs; + ): Promise OperationResult }> { + let args: OperationArgs; + + const onSubscribeResult = await onSubscribe?.(req, params); + if (isResponse(onSubscribeResult)) return onSubscribeResult; + else if ( + isExecutionResult(onSubscribeResult) || + isAsyncIterable(onSubscribeResult) + ) + return { + // even if the result is already available, use + // context because onNext and onComplete needs it + ctx: (typeof context === 'function' + ? await context(req, params) + : context) as Context, + perform() { + return onSubscribeResult; + }, + }; + else if (onSubscribeResult) args = onSubscribeResult; else { // you either provide a schema dynamically through // `onSubscribe` or you set one up during the server setup @@ -538,9 +610,25 @@ export function createHandler< if (typeof query === 'string') { try { query = parse(query); - } catch { - res.writeHead(400, 'GraphQL query syntax error').end(); - return; + } catch (err) { + return [ + JSON.stringify({ + errors: [ + err instanceof Error + ? { + message: err.message, + // TODO: stack might leak sensitive information + // stack: err.stack, + } + : err, + ], + }), + { + status: 400, + statusText: 'Bad Request', + headers: { 'content-type': 'application/json; charset=utf-8' }, + }, + ]; } } @@ -548,6 +636,9 @@ export function createHandler< operationName, document: query, variableValues: variables, + contextValue: (typeof context === 'function' + ? await context(req, params) + : context) as Context, }; args = { ...argsWithoutSchema, @@ -558,319 +649,462 @@ export function createHandler< }; } + let operation: OperationTypeNode; try { const ast = getOperationAST(args.document, args.operationName); if (!ast) throw null; operation = ast.operation; } catch { - res.writeHead(400, 'Unable to detect operation AST').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Unable to detect operation AST' }], + }), + { + status: 400, + statusText: 'Bad Request', + headers: { 'content-type': 'application/json; charset=utf-8' }, + }, + ]; } // mutations cannot happen over GETs as per the spec // Read more: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#get if (operation === 'mutation' && req.method === 'GET') { - res - .writeHead(405, 'Cannot perform mutations over GET', { - Allow: 'POST', - }) - .end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Cannot perform mutations over GET' }], + }), + { + status: 405, + statusText: 'Method Not Allowed', + headers: { + allow: 'POST', + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } - if (!('contextValue' in args)) - args.contextValue = - typeof context === 'function' ? await context(req, args) : context; - // we validate after injecting the context because the process of // reporting the validation errors might need the supplied context value const validationErrs = validate(args.schema, args.document); if (validationErrs.length) { - if (req.headers.accept === 'text/event-stream') { + if (req.headers.get('accept') === 'text/event-stream') { // accept the request and emit the validation error in event streams, // promoting graceful GraphQL error reporting // Read more: https://www.w3.org/TR/eventsource/#processing-model // Read more: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-validation - return [ - args, - function perform() { + return { + ctx: args.contextValue, + perform() { return { errors: validationErrs }; }, - ]; + }; } - res - .writeHead(400, { - 'Content-Type': - req.headers.accept === 'application/json' - ? 'application/json; charset=utf-8' - : 'application/graphql+json; charset=utf-8', - }) - // @ts-expect-error both ServerResponse and Http2ServerResponse have this write signature - .write(JSON.stringify({ errors: validationErrs })); - res.end(); - return; + return [ + JSON.stringify({ errors: validationErrs }), + { + status: 400, + statusText: 'Bad Request', + headers: { 'content-type': 'application/json; charset=utf-8' }, + }, + ]; } - return [ - args, - async function perform() { - let result = - operation === 'subscription' ? subscribe(args) : execute(args); - - const maybeResult = await onOperation?.(req, res, args, result); - if (maybeResult) result = maybeResult; - + return { + ctx: args.contextValue, + async perform() { + const result = await (operation === 'subscription' + ? subscribe(args) + : execute(args)); + const maybeResult = await onOperation?.( + args.contextValue, + req, + args, + result, + ); + if (maybeResult) return maybeResult; return result; }, - ]; + }; } - return async function handler(req: Request, res: Response, body: unknown) { - // authenticate first and acquire unique identification token - const token = await authenticate(req, res); - if (res.writableEnded) return; - if (typeof token !== 'string') throw new Error('Token was not supplied'); + return async function handler(req) { + const token = await authenticate(req); + if (isResponse(token)) return token; - const accept = req.headers.accept ?? '*/*'; + // TODO: make accept detection more resilient + const accept = req.headers.get('accept') || '*/*'; - const stream = streams[token]; + const stream = typeof token === 'string' ? streams[token] : null; if (accept === 'text/event-stream') { + const maybeResponse = await onConnect?.(req); + if (isResponse(maybeResponse)) return maybeResponse; + // if event stream is not registered, process it directly. // this means that distinct connections are used for graphql operations if (!stream) { - let params; - try { - params = await parseReq(req, body); - } catch (err) { - res.writeHead(400, err.message).end(); - return; - } + const paramsOrResponse = await parseReq(req); + if (isResponse(paramsOrResponse)) return paramsOrResponse; + const params = paramsOrResponse; const distinctStream = createStream(null); // reserve space for the operation distinctStream.ops[''] = null; - const prepared = await prepare(req, res, params); - if (res.writableEnded) return; - if (!prepared) - throw new Error( - "Operation preparation didn't respond, yet it was not prepared", - ); - const [args, perform] = prepared; - - const result = await perform(); - if (res.writableEnded) { - if (isAsyncGenerator(result)) result.return(undefined); - return; // `onOperation` responded - } + const prepared = await prepare(req, params); + if (isResponse(prepared)) return prepared; + const result = await prepared.perform(); if (isAsyncIterable(result)) distinctStream.ops[''] = result; - await onConnecting?.(req, res); - if (res.writableEnded) return; - await distinctStream.use(req, res); - await distinctStream.from(req, args, result); - return; + distinctStream.from(prepared.ctx, req, result, null); + return [ + distinctStream.subscribe(), + { + status: 200, + statusText: 'OK', + headers: { + connection: 'keep-alive', + 'cache-control': 'no-cache', + 'content-encoding': 'none', + 'content-type': 'text/event-stream; charset=utf-8', + }, + }, + ]; } // open stream cant exist, only one per token is allowed if (stream.open) { - res.writeHead(409, 'Stream already open').end(); - return; + return [ + JSON.stringify({ errors: [{ message: 'Stream already open' }] }), + { + status: 409, + statusText: 'Conflict', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } - await onConnecting?.(req, res); - if (res.writableEnded) return; - await stream.use(req, res); - return; + return [ + stream.subscribe(), + { + status: 200, + statusText: 'OK', + headers: { + connection: 'keep-alive', + 'cache-control': 'no-cache', + 'content-encoding': 'none', + 'content-type': 'text/event-stream; charset=utf-8', + }, + }, + ]; } - if (req.method === 'PUT') { - // method PUT prepares a stream for future incoming connections. + // if there us no token supplied, exclusively use the "distinct connection mode" + if (typeof token !== 'string') { + return [null, { status: 404, statusText: 'Not Found' }]; + } + // method PUT prepares a stream for future incoming connections + if (req.method === 'PUT') { if (!['*/*', 'text/plain'].includes(accept)) { - res.writeHead(406).end(); - return; + return [null, { status: 406, statusText: 'Not Acceptable' }]; } // streams mustnt exist if putting new one if (stream) { - res.writeHead(409, 'Stream already registered').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Stream already registered' }], + }), + { + status: 409, + statusText: 'Conflict', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } streams[token] = createStream(token); - res - .writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' }) - // @ts-expect-error both ServerResponse and Http2ServerResponse have this write signature - .write(token); - res.end(); - return; + + return [ + token, + { + status: 201, + statusText: 'Created', + headers: { + 'content-type': 'text/plain; charset=utf-8', + }, + }, + ]; } else if (req.method === 'DELETE') { // method DELETE completes an existing operation streaming in streams // streams must exist when completing operations if (!stream) { - res.writeHead(404, 'Stream not found').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Stream not found' }], + }), + { + status: 404, + statusText: 'Not Found', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } const opId = new URL(req.url ?? '', 'http://localhost/').searchParams.get( 'operationId', ); if (!opId) { - res.writeHead(400, 'Operation ID is missing').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Operation ID is missing' }], + }), + { + status: 400, + statusText: 'Bad Request', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } const op = stream.ops[opId]; if (isAsyncGenerator(op)) op.return(undefined); delete stream.ops[opId]; // deleting the operation means no further activity should take place - res.writeHead(200).end(); - return; + return [ + null, + { + status: 200, + statusText: 'OK', + }, + ]; } else if (req.method !== 'GET' && req.method !== 'POST') { // only POSTs and GETs are accepted at this point - res.writeHead(405, { Allow: 'GET, POST, PUT, DELETE' }).end(); - return; + return [ + null, + { + status: 405, + statusText: 'Method Not Allowed', + headers: { + allow: 'GET, POST, PUT, DELETE', + }, + }, + ]; } else if (!stream) { // for all other requests, streams must exist to attach the result onto - res.writeHead(404, 'Stream not found').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Stream not found' }], + }), + { + status: 404, + statusText: 'Not Found', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } - if ( - !['*/*', 'application/graphql+json', 'application/json'].includes(accept) - ) { - res.writeHead(406).end(); - return; + if (!['*/*', 'application/*', 'application/json'].includes(accept)) { + return [ + null, + { + status: 406, + statusText: 'Not Acceptable', + }, + ]; } - let params; - try { - params = await parseReq(req, body); - } catch (err) { - res.writeHead(400, err.message).end(); - return; - } + const paramsOrResponse = await parseReq(req); + if (isResponse(paramsOrResponse)) return paramsOrResponse; + const params = paramsOrResponse; const opId = String(params.extensions?.operationId ?? ''); if (!opId) { - res.writeHead(400, 'Operation ID is missing').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Operation ID is missing' }], + }), + { + status: 400, + statusText: 'Bad Request', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } if (opId in stream.ops) { - res.writeHead(409, 'Operation with ID already exists').end(); - return; + return [ + JSON.stringify({ + errors: [{ message: 'Operation with ID already exists' }], + }), + { + status: 409, + statusText: 'Conflict', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }, + ]; } + // reserve space for the operation through ID stream.ops[opId] = null; - const prepared = await prepare(req, res, params); - if (res.writableEnded) return; - if (!prepared) - throw new Error( - "Operation preparation didn't respond, yet it was not prepared", - ); - const [args, perform] = prepared; + const prepared = await prepare(req, params); + if (isResponse(prepared)) return prepared; // operation might have completed before prepared if (!(opId in stream.ops)) { - res.writeHead(204).end(); - return; + return [ + null, + { + status: 204, + statusText: 'No Content', + }, + ]; } - const result = await perform(); - if (res.writableEnded) { - if (isAsyncGenerator(result)) result.return(undefined); - delete stream.ops[opId]; - return; // `onOperation` responded - } + const result = await prepared.perform(); // operation might have completed before performed if (!(opId in stream.ops)) { if (isAsyncGenerator(result)) result.return(undefined); - res.writeHead(204).end(); - return; + if (!(opId in stream.ops)) { + return [ + null, + { + status: 204, + statusText: 'No Content', + }, + ]; + } } if (isAsyncIterable(result)) stream.ops[opId] = result; - res.writeHead(202).end(); - // streaming to an empty reservation is ok (will be flushed on connect) - await stream.from(req, args, result, opId); + stream.from(prepared.ctx, req, result, opId); + + return [null, { status: 202, statusText: 'Accepted' }]; }; } -async function parseReq( - req: Request, - body: unknown, -): Promise { +async function parseReq( + req: Request, +): Promise { const params: Partial = {}; - - if (req.method === 'GET') { - await new Promise((resolve, reject) => { - try { - const url = new URL(req.url ?? '', 'http://localhost/'); - params.operationName = - url.searchParams.get('operationName') ?? undefined; - params.query = url.searchParams.get('query') ?? undefined; - const variables = url.searchParams.get('variables'); - if (variables) params.variables = JSON.parse(variables); - const extensions = url.searchParams.get('extensions'); - if (extensions) params.extensions = JSON.parse(extensions); - resolve(); - } catch { - reject(new Error('Unparsable URL')); - } - }); - } else if (req.method === 'POST') { - await new Promise((resolve, reject) => { - const end = (body: Record | string) => { + try { + switch (true) { + case req.method === 'GET': { try { - const data = typeof body === 'string' ? JSON.parse(body) : body; - params.operationName = data.operationName; - params.query = data.query; - params.variables = data.variables; - params.extensions = data.extensions; - resolve(); + const [, search] = req.url.split('?'); + const searchParams = new URLSearchParams(search); + params.operationName = searchParams.get('operationName') ?? undefined; + params.query = searchParams.get('query') ?? undefined; + const variables = searchParams.get('variables'); + if (variables) params.variables = JSON.parse(variables); + const extensions = searchParams.get('extensions'); + if (extensions) params.extensions = JSON.parse(extensions); } catch { - reject(new Error('Unparsable body')); + throw new Error('Unparsable URL'); } - }; - if (typeof body === 'string' || isObject(body)) end(body); - else { - let body = ''; - req.on('data', (chunk) => (body += chunk)); - req.on('end', () => end(body)); + break; + } + case req.method === 'POST' && + req.headers.get('content-type')?.includes('application/json'): { + if (!req.body) { + throw new Error('Missing body'); + } + const body = + typeof req.body === 'function' ? await req.body() : req.body; + const data = typeof body === 'string' ? JSON.parse(body) : body; + if (!isObject(data)) { + throw new Error('JSON body must be an object'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Any is ok because values will be chacked below. + params.operationName = data.operationName as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Any is ok because values will be chacked below. + params.query = data.query as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Any is ok because values will be chacked below. + params.variables = data.variables as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Any is ok because values will be chacked below. + params.extensions = data.extensions as any; + break; } - }); - } else throw new Error(`Unsupported method ${req.method}`); // should never happen + default: + return [ + null, + { + status: 415, + statusText: 'Unsupported Media Type', + }, + ]; + } - if (!params.query) throw new Error('Missing query'); - if (params.variables && typeof params.variables !== 'object') - throw new Error('Invalid variables'); - if (params.extensions && typeof params.extensions !== 'object') - throw new Error('Invalid extensions'); + if (params.query == null) throw new Error('Missing query'); + if (typeof params.query !== 'string') throw new Error('Invalid query'); + if ( + params.variables != null && + (typeof params.variables !== 'object' || Array.isArray(params.variables)) + ) { + throw new Error('Invalid variables'); + } + if ( + params.extensions != null && + (typeof params.extensions !== 'object' || + Array.isArray(params.extensions)) + ) { + throw new Error('Invalid extensions'); + } - return params as RequestParams; + // request parameters are checked and now complete + return params as RequestParams; + } catch (err) { + return [ + JSON.stringify({ + errors: [ + err instanceof Error + ? { + message: err.message, + // TODO: stack might leak sensitive information + // stack: err.stack, + } + : err, + ], + }), + { + status: 400, + statusText: 'Bad Request', + headers: { 'content-type': 'application/json; charset=utf-8' }, + }, + ]; + } } -function isAsyncIterable(val: unknown): val is AsyncIterable { - return typeof Object(val)[Symbol.asyncIterator] === 'function'; +function isResponse(val: unknown): val is Response { + // TODO: comprehensive check + return Array.isArray(val); } -export function isAsyncGenerator(val: unknown): val is AsyncGenerator { - return ( - isObject(val) && - typeof Object(val)[Symbol.asyncIterator] === 'function' && - typeof val.return === 'function' - // for lazy ones, we only need the return anyway - // typeof val.throw === 'function' && - // typeof val.next === 'function' - ); +function isExecutionResult(val: unknown): val is ExecutionResult { + // TODO: comprehensive check + return isObject(val); } diff --git a/src/use/express.ts b/src/use/express.ts new file mode 100644 index 00000000..81f3b1d8 --- /dev/null +++ b/src/use/express.ts @@ -0,0 +1,99 @@ +import type { Request, Response } from 'express'; +import { + createHandler as createRawHandler, + HandlerOptions, + OperationContext, +} from '../handler'; + +/** + * @category Server/express + */ +export interface RequestContext { + res: Response; +} + +/** + * The ready-to-use handler for [express](https://expressjs.com). + * + * Errors thrown from the provided options or callbacks (or even due to + * library misuse or potential bugs) will reject the handler or bubble to the + * returned iterator. They are considered internal errors and you should take care + * of them accordingly. + * + * For production environments, its recommended not to transmit the exact internal + * error details to the client, but instead report to an error logging tool or simply + * the console. + * + * ```ts + * import express from 'express'; // yarn add express + * import { createHandler } from 'graphql-sse/lib/use/express'; + * import { schema } from './my-graphql'; + * + * const handler = createHandler({ schema }); + * + * const app = express(); + * + * app.use('/graphql/stream', async (req, res) => { + * try { + * await handler(req, res); + * } catch (err) { + * console.error(err); + * res.writeHead(500).end(); + * } + * }); + * + * server.listen(4000); + * console.log('Listening to port 4000'); + * ``` + * + * @category Server/express + */ +export function createHandler( + options: HandlerOptions, +): (req: Request, res: Response) => Promise { + const handler = createRawHandler(options); + return async function handleRequest(req, res) { + const [body, init] = await handler({ + method: req.method, + url: req.url, + headers: { + get(key) { + const header = req.headers[key]; + return Array.isArray(header) ? header.join('\n') : header; + }, + }, + body: () => + new Promise((resolve, reject) => { + if (req.body) { + // body was parsed by middleware + return req.body; + } + + let body = ''; + req.on('data', (chunk) => (body += chunk)); + req.once('error', reject); + req.once('end', () => { + req.off('error', reject); + resolve(body); + }); + }), + raw: req, + context: { res }, + }); + + res.writeHead(init.status, init.statusText, init.headers); + + if (!body || typeof body === 'string') { + return new Promise((resolve) => res.end(body, () => resolve())); + } + + res.once('close', body.return); + for await (const value of body) { + await new Promise((resolve, reject) => + res.write(value, (err) => (err ? reject(err) : resolve())), + ); + } + res.off('close', body.return); + return new Promise((resolve) => res.end(resolve)); + }; +} diff --git a/src/use/fastify.ts b/src/use/fastify.ts new file mode 100644 index 00000000..648b847c --- /dev/null +++ b/src/use/fastify.ts @@ -0,0 +1,100 @@ +import type { FastifyRequest, FastifyReply } from 'fastify'; +import { + createHandler as createRawHandler, + HandlerOptions, + OperationContext, +} from '../handler'; + +/** + * @category Server/fastify + */ +export interface RequestContext { + reply: FastifyReply; +} + +/** + * The ready-to-use handler for [fastify](https://www.fastify.io). + * + * Errors thrown from the provided options or callbacks (or even due to + * library misuse or potential bugs) will reject the handler or bubble to the + * returned iterator. They are considered internal errors and you should take care + * of them accordingly. + * + * For production environments, its recommended not to transmit the exact internal + * error details to the client, but instead report to an error logging tool or simply + * the console. + * + * ```ts + * import Fastify from 'fastify'; // yarn add fastify + * import { createHandler } from 'graphql-sse/lib/use/fastify'; + * + * const handler = createHandler({ schema }); + * + * const fastify = Fastify(); + * + * fastify.all('/graphql/stream', async (req, reply) => { + * try { + * await handler(req, reply); + * } catch (err) { + * console.error(err); + * reply.code(500).send(); + * } + * }); + * + * fastify.listen({ port: 4000 }); + * console.log('Listening to port 4000'); + * ``` + * + * @category Server/fastify + */ +export function createHandler( + options: HandlerOptions, +): (req: FastifyRequest, reply: FastifyReply) => Promise { + const handler = createRawHandler(options); + return async function handleRequest(req, reply) { + const [body, init] = await handler({ + method: req.method, + url: req.url, + headers: { + get(key) { + const header = req.headers[key]; + return Array.isArray(header) ? header.join('\n') : header; + }, + }, + body: () => + new Promise((resolve, reject) => { + if (req.body) { + // body was parsed by middleware + return req.body; + } + + let body = ''; + req.raw.on('data', (chunk) => (body += chunk)); + req.raw.once('error', reject); + req.raw.once('end', () => { + req.raw.off('error', reject); + resolve(body); + }); + }), + raw: req, + context: { reply }, + }); + + reply.raw.writeHead(init.status, init.statusText, init.headers); + + if (!body || typeof body === 'string') { + return new Promise((resolve) => + reply.raw.end(body, () => resolve()), + ); + } + + reply.raw.once('close', body.return); + for await (const value of body) { + await new Promise((resolve, reject) => + reply.raw.write(value, (err) => (err ? reject(err) : resolve())), + ); + } + reply.raw.off('close', body.return); + return new Promise((resolve) => reply.raw.end(resolve)); + }; +} diff --git a/src/use/fetch.ts b/src/use/fetch.ts new file mode 100644 index 00000000..8369bf92 --- /dev/null +++ b/src/use/fetch.ts @@ -0,0 +1,105 @@ +import { + createHandler as createRawHandler, + HandlerOptions, + OperationContext, +} from '../handler'; + +/** + * @category Server/fetch + */ +export interface RequestContext { + Response: typeof Response; + ReadableStream: typeof ReadableStream; + TextEncoder: typeof TextEncoder; +} + +/** + * The ready-to-use fetch handler. To be used with your favourite fetch + * framework, in a lambda function, or have deploy to the edge. + * + * Errors thrown from the provided options or callbacks (or even due to + * library misuse or potential bugs) will reject the handler or bubble to the + * returned iterator. They are considered internal errors and you should take care + * of them accordingly. + * + * For production environments, its recommended not to transmit the exact internal + * error details to the client, but instead report to an error logging tool or simply + * the console. + * + * ```ts + * import { createHandler } from 'graphql-sse/lib/use/fetch'; + * import { schema } from './my-graphql'; + * + * const handler = createHandler({ schema }); + * + * export async function fetch(req: Request): Promise { + * try { + * return await handler(req); + * } catch (err) { + * console.error(err); + * return new Response(null, { status: 500 }); + * } + * } + * ``` + * + * @category Server/fetch + */ +export function createHandler( + options: HandlerOptions, + reqCtx: Partial = {}, +): (req: Request) => Promise { + const api: RequestContext = { + Response: reqCtx.Response || Response, + TextEncoder: reqCtx.TextEncoder || TextEncoder, + ReadableStream: reqCtx.ReadableStream || ReadableStream, + }; + + const handler = createRawHandler(options); + return async function handleRequest(req) { + const [resp, init] = await handler({ + method: req.method, + url: req.url, + headers: req.headers, + body: () => req.text(), + raw: req, + context: api, + }); + + if (!resp || typeof resp === 'string') { + return new api.Response(resp, init); + } + + let cancelled = false; + const enc = new api.TextEncoder(); + const stream = new api.ReadableStream({ + async pull(controller) { + const { done, value } = await resp.next(); + if (value != null) { + controller.enqueue(enc.encode(value)); + } + if (done) { + controller.close(); + } + }, + async cancel(e) { + cancelled = true; + await resp.return(e); + }, + }); + + if (req.signal.aborted) { + // TODO: can this check be before the readable stream is created? + // it's possible that the request was aborted before listening + resp.return(undefined); + } else { + // make sure to connect the signals as well + req.signal.addEventListener('abort', () => { + if (!cancelled) { + resp.return(); + } + }); + } + + return new api.Response(stream, init); + }; +} diff --git a/src/use/http.ts b/src/use/http.ts new file mode 100644 index 00000000..59fdf19d --- /dev/null +++ b/src/use/http.ts @@ -0,0 +1,94 @@ +import type { IncomingMessage, ServerResponse } from 'http'; +import { + createHandler as createRawHandler, + HandlerOptions, + OperationContext, +} from '../handler'; + +/** + * @category Server/http + */ +export interface RequestContext { + res: ServerResponse; +} + +/** + * The ready-to-use handler for Node's [http](https://nodejs.org/api/http.html). + * + * Errors thrown from the provided options or callbacks (or even due to + * library misuse or potential bugs) will reject the handler or bubble to the + * returned iterator. They are considered internal errors and you should take care + * of them accordingly. + * + * For production environments, its recommended not to transmit the exact internal + * error details to the client, but instead report to an error logging tool or simply + * the console. + * + * ```ts + * import http from 'http'; + * import { createHandler } from 'graphql-sse/lib/use/http'; + * import { schema } from './my-graphql'; + * + * const handler = createHandler({ schema }); + * + * const server = http.createServer(async (req, res) => { + * try { + * await handler(req, res); + * } catch (err) { + * console.error(err); + * res.writeHead(500).end(); + * } + * }); + * + * server.listen(4000); + * console.log('Listening to port 4000'); + * ``` + * + * @category Server/http + */ +export function createHandler( + options: HandlerOptions, +): (req: IncomingMessage, res: ServerResponse) => Promise { + const handler = createRawHandler(options); + return async function handleRequest(req, res) { + const [body, init] = await handler({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The method will always be available with http requests. + method: req.method!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The url will always be available with http requests. + url: req.url!, + headers: { + get(key) { + const header = req.headers[key]; + return Array.isArray(header) ? header.join('\n') : header; + }, + }, + body: () => + new Promise((resolve, reject) => { + let body = ''; + req.on('data', (chunk) => (body += chunk)); + req.once('error', reject); + req.once('end', () => { + req.off('error', reject); + resolve(body); + }); + }), + raw: req, + context: { res }, + }); + + res.writeHead(init.status, init.statusText, init.headers); + + if (!body || typeof body === 'string') { + return new Promise((resolve) => res.end(body, () => resolve())); + } + + res.once('close', body.return); + for await (const value of body) { + await new Promise((resolve, reject) => + res.write(value, (err) => (err ? reject(err) : resolve())), + ); + } + res.off('close', body.return); + return new Promise((resolve) => res.end(resolve)); + }; +} diff --git a/src/use/http2.ts b/src/use/http2.ts new file mode 100644 index 00000000..7c7097e3 --- /dev/null +++ b/src/use/http2.ts @@ -0,0 +1,96 @@ +import type { Http2ServerRequest, Http2ServerResponse } from 'http2'; +import { + createHandler as createRawHandler, + HandlerOptions, + OperationContext, +} from '../handler'; + +/** + * @category Server/http2 + */ +export interface RequestContext { + res: Http2ServerResponse; +} + +/** + * The ready-to-use handler for Node's [http](https://nodejs.org/api/http2.html). + * + * Errors thrown from the provided options or callbacks (or even due to + * library misuse or potential bugs) will reject the handler or bubble to the + * returned iterator. They are considered internal errors and you should take care + * of them accordingly. + * + * For production environments, its recommended not to transmit the exact internal + * error details to the client, but instead report to an error logging tool or simply + * the console. + * + * ```ts + * import http from 'http2'; + * import { createHandler } from 'graphql-sse/lib/use/http2'; + * import { schema } from './my-graphql'; + * + * const handler = createHandler({ schema }); + * + * const server = http.createServer(async (req, res) => { + * try { + * await handler(req, res); + * } catch (err) { + * console.error(err); + * res.writeHead(500).end(); + * } + * }); + * + * server.listen(4000); + * console.log('Listening to port 4000'); + * ``` + * + * @category Server/http2 + */ +export function createHandler( + options: HandlerOptions, +): (req: Http2ServerRequest, res: Http2ServerResponse) => Promise { + const handler = createRawHandler(options); + return async function handleRequest(req, res) { + const [body, init] = await handler({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The method will always be available with http requests. + method: req.method!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The url will always be available with http requests. + url: req.url!, + headers: { + get(key) { + const header = req.headers[key]; + return Array.isArray(header) ? header.join('\n') : header; + }, + }, + body: () => + new Promise((resolve, reject) => { + let body = ''; + req.on('data', (chunk) => (body += chunk)); + req.once('error', reject); + req.once('end', () => { + req.off('error', reject); + resolve(body); + }); + }), + raw: req, + context: { res }, + }); + + res.writeHead(init.status, init.statusText, init.headers); + + if (!body || typeof body === 'string') { + return new Promise((resolve) => + res.end(body || '', () => resolve()), + ); + } + + res.once('close', body.return); + for await (const value of body) { + await new Promise((resolve, reject) => + res.write(value, (err) => (err ? reject(err) : resolve())), + ); + } + res.off('close', body.return); + return new Promise((resolve) => res.end(resolve)); + }; +} diff --git a/tsconfig.json b/tsconfig.json index 293c92d6..4d772862 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "noUnusedLocals": true, "noUnusedParameters": true, "esModuleInterop": true, - "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "useUnknownInCatchVariables": false, "checkJs": true diff --git a/typedoc.js b/typedoc.js index 5bc13dae..26833836 100644 --- a/typedoc.js +++ b/typedoc.js @@ -2,6 +2,7 @@ * @type {Partial} */ const opts = { + entryPointStrategy: 'expand', out: './docs', readme: 'none', plugin: ['typedoc-plugin-markdown'], diff --git a/yarn.lock b/yarn.lock index 8d33ed26..a40c4b79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -658,6 +658,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.18.6 + resolution: "@babel/plugin-syntax-jsx@npm:7.18.6" + dependencies: + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6d37ea972970195f1ffe1a54745ce2ae456e0ac6145fae9aa1480f297248b262ea6ebb93010eddb86ebfacb94f57c05a1fc5d232b9a67325b09060299d515c67 + languageName: node + linkType: hard + "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" @@ -1342,13 +1353,13 @@ __metadata: linkType: hard "@fastify/ajv-compiler@npm:^3.3.1": - version: 3.4.0 - resolution: "@fastify/ajv-compiler@npm:3.4.0" + version: 3.5.0 + resolution: "@fastify/ajv-compiler@npm:3.5.0" dependencies: ajv: ^8.11.0 ajv-formats: ^2.1.1 fast-uri: ^2.0.0 - checksum: 3e03f9673f0f13ce343bfb4a84f4e908d12bd775a2b82ff4bdf09ac062d09c6b89b62df7f96fab970dd61f77a9e43be2908eb28cd59e27654b25931444bde825 + checksum: 5e5b16469f8d586473d0b32e3a9cf38c0d86ef2a6fb7ea12ed7f3665642bd8eb2dde9adcc317814369cb5a58210bfdac35996fa87d1cc23e88bbc799f0b128b0 languageName: node linkType: hard @@ -1360,18 +1371,18 @@ __metadata: linkType: hard "@fastify/error@npm:^3.0.0": - version: 3.1.0 - resolution: "@fastify/error@npm:3.1.0" - checksum: 13f12f40b4b66db2fec70f62863c035471600b29f18d7ed52ca84ed3b2d7c9817b2bde8787b7e61e063ade957968746e037333392d2dd3fa6f9e7232225450d5 + version: 3.2.0 + resolution: "@fastify/error@npm:3.2.0" + checksum: e538ef76fd2dedd0584691e0c891997321a2050092b11089a70090f5a0edab0dc8ab069747aa6025782280824e2348548e051c8e77558baec699bd44e581e187 languageName: node linkType: hard "@fastify/fast-json-stringify-compiler@npm:^4.1.0": - version: 4.1.0 - resolution: "@fastify/fast-json-stringify-compiler@npm:4.1.0" + version: 4.2.0 + resolution: "@fastify/fast-json-stringify-compiler@npm:4.2.0" dependencies: fast-json-stringify: ^5.0.0 - checksum: 5f848f606e23b04904189bf98c44ccae70c4ceaa793d619d3804ba4a9969d4b9846ceef4ac8a53d536a1cf8f1d3c30a4602850a44fc62bdc1893e341442b6e4f + checksum: c79e9aab14fe2693c4ae399824e0bdd2337a5f1ae403aeec0178a566b03633681d10fc16f010bcf503fb13a50127db463cfa25783dfbed1338b9ccd2593124bd languageName: node linkType: hard @@ -1434,51 +1445,50 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/console@npm:28.1.3" +"@jest/console@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/console@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 "@types/node": "*" chalk: ^4.0.0 - jest-message-util: ^28.1.3 - jest-util: ^28.1.3 + jest-message-util: ^29.3.1 + jest-util: ^29.3.1 slash: ^3.0.0 - checksum: fe50d98d26d02ce2901c76dff4bd5429a33c13affb692c9ebf8a578ca2f38a5dd854363d40d6c394f215150791fd1f692afd8e730a4178dda24107c8dfd9750a + checksum: 9eecbfb6df4f5b810374849b7566d321255e6fd6e804546236650384966be532ff75a3e445a3277eadefe67ddf4dc56cd38332abd72d6a450f1bea9866efc6d7 languageName: node linkType: hard -"@jest/core@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/core@npm:28.1.3" +"@jest/core@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/core@npm:29.3.1" dependencies: - "@jest/console": ^28.1.3 - "@jest/reporters": ^28.1.3 - "@jest/test-result": ^28.1.3 - "@jest/transform": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/console": ^29.3.1 + "@jest/reporters": ^29.3.1 + "@jest/test-result": ^29.3.1 + "@jest/transform": ^29.3.1 + "@jest/types": ^29.3.1 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 ci-info: ^3.2.0 exit: ^0.1.2 graceful-fs: ^4.2.9 - jest-changed-files: ^28.1.3 - jest-config: ^28.1.3 - jest-haste-map: ^28.1.3 - jest-message-util: ^28.1.3 - jest-regex-util: ^28.0.2 - jest-resolve: ^28.1.3 - jest-resolve-dependencies: ^28.1.3 - jest-runner: ^28.1.3 - jest-runtime: ^28.1.3 - jest-snapshot: ^28.1.3 - jest-util: ^28.1.3 - jest-validate: ^28.1.3 - jest-watcher: ^28.1.3 + jest-changed-files: ^29.2.0 + jest-config: ^29.3.1 + jest-haste-map: ^29.3.1 + jest-message-util: ^29.3.1 + jest-regex-util: ^29.2.0 + jest-resolve: ^29.3.1 + jest-resolve-dependencies: ^29.3.1 + jest-runner: ^29.3.1 + jest-runtime: ^29.3.1 + jest-snapshot: ^29.3.1 + jest-util: ^29.3.1 + jest-validate: ^29.3.1 + jest-watcher: ^29.3.1 micromatch: ^4.0.4 - pretty-format: ^28.1.3 - rimraf: ^3.0.0 + pretty-format: ^29.3.1 slash: ^3.0.0 strip-ansi: ^6.0.0 peerDependencies: @@ -1486,76 +1496,77 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: cb79f34bafc4637e7130df12257f5b29075892a2be2c7f45c6d4c0420853e80b5dae11016e652530eb234f4c44c00910cdca3c2cd86275721860725073f7d9b4 + checksum: e3ac9201e8a084ccd832b17877b56490402b919f227622bb24f9372931e77b869e60959d34144222ce20fb619d0a6a6be20b257adb077a6b0f430a4584a45b0f languageName: node linkType: hard -"@jest/environment@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/environment@npm:28.1.3" +"@jest/environment@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/environment@npm:29.3.1" dependencies: - "@jest/fake-timers": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/fake-timers": ^29.3.1 + "@jest/types": ^29.3.1 "@types/node": "*" - jest-mock: ^28.1.3 - checksum: 14c496b84aef951df33128cea68988e9de43b2e9d62be9f9c4308d4ac307fa345642813679f80d0a4cedeb900cf6f0b6bb2b92ce089528e8721f72295fdc727f + jest-mock: ^29.3.1 + checksum: 974102aba7cc80508f787bb5504dcc96e5392e0a7776a63dffbf54ddc2c77d52ef4a3c08ed2eedec91965befff873f70cd7c9ed56f62bb132dcdb821730e6076 languageName: node linkType: hard -"@jest/expect-utils@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/expect-utils@npm:28.1.3" +"@jest/expect-utils@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/expect-utils@npm:29.3.1" dependencies: - jest-get-type: ^28.0.2 - checksum: 808ea3a68292a7e0b95490fdd55605c430b4cf209ea76b5b61bfb2a1badcb41bc046810fe4e364bd5fe04663978aa2bd73d8f8465a761dd7c655aeb44cf22987 + jest-get-type: ^29.2.0 + checksum: 7f3b853eb1e4299988f66b9aa49c1aacb7b8da1cf5518dca4ccd966e865947eed8f1bde6c8f5207d8400e9af870112a44b57aa83515ad6ea5e4a04a971863adb languageName: node linkType: hard -"@jest/expect@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/expect@npm:28.1.3" +"@jest/expect@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/expect@npm:29.3.1" dependencies: - expect: ^28.1.3 - jest-snapshot: ^28.1.3 - checksum: 4197f6fdddc33dc45ba4e838f992fc61839c421d7aed0dfe665ef9c2f172bb1df8a8cac9cecee272b40e744a326da521d5e182709fe82a0b936055bfffa3b473 + expect: ^29.3.1 + jest-snapshot: ^29.3.1 + checksum: 1d7b5cc735c8a99bfbed884d80fdb43b23b3456f4ec88c50fd86404b097bb77fba84f44e707fc9b49f106ca1154ae03f7c54dc34754b03f8a54eeb420196e5bf languageName: node linkType: hard -"@jest/fake-timers@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/fake-timers@npm:28.1.3" +"@jest/fake-timers@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/fake-timers@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 "@sinonjs/fake-timers": ^9.1.2 "@types/node": "*" - jest-message-util: ^28.1.3 - jest-mock: ^28.1.3 - jest-util: ^28.1.3 - checksum: cec14d5b14913a54dce64a62912c5456235f5d90b509ceae19c727565073114dae1aaf960ac6be96b3eb94789a3a758b96b72c8fca7e49a6ccac415fbc0321e1 + jest-message-util: ^29.3.1 + jest-mock: ^29.3.1 + jest-util: ^29.3.1 + checksum: b1dafa8cdc439ef428cd772c775f0b22703677f52615513eda11a104bbfc352d7ec69b1225db95d4ef2e1b4ef0f23e1a7d96de5313aeb0950f672e6548ae069d languageName: node linkType: hard -"@jest/globals@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/globals@npm:28.1.3" +"@jest/globals@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/globals@npm:29.3.1" dependencies: - "@jest/environment": ^28.1.3 - "@jest/expect": ^28.1.3 - "@jest/types": ^28.1.3 - checksum: 3504bb23de629d466c6f2b6b75d2e1c1b10caccbbcfb7eaa82d22cc37711c8e364c243929581184846605c023b475ea6c42c2e3ea5994429a988d8d527af32cd + "@jest/environment": ^29.3.1 + "@jest/expect": ^29.3.1 + "@jest/types": ^29.3.1 + jest-mock: ^29.3.1 + checksum: 4d2b9458aabf7c28fd167e53984477498c897b64eec67a7f84b8fff465235cae1456ee0721cb0e7943f0cda443c7656adb9801f9f34e27495b8ebbd9f3033100 languageName: node linkType: hard -"@jest/reporters@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/reporters@npm:28.1.3" +"@jest/reporters@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/reporters@npm:29.3.1" dependencies: "@bcoe/v8-coverage": ^0.2.3 - "@jest/console": ^28.1.3 - "@jest/test-result": ^28.1.3 - "@jest/transform": ^28.1.3 - "@jest/types": ^28.1.3 - "@jridgewell/trace-mapping": ^0.3.13 + "@jest/console": ^29.3.1 + "@jest/test-result": ^29.3.1 + "@jest/transform": ^29.3.1 + "@jest/types": ^29.3.1 + "@jridgewell/trace-mapping": ^0.3.15 "@types/node": "*" chalk: ^4.0.0 collect-v8-coverage: ^1.0.0 @@ -1567,101 +1578,100 @@ __metadata: istanbul-lib-report: ^3.0.0 istanbul-lib-source-maps: ^4.0.0 istanbul-reports: ^3.1.3 - jest-message-util: ^28.1.3 - jest-util: ^28.1.3 - jest-worker: ^28.1.3 + jest-message-util: ^29.3.1 + jest-util: ^29.3.1 + jest-worker: ^29.3.1 slash: ^3.0.0 string-length: ^4.0.1 strip-ansi: ^6.0.0 - terminal-link: ^2.0.0 v8-to-istanbul: ^9.0.1 peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true - checksum: a7440887ce837922cbeaa64c3232eb48aae02aa9123f29fc4280ad3e1afe4b35dcba171ba1d5fd219037c396c5152d9c2d102cff1798dd5ae3bd33ac4759ae0a + checksum: 273e0c6953285f01151e9d84ac1e55744802a1ec79fb62dafeea16a49adfe7b24e7f35bef47a0214e5e057272dbfdacf594208286b7766046fd0f3cfa2043840 languageName: node linkType: hard -"@jest/schemas@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/schemas@npm:28.1.3" +"@jest/schemas@npm:^29.0.0": + version: 29.0.0 + resolution: "@jest/schemas@npm:29.0.0" dependencies: "@sinclair/typebox": ^0.24.1 - checksum: 3cf1d4b66c9c4ffda58b246de1ddcba8e6ad085af63dccdf07922511f13b68c0cc480a7bc620cb4f3099a6f134801c747e1df7bfc7a4ef4dceefbdea3e31e1de + checksum: 41355c78f09eb1097e57a3c5d0ca11c9099e235e01ea5fa4e3953562a79a6a9296c1d300f1ba50ca75236048829e056b00685cd2f1ff8285e56fd2ce01249acb languageName: node linkType: hard -"@jest/source-map@npm:^28.1.2": - version: 28.1.2 - resolution: "@jest/source-map@npm:28.1.2" +"@jest/source-map@npm:^29.2.0": + version: 29.2.0 + resolution: "@jest/source-map@npm:29.2.0" dependencies: - "@jridgewell/trace-mapping": ^0.3.13 + "@jridgewell/trace-mapping": ^0.3.15 callsites: ^3.0.0 graceful-fs: ^4.2.9 - checksum: b82a5c2e93d35d86779c61a02ccb967d1b5cd2e9dd67d26d8add44958637cbbb99daeeb8129c7653389cb440dc2a2f5ae4d2183dc453c67669ff98938b775a3a + checksum: 09f76ab63d15dcf44b3035a79412164f43be34ec189575930f1a00c87e36ea0211ebd6a4fbe2253c2516e19b49b131f348ddbb86223ca7b6bbac9a6bc76ec96e languageName: node linkType: hard -"@jest/test-result@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/test-result@npm:28.1.3" +"@jest/test-result@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/test-result@npm:29.3.1" dependencies: - "@jest/console": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/console": ^29.3.1 + "@jest/types": ^29.3.1 "@types/istanbul-lib-coverage": ^2.0.0 collect-v8-coverage: ^1.0.0 - checksum: 957a5dd2fd2e84aabe86698f93c0825e96128ccaa23abf548b159a9b08ac74e4bde7acf4bec48479243dbdb27e4ea1b68c171846d21fb64855c6b55cead9ef27 + checksum: b24ac283321189b624c372a6369c0674b0ee6d9e3902c213452c6334d037113718156b315364bee8cee0f03419c2bdff5e2c63967193fb422830e79cbb26866a languageName: node linkType: hard -"@jest/test-sequencer@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/test-sequencer@npm:28.1.3" +"@jest/test-sequencer@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/test-sequencer@npm:29.3.1" dependencies: - "@jest/test-result": ^28.1.3 + "@jest/test-result": ^29.3.1 graceful-fs: ^4.2.9 - jest-haste-map: ^28.1.3 + jest-haste-map: ^29.3.1 slash: ^3.0.0 - checksum: 13f8905e6d1ec8286694146f7be3cf90eff801bbdea5e5c403e6881444bb390ed15494c7b9948aa94bd7e9c9a851e0d3002ed6e7371d048b478596e5b23df953 + checksum: a8325b1ea0ce644486fb63bb67cedd3524d04e3d7b1e6c1e3562bf12ef477ecd0cf34044391b2a07d925e1c0c8b4e0f3285035ceca3a474a2c55980f1708caf3 languageName: node linkType: hard -"@jest/transform@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/transform@npm:28.1.3" +"@jest/transform@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/transform@npm:29.3.1" dependencies: "@babel/core": ^7.11.6 - "@jest/types": ^28.1.3 - "@jridgewell/trace-mapping": ^0.3.13 + "@jest/types": ^29.3.1 + "@jridgewell/trace-mapping": ^0.3.15 babel-plugin-istanbul: ^6.1.1 chalk: ^4.0.0 - convert-source-map: ^1.4.0 - fast-json-stable-stringify: ^2.0.0 + convert-source-map: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 graceful-fs: ^4.2.9 - jest-haste-map: ^28.1.3 - jest-regex-util: ^28.0.2 - jest-util: ^28.1.3 + jest-haste-map: ^29.3.1 + jest-regex-util: ^29.2.0 + jest-util: ^29.3.1 micromatch: ^4.0.4 pirates: ^4.0.4 slash: ^3.0.0 write-file-atomic: ^4.0.1 - checksum: dadf618936e0aa84342f07f532801d5bed43cdf95d1417b929e4f8782c872cff1adc84096d5a287a796d0039a2691c06d8450cce5a713a8b52fbb9f872a1e760 + checksum: 673df5900ffc95bc811084e09d6e47948034dea6ab6cc4f81f80977e3a52468a6c2284d0ba9796daf25a62ae50d12f7e97fc9a3a0c587f11f2a479ff5493ca53 languageName: node linkType: hard -"@jest/types@npm:^28.1.3": - version: 28.1.3 - resolution: "@jest/types@npm:28.1.3" +"@jest/types@npm:^29.3.1": + version: 29.3.1 + resolution: "@jest/types@npm:29.3.1" dependencies: - "@jest/schemas": ^28.1.3 + "@jest/schemas": ^29.0.0 "@types/istanbul-lib-coverage": ^2.0.0 "@types/istanbul-reports": ^3.0.0 "@types/node": "*" "@types/yargs": ^17.0.8 chalk: ^4.0.0 - checksum: 1e258d9c063fcf59ebc91e46d5ea5984674ac7ae6cae3e50aa780d22b4405bf2c925f40350bf30013839eb5d4b5e521d956ddf8f3b7c78debef0e75a07f57350 + checksum: 6f9faf27507b845ff3839c1adc6dbd038d7046d03d37e84c9fc956f60718711a801a5094c7eeee6b39ccf42c0ab61347fdc0fa49ab493ae5a8efd2fd41228ee8 languageName: node linkType: hard @@ -1717,7 +1727,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.13, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.17 resolution: "@jridgewell/trace-mapping@npm:0.3.17" dependencies: @@ -2385,7 +2395,7 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.18": +"@types/express-serve-static-core@npm:^4.17.31": version: 4.17.31 resolution: "@types/express-serve-static-core@npm:4.17.31" dependencies: @@ -2396,15 +2406,15 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^4.17.14": - version: 4.17.14 - resolution: "@types/express@npm:4.17.14" +"@types/express@npm:^4.17.15": + version: 4.17.15 + resolution: "@types/express@npm:4.17.15" dependencies: "@types/body-parser": "*" - "@types/express-serve-static-core": ^4.17.18 + "@types/express-serve-static-core": ^4.17.31 "@types/qs": "*" "@types/serve-static": "*" - checksum: 15c1af46d02de834e4a225eccaa9d85c0370fdbb3ed4e1bc2d323d24872309961542b993ae236335aeb3e278630224a6ea002078d39e651d78a3b0356b1eaa79 + checksum: b4acd8a836d4f6409cdf79b12d6e660485249b62500cccd61e7997d2f520093edf77d7f8498ca79d64a112c6434b6de5ca48039b8fde2c881679eced7e96979b languageName: node linkType: hard @@ -2452,13 +2462,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^28.1.8": - version: 28.1.8 - resolution: "@types/jest@npm:28.1.8" +"@types/jest@npm:^29.2.4": + version: 29.2.4 + resolution: "@types/jest@npm:29.2.4" dependencies: - expect: ^28.0.0 - pretty-format: ^28.0.0 - checksum: d4cd36158a3ae1d4b42cc48a77c95de74bc56b84cf81e09af3ee0399c34f4a7da8ab9e787570f10004bd642f9e781b0033c37327fbbf4a8e4b6e37e8ee3693a7 + expect: ^29.0.0 + pretty-format: ^29.0.0 + checksum: 9deb4756fe1b438d41ff1aae7d6c216c9e49e5fe60f539f8edb6698ffeb530ff7b25d37e223439b03602ca3a7397c9c2e53e1a39c7bd616353472fce0cc04107 languageName: node linkType: hard @@ -2997,20 +3007,20 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:^28.1.3": - version: 28.1.3 - resolution: "babel-jest@npm:28.1.3" +"babel-jest@npm:^29.3.1": + version: 29.3.1 + resolution: "babel-jest@npm:29.3.1" dependencies: - "@jest/transform": ^28.1.3 + "@jest/transform": ^29.3.1 "@types/babel__core": ^7.1.14 babel-plugin-istanbul: ^6.1.1 - babel-preset-jest: ^28.1.3 + babel-preset-jest: ^29.2.0 chalk: ^4.0.0 graceful-fs: ^4.2.9 slash: ^3.0.0 peerDependencies: "@babel/core": ^7.8.0 - checksum: 57ccd2296e1839687b5df2fd138c3d00717e0369e385254b012ccd4ee70e75f5d5c8e6cfcdf92d155015b468cfebb847b38e69bb5805d8aaf730e20575127cc6 + checksum: 793848238a771a931ddeb5930b9ec8ab800522ac8d64933665698f4a39603d157e572e20b57d79610277e1df88d3ee82b180d59a21f3570388f602beeb38a595 languageName: node linkType: hard @@ -3027,15 +3037,15 @@ __metadata: languageName: node linkType: hard -"babel-plugin-jest-hoist@npm:^28.1.3": - version: 28.1.3 - resolution: "babel-plugin-jest-hoist@npm:28.1.3" +"babel-plugin-jest-hoist@npm:^29.2.0": + version: 29.2.0 + resolution: "babel-plugin-jest-hoist@npm:29.2.0" dependencies: "@babel/template": ^7.3.3 "@babel/types": ^7.3.3 "@types/babel__core": ^7.1.14 "@types/babel__traverse": ^7.0.6 - checksum: 648d89f9d80f6450ce7e50d0c32eb91b7f26269b47c3e37aaf2e0f2f66a980978345bd6b8c9b8c3aa6a8252ad2bc2c9fb50630e9895622c9a0972af5f70ed20e + checksum: 368d271ceae491ae6b96cd691434859ea589fbe5fd5aead7660df75d02394077273c6442f61f390e9347adffab57a32b564d0fabcf1c53c4b83cd426cb644072 languageName: node linkType: hard @@ -3097,15 +3107,15 @@ __metadata: languageName: node linkType: hard -"babel-preset-jest@npm:^28.1.3": - version: 28.1.3 - resolution: "babel-preset-jest@npm:28.1.3" +"babel-preset-jest@npm:^29.2.0": + version: 29.2.0 + resolution: "babel-preset-jest@npm:29.2.0" dependencies: - babel-plugin-jest-hoist: ^28.1.3 + babel-plugin-jest-hoist: ^29.2.0 babel-preset-current-node-syntax: ^1.0.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: 8248a4a5ca4242cc06ad13b10b9183ad2664da8fb0da060c352223dcf286f0ce9c708fa17901dc44ecabec25e6d309e5e5b9830a61dd777c3925f187a345a47d + checksum: 1b09a2db968c36e064daf98082cfffa39c849b63055112ddc56fc2551fd0d4783897265775b1d2f8a257960a3339745de92e74feb01bad86d41c4cecbfa854fc languageName: node linkType: hard @@ -3656,13 +3666,20 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 languageName: node linkType: hard +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 + languageName: node + linkType: hard + "cookie-signature@npm:1.0.6": version: 1.0.6 resolution: "cookie-signature@npm:1.0.6" @@ -3733,13 +3750,6 @@ __metadata: languageName: node linkType: hard -"data-uri-to-buffer@npm:^4.0.0": - version: 4.0.0 - resolution: "data-uri-to-buffer@npm:4.0.0" - checksum: a010653869abe8bb51259432894ac62c52bf79ad761d418d94396f48c346f2ae739c46b254e8bb5987bded8a653d467db1968db3a69bab1d33aa5567baa5cfc7 - languageName: node - linkType: hard - "dateformat@npm:^3.0.0": version: 3.0.3 resolution: "dateformat@npm:3.0.3" @@ -3897,10 +3907,10 @@ __metadata: languageName: node linkType: hard -"diff-sequences@npm:^28.1.1": - version: 28.1.1 - resolution: "diff-sequences@npm:28.1.1" - checksum: e2529036505567c7ca5a2dea86b6bcd1ca0e3ae63bf8ebf529b8a99cfa915bbf194b7021dc1c57361a4017a6d95578d4ceb29fabc3232a4f4cb866a2726c7690 +"diff-sequences@npm:^29.3.1": + version: 29.3.1 + resolution: "diff-sequences@npm:29.3.1" + checksum: 8edab8c383355022e470779a099852d595dd856f9f5bd7af24f177e74138a668932268b4c4fd54096eed643861575c3652d4ecbbb1a9d710488286aed3ffa443 languageName: node linkType: hard @@ -3961,10 +3971,10 @@ __metadata: languageName: node linkType: hard -"emittery@npm:^0.10.2": - version: 0.10.2 - resolution: "emittery@npm:0.10.2" - checksum: ee3e21788b043b90885b18ea756ec3105c1cedc50b29709c92b01e239c7e55345d4bb6d3aef4ddbaf528eef448a40b3bb831bad9ee0fc9c25cbf1367ab1ab5ac +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 2b089ab6306f38feaabf4f6f02792f9ec85fc054fda79f44f6790e61bbf6bc4e1616afb9b232e0c5ec5289a8a452f79bfa6d905a6fd64e94b49981f0934001c6 languageName: node linkType: hard @@ -4253,13 +4263,6 @@ __metadata: languageName: node linkType: hard -"eventsource@npm:^2.0.2": - version: 2.0.2 - resolution: "eventsource@npm:2.0.2" - checksum: c0072d972753e10c705d9b2285b559184bf29d011bc208973dde9c8b6b8b7b6fdad4ef0846cecb249f7b1585e860fdf324cbd2ac854a76bc53649e797496e99a - languageName: node - linkType: hard - "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -4284,16 +4287,16 @@ __metadata: languageName: node linkType: hard -"expect@npm:^28.0.0, expect@npm:^28.1.3": - version: 28.1.3 - resolution: "expect@npm:28.1.3" +"expect@npm:^29.0.0, expect@npm:^29.3.1": + version: 29.3.1 + resolution: "expect@npm:29.3.1" dependencies: - "@jest/expect-utils": ^28.1.3 - jest-get-type: ^28.0.2 - jest-matcher-utils: ^28.1.3 - jest-message-util: ^28.1.3 - jest-util: ^28.1.3 - checksum: 101e0090de300bcafedb7dbfd19223368a2251ce5fe0105bbb6de5720100b89fb6b64290ebfb42febc048324c76d6a4979cdc4b61eb77747857daf7a5de9b03d + "@jest/expect-utils": ^29.3.1 + jest-get-type: ^29.2.0 + jest-matcher-utils: ^29.3.1 + jest-message-util: ^29.3.1 + jest-util: ^29.3.1 + checksum: e9588c2a430b558b9a3dc72d4ad05f36b047cb477bc6a7bb9cfeef7614fe7e5edbab424c2c0ce82739ee21ecbbbd24596259528209f84cd72500cc612d910d30 languageName: node linkType: hard @@ -4363,7 +4366,7 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:^2.0.0": +"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb @@ -4462,16 +4465,6 @@ __metadata: languageName: node linkType: hard -"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": - version: 3.2.0 - resolution: "fetch-blob@npm:3.2.0" - dependencies: - node-domexception: ^1.0.0 - web-streams-polyfill: ^3.0.3 - checksum: f19bc28a2a0b9626e69fd7cf3a05798706db7f6c7548da657cbf5026a570945f5eeaedff52007ea35c8bcd3d237c58a20bf1543bc568ab2422411d762dd3d5bf - languageName: node - linkType: hard - "figures@npm:^2.0.0": version: 2.0.0 resolution: "figures@npm:2.0.0" @@ -4589,15 +4582,6 @@ __metadata: languageName: node linkType: hard -"formdata-polyfill@npm:^4.0.10": - version: 4.0.10 - resolution: "formdata-polyfill@npm:4.0.10" - dependencies: - fetch-blob: ^3.1.2 - checksum: 82a34df292afadd82b43d4a740ce387bc08541e0a534358425193017bf9fb3567875dc5f69564984b1da979979b70703aa73dee715a17b6c229752ae736dd9db - languageName: node - linkType: hard - "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -4868,22 +4852,19 @@ __metadata: "@semantic-release/git": ^10.0.1 "@types/eslint": ^8.4.10 "@types/eventsource": ^1.1.10 - "@types/express": ^4.17.14 + "@types/express": ^4.17.15 "@types/glob": ^8.0.0 - "@types/jest": ^28.1.8 + "@types/jest": ^29.2.4 "@typescript-eslint/eslint-plugin": ^5.45.1 "@typescript-eslint/parser": ^5.45.1 - babel-jest: ^28.1.3 + babel-jest: ^29.3.1 eslint: ^8.29.0 eslint-config-prettier: ^8.5.0 - eventsource: ^2.0.2 express: ^4.18.2 fastify: ^4.10.2 glob: ^8.0.3 graphql: ^16.6.0 - jest: ^28.1.3 - jest-jasmine2: ^28.1.3 - node-fetch: ^3.3.0 + jest: ^29.3.1 prettier: ^2.8.0 rollup: ^3.6.0 semantic-release: ^19.0.5 @@ -5435,57 +5416,57 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-changed-files@npm:28.1.3" +"jest-changed-files@npm:^29.2.0": + version: 29.2.0 + resolution: "jest-changed-files@npm:29.2.0" dependencies: execa: ^5.0.0 p-limit: ^3.1.0 - checksum: c78af14a68b9b19101623ae7fde15a2488f9b3dbe8cca12a05c4a223bc9bfd3bf41ee06830f20fb560c52434435d6153c9cc6cf450b1f7b03e5e7f96a953a6a6 + checksum: 8ad8290324db1de2ee3c9443d3e3fbfdcb6d72ec7054c5796be2854b2bc239dea38a7c797c8c9c2bd959f539d44305790f2f75b18f3046b04317ed77c7480cb1 languageName: node linkType: hard -"jest-circus@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-circus@npm:28.1.3" +"jest-circus@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-circus@npm:29.3.1" dependencies: - "@jest/environment": ^28.1.3 - "@jest/expect": ^28.1.3 - "@jest/test-result": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/environment": ^29.3.1 + "@jest/expect": ^29.3.1 + "@jest/test-result": ^29.3.1 + "@jest/types": ^29.3.1 "@types/node": "*" chalk: ^4.0.0 co: ^4.6.0 dedent: ^0.7.0 is-generator-fn: ^2.0.0 - jest-each: ^28.1.3 - jest-matcher-utils: ^28.1.3 - jest-message-util: ^28.1.3 - jest-runtime: ^28.1.3 - jest-snapshot: ^28.1.3 - jest-util: ^28.1.3 + jest-each: ^29.3.1 + jest-matcher-utils: ^29.3.1 + jest-message-util: ^29.3.1 + jest-runtime: ^29.3.1 + jest-snapshot: ^29.3.1 + jest-util: ^29.3.1 p-limit: ^3.1.0 - pretty-format: ^28.1.3 + pretty-format: ^29.3.1 slash: ^3.0.0 stack-utils: ^2.0.3 - checksum: b635e60a9c92adaefc3f24def8eba691e7c2fdcf6c9fa640cddf2eb8c8b26ee62eab73ebb88798fd7c52a74c1495a984e39b748429b610426f02e9d3d56e09b2 + checksum: 125710debd998ad9693893e7c1235e271b79f104033b8169d82afe0bc0d883f8f5245feef87adcbb22ad27ff749fd001aa998d11a132774b03b4e2b8af77d5d8 languageName: node linkType: hard -"jest-cli@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-cli@npm:28.1.3" +"jest-cli@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-cli@npm:29.3.1" dependencies: - "@jest/core": ^28.1.3 - "@jest/test-result": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/core": ^29.3.1 + "@jest/test-result": ^29.3.1 + "@jest/types": ^29.3.1 chalk: ^4.0.0 exit: ^0.1.2 graceful-fs: ^4.2.9 import-local: ^3.0.2 - jest-config: ^28.1.3 - jest-util: ^28.1.3 - jest-validate: ^28.1.3 + jest-config: ^29.3.1 + jest-util: ^29.3.1 + jest-validate: ^29.3.1 prompts: ^2.0.1 yargs: ^17.3.1 peerDependencies: @@ -5495,34 +5476,34 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: fb424576bf38346318daddee3fcc597cd78cb8dda1759d09c529d8ba1a748f2765c17b00671072a838826e59465a810ff8a232bc6ba2395c131bf3504425a363 + checksum: 829895d33060042443bd1e9e87eb68993773d74f2c8a9b863acf53cece39d227ae0e7d76df2e9c5934c414bdf70ce398a34b3122cfe22164acb2499a74d7288d languageName: node linkType: hard -"jest-config@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-config@npm:28.1.3" +"jest-config@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-config@npm:29.3.1" dependencies: "@babel/core": ^7.11.6 - "@jest/test-sequencer": ^28.1.3 - "@jest/types": ^28.1.3 - babel-jest: ^28.1.3 + "@jest/test-sequencer": ^29.3.1 + "@jest/types": ^29.3.1 + babel-jest: ^29.3.1 chalk: ^4.0.0 ci-info: ^3.2.0 deepmerge: ^4.2.2 glob: ^7.1.3 graceful-fs: ^4.2.9 - jest-circus: ^28.1.3 - jest-environment-node: ^28.1.3 - jest-get-type: ^28.0.2 - jest-regex-util: ^28.0.2 - jest-resolve: ^28.1.3 - jest-runner: ^28.1.3 - jest-util: ^28.1.3 - jest-validate: ^28.1.3 + jest-circus: ^29.3.1 + jest-environment-node: ^29.3.1 + jest-get-type: ^29.2.0 + jest-regex-util: ^29.2.0 + jest-resolve: ^29.3.1 + jest-runner: ^29.3.1 + jest-util: ^29.3.1 + jest-validate: ^29.3.1 micromatch: ^4.0.4 parse-json: ^5.2.0 - pretty-format: ^28.1.3 + pretty-format: ^29.3.1 slash: ^3.0.0 strip-json-comments: ^3.1.1 peerDependencies: @@ -5533,159 +5514,135 @@ __metadata: optional: true ts-node: optional: true - checksum: ddabffd3a3a8cb6c2f58f06cdf3535157dbf8c70bcde3e5c3de7bee6a8d617840ffc8cffb0083e38c6814f2a08c225ca19f58898efaf4f351af94679f22ce6bc + checksum: 6e663f04ae1024a53a4c2c744499b4408ca9a8b74381dd5e31b11bb3c7393311ecff0fb61b06287768709eb2c9e5a2fd166d258f5a9123abbb4c5812f99c12fe languageName: node linkType: hard -"jest-diff@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-diff@npm:28.1.3" +"jest-diff@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-diff@npm:29.3.1" dependencies: chalk: ^4.0.0 - diff-sequences: ^28.1.1 - jest-get-type: ^28.0.2 - pretty-format: ^28.1.3 - checksum: fa8583e0ccbe775714ce850b009be1b0f6b17a4b6759f33ff47adef27942ebc610dbbcc8a5f7cfb7f12b3b3b05afc9fb41d5f766674616025032ff1e4f9866e0 + diff-sequences: ^29.3.1 + jest-get-type: ^29.2.0 + pretty-format: ^29.3.1 + checksum: ac5c09745f2b1897e6f53216acaf6ed44fc4faed8e8df053ff4ac3db5d2a1d06a17b876e49faaa15c8a7a26f5671bcbed0a93781dcc2835f781c79a716a591a9 languageName: node linkType: hard -"jest-docblock@npm:^28.1.1": - version: 28.1.1 - resolution: "jest-docblock@npm:28.1.1" +"jest-docblock@npm:^29.2.0": + version: 29.2.0 + resolution: "jest-docblock@npm:29.2.0" dependencies: detect-newline: ^3.0.0 - checksum: 22fca68d988ecb2933bc65f448facdca85fc71b4bd0a188ea09a5ae1b0cc3a049a2a6ec7e7eaa2542c1d5cb5e5145e420a3df4fa280f5070f486c44da1d36151 + checksum: b3f1227b7d73fc9e4952180303475cf337b36fa65c7f730ac92f0580f1c08439983262fee21cf3dba11429aa251b4eee1e3bc74796c5777116b400d78f9d2bbe languageName: node linkType: hard -"jest-each@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-each@npm:28.1.3" +"jest-each@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-each@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 chalk: ^4.0.0 - jest-get-type: ^28.0.2 - jest-util: ^28.1.3 - pretty-format: ^28.1.3 - checksum: 5c5b8ccb1484e58b027bea682cfa020a45e5bf5379cc7c23bdec972576c1dc3c3bf03df2b78416cefc1a58859dd33b7cf5fff54c370bc3c0f14a3e509eb87282 + jest-get-type: ^29.2.0 + jest-util: ^29.3.1 + pretty-format: ^29.3.1 + checksum: 16d51ef8f96fba44a3479f1c6f7672027e3b39236dc4e41217c38fe60a3b66b022ffcee72f8835a442f7a8a0a65980a93fb8e73a9782d192452526e442ad049a languageName: node linkType: hard -"jest-environment-node@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-environment-node@npm:28.1.3" +"jest-environment-node@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-environment-node@npm:29.3.1" dependencies: - "@jest/environment": ^28.1.3 - "@jest/fake-timers": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/environment": ^29.3.1 + "@jest/fake-timers": ^29.3.1 + "@jest/types": ^29.3.1 "@types/node": "*" - jest-mock: ^28.1.3 - jest-util: ^28.1.3 - checksum: 1048fe306a6a8b0880a4c66278ebb57479f29c12cff89aab3aa79ab77a8859cf17ab8aa9919fd21c329a7db90e35581b43664e694ad453d5b04e00f3c6420469 + jest-mock: ^29.3.1 + jest-util: ^29.3.1 + checksum: 16d4854bd2d35501bd4862ca069baf27ce9f5fd7642fdcab9d2dab49acd28c082d0c8882bf2bb28ed7bbaada486da577c814c9688ddc62d1d9f74a954fde996a languageName: node linkType: hard -"jest-get-type@npm:^28.0.2": - version: 28.0.2 - resolution: "jest-get-type@npm:28.0.2" - checksum: 5281d7c89bc8156605f6d15784f45074f4548501195c26e9b188742768f72d40948252d13230ea905b5349038865a1a8eeff0e614cc530ff289dfc41fe843abd +"jest-get-type@npm:^29.2.0": + version: 29.2.0 + resolution: "jest-get-type@npm:29.2.0" + checksum: e396fd880a30d08940ed8a8e43cd4595db1b8ff09649018eb358ca701811137556bae82626af73459e3c0f8c5e972ed1e57fd3b1537b13a260893dac60a90942 languageName: node linkType: hard -"jest-haste-map@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-haste-map@npm:28.1.3" +"jest-haste-map@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-haste-map@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 "@types/graceful-fs": ^4.1.3 "@types/node": "*" anymatch: ^3.0.3 fb-watchman: ^2.0.0 fsevents: ^2.3.2 graceful-fs: ^4.2.9 - jest-regex-util: ^28.0.2 - jest-util: ^28.1.3 - jest-worker: ^28.1.3 + jest-regex-util: ^29.2.0 + jest-util: ^29.3.1 + jest-worker: ^29.3.1 micromatch: ^4.0.4 walker: ^1.0.8 dependenciesMeta: fsevents: optional: true - checksum: d05fdc108645fc2b39fcd4001952cc7a8cb550e93494e98c1e9ab1fc542686f6ac67177c132e564cf94fe8f81503f3f8db8b825b9b713dc8c5748aec63ba4688 + checksum: 97ea26af0c28a2ba568c9c65d06211487bbcd501cb4944f9d55e07fd2b00ad96653ea2cc9033f3d5b7dc1feda33e47ae9cc56b400191ea4533be213c9f82e67c languageName: node linkType: hard -"jest-jasmine2@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-jasmine2@npm:28.1.3" +"jest-leak-detector@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-leak-detector@npm:29.3.1" dependencies: - "@jest/environment": ^28.1.3 - "@jest/expect": ^28.1.3 - "@jest/source-map": ^28.1.2 - "@jest/test-result": ^28.1.3 - "@jest/types": ^28.1.3 - "@types/node": "*" - chalk: ^4.0.0 - co: ^4.6.0 - is-generator-fn: ^2.0.0 - jest-each: ^28.1.3 - jest-matcher-utils: ^28.1.3 - jest-message-util: ^28.1.3 - jest-runtime: ^28.1.3 - jest-snapshot: ^28.1.3 - jest-util: ^28.1.3 - p-limit: ^3.1.0 - pretty-format: ^28.1.3 - checksum: 1d2f13645108d31ef4f67dc99ac2ebc9c7b5855218843a2e035246cfd9c06c87962ff79d4aaecbd9ef325f1365fc8a76903f0b703000b5ada3bcc2ccd3284098 + jest-get-type: ^29.2.0 + pretty-format: ^29.3.1 + checksum: 0dd8ed31ae0b5a3d14f13f567ca8567f2663dd2d540d1e55511d3b3fd7f80a1d075392179674ebe9fab9be0b73678bf4d2f8bbbc0f4bdd52b9815259194da559 languageName: node linkType: hard -"jest-leak-detector@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-leak-detector@npm:28.1.3" - dependencies: - jest-get-type: ^28.0.2 - pretty-format: ^28.1.3 - checksum: 2e976a4880cf9af11f53a19f6a3820e0f90b635a900737a5427fc42e337d5628ba446dcd7c020ecea3806cf92bc0bbf6982ed62a9cd84e5a13d8751aa30fbbb7 - languageName: node - linkType: hard - -"jest-matcher-utils@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-matcher-utils@npm:28.1.3" +"jest-matcher-utils@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-matcher-utils@npm:29.3.1" dependencies: chalk: ^4.0.0 - jest-diff: ^28.1.3 - jest-get-type: ^28.0.2 - pretty-format: ^28.1.3 - checksum: 6b34f0cf66f6781e92e3bec97bf27796bd2ba31121e5c5997218d9adba6deea38a30df5203937d6785b68023ed95cbad73663cc9aad6fb0cb59aeb5813a58daf + jest-diff: ^29.3.1 + jest-get-type: ^29.2.0 + pretty-format: ^29.3.1 + checksum: 311e8d9f1e935216afc7dd8c6acf1fbda67a7415e1afb1bf72757213dfb025c1f2dc5e2c185c08064a35cdc1f2d8e40c57616666774ed1b03e57eb311c20ec77 languageName: node linkType: hard -"jest-message-util@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-message-util@npm:28.1.3" +"jest-message-util@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-message-util@npm:29.3.1" dependencies: "@babel/code-frame": ^7.12.13 - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 "@types/stack-utils": ^2.0.0 chalk: ^4.0.0 graceful-fs: ^4.2.9 micromatch: ^4.0.4 - pretty-format: ^28.1.3 + pretty-format: ^29.3.1 slash: ^3.0.0 stack-utils: ^2.0.3 - checksum: 1f266854166dcc6900d75a88b54a25225a2f3710d463063ff1c99021569045c35c7d58557b25447a17eb3a65ce763b2f9b25550248b468a9d4657db365f39e96 + checksum: 15d0a2fca3919eb4570bbf575734780c4b9e22de6aae903c4531b346699f7deba834c6c86fe6e9a83ad17fac0f7935511cf16dce4d71a93a71ebb25f18a6e07b languageName: node linkType: hard -"jest-mock@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-mock@npm:28.1.3" +"jest-mock@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-mock@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 "@types/node": "*" - checksum: a573bf8e5f12f4c29c661266c31b5c6b69a28d3195b83049983bce025b2b1a0152351567e89e63b102ef817034c2a3aa97eda4e776f3bae2aee54c5765573aa7 + jest-util: ^29.3.1 + checksum: 9098852cb2866db4a1a59f9f7581741dfc572f648e9e574a1b187fd69f5f2f6190ad387ede21e139a8b80a6a1343ecc3d6751cd2ae1ae11d7ea9fa1950390fb2 languageName: node linkType: hard @@ -5701,193 +5658,195 @@ __metadata: languageName: node linkType: hard -"jest-regex-util@npm:^28.0.2": - version: 28.0.2 - resolution: "jest-regex-util@npm:28.0.2" - checksum: 0ea8c5c82ec88bc85e273c0ec82e0c0f35f7a1e2d055070e50f0cc2a2177f848eec55f73e37ae0d045c3db5014c42b2f90ac62c1ab3fdb354d2abd66a9e08add +"jest-regex-util@npm:^29.2.0": + version: 29.2.0 + resolution: "jest-regex-util@npm:29.2.0" + checksum: 7c533e51c51230dac20c0d7395b19b8366cb022f7c6e08e6bcf2921626840ff90424af4c9b4689f02f0addfc9b071c4cd5f8f7a989298a4c8e0f9c94418ca1c3 languageName: node linkType: hard -"jest-resolve-dependencies@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-resolve-dependencies@npm:28.1.3" +"jest-resolve-dependencies@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-resolve-dependencies@npm:29.3.1" dependencies: - jest-regex-util: ^28.0.2 - jest-snapshot: ^28.1.3 - checksum: 4eea9ec33aefc1c71dc5956391efbcc7be76bda986b366ab3931d99c5f7ed01c9ebd7520e405ea2c76e1bb2c7ce504be6eca2b9831df16564d1e625500f3bfe7 + jest-regex-util: ^29.2.0 + jest-snapshot: ^29.3.1 + checksum: 6ec4727a87c6e7954e93de9949ab9967b340ee2f07626144c273355f05a2b65fa47eb8dece2d6e5f4fd99cdb893510a3540aa5e14ba443f70b3feb63f6f98982 languageName: node linkType: hard -"jest-resolve@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-resolve@npm:28.1.3" +"jest-resolve@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-resolve@npm:29.3.1" dependencies: chalk: ^4.0.0 graceful-fs: ^4.2.9 - jest-haste-map: ^28.1.3 + jest-haste-map: ^29.3.1 jest-pnp-resolver: ^1.2.2 - jest-util: ^28.1.3 - jest-validate: ^28.1.3 + jest-util: ^29.3.1 + jest-validate: ^29.3.1 resolve: ^1.20.0 resolve.exports: ^1.1.0 slash: ^3.0.0 - checksum: df61a490c93f4f4cf52135e43d6a4fcacb07b0b7d4acc6319e9289529c1d14f2d8e1638e095dbf96f156834802755e38db68caca69dba21a3261ee711d4426b6 + checksum: 0dea22ed625e07b8bfee52dea1391d3a4b453c1a0c627a0fa7c22e44bb48e1c289afe6f3c316def70753773f099c4e8f436c7a2cc12fcc6c7dd6da38cba2cd5f languageName: node linkType: hard -"jest-runner@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-runner@npm:28.1.3" +"jest-runner@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-runner@npm:29.3.1" dependencies: - "@jest/console": ^28.1.3 - "@jest/environment": ^28.1.3 - "@jest/test-result": ^28.1.3 - "@jest/transform": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/console": ^29.3.1 + "@jest/environment": ^29.3.1 + "@jest/test-result": ^29.3.1 + "@jest/transform": ^29.3.1 + "@jest/types": ^29.3.1 "@types/node": "*" chalk: ^4.0.0 - emittery: ^0.10.2 + emittery: ^0.13.1 graceful-fs: ^4.2.9 - jest-docblock: ^28.1.1 - jest-environment-node: ^28.1.3 - jest-haste-map: ^28.1.3 - jest-leak-detector: ^28.1.3 - jest-message-util: ^28.1.3 - jest-resolve: ^28.1.3 - jest-runtime: ^28.1.3 - jest-util: ^28.1.3 - jest-watcher: ^28.1.3 - jest-worker: ^28.1.3 + jest-docblock: ^29.2.0 + jest-environment-node: ^29.3.1 + jest-haste-map: ^29.3.1 + jest-leak-detector: ^29.3.1 + jest-message-util: ^29.3.1 + jest-resolve: ^29.3.1 + jest-runtime: ^29.3.1 + jest-util: ^29.3.1 + jest-watcher: ^29.3.1 + jest-worker: ^29.3.1 p-limit: ^3.1.0 source-map-support: 0.5.13 - checksum: 32405cd970fa6b11e039192dae699fd1bcc6f61f67d50605af81d193f24dd4373b25f5fcc1c571a028ec1b02174e8a4b6d0d608772063fb06f08a5105693533b + checksum: 61ad445d8a5f29573332f27a21fc942fb0d2a82bf901a0ea1035bf3bd7f349d1e425f71f54c3a3f89b292a54872c3248d395a2829d987f26b6025b15530ea5d2 languageName: node linkType: hard -"jest-runtime@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-runtime@npm:28.1.3" +"jest-runtime@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-runtime@npm:29.3.1" dependencies: - "@jest/environment": ^28.1.3 - "@jest/fake-timers": ^28.1.3 - "@jest/globals": ^28.1.3 - "@jest/source-map": ^28.1.2 - "@jest/test-result": ^28.1.3 - "@jest/transform": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/environment": ^29.3.1 + "@jest/fake-timers": ^29.3.1 + "@jest/globals": ^29.3.1 + "@jest/source-map": ^29.2.0 + "@jest/test-result": ^29.3.1 + "@jest/transform": ^29.3.1 + "@jest/types": ^29.3.1 + "@types/node": "*" chalk: ^4.0.0 cjs-module-lexer: ^1.0.0 collect-v8-coverage: ^1.0.0 - execa: ^5.0.0 glob: ^7.1.3 graceful-fs: ^4.2.9 - jest-haste-map: ^28.1.3 - jest-message-util: ^28.1.3 - jest-mock: ^28.1.3 - jest-regex-util: ^28.0.2 - jest-resolve: ^28.1.3 - jest-snapshot: ^28.1.3 - jest-util: ^28.1.3 + jest-haste-map: ^29.3.1 + jest-message-util: ^29.3.1 + jest-mock: ^29.3.1 + jest-regex-util: ^29.2.0 + jest-resolve: ^29.3.1 + jest-snapshot: ^29.3.1 + jest-util: ^29.3.1 slash: ^3.0.0 strip-bom: ^4.0.0 - checksum: b17c40af858e74dafa4f515ef3711c1e9ef3d4ad7d74534ee0745422534bc04fd166d4eceb62a3aa7dc951505d6f6d2a81d16e90bebb032be409ec0500974a36 + checksum: 82f27b48f000be074064a854e16e768f9453e9b791d8c5f9316606c37f871b5b10f70544c1b218ab9784f00bd972bb77f868c5ab6752c275be2cd219c351f5a7 languageName: node linkType: hard -"jest-snapshot@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-snapshot@npm:28.1.3" +"jest-snapshot@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-snapshot@npm:29.3.1" dependencies: "@babel/core": ^7.11.6 "@babel/generator": ^7.7.2 + "@babel/plugin-syntax-jsx": ^7.7.2 "@babel/plugin-syntax-typescript": ^7.7.2 "@babel/traverse": ^7.7.2 "@babel/types": ^7.3.3 - "@jest/expect-utils": ^28.1.3 - "@jest/transform": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/expect-utils": ^29.3.1 + "@jest/transform": ^29.3.1 + "@jest/types": ^29.3.1 "@types/babel__traverse": ^7.0.6 "@types/prettier": ^2.1.5 babel-preset-current-node-syntax: ^1.0.0 chalk: ^4.0.0 - expect: ^28.1.3 + expect: ^29.3.1 graceful-fs: ^4.2.9 - jest-diff: ^28.1.3 - jest-get-type: ^28.0.2 - jest-haste-map: ^28.1.3 - jest-matcher-utils: ^28.1.3 - jest-message-util: ^28.1.3 - jest-util: ^28.1.3 + jest-diff: ^29.3.1 + jest-get-type: ^29.2.0 + jest-haste-map: ^29.3.1 + jest-matcher-utils: ^29.3.1 + jest-message-util: ^29.3.1 + jest-util: ^29.3.1 natural-compare: ^1.4.0 - pretty-format: ^28.1.3 + pretty-format: ^29.3.1 semver: ^7.3.5 - checksum: 2a46a5493f1fb50b0a236a21f25045e7f46a244f9f3ae37ef4fbcd40249d0d68bb20c950ce77439e4e2cac985b05c3061c90b34739bf6069913a1199c8c716e1 + checksum: d7d0077935e78c353c828be78ccb092e12ba7622cb0577f21641fadd728ae63a7c1f4a0d8113bfb38db3453a64bfa232fb1cdeefe0e2b48c52ef4065b0ab75ae languageName: node linkType: hard -"jest-util@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-util@npm:28.1.3" +"jest-util@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-util@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 "@types/node": "*" chalk: ^4.0.0 ci-info: ^3.2.0 graceful-fs: ^4.2.9 picomatch: ^2.2.3 - checksum: fd6459742c941f070223f25e38a2ac0719aad92561591e9fb2a50d602a5d19d754750b79b4074327a42b00055662b95da3b006542ceb8b54309da44d4a62e721 + checksum: f67c60f062b94d21cb60e84b3b812d64b7bfa81fe980151de5c17a74eb666042d0134e2e756d099b7606a1fcf1d633824d2e58197d01d76dde1e2dc00dfcd413 languageName: node linkType: hard -"jest-validate@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-validate@npm:28.1.3" +"jest-validate@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-validate@npm:29.3.1" dependencies: - "@jest/types": ^28.1.3 + "@jest/types": ^29.3.1 camelcase: ^6.2.0 chalk: ^4.0.0 - jest-get-type: ^28.0.2 + jest-get-type: ^29.2.0 leven: ^3.1.0 - pretty-format: ^28.1.3 - checksum: 95e0513b3803c3372a145cda86edbdb33d9dfeaa18818176f2d581e821548ceac9a179f065b6d4671a941de211354efd67f1fff8789a4fb89962565c85f646db + pretty-format: ^29.3.1 + checksum: 92584f0b8ac284235f12b3b812ccbc43ef6dea080a3b98b1aa81adbe009e962d0aa6131f21c8157b30ac3d58f335961694238a93d553d1d1e02ab264c923778c languageName: node linkType: hard -"jest-watcher@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-watcher@npm:28.1.3" +"jest-watcher@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-watcher@npm:29.3.1" dependencies: - "@jest/test-result": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/test-result": ^29.3.1 + "@jest/types": ^29.3.1 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 - emittery: ^0.10.2 - jest-util: ^28.1.3 + emittery: ^0.13.1 + jest-util: ^29.3.1 string-length: ^4.0.1 - checksum: 8f6d674a4865e7df251f71544f1b51f06fd36b5a3a61f2ac81aeb81fa2a196be354fba51d0f97911c88f67cd254583b3a22ee124bf2c5b6ee2fadec27356c207 + checksum: 60d189473486c73e9d540406a30189da5a3c67bfb0fb4ad4a83991c189135ef76d929ec99284ca5a505fe4ee9349ae3c99b54d2e00363e72837b46e77dec9642 languageName: node linkType: hard -"jest-worker@npm:^28.1.3": - version: 28.1.3 - resolution: "jest-worker@npm:28.1.3" +"jest-worker@npm:^29.3.1": + version: 29.3.1 + resolution: "jest-worker@npm:29.3.1" dependencies: "@types/node": "*" + jest-util: ^29.3.1 merge-stream: ^2.0.0 supports-color: ^8.0.0 - checksum: e921c9a1b8f0909da9ea07dbf3592f95b653aef3a8bb0cbcd20fc7f9a795a1304adecac31eecb308992c167e8d7e75c522061fec38a5928ace0f9571c90169ca + checksum: 38687fcbdc2b7ddc70bbb5dfc703ae095b46b3c7f206d62ecdf5f4d16e336178e217302138f3b906125576bb1cfe4cfe8d43681276fa5899d138ed9422099fb3 languageName: node linkType: hard -"jest@npm:^28.1.3": - version: 28.1.3 - resolution: "jest@npm:28.1.3" +"jest@npm:^29.3.1": + version: 29.3.1 + resolution: "jest@npm:29.3.1" dependencies: - "@jest/core": ^28.1.3 - "@jest/types": ^28.1.3 + "@jest/core": ^29.3.1 + "@jest/types": ^29.3.1 import-local: ^3.0.2 - jest-cli: ^28.1.3 + jest-cli: ^29.3.1 peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -5895,7 +5854,7 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: b9dcb542eb7c16261c281cdc2bf37155dbb3f1205bae0b567f05051db362c85ddd4b765f126591efb88f6d298eb10336d0aa6c7d5373b4d53f918137a9a70182 + checksum: 613f4ec657b14dd84c0056b2fef1468502927fd551bef0b19d4a91576a609678fb316c6a5b5fc6120dd30dd4ff4569070ffef3cb507db9bb0260b28ddaa18d7a languageName: node linkType: hard @@ -6762,13 +6721,6 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:^1.0.0": - version: 1.0.0 - resolution: "node-domexception@npm:1.0.0" - checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f - languageName: node - linkType: hard - "node-emoji@npm:^1.11.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" @@ -6792,17 +6744,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^3.3.0": - version: 3.3.0 - resolution: "node-fetch@npm:3.3.0" - dependencies: - data-uri-to-buffer: ^4.0.0 - fetch-blob: ^3.1.4 - formdata-polyfill: ^4.0.10 - checksum: e9936908d2783d3c48a038e187f8062de294d75ef43ec8ab812d7cbd682be2b67605868758d2e9cad6103706dcfe4a9d21d78f6df984e8edf10e7a5ce2e665f8 - languageName: node - linkType: hard - "node-gyp@npm:^9.0.0, node-gyp@npm:^9.1.0, node-gyp@npm:latest": version: 9.3.0 resolution: "node-gyp@npm:9.3.0" @@ -7479,8 +7420,8 @@ __metadata: linkType: hard "pino@npm:^8.5.0": - version: 8.7.0 - resolution: "pino@npm:8.7.0" + version: 8.8.0 + resolution: "pino@npm:8.8.0" dependencies: atomic-sleep: ^1.0.0 fast-redact: ^3.1.1 @@ -7495,7 +7436,7 @@ __metadata: thread-stream: ^2.0.0 bin: pino: bin.js - checksum: 4aa2e320aa88f4a90fd25884ee4e3b9ef7963b3c59c514f3693b5a5c987b112cf3ab4e39a8c51efe32c861f5c058d7cfa7fcda59d964ed878f842fdbc6ab2876 + checksum: 69256469221b332776333069d637100053eb15dc7baa3f95897d7864bf2e6fd99f15cfd9cfc94b56e7b00da2e6b349c49affcff1abaca9a3d4b7ba63ff86b4f0 languageName: node linkType: hard @@ -7551,15 +7492,14 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^28.0.0, pretty-format@npm:^28.1.3": - version: 28.1.3 - resolution: "pretty-format@npm:28.1.3" +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.3.1": + version: 29.3.1 + resolution: "pretty-format@npm:29.3.1" dependencies: - "@jest/schemas": ^28.1.3 - ansi-regex: ^5.0.1 + "@jest/schemas": ^29.0.0 ansi-styles: ^5.0.0 react-is: ^18.0.0 - checksum: e69f857358a3e03d271252d7524bec758c35e44680287f36c1cb905187fbc82da9981a6eb07edfd8a03bc3cbeebfa6f5234c13a3d5b59f2bbdf9b4c4053e0a7f + checksum: 9917a0bb859cd7a24a343363f70d5222402c86d10eb45bcc2f77b23a4e67586257390e959061aec22762a782fe6bafb59bf34eb94527bc2e5d211afdb287eb4e languageName: node linkType: hard @@ -8405,11 +8345,11 @@ __metadata: linkType: hard "sonic-boom@npm:^3.1.0": - version: 3.2.0 - resolution: "sonic-boom@npm:3.2.0" + version: 3.2.1 + resolution: "sonic-boom@npm:3.2.1" dependencies: atomic-sleep: ^1.0.0 - checksum: 526669b78e0ac3bcbe2a53e5ac8960d3b25e61d8e6a46eaed5a0c46d7212c5f638bb136236870babedfcb626063711ba8f81e538f88b79e6a90a5b2ff71943b4 + checksum: 674d0af31c67818c99a0956482720b853bdaa6e46a63814db9fd28024580836a4cfdafa2c2ba6b0ec08449e9a92a5e7959530689b2c1fb5c83f786708cd7a7da languageName: node linkType: hard @@ -8676,7 +8616,7 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.2.0": +"supports-hyperlinks@npm:^2.2.0": version: 2.3.0 resolution: "supports-hyperlinks@npm:2.3.0" dependencies: @@ -8727,16 +8667,6 @@ __metadata: languageName: node linkType: hard -"terminal-link@npm:^2.0.0": - version: 2.1.1 - resolution: "terminal-link@npm:2.1.1" - dependencies: - ansi-escapes: ^4.2.1 - supports-hyperlinks: ^2.0.0 - checksum: ce3d2cd3a438c4a9453947aa664581519173ea40e77e2534d08c088ee6dda449eabdbe0a76d2a516b8b73c33262fedd10d5270ccf7576ae316e3db170ce6562f - languageName: node - linkType: hard - "terser@npm:^5.15.1": version: 5.16.1 resolution: "terser@npm:5.16.1" @@ -9238,13 +9168,6 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^3.0.3": - version: 3.2.1 - resolution: "web-streams-polyfill@npm:3.2.1" - checksum: b119c78574b6d65935e35098c2afdcd752b84268e18746606af149e3c424e15621b6f1ff0b42b2676dc012fc4f0d313f964b41a4b5031e525faa03997457da02 - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1"