Skip to content

Commit 53b2964

Browse files
committed
Copy code from vue-apollo-smart-ops/error
1 parent 82de629 commit 53b2964

5 files changed

Lines changed: 361 additions & 0 deletions

File tree

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"optionalDependencies": {
3232
},
3333
"peerDependencies": {
34+
"apollo-client": ">=2.6",
35+
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
36+
"vue": ">=2.6"
3437
},
3538
"devDependencies": {
3639
"@types/jest": "27.4.0",

src/ApolloErrorHandlerResult.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
ApolloErrorType,
3+
InputValidationError,
4+
NetworkError,
5+
ProcessedApolloError,
6+
ServerError,
7+
UnauthorizedError,
8+
UserInputError,
9+
ValidationRuleViolation,
10+
} from './types';
11+
12+
export interface ApolloErrorHandlerResultInterface {
13+
allErrors: ProcessedApolloError[];
14+
validationRuleViolations?: ValidationRuleViolation[];
15+
}
16+
17+
export class ApolloErrorHandlerResult implements ApolloErrorHandlerResultInterface {
18+
public readonly allErrors: ProcessedApolloError[];
19+
20+
public constructor(
21+
public readonly unhandledErrors: ProcessedApolloError[],
22+
public readonly handledErrors: ProcessedApolloError[],
23+
) {
24+
this.allErrors = [...unhandledErrors, ...handledErrors];
25+
}
26+
27+
public get networkErrors(): NetworkError[] {
28+
return this.allErrors.filter((e): e is NetworkError => e.type === ApolloErrorType.NETWORK_ERROR);
29+
}
30+
31+
public get serverErrors(): ServerError[] {
32+
return this.allErrors.filter((e): e is ServerError => e.type === ApolloErrorType.SERVER_ERROR);
33+
}
34+
35+
public get unauthorizedErrors(): UnauthorizedError[] {
36+
return this.allErrors.filter((e): e is UnauthorizedError => e.type === ApolloErrorType.UNAUTHORIZED_ERROR);
37+
}
38+
39+
public get userInputErrors(): UserInputError[] {
40+
return this.allErrors.filter((e): e is UserInputError => e.type === ApolloErrorType.BAD_USER_INPUT);
41+
}
42+
43+
public get inputValidationErrors(): InputValidationError[] {
44+
return this.allErrors.filter((e): e is InputValidationError => e.type === ApolloErrorType.INPUT_VALIDATION_ERROR);
45+
}
46+
47+
public get validationRuleViolations(): ValidationRuleViolation[] {
48+
return this.inputValidationErrors.flatMap(e => e.violations);
49+
}
50+
}

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* @file Automatically generated by barrelsby.
3+
*/
4+
5+
export * from './ApolloErrorHandlerResult';
6+
export * from './processApolloError';
7+
export * from './types';

