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:
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user