graphql#GraphQLError TypeScript Examples

The following examples show how to use graphql#GraphQLError. 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: server-utils.ts    From anthem with Apache License 2.0 7 votes vote down vote up
requestErrorHandler = (error: GraphQLError): GraphQLError => {
  if (ENV.ENABLE_LOGGING) {
    console.log(chalk.red("Request Failed:"));
    console.log(chalk.red(`- ${JSON.stringify(error)}\n`));
  }

  // Report error to Sentry
  Sentry.captureException(error);

  return error;
}
Example #2
Source File: global-id.scalar.ts    From nestjs-relay with MIT License 6 votes vote down vote up
parseLiteral(ast: ValueNode): ResolvedGlobalId {
    if (ast.kind !== Kind.STRING) {
      throw new GraphQLError(`Invalid ID type: ${ast.kind}`);
    }
    const { id, type } = fromGlobalId(ast.value);
    if (!id || !type) {
      throw new GraphQLError(`Invalid ID: ${ast.value}`);
    }
    return new ResolvedGlobalId({ type, id });
  }
Example #3
Source File: apolloConfig.ts    From liferay-grow with MIT License 6 votes vote down vote up
private formatError(error: GraphQLError) {
    const { message, path } = error;

    logger.error(
      `Message: ${message.toUpperCase()} / On Path: ${JSON.stringify(path)}`,
    );

    return error;
  }
Example #4
Source File: index.ts    From hono with MIT License 6 votes vote down vote up
errorMessages = (
  messages: string[],
  graphqlErrors?: readonly GraphQLError[] | readonly GraphQLFormattedError[]
) => {
  if (graphqlErrors) {
    return {
      errors: graphqlErrors,
    }
  }

  return {
    errors: messages.map((message) => {
      return {
        message: message,
      }
    }),
  }
}
Example #5
Source File: validation.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function NoTypenameAlias(context: ValidationContext) {
  return {
    Field(node: FieldNode) {
      const aliasName = node.alias && node.alias.value;
      if (aliasName == '__typename') {
        context.reportError(
          new GraphQLError('Apollo needs to be able to insert __typename when needed, please do not use it as an alias', [node])
        );
      }
    },
  };
}
Example #6
Source File: validation.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function NoAnonymousQueries(context: ValidationContext) {
  return {
    OperationDefinition(node: OperationDefinitionNode) {
      if (!node.name) {
        context.reportError(new GraphQLError('Apollo does not support anonymous operations', [node]));
      }
      return false;
    },
  };
}
Example #7
Source File: apollo-server.ts    From clean-ts-api with GNU General Public License v3.0 6 votes vote down vote up
handleErrors = (response: any, errors: readonly GraphQLError[]): void => {
  errors?.forEach(error => {
    response.data = undefined
    if (checkError(error, 'UserInputError')) {
      response.http.status = 400
    } else if (checkError(error, 'AuthenticationError')) {
      response.http.status = 401
    } else if (checkError(error, 'ForbiddenError')) {
      response.http.status = 403
    } else {
      response.http.status = 500
    }
  })
}
Example #8
Source File: graphql.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
// Utility functions extracted from graphql-js

/**
 * Extracts the root type of the operation from the schema.
 */
export function getOperationRootType(schema: GraphQLSchema, operation: OperationDefinitionNode) {
  switch (operation.operation) {
    case 'query':
      return schema.getQueryType();
    case 'mutation':
      const mutationType = schema.getMutationType();
      if (!mutationType) {
        throw new GraphQLError('Schema is not configured for mutations', [operation]);
      }
      return mutationType;
    case 'subscription':
      const subscriptionType = schema.getSubscriptionType();
      if (!subscriptionType) {
        throw new GraphQLError('Schema is not configured for subscriptions', [operation]);
      }
      return subscriptionType;
    default:
      throw new GraphQLError('Can only compile queries, mutations and subscriptions', [operation]);
  }
}
Example #9
Source File: codeGeneration.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function interfaceNameFromOperation({ operationName, operationType }: { operationName: string; operationType: string }) {
  switch (operationType) {
    case 'query':
      return `${operationName}Query`;
      break;
    case 'mutation':
      return `${operationName}Mutation`;
      break;
    case 'subscription':
      return `${operationName}Subscription`;
      break;
    default:
      throw new GraphQLError(`Unsupported operation type "${operationType}"`);
  }
}
Example #10
Source File: errors.ts    From amplify-codegen with Apache License 2.0 6 votes vote down vote up
export function logError(error: Error) {
  if (error instanceof ToolError) {
    logErrorMessage(error.message);
  } else if (error instanceof GraphQLError) {
    const fileName = error.source && error.source.name;
    if (error.locations) {
      for (const location of error.locations) {
        logErrorMessage(error.message, fileName, location.line);
      }
    } else {
      logErrorMessage(error.message, fileName);
    }
  } else {
    console.log(error.stack);
  }
}
Example #11
Source File: graphql-upload.ts    From relate with GNU General Public License v3.0 5 votes vote down vote up
GraphQLUpload = new GraphQLScalarType({
    name: 'Upload',
    description: 'The `Upload` scalar type represents a file upload.',
    async parseValue(value: Promise<IFileUpload>): Promise<IFileUpload> {
        const upload = await value;

        // if we are coming from the sofa REST API the file has already been flushed to disk
        if (upload.path) {
            const uploadedFilePath = upload.path;

            return {
                ...upload,
                createReadStream: () => fse.createReadStream(uploadedFilePath),
            };
        }

        const stream = upload.createReadStream();
        const fileType = await FileType.fromStream(stream);

        // if the file is, e.g .cypher (application/octet-stream) or .txt (text/plain)
        // fileType returns undefined but we want to allow these
        // assume undefined in this case means its some sort of text stream
        if (fileType && fileType?.mime !== upload.mimetype) {
            throw new GraphQLError('Mime type does not match file content.');
        }

        return upload;
    },
    parseLiteral(ast): void {
        throw new GraphQLError('Upload literal unsupported.', ast);
    },
    serialize(value: IFileUpload): any {
        // sofa-api calls .serialize() on GraphQL scalars
        return {
            filename: value.filename,
            mimetype: value.mimetype,
            encoding: value.encoding,
            path: value.path,
        };
    },
})
Example #12
Source File: parser.ts    From graphql-eslint with MIT License 5 votes vote down vote up
export function parseForESLint(code: string, options: ParserOptions = {}): GraphQLESLintParseResult {
  try {
    const filePath = options.filePath || '';
    const realFilepath = filePath && getOnDiskFilepath(filePath);

    const gqlConfig = loadGraphQLConfig(options);
    const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();

    const schema = getSchema(projectForFile, options);
    const siblingOperations = getSiblingOperations(projectForFile);

    const { document } = parseGraphQLSDL(filePath, code, {
      ...options.graphQLParserOptions,
      noLocation: false,
    });

    const comments = extractComments(document.loc);
    const tokens = extractTokens(filePath, code);
    const rootTree = convertToESTree(document, schema instanceof GraphQLSchema ? schema : null);

    return {
      services: {
        schema,
        siblingOperations,
      },
      ast: {
        comments,
        tokens,
        loc: rootTree.loc,
        range: rootTree.range as [number, number],
        type: 'Program',
        sourceType: 'script',
        body: [rootTree as any],
      },
    };
  } catch (error) {
    error.message = `[graphql-eslint] ${error.message}`;
    // In case of GraphQL parser error, we report it to ESLint as a parser error that matches the requirements
    // of ESLint. This will make sure to display it correctly in IDEs and lint results.
    if (error instanceof GraphQLError) {
      const eslintError = {
        index: error.positions[0],
        lineNumber: error.locations[0].line,
        column: error.locations[0].column - 1,
        message: error.message,
      };
      throw eslintError;
    }
    throw error;
  }
}
Example #13
Source File: index.ts    From amplify-codegen with Apache License 2.0 5 votes vote down vote up
export function compileToIR(schema: GraphQLSchema, document: DocumentNode, options: CompilerOptions = {}): CompilerContext {
  if (options.addTypename) {
    document = withTypenameFieldAddedWhereNeeded(document);
  }

  const compiler = new Compiler(schema, options);

  const operations: { [operationName: string]: Operation } = Object.create(null);
  const fragments: { [fragmentName: string]: Fragment } = Object.create(null);

  for (const definition of document.definitions) {
    try {
      switch (definition.kind) {
        case Kind.OPERATION_DEFINITION:
          const operation = compiler.compileOperation(definition);
          operations[operation.operationName] = operation;
          break;
        case Kind.FRAGMENT_DEFINITION:
          const fragment = compiler.compileFragment(definition);
          fragments[fragment.fragmentName] = fragment;
          break;
      }
    } catch (e) {
      if (e instanceof GraphQLError) {
        if (definition.kind === 'OperationDefinition' && definition.name) {
          const locInfo = definition.name.loc ? `in ${definition.name.loc.source.name}` : '';
          console.log(`${e.message} but found ${definition.operation}: ${definition.name.value} ${locInfo}  `);
        } else {
          console.log(e.message);
        }
      }
    }
  }

  for (const fragmentSpread of compiler.unresolvedFragmentSpreads) {
    const fragment = fragments[fragmentSpread.fragmentName];
    if (!fragment) {
      throw new Error(`Cannot find fragment "${fragmentSpread.fragmentName}"`);
    }

    // Compute the intersection between the possible,types of the fragment spread and the fragment.
    const possibleTypes = fragment.selectionSet.possibleTypes.filter(type => fragmentSpread.selectionSet.possibleTypes.includes(type));

    fragmentSpread.isConditional = fragment.selectionSet.possibleTypes.some(
      type => !fragmentSpread.selectionSet.possibleTypes.includes(type),
    );

    fragmentSpread.selectionSet = {
      possibleTypes,
      selections: fragment.selectionSet.selections,
    };
  }

  const typesUsed = compiler.typesUsed;

  return { schema, typesUsed, operations, fragments, options };
}
Example #14
Source File: index.ts    From payload with MIT License 5 votes vote down vote up
customFormatErrorFn: (error: GraphQLError) => GraphQLFormattedError;
Example #15
Source File: index.ts    From graphql-mesh with MIT License 5 votes vote down vote up
private errors = new WeakMap<DelegationContext, GraphQLError[]>();
Example #16
Source File: index.ts    From graphql-mesh with MIT License 5 votes vote down vote up
transformRequest(executionRequest: ExecutionRequest, delegationContext: DelegationContext): ExecutionRequest {
    const { transformedSchema, rootValue, args, context, info } = delegationContext;
    if (transformedSchema) {
      const errors: GraphQLError[] = [];
      const resolverData: ResolverData = {
        env: process.env,
        root: rootValue,
        args,
        context,
        info,
      };
      const typeInfo = new TypeInfo(transformedSchema);
      let remainingFields = 0;
      const newDocument = visit(
        executionRequest.document,
        visitWithTypeInfo(typeInfo, {
          Field: () => {
            const parentType = typeInfo.getParentType();
            const fieldDef = typeInfo.getFieldDef();
            const path = `${parentType.name}.${fieldDef.name}`;
            const rateLimitConfig = this.pathRateLimitDef.get(path);
            if (rateLimitConfig) {
              const identifier = stringInterpolator.parse(rateLimitConfig.identifier, resolverData);
              const mapKey = `${identifier}-${path}`;
              let remainingTokens = this.tokenMap.get(mapKey);

              if (remainingTokens == null) {
                remainingTokens = rateLimitConfig.max;
                const timeout = setTimeout(() => {
                  this.tokenMap.delete(mapKey);
                  this.timeouts.delete(timeout);
                }, rateLimitConfig.ttl);
                this.timeouts.add(timeout);
              }

              if (remainingTokens === 0) {
                errors.push(new GraphQLError(`Rate limit of "${path}" exceeded for "${identifier}"`));
                // Remove this field from the selection set
                return null;
              } else {
                this.tokenMap.set(mapKey, remainingTokens - 1);
              }
            }
            remainingFields++;
            return false;
          },
        })
      );
      if (remainingFields === 0) {
        if (errors.length === 1) {
          throw errors[0];
        } else if (errors.length > 0) {
          throw new AggregateError(errors);
        }
      }
      this.errors.set(delegationContext, errors);
      return {
        ...executionRequest,
        document: newDocument,
      };
    }
    return executionRequest;
  }
