Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1a2110f
feat(http): support custom predicate functions in http request handlers
ytoshiki Jun 30, 2025
ba63e46
feat(graphql): support custom predicate functions in graphql request …
ytoshiki Jul 1, 2025
d0253f8
fix: change path type
ytoshiki Jul 1, 2025
b01417f
test: node
ytoshiki Jul 1, 2025
570692b
test: browser
ytoshiki Jul 1, 2025
157b54c
chore: change import
ytoshiki Jul 1, 2025
dc0efc7
chore: delete tests
ytoshiki Jul 1, 2025
c5b9eb2
chore: fix path
ytoshiki Jul 1, 2025
3388f96
chore: fix path resolving error
ytoshiki Jul 1, 2025
6f8d5fa
Merge branch 'main' into feature/custom-predicate-function
ytoshiki Jul 1, 2025
d820b6e
chore: change path
ytoshiki Jul 1, 2025
5763c33
fix: test
ytoshiki Jul 1, 2025
1e9a8e3
fix(http): rename path to predicate
ytoshiki Jul 5, 2025
8b00de3
fix(graphql): rename operationName to predicate
ytoshiki Jul 5, 2025
da1f44c
feat: add predicate caching to RequestHandler for performance optimiz…
ytoshiki Jul 5, 2025
06f01e9
fix: predicate cache logic
ytoshiki Jul 5, 2025
4452a33
chore: use consistent typeof checks
ytoshiki Jul 5, 2025
dc60641
Revert "chore: use consistent typeof checks"
ytoshiki Jul 6, 2025
9ffca63
Revert "fix: predicate cache logic"
ytoshiki Jul 6, 2025
b37a5e0
Revert "feat: add predicate caching to RequestHandler for performance…
ytoshiki Jul 6, 2025
fd5cd0f
Merge branch 'main' into feature/custom-predicate-function
ytoshiki Jul 11, 2025
c3f89aa
Merge branch 'main' into feature/custom-predicate-function
kettanaito Aug 28, 2025
7dd1046
fix: polish handler classes
kettanaito Aug 28, 2025
54f0481
fix(GraphQLHandler): support `{ matches }` predicate return type
kettanaito Aug 28, 2025
98ff915
fix(core): export `GraphQLOperationType` type
kettanaito Aug 28, 2025
61d5a99
fix(HttpHandler): path params inference
kettanaito Aug 28, 2025
7cb1fb6
test: add type tests for custom predicate
kettanaito Aug 28, 2025
4baba5c
chore: polish handler tests
kettanaito Aug 29, 2025
18dfcc9
test: add unit tests for custom predicate function
kettanaito Aug 29, 2025
8ea8e27
test: polish node custom predicate tests
kettanaito Aug 29, 2025
4765f98
test: polish graphql custom predicate tests
kettanaito Aug 29, 2025
af5b4d6
test: polish e2e custom predicate tests
kettanaito Aug 29, 2025
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
10 changes: 9 additions & 1 deletion src/core/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GraphQLResolverExtras,
GraphQLResponseBody,
GraphQLQuery,
GraphQLCustomPredicate,
} from './handlers/GraphQLHandler'
import type { Path } from './utils/matching/matchRequestUrl'

