Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
import { ExecutionResult } from 'graphql';
import * as sha256 from 'hash.js/lib/hash/sha/256';

import { HTTPCache } from 'apollo-datasource-rest';
import { CacheControlExtensionOptions } from 'apollo-cache-control';

import { omit } from 'lodash';

import { runQuery, QueryOptions } from './runQuery';
import {
  default as GraphQLOptions,
  resolveGraphqlOptions,
} from './graphqlOptions';
import {
  formatApolloErrors,
  PersistedQueryNotSupportedError,
  PersistedQueryNotFoundError,
} from 'apollo-server-errors';
import { calculateCacheControlHeaders, HttpHeaderCalculation } from './caching';

export interface HttpQueryRequest {
  method: string;
  // query is either the POST body or the GET query string map.  In the GET
  // case, all values are strings and need to be parsed as JSON; in the POST
  // case they should already be parsed. query has keys like 'query' (whose
  // value should always be a string), 'variables', 'operationName',
  // 'extensions', etc.
  query: Record<string, any> | Array<Record<string, any>>;
  options:
    | GraphQLOptions
    | ((...args: Array<any>) => Promise<GraphQLOptions> | GraphQLOptions);
  request: Pick<Request, 'url' | 'method' | 'headers'>;
}

// The result of a curl does not appear well in the terminal, so we add an extra new line
function prettyJSONStringify(toStringfy) {
  return JSON.stringify(toStringfy) + '\n';
}

export interface ApolloServerHttpResponse {
  headers?: Record<string, string>;
  // ResponseInit contains the follow, which we do not use
  // status?: number;
  // statusText?: string;
}

export interface HttpQueryResponse {
  graphqlResponse: string;
  responseInit: ApolloServerHttpResponse;
}

export class HttpQueryError extends Error {
  public statusCode: number;
  public isGraphQLError: boolean;
  public headers: { [key: string]: string };

  constructor(
    statusCode: number,
    message: string,
    isGraphQLError: boolean = false,
    headers?: { [key: string]: string },
  ) {
    super(message);
    this.name = 'HttpQueryError';
    this.statusCode = statusCode;
    this.isGraphQLError = isGraphQLError;
    this.headers = headers;
  }
}

function throwHttpGraphQLError(
  statusCode,
  errors: Array<Error>,
  optionsObject,
) {
  throw new HttpQueryError(
    statusCode,
    prettyJSONStringify({
      errors: formatApolloErrors(errors, {
        debug: optionsObject.debug,
        formatter: optionsObject.formatError,
      }),
    }),
    true,
    {
      'Content-Type': 'application/json',
    },
  );
}

