Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DocumentNode } from 'graphql'
import { Path } from 'node-match-path'
import { ResponseResolver } from './handlers/RequestHandler'
import {
Expand All @@ -9,6 +10,14 @@ import {
GraphQLHandlerNameSelector,
} from './handlers/GraphQLHandler'

export interface TypedDocumentNode<
Result = { [key: string]: any },
Variables = { [key: string]: any },
> extends DocumentNode {
__resultType?: Result
__variablesType?: Variables
}

function createScopedGraphQLHandler(
operationType: ExpectedOperationTypeNode,
url: Path,
Expand All @@ -17,7 +26,10 @@ function createScopedGraphQLHandler(
Query extends Record<string, any>,
Variables extends GraphQLVariables = GraphQLVariables,
>(
operationName: GraphQLHandlerNameSelector,
operationName:
| GraphQLHandlerNameSelector
| DocumentNode
| TypedDocumentNode<Query, Variables>,
resolver: ResponseResolver<
GraphQLRequest<Variables>,
GraphQLContext<Query>
Expand Down
69 changes: 69 additions & 0 deletions src/handlers/GraphQLHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @jest-environment jsdom
*/
import { parse } from 'graphql'
import { Headers } from 'headers-utils/lib'
import { context } from '..'
import { createMockedRequest } from '../../test/support/utils'
Expand All @@ -10,6 +11,7 @@ import {
GraphQLHandler,
GraphQLRequest,
GraphQLRequestBody,
isDocumentNode,
} from './GraphQLHandler'
import { MockedRequest, ResponseResolver } from './RequestHandler'

Expand Down Expand Up @@ -82,6 +84,51 @@ describe('info', () => {
expect(handler.info.operationType).toEqual('mutation')
expect(handler.info.operationName).toEqual('Login')
})

test('parses a query operation name from a given DocumentNode', () => {
const node = parse(`
query GetUser {
user {
firstName
}
}
`)

const handler = new GraphQLHandler('query', node, '*', resolver)

expect(handler.info).toHaveProperty('header', 'query GetUser (origin: *)')
expect(handler.info).toHaveProperty('operationType', 'query')
expect(handler.info).toHaveProperty('operationName', 'GetUser')
})

test('parses a mutation operation name from a given DocumentNode', () => {
const node = parse(`
mutation Login {
user {
id
}
}
`)
const handler = new GraphQLHandler('mutation', node, '*', resolver)

expect(handler.info).toHaveProperty('header', 'mutation Login (origin: *)')
expect(handler.info).toHaveProperty('operationType', 'mutation')
expect(handler.info).toHaveProperty('operationName', 'Login')
})

test('throws an exception given a DocumentNode with a mismatched operation type', () => {
const node = parse(`
mutation CreateUser {
user {
firstName
}
}
`)

expect(() => new GraphQLHandler('query', node, '*', resolver)).toThrow(
'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "query", but got "mutation").',
)
})
})

describe('parse', () => {
Expand Down Expand Up @@ -374,3 +421,25 @@ describe('run', () => {
expect(result).toBeNull()
})
})

describe('isDocumentNode', () => {
it('returns true given a valid DocumentNode', () => {
const node = parse(`
query GetUser {
user {
login
}
}
`)

expect(isDocumentNode(node)).toEqual(true)
})

it('returns false given an arbitrary input', () => {
expect(isDocumentNode(null)).toEqual(false)
expect(isDocumentNode(undefined)).toEqual(false)
expect(isDocumentNode('')).toEqual(false)
expect(isDocumentNode('value')).toEqual(false)
expect(isDocumentNode(/value/)).toEqual(false)
})
})
39 changes: 35 additions & 4 deletions src/handlers/GraphQLHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OperationTypeNode } from 'graphql'
import { DocumentNode, OperationTypeNode } from 'graphql'
import { Path } from 'node-match-path'
import { SerializedResponse } from '../setupWorker/glossary'
import { set } from '../context/set'
Expand All @@ -22,13 +22,14 @@ import {
ParsedGraphQLRequest,
GraphQLMultipartRequestBody,
parseGraphQLRequest,
parseDocumentNode,
} from '../utils/internal/parseGraphQLRequest'
import { getPublicUrlFromRequest } from '../utils/request/getPublicUrlFromRequest'
import { tryCatch } from '../utils/internal/tryCatch'
import { devUtils } from '../utils/internal/devUtils'

export type ExpectedOperationTypeNode = OperationTypeNode | 'all'
export type GraphQLHandlerNameSelector = RegExp | string
export type GraphQLHandlerNameSelector = DocumentNode | RegExp | string

// GraphQL related context should contain utility functions
// useful for GraphQL. Functions like `xml()` bear no value
Expand Down Expand Up @@ -76,6 +77,16 @@ export interface GraphQLRequest<Variables extends GraphQLVariables>
variables: Variables
}