Example #17
Source File: getTypeResolverFromOutputTCs.ts    From graphql-mesh with MIT License 5 votes vote down vote up
export function getTypeResolverFromOutputTCs(
  ajv: Ajv,
  outputTypeComposers: (ObjectTypeComposer | UnionTypeComposer)[],
  statusCodeOneOfIndexMap?: Record<string, number>
): GraphQLTypeResolver<any, any> {
  const statusCodeTypeMap = new Map<string, ObjectTypeComposer | UnionTypeComposer>();
  for (const statusCode in statusCodeOneOfIndexMap) {
    statusCodeTypeMap.set(statusCode.toString(), outputTypeComposers[statusCodeOneOfIndexMap[statusCode]]);
  }
  return function resolveType(data: any, context: any, info: GraphQLResolveInfo) {
    if (data.__typename) {
      return data.__typename;
    } else if (data.resourceType) {
      return data.resourceType;
    }
    if (data.$response && statusCodeOneOfIndexMap) {
      const responseData: ResponseData = data.$response;
      const type = statusCodeTypeMap.get(responseData.status.toString()) || statusCodeTypeMap.get('default');
      if (type) {
        if ('getFields' in type) {
          return type.getTypeName();
        } else {
          return type.getResolveType()(data, context, info, type.getType());
        }
      }
    }
    const validationErrors: Record<string, ErrorObject[]> = {};
    for (const outputTypeComposer of outputTypeComposers) {
      const validateFn = outputTypeComposer.getExtension('validateWithJSONSchema') as ValidateFunction;
      if (validateFn) {
        const isValid = validateFn(data);
        const typeName = outputTypeComposer.getTypeName();
        if (isValid) {
          if ('getFields' in outputTypeComposer) {
            return outputTypeComposer.getTypeName();
          } else {
            return outputTypeComposer.getResolveType()(data, context, info, outputTypeComposer.getType());
          }
        }
        validationErrors[typeName] = ajv.errors || validateFn.errors;
      }
    }
    if (data.$response) {
      const responseData: ResponseData = data.$response;
      const error = new GraphQLError(
        `HTTP Error: ${responseData.status}`,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        {
          ...responseData,
          responseJson: data,
        }
      );
      console.error(error);
      return error;
    }
    const error = new GraphQLError(`Received data doesn't met the union`, null, null, null, null, null, {
      validationErrors,
    });
    console.error(error);
    return error;
  };
}
Example #18
Source File: addExecutionLogicToComposer.ts    From graphql-mesh with MIT License 5 votes vote down vote up
function createError(message: string, extensions?: any) {
  return new GraphQLError(message, undefined, undefined, undefined, undefined, undefined, extensions);
}
Example #19
Source File: resolver_builder.ts    From graphql-mesh with MIT License 5 votes vote down vote up
/**
 * Create a new GraphQLError with an extensions field
 */
function graphQLErrorWithExtensions(message: string, extensions: { [key: string]: any }): GraphQLError {
  return new GraphQLError(message, null, null, null, null, null, extensions);
}
Example #20
Source File: apollo-server.ts    From clean-ts-api with GNU General Public License v3.0 5 votes vote down vote up
checkError = (error: GraphQLError, errorName: string): boolean => {
  return [error.name, error.originalError?.name].some(name => name === errorName)
}
Example #21
Source File: global-id.scalar.ts    From nestjs-relay with MIT License 5 votes vote down vote up
parseValue(value: string): ResolvedGlobalId {
    const { id, type } = fromGlobalId(value);
    if (!id || !type) {
      throw new GraphQLError(`Invalid ID: ${value}`);
    }
    return new ResolvedGlobalId({ type, id });
  }
Example #22
Source File: index.ts    From server with Apache License 2.0 4 votes vote down vote up
(async () => {
  logger.info(`Server version: ${  version.buildMajor  }.${  version.buildMinor  }.${  version.buildRevision}`)
  logger.info(`Arguments: ${  process.argv}`)
  
  // Construct a schema, using GraphQL schema language
  const typeDefs = await importSchema('./schema/index.graphql'); 
  const schema = await makeExecutableSchema({ typeDefs, resolvers })

  await initModels();

  let channelTypes = undefined
  
  if (process.env.OPENPIM_KEY) {
    const keyData = 
`-----BEGIN RSA PUBLIC KEY-----
MEgCQQDG0oEGhlYcN12BqBeMn9aRwfrPElOol7prVUQNAggZjurOQ5DyjbPh9uOW
XWhRphP+pl2nJQLVRu+oDpf2wKc/AgMBAAE=
-----END RSA PUBLIC KEY-----
`
    const publicKey: crypto.KeyObject = crypto.createPublicKey({key: keyData, type: 'pkcs1', format: 'pem'})
    const str = Buffer.from(process.env.OPENPIM_KEY, 'base64').toString('ascii')
    const idx = str.lastIndexOf('|')
    const sign = str.substring(idx+1)
    const data = str.substring(0, idx)
    const split = data.split('|')
    const options: any = { key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }
    const isVerified = crypto.verify( "sha256", Buffer.from(data), options, Buffer.from(sign, 'base64'))
    if (isVerified) {
      channelTypes = JSON.parse("[" + split[0] + "]")
      logger.info('Found key for company: ' + split[1]+' with data: '+channelTypes)
    } else {
      logger.error('Wrong key')
      channelTypes = []
    }
  }

  ModelsManager.getInstance().init(channelTypes)
  ChannelsManagerFactory.getInstance().init()
  
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(cors());
  
  app.use('/graphql', graphqlHTTP(async (request: IncomingMessage ) => ({
    schema,
    graphiql: false,
    context: await Context.create(request),
    customFormatErrorFn: (error: GraphQLError) => {
      const params = {
        message: error.message
      };
      logger.error('ERROR -', error, error.source);
      logger.error(`   request - ${ JSON.stringify((<any>request).body)}`);
      return (params);
    }
  })));

  app.get('/healthcheck', async (req, res) => {
    res.json({result: "OK"})
  })

  app.post('/asset-upload', async (req, res) => {
    try {
      const context = await Context.create(req)
      context.checkAuth()

      await processUpload(context, req, res)
    } catch (error: any) {
      res.status(400).send(error.message)
    }
  })

  app.post('/asset-create-upload', async (req, res) => {
    try {
      const context = await Context.create(req)
      context.checkAuth()

      await processCreateUpload(context, req, res)
    } catch (error: any) {
      res.status(400).send(error.message)
    }
  })

  app.get('/asset/:id', async (req, res) => {
    try {
      const context = await Context.create(req)
      await processDownload(context, req, res, false)
    } catch (error: any) {
      res.status(400).send(error.message)
    }
  })

  app.get('/asset/:id/thumb', async (req, res) => {
    try {
      const context = await Context.create(req)
      context.checkAuth()
      await processDownload(context, req, res, true)
    } catch (error: any) {
      res.status(400).send(error.message)
    }
  })

  app.get('/asset-channel/:id', async (req, res) => {
    try {
      const context = await Context.create(req)
      context.checkAuth()
      await processChannelDownload(context, req, res, false)
    } catch (error: any) {
      res.status(400).send(error.message)
    }
  })

  app.listen(process.env.PORT);
  logger.info('Running a GraphQL API server at http://localhost:' + process.env.PORT + '/graphql');
})();
Example #23
Source File: codeGeneration.ts    From amplify-codegen with Apache License 2.0 4 votes vote down vote up
classDeclarationForOperation(operation: Operation) {
    const { operationName, operationType, variables, source, selectionSet } = operation;

    let className;
    let protocol;

    switch (operationType) {
      case 'query':
        className = `${this.helpers.operationClassName(operationName)}Query`;
        protocol = 'GraphQLQuery';
        break;
      case 'mutation':
        className = `${this.helpers.operationClassName(operationName)}Mutation`;
        protocol = 'GraphQLMutation';
        break;
      case 'subscription':
        className = `${this.helpers.operationClassName(operationName)}Subscription`;
        protocol = 'GraphQLSubscription';
        break;
      default:
        throw new GraphQLError(`Unsupported operation type "${operationType}"`);
    }

    this.classDeclaration(
      {
        className,
        modifiers: ['public', 'final'],
        adoptedProtocols: [protocol],
      },
      () => {
        if (source) {
          this.printOnNewline('public static let operationString =');
          this.withIndent(() => {
            this.multilineString(source);
          });
        }

        const fragmentsReferenced = collectFragmentsReferenced(operation.selectionSet, this.context.fragments);

        if (this.context.options.generateOperationIds) {
          const { operationId } = generateOperationId(operation, this.context.fragments, fragmentsReferenced);
          operation.operationId = operationId;
          this.printNewlineIfNeeded();
          this.printOnNewline(`public static let operationIdentifier: String? = "${operationId}"`);
        }

        if (fragmentsReferenced.size > 0) {
          this.printNewlineIfNeeded();
          this.printOnNewline('public static var requestString: String { return operationString');
          fragmentsReferenced.forEach(fragmentName => {
            this.print(`.appending(${this.helpers.structNameForFragmentName(fragmentName)}.fragmentString)`);
          });
          this.print(' }');
        }

        this.printNewlineIfNeeded();

        if (variables && variables.length > 0) {
          const properties = variables.map(({ name, type }) => {
            const typeName = this.helpers.typeNameFromGraphQLType(type);
            const isOptional = !(isNonNullType(type) || (isListType(type) && isNonNullType(type.ofType)));
            return { name, propertyName: name, type, typeName, isOptional };
          });

          this.propertyDeclarations(properties);

          this.printNewlineIfNeeded();
          this.initializerDeclarationForProperties(properties);

          this.printNewlineIfNeeded();
          this.printOnNewline(`public var variables: GraphQLMap?`);
          this.withinBlock(() => {
            this.printOnNewline(
              wrap(
                `return [`,
                join(properties.map(({ name, propertyName }) => `"${name}": ${escapeIdentifierIfNeeded(propertyName)}`), ', ') || ':',
                `]`
              )
            );
          });
        } else {
          this.initializerDeclarationForProperties([]);
        }

        this.structDeclarationForSelectionSet({
          structName: 'Data',
          selectionSet,
        });
      }
    );
  }
