graphql#ExecutionArgs TypeScript Examples

The following examples show how to use graphql#ExecutionArgs. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: handler.ts    From graphql-sse with MIT License 4 votes vote down vote up
/**
 * 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.
 *
 * @category Server
 */
export function createHandler<
  Request extends IncomingMessage = IncomingMessage,
  Response extends ServerResponse = ServerResponse,
>(options: HandlerOptions<Request, Response>): Handler<Request, Response> {
  const {
    schema,
    context,
    validate = graphqlValidate,
    execute = graphqlExecute,
    subscribe = graphqlSubscribe,
    authenticate = function extractOrCreateStreamToken(req) {
      const headerToken =
        req.headers[TOKEN_HEADER_KEY] || req.headers['x-graphql-stream-token']; // @deprecated >v1.0.0
      if (headerToken)
        return Array.isArray(headerToken) ? headerToken.join('') : headerToken;

      const urlToken = new URL(
        req.url ?? '',
        'http://localhost/',
      ).searchParams.get(TOKEN_QUERY_KEY);
      if (urlToken) return urlToken;

      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
        const r = (Math.random() * 16) | 0,
          v = c == 'x' ? r : (r & 0x3) | 0x8;
        return v.toString(16);
      });
    },
    onConnecting,
    onConnected,
    onSubscribe,
    onOperation,
    onNext,
    onComplete,
    onDisconnect,
  } = options;

  const streams: Record<string, Stream> = {};

  function createStream(token: string | null): Stream<Request, Response> {
    let request: Request | null = null,
      response: Response | null = null,
      pinger: ReturnType<typeof setInterval>,
      disposed = false;
    const pendingMsgs: string[] = [];
    const ops: Record<
      string,
      AsyncGenerator<unknown> | AsyncIterable<unknown> | null
    > = {};

    function write(msg: unknown) {
      return new Promise<boolean>((resolve, reject) => {
        if (disposed || !response || !response.writable) return resolve(false);
        response.write(msg, (err) => {
          if (err) return reject(err);
          resolve(true);
        });
      });
    }

    async function emit<E extends StreamEvent>(
      event: E,
      data: StreamData<E> | StreamDataForID<E>,
    ): Promise<void> {
      let msg = `event: ${event}`;
      if (data) msg += `\ndata: ${JSON.stringify(data)}`;
      msg += '\n\n';

      const wrote = await write(msg);
      if (!wrote) pendingMsgs.push(msg);
    }

    async function dispose() {
      if (disposed) return;
      disposed = true;

      // 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);
      }

      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      response!.end(); // response must exist at this point
      response = null;
      clearInterval(pinger);

      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      onDisconnect?.(request!); // request must exist at this point
      request = null;
    }

    return {
      get open() {
        return disposed || Boolean(response);
      },
      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');
        res.flushHeaders();

        // write an empty message because some browsers (like Firefox and Safari)
        // dont accept the header flush
        await write(':\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');
        }

        await onConnected?.(req);
      },
      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,
            );
          }
        } 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);

        // 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?.(operationReq, args);
      },
    };
  }

  async function prepare(
    req: Request,
    res: Response,
    params: RequestParams,
  ): Promise<[args: ExecutionArgs, perform: () => OperationResult] | void> {
    let args: ExecutionArgs, operation: OperationTypeNode;

    const maybeExecArgs = await onSubscribe?.(req, res, params);
    if (maybeExecArgs) args = maybeExecArgs;
    else {
      // you either provide a schema dynamically through
      // `onSubscribe` or you set one up during the server setup
      if (!schema) throw new Error('The GraphQL schema is not provided');

      const { operationName, variables } = params;
      let { query } = params;

      if (typeof query === 'string') {
        try {
          query = parse(query);
        } catch {
          res.writeHead(400, 'GraphQL query syntax error').end();
          return;
        }
      }

      const argsWithoutSchema = {
        operationName,
        document: query,
        variableValues: variables,
      };
      args = {
        ...argsWithoutSchema,
        schema:
          typeof schema === 'function'
            ? await schema(req, argsWithoutSchema)
            : schema,
      };
    }

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

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

    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') {
        // 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 { errors: validationErrs };
          },
        ];
      }

      res
        .writeHead(400, {
          'Content-Type':
            req.headers.accept === 'application/json'
              ? 'application/json; charset=utf-8'
              : 'application/graphql+json; charset=utf-8',
        })
        .write(JSON.stringify({ errors: validationErrs }));
      res.end();
      return;
    }

    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 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');

    const accept = req.headers.accept ?? '*/*';

    const stream = streams[token];

    if (accept === 'text/event-stream') {
      // 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 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
        }

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

      // open stream cant exist, only one per token is allowed
      if (stream.open) {
        res.writeHead(409, 'Stream already open').end();
        return;
      }

      await onConnecting?.(req, res);
      if (res.writableEnded) return;
      await stream.use(req, res);
      return;
    }

    if (req.method === 'PUT') {
      // method PUT prepares a stream for future incoming connections.

      if (!['*/*', 'text/plain'].includes(accept)) {
        res.writeHead(406).end();
        return;
      }

      // streams mustnt exist if putting new one
      if (stream) {
        res.writeHead(409, 'Stream already registered').end();
        return;
      }

      streams[token] = createStream(token);
      res
        .writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' })
        .write(token);
      res.end();
      return;
    } 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;
      }

      const opId = new URL(req.url ?? '', 'http://localhost/').searchParams.get(
        'operationId',
      );
      if (!opId) {
        res.writeHead(400, 'Operation ID is missing').end();
        return;
      }

      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;
    } else if (req.method !== 'GET' && req.method !== 'POST') {
      // only POSTs and GETs are accepted at this point
      res.writeHead(405, undefined, { Allow: 'GET, POST, PUT, DELETE' }).end();
      return;
    } else if (!stream) {
      // for all other requests, streams must exist to attach the result onto
      res.writeHead(404, 'Stream not found').end();
      return;
    }

    if (
      !['*/*', 'application/graphql+json', 'application/json'].includes(accept)
    ) {
      res.writeHead(406).end();
      return;
    }

    let params;
    try {
      params = await parseReq(req, body);
    } catch (err) {
      res.writeHead(400, err.message).end();
      return;
    }

    const opId = String(params.extensions?.operationId ?? '');
    if (!opId) {
      res.writeHead(400, 'Operation ID is missing').end();
      return;
    }
    if (opId in stream.ops) {
      res.writeHead(409, 'Operation with ID already exists').end();
      return;
    }
    // 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;

    // operation might have completed before prepared
    if (!(opId in stream.ops)) {
      res.writeHead(204).end();
      return;
    }

    const result = await perform();
    if (res.writableEnded) {
      if (isAsyncGenerator(result)) result.return(undefined);
      delete stream.ops[opId];
      return; // `onOperation` responded
    }

    // operation might have completed before performed
    if (!(opId in stream.ops)) {
      if (isAsyncGenerator(result)) result.return(undefined);
      res.writeHead(204).end();
      return;
    }

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