From f568475a1e78137e4a5d174424b875b1c5c12ac2 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 27 Jul 2025 21:59:35 +0100 Subject: [PATCH] allow passing an array of specific schemas to skip --- .../toolkit/src/query/core/buildThunks.ts | 36 ++- packages/toolkit/src/query/createApi.ts | 7 +- .../toolkit/src/query/endpointDefinitions.ts | 14 +- packages/toolkit/src/query/index.ts | 1 + packages/toolkit/src/query/standardSchema.ts | 13 +- .../toolkit/src/query/tests/createApi.test.ts | 233 +++++------------- 6 files changed, 121 insertions(+), 183 deletions(-) diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 048c98b9f3..f9273b812c 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -31,6 +31,7 @@ import type { SchemaFailureConverter, SchemaFailureHandler, SchemaFailureInfo, + SchemaType, } from '../endpointDefinitions' import { calculateProvidedBy, @@ -68,7 +69,11 @@ import { isRejectedWithValue, SHOULD_AUTOBATCH, } from './rtkImports' -import { parseWithSchema, NamedSchemaError } from '../standardSchema' +import { + parseWithSchema, + NamedSchemaError, + shouldSkip, +} from '../standardSchema' export type BuildThunksApiEndpointQuery< Definition extends QueryDefinition, @@ -346,7 +351,7 @@ export function buildThunks< selectors: AllSelectors onSchemaFailure: SchemaFailureHandler | undefined catchSchemaFailure: SchemaFailureConverter | undefined - skipSchemaValidation: boolean | undefined + skipSchemaValidation: boolean | SchemaType[] | undefined }) { type State = RootState @@ -569,7 +574,7 @@ export function buildThunks< const { extraOptions, argSchema, rawResponseSchema, responseSchema } = endpointDefinition - if (argSchema && !skipSchemaValidation) { + if (argSchema && !shouldSkip(skipSchemaValidation, 'arg')) { finalQueryArg = await parseWithSchema( argSchema, finalQueryArg, @@ -633,7 +638,10 @@ export function buildThunks< let { data } = result - if (rawResponseSchema && !skipSchemaValidation) { + if ( + rawResponseSchema && + !shouldSkip(skipSchemaValidation, 'rawResponse') + ) { data = await parseWithSchema( rawResponseSchema, result.data, @@ -648,7 +656,7 @@ export function buildThunks< finalQueryArg, ) - if (responseSchema && !skipSchemaValidation) { + if (responseSchema && !shouldSkip(skipSchemaValidation, 'response')) { transformedResponse = await parseWithSchema( responseSchema, transformedResponse, @@ -751,7 +759,11 @@ export function buildThunks< finalQueryReturnValue = await executeRequest(arg.originalArgs) } - if (metaSchema && !skipSchemaValidation && finalQueryReturnValue.meta) { + if ( + metaSchema && + !shouldSkip(skipSchemaValidation, 'meta') && + finalQueryReturnValue.meta + ) { finalQueryReturnValue.meta = await parseWithSchema( metaSchema, finalQueryReturnValue.meta, @@ -781,7 +793,10 @@ export function buildThunks< let { value, meta } = caughtError try { - if (rawErrorResponseSchema && !skipSchemaValidation) { + if ( + rawErrorResponseSchema && + !shouldSkip(skipSchemaValidation, 'rawErrorResponse') + ) { value = await parseWithSchema( rawErrorResponseSchema, value, @@ -790,7 +805,7 @@ export function buildThunks< ) } - if (metaSchema && !skipSchemaValidation) { + if (metaSchema && !shouldSkip(skipSchemaValidation, 'meta')) { meta = await parseWithSchema(metaSchema, meta, 'metaSchema', meta) } let transformedErrorResponse = await transformErrorResponse( @@ -798,7 +813,10 @@ export function buildThunks< meta, arg.originalArgs, ) - if (errorResponseSchema && !skipSchemaValidation) { + if ( + errorResponseSchema && + !shouldSkip(skipSchemaValidation, 'errorResponse') + ) { transformedErrorResponse = await parseWithSchema( errorResponseSchema, transformedErrorResponse, diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index 0bf4bb4a6d..e91bb4231f 100644 --- a/packages/toolkit/src/query/createApi.ts +++ b/packages/toolkit/src/query/createApi.ts @@ -8,6 +8,7 @@ import type { EndpointDefinitions, SchemaFailureConverter, SchemaFailureHandler, + SchemaType, } from './endpointDefinitions' import { DefinitionType, @@ -280,6 +281,8 @@ export interface CreateApiOptions< * * If set to `true`, will skip schema validation for all endpoints, unless overridden by the endpoint. * + * Can be overridden for specific schemas by passing an array of schema types to skip. + * * @example * ```ts * // codeblock-meta no-transpile @@ -288,7 +291,7 @@ export interface CreateApiOptions< * * const api = createApi({ * baseQuery: fetchBaseQuery({ baseUrl: '/' }), - * skipSchemaValidation: process.env.NODE_ENV === "test", // skip schema validation in tests, since we'll be mocking the response + * skipSchemaValidation: process.env.NODE_ENV === "test" ? ["response"] : false, // skip schema validation for response in tests, since we'll be mocking the response * endpoints: (build) => ({ * getPost: build.query({ * query: ({ id }) => `/post/${id}`, @@ -298,7 +301,7 @@ export interface CreateApiOptions< * }) * ``` */ - skipSchemaValidation?: boolean + skipSchemaValidation?: boolean | SchemaType[] } export type CreateApi = { diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 0358eba2fd..9b092c9a5a 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -249,6 +249,14 @@ type BaseEndpointTypes< RawResultType: RawResultType } +export type SchemaType = + | 'arg' + | 'rawResponse' + | 'response' + | 'rawErrorResponse' + | 'errorResponse' + | 'meta' + interface CommonEndpointDefinition< QueryArg, BaseQuery extends BaseQueryFn, @@ -427,6 +435,8 @@ interface CommonEndpointDefinition< * If set to `true`, will skip schema validation for this endpoint. * Overrides the global setting. * + * Can be overridden for specific schemas by passing an array of schema types to skip. + * * @example * ```ts * // codeblock-meta no-transpile @@ -439,13 +449,13 @@ interface CommonEndpointDefinition< * getPost: build.query({ * query: ({ id }) => `/post/${id}`, * responseSchema: v.object({ id: v.number(), name: v.string() }), - * skipSchemaValidation: process.env.NODE_ENV === "test", // skip schema validation in tests, since we'll be mocking the response + * skipSchemaValidation: process.env.NODE_ENV === "test" ? ["response"] : false, // skip schema validation for response in tests, since we'll be mocking the response * }), * }) * }) * ``` */ - skipSchemaValidation?: boolean + skipSchemaValidation?: boolean | SchemaType[] } export type BaseEndpointDefinition< diff --git a/packages/toolkit/src/query/index.ts b/packages/toolkit/src/query/index.ts index 96a06166bf..d76c95efbf 100644 --- a/packages/toolkit/src/query/index.ts +++ b/packages/toolkit/src/query/index.ts @@ -49,6 +49,7 @@ export type { SchemaFailureHandler, SchemaFailureConverter, SchemaFailureInfo, + SchemaType, } from './endpointDefinitions' export { fetchBaseQuery } from './fetchBaseQuery' export type { diff --git a/packages/toolkit/src/query/standardSchema.ts b/packages/toolkit/src/query/standardSchema.ts index 75dd22f0d5..2c42a2b44e 100644 --- a/packages/toolkit/src/query/standardSchema.ts +++ b/packages/toolkit/src/query/standardSchema.ts @@ -1,21 +1,30 @@ import type { StandardSchemaV1 } from '@standard-schema/spec' import { SchemaError } from '@standard-schema/utils' +import type { SchemaType } from './endpointDefinitions' export class NamedSchemaError extends SchemaError { constructor( issues: readonly StandardSchemaV1.Issue[], public readonly value: any, - public readonly schemaName: string, + public readonly schemaName: `${SchemaType}Schema`, public readonly _bqMeta: any, ) { super(issues) } } +export const shouldSkip = ( + skipSchemaValidation: boolean | SchemaType[] | undefined, + schemaName: SchemaType, +) => + Array.isArray(skipSchemaValidation) + ? skipSchemaValidation.includes(schemaName) + : !!skipSchemaValidation + export async function parseWithSchema( schema: Schema, data: unknown, - schemaName: string, + schemaName: `${SchemaType}Schema`, bqMeta: any, ): Promise> { const result = await schema['~standard'].validate(data) diff --git a/packages/toolkit/src/query/tests/createApi.test.ts b/packages/toolkit/src/query/tests/createApi.test.ts index 824977a3b7..4ad3a9efe4 100644 --- a/packages/toolkit/src/query/tests/createApi.test.ts +++ b/packages/toolkit/src/query/tests/createApi.test.ts @@ -12,6 +12,7 @@ import type { FetchBaseQueryMeta, OverrideResultType, SchemaFailureConverter, + SchemaType, SerializeQueryArgs, TagTypesFromApi, } from '@reduxjs/toolkit/query' @@ -1227,7 +1228,7 @@ describe('endpoint schemas', () => { value, arg, }: { - schemaName: string + schemaName: `${SchemaType}Schema` value: unknown arg: unknown }) { @@ -1244,30 +1245,49 @@ describe('endpoint schemas', () => { } } + interface SkipApiOptions { + globalSkip?: boolean + endpointSkip?: boolean + useArray?: boolean + globalCatch?: boolean + endpointCatch?: boolean + } + + const apiOptions = ( + type: SchemaType, + { useArray, globalSkip, globalCatch }: SkipApiOptions = {}, + ) => ({ + onSchemaFailure: onSchemaFailureGlobal, + skipSchemaValidation: useArray ? globalSkip && [type] : globalSkip, + catchSchemaFailure: globalCatch ? schemaConverter : undefined, + }) + + const endpointOptions = ( + type: SchemaType, + { useArray, endpointSkip, endpointCatch }: SkipApiOptions = {}, + ) => ({ + onSchemaFailure: onSchemaFailureEndpoint, + skipSchemaValidation: useArray ? endpointSkip && [type] : endpointSkip, + catchSchemaFailure: endpointCatch ? schemaConverter : undefined, + }) + + const skipCases: [string, SkipApiOptions][] = [ + ['globally', { globalSkip: true }], + ['on the endpoint', { endpointSkip: true }], + ['globally (array)', { globalSkip: true, useArray: true }], + ['on the endpoint (array)', { endpointSkip: true, useArray: true }], + ] + describe('argSchema', () => { - const makeApi = ({ - globalSkip, - endpointSkip, - globalCatch, - endpointCatch, - }: { - globalSkip?: boolean - endpointSkip?: boolean - globalCatch?: boolean - endpointCatch?: boolean - } = {}) => + const makeApi = (opts?: SkipApiOptions) => createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), - onSchemaFailure: onSchemaFailureGlobal, - skipSchemaValidation: globalSkip, - catchSchemaFailure: globalCatch ? schemaConverter : undefined, + ...apiOptions('arg', opts), endpoints: (build) => ({ query: build.query({ query: ({ id }) => `/post/${id}`, argSchema: v.object({ id: v.number() }), - onSchemaFailure: onSchemaFailureEndpoint, - skipSchemaValidation: endpointSkip, - catchSchemaFailure: endpointCatch ? schemaConverter : undefined, + ...endpointOptions('arg', opts), }), }), }) @@ -1296,22 +1316,9 @@ describe('endpoint schemas', () => { arg: { id: '1' }, }) }) - test('can be skipped globally', async () => { - const api = makeApi({ globalSkip: true }) - - const storeRef = setupApiStore(api, undefined, { - withoutTestLifecycles: true, - }) - - const result = await storeRef.store.dispatch( - // @ts-expect-error - api.endpoints.query.initiate({ id: '1' }), - ) - expect(result?.error).toBeUndefined() - }) - test('can be skipped on the endpoint', async () => { - const api = makeApi({ endpointSkip: true }) + test.each(skipCases)('can be skipped %s', async (_, arg) => { + const api = makeApi(arg) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, @@ -1387,29 +1394,15 @@ describe('endpoint schemas', () => { }) }) describe('rawResponseSchema', () => { - const makeApi = ({ - globalSkip, - endpointSkip, - globalCatch, - endpointCatch, - }: { - globalSkip?: boolean - endpointSkip?: boolean - globalCatch?: boolean - endpointCatch?: boolean - } = {}) => + const makeApi = (opts?: SkipApiOptions) => createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), - onSchemaFailure: onSchemaFailureGlobal, - catchSchemaFailure: globalCatch ? schemaConverter : undefined, - skipSchemaValidation: globalSkip, + ...apiOptions('rawResponse', opts), endpoints: (build) => ({ query: build.query<{ success: boolean }, void>({ query: () => '/success', rawResponseSchema: v.object({ value: v.literal('success!') }), - onSchemaFailure: onSchemaFailureEndpoint, - catchSchemaFailure: endpointCatch ? schemaConverter : undefined, - skipSchemaValidation: endpointSkip, + ...endpointOptions('rawResponse', opts), }), }), }) @@ -1428,8 +1421,8 @@ describe('endpoint schemas', () => { arg: undefined, }) }) - test('can be skipped globally', async () => { - const api = makeApi({ globalSkip: true }) + test.each(skipCases)('can be skipped %s', async (_, arg) => { + const api = makeApi(arg) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) @@ -1488,30 +1481,16 @@ describe('endpoint schemas', () => { }) }) describe('responseSchema', () => { - const makeApi = ({ - globalSkip, - endpointSkip, - globalCatch, - endpointCatch, - }: { - globalSkip?: boolean - endpointSkip?: boolean - globalCatch?: boolean - endpointCatch?: boolean - } = {}) => + const makeApi = (opts?: SkipApiOptions) => createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), - onSchemaFailure: onSchemaFailureGlobal, - catchSchemaFailure: globalCatch ? schemaConverter : undefined, - skipSchemaValidation: globalSkip, + ...apiOptions('response', opts), endpoints: (build) => ({ query: build.query<{ success: boolean }, void>({ query: () => '/success', transformResponse: () => ({ success: false }), responseSchema: v.object({ success: v.literal(true) }), - onSchemaFailure: onSchemaFailureEndpoint, - catchSchemaFailure: endpointCatch ? schemaConverter : undefined, - skipSchemaValidation: endpointSkip, + ...endpointOptions('response', opts), }), }), }) @@ -1531,18 +1510,8 @@ describe('endpoint schemas', () => { arg: undefined, }) }) - test('can be skipped globally', async () => { - const api = makeApi({ globalSkip: true }) - const storeRef = setupApiStore(api, undefined, { - withoutTestLifecycles: true, - }) - const result = await storeRef.store.dispatch( - api.endpoints.query.initiate(), - ) - expect(result?.error).toBeUndefined() - }) - test('can be skipped on the endpoint', async () => { - const api = makeApi({ endpointSkip: true }) + test.each(skipCases)('can be skipped %s', async (_, arg) => { + const api = makeApi(arg) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) @@ -1591,22 +1560,10 @@ describe('endpoint schemas', () => { }) }) describe('rawErrorResponseSchema', () => { - const makeApi = ({ - globalSkip, - endpointSkip, - globalCatch, - endpointCatch, - }: { - globalSkip?: boolean - endpointSkip?: boolean - globalCatch?: boolean - endpointCatch?: boolean - } = {}) => + const makeApi = (opts?: SkipApiOptions) => createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), - onSchemaFailure: onSchemaFailureGlobal, - catchSchemaFailure: globalCatch ? schemaConverter : undefined, - skipSchemaValidation: globalSkip, + ...apiOptions('rawErrorResponse', opts), endpoints: (build) => ({ query: build.query<{ success: boolean }, void>({ query: () => '/error', @@ -1614,9 +1571,7 @@ describe('endpoint schemas', () => { status: v.pipe(v.number(), v.minValue(400), v.maxValue(499)), data: v.unknown(), }), - onSchemaFailure: onSchemaFailureEndpoint, - catchSchemaFailure: endpointCatch ? schemaConverter : undefined, - skipSchemaValidation: endpointSkip, + ...endpointOptions('rawErrorResponse', opts), }), }), }) @@ -1635,18 +1590,8 @@ describe('endpoint schemas', () => { arg: undefined, }) }) - test('can be skipped globally', async () => { - const api = makeApi({ globalSkip: true }) - const storeRef = setupApiStore(api, undefined, { - withoutTestLifecycles: true, - }) - const result = await storeRef.store.dispatch( - api.endpoints.query.initiate(), - ) - expect(result?.error).not.toEqual(serializedSchemaError) - }) - test('can be skipped on the endpoint', async () => { - const api = makeApi({ endpointSkip: true }) + test.each(skipCases)('can be skipped %s', async (_, arg) => { + const api = makeApi(arg) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) @@ -1695,22 +1640,10 @@ describe('endpoint schemas', () => { }) }) describe('errorResponseSchema', () => { - const makeApi = ({ - globalSkip, - endpointSkip, - globalCatch, - endpointCatch, - }: { - globalSkip?: boolean - endpointSkip?: boolean - globalCatch?: boolean - endpointCatch?: boolean - } = {}) => + const makeApi = (opts?: SkipApiOptions) => createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), - onSchemaFailure: onSchemaFailureGlobal, - catchSchemaFailure: globalCatch ? schemaConverter : undefined, - skipSchemaValidation: globalSkip, + ...apiOptions('errorResponse', opts), endpoints: (build) => ({ query: build.query<{ success: boolean }, void>({ query: () => '/error', @@ -1724,9 +1657,7 @@ describe('endpoint schemas', () => { error: v.literal('oh no'), data: v.unknown(), }), - onSchemaFailure: onSchemaFailureEndpoint, - catchSchemaFailure: endpointCatch ? schemaConverter : undefined, - skipSchemaValidation: endpointSkip, + ...endpointOptions('errorResponse', opts), }), }), }) @@ -1749,18 +1680,8 @@ describe('endpoint schemas', () => { arg: undefined, }) }) - test('can be skipped globally', async () => { - const api = makeApi({ globalSkip: true }) - const storeRef = setupApiStore(api, undefined, { - withoutTestLifecycles: true, - }) - const result = await storeRef.store.dispatch( - api.endpoints.query.initiate(), - ) - expect(result?.error).not.toEqual(serializedSchemaError) - }) - test('can be skipped on the endpoint', async () => { - const api = makeApi({ endpointSkip: true }) + test.each(skipCases)('can be skipped %s', async (_, arg) => { + const api = makeApi(arg) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) @@ -1817,22 +1738,10 @@ describe('endpoint schemas', () => { }) }) describe('metaSchema', () => { - const makeApi = ({ - globalSkip, - endpointSkip, - globalCatch, - endpointCatch, - }: { - globalSkip?: boolean - endpointSkip?: boolean - globalCatch?: boolean - endpointCatch?: boolean - } = {}) => + const makeApi = (opts?: SkipApiOptions) => createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), - onSchemaFailure: onSchemaFailureGlobal, - catchSchemaFailure: globalCatch ? schemaConverter : undefined, - skipSchemaValidation: globalSkip, + ...apiOptions('meta', opts), endpoints: (build) => ({ query: build.query<{ success: boolean }, void>({ query: () => '/success', @@ -1841,9 +1750,7 @@ describe('endpoint schemas', () => { response: v.instance(Response), timestamp: v.number(), }), - onSchemaFailure: onSchemaFailureEndpoint, - catchSchemaFailure: endpointCatch ? schemaConverter : undefined, - skipSchemaValidation: endpointSkip, + ...endpointOptions('meta', opts), }), }), }) @@ -1865,18 +1772,8 @@ describe('endpoint schemas', () => { arg: undefined, }) }) - test('can be skipped globally', async () => { - const api = makeApi({ globalSkip: true }) - const storeRef = setupApiStore(api, undefined, { - withoutTestLifecycles: true, - }) - const result = await storeRef.store.dispatch( - api.endpoints.query.initiate(), - ) - expect(result?.error).toBeUndefined() - }) - test('can be skipped on the endpoint', async () => { - const api = makeApi({ endpointSkip: true }) + test.each(skipCases)('can be skipped %s', async (_, arg) => { + const api = makeApi(arg) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, })