Expand All @@ -30,7 +31,8 @@ export type GraphQLRequestHandler = <
operationName:
| GraphQLHandlerNameSelector
| DocumentNode
| TypedDocumentNode<Query, Variables>,
| TypedDocumentNode<Query, Variables>
| GraphQLCustomPredicate<GraphQLVariables>,
resolver: GraphQLResponseResolver<
[Query] extends [never] ? GraphQLQuery : Query,
Variables
Expand All @@ -52,6 +54,12 @@ function createScopedGraphQLHandler(
url: Path,
): GraphQLRequestHandler {
return (operationName, resolver, options = {}) => {
if (typeof operationName === 'function') {
return new GraphQLHandler(operationType, '', url, resolver, {
...options,
predicate: operationName,
})
}
return new GraphQLHandler(
operationType,
operationName,
Expand Down
134 changes: 127 additions & 7 deletions src/core/handlers/GraphQLHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,13 +557,13 @@ describe('predicate', () => {
})

expect(
handler.predicate({
await handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).toBe(true)
expect(
handler.predicate({
await handler.predicate({
request: alienRequest,
parsedResult: await handler.parse({ request: alienRequest }),
}),
Expand Down Expand Up @@ -591,13 +591,13 @@ describe('predicate', () => {
})

expect(
handler.predicate({
await handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).toBe(true)
expect(
handler.predicate({
await handler.predicate({
request: alienRequest,
parsedResult: await handler.parse({ request: alienRequest }),
}),
Expand All @@ -618,7 +618,7 @@ describe('predicate', () => {
})

expect(
handler.predicate({
await handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
Expand All @@ -643,13 +643,13 @@ describe('predicate', () => {
})

expect(
handler.predicate({
await handler.predicate({
request,
parsedResult: await handler.parse({ request }),
}),
).toBe(true)
expect(
handler.predicate({
await handler.predicate({
request: alienRequest,
parsedResult: await handler.parse({ request: alienRequest }),
}),
Expand Down Expand Up @@ -836,3 +836,123 @@ describe('request', () => {
)
})
})

describe('custom predicate', () => {
test('receives cookies parameter', async () => {
let receivedCookies: Record<string, string> | undefined
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'',
'*',
resolver,
{
predicate: ({ cookies }) => {
receivedCookies = cookies
return true
},
},
)
const request = createPostGraphQLRequest(
{
query: GET_USER,
variables: { userId: 'abc-123' },
},
'https://example.com',
)
request.headers.set('Cookie', 'token=secret')
const requestId = createRequestId()
await handler.run({ request, requestId })
expect(receivedCookies).toEqual({ token: 'secret' })
})

test('receives query parameter', async () => {
let receivedQuery = ''
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'',
'*',
resolver,
{
predicate: ({ query }) => {
receivedQuery = query
return true
},
},
)
const request = createPostGraphQLRequest({
query: GET_USER,
variables: { userId: 'abc-123' },
})
const requestId = createRequestId()
await handler.run({ request, requestId })
expect(receivedQuery).toBe(GET_USER)
})

test('receives operationName parameter', async () => {
let receivedOperationName = ''
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'',
'*',
resolver,
{
predicate: ({ operationName }) => {
receivedOperationName = operationName
return true
},
},
)
const request = createPostGraphQLRequest({
query: GET_USER,
variables: { userId: 'abc-123' },
})
const requestId = createRequestId()
await handler.run({ request, requestId })
expect(receivedOperationName).toBe('GetUser')
})

test('does not match if custom predicate returns true', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'',
'*',
resolver,
{
predicate: async ({ variables }) => {
return variables?.userId === 'abc-123'
},
},
)
const request = createPostGraphQLRequest({
query: GET_USER,
variables: { userId: 'abc-123' },
})
const requestId = createRequestId()
const result = await handler.run({ request, requestId })
expect(result?.response).toBeDefined()
expect(await result?.response?.json()).toEqual({
data: { user: { id: 'abc-123' } },
})
})

test('does not match if custom predicate returns false', async () => {
const handler = new GraphQLHandler(
OperationTypeNode.QUERY,
'',
'*',
resolver,
{
predicate: async ({ variables }) => {
return variables?.userId === 'not-match'
},
},
)
const request = createPostGraphQLRequest({
query: GET_USER,
variables: { userId: 'abc-123' },
})
const requestId = createRequestId()
const result = await handler.run({ request, requestId })
expect(result).toBeNull()
})
})
37 changes: 29 additions & 8 deletions src/core/handlers/GraphQLHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export type GraphQLResponseBody<BodyType extends DefaultBodyType> =
| null
| undefined

export type GraphQLCustomPredicate<GraphQLVariables> = (args: {
request: Request
query: string
operationName: string
variables: GraphQLVariables
cookies: Record<string, string>
}) => boolean | Promise<boolean>

export function isDocumentNode(
value: DocumentNode | any,
): value is DocumentNode {
Expand All @@ -92,6 +100,7 @@ export class GraphQLHandler extends RequestHandler<
GraphQLResolverExtras<any>
> {
private endpoint: Path
private customPredicate?: GraphQLCustomPredicate<GraphQLVariables>

static parsedRequestCache = new WeakMap<
Request,
Expand All @@ -103,7 +112,9 @@ export class GraphQLHandler extends RequestHandler<
operationName: GraphQLHandlerNameSelector,
endpoint: Path,
resolver: ResponseResolver<GraphQLResolverExtras<any>, any, any>,
options?: RequestHandlerOptions,
options?: RequestHandlerOptions & {
predicate?: GraphQLCustomPredicate<GraphQLVariables>
},
) {
let resolvedOperationName = operationName

Expand Down Expand Up @@ -140,6 +151,7 @@ export class GraphQLHandler extends RequestHandler<
options,
})

this.customPredicate = options?.predicate
this.endpoint = endpoint
}

Expand Down Expand Up @@ -195,33 +207,42 @@ export class GraphQLHandler extends RequestHandler<
}
}

predicate(args: {
async predicate(args: {
request: Request
parsedResult: GraphQLRequestParsedResult
}) {
}): Promise<boolean> {
if (this.customPredicate) {
// Clone the request to avoid locking its body stream.
// The body can only be read once, so cloning allows both
// the predicate and resolver to access it independently.
const clonedRequest = args.request.clone()

return await this.customPredicate({
request: clonedRequest,
...this.extendResolverArgs({
request: clonedRequest,
parsedResult: args.parsedResult,
}),
})
}
if (args.parsedResult.operationType === undefined) {
return false
}

if (!args.parsedResult.operationName && this.info.operationType !== 'all') {
const publicUrl = toPublicUrl(args.request.url)

devUtils.warn(`\
Failed to intercept a GraphQL request at "${args.request.method} ${publicUrl}": anonymous GraphQL operations are not supported.

Consider 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`)
return false
}

const hasMatchingOperationType =
this.info.operationType === 'all' ||
args.parsedResult.operationType === this.info.operationType

const hasMatchingOperationName =
this.info.operationName instanceof RegExp
? this.info.operationName.test(args.parsedResult.operationName || '')
: args.parsedResult.operationName === this.info.operationName

return (
args.parsedResult.match.matches &&
hasMatchingOperationType &&
Expand Down
Loading