Skip to content

Commit

Permalink
fix: accept a narrower response body type by default (#2107)
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Mar 25, 2024
1 parent 16fadfe commit d35ef92
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 83 deletions.
5 changes: 3 additions & 2 deletions src/core/HttpResponse.ts
@@ -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
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
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
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
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
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 },
})
})
})

0 comments on commit d35ef92

Please sign in to comment.