Skip to content

Commit 116db6e

Browse files
committed
add result, error, and meta schemas
1 parent d81dd20 commit 116db6e

File tree

3 files changed

+198
-31
lines changed

3 files changed

+198
-31
lines changed

packages/toolkit/src/query/core/buildThunks.ts

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -488,10 +488,13 @@ export function buildThunks<
488488
},
489489
) => {
490490
const endpointDefinition = endpointDefinitions[arg.endpointName]
491+
const { metaSchema } = endpointDefinition
491492

492493
try {
493-
let transformResponse: TransformCallback =
494-
getTransformCallbackForEndpoint(endpointDefinition, 'transformResponse')
494+
let transformResponse = getTransformCallbackForEndpoint(
495+
endpointDefinition,
496+
'transformResponse',
497+
)
495498

496499
const baseQueryApi = {
497500
signal,
@@ -548,11 +551,10 @@ export function buildThunks<
548551
finalQueryArg: unknown,
549552
): Promise<QueryReturnValue> {
550553
let result: QueryReturnValue
551-
const { extraOptions, argSchema } = endpointDefinition
554+
const { extraOptions, argSchema, rawResultSchema, resultSchema } =
555+
endpointDefinition
552556

553-
if (argSchema) {
554-
await parseWithSchema(argSchema, finalQueryArg)
555-
}
557+
if (argSchema) await parseWithSchema(argSchema, finalQueryArg)
556558

557559
if (forceQueryFn) {
558560
// upsertQueryData relies on this to pass in the user-provided value
@@ -607,12 +609,25 @@ export function buildThunks<
607609

608610
if (result.error) throw new HandledError(result.error, result.meta)
609611

610-
const transformedResponse = await transformResponse(
611-
result.data,
612+
let { data } = result
613+
614+
if (rawResultSchema) {
615+
data = await parseWithSchema(rawResultSchema, result.data)
616+
}
617+
618+
let transformedResponse = await transformResponse(
619+
data,
612620
result.meta,
613621
finalQueryArg,
614622
)
615623

624+
if (resultSchema) {
625+
transformedResponse = await parseWithSchema(
626+
resultSchema,
627+
transformedResponse,
628+
)
629+
}
630+
616631
return {
617632
...result,
618633
data: transformedResponse,
@@ -702,6 +717,13 @@ export function buildThunks<
702717
finalQueryReturnValue = await executeRequest(arg.originalArgs)
703718
}
704719

720+
if (metaSchema && finalQueryReturnValue.meta) {
721+
finalQueryReturnValue.meta = await parseWithSchema(
722+
metaSchema,
723+
finalQueryReturnValue.meta,
724+
)
725+
}
726+
705727
// console.log('Final result: ', transformedData)
706728
return fulfillWithValue(
707729
finalQueryReturnValue.data,
@@ -711,25 +733,43 @@ export function buildThunks<
711733
}),
712734
)
713735
} catch (error) {
714-
let catchedError = error
715-
if (catchedError instanceof HandledError) {
716-
let transformErrorResponse: TransformCallback =
717-
getTransformCallbackForEndpoint(
718-
endpointDefinition,
719-
'transformErrorResponse',
720-
)
736+
let caughtError = error
737+
if (caughtError instanceof HandledError) {
738+
let transformErrorResponse = getTransformCallbackForEndpoint(
739+
endpointDefinition,
740+
'transformErrorResponse',
741+
)
742+
const { rawErrorSchema, errorSchema } = endpointDefinition
743+
744+
let { value, meta } = caughtError
745+
746+
if (rawErrorSchema) {
747+
value = await parseWithSchema(rawErrorSchema, value)
748+
}
749+
750+
if (metaSchema) {
751+
meta = await parseWithSchema(metaSchema, meta)
752+
}
721753

722754
try {
755+
let transformedErrorResponse = await transformErrorResponse(
756+
value,
757+
meta,
758+
arg.originalArgs,
759+
)
760+
if (errorSchema) {
761+
transformedErrorResponse = await parseWithSchema(
762+
errorSchema,
763+
transformedErrorResponse,
764+
)
765+
}
766+
723767
return rejectWithValue(
724-
await transformErrorResponse(
725-
catchedError.value,
726-
catchedError.meta,
727-
arg.originalArgs,
728-
),
729-
addShouldAutoBatch({ baseQueryMeta: catchedError.meta }),
768+
transformedErrorResponse,
769+
addShouldAutoBatch({ baseQueryMeta: meta }),
730770
)
731771
} catch (e) {
732-
catchedError = e
772+
caughtError = e
733773
}
734774
}
735775
if (
@@ -739,12 +779,12 @@ export function buildThunks<
739779
console.error(
740780
`An unhandled error occurred processing a request for the endpoint "${arg.endpointName}".
741781
In the case of an unhandled error, no tags will be "provided" or "invalidated".`,
742-
catchedError,
782+
caughtError,
743783
)
744784
} else {
745-
console.error(catchedError)
785+
console.error(caughtError)
746786
}
747-
throw catchedError
787+
throw caughtError
748788
}
749789
}
750790

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ type EndpointDefinitionWithQuery<
119119

120120
/** A schema for the result *before* it's passed to `transformResponse` */
121121
rawResultSchema?: StandardSchemaV1<BaseQueryResult<BaseQuery>>
122+
123+
/** A schema for the error object returned by the `query` or `queryFn`, *before* it's passed to `transformErrorResponse` */
124+
rawErrorSchema?: StandardSchemaV1<BaseQueryError<BaseQuery>>
122125
}
123126

124127
type EndpointDefinitionWithQueryFn<
@@ -180,6 +183,7 @@ type EndpointDefinitionWithQueryFn<
180183
transformResponse?: never
181184
transformErrorResponse?: never
182185
rawResultSchema?: never
186+
rawErrorSchema?: never
183187
/**
184188
* Defaults to `true`.
185189
*
@@ -217,9 +221,12 @@ export type BaseEndpointDefinition<
217221
/** A schema for the result (including `transformResponse` if provided) */
218222
resultSchema?: StandardSchemaV1<ResultType>
219223

220-
/** A schema for the error object returned by the `query` or `queryFn` */
224+
/** A schema for the error object returned by the `query` or `queryFn` (including `transformErrorResponse` if provided) */
221225
errorSchema?: StandardSchemaV1<BaseQueryError<BaseQuery>>
222226

227+
/** A schema for the `meta` property returned by the `query` or `queryFn` */
228+
metaSchema?: StandardSchemaV1<BaseQueryMeta<BaseQuery>>
229+
223230
/* phantom type */
224231
[resultType]?: ResultType
225232
/* phantom type */

packages/toolkit/src/query/tests/createApi.test.ts

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { SerializedError } from '@reduxjs/toolkit'
88
import { configureStore, createAction, createReducer } from '@reduxjs/toolkit'
99
import type {
1010
DefinitionsFromApi,
11+
FetchBaseQueryError,
1112
FetchBaseQueryMeta,
1213
OverrideResultType,
1314
SerializeQueryArgs,
@@ -28,6 +29,7 @@ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
2829

2930
afterEach(() => {
3031
vi.clearAllMocks()
32+
server.resetHandlers()
3133
})
3234

3335
afterAll(() => {
@@ -1188,15 +1190,11 @@ describe('timeout behavior', () => {
11881190

11891191
describe('endpoint schemas', () => {
11901192
test("can be used to validate the endpoint's arguments", async () => {
1191-
server.use(
1192-
http.get('https://example.com/success/1', () => HttpResponse.json({})),
1193-
)
1194-
11951193
const api = createApi({
11961194
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
11971195
endpoints: (build) => ({
11981196
query: build.query<unknown, { id: number }>({
1199-
query: ({ id }) => `/success/${id}`,
1197+
query: ({ id }) => `/post/${id}`,
12001198
argSchema: v.object({ id: v.number() }),
12011199
}),
12021200
}),
@@ -1223,4 +1221,126 @@ describe('endpoint schemas', () => {
12231221
stack: expect.any(String),
12241222
})
12251223
})
1224+
test("can be used to validate the endpoint's raw result", async () => {
1225+
const api = createApi({
1226+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1227+
endpoints: (build) => ({
1228+
query: build.query<{ success: boolean }, void>({
1229+
query: () => '/success',
1230+
rawResultSchema: v.object({ value: v.literal('success!') }),
1231+
}),
1232+
}),
1233+
})
1234+
const storeRef = setupApiStore(api, undefined, {
1235+
withoutTestLifecycles: true,
1236+
})
1237+
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1238+
expect(result?.error).toEqual<SerializedError>({
1239+
name: 'SchemaError',
1240+
message: expect.any(String),
1241+
stack: expect.any(String),
1242+
})
1243+
})
1244+
test("can be used to validate the endpoint's final result", async () => {
1245+
server.use(
1246+
http.get('https://example.com/success/', () =>
1247+
HttpResponse.json({ success: true }),
1248+
),
1249+
)
1250+
const api = createApi({
1251+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1252+
endpoints: (build) => ({
1253+
query: build.query<{ success: boolean }, void>({
1254+
query: () => '/success',
1255+
transformResponse: () => ({ success: false }),
1256+
resultSchema: v.object({ success: v.literal(true) }),
1257+
}),
1258+
}),
1259+
})
1260+
const storeRef = setupApiStore(api, undefined, {
1261+
withoutTestLifecycles: true,
1262+
})
1263+
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1264+
expect(result?.error).toEqual<SerializedError>({
1265+
name: 'SchemaError',
1266+
message: expect.any(String),
1267+
stack: expect.any(String),
1268+
})
1269+
})
1270+
test("can be used to validate the endpoint's raw error result", async () => {
1271+
const api = createApi({
1272+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1273+
endpoints: (build) => ({
1274+
query: build.query<{ success: boolean }, void>({
1275+
query: () => '/error',
1276+
rawErrorSchema: v.object({
1277+
status: v.pipe(v.number(), v.minValue(400), v.maxValue(499)),
1278+
data: v.unknown(),
1279+
}),
1280+
}),
1281+
}),
1282+
})
1283+
const storeRef = setupApiStore(api, undefined, {
1284+
withoutTestLifecycles: true,
1285+
})
1286+
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1287+
expect(result?.error).toEqual<SerializedError>({
1288+
name: 'SchemaError',
1289+
message: expect.any(String),
1290+
stack: expect.any(String),
1291+
})
1292+
})
1293+
test("can be used to validate the endpoint's final error result", async () => {
1294+
const api = createApi({
1295+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1296+
endpoints: (build) => ({
1297+
query: build.query<{ success: boolean }, void>({
1298+
query: () => '/error',
1299+
transformErrorResponse: (error): FetchBaseQueryError => ({
1300+
status: 'CUSTOM_ERROR',
1301+
data: error,
1302+
error: 'whoops',
1303+
}),
1304+
errorSchema: v.object({
1305+
status: v.literal('CUSTOM_ERROR'),
1306+
error: v.literal('oh no'),
1307+
data: v.unknown(),
1308+
}),
1309+
}),
1310+
}),
1311+
})
1312+
const storeRef = setupApiStore(api, undefined, {
1313+
withoutTestLifecycles: true,
1314+
})
1315+
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1316+
expect(result?.error).toEqual<SerializedError>({
1317+
name: 'SchemaError',
1318+
message: expect.any(String),
1319+
stack: expect.any(String),
1320+
})
1321+
})
1322+
test("can be used to validate the endpoint's meta result", async () => {
1323+
const api = createApi({
1324+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1325+
endpoints: (build) => ({
1326+
query: build.query<{ success: boolean }, void>({
1327+
query: () => '/success',
1328+
metaSchema: v.object({
1329+
request: v.instance(Request),
1330+
response: v.instance(Response),
1331+
timestamp: v.number(),
1332+
}),
1333+
}),
1334+
}),
1335+
})
1336+
const storeRef = setupApiStore(api, undefined, {
1337+
withoutTestLifecycles: true,
1338+
})
1339+
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1340+
expect(result?.error).toEqual<SerializedError>({
1341+
name: 'SchemaError',
1342+
message: expect.any(String),
1343+
stack: expect.any(String),
1344+
})
1345+
})
12261346
})

0 commit comments

Comments
 (0)