Example #24
Source File: index.ts    From hono with MIT License 4 votes vote down vote up
graphqlServer = (options: Options) => {
  const schema = options.schema
  const rootValue = options.rootValue
  const pretty = options.pretty ?? false
  const validationRules = options.validationRules ?? []
  // const showGraphiQL = options.graphiql ?? false

  return async (c: Context, next: Next) => {
    // GraphQL HTTP only supports GET and POST methods.
    if (c.req.method !== 'GET' && c.req.method !== 'POST') {
      return c.json(errorMessages(['GraphQL only supports GET and POST requests.']), 405, {
        Allow: 'GET, POST',
      })
    }

    let params: GraphQLParams
    try {
      params = await getGraphQLParams(c.req)
    } catch (e) {
      if (e instanceof Error) {
        console.error(`${e.stack || e.message}`)
        return c.json(errorMessages([e.message], [e]), 400)
      }
      throw e
    }

    const { query, variables, operationName } = params

    if (query == null) {
      return c.json(errorMessages(['Must provide query string.']), 400)
    }

    const schemaValidationErrors = validateSchema(schema)
    if (schemaValidationErrors.length > 0) {
      // Return 500: Internal Server Error if invalid schema.
      return c.json(
        errorMessages(['GraphQL schema validation error.'], schemaValidationErrors),
        500
      )
    }

    let documentAST: DocumentNode
    try {
      documentAST = parse(new Source(query, 'GraphQL request'))
    } catch (syntaxError: unknown) {
      // Return 400: Bad Request if any syntax errors errors exist.
      if (syntaxError instanceof Error) {
        console.error(`${syntaxError.stack || syntaxError.message}`)
        const e = new GraphQLError(syntaxError.message, {
          originalError: syntaxError,
        })
        return c.json(errorMessages(['GraphQL syntax error.'], [e]), 400)
      }
      throw syntaxError
    }

    // Validate AST, reporting any errors.
    const validationErrors = validate(schema, documentAST, [...specifiedRules, ...validationRules])

    if (validationErrors.length > 0) {
      // Return 400: Bad Request if any validation errors exist.
      return c.json(errorMessages(['GraphQL validation error.'], validationErrors), 400)
    }

    if (c.req.method === 'GET') {
      // Determine if this GET request will perform a non-query.
      const operationAST = getOperationAST(documentAST, operationName)
      if (operationAST && operationAST.operation !== 'query') {
        /*
        Now , does not support GraphiQL
        if (showGraphiQL) {
          //return respondWithGraphiQL(response, graphiqlOptions, params)
        }
        */

        // Otherwise, report a 405: Method Not Allowed error.
        return c.json(
          errorMessages([
            `Can only perform a ${operationAST.operation} operation from a POST request.`,
          ]),
          405,
          { Allow: 'POST' }
        )
      }
    }

    let result: FormattedExecutionResult

    try {
      result = await execute({
        schema,
        document: documentAST,
        rootValue,
        variableValues: variables,
        operationName: operationName,
      })
    } catch (contextError: unknown) {
      if (contextError instanceof Error) {
        console.error(`${contextError.stack || contextError.message}`)
        const e = new GraphQLError(contextError.message, {
          originalError: contextError,
          nodes: documentAST,
        })
        // Return 400: Bad Request if any execution context errors exist.
        return c.json(errorMessages(['GraphQL execution context error.'], [e]), 400)
      }
      throw contextError
    }

    if (!result.data) {
      if (result.errors) {
        return c.json(errorMessages([result.errors.toString()], result.errors), 500)
      }
    }

    /*
    Now, does not support GraphiQL
    if (showGraphiQL) {
    }
    */

    if (pretty) {
      const payload = JSON.stringify(result, null, pretty ? 2 : 0)
      return c.text(payload, 200, {
        'Content-Type': 'application/json',
      })
    } else {
      return c.json(result)
    }

    await next() // XXX
  }
}
Example #25
Source File: index.ts    From amplify-codegen with Apache License 2.0 4 votes vote down vote up
compileSelection(
    selectionNode: SelectionNode,
    parentType: GraphQLCompositeType,
    possibleTypes: GraphQLObjectType[],
    visitedFragments: Set<string>,
  ): Selection | null {
    switch (selectionNode.kind) {
      case Kind.FIELD: {
        const name = selectionNode.name.value;
        const alias = selectionNode.alias ? selectionNode.alias.value : undefined;

        const fieldDef = getFieldDef(this.schema, parentType, selectionNode);
        if (!fieldDef) {
          throw new GraphQLError(`Cannot query field "${name}" on type "${String(parentType)}"`, [selectionNode]);
        }

        const fieldType = fieldDef.type;
        const unmodifiedFieldType = getNamedType(fieldType);

        this.addTypeUsed(unmodifiedFieldType);

        const { description, isDeprecated, deprecationReason } = fieldDef;

        const responseKey = alias || name;

        const args =
          selectionNode.arguments && selectionNode.arguments.length > 0
            ? selectionNode.arguments.map(arg => {
                const name = arg.name.value;
                const argDef = fieldDef.args.find(argDef => argDef.name === arg.name.value);
                return {
                  name,
                  value: valueFromValueNode(arg.value),
                  type: (argDef && argDef.type) || undefined,
                };
              })
            : undefined;

        let field: Field = {
          kind: 'Field',
          responseKey,
          name,
          alias,
          args,
          type: fieldType,
          description: !isMetaFieldName(name) && description ? description : undefined,
          isDeprecated,
          deprecationReason: deprecationReason || undefined,
        };

        if (isCompositeType(unmodifiedFieldType)) {
          const selectionSetNode = selectionNode.selectionSet;
          if (!selectionSetNode) {
            throw new GraphQLError(`Composite field "${name}" on type "${String(parentType)}" requires selection set`, [selectionNode]);
          }

          field.selectionSet = this.compileSelectionSet(selectionNode.selectionSet as SelectionSetNode, unmodifiedFieldType);
        }
        return field;
      }
      case Kind.INLINE_FRAGMENT: {
        const typeNode = selectionNode.typeCondition;
        const type = typeNode ? (typeFromAST(this.schema, typeNode) as GraphQLCompositeType) : parentType;
        const possibleTypesForTypeCondition = this.possibleTypesForType(type).filter(type => possibleTypes.includes(type));
        return {
          kind: 'TypeCondition',
          type,
          selectionSet: this.compileSelectionSet(selectionNode.selectionSet, type, possibleTypesForTypeCondition),
        };
      }
      case Kind.FRAGMENT_SPREAD: {
        const fragmentName = selectionNode.name.value;
        if (visitedFragments.has(fragmentName)) return null;
        visitedFragments.add(fragmentName);

        const fragmentSpread: FragmentSpread = {
          kind: 'FragmentSpread',
          fragmentName,
          selectionSet: {
            possibleTypes,
            selections: [],
          },
        };
        this.unresolvedFragmentSpreads.push(fragmentSpread);
        return fragmentSpread;
      }
    }
  }
