App version mismatch handling between frontend and backend (#13368)
https://github.com/user-attachments/assets/d153f177-4d70-4ec6-8693-15413e550938
This commit is contained in:
@ -4,6 +4,7 @@ import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
|
|||||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||||
import { useApolloFactory } from '../useApolloFactory';
|
import { useApolloFactory } from '../useApolloFactory';
|
||||||
|
|
||||||
enableFetchMocks();
|
enableFetchMocks();
|
||||||
@ -25,7 +26,9 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|||||||
initialEntries={['/welcome', '/verify', '/opportunities']}
|
initialEntries={['/welcome', '/verify', '/opportunities']}
|
||||||
initialIndex={2}
|
initialIndex={2}
|
||||||
>
|
>
|
||||||
{children}
|
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="test-instance-id">
|
||||||
|
{children}
|
||||||
|
</SnackBarComponentInstanceContextProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
@ -13,7 +13,9 @@ import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
|||||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||||
|
import { appVersionState } from '@/client-config/states/appVersionState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { ApolloFactory, Options } from '../services/apollo.factory';
|
import { ApolloFactory, Options } from '../services/apollo.factory';
|
||||||
|
|
||||||
@ -26,6 +28,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||||
currentWorkspaceState,
|
currentWorkspaceState,
|
||||||
);
|
);
|
||||||
|
const appVersion = useRecoilValue(appVersionState);
|
||||||
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
);
|
);
|
||||||
@ -35,6 +38,8 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
const setPreviousUrl = useSetRecoilState(previousUrlState);
|
const setPreviousUrl = useSetRecoilState(previousUrlState);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const { enqueueErrorSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const apolloClient = useMemo(() => {
|
const apolloClient = useMemo(() => {
|
||||||
apolloRef.current = new ApolloFactory({
|
apolloRef.current = new ApolloFactory({
|
||||||
uri: `${REACT_APP_SERVER_BASE_URL}/graphql`,
|
uri: `${REACT_APP_SERVER_BASE_URL}/graphql`,
|
||||||
@ -54,6 +59,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
connectToDevTools: process.env.IS_DEBUG_MODE === 'true',
|
connectToDevTools: process.env.IS_DEBUG_MODE === 'true',
|
||||||
currentWorkspaceMember: currentWorkspaceMember,
|
currentWorkspaceMember: currentWorkspaceMember,
|
||||||
currentWorkspace: currentWorkspace,
|
currentWorkspace: currentWorkspace,
|
||||||
|
appVersion,
|
||||||
onTokenPairChange: (tokenPair) => {
|
onTokenPairChange: (tokenPair) => {
|
||||||
setTokenPair(tokenPair);
|
setTokenPair(tokenPair);
|
||||||
},
|
},
|
||||||
@ -73,6 +79,14 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
navigate(AppPath.SignInUp);
|
navigate(AppPath.SignInUp);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onAppVersionMismatch: (message) => {
|
||||||
|
enqueueErrorSnackBar({
|
||||||
|
message,
|
||||||
|
options: {
|
||||||
|
dedupeKey: 'app-version-mismatch',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
extraLinks: [],
|
extraLinks: [],
|
||||||
isDebugMode: process.env.IS_DEBUG_MODE === 'true',
|
isDebugMode: process.env.IS_DEBUG_MODE === 'true',
|
||||||
// Override options
|
// Override options
|
||||||
@ -87,6 +101,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
setCurrentWorkspaceMember,
|
setCurrentWorkspaceMember,
|
||||||
setCurrentWorkspace,
|
setCurrentWorkspace,
|
||||||
setPreviousUrl,
|
setPreviousUrl,
|
||||||
|
enqueueErrorSnackBar,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useUpdateEffect(() => {
|
useUpdateEffect(() => {
|
||||||
@ -101,5 +116,11 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
}
|
}
|
||||||
}, [currentWorkspace]);
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
|
useUpdateEffect(() => {
|
||||||
|
if (isDefined(apolloRef.current)) {
|
||||||
|
apolloRef.current.updateAppVersion(appVersion);
|
||||||
|
}
|
||||||
|
}, [appVersion]);
|
||||||
|
|
||||||
return apolloClient;
|
return apolloClient;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -64,6 +64,7 @@ const createMockOptions = (): Options<any> => ({
|
|||||||
isDebugMode: true,
|
isDebugMode: true,
|
||||||
onError: mockOnError,
|
onError: mockOnError,
|
||||||
onNetworkError: mockOnNetworkError,
|
onNetworkError: mockOnNetworkError,
|
||||||
|
appVersion: '1.0.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeRequest = async () => {
|
const makeRequest = async () => {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { logDebug } from '~/utils/logDebug';
|
|||||||
|
|
||||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import {
|
import {
|
||||||
DefinitionNode,
|
DefinitionNode,
|
||||||
DirectiveNode,
|
DirectiveNode,
|
||||||
@ -45,16 +46,19 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
|||||||
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
||||||
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
|
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
|
||||||
onUnauthenticatedError?: () => void;
|
onUnauthenticatedError?: () => void;
|
||||||
|
onAppVersionMismatch?: (message: string) => void;
|
||||||
currentWorkspaceMember: CurrentWorkspaceMember | null;
|
currentWorkspaceMember: CurrentWorkspaceMember | null;
|
||||||
currentWorkspace: CurrentWorkspace | null;
|
currentWorkspace: CurrentWorkspace | null;
|
||||||
extraLinks?: ApolloLink[];
|
extraLinks?: ApolloLink[];
|
||||||
isDebugMode?: boolean;
|
isDebugMode?: boolean;
|
||||||
|
appVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||||
private client: ApolloClient<TCacheShape>;
|
private client: ApolloClient<TCacheShape>;
|
||||||
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
|
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
|
||||||
private currentWorkspace: CurrentWorkspace | null = null;
|
private currentWorkspace: CurrentWorkspace | null = null;
|
||||||
|
private appVersion?: string;
|
||||||
|
|
||||||
constructor(opts: Options<TCacheShape>) {
|
constructor(opts: Options<TCacheShape>) {
|
||||||
const {
|
const {
|
||||||
@ -63,15 +67,18 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
onNetworkError,
|
onNetworkError,
|
||||||
onTokenPairChange,
|
onTokenPairChange,
|
||||||
onUnauthenticatedError,
|
onUnauthenticatedError,
|
||||||
|
onAppVersionMismatch,
|
||||||
currentWorkspaceMember,
|
currentWorkspaceMember,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
extraLinks,
|
extraLinks,
|
||||||
isDebugMode,
|
isDebugMode,
|
||||||
|
appVersion,
|
||||||
...options
|
...options
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
this.currentWorkspaceMember = currentWorkspaceMember;
|
this.currentWorkspaceMember = currentWorkspaceMember;
|
||||||
this.currentWorkspace = currentWorkspace;
|
this.currentWorkspace = currentWorkspace;
|
||||||
|
this.appVersion = appVersion;
|
||||||
|
|
||||||
const buildApolloLink = (): ApolloLink => {
|
const buildApolloLink = (): ApolloLink => {
|
||||||
const uploadLink = createUploadLink({
|
const uploadLink = createUploadLink({
|
||||||
@ -111,6 +118,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
...(this.currentWorkspace?.metadataVersion && {
|
...(this.currentWorkspace?.metadataVersion && {
|
||||||
'X-Schema-Version': `${this.currentWorkspace.metadataVersion}`,
|
'X-Schema-Version': `${this.currentWorkspace.metadataVersion}`,
|
||||||
}),
|
}),
|
||||||
|
...(this.appVersion && { 'X-App-Version': this.appVersion }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -158,6 +166,13 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (graphQLError?.extensions?.code) {
|
switch (graphQLError?.extensions?.code) {
|
||||||
|
case 'APP_VERSION_MISMATCH': {
|
||||||
|
onAppVersionMismatch?.(
|
||||||
|
(graphQLError.extensions?.userFriendlyMessage as string) ||
|
||||||
|
t`Your app version is out of date. Please refresh the page.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'UNAUTHENTICATED': {
|
case 'UNAUTHENTICATED': {
|
||||||
return handleTokenRenewal(operation, forward);
|
return handleTokenRenewal(operation, forward);
|
||||||
}
|
}
|
||||||
@ -289,6 +304,10 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
this.currentWorkspace = workspace;
|
this.currentWorkspace = workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAppVersion(appVersion?: string) {
|
||||||
|
this.appVersion = appVersion;
|
||||||
|
}
|
||||||
|
|
||||||
getClient() {
|
getClient() {
|
||||||
return this.client;
|
return this.client;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||||
|
import { appVersionState } from '@/client-config/states/appVersionState';
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
||||||
@ -97,6 +98,8 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
isImapSmtpCaldavEnabledState,
|
isImapSmtpCaldavEnabledState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setAppVersion = useSetRecoilState(appVersionState);
|
||||||
|
|
||||||
const { data, loading, error, fetchClientConfig } = useClientConfig();
|
const { data, loading, error, fetchClientConfig } = useClientConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -133,7 +136,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
isErrored: false,
|
isErrored: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
}));
|
}));
|
||||||
|
setAppVersion(data.clientConfig.appVersion);
|
||||||
setAuthProviders({
|
setAuthProviders({
|
||||||
google: data?.clientConfig.authProviders.google,
|
google: data?.clientConfig.authProviders.google,
|
||||||
microsoft: data?.clientConfig.authProviders.microsoft,
|
microsoft: data?.clientConfig.authProviders.microsoft,
|
||||||
@ -217,6 +220,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setIsConfigVariablesInDbEnabled,
|
setIsConfigVariablesInDbEnabled,
|
||||||
setCalendarBookingPageId,
|
setCalendarBookingPageId,
|
||||||
setIsImapSmtpCaldavEnabled,
|
setIsImapSmtpCaldavEnabled,
|
||||||
|
setAppVersion,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
|
export const appVersionState = createState<string | undefined>({
|
||||||
|
key: 'appVersion',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
|
appVersion?: string;
|
||||||
aiModels: Array<ClientAiModelConfig>;
|
aiModels: Array<ClientAiModelConfig>;
|
||||||
analyticsEnabled: boolean;
|
analyticsEnabled: boolean;
|
||||||
api: ApiConfig;
|
api: ApiConfig;
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export class GraphQLConfigService
|
|||||||
useGraphQLErrorHandlerHook({
|
useGraphQLErrorHandlerHook({
|
||||||
metricsService: this.metricsService,
|
metricsService: this.metricsService,
|
||||||
exceptionHandlerService: this.exceptionHandlerService,
|
exceptionHandlerService: this.exceptionHandlerService,
|
||||||
|
twentyConfigService: this.twentyConfigService,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export const metadataModuleFactory = async (
|
|||||||
useGraphQLErrorHandlerHook({
|
useGraphQLErrorHandlerHook({
|
||||||
metricsService: metricsService,
|
metricsService: metricsService,
|
||||||
exceptionHandlerService,
|
exceptionHandlerService,
|
||||||
|
twentyConfigService,
|
||||||
}),
|
}),
|
||||||
useCachedMetadata({
|
useCachedMetadata({
|
||||||
cacheGetter: cacheStorageService.get.bind(cacheStorageService),
|
cacheGetter: cacheStorageService.get.bind(cacheStorageService),
|
||||||
|
|||||||
@ -108,6 +108,9 @@ class PublicFeatureFlag {
|
|||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ClientConfig {
|
export class ClientConfig {
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
appVersion?: string;
|
||||||
|
|
||||||
@Field(() => AuthProviders, { nullable: false })
|
@Field(() => AuthProviders, { nullable: false })
|
||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
|
|
||||||
|
|||||||
@ -100,6 +100,7 @@ describe('ClientConfigService', () => {
|
|||||||
const result = await service.getClientConfig();
|
const result = await service.getClientConfig();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
appVersion: '1.0.0',
|
||||||
billing: {
|
billing: {
|
||||||
isBillingEnabled: true,
|
isBillingEnabled: true,
|
||||||
billingUrl: 'https://billing.example.com',
|
billingUrl: 'https://billing.example.com',
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export class ClientConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clientConfig: ClientConfig = {
|
const clientConfig: ClientConfig = {
|
||||||
|
appVersion: this.twentyConfigService.get('APP_VERSION'),
|
||||||
billing: {
|
billing: {
|
||||||
isBillingEnabled: this.twentyConfigService.get('IS_BILLING_ENABLED'),
|
isBillingEnabled: this.twentyConfigService.get('IS_BILLING_ENABLED'),
|
||||||
billingUrl: this.twentyConfigService.get('BILLING_PLAN_REQUIRED_LINK'),
|
billingUrl: this.twentyConfigService.get('BILLING_PLAN_REQUIRED_LINK'),
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
} from '@envelop/core';
|
} from '@envelop/core';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
||||||
|
import semver from 'semver';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ import {
|
|||||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import {
|
import {
|
||||||
graphQLErrorCodesToFilter,
|
graphQLErrorCodesToFilter,
|
||||||
shouldCaptureException,
|
shouldCaptureException,
|
||||||
@ -26,6 +29,9 @@ import {
|
|||||||
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
||||||
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
||||||
const SCHEMA_MISMATCH_ERROR = 'Schema version mismatch.';
|
const SCHEMA_MISMATCH_ERROR = 'Schema version mismatch.';
|
||||||
|
const APP_VERSION_HEADER = 'x-app-version';
|
||||||
|
const APP_VERSION_MISMATCH_ERROR = 'App version mismatch.';
|
||||||
|
const APP_VERSION_MISMATCH_CODE = 'APP_VERSION_MISMATCH';
|
||||||
|
|
||||||
type GraphQLErrorHandlerHookOptions = {
|
type GraphQLErrorHandlerHookOptions = {
|
||||||
metricsService: MetricsService;
|
metricsService: MetricsService;
|
||||||
@ -34,6 +40,8 @@ type GraphQLErrorHandlerHookOptions = {
|
|||||||
* The exception handler service to use.
|
* The exception handler service to use.
|
||||||
*/
|
*/
|
||||||
exceptionHandlerService: ExceptionHandlerService;
|
exceptionHandlerService: ExceptionHandlerService;
|
||||||
|
|
||||||
|
twentyConfigService: TwentyConfigService;
|
||||||
/**
|
/**
|
||||||
* The key of the event id in the error's extension. `null` to disable.
|
* The key of the event id in the error's extension. `null` to disable.
|
||||||
* @default exceptionEventId
|
* @default exceptionEventId
|
||||||
@ -230,17 +238,55 @@ export const useGraphQLErrorHandlerHook = <
|
|||||||
const headers = context.req.headers;
|
const headers = context.req.headers;
|
||||||
const currentMetadataVersion = context.req.workspaceMetadataVersion;
|
const currentMetadataVersion = context.req.workspaceMetadataVersion;
|
||||||
const requestMetadataVersion = headers[SCHEMA_VERSION_HEADER];
|
const requestMetadataVersion = headers[SCHEMA_VERSION_HEADER];
|
||||||
|
const backendAppVersion =
|
||||||
|
options.twentyConfigService.get('APP_VERSION');
|
||||||
|
const appVersionHeaderValue = headers[APP_VERSION_HEADER];
|
||||||
|
const frontEndAppVersion =
|
||||||
|
appVersionHeaderValue && Array.isArray(appVersionHeaderValue)
|
||||||
|
? appVersionHeaderValue[0]
|
||||||
|
: appVersionHeaderValue;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
requestMetadataVersion &&
|
requestMetadataVersion &&
|
||||||
requestMetadataVersion !== `${currentMetadataVersion}`
|
requestMetadataVersion !== `${currentMetadataVersion}`
|
||||||
) {
|
) {
|
||||||
|
options.metricsService.incrementCounter({
|
||||||
|
key: MetricsKeys.SchemaVersionMismatch,
|
||||||
|
});
|
||||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
|
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
|
||||||
extensions: {
|
extensions: {
|
||||||
userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`,
|
userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!frontEndAppVersion ||
|
||||||
|
!backendAppVersion ||
|
||||||
|
!semver.valid(frontEndAppVersion) ||
|
||||||
|
!semver.valid(backendAppVersion)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontEndMajor = semver.parse(frontEndAppVersion)?.major;
|
||||||
|
const backendMajor = semver.parse(backendAppVersion)?.major;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(frontEndMajor) &&
|
||||||
|
isDefined(backendMajor) &&
|
||||||
|
frontEndMajor < backendMajor
|
||||||
|
) {
|
||||||
|
options.metricsService.incrementCounter({
|
||||||
|
key: MetricsKeys.AppVersionMismatch,
|
||||||
|
});
|
||||||
|
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
|
||||||
|
extensions: {
|
||||||
|
code: APP_VERSION_MISMATCH_CODE,
|
||||||
|
userFriendlyMessage: t`Your app version is out of date. Please refresh the page to continue.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,4 +23,6 @@ export enum MetricsKeys {
|
|||||||
WorkflowRunFailedToEnqueue = 'workflow-run/failed/to-enqueue',
|
WorkflowRunFailedToEnqueue = 'workflow-run/failed/to-enqueue',
|
||||||
AIToolExecutionFailed = 'ai-tool-execution/failed',
|
AIToolExecutionFailed = 'ai-tool-execution/failed',
|
||||||
AIToolExecutionSucceeded = 'ai-tool-execution/succeeded',
|
AIToolExecutionSucceeded = 'ai-tool-execution/succeeded',
|
||||||
|
SchemaVersionMismatch = 'schema-version/mismatch',
|
||||||
|
AppVersionMismatch = 'app-version/mismatch',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user