Skip to content

Commit 51d26ae

Browse files
authored
Update message format for CombinedGraphQLErrors and CombinedProtocolErrors (#12557)
1 parent 5dffbbe commit 51d26ae

File tree

9 files changed

+343
-27
lines changed

9 files changed

+343
-27
lines changed

.api-reports/api-report-errors.api.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,46 @@ import type { FetchResult } from '@apollo/client/link/core';
99
import type { FetchResult as FetchResult_2 } from '@apollo/client';
1010
import type { GraphQLFormattedError } from 'graphql';
1111

12+
// @public (undocumented)
13+
export namespace CombinedGraphQLErrors {
14+
// (undocumented)
15+
export type MessageFormatter = (errors: ReadonlyArray<GraphQLFormattedError>, options: MessageFormatterOptions) => string;
16+
// (undocumented)
17+
export interface MessageFormatterOptions {
18+
// (undocumented)
19+
defaultFormatMessage: (errors: ReadonlyArray<GraphQLFormattedError>) => string;
20+
// (undocumented)
21+
result: FetchResult_2<unknown>;
22+
}
23+
}
24+
1225
// @public
1326
export class CombinedGraphQLErrors extends Error {
1427
constructor(result: FetchResult_2<unknown>);
1528
readonly data: Record<string, unknown> | null | undefined;
1629
readonly errors: ReadonlyArray<GraphQLFormattedError>;
30+
static formatMessage: CombinedGraphQLErrors.MessageFormatter;
1731
static is(error: unknown): error is CombinedGraphQLErrors;
1832
}
1933

34+
// @public (undocumented)
35+
export namespace CombinedProtocolErrors {
36+
// (undocumented)
37+
export type MessageFormatter = (errors: ReadonlyArray<GraphQLFormattedError>, options: MessageFormatterOptions) => string;
38+
// (undocumented)
39+
export interface MessageFormatterOptions {
40+
// (undocumented)
41+
defaultFormatMessage: (errors: ReadonlyArray<GraphQLFormattedError>) => string;
42+
}
43+
}
44+
2045
// @public
2146
export class CombinedProtocolErrors extends Error {
2247
constructor(protocolErrors: Array<GraphQLFormattedError> | ReadonlyArray<GraphQLFormattedError>);
2348
// (undocumented)
2449
errors: ReadonlyArray<GraphQLFormattedError>;
50+
// (undocumented)
51+
static formatMessage: CombinedProtocolErrors.MessageFormatter;
2552
static is(error: unknown): error is CombinedProtocolErrors;
2653
}
2754

.api-reports/api-report.api.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,19 +390,46 @@ type CombineByTypeName<T extends {
390390
[TypeName in NonNullable<T["__typename"]>]: Prettify<MergeUnions<ExtractByMatchingTypeNames<T, TypeName>>>;
391391
}[NonNullable<T["__typename"]>];
392392

393+
// @public (undocumented)
394+
export namespace CombinedGraphQLErrors {
395+
// (undocumented)
396+
export type MessageFormatter = (errors: ReadonlyArray<GraphQLFormattedError>, options: MessageFormatterOptions) => string;
397+
// (undocumented)
398+
export interface MessageFormatterOptions {
399+
// (undocumented)
400+
defaultFormatMessage: (errors: ReadonlyArray<GraphQLFormattedError>) => string;
401+
// (undocumented)
402+
result: FetchResult<unknown>;
403+
}
404+
}
405+
393406
// @public
394407
export class CombinedGraphQLErrors extends Error {
395408
constructor(result: FetchResult<unknown>);
396409
readonly data: Record<string, unknown> | null | undefined;
397410
readonly errors: ReadonlyArray<GraphQLFormattedError>;
411+
static formatMessage: CombinedGraphQLErrors.MessageFormatter;
398412
static is(error: unknown): error is CombinedGraphQLErrors;
399413
}
400414

415+
// @public (undocumented)
416+
export namespace CombinedProtocolErrors {
417+
// (undocumented)
418+
export type MessageFormatter = (errors: ReadonlyArray<GraphQLFormattedError>, options: MessageFormatterOptions) => string;
419+
// (undocumented)
420+
export interface MessageFormatterOptions {
421+
// (undocumented)
422+
defaultFormatMessage: (errors: ReadonlyArray<GraphQLFormattedError>) => string;
423+
}
424+
}
425+
401426
// @public
402427
export class CombinedProtocolErrors extends Error {
403428
constructor(protocolErrors: Array<GraphQLFormattedError> | ReadonlyArray<GraphQLFormattedError>);
404429
// (undocumented)
405430
errors: ReadonlyArray<GraphQLFormattedError>;
431+
// (undocumented)
432+
static formatMessage: CombinedProtocolErrors.MessageFormatter;
406433
static is(error: unknown): error is CombinedProtocolErrors;
407434
}
408435

.changeset/cool-kiwis-hunt.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@apollo/client": minor
3+
---
4+
5+
Add ability to specify message formatter for `CombinedGraphQLErrors` and `CombinedProtocolErrors`. To provide your own message formatter, override the static `formatMessage` property on these classes.
6+
7+
```ts
8+
CombinedGraphQLErrors.formatMessage = (errors, { result, defaultFormatMessage }) => {
9+
return "Some formatted message"
10+
};
11+
12+
CombinedProtocolErrors.formatMessage = (errors, { defaultFormatMessage }) => {
13+
return "Some formatted message"
14+
};
15+
```

.changeset/seven-dragons-repair.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Update format of the error message for `CombinedGraphQLErrors` and `CombinedProtocolErrors` to be more like v3.x.
6+
7+
```diff
8+
console.log(error.message);
9+
- `The GraphQL server returned with errors:
10+
- - Email not found
11+
- - Username already in use`
12+
+ `Email not found
13+
+ Username already in use`
14+
```

.size-limits.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43100,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38618,
2+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43116,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38560,
44
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33026,
5-
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27957
5+
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27953
66
}

