Rework locale computation on BE (#13247)

Context:

Users are complaining to see their workspace in a language they don't
know. This behavior is transient, happens on data model update and
disappear on refresh
I've check the cache for users that got the issue and did not spot any
weird language
==> I think we somehow fallback the the request header locale. I feel we
should always use the userWorkspace.locale, request locale should not be
used in BE in my opinion except for unauthenticated endpoints. I'm also
adding logs to understand the locale issue
In this PR:

rename user.workspaces into user.userWorkspaces which is more correct
improve / simplify LOCALES typing
This commit is contained in:
Charles Bochet
2025-07-16 18:51:46 +02:00
committed by GitHub
parent 7fde4944d8
commit b25f50e288
20 changed files with 119 additions and 78 deletions

View File

@ -1,7 +1,10 @@
import { createHash } from 'crypto';
import { isNonEmptyString } from '@sniptt/guards';
import { Request } from 'express';
import { Plugin } from 'graphql-yoga';
import { isDefined } from 'twenty-shared/utils';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export type CacheMetadataPluginConfig = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -12,19 +15,26 @@ export type CacheMetadataPluginConfig = {
};
export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const computeCacheKey = (serverContext: any) => {
const workspaceId = serverContext.req.workspace?.id ?? 'anonymous';
const workspaceMetadataVersion =
serverContext.req.workspaceMetadataVersion ?? '0';
const operationName = getOperationName(serverContext);
const locale = serverContext.req.locale;
const localeCacheKey = isNonEmptyString(locale) ? `:${locale}` : '';
const computeCacheKey = ({
operationName,
request,
}: {
operationName: string;
request: Pick<Request, 'workspace' | 'locale' | 'body'>;
}) => {
const workspace = request.workspace;
if (!isDefined(workspace)) {
throw new InternalServerError('Workspace is not defined');
}
const workspaceMetadataVersion = workspace.metadataVersion ?? '0';
const locale = request.locale;
const queryHash = createHash('sha256')
.update(serverContext.req.body.query)
.update(request.body.query)
.digest('hex');
return `graphql:operations:${operationName}:${workspaceId}:${workspaceMetadataVersion}${localeCacheKey}:${queryHash}`;
return `graphql:operations:${operationName}:${workspace.id}:${workspaceMetadataVersion}:${locale}:${queryHash}`;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -37,7 +47,11 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
return;
}
const cacheKey = computeCacheKey(serverContext);
const cacheKey = computeCacheKey({
operationName: getOperationName(serverContext),
// TODO: we should probably override the graphql-yoga request type to include the workspace and locale
request: (serverContext as unknown as { req: Request }).req,
});
const cachedResponse = await config.cacheGetter(cacheKey);
if (cachedResponse) {
@ -51,7 +65,10 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
return;
}
const cacheKey = computeCacheKey(serverContext);
const cacheKey = computeCacheKey({
operationName: getOperationName(serverContext),
request: (serverContext as unknown as { req: Request }).req,
});
const cachedResponse = await config.cacheGetter(cacheKey);