Upgrade Apollo Client to v4 and refactor error handling (#18584)
## Summary This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error handling patterns across the codebase to use a new centralized `useSnackBarOnQueryError` hook. ## Key Changes - **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to `^3.11.0` in root package.json - **New Hook**: Added `useSnackBarOnQueryError` hook for centralized Apollo query error handling with snack bar notifications - **Error Handling Refactor**: Updated 100+ files to use the new error handling pattern: - Removed direct `ApolloError` imports where no longer needed - Replaced manual error handling logic with `useSnackBarOnQueryError` hook - Simplified error handling in hooks and components across multiple modules - **GraphQL Codegen**: Updated codegen configuration files to work with Apollo Client v3.11.0 - **Type Definitions**: Added TypeScript declaration file for `apollo-upload-client` module - **Test Updates**: Updated test files to reflect new error handling patterns ## Notable Implementation Details - The new `useSnackBarOnQueryError` hook provides a consistent way to handle Apollo query errors with automatic snack bar notifications - Changes span across multiple feature areas: auth, object records, settings, workflows, billing, and more - All changes maintain backward compatibility while improving code maintainability and reducing duplication - Jest configuration updated to work with the new Apollo Client version https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ describe('StreamingRestLink', () => {
|
||||
`,
|
||||
variables: {},
|
||||
getContext: () => ({}),
|
||||
} as Operation;
|
||||
} as unknown as Operation;
|
||||
|
||||
const result = streamingLink.request(operation, mockForward);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('StreamingRestLink', () => {
|
||||
operationName: 'StreamTest',
|
||||
extensions: {},
|
||||
setContext: jest.fn(),
|
||||
} as Operation;
|
||||
} as unknown as Operation;
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
@@ -95,7 +95,7 @@ describe('StreamingRestLink', () => {
|
||||
`,
|
||||
variables: {},
|
||||
getContext: () => ({}),
|
||||
} as Operation;
|
||||
} as unknown as Operation;
|
||||
|
||||
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('StreamingRestLink', () => {
|
||||
`,
|
||||
variables: {},
|
||||
getContext: () => ({}),
|
||||
} as Operation;
|
||||
} as unknown as Operation;
|
||||
|
||||
const mockResponse = { ok: false, status: 404 };
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
@@ -160,7 +160,7 @@ describe('StreamingRestLink', () => {
|
||||
`,
|
||||
variables: {},
|
||||
getContext: () => ({}),
|
||||
} as Operation;
|
||||
} as unknown as Operation;
|
||||
|
||||
const directive = (streamingLink as any).extractStreamDirective(
|
||||
operation,
|
||||
@@ -183,7 +183,7 @@ describe('StreamingRestLink', () => {
|
||||
`,
|
||||
variables: {},
|
||||
getContext: () => ({}),
|
||||
} as Operation;
|
||||
} as unknown as Operation;
|
||||
|
||||
const directive = (streamingLink as any).extractStreamDirective(
|
||||
operation,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApolloLink } from '@apollo/client';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
export const createCaptchaRefreshLink = (
|
||||
requestFreshCaptchaToken: () => void,
|
||||
@@ -8,12 +9,14 @@ export const createCaptchaRefreshLink = (
|
||||
|
||||
const hasCaptchaToken = variables != null && 'captchaToken' in variables;
|
||||
|
||||
return forward(operation).map((response) => {
|
||||
if (hasCaptchaToken) {
|
||||
requestFreshCaptchaToken();
|
||||
}
|
||||
return forward(operation).pipe(
|
||||
map((response) => {
|
||||
if (hasCaptchaToken) {
|
||||
requestFreshCaptchaToken();
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
return response;
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApolloLink, gql, type Operation } from '@apollo/client';
|
||||
import { map } from 'rxjs';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
@@ -11,6 +12,10 @@ const getGroup = (collapsed: boolean) =>
|
||||
: console.group.bind(console);
|
||||
|
||||
const parseQuery = (queryString: string) => {
|
||||
if (!queryString.trim()) {
|
||||
return ['Generic', ''];
|
||||
}
|
||||
|
||||
const queryObj = gql`
|
||||
${queryString}
|
||||
`;
|
||||
@@ -51,57 +56,59 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
|
||||
return forward(operation);
|
||||
}
|
||||
|
||||
return forward(operation).map((result) => {
|
||||
const time = Date.now() - operation.getContext().start;
|
||||
const errors = result.errors ?? result.data?.[queryName]?.errors;
|
||||
const hasError = Boolean(errors);
|
||||
return forward(operation).pipe(
|
||||
map((result) => {
|
||||
const time = Date.now() - operation.getContext().start;
|
||||
const errors = result.errors ?? result.data?.[queryName]?.errors;
|
||||
const hasError = Boolean(errors);
|
||||
|
||||
try {
|
||||
const titleArgs = formatTitle(
|
||||
operationType,
|
||||
schemaName,
|
||||
queryName,
|
||||
time,
|
||||
);
|
||||
try {
|
||||
const titleArgs = formatTitle(
|
||||
operationType,
|
||||
schemaName,
|
||||
queryName,
|
||||
time,
|
||||
);
|
||||
|
||||
getGroup(!hasError)(...titleArgs);
|
||||
getGroup(!hasError)(...titleArgs);
|
||||
|
||||
if (isDefined(errors)) {
|
||||
errors.forEach((err: any) => {
|
||||
logDebug(
|
||||
`%c${err.message}`,
|
||||
// oxlint-disable-next-line twenty/no-hardcoded-colors
|
||||
'color: #F51818; font-weight: lighter',
|
||||
);
|
||||
});
|
||||
if (isDefined(errors)) {
|
||||
errors.forEach((err: any) => {
|
||||
logDebug(
|
||||
`%c${err.message}`,
|
||||
// oxlint-disable-next-line twenty/no-hardcoded-colors
|
||||
'color: #F51818; font-weight: lighter',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
logDebug('HEADERS: ', headers);
|
||||
|
||||
if (Object.keys(variables).length !== 0) {
|
||||
logDebug('VARIABLES', variables);
|
||||
}
|
||||
|
||||
logDebug('QUERY', query);
|
||||
|
||||
if (isDefined(result.data)) {
|
||||
logDebug('RESULT', result.data);
|
||||
}
|
||||
if (isDefined(errors)) {
|
||||
logDebug('ERRORS', errors);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
} catch {
|
||||
// this may happen if console group is not supported
|
||||
logDebug(
|
||||
`${operationType} ${schemaName}::${queryName} (in ${time} ms)`,
|
||||
);
|
||||
if (isDefined(errors)) {
|
||||
logError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
logDebug('HEADERS: ', headers);
|
||||
|
||||
if (Object.keys(variables).length !== 0) {
|
||||
logDebug('VARIABLES', variables);
|
||||
}
|
||||
|
||||
logDebug('QUERY', query);
|
||||
|
||||
if (isDefined(result.data)) {
|
||||
logDebug('RESULT', result.data);
|
||||
}
|
||||
if (isDefined(errors)) {
|
||||
logDebug('ERRORS', errors);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
} catch {
|
||||
// this may happen if console group is not supported
|
||||
logDebug(
|
||||
`${operationType} ${schemaName}::${queryName} (in ${time} ms)`,
|
||||
);
|
||||
if (isDefined(errors)) {
|
||||
logError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
ApolloLink,
|
||||
Observable,
|
||||
type Operation,
|
||||
type ServerError,
|
||||
} from '@apollo/client/core';
|
||||
import { type FetchResult } from '@apollo/client/link/core';
|
||||
type FetchResult,
|
||||
} from '@apollo/client';
|
||||
import { ServerError } from '@apollo/client/errors';
|
||||
import { type ArgumentNode, type DirectiveNode } from 'graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -62,13 +62,10 @@ export class StreamingRestLink extends ApolloLink {
|
||||
fetch(url, requestConfig)
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const networkError = new Error(
|
||||
`HTTP error! status: ${response.status}`,
|
||||
) as ServerError;
|
||||
|
||||
networkError.statusCode = response.status;
|
||||
|
||||
throw networkError;
|
||||
throw new ServerError(`HTTP error! status: ${response.status}`, {
|
||||
response,
|
||||
bodyText: '',
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
|
||||
Reference in New Issue
Block a user