src/processApolloError.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import Vue from 'vue';
2+
import {
3+
ApolloError,
4+
ApolloErrorType,
5+
ApolloOperationContext,
6+
GraphQLError,
7+
InputValidationError,
8+
NetworkError,
9+
ProcessedApolloError,
10+
ServerError,
11+
UnauthorizedError,
12+
} from './types';
13+
14+
export function isApolloError(error: ApolloError | any): error is ApolloError {
15+
return error.graphQLErrors !== undefined;
16+
}
17+
18+
export function isGraphQLError(error: GraphQLError | any): error is GraphQLError {
19+
return error.extensions !== undefined;
20+
}
21+
22+
function normalizeErrorMessage(message: GraphQLError['message']): string {
23+
if (typeof message === 'object') {
24+
if (message.error != null) {
25+
return message.error;
26+
}
27+
28+
return 'Unknown error: ' + JSON.stringify(message);
29+
}
30+
31+
return message;
32+
}
33+
34+
function normalizeError(error: GraphQLError | Error): Error {
35+
if (isGraphQLError(error)) {
36+
return {
37+
...error,
38+
message: normalizeErrorMessage(error.message),
39+
};
40+
}
41+
42+
return error;
43+
}
44+
45+
function translateErrorMessage(messageOrCode: string, translations: Record<string, string>): string {
46+
return translations[messageOrCode] ?? messageOrCode;
47+
}
48+
49+
export interface ApolloErrorProcessorOptions<TApp = Vue, TContext = ApolloOperationContext> {
50+
app?: TApp;
51+
context?: TContext;
52+
isUnauthorizedError?: (error: GraphQLError) => boolean;
53+
onUnauthorizedError?: (error: UnauthorizedError) => boolean | void;
54+
onInputValidationError?: (error: InputValidationError) => boolean | void;
55+
onServerError?: (error: ServerError) => boolean | void;
56+
onNetworkError?: (error: NetworkError) => boolean | void;
57+
translations?: Record<string, string>;
58+
}
59+
60+
export const defaultErrorMessageTranslations: Record<string, string> = {
61+
FAILED_TO_FETCH:
62+
'Unable to communicate with server. The service may be down or you may be offline. Try again in a moment.',
63+
INTERNAL_SERVER_ERROR: `A server error has occurred.`,
64+
};
65+
66+
const defaultProcessorOptions: ApolloErrorProcessorOptions = {
67+
isUnauthorizedError: (error: GraphQLError) =>
68+
error.message === 'Unauthorized' ||
69+
error.extensions?.code === 'FORBIDDEN' ||
70+
error.extensions?.exception?.status === 401,
71+
translations: defaultErrorMessageTranslations,
72+
};
73+
74+
export interface ApolloErrorProcessorResult {
75+
unhandledErrors: ProcessedApolloError[];
76+
handledErrors: ProcessedApolloError[];
77+
}
78+
79+
function processGraphQLError(
80+
error: GraphQLError,
81+
options: ApolloErrorProcessorOptions = {},
82+
): ApolloErrorProcessorResult {
83+
options = { ...defaultProcessorOptions, ...options };
84+
const { isUnauthorizedError, onUnauthorizedError, onInputValidationError, onServerError, translations } = options;
85+
86+
if (isUnauthorizedError != null && isUnauthorizedError(error)) {
87+
// Unauthorized (not logged in, or not allowed) error
88+
const processedError: UnauthorizedError = {
89+
type: ApolloErrorType.UNAUTHORIZED_ERROR,
90+
error: normalizeError(error),
91+
message: normalizeErrorMessage(error.message),
92+
path: error.path,
93+
};
94+
95+
if (onUnauthorizedError != null && onUnauthorizedError(processedError)) {
96+
return { unhandledErrors: [], handledErrors: [processedError] };
97+
}
98+
99+
return { unhandledErrors: [processedError], handledErrors: [] };
100+
}
101+
102+
if (error.extensions?.validationErrors != null) {
103+
// User input validation error
104+
const processedError: InputValidationError = {
105+
type: ApolloErrorType.INPUT_VALIDATION_ERROR,
106+
error: normalizeError(error),
107+
message: normalizeErrorMessage(error.message),
108+
path: error.path,
109+
invalidArgs: error.extensions.invalidArgs,
110+
violations: error.extensions.validationErrors,
111+
};
112+
113+
if (onInputValidationError != null && onInputValidationError(processedError)) {
114+
return { unhandledErrors: [], handledErrors: [processedError] };
115+
}
116+
117+
return { unhandledErrors: [processedError], handledErrors: [] };
118+
}
119+
120+
// Other GraphQL resolver error - probably a bug
121+
const processedError: ServerError = {
122+
type: error.extensions?.code != null ? error.extensions.code : ApolloErrorType.SERVER_ERROR,
123+
error: normalizeError(error),
124+
message: translateErrorMessage('INTERNAL_SERVER_ERROR', translations ?? defaultErrorMessageTranslations),
125+
path: error.path,
126+
};
127+
128+
if (onServerError != null && onServerError(processedError)) {
129+
return { unhandledErrors: [], handledErrors: [processedError] };
130+
}
131+
132+
return { unhandledErrors: [processedError], handledErrors: [] };
133+
}
134+
135+
function processNetworkError(
136+
error: ApolloError,
137+
options: ApolloErrorProcessorOptions = {},
138+
): ApolloErrorProcessorResult {
139+
options = { ...defaultProcessorOptions, ...options };
140+
const { onNetworkError, translations } = options;
141+
142+
let message: string =
143+
error.networkError != null && error.networkError.message != null
144+
? normalizeErrorMessage(error.networkError.message)
145+
: normalizeErrorMessage(error.message);
146+
147+
switch (message) {
148+
case 'Failed to fetch':
149+
message = translateErrorMessage('FAILED_TO_FETCH', translations ?? defaultErrorMessageTranslations);
150+
break;
151+
}
152+
153+
const processedError: NetworkError = {
154+
type: ApolloErrorType.NETWORK_ERROR,
155+
error,
156+
statusCode: error.networkError != null ? error.networkError.statusCode : undefined,
157+
message,
158+
};
159+
160+
if (onNetworkError != null && onNetworkError(processedError)) {
161+
return { unhandledErrors: [], handledErrors: [processedError] };
162+
}
163+
164+
return { unhandledErrors: [processedError], handledErrors: [processedError] };
165+
}
166+
167+
export function processApolloError(
168+
error: ApolloError,
169+
options: ApolloErrorProcessorOptions = {},
170+
): ApolloErrorProcessorResult {
171+
options = { ...defaultProcessorOptions, ...options };
172+
const { onServerError } = options;
173+
174+
if (error.graphQLErrors != null && error.graphQLErrors.length > 0) {
175+
// Successful request but with errors from the resolver
176+
const errorProcessorResults = error.graphQLErrors.map(graphQLError => processGraphQLError(graphQLError, options));
177+
178+
return {
179+
unhandledErrors: errorProcessorResults.flatMap(result => result.unhandledErrors),
180+
handledErrors: errorProcessorResults.flatMap(result => result.handledErrors),
181+
};
182+
}
183+
184+
if (error.networkError?.result?.errors != null && error.networkError.result.errors.length > 0) {
185+
// Network error that contains GraphQL errors inside it. Can occur when server responds with a non-200 status code
186+
const errorProcessorResults = error.networkError.result.errors.map(graphQLError =>
187+
processGraphQLError(graphQLError, options),
188+
);
189+
190+
return {
191+
unhandledErrors: errorProcessorResults.flatMap(result => result.unhandledErrors),
192+
handledErrors: errorProcessorResults.flatMap(result => result.handledErrors),
193+
};
194+
}
195+
196+
if (error.networkError != null) {
197+
// Network error, e.g. server is not responding or some other exception occurs
198+
return processNetworkError(error, options);
199+
}
200+
201+
// Some other internal server error
202+
const processedError: ServerError = {
203+
type: ApolloErrorType.SERVER_ERROR,
204+
error,
205+
message: error.message,
206+
};
207+
208+
if (onServerError != null && onServerError(processedError)) {
209+
return { unhandledErrors: [], handledErrors: [processedError] };
210+
}
211+
212+
return { unhandledErrors: [processedError], handledErrors: [] };
213+
}

