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:
Félix Malfait
2026-03-13 14:59:46 +01:00
committed by GitHub
parent 172bbd01bc
commit b470cb21a1
386 changed files with 3482 additions and 13593 deletions
@@ -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) {