export async function runHttpQuery(
  handlerArguments: Array<any>,
  request: HttpQueryRequest,
): Promise<HttpQueryResponse> {
  let isGetRequest: boolean = false;
  let optionsObject: GraphQLOptions;
  const debugDefault =
    process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
  let cacheControl: CacheControlExtensionOptions & {
    calculateHttpHeaders: boolean | HttpHeaderCalculation;
    stripFormattedExtensions: boolean;
  };

  try {
    optionsObject = await resolveGraphqlOptions(
      request.options,
      ...handlerArguments,
    );
  } catch (e) {
    // The options can be generated asynchronously, so we don't have access to
    // the normal options provided by the user, such as: formatError,
    // debug. Therefore, we need to do some unnatural things, such
    // as use NODE_ENV to determine the debug settings
    e.message = `Invalid options provided to ApolloServer: ${e.message}`;
    if (!debugDefault) {
      e.warning = `To remove the stacktrace, set the NODE_ENV environment variable to production if the options creation can fail`;
    }
    throwHttpGraphQLError(500, [e], { debug: debugDefault });
  }
  if (optionsObject.debug === undefined) {
    optionsObject.debug = debugDefault;
  }
  let requestPayload;

  switch (request.method) {
    case 'POST':
      if (!request.query || Object.keys(request.query).length === 0) {
        throw new HttpQueryError(
          500,
          'POST body missing. Did you forget use body-parser middleware?',
        );
      }

      requestPayload = request.query;
      break;
    case 'GET':
      if (!request.query || Object.keys(request.query).length === 0) {
        throw new HttpQueryError(400, 'GET query missing.');
      }

      isGetRequest = true;
      requestPayload = request.query;
      break;

    default:
      throw new HttpQueryError(
        405,
        'Apollo Server supports only GET/POST requests.',
        false,
        {
          Allow: 'GET, POST',
        },
      );
  }

  let isBatch = true;
  // TODO: do something different here if the body is an array.
  // Throw an error if body isn't either array or object.
  if (!Array.isArray(requestPayload)) {
    isBatch = false;
    requestPayload = [requestPayload];
  }

  const requests = requestPayload.map(async requestParams => {
    try {
      let queryString: string | undefined = requestParams.query;
      let extensions = requestParams.extensions;
      let persistedQueryHit = false;
      let persistedQueryRegister = false;

      if (isGetRequest && extensions) {
        // For GET requests, we have to JSON-parse extensions. (For POST
        // requests they get parsed as part of parsing the larger body they're
        // inside.)
        try {
          extensions = JSON.parse(extensions);
        } catch (error) {
          throw new HttpQueryError(400, 'Extensions are invalid JSON.');
        }
      }

      if (extensions && extensions.persistedQuery) {
        // It looks like we've received an Apollo Persisted Query. Check if we
        // support them. In an ideal world, we always would, however since the
        // middleware options are created every request, it does not make sense
        // to create a default cache here and save a referrence to use across
        // requests
        if (
          !optionsObject.persistedQueries ||
          !optionsObject.persistedQueries.cache
        ) {
          if (isBatch) {
            // A batch can contain another query that returns data,
            // so we don't error out the entire request with an HttpError
            throw new PersistedQueryNotSupportedError();
          }
          // Return 200 to simplify processing: we want this to be intepreted by
          // the client as data worth interpreting, not an error.
          throwHttpGraphQLError(
            200,
            [new PersistedQueryNotSupportedError()],
            optionsObject,
          );
        } else if (extensions.persistedQuery.version !== 1) {
          throw new HttpQueryError(400, 'Unsupported persisted query version');
        }

        const sha = extensions.persistedQuery.sha256Hash;

        if (queryString === undefined) {
          queryString = await optionsObject.persistedQueries.cache.get(sha);
          if (queryString) {
            persistedQueryHit = true;
          } else {
            if (isBatch) {
              // A batch can contain multiple undefined persisted queries,
              // so we don't error out the entire request with an HttpError
              throw new PersistedQueryNotFoundError();
            }
            throwHttpGraphQLError(
              200,
              [new PersistedQueryNotFoundError()],
              optionsObject,
            );
          }
        } else {
          const calculatedSha = sha256()
            .update(queryString)
            .digest('hex');
          if (sha !== calculatedSha) {
            throw new HttpQueryError(400, 'provided sha does not match query');
          }
          persistedQueryRegister = true;

          // Do the store completely asynchronously
          Promise.resolve()
            .then(() => {
              // We do not wait on the cache storage to complete
              return optionsObject.persistedQueries.cache.set(sha, queryString);
            })
            .catch(error => {
              console.warn(error);
            });
        }
      }

      // We ensure that there is a queryString or parsedQuery after formatParams
      if (queryString && typeof queryString !== 'string') {
        // Check for a common error first.
        if (queryString && (queryString as any).kind === 'Document') {
          throw new HttpQueryError(
            400,
            "GraphQL queries must be strings. It looks like you're sending the " +
              'internal graphql-js representation of a parsed query in your ' +
              'request instead of a request in the GraphQL query language. You ' +
              'can convert an AST to a string using the `print` function from ' +
              '`graphql`, or use a client like `apollo-client` which converts ' +
              'the internal representation to a string for you.',
          );
        }
        throw new HttpQueryError(400, 'GraphQL queries must be strings.');
      }

      // GET operations should only be queries (not mutations). We want to throw
      // a particular HTTP error in that case, but we don't actually parse the
      // query until we're in runQuery, so we declare the error we want to throw
      // here and pass it into runQuery.
      // TODO this could/should be added as a validation rule rather than an ad hoc error
      let nonQueryError;
      if (isGetRequest) {
        nonQueryError = new HttpQueryError(
          405,
          `GET supports only query operation`,
          false,
          {
            Allow: 'POST',
          },
        );
      }

      const operationName = requestParams.operationName;

      let variables = requestParams.variables;
      if (typeof variables === 'string') {
        try {
          // XXX Really we should only do this for GET requests, but for
          // compatibility reasons we'll keep doing this at least for now for
          // broken clients that ship variables in a string for no good reason.
          variables = JSON.parse(variables);
        } catch (error) {
          throw new HttpQueryError(400, 'Variables are invalid JSON.');
        }
      }

      let context = optionsObject.context;
      if (!context) {
        // appease typescript compiler, otherwise could use || {}
        context = {};
      } else if (typeof context === 'function') {
        try {
          context = await context();
        } catch (e) {
          e.message = `Context creation failed: ${e.message}`;
          throwHttpGraphQLError(500, [e], optionsObject);
        }
      } else if (isBatch) {
        context = Object.assign(
          Object.create(Object.getPrototypeOf(context)),
          context,
        );
      }

      if (optionsObject.dataSources) {
        const dataSources = optionsObject.dataSources() || {};

        // we use the cache provided to the request and add the Http semantics on top
        const httpCache = new HTTPCache(optionsObject.cache);

        for (const dataSource of Object.values(dataSources)) {
          dataSource.context = context;
          dataSource.httpCache = httpCache;
        }

        if ('dataSources' in context) {
          throw new Error(
            'Please use the dataSources config option instead of putting dataSources on the context yourself.',
          );
        }

        (context as any).dataSources = dataSources;
      }

      if (optionsObject.cacheControl !== false) {
        if (
          typeof optionsObject.cacheControl === 'boolean' &&
          optionsObject.cacheControl === true
        ) {
          // cacheControl: true means that the user needs the cache-control
          // extensions. This means we are running the proxy, so we should not
          // strip out the cache control extension and not add cache-control headers
          cacheControl = {
            stripFormattedExtensions: false,
            calculateHttpHeaders: false,
            defaultMaxAge: 0,
          };
        } else {
          // Default behavior is to run default header calculation and return
          // no cacheControl extensions
          cacheControl = {
            stripFormattedExtensions: true,
            calculateHttpHeaders: true,
            defaultMaxAge: 0,
            ...optionsObject.cacheControl,
          };
        }
      }

      let params: QueryOptions = {
        schema: optionsObject.schema,
        queryString,
        nonQueryError,
        variables: variables,
        context,
        rootValue: optionsObject.rootValue,
        operationName: operationName,
        validationRules: optionsObject.validationRules,
        formatError: optionsObject.formatError,
        formatResponse: optionsObject.formatResponse,
        fieldResolver: optionsObject.fieldResolver,
        debug: optionsObject.debug,
        tracing: optionsObject.tracing,
        cacheControl: cacheControl
          ? omit(cacheControl, [
              'calculateHttpHeaders',
              'stripFormattedExtensions',
            ])
          : false,
        request: request.request,
        extensions: optionsObject.extensions,
        persistedQueryHit,
        persistedQueryRegister,
      };

      if (optionsObject.formatParams) {
        params = optionsObject.formatParams(params);
      }

      if (!params.queryString && !params.parsedQuery) {
        // Note that we've already thrown a different error if it looks like APQ.
        throw new HttpQueryError(400, 'Must provide query string.');
      }

      // @@fork
      // could also add response
      context.request = request
      context.httpRequest = request.request
      // @@fork
      // console.log('[APOLLO] ABOUT_TO_DO_RUN_QUERY, HERE_ARE_PARAMS', params)
      console.log('HEADERS', request.request.headers)


      return runQuery(params);
    } catch (e) {
      // Populate any HttpQueryError to our handler which should
      // convert it to Http Error.
      if (e.name === 'HttpQueryError') {
        // async function wraps this in a Promise
        throw e;
      }

      return {
        errors: formatApolloErrors([e], {
          formatter: optionsObject.formatError,
          debug: optionsObject.debug,
        }),
      };
    }
  }) as Array<Promise<ExecutionResult & { extensions?: Record<string, any> }>>;

  const responses = await Promise.all(requests);

  const responseInit: ApolloServerHttpResponse = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  if (cacheControl) {
    if (cacheControl.calculateHttpHeaders) {
      const calculatedHeaders =
        typeof cacheControl.calculateHttpHeaders === 'function'
          ? cacheControl.calculateHttpHeaders(responses)
          : calculateCacheControlHeaders(responses);

      responseInit.headers = {
        ...responseInit.headers,
        ...calculatedHeaders,
      };
    }

    if (cacheControl.stripFormattedExtensions) {
      responses.forEach(response => {
        if (response.extensions) {
          delete response.extensions.cacheControl;
          if (Object.keys(response.extensions).length === 0) {
            delete response.extensions;
          }
        }
      });
    }
  }

  if (!isBatch) {
    const graphqlResponse = responses[0];
    // This code is run on parse/validation errors and any other error that
    // doesn't reach GraphQL execution
    if (graphqlResponse.errors && typeof graphqlResponse.data === 'undefined') {
      throwHttpGraphQLError(400, graphqlResponse.errors as any, optionsObject);
    }
    const stringified = prettyJSONStringify(graphqlResponse);

    responseInit['Content-Length'] = Buffer.byteLength(
      stringified,
      'utf8',
    ).toString();

    return {
      graphqlResponse: stringified,
      responseInit,
    };
  }

  const stringified = prettyJSONStringify(responses);

  responseInit['Content-Length'] = Buffer.byteLength(
    stringified,
    'utf8',
  ).toString();

  return {
    graphqlResponse: stringified,
    responseInit,
  };
}