src/types.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ApolloError as BaseApolloError } from 'apollo-client';
2+
import { GraphQLError as BaseGraphQLError } from 'graphql';
3+
import { Vue } from 'vue/types/vue';
4+
import { ApolloErrorHandlerResultInterface } from './ApolloErrorHandlerResult';
5+
6+
export type ApolloOperationContext<TAttrs = Record<string, any>> = TAttrs;
7+
8+
export enum ApolloErrorType {
9+
NETWORK_ERROR = 'NETWORK_ERROR',
10+
SERVER_ERROR = 'SERVER_ERROR',
11+
UNAUTHORIZED_ERROR = 'UNAUTHORIZED_ERROR',
12+
BAD_USER_INPUT = 'BAD_USER_INPUT',
13+
INPUT_VALIDATION_ERROR = 'INPUT_VALIDATION_ERROR',
14+
}
15+
16+
// Upstream type declares networkError as Error, but it can contain additional properties, we override to add these
17+
export type ApolloError = BaseApolloError & {
18+
networkError: ApolloNetworkError | null;
19+
graphQLErrors: GraphQLError[];
20+
};
21+
22+
export interface ApolloNetworkError extends Error {
23+
statusCode?: number;
24+
response?: any;
25+
result?: {
26+
errors: GraphQLError[];
27+
};
28+
}
29+
30+
export type GraphQLError = Omit<BaseGraphQLError, 'message'> & {
31+
message: string | Record<string, any>;
32+
};
33+
export type ProcessedApolloError =
34+
| NetworkError
35+
| ServerError
36+
| UnauthorizedError
37+
| UserInputError
38+
| InputValidationError;
39+
40+
export interface NetworkError {
41+
type: ApolloErrorType.NETWORK_ERROR;
42+
error: Error;
43+
message: string;
44+
statusCode?: number;
45+
}
46+
47+
export interface ServerError {
48+
type: ApolloErrorType.SERVER_ERROR;
49+
error: Error;
50+
path?: readonly (string | number)[];
51+
message: string;
52+
}
53+
54+
export interface UnauthorizedError {
55+
type: ApolloErrorType.UNAUTHORIZED_ERROR;
56+
error: Error;
57+
path?: readonly (string | number)[];
58+
message: string;
59+
}
60+
61+
export interface UserInputError {
62+
type: ApolloErrorType.BAD_USER_INPUT;
63+
error: Error;
64+
path?: readonly (string | number)[];
65+
message: string;
66+
}
67+
68+
export interface InputValidationError {
69+
type: ApolloErrorType.INPUT_VALIDATION_ERROR;
70+
error: Error;
71+
path?: readonly (string | number)[];
72+
message: string;
73+
invalidArgs: string[];
74+
violations: ValidationRuleViolation[];
75+
}
76+
77+
export interface ValidationRuleViolation {
78+
path: string[];
79+
message: string;
80+
value?: any;
81+
}
82+
83+
export type ApolloOperationErrorHandlerFunction<
84+
TError = BaseApolloError,
85+
TApp extends Vue = Vue,
86+
TContext = ApolloOperationContext,
87+
TResult = ApolloErrorHandlerResultInterface,
88+
> = (error: TError, app: TApp, context?: TContext) => TResult;

0 commit comments

Comments
 (0)