UNPKG

msw

Version:

Seamless REST/GraphQL API mocking library for browser and Node.js.

1 lines 15.8 kB
{"version":3,"sources":["../../../src/core/handlers/GraphQLHandler.ts"],"sourcesContent":["import {\n parse,\n type DocumentNode,\n type GraphQLError,\n type OperationTypeNode,\n} from 'graphql'\nimport {\n DefaultBodyType,\n RequestHandler,\n RequestHandlerDefaultInfo,\n RequestHandlerOptions,\n ResponseResolver,\n} from './RequestHandler'\nimport { getTimestamp } from '../utils/logging/getTimestamp'\nimport { getStatusCodeColor } from '../utils/logging/getStatusCodeColor'\nimport { serializeRequest } from '../utils/logging/serializeRequest'\nimport { serializeResponse } from '../utils/logging/serializeResponse'\nimport { Match, matchRequestUrl, Path } from '../utils/matching/matchRequestUrl'\nimport {\n ParsedGraphQLRequest,\n GraphQLMultipartRequestBody,\n parseGraphQLRequest,\n parseDocumentNode,\n ParsedGraphQLQuery,\n} from '../utils/internal/parseGraphQLRequest'\nimport { toPublicUrl } from '../utils/request/toPublicUrl'\nimport { devUtils } from '../utils/internal/devUtils'\nimport { getAllRequestCookies } from '../utils/request/getRequestCookies'\nimport { invariant } from 'outvariant'\n\nexport interface DocumentTypeDecoration<\n Result = { [key: string]: any },\n Variables = { [key: string]: any },\n> {\n __apiType?: (variables: Variables) => Result\n __resultType?: Result\n __variablesType?: Variables\n}\n\nexport type GraphQLOperationType = OperationTypeNode | 'all'\nexport type GraphQLHandlerNameSelector = DocumentNode | RegExp | string\n\nexport type GraphQLQuery = Record<string, any> | null\nexport type GraphQLVariables = Record<string, any>\n\nexport interface GraphQLHandlerInfo extends RequestHandlerDefaultInfo {\n operationType: GraphQLOperationType\n operationName: GraphQLHandlerNameSelector | GraphQLCustomPredicate\n}\n\nexport type GraphQLRequestParsedResult = {\n match: Match\n cookies: Record<string, string>\n} & (\n | ParsedGraphQLRequest<GraphQLVariables>\n /**\n * An empty version of the ParsedGraphQLRequest\n * which simplifies the return type of the resolver\n * when the request is to a non-matching endpoint\n */\n | {\n operationType?: undefined\n operationName?: undefined\n query?: undefined\n variables?: undefined\n }\n)\n\nexport type GraphQLResolverExtras<Variables extends GraphQLVariables> = {\n query: string\n operationName: string\n variables: Variables\n cookies: Record<string, string>\n}\n\nexport type GraphQLRequestBody<VariablesType extends GraphQLVariables> =\n | GraphQLJsonRequestBody<VariablesType>\n | GraphQLMultipartRequestBody\n | Record<string, any>\n | undefined\n\nexport interface GraphQLJsonRequestBody<Variables extends GraphQLVariables> {\n query: string\n variables?: Variables\n}\n\nexport type GraphQLResponseBody<BodyType extends DefaultBodyType> =\n | {\n data?: BodyType | null\n errors?: readonly Partial<GraphQLError>[] | null\n extensions?: Record<string, any>\n }\n | null\n | undefined\n\nexport type GraphQLCustomPredicate = (args: {\n request: Request\n query: string\n operationType: GraphQLOperationType\n operationName: string\n variables: GraphQLVariables\n cookies: Record<string, string>\n}) => GraphQLCustomPredicateResult | Promise<GraphQLCustomPredicateResult>\n\nexport type GraphQLCustomPredicateResult = boolean | { matches: boolean }\n\nexport type GraphQLPredicate<Query = any, Variables = any> =\n | GraphQLHandlerNameSelector\n | DocumentTypeDecoration<Query, Variables>\n | GraphQLCustomPredicate\n\nexport function isDocumentNode(\n value: DocumentNode | any,\n): value is DocumentNode {\n if (value == null) {\n return false\n }\n\n return typeof value === 'object' && 'kind' in value && 'definitions' in value\n}\n\nfunction isDocumentTypeDecoration(\n value: any,\n): value is DocumentTypeDecoration<any, any> {\n return value instanceof String\n}\n\nexport class GraphQLHandler extends RequestHandler<\n GraphQLHandlerInfo,\n GraphQLRequestParsedResult,\n GraphQLResolverExtras<any>\n> {\n private endpoint: Path\n\n static parsedRequestCache = new WeakMap<\n Request,\n ParsedGraphQLRequest<GraphQLVariables>\n >()\n\n static #parseOperationName(\n predicate: GraphQLPredicate,\n operationType: GraphQLOperationType,\n ): GraphQLHandlerInfo['operationName'] {\n const getOperationName = (node: ParsedGraphQLQuery): string => {\n invariant(\n node.operationType === operationType,\n 'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected \"%s\" but got \"%s\").',\n operationType,\n node.operationType,\n )\n\n invariant(\n node.operationName,\n 'Failed to create a GraphQL handler: provided a DocumentNode without operation name',\n )\n\n return node.operationName\n }\n\n if (isDocumentNode(predicate)) {\n return getOperationName(parseDocumentNode(predicate))\n }\n\n if (isDocumentTypeDecoration(predicate)) {\n const documentNode = parse(predicate.toString())\n\n invariant(\n isDocumentNode(documentNode),\n 'Failed to create a GraphQL handler: given TypedDocumentString (%s) does not produce a valid DocumentNode',\n predicate,\n )\n\n return getOperationName(parseDocumentNode(documentNode))\n }\n\n return predicate\n }\n\n constructor(\n operationType: GraphQLOperationType,\n predicate: GraphQLPredicate,\n endpoint: Path,\n resolver: ResponseResolver<GraphQLResolverExtras<any>, any, any>,\n options?: RequestHandlerOptions,\n ) {\n const operationName = GraphQLHandler.#parseOperationName(\n predicate,\n operationType,\n )\n\n const displayOperationName =\n typeof operationName === 'function' ? '[custom predicate]' : operationName\n\n const header =\n operationType === 'all'\n ? `${operationType} (origin: ${endpoint.toString()})`\n : `${operationType}${displayOperationName ? ` ${displayOperationName}` : ''} (origin: ${endpoint.toString()})`\n\n super({\n info: {\n header,\n operationType,\n operationName: GraphQLHandler.#parseOperationName(\n predicate,\n operationType,\n ),\n },\n resolver,\n options,\n })\n\n this.endpoint = endpoint\n }\n\n /**\n * Parses the request body, once per request, cached across all\n * GraphQL handlers. This is done to avoid multiple parsing of the\n * request body, which each requires a clone of the request.\n */\n async parseGraphQLRequestOrGetFromCache(\n request: Request,\n ): Promise<ParsedGraphQLRequest<GraphQLVariables>> {\n if (!GraphQLHandler.parsedRequestCache.has(request)) {\n GraphQLHandler.parsedRequestCache.set(\n request,\n await parseGraphQLRequest(request).catch((error) => {\n console.error(error)\n return undefined\n }),\n )\n }\n\n return GraphQLHandler.parsedRequestCache.get(request)\n }\n\n async parse(args: { request: Request }): Promise<GraphQLRequestParsedResult> {\n /**\n * If the request doesn't match a specified endpoint, there's no\n * need to parse it since there's no case where we would handle this\n */\n const match = matchRequestUrl(new URL(args.request.url), this.endpoint)\n const cookies = getAllRequestCookies(args.request)\n\n if (!match.matches) {\n return {\n match,\n cookies,\n }\n }\n\n const parsedResult = await this.parseGraphQLRequestOrGetFromCache(\n args.request,\n )\n\n if (typeof parsedResult === 'undefined') {\n return {\n match,\n cookies,\n }\n }\n\n return {\n match,\n cookies,\n query: parsedResult.query,\n operationType: parsedResult.operationType,\n operationName: parsedResult.operationName,\n variables: parsedResult.variables,\n }\n }\n\n async predicate(args: {\n request: Request\n parsedResult: GraphQLRequestParsedResult\n }): Promise<boolean> {\n if (args.parsedResult.operationType === undefined) {\n return false\n }\n\n if (!args.parsedResult.operationName && this.info.operationType !== 'all') {\n const publicUrl = toPublicUrl(args.request.url)\n\n devUtils.warn(`\\\nFailed to intercept a GraphQL request at \"${args.request.method} ${publicUrl}\": anonymous GraphQL operations are not supported.\n\nConsider naming this operation or using \"graphql.operation()\" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver`)\n return false\n }\n\n const hasMatchingOperationType =\n this.info.operationType === 'all' ||\n args.parsedResult.operationType === this.info.operationType\n\n /**\n * Check if the operation name matches the outgoing GraphQL request.\n * @note Unlike the HTTP handler, the custom predicate functions are invoked\n * during predicate, not parsing, because GraphQL request parsing happens first,\n * and non-GraphQL requests are filtered out automatically.\n */\n const hasMatchingOperationName = await this.matchOperationName({\n request: args.request,\n parsedResult: args.parsedResult,\n })\n\n return (\n args.parsedResult.match.matches &&\n hasMatchingOperationType &&\n hasMatchingOperationName\n )\n }\n\n private async matchOperationName(args: {\n request: Request\n parsedResult: GraphQLRequestParsedResult\n }): Promise<boolean> {\n if (typeof this.info.operationName === 'function') {\n const customPredicateResult = await this.info.operationName({\n request: args.request,\n ...this.extendResolverArgs({\n request: args.request,\n parsedResult: args.parsedResult,\n }),\n })\n\n /**\n * @note Keep the { matches } signature in case we decide to support path parameters\n * in GraphQL handlers. If that happens, the custom predicate would have to be moved\n * to the parsing phase, the same as we have for the HttpHandler, and the user will\n * have a possibility to return parsed path parameters from the custom predicate.\n */\n return typeof customPredicateResult === 'boolean'\n ? customPredicateResult\n : customPredicateResult.matches\n }\n\n if (this.info.operationName instanceof RegExp) {\n return this.info.operationName.test(args.parsedResult.operationName || '')\n }\n\n return args.parsedResult.operationName === this.info.operationName\n }\n\n protected extendResolverArgs(args: {\n request: Request\n parsedResult: GraphQLRequestParsedResult\n }) {\n return {\n query: args.parsedResult.query || '',\n operationType: args.parsedResult.operationType!,\n operationName: args.parsedResult.operationName || '',\n variables: args.parsedResult.variables || {},\n cookies: args.parsedResult.cookies,\n }\n }\n\n async log(args: {\n request: Request\n response: Response\n parsedResult: GraphQLRequestParsedResult\n }) {\n const loggedRequest = await serializeRequest(args.request)\n const loggedResponse = await serializeResponse(args.response)\n const statusColor = getStatusCodeColor(loggedResponse.status)\n const requestInfo = args.parsedResult.operationName\n ? `${args.parsedResult.operationType} ${args.parsedResult.operationName}`\n : `anonymous ${args.parsedResult.operationType}`\n\n console.groupCollapsed(\n devUtils.formatMessage(\n `${getTimestamp()} ${requestInfo} (%c${loggedResponse.status} ${\n loggedResponse.statusText\n }%c)`,\n ),\n `color:${statusColor}`,\n 'color:inherit',\n )\n // eslint-disable-next-line no-console\n console.log('Request:', loggedRequest)\n // eslint-disable-next-line no-console\n console.log('Handler:', this)\n // eslint-disable-next-line no-console\n console.log('Response:', loggedResponse)\n console.groupEnd()\n }\n}\n"],"mappings":"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","names":[]}