src/errors/CombinedGraphQLErrors.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ import { getGraphQLErrorsFromResult } from "@apollo/client/utilities";
55

66
import { brand, isBranded } from "./utils.js";
77

8+
export declare namespace CombinedGraphQLErrors {
9+
export interface MessageFormatterOptions {
10+
result: FetchResult<unknown>;
11+
defaultFormatMessage: (
12+
errors: ReadonlyArray<GraphQLFormattedError>
13+
) => string;
14+
}
15+
16+
export type MessageFormatter = (
17+
errors: ReadonlyArray<GraphQLFormattedError>,
18+
options: MessageFormatterOptions
19+
) => string;
20+
}
21+
22+
function defaultFormatMessage(errors: ReadonlyArray<GraphQLFormattedError>) {
23+
return (
24+
errors
25+
// Handle non-spec-compliant servers: See #1185
26+
.filter((e) => e)
27+
.map((e) => e.message || "Error message not found.")
28+
.join("\n")
29+
);
30+
}
31+
832
/**
933
* Represents the combined list of GraphQL errors returned from the server in a
1034
* GraphQL response.
@@ -15,6 +39,13 @@ export class CombinedGraphQLErrors extends Error {
1539
return isBranded(error, "CombinedGraphQLErrors");
1640
}
1741

42+
/**
43+
* Formats the error message used for the error `message` property. Override
44+
* to provide your own formatting.
45+
*/
46+
static formatMessage: CombinedGraphQLErrors.MessageFormatter =
47+
defaultFormatMessage;
48+
1849
/**
1950
* The raw list of GraphQL errors returned in a GraphQL response.
2051
*/
@@ -28,7 +59,12 @@ export class CombinedGraphQLErrors extends Error {
2859
constructor(result: FetchResult<unknown>) {
2960
const errors = getGraphQLErrorsFromResult(result);
3061

31-
super(formatMessage(errors));
62+
super(
63+
CombinedGraphQLErrors.formatMessage(errors, {
64+
result,
65+
defaultFormatMessage,
66+
})
67+
);
3268
this.errors = errors;
3369
this.data = result.data as Record<string, unknown>;
3470
this.name = "CombinedGraphQLErrors";
@@ -37,16 +73,3 @@ export class CombinedGraphQLErrors extends Error {
3773
Object.setPrototypeOf(this, CombinedGraphQLErrors.prototype);
3874
}
3975
}
40-
41-
function formatMessage(
42-
errors: Array<GraphQLFormattedError> | ReadonlyArray<GraphQLFormattedError>
43-
) {
44-
const messageList = errors
45-
// Handle non-spec-compliant servers: See #1185
46-
.filter((e) => e)
47-
.map((e) => `- ${e.message}`)
48-
.join("\n");
49-
50-
return `The GraphQL server returned with errors:
51-
${messageList}`;
52-
}

src/errors/CombinedProtocolErrors.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ import type { GraphQLFormattedError } from "graphql";
22

33
import { brand, isBranded } from "./utils.js";
44

5+
export declare namespace CombinedProtocolErrors {
6+
export interface MessageFormatterOptions {
7+
defaultFormatMessage: (
8+
errors: ReadonlyArray<GraphQLFormattedError>
9+
) => string;
10+
}
11+
12+
export type MessageFormatter = (
13+
errors: ReadonlyArray<GraphQLFormattedError>,
14+
options: MessageFormatterOptions
15+
) => string;
16+
}
17+
18+
function defaultFormatMessage(errors: ReadonlyArray<GraphQLFormattedError>) {
19+
return errors.map((e) => e.message || "Error message not found.").join("\n");
20+
}
21+
522
/**
623
* Fatal transport-level errors returned when executing a subscription using the
724
* multipart HTTP subscription protocol. See the documentation on the
@@ -13,27 +30,25 @@ export class CombinedProtocolErrors extends Error {
1330
return isBranded(error, "CombinedProtocolErrors");
1431
}
1532

33+
static formatMessage: CombinedProtocolErrors.MessageFormatter =
34+
defaultFormatMessage;
35+
1636
errors: ReadonlyArray<GraphQLFormattedError>;
1737

1838
constructor(
1939
protocolErrors:
2040
| Array<GraphQLFormattedError>
2141
| ReadonlyArray<GraphQLFormattedError>
2242
) {
23-
super(formatMessage(protocolErrors));
43+
super(
44+
CombinedProtocolErrors.formatMessage(protocolErrors, {
45+
defaultFormatMessage,
46+
})
47+
);
2448
this.name = "CombinedProtocolErrors";
2549
this.errors = protocolErrors;
2650

2751
brand(this);
2852
Object.setPrototypeOf(this, CombinedProtocolErrors.prototype);
2953
}
3054
}
31-
32-
function formatMessage(
33-
errors: Array<GraphQLFormattedError> | ReadonlyArray<GraphQLFormattedError>
34-
) {
35-
const messageList = errors.map((e) => `- ${e.message}`).join("\n");
36-
37-
return `The GraphQL server returned with errors:
38-
${messageList}`;
39-
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { FetchResult } from "@apollo/client";
2+
import { CombinedGraphQLErrors } from "@apollo/client/errors";
3+
4+
const defaultFormatMessage = CombinedGraphQLErrors.formatMessage;
5+
6+
afterEach(() => {
7+
CombinedGraphQLErrors.formatMessage = defaultFormatMessage;
8+
});
9+
10+
test("uses default message format", () => {
11+
const error = new CombinedGraphQLErrors({
12+
errors: [{ message: "Email already taken" }],
13+
});
14+
15+
expect(error.message).toMatchInlineSnapshot(`"Email already taken"`);
16+
17+
const multipleErrors = new CombinedGraphQLErrors({
18+
errors: [
19+
{ message: "Username already in use" },
20+
{ message: "Password doesn't match" },
21+
],
22+
});
23+
24+
expect(multipleErrors.message).toMatchInlineSnapshot(`
25+
"Username already in use
26+
Password doesn't match"
27+
`);
28+
});
29+
30+
test("adds default message for empty error messages", () => {
31+
const error = new CombinedGraphQLErrors({
32+
errors: [{ message: "" }],
33+
});
34+
35+
expect(error.message).toMatchInlineSnapshot(`"Error message not found."`);
36+
37+
const multipleErrors = new CombinedGraphQLErrors({
38+
errors: [{ message: "Username already in use" }, { message: "" }],
39+
});
40+
41+
expect(multipleErrors.message).toMatchInlineSnapshot(`
42+
"Username already in use
43+
Error message not found."
44+
`);
45+
});
46+
47+
test("allows message formatter to be overwritten", () => {
48+
const errors = [{ message: "Email already taken" }];
49+
const result: FetchResult = { data: { registerUser: null }, errors };
50+
51+
{
52+
const formatMessage = jest.fn(() => "Errors happened");
53+
CombinedGraphQLErrors.formatMessage = formatMessage;
54+
55+
const error = new CombinedGraphQLErrors(result);
56+
57+
expect(error.message).toBe("Errors happened");
58+
expect(formatMessage).toHaveBeenCalledWith(errors, {
59+
defaultFormatMessage: expect.any(Function),
60+
result,
61+
});
62+
}
63+
64+
{
65+
const formatMessage = jest.fn(() => "Oops. Something went wrong");
66+
CombinedGraphQLErrors.formatMessage = formatMessage;
67+
68+
const error = new CombinedGraphQLErrors(result);
69+
70+
expect(error.message).toBe("Oops. Something went wrong");
71+
expect(formatMessage).toHaveBeenCalledWith(errors, {
72+
defaultFormatMessage: expect.any(Function),
73+
result,
74+
});
75+
}
76+
});
77+
78+
test("can use default formatter from options", () => {
79+
CombinedGraphQLErrors.formatMessage = (errors, { defaultFormatMessage }) =>
80+
`Overwritten error message:\n ${defaultFormatMessage(errors)}`;
81+
82+
const error = new CombinedGraphQLErrors({
83+
errors: [{ message: "Email already taken" }],
84+
});
85+
86+
expect(error.message).toMatchInlineSnapshot(`
87+
"Overwritten error message:
88+
Email already taken"
89+
`);
90+
91+
const multipleErrors = new CombinedGraphQLErrors({
92+
errors: [
93+
{ message: "Username already in use" },
94+
{ message: "Password doesn't match" },
95+
],
96+
});
97+
98+
expect(multipleErrors.message).toMatchInlineSnapshot(`
99+
"Overwritten error message:
100+
Username already in use
101+
Password doesn't match"
102+
`);
103+
});

0 commit comments

Comments
 (0)