Query dynamic cache key computation (#12814)

In this PR:
- add query hashKey to ObjectMetadataItems query graphql cache to avoid
caching outdated queries
- improve performance by removing ResolveField at FieldLevel and adding
this at resolver level
This commit is contained in:
Charles Bochet
2025-06-24 14:04:00 +02:00
committed by GitHub
parent 48347095d2
commit 4ac208cf1c
6 changed files with 44 additions and 57 deletions

View File

@ -145,14 +145,15 @@ export const SettingsObjectNewFieldConfigure = () => {
}); });
} }
navigate(SettingsPath.ObjectDetail, {
objectNamePlural,
});
// TODO: fix optimistic update logic // TODO: fix optimistic update logic
// Forcing a refetch for now but it's not ideal // Forcing a refetch for now but it's not ideal
await apolloClient.refetchQueries({ await apolloClient.refetchQueries({
include: ['FindManyViews', 'CombinedFindManyRecords'], include: ['FindManyViews', 'CombinedFindManyRecords'],
}); });
navigate(SettingsPath.ObjectDetail, {
objectNamePlural,
});
setIsSaving(false); setIsSaving(false);
} catch (error) { } catch (error) {
setIsSaving(false); setIsSaving(false);

View File

@ -1,3 +1,5 @@
import { createHash } from 'crypto';
import { isDefined } from 'class-validator'; import { isDefined } from 'class-validator';
import { Plugin } from 'graphql-yoga'; import { Plugin } from 'graphql-yoga';
@ -20,8 +22,11 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
const localeCacheKey = isDefined(serverContext.req.headers['x-locale']) const localeCacheKey = isDefined(serverContext.req.headers['x-locale'])
? `:${locale}` ? `:${locale}`
: ''; : '';
const queryHash = createHash('sha256')
.update(serverContext.req.body.query)
.digest('hex');
return `graphql:operations:${operationName}:${workspaceId}:${workspaceMetadataVersion}${localeCacheKey}`; return `graphql:operations:${operationName}:${workspaceId}:${workspaceMetadataVersion}${localeCacheKey}:${queryHash}`;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import DataLoader from 'dataloader'; import DataLoader from 'dataloader';
import { APP_LOCALES } from 'twenty-shared/translations';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
@ -37,6 +38,7 @@ export type RelationLoaderPayload = {
export type FieldMetadataLoaderPayload = { export type FieldMetadataLoaderPayload = {
workspaceId: string; workspaceId: string;
objectMetadata: Pick<ObjectMetadataInterface, 'id'>; objectMetadata: Pick<ObjectMetadataInterface, 'id'>;
locale?: keyof typeof APP_LOCALES;
}; };
export type IndexMetadataLoaderPayload = { export type IndexMetadataLoaderPayload = {
@ -140,11 +142,34 @@ export class DataloaderService {
Object.values(objectMetadataMaps.byId[id].fieldsById).map( Object.values(objectMetadataMaps.byId[id].fieldsById).map(
// TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface // TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface
(fieldMetadata) => { (fieldMetadata) => {
const overridesFieldToCompute = [
'icon',
'label',
'description',
] as const satisfies (keyof FieldMetadataInterface)[];
const overrides = overridesFieldToCompute.reduce<
Partial<
Record<(typeof overridesFieldToCompute)[number], string>
>
>(
(acc, field) => ({
...acc,
[field]: this.fieldMetadataService.resolveOverridableString(
fieldMetadata,
field,
dataLoaderParams[0].locale,
),
}),
{},
);
return { return {
...fieldMetadata, ...fieldMetadata,
createdAt: new Date(fieldMetadata.createdAt), createdAt: new Date(fieldMetadata.createdAt),
updatedAt: new Date(fieldMetadata.updatedAt), updatedAt: new Date(fieldMetadata.updatedAt),
workspaceId: workspaceId, workspaceId: workspaceId,
...overrides,
}; };
}, },
), ),

View File

@ -55,43 +55,6 @@ export class FieldMetadataResolver {
private readonly beforeUpdateOneField: BeforeUpdateOneField<UpdateFieldInput>, private readonly beforeUpdateOneField: BeforeUpdateOneField<UpdateFieldInput>,
) {} ) {}
@ResolveField(() => String, { nullable: true })
async label(
@Parent() fieldMetadata: FieldMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.fieldMetadataService.resolveOverridableString(
fieldMetadata,
'label',
context.req.headers['x-locale'],
);
}
@ResolveField(() => String, { nullable: true })
async description(
@Parent() fieldMetadata: FieldMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.fieldMetadataService.resolveOverridableString(
fieldMetadata,
'description',
context.req.headers['x-locale'],
);
}
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@ResolveField(() => String, { nullable: true })
async icon(
@Parent() fieldMetadata: FieldMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.fieldMetadataService.resolveOverridableString(
fieldMetadata,
'icon',
context.req.headers['x-locale'],
);
}
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL)) @UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
@Mutation(() => FieldMetadataDTO) @Mutation(() => FieldMetadataDTO)
async createOneField( async createOneField(

View File

@ -4,7 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { APP_LOCALES } from 'twenty-shared/translations';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { DataSource, FindOneOptions, In, Repository } from 'typeorm'; import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
@ -608,26 +608,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
return fieldMetadataInput; return fieldMetadataInput;
} }
async resolveOverridableString( resolveOverridableString(
fieldMetadata: FieldMetadataDTO, fieldMetadata: Pick<
FieldMetadataDTO,
'label' | 'description' | 'icon' | 'isCustom' | 'standardOverrides'
>,
labelKey: 'label' | 'description' | 'icon', labelKey: 'label' | 'description' | 'icon',
locale: keyof typeof APP_LOCALES | undefined, locale: keyof typeof APP_LOCALES | undefined,
): Promise<string> { ): string {
if (fieldMetadata.isCustom) { if (fieldMetadata.isCustom) {
return fieldMetadata[labelKey] ?? ''; return fieldMetadata[labelKey] ?? '';
} }
if (!locale || locale === SOURCE_LOCALE) {
if (
fieldMetadata.standardOverrides &&
isDefined(fieldMetadata.standardOverrides[labelKey])
) {
return fieldMetadata.standardOverrides[labelKey] as string;
}
return fieldMetadata[labelKey] ?? '';
}
const translationValue = const translationValue =
// @ts-expect-error legacy noImplicitAny // @ts-expect-error legacy noImplicitAny
fieldMetadata.standardOverrides?.translations?.[locale]?.[labelKey]; fieldMetadata.standardOverrides?.translations?.[locale]?.[labelKey];

View File

@ -134,13 +134,14 @@ export class ObjectMetadataResolver {
async fieldsList( async fieldsList(
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@Parent() objectMetadata: ObjectMetadataDTO, @Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: { loaders: IDataloaders }, @Context() context: { loaders: IDataloaders } & I18nContext,
): Promise<FieldMetadataDTO[]> { ): Promise<FieldMetadataDTO[]> {
try { try {
const fieldMetadataItems = await context.loaders.fieldMetadataLoader.load( const fieldMetadataItems = await context.loaders.fieldMetadataLoader.load(
{ {
objectMetadata, objectMetadata,
workspaceId: workspace.id, workspaceId: workspace.id,
locale: context.req.headers['x-locale'],
}, },
); );