Skip to content

Commit 531e8ce

Browse files
longilitykettanaito
authored andcommitted
feat(gql): support TypedDocumentNode
Uses DocumentNode in GraphQL tests feat: remove @graphql-typed-document-node/core and duplicate `TypedDocumentNode` interface fix: eslint issues test: TypedDocumentNode expected typings Adds positive assertions to "graphql.test-d.ts"
1 parent a1aa7db commit 531e8ce

8 files changed

Lines changed: 382 additions & 15 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"devDependencies": {
8888
"@babel/core": "^7.14.3",
8989
"@babel/preset-env": "^7.14.2",
90+
"@graphql-typed-document-node/core": "^3.1.0",
9091
"@open-draft/test-server": "^0.2.3",
9192
"@rollup/plugin-commonjs": "^19.0.0",
9293
"@rollup/plugin-inject": "^4.0.2",

src/graphql.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DocumentNode } from 'graphql'
12
import { Mask } from './setupWorker/glossary'
23
import { ResponseResolver } from './handlers/RequestHandler'
34
import {
@@ -9,6 +10,18 @@ import {
910
GraphQLHandlerNameSelector,
1011
} from './handlers/GraphQLHandler'
1112

13+
export interface TypedDocumentNode<
14+
Result = {
15+
[key: string]: any
16+
},
17+
Variables = {
18+
[key: string]: any
19+
}
20+
> extends DocumentNode {
21+
__resultType?: Result
22+
__variablesType?: Variables
23+
}
24+
1225
function createScopedGraphQLHandler(
1326
operationType: ExpectedOperationTypeNode,
1427
url: Mask,
@@ -17,7 +30,10 @@ function createScopedGraphQLHandler(
1730
Query extends Record<string, any>,
1831
Variables extends GraphQLVariables = GraphQLVariables
1932
>(
20-
operationName: GraphQLHandlerNameSelector,
33+
operationName:
34+
| GraphQLHandlerNameSelector
35+
| DocumentNode
36+
| TypedDocumentNode<Query, Variables>,
2137
resolver: ResponseResolver<
2238
GraphQLRequest<Variables>,
2339
GraphQLContext<Query>

src/handlers/GraphQLHandler.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { parse } from 'graphql'
12
import { Headers } from 'headers-utils/lib'
23
import { context } from '..'
34
import { createMockedRequest } from '../../test/support/utils'
@@ -7,6 +8,7 @@ import {
78
GraphQLHandler,
89
GraphQLRequest,
910
GraphQLRequestBody,
11+
isDocumentNode,
1012
} from './GraphQLHandler'
1113
import { MockedRequest, ResponseResolver } from './RequestHandler'
1214

@@ -79,6 +81,51 @@ describe('info', () => {
7981
expect(handler.info).toHaveProperty('operationType', 'mutation')
8082
expect(handler.info).toHaveProperty('operationName', 'Login')
8183
})
84+
85+
test('parses a query operation name from a given DocumentNode', () => {
86+
const node = parse(`
87+
query GetUser {
88+
user {
89+
firstName
90+
}
91+
}
92+
`)
93+
94+
const handler = new GraphQLHandler('query', node, '*', resolver)
95+
96+
expect(handler.info).toHaveProperty('header', 'query GetUser (origin: *)')
97+
expect(handler.info).toHaveProperty('operationType', 'query')
98+
expect(handler.info).toHaveProperty('operationName', 'GetUser')
99+
})
100+
101+
test('parses a mutation operation name from a given DocumentNode', () => {
102+
const node = parse(`
103+
mutation Login {
104+
user {
105+
id
106+
}
107+
}
108+
`)
109+
const handler = new GraphQLHandler('mutation', node, '*', resolver)
110+
111+
expect(handler.info).toHaveProperty('header', 'mutation Login (origin: *)')
112+
expect(handler.info).toHaveProperty('operationType', 'mutation')
113+
expect(handler.info).toHaveProperty('operationName', 'Login')
114+
})
115+
116+
test('throws an exception given a DocumentNode with a mismatched operation type', () => {
117+
const node = parse(`
118+
mutation CreateUser {
119+
user {
120+
firstName
121+
}
122+
}
123+
`)
124+
125+
expect(() => new GraphQLHandler('query', node, '*', resolver)).toThrow(
126+
'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "query", but got "mutation").',
127+
)
128+
})
82129
})
83130

84131
describe('parse', () => {
@@ -371,3 +418,25 @@ describe('run', () => {
371418
expect(result).toBeNull()
372419
})
373420
})
421+
422+
describe('isDocumentNode', () => {
423+
it('returns true given a valid DocumentNode', () => {
424+
const node = parse(`
425+
query GetUser {
426+
user {
427+
login
428+
}
429+
}
430+
`)
431+
432+
expect(isDocumentNode(node)).toEqual(true)
433+
})
434+
435+
it('returns false given an arbitrary input', () => {
436+
expect(isDocumentNode(null)).toEqual(false)
437+
expect(isDocumentNode(undefined)).toEqual(false)
438+
expect(isDocumentNode('')).toEqual(false)
439+
expect(isDocumentNode('value')).toEqual(false)
440+
expect(isDocumentNode(/value/)).toEqual(false)
441+
})
442+
})

src/handlers/GraphQLHandler.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { OperationTypeNode } from 'graphql'
1+
import { DocumentNode, OperationTypeNode } from 'graphql'
22
import { Mask, SerializedResponse } from '../setupWorker/glossary'
33
import { set } from '../context/set'
44
import { status } from '../context/status'
@@ -21,12 +21,13 @@ import {
2121
ParsedGraphQLRequest,
2222
GraphQLMultipartRequestBody,
2323
parseGraphQLRequest,
24+
parseDocumentNode,
2425
} from '../utils/internal/parseGraphQLRequest'
2526
import { getPublicUrlFromRequest } from '../utils/request/getPublicUrlFromRequest'
2627
import { tryCatch } from '../utils/internal/tryCatch'
2728

2829
export type ExpectedOperationTypeNode = OperationTypeNode | 'all'
29-
export type GraphQLHandlerNameSelector = RegExp | string
30+
export type GraphQLHandlerNameSelector = DocumentNode | RegExp | string
3031

3132
// GraphQL related context should contain utility functions
3233
// useful for GraphQL. Functions like `xml()` bear no value
@@ -74,6 +75,16 @@ export interface GraphQLRequest<Variables extends GraphQLVariables>
7475
variables: Variables
7576
}
7677

78+
export function isDocumentNode(
79+
value: DocumentNode | any,
80+
): value is DocumentNode {
81+
if (value == null) {
82+
return false
83+
}
84+
85+
return typeof value === 'object' && 'kind' in value && 'definitions' in value
86+
}
87+
7788
export class GraphQLHandler<
7889
Request extends GraphQLRequest<any> = GraphQLRequest<any>
7990
> extends RequestHandler<
@@ -90,16 +101,36 @@ export class GraphQLHandler<
90101
endpoint: Mask,
91102
resolver: ResponseResolver<any, any>,
92103
) {
104+
let resolvedOperationName = operationName
105+
106+
if (isDocumentNode(operationName)) {
107+
const parsedNode = parseDocumentNode(operationName)
108+
109+
if (parsedNode.operationType !== operationType) {
110+
throw new Error(
111+
`Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "${operationType}", but got "${parsedNode.operationType}").`,
112+
)
113+
}
114+
115+
if (!parsedNode.operationName) {
116+
throw new Error(
117+
`Failed to create a GraphQL handler: provided a DocumentNode with no operation name.`,
118+
)
119+
}
120+
121+
resolvedOperationName = parsedNode.operationName
122+
}
123+
93124
const header =
94125
operationType === 'all'
95126
? `${operationType} (origin: ${endpoint.toString()})`
96-
: `${operationType} ${operationName} (origin: ${endpoint.toString()})`
127+
: `${operationType} ${resolvedOperationName} (origin: ${endpoint.toString()})`
97128

98129
super({
99130
info: {
100131
header,
101132
operationType,
102-
operationName,
133+
operationName: resolvedOperationName,
103134
},
104135
ctx: graphqlContext,
105136
resolver,

src/utils/internal/parseGraphQLRequest.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { OperationDefinitionNode, OperationTypeNode, parse } from 'graphql'
1+
import {
2+
DocumentNode,
3+
OperationDefinitionNode,
4+
OperationTypeNode,
5+
parse,
6+
} from 'graphql'
27
import { GraphQLVariables } from '../../handlers/GraphQLHandler'
38
import { MockedRequest } from '../../handlers/RequestHandler'
49
import { getPublicUrlFromRequest } from '../request/getPublicUrlFromRequest'
@@ -22,18 +27,21 @@ export type ParsedGraphQLRequest<
2227
})
2328
| undefined
2429

30+
export function parseDocumentNode(node: DocumentNode): ParsedGraphQLQuery {
31+
const operationDef = node.definitions.find((def) => {
32+
return def.kind === 'OperationDefinition'
33+
}) as OperationDefinitionNode
34+
35+
return {
36+
operationType: operationDef?.operation,
37+
operationName: operationDef?.name?.value,
38+
}
39+
}
40+
2541
function parseQuery(query: string): ParsedGraphQLQuery | Error {
2642
try {
2743
const ast = parse(query)
28-
29-
const operationDef = ast.definitions.find((def) => {
30-
return def.kind === 'OperationDefinition'
31-
}) as OperationDefinitionNode
32-
33-
return {
34-
operationType: operationDef?.operation,
35-
operationName: operationDef?.name?.value,
36-
}
44+
return parseDocumentNode(ast)
3745
} catch (error) {
3846
return error
3947
}

test/typings/graphql.test-d.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { parse } from 'graphql'
12
import { graphql } from 'msw'
3+
import { GetUserDetailDocument, LoginDocument } from 'graphql.test-data'
24

35
graphql.query<{ key: string }>('', (req, res, ctx) => {
46
return res(
@@ -50,3 +52,89 @@ graphql.operation<
5052
>((req, res, ctx) => {
5153
return res(ctx.data({ key: 'pass' }))
5254
})
55+
56+
/**
57+
* Supports `DocumentNode` as the GraphQL operation name.
58+
*/
59+
const getUser = parse(`
60+
query GetUser {
61+
user {
62+
firstName
63+
}
64+
}
65+
`)
66+
graphql.query(getUser, (req, res, ctx) =>
67+
res(
68+
ctx.data({
69+
// Cannot extract query type from the runtime `DocumentNode`.
70+
arbitrary: true,
71+
}),
72+
),
73+
)
74+
75+
const createUser = parse(`
76+
mutation CreateUser {
77+
user {
78+
id
79+
}
80+
}
81+
`)
82+
graphql.mutation(createUser, (req, res, ctx) =>
83+
res(
84+
ctx.data({
85+
arbitrary: true,
86+
}),
87+
),
88+
)
89+
90+
/**
91+
* Supports `TypedDocumentNode` as the GraphQL operation name.
92+
*/
93+
graphql.query(GetUserDetailDocument, (req, res, ctx) => {
94+
return res(
95+
ctx.data({
96+
user: {
97+
id: req.variables.userId,
98+
firstName: 'John',
99+
age: 24,
100+
},
101+
}),
102+
)
103+
})
104+
105+
graphql.mutation(LoginDocument, (req, res, ctx) => {
106+
req.variables.username
107+
return res(
108+
ctx.data({
109+
login: {
110+
id: 'abc-123',
111+
},
112+
}),
113+
)
114+
})
115+
116+
graphql.query(GetUserDetailDocument, (req, res, ctx) => {
117+
req.variables.userId
118+
// @ts-expect-error Unknown operation variable.
119+
req.variables.unknownVariable
120+
121+
return res(
122+
ctx.data(
123+
// @ts-expect-error Mocked response doesn't match the query type.
124+
{},
125+
),
126+
)
127+
})
128+
129+
graphql.mutation(LoginDocument, (req, res, ctx) => {
130+
req.variables.username
131+
// @ts-expect-error Unknown operation variable.
132+
req.variables.unknownVariable
133+
134+
return res(
135+
ctx.data(
136+
// @ts-expect-error Mocked response doesn't match the query type.
137+
{},
138+
),
139+
)
140+
})

0 commit comments

Comments
 (0)