Example #26
Source File: addExecutionLogicToComposer.ts    From graphql-mesh with MIT License 4 votes vote down vote up
export async function addExecutionLogicToComposer(
  schemaComposer: SchemaComposer,
  {
    fetch: globalFetch,
    logger,
    operations,
    operationHeaders,
    baseUrl,
    pubsub: globalPubsub,
    queryParams,
  }: AddExecutionLogicToComposerOptions
) {
  logger.debug(`Attaching execution logic to the schema`);
  for (const operationConfig of operations) {
    const { httpMethod, rootTypeName, fieldName } = getOperationMetadata(operationConfig);
    const operationLogger = logger.child(`${rootTypeName}.${fieldName}`);

    const interpolationStrings: string[] = [
      ...Object.values(operationHeaders || {}),
      ...Object.values(queryParams || {}),
      baseUrl,
    ];

    const rootTypeComposer = schemaComposer[rootTypeName];

    const field = rootTypeComposer.getField(fieldName);

    if (isPubSubOperationConfig(operationConfig)) {
      field.description = operationConfig.description || `PubSub Topic: ${operationConfig.pubsubTopic}`;
      field.subscribe = (root, args, context, info) => {
        const pubsub = context?.pubsub || globalPubsub;
        if (!pubsub) {
          return new GraphQLError(`You should have PubSub defined in either the config or the context!`);
        }
        const interpolationData = { root, args, context, info, env: process.env };
        const pubsubTopic = stringInterpolator.parse(operationConfig.pubsubTopic, interpolationData);
        operationLogger.debug(`=> Subscribing to pubSubTopic: ${pubsubTopic}`);
        return pubsub.asyncIterator(pubsubTopic);
      };
      field.resolve = root => {
        operationLogger.debug('Received ', root, ' from ', operationConfig.pubsubTopic);
        return root;
      };
      interpolationStrings.push(operationConfig.pubsubTopic);
    } else if (operationConfig.path) {
      if (process.env.DEBUG) {
        field.description = `
    ***Original Description***: ${operationConfig.description || '(none)'}
    ***Method***: ${operationConfig.method}
    ***Base URL***: ${baseUrl}
    ***Path***: ${operationConfig.path}
`;
      } else {
        field.description = operationConfig.description;
      }
      field.resolve = async (root, args, context) => {
        operationLogger.debug(`=> Resolving`);
        const interpolationData = { root, args, context, env: process.env };
        const interpolatedBaseUrl = stringInterpolator.parse(baseUrl, interpolationData);
        const interpolatedPath = stringInterpolator.parse(operationConfig.path, interpolationData);
        let fullPath = urlJoin(interpolatedBaseUrl, interpolatedPath);
        const headers = {
          ...operationHeaders,
          ...operationConfig?.headers,
        };
        for (const headerName in headers) {
          headers[headerName] = stringInterpolator.parse(headers[headerName], interpolationData);
        }
        const requestInit: RequestInit = {
          method: httpMethod,
          headers,
        };
        if (queryParams) {
          const interpolatedQueryParams: Record<string, any> = {};
          for (const queryParamName in queryParams) {
            interpolatedQueryParams[queryParamName] = stringInterpolator.parse(
              queryParams[queryParamName],
              interpolationData
            );
          }
          const queryParamsString = qsStringify(interpolatedQueryParams, { indices: false });
          fullPath += fullPath.includes('?') ? '&' : '?';
          fullPath += queryParamsString;
        }
        // Handle binary data
        if ('binary' in operationConfig) {
          const binaryUpload = await args.input;
          if (isFileUpload(binaryUpload)) {
            const readable = binaryUpload.createReadStream();
            const chunks: number[] = [];
            for await (const chunk of readable) {
              for (const byte of chunk) {
                chunks.push(byte);
              }
            }
            requestInit.body = new Uint8Array(chunks);

            const [, contentType] = Object.entries(headers).find(([key]) => key.toLowerCase() === 'content-type') || [];
            if (!contentType) {
              headers['content-type'] = binaryUpload.mimetype;
            }
          }
          requestInit.body = binaryUpload;
        } else {
          if (operationConfig.requestBaseBody != null) {
            args.input = args.input || {};
            for (const key in operationConfig.requestBaseBody) {
              const configValue = operationConfig.requestBaseBody[key];
              if (typeof configValue === 'string') {
                const value = stringInterpolator.parse(configValue, interpolationData);
                lodashSet(args.input, key, value);
              } else {
                args.input[key] = configValue;
              }
            }
          }
          // Resolve union input
          const input = (args.input = resolveDataByUnionInputType(
            cleanObject(args.input),
            field.args?.input?.type?.getType(),
            schemaComposer
          ));
          if (input != null) {
            switch (httpMethod) {
              case 'GET':
              case 'HEAD':
              case 'CONNECT':
              case 'OPTIONS':
              case 'TRACE': {
                fullPath += fullPath.includes('?') ? '&' : '?';
                fullPath += qsStringify(input, { indices: false });
                break;
              }
              case 'POST':
              case 'PUT':
              case 'PATCH':
              case 'DELETE': {
                const [, contentType] =
                  Object.entries(headers).find(([key]) => key.toLowerCase() === 'content-type') || [];
                if (contentType?.startsWith('application/x-www-form-urlencoded')) {
                  requestInit.body = qsStringify(input, { indices: false });
                } else {
                  requestInit.body = JSON.stringify(input);
                }
                break;
              }
              default:
                return createError(`Unknown HTTP Method: ${httpMethod}`, {
                  url: fullPath,
                  method: httpMethod,
                });
            }
          }
        }

        // Delete unused queryparams
        const [actualPath, queryString] = fullPath.split('?');
        if (queryString) {
          const queryParams = qsParse(queryString);
          const cleanedQueryParams = cleanObject(queryParams);
          fullPath = actualPath + '?' + qsStringify(cleanedQueryParams, { indices: false });
        }

        operationLogger.debug(`=> Fetching `, fullPath, `=>`, requestInit);
        const fetch: typeof globalFetch = context?.fetch || globalFetch;
        if (!fetch) {
          return createError(`You should have fetch defined in either the config or the context!`, {
            url: fullPath,
            method: httpMethod,
          });
        }
        const response = await fetch(fullPath, requestInit);
        // If return type is a file
        if (field.type.getTypeName() === 'File') {
          return response.blob();
        }
        const responseText = await response.text();
        operationLogger.debug(`=> Received`, {
          headers: response.headers,
          text: responseText,
        });
        let responseJson: any;
        try {
          responseJson = JSON.parse(responseText);
        } catch (error) {
          const returnNamedGraphQLType = getNamedType(field.type.getType());
          // The result might be defined as scalar
          if (isScalarType(returnNamedGraphQLType)) {
            operationLogger.debug(` => Return type is not a JSON so returning ${responseText}`);
            return responseText;
          } else if (response.status === 204) {
            responseJson = {};
          } else {
            return createError(`Unexpected response`, {
              url: fullPath,
              method: httpMethod,
              responseText,
              error,
            });
          }
        }

        if (!response.status.toString().startsWith('2')) {
          const returnNamedGraphQLType = getNamedType(field.type.getType());
          if (!isUnionType(returnNamedGraphQLType)) {
            return createError(`HTTP Error: ${response.status}`, {
              url: fullPath,
              method: httpMethod,
              ...(response.statusText ? { status: response.statusText } : {}),
              responseJson,
            });
          }
        }

        operationLogger.debug(`Returning `, responseJson);
        // Sometimes API returns an array but the return type is not an array
        const isListReturnType = isListTypeOrNonNullListType(field.type.getType());
        const isArrayResponse = Array.isArray(responseJson);
        if (isListReturnType && !isArrayResponse) {
          operationLogger.debug(`Response is not array but return type is list. Normalizing the response`);
          responseJson = [responseJson];
        }
        if (!isListReturnType && isArrayResponse) {
          operationLogger.debug(`Response is array but return type is not list. Normalizing the response`);
          responseJson = responseJson[0];
        }

        const addResponseMetadata = (obj: any) => {
          return {
            ...obj,
            $url: fullPath,
            $method: httpMethod,
            $request: {
              query: {
                ...obj,
                ...args,
                ...args.input,
              },
              path: {
                ...obj,
                ...args,
              },
              header: requestInit.headers,
            },
            $response: {
              url: fullPath,
              method: httpMethod,
              status: response.status,
              statusText: response.statusText,
              body: obj,
            },
          };
        };
        operationLogger.debug(`Adding response metadata to the response object`);
        return Array.isArray(responseJson)
          ? responseJson.map(obj => addResponseMetadata(obj))
          : addResponseMetadata(responseJson);
      };
      interpolationStrings.push(...Object.values(operationConfig.headers || {}));
      interpolationStrings.push(operationConfig.path);

      if ('links' in operationConfig) {
        for (const linkName in operationConfig.links) {
          const linkObj = operationConfig.links[linkName];
          const typeTC = schemaComposer.getOTC(field.type.getTypeName());
          typeTC.addFields({
            [linkName]: () => {
              const targetField = schemaComposer.Query.getField(linkObj.fieldName);
              return {
                ...targetField,
                args: {},
                description: linkObj.description || targetField.description,
                resolve: (root, args, context, info) =>
                  linkResolver(linkObj.args, targetField.resolve, root, args, context, info),
              };
            },
          });
        }
      } else if ('responseByStatusCode' in operationConfig) {
        const unionOrSingleTC = schemaComposer.getAnyTC(getNamedType(field.type.getType()));
        const types = 'getTypes' in unionOrSingleTC ? unionOrSingleTC.getTypes() : [unionOrSingleTC];
        const statusCodeOneOfIndexMap =
          (unionOrSingleTC.getExtension('statusCodeOneOfIndexMap') as Record<string, number>) || {};
        for (const statusCode in operationConfig.responseByStatusCode) {
          const responseConfig = operationConfig.responseByStatusCode[statusCode];
          for (const linkName in responseConfig.links) {
            const typeTCThunked = types[statusCodeOneOfIndexMap[statusCode] || 0];
            const typeTC = schemaComposer.getOTC(typeTCThunked.getTypeName());
            typeTC.addFields({
              [linkName]: () => {
                const linkObj = responseConfig.links[linkName];
                const targetField = schemaComposer.Query.getField(linkObj.fieldName);
                return {
                  ...targetField,
                  args: {},
                  description: linkObj.description || targetField.description,
                  resolve: (root, args, context, info) =>
                    linkResolver(linkObj.args, targetField.resolve, root, args, context, info),
                };
              },
            });
          }
        }
      }
    }
    const { args: globalArgs } = parseInterpolationStrings(interpolationStrings, operationConfig.argTypeMap);
    rootTypeComposer.addFieldArgs(fieldName, globalArgs);
  }

  logger.debug(`Building the executable schema.`);
  return schemaComposer;
}
Example #27
Source File: apolloServer.spec.ts    From apollo-server-vercel with MIT License 4 votes vote down vote up
describe(`apolloServer`, () => {
  let app = null as ReturnType<typeof createApp>;
  let didEncounterErrors: jest.Mock<
    ReturnType<GraphQLRequestListener["didEncounterErrors"]>,
    Parameters<GraphQLRequestListener["didEncounterErrors"]>
  >;

  afterEach(() => {
    if (app) {
      app = null;
    }
  });

  describe(`graphqlHTTP`, () => {
    it(`rejects the request if the method is not POST or GET`, async () => {
      app = createApp();
      const res = await request(app).head(`/graphql`).send();
      expect(res.status).toStrictEqual(405);
      expect(res.header.allow).toStrictEqual(`GET, POST`);
    });

    it(`throws an error if POST body is missing`, async () => {
      app = createApp();
      const res = await request(app).post(`/graphql`).send();
      expect(res.status).toStrictEqual(500);
      expect((res.error as HTTPError).text).toMatch(`POST body missing.`);
    });

    it(`throws an error if GET query is missing`, async () => {
      app = createApp();
      const res = await request(app).get(`/graphql`);
      expect(res.status).toStrictEqual(400);
      expect((res.error as HTTPError).text).toMatch(`GET query missing.`);
    });

    it(`can handle a basic GET request`, async () => {
      app = createApp();
      const expected = {
        testString: `it works`
      };
      const query = {
        query: `
          query test {
            testString
          }
        `
      };
      const res = await request(app).get(`/graphql`).query(query);
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`can handle a basic implicit GET request`, async () => {
      app = createApp();
      const expected = {
        testString: `it works`
      };
      const query = {
        query: `
          {
            testString
          }
        `
      };
      const res = await request(app).get(`/graphql`).query(query);
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`throws error if trying to use mutation using GET request`, async () => {
      didEncounterErrors = jest.fn();
      app = createApp({
        schema,
        plugins: [
          {
            requestDidStart(): { didEncounterErrors: typeof didEncounterErrors } {
              return { didEncounterErrors };
            }
          }
        ]
      });
      const query = {
        query: `
          mutation test {
            testMutation(echo: "ping")
          }
        `
      };
      const res = await request(app).get(`/graphql`).query(query);

      expect(res.status).toStrictEqual(405);
      expect(res.header.allow).toStrictEqual(`POST`);
      expect((res.error as HTTPError).text).toMatch(`GET supports only query operation`);
      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([
            expect.objectContaining({
              message: `GET supports only query operation`
            })
          ])
        })
      );
    });

    it(`throws error if trying to use mutation with fragment using GET request`, async () => {
      didEncounterErrors = jest.fn();
      app = createApp({
        schema,
        plugins: [
          {
            requestDidStart(): { didEncounterErrors: typeof didEncounterErrors } {
              return { didEncounterErrors };
            }
          }
        ]
      });
      const query = {
        query: `
          fragment PersonDetails on PersonType {
            firstName
          }
          mutation test {
            testPerson(firstName: "Test", lastName: "Me") {
              ...PersonDetails
            }
          }
        `
      };
      const res = await request(app).get(`/graphql`).query(query);
      expect(res.status).toStrictEqual(405);
      expect(res.header.allow).toStrictEqual(`POST`);
      expect((res.error as HTTPError).text).toMatch(`GET supports only query operation`);
      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([
            expect.objectContaining({
              message: `GET supports only query operation`
            })
          ])
        })
      );
    });

    it(`can handle a GET request with variables`, async () => {
      app = createApp();
      const query = {
        query: `
          query test($echo: String) {
            testArgument(echo: $echo)
          }
        `,
        variables: JSON.stringify({ echo: `world` })
      };
      const expected = {
        testArgument: `hello world`
      };
      const res = await request(app).get(`/graphql`).query(query);
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`can handle a basic request`, async () => {
      app = createApp();
      const expected = {
        testString: `it works`
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testString
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`can handle a basic request with cacheControl`, async () => {
      app = createApp({ schema, cacheControl: true });
      const expected = {
        testPerson: { firstName: `Jane` }
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testPerson {
                firstName
              }
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
      expect(res.body.extensions).toStrictEqual({
        cacheControl: {
          version: 1,
          hints: [{ maxAge: 0, path: [`testPerson`] }]
        }
      });
    });

    it(`can handle a basic request with cacheControl and defaultMaxAge`, async () => {
      app = createApp({
        schema,
        cacheControl: {
          defaultMaxAge: 5,
          stripFormattedExtensions: false,
          calculateHttpHeaders: false
        }
      });
      const expected = {
        testPerson: { firstName: `Jane` }
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testPerson {
                firstName
              }
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
      expect(res.body.extensions).toStrictEqual({
        cacheControl: {
          version: 1,
          hints: [{ maxAge: 5, path: [`testPerson`] }]
        }
      });
    });

    it(`returns PersistedQueryNotSupported to a GET request if PQs disabled`, async () => {
      app = createApp({ schema, persistedQueries: false });
      const res = await request(app)
        .get(`/graphql`)
        .query({
          extensions: JSON.stringify({
            persistedQuery: {
              version: 1,
              sha256Hash: `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
            }
          })
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors).toBeDefined();
      expect(res.body.errors[0].message).toStrictEqual(`PersistedQueryNotSupported`);
    });

    it(`returns PersistedQueryNotSupported to a POST request if PQs disabled`, async () => {
      app = createApp({ schema, persistedQueries: false });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          extensions: {
            persistedQuery: {
              version: 1,
              sha256Hash: `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
            }
          }
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors).toBeDefined();
      expect(res.body.errors).toHaveLength(1);
      expect(res.body.errors[0].message).toStrictEqual(`PersistedQueryNotSupported`);
    });

    it(`returns PersistedQueryNotFound to a GET request`, async () => {
      app = createApp();
      const res = await request(app)
        .get(`/graphql`)
        .query({
          extensions: JSON.stringify({
            persistedQuery: {
              version: 1,
              sha256Hash: `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
            }
          })
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors).toBeDefined();
      expect(res.body.errors).toHaveLength(1);
      expect(res.body.errors[0].message).toStrictEqual(`PersistedQueryNotFound`);
    });

    it(`returns PersistedQueryNotFound to a POST request`, async () => {
      app = createApp();
      const res = await request(app)
        .post(`/graphql`)
        .send({
          extensions: {
            persistedQuery: {
              version: 1,
              sha256Hash: `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
            }
          }
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors).toBeDefined();
      expect(res.body.errors).toHaveLength(1);
      expect(res.body.errors[0].message).toStrictEqual(`PersistedQueryNotFound`);
    });

    it(`can handle a request with variables`, async () => {
      app = createApp();
      const expected = {
        testArgument: `hello world`
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test($echo: String) {
              testArgument(echo: $echo)
            }
          `,
          variables: { echo: `world` }
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`can handle a request with variables as string`, async () => {
      app = createApp();
      const expected = {
        testArgument: `hello world`
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test($echo: String!) {
              testArgument(echo: $echo)
            }
          `,
          variables: `{ "echo": "world" }`
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`can handle a request with variables as an invalid string`, async () => {
      app = createApp();
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test($echo: String!) {
              testArgument(echo: $echo)
            }
          `,
          variables: `{ echo: "world" }`
        });
      expect(res.status).toStrictEqual(400);
      expect((res.error as HTTPError).text).toMatch(`Variables are invalid JSON.`);
    });

    it(`can handle a request with operationName`, async () => {
      app = createApp();
      const expected = {
        testString: `it works`
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test($echo: String) {
              testArgument(echo: $echo)
            }
            query test2 {
              testString
            }
          `,
          variables: { echo: `world` },
          operationName: `test2`
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`can handle introspection request`, async () => {
      app = createApp();
      const res = await request(app).post(`/graphql`).send({ query: getIntrospectionQuery() });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data.__schema.types[0].fields[0].name).toStrictEqual(`testString`);
    });

    it(`does not accept a query AST`, async () => {
      app = createApp();
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: gql`
            query test {
              testString
            }
          `
        });
      expect(res.status).toStrictEqual(400);
      expect(res.text).toMatch(`GraphQL queries must be strings`);
    });

    it(`can handle batch requests`, async () => {
      app = createApp();
      const expected = [
        {
          data: {
            testString: `it works`
          }
        },
        {
          data: {
            testArgument: `hello yellow`
          }
        }
      ];
      const res = await request(app)
        .post(`/graphql`)
        .send([
          {
            query: `
              query test($echo: String) {
                testArgument(echo: $echo)
              }
              query test2 {
                testString
              }
            `,
            variables: { echo: `world` },
            operationName: `test2`
          },
          {
            query: `
              query testX($echo: String) {
                testArgument(echo: $echo)
              }
            `,
            variables: { echo: `yellow` },
            operationName: `testX`
          }
        ]);
      expect(res.status).toStrictEqual(200);
      expect(res.body).toStrictEqual(expected);
    });

    it(`can handle batch requests 2`, async () => {
      app = createApp();
      const expected = [
        {
          data: {
            testString: `it works`
          }
        }
      ];
      const res = await request(app)
        .post(`/graphql`)
        .send([
          {
            query: `
              query test($echo: String) {
                testArgument(echo: $echo)
              }
              query test2 {
                testString
              }
            `,
            variables: { echo: `world` },
            operationName: `test2`
          }
        ]);
      expect(res.status).toStrictEqual(200);
      expect(res.body).toStrictEqual(expected);
    });

    it(`can handle batch requests in parallel`, async () => {
      const parallels = 100;
      const delayPerReq = 40;

      app = createApp();
      const expected = Array(parallels).fill({
        data: { testStringWithDelay: `it works` }
      });
      const res = await request(app)
        .post(`/graphql`)
        .send(
          Array(parallels).fill({
            query: `
              query test($delay: Int!) {
                testStringWithDelay(delay: $delay)
              }
            `,
            operationName: `test`,
            variables: { delay: delayPerReq }
          })
        );
      expect(res.status).toStrictEqual(200);
      expect(res.body).toStrictEqual(expected);
    }, 3000); // this test will fail due to timeout if running serially.

    it(`clones batch context`, async () => {
      app = createApp({
        schema,
        context: { testField: `expected` }
      });
      const expected = [
        {
          data: {
            testContext: `expected`
          }
        },
        {
          data: {
            testContext: `expected`
          }
        }
      ];
      const res = await request(app)
        .post(`/graphql`)
        .send([
          {
            query: `
              query test {
                testContext
              }
            `
          },
          {
            query: `
              query test {
                testContext
              }
            `
          }
        ]);
      expect(res.status).toStrictEqual(200);
      expect(res.body).toStrictEqual(expected);
    });

    it(`executes batch context if it is a function`, async () => {
      let callCount = 0;
      app = createApp({
        schema,
        context: () => {
          callCount++;
          return { testField: `expected` };
        }
      });
      const expected = [
        {
          data: {
            testContext: `expected`
          }
        },
        {
          data: {
            testContext: `expected`
          }
        }
      ];
      const res = await request(app)
        .post(`/graphql`)
        .send([
          {
            query: `
              query test {
                testContext
              }
            `
          },
          {
            query: `
              query test {
                testContext
              }
            `
          }
        ]);
      // XXX In AS 1.0 we ran context once per GraphQL operation (so this
      // was 2) rather than once per HTTP request. Was this actually
      // helpful? Honestly we're not sure why you'd use a function in the
      // 1.0 API anyway since the function didn't actually get any useful
      // arguments. Right now there's some weirdness where a context
      // function is actually evaluated both inside ApolloServer and in
      // runHttpQuery.
      expect(callCount).toStrictEqual(1);
      expect(res.status).toStrictEqual(200);
      expect(res.body).toStrictEqual(expected);
    });

    it(`can handle a request with a mutation`, async () => {
      app = createApp();
      const expected = {
        testMutation: `not really a mutation, but who cares: world`
      };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            mutation test($echo: String) {
              testMutation(echo: $echo)
            }
          `,
          variables: { echo: `world` }
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data).toStrictEqual(expected);
    });

    it(`applies the formatResponse function`, async () => {
      app = createApp({
        schema,
        formatResponse(response) {
          response.extensions = { it: `works` };
          return response;
        }
      });
      const expected = { it: `works` };
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            mutation test($echo: String) {
              testMutation(echo: $echo)
            }
          `,
          variables: { echo: `world` }
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.extensions).toStrictEqual(expected);
    });

    it(`passes the context to the resolver`, async () => {
      const expected = `context works`;
      app = createApp({
        schema,
        context: { testField: expected }
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testContext
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data.testContext).toStrictEqual(expected);
    });

    it(`passes the rootValue to the resolver`, async () => {
      const expected = `it passes rootValue`;
      app = createApp({
        schema,
        rootValue: expected
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testRootValue
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.data.testRootValue).toStrictEqual(expected);
    });

    it(`passes the rootValue function result to the resolver for query`, async () => {
      const expectedQuery = `query: it passes rootValue`;
      app = createApp({
        schema,
        rootValue: () => expectedQuery
      });
      const queryRes = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testRootValue
            }
          `
        });
      expect(queryRes.status).toStrictEqual(200);
      expect(queryRes.body.data.testRootValue).toStrictEqual(expectedQuery);
    });

    it(`passes the rootValue function result to the resolver for mutation`, async () => {
      const expectedMutation = `mutation: it passes rootValue`;
      app = createApp({
        schema,
        rootValue: () => expectedMutation
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            mutation test {
              testRootValue
            }
          `
        });
      res.body; //?
      expect(res.status).toStrictEqual(200);
      expect(res.body.data.testRootValue).toStrictEqual(expectedMutation);
    });

    it(`returns errors`, async () => {
      const expected = `Secret error message`;
      app = createApp({
        schema
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testError
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors[0].message).toStrictEqual(expected);
    });

    it(`applies formatError if provided`, async () => {
      const expected = `--blank--`;
      app = createApp({
        schema,
        formatError: (error) => {
          expect(error instanceof Error).toBe(true);
          return { message: expected };
        }
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testError
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors[0].message).toStrictEqual(expected);
    });

    it(`formatError receives error that passes instanceof checks`, async () => {
      const expected = `--blank--`;
      app = createApp({
        schema,
        formatError: (error) => {
          expect(error instanceof Error).toBe(true);
          expect(error instanceof GraphQLError).toBe(true);
          return { message: expected };
        }
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testError
            }
          `
        });
      expect(res.status).toStrictEqual(200);
      expect(res.body.errors[0].message).toStrictEqual(expected);
    });

    it(`allows for custom error formatting to sanitize`, async () => {
      app = createApp({
        schema: TestSchema,
        formatError(error) {
          return { message: `Custom error format: ${error.message}` };
        }
      });

      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            {
              thrower
            }
          `
        });

      expect(res.status).toStrictEqual(200);
      expect(JSON.parse(res.text)).toStrictEqual({
        data: null,
        errors: [
          {
            message: `Custom error format: Throws!`
          }
        ]
      });
    });

    it(`allows for custom error formatting to elaborate`, async () => {
      app = createApp({
        schema: TestSchema,
        formatError(error) {
          return {
            message: error.message,
            locations: error.locations,
            stack: `Stack trace`
          };
        }
      });

      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            {
              thrower
            }
          `
        });

      expect(res.status).toStrictEqual(200);
      expect(JSON.parse(res.text)).toStrictEqual({
        data: null,
        errors: [
          {
            message: `Throws!`,
            locations: [{ line: 3, column: 15 }],
            stack: `Stack trace`
          }
        ]
      });
    });

    it(`sends internal server error when formatError fails`, async () => {
      app = createApp({
        schema,
        formatError: () => {
          throw new Error(`I should be caught`);
        }
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testError
            }
          `
        });
      expect(res.body.errors[0].message).toStrictEqual(`Internal server error`);
    });

    it(`applies additional validationRules`, async () => {
      const expected = `alwaysInvalidRule was really invalid!`;
      const alwaysInvalidRule = (context): { enter: () => unknown } => ({
        enter(): unknown {
          context.reportError(new GraphQLError(expected));
          return BREAK;
        }
      });
      app = createApp({
        schema,
        validationRules: [alwaysInvalidRule]
      });
      const res = await request(app)
        .post(`/graphql`)
        .send({
          query: `
            query test {
              testString
            }
          `
        });
      expect(res.status).toStrictEqual(400);
      expect(res.body.errors[0].message).toStrictEqual(expected);
    });
  });

  describe(`server setup`, () => {
    it(`throws error on 404 routes`, async () => {
      app = createApp();

      const query = {
        query: `
          {
            testString
          }
        `
      };
      const res = await request(app).get(`/bogus-route`).query(query);
      expect(res.status).toStrictEqual(404);
    });
  });

  describe(`Persisted Queries`, () => {
    const query = `{testString}`;
    const query2 = `{ testString }`;

    const hash = sha256.create().update(query).hex();
    const extensions = {
      persistedQuery: {
        version: VERSION,
        sha256Hash: hash
      }
    };

    const extensions2 = {
      persistedQuery: {
        version: VERSION,
        sha256Hash: sha256.create().update(query2).hex()
      }
    };

    const createMockCache = (): KeyValueCache => {
      const map = new Map<string, string>();
      return {
        set: jest.fn(async (key, val) => {
          // eslint-disable-next-line @typescript-eslint/await-thenable
          await map.set(key, val);
        }),
        // eslint-disable-next-line @typescript-eslint/require-await
        get: jest.fn(async (key) => map.get(key)),
        // eslint-disable-next-line @typescript-eslint/require-await
        delete: jest.fn(async (key) => map.delete(key))
      };
    };

    // eslint-disable-next-line @typescript-eslint/no-shadow
    let didEncounterErrors: jest.Mock<
      ReturnType<GraphQLRequestListener["didEncounterErrors"]>,
      Parameters<GraphQLRequestListener["didEncounterErrors"]>
    >;

    let didResolveSource: jest.Mock<
      ReturnType<GraphQLRequestListener["didResolveSource"]>,
      Parameters<GraphQLRequestListener["didResolveSource"]>
    >;

    let cache: KeyValueCache;

    const createApqApp = (apqOptions: PersistedQueryOptions = {}): ReturnType<typeof createApp> =>
      createApp({
        schema,
        plugins: [
          {
            requestDidStart() {
              return {
                didResolveSource,
                didEncounterErrors
              };
            }
          }
        ],
        persistedQueries: {
          cache,
          ...apqOptions
        }
      });

    beforeEach(() => {
      cache = createMockCache();
      didResolveSource = jest.fn();
      didEncounterErrors = jest.fn();
    });

    it(`when ttlSeconds is set, passes ttl to the apq cache set call`, async () => {
      app = createApqApp({ ttl: 900 });

      await request(app).post(`/graphql`).send({
        extensions,
        query
      });

      // eslint-disable-next-line @typescript-eslint/unbound-method
      expect(cache.set).toHaveBeenCalledWith(
        expect.stringMatching(/^apq:/),
        query,
        expect.objectContaining({
          ttl: 900
        })
      );
      expect(didResolveSource.mock.calls[0][0]).toHaveProperty(`source`, query);
    });

    it(`when ttlSeconds is unset, ttl is not passed to apq cache`, async () => {
      app = createApqApp();

      await request(app).post(`/graphql`).send({
        extensions,
        query
      });

      // eslint-disable-next-line @typescript-eslint/unbound-method
      expect(cache.set).toHaveBeenCalledWith(
        expect.stringMatching(/^apq:/),
        `{testString}`,
        expect.not.objectContaining({
          ttl: 900
        })
      );
      expect(didResolveSource.mock.calls[0][0]).toHaveProperty(`source`, query);
    });

    it(`errors when version is not specified`, async () => {
      app = createApqApp();

      const result = await request(app)
        .get(`/graphql`)
        .query({
          query,
          extensions: JSON.stringify({
            persistedQuery: {
              // Version intentionally omitted.
              sha256Hash: extensions.persistedQuery.sha256Hash
            }
          })
        });

      expect(result).toMatchObject({
        status: 400,
        // Different integrations' response text varies in format.
        text: expect.stringContaining(`Unsupported persisted query version`),
        req: expect.objectContaining({
          method: `GET`
        })
      });

      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([
            expect.objectContaining({
              message: `Unsupported persisted query version`
            })
          ])
        })
      );
    });

    it(`errors when version is unsupported`, async () => {
      app = createApqApp();

      const result = await request(app)
        .get(`/graphql`)
        .query({
          query,
          extensions: JSON.stringify({
            persistedQuery: {
              // Version intentionally wrong.
              version: VERSION + 1,
              sha256Hash: extensions.persistedQuery.sha256Hash
            }
          })
        });

      expect(result).toMatchObject({
        status: 400,
        // Different integrations' response text varies in format.
        text: expect.stringContaining(`Unsupported persisted query version`),
        req: expect.objectContaining({
          method: `GET`
        })
      });

      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([
            expect.objectContaining({
              message: `Unsupported persisted query version`
            })
          ])
        })
      );
    });

    it(`errors when hash is mismatched`, async () => {
      app = createApqApp();

      const result = await request(app)
        .get(`/graphql`)
        .query({
          query,
          extensions: JSON.stringify({
            persistedQuery: {
              version: 1,
              // Sha intentionally wrong.
              sha256Hash: extensions.persistedQuery.sha256Hash.substr(0, 5)
            }
          })
        });

      expect(result).toMatchObject({
        status: 400,
        // Different integrations' response text varies in format.
        text: expect.stringContaining(`provided sha does not match query`),
        req: expect.objectContaining({
          method: `GET`
        })
      });

      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([
            expect.objectContaining({
              message: `provided sha does not match query`
            })
          ])
        })
      );

      expect(didResolveSource).not.toHaveBeenCalled();
    });

    it(`returns PersistedQueryNotFound on the first try`, async () => {
      app = createApqApp();

      const res = await request(app).post(`/graphql`).send({
        extensions
      });

      expect(res.body.data).toBeUndefined();
      expect(res.body.errors).toHaveLength(1);
      expect(res.body.errors[0].message).toStrictEqual(`PersistedQueryNotFound`);
      expect(res.body.errors[0].extensions.code).toStrictEqual(`PERSISTED_QUERY_NOT_FOUND`);

      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([expect.any(PersistedQueryNotFoundError)])
        })
      );

      expect(didResolveSource).not.toHaveBeenCalled();
    });

    it(`returns result on the second try`, async () => {
      app = createApqApp();

      await request(app).post(`/graphql`).send({
        extensions
      });

      // Only the first request should result in an error.
      expect(didEncounterErrors).toHaveBeenCalledTimes(1);
      expect(didEncounterErrors).toHaveBeenCalledWith(
        expect.objectContaining({
          errors: expect.arrayContaining([expect.any(PersistedQueryNotFoundError)])
        })
      );

      expect(didResolveSource).not.toHaveBeenCalled();

      const result = await request(app).post(`/graphql`).send({
        extensions,
        query
      });

      // There should be no additional errors now.  In other words, we'll
      // re-assert that we've been called the same single time that we
      // asserted above.
      expect(didEncounterErrors).toHaveBeenCalledTimes(1);

      expect(didResolveSource.mock.calls[0][0]).toHaveProperty(`source`, query);

      expect(result.body.data).toStrictEqual({ testString: `it works` });
      expect(result.body.errors).toBeUndefined();
    });

    it(`returns with batched persisted queries`, async () => {
      app = createApqApp();

      const errors = await request(app)
        .post(`/graphql`)
        .send([
          {
            extensions
          },
          {
            extensions: extensions2
          }
        ]);

      expect(errors.body[0].data).toBeUndefined();
      expect(errors.body[1].data).toBeUndefined();
      expect(errors.body[0].errors[0].message).toStrictEqual(`PersistedQueryNotFound`);
      expect(errors.body[0].errors[0].extensions.code).toStrictEqual(`PERSISTED_QUERY_NOT_FOUND`);
      expect(errors.body[1].errors[0].message).toStrictEqual(`PersistedQueryNotFound`);
      expect(errors.body[1].errors[0].extensions.code).toStrictEqual(`PERSISTED_QUERY_NOT_FOUND`);

      const result = await request(app)
        .post(`/graphql`)
        .send([
          {
            extensions,
            query
          },
          {
            extensions: extensions2,
            query: query2
          }
        ]);

      expect(result.body[0].data).toStrictEqual({ testString: `it works` });
      expect(result.body[0].data).toStrictEqual({ testString: `it works` });
      expect(result.body.errors).toBeUndefined();
    });

    it(`returns result on the persisted query`, async () => {
      app = createApqApp();

      await request(app).post(`/graphql`).send({
        extensions
      });

      expect(didResolveSource).not.toHaveBeenCalled();

      await request(app).post(`/graphql`).send({
        extensions,
        query
      });
      const res = await request(app).post(`/graphql`).send({
        extensions
      });

      expect(didResolveSource.mock.calls[0][0]).toHaveProperty(`source`, query);

      expect(res.body.data).toStrictEqual({ testString: `it works` });
      expect(res.body.errors).toBeUndefined();
    });

    it(`returns error when hash does not match`, async () => {
      app = createApqApp();

      const res = await request(app)
        .post(`/graphql`)
        .send({
          extensions: {
            persistedQuery: {
              version: VERSION,
              sha: `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
            }
          },
          query
        });
      expect(res.status).toStrictEqual(400);
      expect((res.error as HTTPError).text).toMatch(/does not match query/);
      expect(didResolveSource).not.toHaveBeenCalled();
    });

    it(`returns correct result using get request`, async () => {
      app = createApqApp();

      await request(app).post(`/graphql`).send({
        extensions,
        query
      });
      const result = await request(app)
        .get(`/graphql`)
        .query({
          extensions: JSON.stringify(extensions)
        });
      expect(result.body.data).toStrictEqual({ testString: `it works` });
      expect(didResolveSource.mock.calls[0][0]).toHaveProperty(`source`, query);
    });
  });
});
Example #28
Source File: SentryLink.test.ts    From apollo-link-sentry with MIT License 4 votes vote down vote up
describe('SentryLink', () => {
  beforeAll(() => {
    Sentry.init({
      dsn: 'https://[email protected]/000001',
      transport: sentryTransport,
      defaultIntegrations: false,
    });
  });

  beforeEach(() => {
    testkit.reset();

    Sentry.configureScope((scope) => {
      scope.clearBreadcrumbs();
      scope.setTransactionName();
      scope.setFingerprint([]);
    });
  });

  it('should attach a sentry breadcrumb for an apolloOperation', (done) => {
    const link = ApolloLink.from([new SentryLink(), nullLink]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        expect(report.breadcrumbs).toHaveLength(1);

        const [breadcrumb] = report.breadcrumbs;
        expect(breadcrumb.type).toBe('http');

        const data = breadcrumb.data as GraphQLBreadcrumb['data'];
        expect(data).not.toHaveProperty('query');
        expect(data).not.toHaveProperty('variables');
        expect(data).not.toHaveProperty('result');
        expect(data).not.toHaveProperty('error');
        expect(data).not.toHaveProperty('cache');
        expect(data).not.toHaveProperty('context');

        done();
      },
    });
  });

  it('should attach a breadcrumb for each apolloOperation', (done) => {
    const includeErrorAndResultOptions: SentryLinkOptions = {
      attachBreadcrumbs: { includeFetchResult: true, includeError: true },
    };

    const result = { data: { foo: true } };
    const withResult = ApolloLink.from([
      new SentryLink(includeErrorAndResultOptions),
      new ApolloLink(
        () =>
          new Observable((observer) => {
            observer.next(result);
            observer.complete();
          }),
      ),
    ]);

    const error = new Error();
    const withError = ApolloLink.from([
      new SentryLink(includeErrorAndResultOptions),
      new ApolloLink(() => {
        return new Observable((observer) => observer.error(error));
      }),
    ]);

    execute(withResult, {
      query: parse(`query SuccessQuery { foo }`),
    }).subscribe({
      complete() {
        execute(withError, {
          query: parse(`mutation FailureMutation { bar }`),
        }).subscribe({
          error(exception) {
            Sentry.captureException(exception);

            const [report] = testkit.reports();
            expect(report.breadcrumbs).toHaveLength(2);

            const [success, failure] =
              report.breadcrumbs as Array<GraphQLBreadcrumb>;

            expect(success.category).toBe('graphql.query');
            expect(success.level).toBe(Severity.Info);
            expect(success.data.operationName).toBe('SuccessQuery');
            expect(success.data.fetchResult).toBe(stringify(result));
            expect(success.data).not.toHaveProperty('error');

            expect(failure.category).toBe('graphql.mutation');
            expect(failure.level).toBe(Severity.Error);
            expect(failure.data.operationName).toBe('FailureMutation');
            expect(failure.data).not.toHaveProperty('result');
            expect(failure.data.error).toBe(stringify(error));

            done();
          },
        });
      },
    });
  });

  it('should attach a breadcrumb with an error and null data', (done) => {
    const errors = [new GraphQLError('failure')];
    const result = {
      data: { foo: null },
      errors: errors,
    };
    const withPartialErrors = ApolloLink.from([
      new SentryLink({
        attachBreadcrumbs: { includeError: true },
      }),
      new ApolloLink(
        () =>
          new Observable((observer) => {
            observer.next(result);
            observer.complete();
          }),
      ),
    ]);

    execute(withPartialErrors, {
      query: parse(`query PartialErrors { foo }`),
    }).subscribe({
      complete() {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        expect(report.breadcrumbs).toHaveLength(1);

        const [breadcrumb] = report.breadcrumbs as Array<GraphQLBreadcrumb>;

        expect(breadcrumb.category).toBe('graphql.query');
        expect(breadcrumb.level).toBe(Severity.Error);
        expect(breadcrumb.data.operationName).toBe('PartialErrors');
        expect(breadcrumb.data.fetchResult).not.toBeDefined();
        expect(breadcrumb.data.error).toBe(
          stringify(new ApolloError({ graphQLErrors: errors })),
        );

        done();
      },
    });
  });

  it('should attach a breadcrumb with partial errors', (done) => {
    const errors = [
      new GraphQLError('partial failure'),
      new GraphQLError('another failure'),
    ];
    const result = {
      data: { foo: true },
      errors: errors,
    };
    const withPartialErrors = ApolloLink.from([
      new SentryLink({
        attachBreadcrumbs: { includeFetchResult: true, includeError: true },
      }),
      new ApolloLink(
        () =>
          new Observable((observer) => {
            observer.next(result);
            observer.complete();
          }),
      ),
    ]);

    execute(withPartialErrors, {
      query: parse(`query PartialErrors { foo }`),
    }).subscribe({
      complete() {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        expect(report.breadcrumbs).toHaveLength(1);

        const [breadcrumb] = report.breadcrumbs as Array<GraphQLBreadcrumb>;

        expect(breadcrumb.category).toBe('graphql.query');
        expect(breadcrumb.level).toBe(Severity.Error);
        expect(breadcrumb.data.operationName).toBe('PartialErrors');
        expect(breadcrumb.data.fetchResult).toBe(stringify(result));
        expect(breadcrumb.data.error).toBe(
          stringify(new ApolloError({ graphQLErrors: errors })),
        );

        done();
      },
    });
  });

  it('should mark results with errors with level error', (done) => {
    const link = ApolloLink.from([
      new SentryLink(),
      new ApolloLink(
        () =>
          new Observable((observer) => {
            observer.next({
              errors: [new GraphQLError('some message')],
            });
            observer.complete();
          }),
      ),
    ]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        expect(report.breadcrumbs).toHaveLength(1);

        const [breadcrumb] = report.breadcrumbs;
        expect(breadcrumb.level).toBe(Severity.Error);

        done();
      },
    });
  });

  it('should allow inclusion of results from server errors', (done) => {
    const message = 'some message';
    const fetchResult = { errors: [{ message: 'GraphQL error message' }] };

    const serverError: ServerError = {
      name: 'bla',
      message: message,
      response: new Response(),
      result: fetchResult,
      statusCode: 500,
    };

    const link = ApolloLink.from([
      new SentryLink({
        attachBreadcrumbs: {
          includeError: true,
          includeFetchResult: true,
        },
      }),
      new ApolloLink(
        () =>
          new Observable((observer) => {
            observer.error(serverError);
          }),
      ),
    ]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      error: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        expect(report.breadcrumbs).toHaveLength(1);

        const [breadcrumb] = report.breadcrumbs as Array<GraphQLBreadcrumb>;
        expect(breadcrumb.data.error).toEqual(
          stringify({
            name: serverError.name,
            message: serverError.message,
            statusCode: serverError.statusCode,
          }),
        );
        expect(breadcrumb.data.fetchResult).toEqual(stringify(fetchResult));

        done();
      },
    });
  });

  it('should not attach the breadcrumb if the option is disabled', (done) => {
    const link = ApolloLink.from([
      new SentryLink({ attachBreadcrumbs: false }),
      nullLink,
    ]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        expect(report.breadcrumbs).toHaveLength(0);

        done();
      },
    });
  });

  it('should set the transaction name by default', (done) => {
    const link = ApolloLink.from([new SentryLink(), nullLink]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        const { originalReport } = report;

        expect(originalReport.transaction).toBe('Foo');

        done();
      },
    });
  });

  it('should not set the transaction if the option is disabled', (done) => {
    const link = ApolloLink.from([
      new SentryLink({ setTransaction: false }),
      nullLink,
    ]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        const { originalReport } = report;

        expect(originalReport.transaction).toBeUndefined();

        done();
      },
    });
  });

  it('should set the fingerprint by default', (done) => {
    const link = ApolloLink.from([new SentryLink(), nullLink]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        const { originalReport } = report;

        expect(originalReport.fingerprint).toStrictEqual([
          DEFAULT_FINGERPRINT,
          'Foo',
        ]);

        done();
      },
    });
  });

  it('should not set the fingerprint if the option is disabled', (done) => {
    const link = ApolloLink.from([
      new SentryLink({ setFingerprint: false }),
      nullLink,
    ]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        const { originalReport } = report;

        expect(originalReport.fingerprint).toBeUndefined();

        done();
      },
    });
  });

  it('should allow filtering out operations', (done) => {
    const link = ApolloLink.from([
      new SentryLink({
        shouldHandleOperation: (operation) =>
          operation.operationName === 'Handle',
      }),
      nullLink,
    ]);

    execute(link, { query: parse(`query Handle { foo }`) }).subscribe({
      complete: () => {
        execute(link, { query: parse(`query Discard { foo }`) }).subscribe({
          complete: () => {
            Sentry.captureException(new Error());

            const [report] = testkit.reports();

            expect(report.breadcrumbs).toHaveLength(1);
            const [handle] = report.breadcrumbs;

            expect(handle.data?.operationName).toBe('Handle');

            done();
          },
        });
      },
    });
  });

  it('should allow altering the breadcrumb with beforeBreadcrumb', (done) => {
    const link = ApolloLink.from([
      new SentryLink({
        attachBreadcrumbs: {
          transform: (breadcrumb, operation) => ({
            ...breadcrumb,
            data: {
              ...breadcrumb.data,
              foo: operation.operationName,
            },
          }),
        },
      }),
      nullLink,
    ]);

    execute(link, { query: parse(`query Foo { foo }`) }).subscribe({
      complete: () => {
        Sentry.captureException(new Error());

        const [report] = testkit.reports();
        const [breadcrumb] = report.breadcrumbs;

        expect(breadcrumb.data?.foo).toBe('Foo');

        done();
      },
    });
  });
});
Example #29
Source File: selection-set-depth.ts    From graphql-eslint with MIT License 4 votes vote down vote up
rule: GraphQLESLintRule<[SelectionSetDepthRuleConfig]> = {
  meta: {
    type: 'suggestion',
    hasSuggestions: true,
    docs: {
      category: 'Operations',
      description:
        'Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://npmjs.com/package/graphql-depth-limit).',
      url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
      requiresSiblings: true,
      examples: [
        {
          title: 'Incorrect',
          usage: [{ maxDepth: 1 }],
          code: `
            query deep2 {
              viewer { # Level 0
                albums { # Level 1
                  title # Level 2
                }
              }
            }
          `,
        },
        {
          title: 'Correct',
          usage: [{ maxDepth: 4 }],
          code: `
            query deep2 {
              viewer { # Level 0
                albums { # Level 1
                  title # Level 2
                }
              }
            }
          `,
        },
        {
          title: 'Correct (ignored field)',
          usage: [{ maxDepth: 1, ignore: ['albums'] }],
          code: `
            query deep2 {
              viewer { # Level 0
                albums { # Level 1
                  title # Level 2
                }
              }
            }
          `,
        },
      ],
      recommended: true,
      configOptions: [{ maxDepth: 7 }],
    },
    schema: {
      type: 'array',
      minItems: 1,
      maxItems: 1,
      items: {
        type: 'object',
        additionalProperties: false,
        required: ['maxDepth'],
        properties: {
          maxDepth: {
            type: 'number',
          },
          ignore: ARRAY_DEFAULT_OPTIONS,
        },
      },
    },
  },
  create(context) {
    let siblings: SiblingOperations | null = null;

    try {
      siblings = requireSiblingsOperations(RULE_ID, context);
    } catch {
      logger.warn(
        `Rule "${RULE_ID}" works best with siblings operations loaded. For more info: https://bit.ly/graphql-eslint-operations`
      );
    }

    const { maxDepth, ignore = [] } = context.options[0];
    const checkFn = depthLimit(maxDepth, { ignore });

    return {
      'OperationDefinition, FragmentDefinition'(node: GraphQLESTreeNode<ExecutableDefinitionNode>) {
        try {
          const rawNode = node.rawNode();
          const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : [];
          const document: DocumentNode = {
            kind: Kind.DOCUMENT,
            definitions: [rawNode, ...fragmentsInUse],
          };

          checkFn({
            getDocument: () => document,
            reportError(error: GraphQLError) {
              const { line, column } = error.locations[0];
              context.report({
                loc: {
                  line,
                  column: column - 1,
                },
                message: error.message,
                suggest: [
                  {
                    desc: 'Remove selections',
                    fix(fixer) {
                      const ancestors = context.getAncestors();
                      const token = (ancestors[0] as AST.Program).tokens.find(
                        token => token.loc.start.line === line && token.loc.start.column === column - 1
                      );
                      const sourceCode = context.getSourceCode();
                      const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]) as any;
                      const parentNode = foundNode.parent.parent;
                      return fixer.remove(foundNode.kind === 'Name' ? parentNode.parent : parentNode);
                    },
                  },
                ],
              });
            },
          });
        } catch (e) {
          logger.warn(
            `Rule "${RULE_ID}" check failed due to a missing siblings operations. For more info: https://bit.ly/graphql-eslint-operations`,
            e
          );
        }
      },
    };
  },
}