export function isDocumentNode(
value: DocumentNode | any,
): value is DocumentNode {
if (value == null) {
return false
}

return typeof value === 'object' && 'kind' in value && 'definitions' in value
}

export class GraphQLHandler<
Request extends GraphQLRequest<any> = GraphQLRequest<any>,
> extends RequestHandler<
Expand All @@ -92,16 +103,36 @@ export class GraphQLHandler<
endpoint: Path,
resolver: ResponseResolver<any, any>,
) {
let resolvedOperationName = operationName

if (isDocumentNode(operationName)) {
const parsedNode = parseDocumentNode(operationName)

if (parsedNode.operationType !== operationType) {
throw new Error(
`Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "${operationType}", but got "${parsedNode.operationType}").`,
)
}

if (!parsedNode.operationName) {
throw new Error(
`Failed to create a GraphQL handler: provided a DocumentNode with no operation name.`,
)
}

resolvedOperationName = parsedNode.operationName
}

const header =
operationType === 'all'
? `${operationType} (origin: ${endpoint.toString()})`
: `${operationType} ${operationName} (origin: ${endpoint.toString()})`
: `${operationType} ${resolvedOperationName} (origin: ${endpoint.toString()})`

super({
info: {
header,
operationType,
operationName,
operationName: resolvedOperationName,
},
ctx: graphqlContext,
resolver,
Expand Down
28 changes: 18 additions & 10 deletions src/utils/internal/parseGraphQLRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { OperationDefinitionNode, OperationTypeNode, parse } from 'graphql'
import {
DocumentNode,
OperationDefinitionNode,
OperationTypeNode,
parse,
} from 'graphql'
import { GraphQLVariables } from '../../handlers/GraphQLHandler'
import { MockedRequest } from '../../handlers/RequestHandler'
import { getPublicUrlFromRequest } from '../request/getPublicUrlFromRequest'
Expand All @@ -23,18 +28,21 @@ export type ParsedGraphQLRequest<
})
| undefined

export function parseDocumentNode(node: DocumentNode): ParsedGraphQLQuery {
const operationDef = node.definitions.find((def) => {
return def.kind === 'OperationDefinition'
}) as OperationDefinitionNode

return {
operationType: operationDef?.operation,
operationName: operationDef?.name?.value,
}
}

function parseQuery(query: string): ParsedGraphQLQuery | Error {
try {
const ast = parse(query)

const operationDef = ast.definitions.find((def) => {
return def.kind === 'OperationDefinition'
}) as OperationDefinitionNode

return {
operationType: operationDef?.operation,
operationName: operationDef?.name?.value,
}
return parseDocumentNode(ast)
} catch (error) {
return error
}
Expand Down
70 changes: 70 additions & 0 deletions test/graphql-api/document-node.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { parse } from 'graphql'
import { setupWorker, graphql } from 'msw'

const GetUser = parse(`
query GetUser {
user {
firstName
}
}
`)

const Login = parse(`
mutation Login($username: String!) {
session {
id
}
user {
username
}
}
`)

const GetSubscription = parse(`
query GetSubscription {
subscription {
id
}
}
`)

const github = graphql.link('https://api.github.com/graphql')

const worker = setupWorker(
// "DocumentNode" can be used as the expected query/mutation.
graphql.query(GetUser, (req, res, ctx) => {
return res(
ctx.data({
// Note that inferring the query body and variables
// is impossible with the native "DocumentNode".
// Consider using tools like GraphQL Code Generator.
user: {
firstName: 'John',
},
}),
)
}),
graphql.mutation(Login, (req, res, ctx) => {
return res(
ctx.data({
session: {
id: 'abc-123',
},
user: {
username: req.variables.username,
},
}),
)
}),
github.query(GetSubscription, (req, res, ctx) => {
return res(
ctx.data({
subscription: {
id: 123,
},
}),
)
}),
)

worker.start()
Loading