522 lines
17 KiB
Plaintext
522 lines
17 KiB
Plaintext
import { setupApiStore } from '@internal/tests/utils/helpers'
|
|
import type { EntityState, SerializedError } from '@reduxjs/toolkit'
|
|
import { configureStore, createEntityAdapter } from '@reduxjs/toolkit'
|
|
import type {
|
|
DefinitionsFromApi,
|
|
FetchBaseQueryError,
|
|
FetchBaseQueryMeta,
|
|
MutationDefinition,
|
|
OverrideResultType,
|
|
QueryDefinition,
|
|
TagDescription,
|
|
TagTypesFromApi,
|
|
} from '@reduxjs/toolkit/query'
|
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
|
|
import * as v from 'valibot'
|
|
import type { Post } from './mocks/handlers'
|
|
|
|
describe('type tests', () => {
|
|
test('sensible defaults', () => {
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery(),
|
|
endpoints: (build) => ({
|
|
getUser: build.query<unknown, void>({
|
|
query(id) {
|
|
return { url: `user/${id}` }
|
|
},
|
|
}),
|
|
updateUser: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
}),
|
|
}),
|
|
})
|
|
|
|
configureStore({
|
|
reducer: {
|
|
[api.reducerPath]: api.reducer,
|
|
},
|
|
middleware: (gDM) => gDM().concat(api.middleware),
|
|
})
|
|
|
|
expectTypeOf(api.reducerPath).toEqualTypeOf<'api'>()
|
|
|
|
expectTypeOf(api.util.invalidateTags)
|
|
.parameter(0)
|
|
.toEqualTypeOf<(null | undefined | TagDescription<never>)[]>()
|
|
})
|
|
|
|
describe('endpoint definition typings', () => {
|
|
const api = createApi({
|
|
baseQuery: (from: 'From'): { data: 'To' } | Promise<{ data: 'To' }> => ({
|
|
data: 'To',
|
|
}),
|
|
endpoints: () => ({}),
|
|
tagTypes: ['typeA', 'typeB'],
|
|
})
|
|
|
|
test('query: query & transformResponse types', () => {
|
|
api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
query: build.query<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query1: build.query<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Error') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query2: build.query<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Arg') => 'Error' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query3: build.query<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'Error') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query4: build.query<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'To') {
|
|
return 'Error' as const
|
|
},
|
|
}),
|
|
queryInference1: build.query<'RetVal', 'Arg'>({
|
|
query: (x) => {
|
|
expectTypeOf(x).toEqualTypeOf<'Arg'>()
|
|
|
|
return 'From'
|
|
},
|
|
transformResponse(r) {
|
|
expectTypeOf(r).toEqualTypeOf<'To'>()
|
|
|
|
return 'RetVal'
|
|
},
|
|
}),
|
|
queryInference2: (() => {
|
|
const query = build.query({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
})
|
|
|
|
expectTypeOf(query).toMatchTypeOf<
|
|
QueryDefinition<'Arg', any, any, 'RetVal'>
|
|
>()
|
|
|
|
return query
|
|
})(),
|
|
}),
|
|
})
|
|
})
|
|
|
|
test('mutation: query & transformResponse types', () => {
|
|
api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
query: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query1: build.mutation<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Error') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query2: build.mutation<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Arg') => 'Error' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query3: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'Error') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query4: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'To') {
|
|
return 'Error' as const
|
|
},
|
|
}),
|
|
mutationInference1: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x) => {
|
|
expectTypeOf(x).toEqualTypeOf<'Arg'>()
|
|
|
|
return 'From'
|
|
},
|
|
transformResponse(r) {
|
|
expectTypeOf(r).toEqualTypeOf<'To'>()
|
|
|
|
return 'RetVal'
|
|
},
|
|
}),
|
|
mutationInference2: (() => {
|
|
const query = build.mutation({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
})
|
|
|
|
expectTypeOf(query).toMatchTypeOf<
|
|
MutationDefinition<'Arg', any, any, 'RetVal'>
|
|
>()
|
|
|
|
return query
|
|
})(),
|
|
}),
|
|
})
|
|
})
|
|
|
|
describe('enhancing endpoint definitions', () => {
|
|
const baseQuery = (x: string) => ({ data: 'success' })
|
|
|
|
function getNewApi() {
|
|
return createApi({
|
|
baseQuery,
|
|
tagTypes: ['old'],
|
|
endpoints: (build) => ({
|
|
query1: build.query<'out1', 'in1'>({ query: (id) => `${id}` }),
|
|
query2: build.query<'out2', 'in2'>({ query: (id) => `${id}` }),
|
|
mutation1: build.mutation<'out1', 'in1'>({
|
|
query: (id) => `${id}`,
|
|
}),
|
|
mutation2: build.mutation<'out2', 'in2'>({
|
|
query: (id) => `${id}`,
|
|
}),
|
|
}),
|
|
})
|
|
}
|
|
|
|
const api1 = getNewApi()
|
|
|
|
test('warn on wrong tagType', () => {
|
|
const storeRef = setupApiStore(api1, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
api1.enhanceEndpoints({
|
|
endpoints: {
|
|
query1: {
|
|
// @ts-expect-error
|
|
providesTags: ['new'],
|
|
},
|
|
query2: {
|
|
// @ts-expect-error
|
|
providesTags: ['missing'],
|
|
},
|
|
},
|
|
})
|
|
|
|
const enhanced = api1.enhanceEndpoints({
|
|
addTagTypes: ['new'],
|
|
endpoints: {
|
|
query1: {
|
|
providesTags: ['new'],
|
|
},
|
|
query2: {
|
|
// @ts-expect-error
|
|
providesTags: ['missing'],
|
|
},
|
|
},
|
|
})
|
|
|
|
storeRef.store.dispatch(api1.endpoints.query1.initiate('in1'))
|
|
|
|
storeRef.store.dispatch(api1.endpoints.query2.initiate('in2'))
|
|
|
|
enhanced.enhanceEndpoints({
|
|
endpoints: {
|
|
query1: {
|
|
// returned `enhanced` api contains "new" entityType
|
|
providesTags: ['new'],
|
|
},
|
|
query2: {
|
|
// @ts-expect-error
|
|
providesTags: ['missing'],
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
test('modify', () => {
|
|
const storeRef = setupApiStore(api1, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
api1.enhanceEndpoints({
|
|
endpoints: {
|
|
query1: {
|
|
query: (x) => {
|
|
expectTypeOf(x).toEqualTypeOf<'in1'>()
|
|
|
|
return 'modified1'
|
|
},
|
|
},
|
|
query2(definition) {
|
|
definition.query = (x) => {
|
|
expectTypeOf(x).toEqualTypeOf<'in2'>()
|
|
|
|
return 'modified2'
|
|
}
|
|
},
|
|
mutation1: {
|
|
query: (x) => {
|
|
expectTypeOf(x).toEqualTypeOf<'in1'>()
|
|
|
|
return 'modified1'
|
|
},
|
|
},
|
|
mutation2(definition) {
|
|
definition.query = (x) => {
|
|
expectTypeOf(x).toEqualTypeOf<'in2'>()
|
|
|
|
return 'modified2'
|
|
}
|
|
},
|
|
// @ts-expect-error
|
|
nonExisting: {},
|
|
},
|
|
})
|
|
|
|
storeRef.store.dispatch(api1.endpoints.query1.initiate('in1'))
|
|
storeRef.store.dispatch(api1.endpoints.query2.initiate('in2'))
|
|
storeRef.store.dispatch(api1.endpoints.mutation1.initiate('in1'))
|
|
storeRef.store.dispatch(api1.endpoints.mutation2.initiate('in2'))
|
|
})
|
|
|
|
test('updated transform response types', async () => {
|
|
const baseApi = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
tagTypes: ['old'],
|
|
endpoints: (build) => ({
|
|
query1: build.query<'out1', void>({ query: () => 'success' }),
|
|
mutation1: build.mutation<'out1', void>({ query: () => 'success' }),
|
|
}),
|
|
})
|
|
|
|
type Transformed = { value: string }
|
|
|
|
type Definitions = DefinitionsFromApi<typeof api1>
|
|
|
|
type TagTypes = TagTypesFromApi<typeof api1>
|
|
|
|
type Q1Definition = OverrideResultType<
|
|
Definitions['query1'],
|
|
Transformed
|
|
>
|
|
|
|
type M1Definition = OverrideResultType<
|
|
Definitions['mutation1'],
|
|
Transformed
|
|
>
|
|
|
|
type UpdatedDefinitions = Omit<Definitions, 'query1' | 'mutation1'> & {
|
|
query1: Q1Definition
|
|
mutation1: M1Definition
|
|
}
|
|
|
|
const enhancedApi = baseApi.enhanceEndpoints<
|
|
TagTypes,
|
|
UpdatedDefinitions
|
|
>({
|
|
endpoints: {
|
|
query1: {
|
|
transformResponse: (a, b, c) => ({
|
|
value: 'transformed',
|
|
}),
|
|
},
|
|
mutation1: {
|
|
transformResponse: (a, b, c) => ({
|
|
value: 'transformed',
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
|
|
const storeRef = setupApiStore(enhancedApi, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const queryResponse = await storeRef.store.dispatch(
|
|
enhancedApi.endpoints.query1.initiate(),
|
|
)
|
|
|
|
expectTypeOf(queryResponse.data).toMatchTypeOf<
|
|
Transformed | undefined
|
|
>()
|
|
|
|
const mutationResponse = await storeRef.store.dispatch(
|
|
enhancedApi.endpoints.mutation1.initiate(),
|
|
)
|
|
|
|
expectTypeOf(mutationResponse).toMatchTypeOf<
|
|
| { data: Transformed }
|
|
| { error: FetchBaseQueryError | SerializedError }
|
|
>()
|
|
})
|
|
})
|
|
describe('endpoint schemas', () => {
|
|
const argSchema = v.object({ id: v.number() })
|
|
const postSchema = v.object({
|
|
id: v.number(),
|
|
title: v.string(),
|
|
body: v.string(),
|
|
}) satisfies v.GenericSchema<Post>
|
|
const errorResponseSchema = v.object({
|
|
status: v.number(),
|
|
data: v.unknown(),
|
|
}) satisfies v.GenericSchema<FetchBaseQueryError>
|
|
const metaSchema = v.object({
|
|
request: v.instance(Request),
|
|
response: v.optional(v.instance(Response)),
|
|
}) satisfies v.GenericSchema<FetchBaseQueryMeta>
|
|
test('schemas must match', () => {
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
endpoints: (build) => ({
|
|
query: build.query<Post, { id: number }>({
|
|
query: ({ id }) => `/post/${id}`,
|
|
argSchema,
|
|
responseSchema: postSchema,
|
|
errorResponseSchema,
|
|
metaSchema,
|
|
}),
|
|
bothMismatch: build.query<Post, { id: number }>({
|
|
query: ({ id }) => `/post/${id}`,
|
|
// @ts-expect-error wrong schema
|
|
argSchema: v.object({ id: v.string() }),
|
|
// @ts-expect-error wrong schema
|
|
responseSchema: v.object({ id: v.string() }),
|
|
// @ts-expect-error wrong schema
|
|
errorResponseSchema: v.object({ status: v.string() }),
|
|
// @ts-expect-error wrong schema
|
|
metaSchema: v.object({ request: v.string() }),
|
|
}),
|
|
inputMismatch: build.query<Post, { id: number }>({
|
|
query: ({ id }) => `/post/${id}`,
|
|
// @ts-expect-error can't expect different input
|
|
argSchema: v.object({
|
|
id: v.pipe(v.string(), v.transform(Number), v.number()),
|
|
}),
|
|
// @ts-expect-error can't expect different input
|
|
responseSchema: v.object({
|
|
...postSchema.entries,
|
|
id: v.pipe(v.string(), v.transform(Number)),
|
|
}) satisfies v.GenericSchema<any, Post>,
|
|
// @ts-expect-error can't expect different input
|
|
errorResponseSchema: v.object({
|
|
...errorResponseSchema.entries,
|
|
status: v.pipe(v.string(), v.transform(Number)),
|
|
}) satisfies v.GenericSchema<any, FetchBaseQueryError>,
|
|
// @ts-expect-error can't expect different input
|
|
metaSchema: v.object({
|
|
...metaSchema.entries,
|
|
request: v.pipe(
|
|
v.string(),
|
|
v.transform((url) => new Request(url)),
|
|
),
|
|
}) satisfies v.GenericSchema<any, FetchBaseQueryMeta>,
|
|
}),
|
|
outputMismatch: build.query<Post, { id: number }>({
|
|
query: ({ id }) => `/post/${id}`,
|
|
// @ts-expect-error can't provide different output
|
|
argSchema: v.object({
|
|
id: v.pipe(v.number(), v.transform(String)),
|
|
}),
|
|
// @ts-expect-error can't provide different output
|
|
responseSchema: v.object({
|
|
...postSchema.entries,
|
|
id: v.pipe(v.number(), v.transform(String)),
|
|
}) satisfies v.GenericSchema<Post, any>,
|
|
// @ts-expect-error can't provide different output
|
|
errorResponseSchema: v.object({
|
|
...errorResponseSchema.entries,
|
|
status: v.pipe(v.number(), v.transform(String)),
|
|
}) satisfies v.GenericSchema<FetchBaseQueryError, any>,
|
|
// @ts-expect-error can't provide different output
|
|
metaSchema: v.object({
|
|
...metaSchema.entries,
|
|
request: v.pipe(
|
|
v.instance(Request),
|
|
v.transform((r) => r.url),
|
|
),
|
|
}) satisfies v.GenericSchema<FetchBaseQueryMeta, any>,
|
|
}),
|
|
}),
|
|
})
|
|
})
|
|
test('schemas as a source of inference', () => {
|
|
const postAdapter = createEntityAdapter<Post>()
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
endpoints: (build) => ({
|
|
query: build.query({
|
|
query: ({ id }: { id: number }) => `/post/${id}`,
|
|
responseSchema: postSchema,
|
|
}),
|
|
query2: build.query({
|
|
query: (arg) => {
|
|
expectTypeOf(arg).toEqualTypeOf<{ id: number }>()
|
|
return `/post/${arg.id}`
|
|
},
|
|
argSchema,
|
|
responseSchema: postSchema,
|
|
}),
|
|
query3: build.query({
|
|
query: (_arg: void) => `/posts`,
|
|
rawResponseSchema: v.array(postSchema),
|
|
transformResponse: (posts) => {
|
|
expectTypeOf(posts).toEqualTypeOf<Post[]>()
|
|
return postAdapter.getInitialState(undefined, posts)
|
|
},
|
|
}),
|
|
}),
|
|
})
|
|
|
|
expectTypeOf(api.endpoints.query.Types.QueryArg).toEqualTypeOf<{
|
|
id: number
|
|
}>()
|
|
expectTypeOf(api.endpoints.query.Types.ResultType).toEqualTypeOf<Post>()
|
|
expectTypeOf(api.endpoints.query.Types.RawResultType).toBeAny()
|
|
|
|
expectTypeOf(api.endpoints.query2.Types.QueryArg).toEqualTypeOf<{
|
|
id: number
|
|
}>()
|
|
expectTypeOf(
|
|
api.endpoints.query2.Types.ResultType,
|
|
).toEqualTypeOf<Post>()
|
|
expectTypeOf(api.endpoints.query2.Types.RawResultType).toBeAny()
|
|
|
|
expectTypeOf(api.endpoints.query3.Types.QueryArg).toEqualTypeOf<void>()
|
|
expectTypeOf(api.endpoints.query3.Types.ResultType).toEqualTypeOf<
|
|
EntityState<Post, Post['id']>
|
|
>()
|
|
expectTypeOf(api.endpoints.query3.Types.RawResultType).toEqualTypeOf<
|
|
Post[]
|
|
>()
|
|
})
|
|
})
|
|
})
|
|
})
|