Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: accept a narrower response body type by default #2107

Merged
merged 2 commits into from
Mar 25, 2024
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
5 changes: 3 additions & 2 deletions src/core/HttpResponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DefaultBodyType, JsonBodyType } from './handlers/RequestHandler'
import type { NoInfer } from './typeUtils'
import {
decorateResponse,
normalizeResponseInit,
Expand Down Expand Up @@ -48,7 +49,7 @@ export class HttpResponse extends Response {
* HttpResponse.text('Error', { status: 500 })
*/
static text<BodyType extends string>(
body?: BodyType | null,
body?: NoInfer<BodyType> | null,
init?: HttpResponseInit,
): StrictResponse<BodyType> {
const responseInit = normalizeResponseInit(init)
Expand Down Expand Up @@ -77,7 +78,7 @@ export class HttpResponse extends Response {
* HttpResponse.json({ error: 'Not Authorized' }, { status: 401 })
*/
static json<BodyType extends JsonBodyType>(
body?: BodyType | null,
body?: NoInfer<BodyType> | null,
init?: HttpResponseInit,
): StrictResponse<BodyType> {
const responseInit = normalizeResponseInit(init)
Expand Down
9 changes: 6 additions & 3 deletions src/core/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export type GraphQLRequestHandler = <
| GraphQLHandlerNameSelector
| DocumentNode
| TypedDocumentNode<Query, Variables>,
resolver: GraphQLResponseResolver<Query, Variables>,
resolver: GraphQLResponseResolver<
[Query] extends [never] ? GraphQLQuery : Query,
Variables
>,
options?: RequestHandlerOptions,
) => GraphQLHandler

Expand All @@ -41,7 +44,7 @@ export type GraphQLResponseResolver<
> = ResponseResolver<
GraphQLResolverExtras<Variables>,
null,
GraphQLResponseBody<Query>
GraphQLResponseBody<[Query] extends [never] ? GraphQLQuery : Query>
>

function createScopedGraphQLHandler(
Expand All @@ -61,7 +64,7 @@ function createScopedGraphQLHandler(

function createGraphQLOperationHandler(url: Path) {
return <
Query extends Record<string, any>,
Query extends GraphQLQuery = GraphQLQuery,
Variables extends GraphQLVariables = GraphQLVariables,
>(
resolver: ResponseResolver<
Expand Down
11 changes: 7 additions & 4 deletions src/core/handlers/GraphQLHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ export interface GraphQLJsonRequestBody<Variables extends GraphQLVariables> {
variables?: Variables
}

export interface GraphQLResponseBody<BodyType extends DefaultBodyType> {
data?: BodyType | null
errors?: readonly Partial<GraphQLError>[] | null
}
export type GraphQLResponseBody<BodyType extends DefaultBodyType> =
| {
data?: BodyType | null
errors?: readonly Partial<GraphQLError>[] | null
}
| null
| undefined

export function isDocumentNode(
value: DocumentNode | any,
Expand Down
6 changes: 6 additions & 0 deletions src/core/typeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ export type RequiredDeep<
: RequiredDeep<NonNullable<Type[Key]>, U>
}
: Type

/**
* @fixme Remove this once TS 5.4 is the lowest supported version.
* Because "NoInfer" is a built-in type utility there.
*/
export type NoInfer<T> = [T][T extends any ? 0 : never]
8 changes: 5 additions & 3 deletions test/typings/custom-resolver.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ it('custom http resolver has correct parameters type', () => {

http.get<{ id: string }, never, 'hello'>(
'/user/:id',
// @ts-expect-error Response body doesn't match the response type.
withDelay(250, ({ params }) => {
expectTypeOf(params).toEqualTypeOf<{ id: string }>()
return HttpResponse.text('non-matching')
return HttpResponse.text(
// @ts-expect-error Response body doesn't match the response type.
'non-matching',
)
}),
)
})
Expand Down Expand Up @@ -72,12 +74,12 @@ it('custom graphql resolver has correct variables and response type', () => {
it('custom graphql resolver does not accept unknown variables', () => {
graphql.query<{ number: number }, { id: string }>(
'GetUser',
// @ts-expect-error Incompatible response query type.
identityGraphQLResolver(({ variables }) => {
expectTypeOf(variables).toEqualTypeOf<{ id: string }>()

return HttpResponse.json({
data: {
// @ts-expect-error Incompatible response query type.
user: {
id: variables.id,
},
Expand Down
105 changes: 51 additions & 54 deletions test/typings/graphql.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@ it('graphql mutation allows explicit null as the response body type for the muta
})
})
it('graphql mutation does not allow mismatched mutation response', () => {
graphql.mutation<{ key: string }>(
'MutateData',
// @ts-expect-error Response data doesn't match the query type.
() => {
return HttpResponse.json({ data: {} })
},
)
graphql.mutation<{ key: string }>('MutateData', () => {
return HttpResponse.json({
// @ts-expect-error Response data doesn't match the query type.
data: {},
})
})
})

it("graphql query does not accept null as variables' generic query type ", () => {
Expand All @@ -53,6 +52,7 @@ it("graphql query does not accept null as variables' generic query type ", () =>
null
>('', () => {})
})

it("graphql query accepts the correct type for the variables' generic query type", () => {
/**
* Response body type (GraphQL query type).
Expand All @@ -76,15 +76,14 @@ it('graphql query allows explicit null as the response body type for the query',
})

it('graphql query does not accept invalid data type for the response body type for the query', () => {
graphql.query<{ id: string }>(
'GetUser',
// @ts-expect-error "id" type is incorrect
() => {
return HttpResponse.json({
data: { id: 123 },
})
},
)
graphql.query<{ id: string }>('GetUser', () => {
return HttpResponse.json({
data: {
// @ts-expect-error "id" type is incorrect
id: 123,
},
})
})
})

it('graphql query does not allow empty response when the query type is defined', () => {
Expand Down Expand Up @@ -114,12 +113,12 @@ it("graphql operation does not accept null as variables' generic operation type"
})

it('graphql operation does not allow mismatched operation response', () => {
graphql.operation<{ key: string }>(
// @ts-expect-error Response data doesn't match the query type.
() => {
return HttpResponse.json({ data: {} })
},
)
graphql.operation<{ key: string }>(() => {
return HttpResponse.json({
// @ts-expect-error Response data doesn't match the query type.
data: {},
})
})
})

it('graphql operation allows explicit null as the response body type for the operation', () => {
Expand All @@ -140,64 +139,62 @@ it('graphql handlers allow passthrough responses', () => {

return HttpResponse.json({ data: {} })
})
})

it("graphql variables cannot extract type from the runtime 'DocumentNode'", () => {
/**
* Supports `DocumentNode` as the GraphQL operation name.
*/
const getUser = parse(`
it("graphql variables cannot extract type from the runtime 'DocumentNode'", () => {
/**
* Supports `DocumentNode` as the GraphQL operation name.
*/
const getUser = parse(`
query GetUser {
user {
firstName
}
}
`)
graphql.query(getUser, () => {
return HttpResponse.json({
// Cannot extract query type from the runtime `DocumentNode`.
data: { arbitrary: true },
})
graphql.query(getUser, () => {
return HttpResponse.json({
// Cannot extract query type from the runtime `DocumentNode`.
data: { arbitrary: true },
})
})
})

it('graphql query cannot extract variable and reponse types', () => {
const getUserById = parse(`
it('graphql query cannot extract variable and reponse types', () => {
const getUserById = parse(`
query GetUserById($userId: String!) {
user(id: $userId) {
firstName
}
}
`)
graphql.query(getUserById, ({ variables }) => {
variables.userId.toUpperCase()

// Extracting variables from the native "DocumentNode" is impossible.
variables.foo

return HttpResponse.json({
data: {
user: {
firstName: 'John',
// Extracting a query body type from the "DocumentNode" is impossible.
lastName: 'Maverick',
},
graphql.query(getUserById, ({ variables }) => {
// Cannot extract variables type from a DocumentNode.
expectTypeOf(variables).toEqualTypeOf<Record<string, any>>()

return HttpResponse.json({
data: {
user: {
firstName: 'John',
// Extracting a query body type from the "DocumentNode" is impossible.
lastName: 'Maverick',
},
})
},
})
})
})

it('graphql mutation cannot extract variable and reponse types', () => {
const createUser = parse(`
it('graphql mutation cannot extract variable and reponse types', () => {
const createUser = parse(`
mutation CreateUser {
user {
id
}
}
`)
graphql.mutation(createUser, () => {
return HttpResponse.json({
data: { arbitrary: true },
})
graphql.mutation(createUser, () => {
return HttpResponse.json({
data: { arbitrary: true },
})
})
})
Loading
Loading