Skip to content

Commit 16fd363

Browse files
Support mapError for Mutations
1 parent 3f2d8e2 commit 16fd363

File tree

7 files changed

+580
-5
lines changed

7 files changed

+580
-5
lines changed

apps/website/docs/api/factories/create_json_mutation.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ Config fields:
3838
- `params`: params which were passed to the [_Mutation_](/api/primitives/mutation)
3939
- `headers`: <Badge type="tip" text="since v0.13" /> raw response headers
4040

41+
- `mapError?`: <Badge type="tip" text="since v0.14" /> optional mapper for the error, available overloads:
42+
43+
- `(err) => mapped`
44+
- `{ source: Store, fn: (err, sourceValue) => mapped }`
45+
46+
`err` object contains:
47+
48+
- `error`: the original error that occurred
49+
- `params`: params which were passed to the [_Mutation_](/api/primitives/mutation)
50+
- `headers`: raw response headers (available for HTTP errors and contract/validation errors where the response was received, not available for network errors)
51+
4152
- `status.expected`: `number` or `Array<number>` of expected HTTP status codes, if the response status code is not in the list, the mutation will be treated as failed
4253

4354
- `concurrency?`: concurrency settings for the [_Mutation_](/api/primitives/mutation)
@@ -50,3 +61,77 @@ Config fields:
5061
:::
5162

5263
- `abort?`: [_Event_](https://effector.dev/en/api/effector/event/) after calling which all in-flight requests will be aborted
64+
65+
## Examples
66+
67+
### Error mapping
68+
69+
You can transform errors before they are passed to the [_Mutation_](/api/primitives/mutation) using `mapError`:
70+
71+
```ts
72+
const loginMutation = createJsonMutation({
73+
params: declareParams<{ login: string; password: string }>(),
74+
request: {
75+
method: 'POST',
76+
url: 'https://api.salo.com/login',
77+
body: ({ login, password }) => ({ login, password }),
78+
},
79+
response: {
80+
contract: loginContract,
81+
mapError({ error, params, headers }) {
82+
if (isHttpError({ status: 401, error })) {
83+
return {
84+
type: 'unauthorized',
85+
message: 'Invalid credentials',
86+
requestId: headers?.get('X-Request-Id'),
87+
};
88+
}
89+
if (isHttpError({ status: 429, error })) {
90+
return {
91+
type: 'rate_limited',
92+
message: 'Too many attempts, please try again later',
93+
requestId: headers?.get('X-Request-Id'),
94+
};
95+
}
96+
return {
97+
type: 'unknown',
98+
message: 'Something went wrong',
99+
requestId: headers?.get('X-Request-Id'),
100+
};
101+
},
102+
},
103+
});
104+
```
105+
106+
With a sourced mapper:
107+
108+
```ts
109+
const $errorMessages = createStore({
110+
401: 'Invalid credentials',
111+
429: 'Too many attempts',
112+
});
113+
114+
const loginMutation = createJsonMutation({
115+
params: declareParams<{ login: string; password: string }>(),
116+
request: {
117+
method: 'POST',
118+
url: 'https://api.salo.com/login',
119+
body: ({ login, password }) => ({ login, password }),
120+
},
121+
response: {
122+
contract: loginContract,
123+
mapError: {
124+
source: $errorMessages,
125+
fn({ error, headers }, messages) {
126+
if (isHttpError({ error })) {
127+
return {
128+
message: messages[error.status] ?? 'Unknown error',
129+
requestId: headers?.get('X-Request-Id'),
130+
};
131+
}
132+
return { message: 'Network error', requestId: null };
133+
},
134+
},
135+
},
136+
});
137+
```

apps/website/docs/api/factories/create_mutation.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,54 @@ const loginMutation = createMutation({
7171
// params: { login: string, password: string }
7272
// }>
7373
```
74+
75+
### `createMutation({ effect, contract?, mapError: Function })` <Badge type="tip" text="since v0.14" />
76+
77+
Creates [_Mutation_](/api/primitives/mutation) based on given [_Effect_](https://effector.dev/en/api/effector/effect/). When the [_Mutation_](/api/primitives/mutation) fails, the error is passed to `mapError` callback, and the result of the callback will be treated as the error of the [_Mutation_](/api/primitives/mutation).
78+
79+
```ts
80+
const loginMutation = createMutation({
81+
effect: loginFx,
82+
contract: loginContract,
83+
mapError({ error, params }) {
84+
// Transform any error into a user-friendly message
85+
if (isHttpError({ status: 401, error })) {
86+
return { code: 'UNAUTHORIZED', message: 'Invalid credentials' };
87+
}
88+
return { code: 'UNKNOWN', message: 'Failed to login' };
89+
},
90+
});
91+
92+
// typeof loginMutation.finished.failure === Event<{
93+
// error: { code: string, message: string },
94+
// params: { login: string, password: string }
95+
// }>
96+
```
97+
98+
### `createMutation({ effect, contract?, mapError: { source, fn } })` <Badge type="tip" text="since v0.14" />
99+
100+
Creates [_Mutation_](/api/primitives/mutation) based on given [_Effect_](https://effector.dev/en/api/effector/effect/). When the [_Mutation_](/api/primitives/mutation) fails, the error is passed to `mapError.fn` callback as well as original parameters of the [_Mutation_](/api/primitives/mutation) and current value of `mapError.source` [_Store_](https://effector.dev/en/api/effector/store/), result of the callback will be treated as the error of the [_Mutation_](/api/primitives/mutation).
101+
102+
```ts
103+
const $errorMessages = createStore({
104+
401: 'Invalid credentials',
105+
403: 'Access denied',
106+
});
107+
108+
const loginMutation = createMutation({
109+
effect: loginFx,
110+
contract: loginContract,
111+
mapError: {
112+
source: $errorMessages,
113+
fn({ error, params }, errorMessages) {
114+
if (isHttpError({ error })) {
115+
return {
116+
message: errorMessages[error.status] ?? 'Unknown error',
117+
status: error.status,
118+
};
119+
}
120+
return { message: 'Network error', status: null };
121+
},
122+
},
123+
});
124+
```

apps/website/docs/recipes/data_flow.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,36 @@ const userQuery = createJsonQuery({
175175
The error mapper receives the following data:
176176

177177
- `error`: the original error that occurred
178-
- `params`: the parameters that were passed to the [_Query_](/api/primitives/query)
178+
- `params`: the parameters that were passed to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation)
179179
- `headers`: raw response headers (available for HTTP errors and contract/validation errors where the response was received, not available for network errors)
180180

181+
The same `mapError` option is available for [_Mutations_](/api/primitives/mutation):
182+
183+
```ts
184+
const $errorMessages = createStore({
185+
401: 'Invalid credentials',
186+
429: 'Too many attempts',
187+
});
188+
189+
const loginMutation = createJsonMutation({
190+
//...
191+
response: {
192+
mapError: {
193+
source: $errorMessages,
194+
fn: ({ error, headers }, messages) => {
195+
if (isHttpError({ error })) {
196+
return {
197+
message: messages[error.status] ?? 'Unknown error',
198+
requestId: headers?.get('X-Request-Id'),
199+
};
200+
}
201+
return { message: 'Network error', requestId: null };
202+
},
203+
},
204+
},
205+
});
206+
```
207+
181208
## Data-flow in basic factories
182209

183210
**Basic factories** are used to create _Remote Operations_ with a more control of data-flow in user-land. In this case, the user-land code have to describe **request-response cycle** and **response parsing** stages. Other stages could be handled by the library, but it is not required for **basic factories**.

0 commit comments

Comments
 (0)