Improve performance on metadata computation (#12785)

In this PR:

## Improve recompute metadata cache performance. We are aiming for
~100ms

Deleting relationMetadata table and FKs pointing on it
Fetching indexMetadata and indexFieldMetadata in a separate query as
typeorm is suboptimizing

## Remove caching lock

As recomputing the metadata cache is lighter, we try to stop preventing
multiple concurrent computations. This also simplifies interfaces

## Introduce self recovery mecanisms to recompute cache automatically if
corrupted

Aka getFreshObjectMetadataMaps

## custom object resolver performance improvement:  1sec to 200ms

Double check queries and indexes used while creating a custom object
Remove the queries to db to use the cached objectMetadataMap

## reduce objectMetadataMaps to 500kb
<img width="222" alt="image"
src="https://github.com/user-attachments/assets/2370dc80-49b6-4b63-8d5e-30c5ebdaa062"
/>

We used to stored 3 fieldMetadataMaps (byId, byName, byJoinColumnName).
While this is great for devXP, this is not great for performances.
Using the same mecanisme as for objectMetadataMap: we only keep byIdMap
and introduce two otherMaps to idByName, idByJoinColumnName to make the
bridge

## Add dataloader on IndexMetadata (aka indexMetadataList in the API)

## Improve field resolver performances too

## Deprecate ClientConfig
This commit is contained in:
Charles Bochet
2025-06-23 21:06:17 +02:00
committed by GitHub
parent 6aee42ab22
commit d5c974054d
145 changed files with 1485 additions and 2245 deletions

View File

@ -1,75 +0,0 @@
import { gql } from '@apollo/client';
export const GET_CLIENT_CONFIG = gql`
query GetClientConfig {
clientConfig {
aiModels {
modelId
label
provider
inputCostPer1kTokensInCredits
outputCostPer1kTokensInCredits
}
billing {
isBillingEnabled
billingUrl
trialPeriods {
duration
isCreditCardRequired
}
}
authProviders {
google
password
microsoft
sso {
id
name
type
status
issuer
}
}
signInPrefilled
isMultiWorkspaceEnabled
isEmailVerificationRequired
defaultSubdomain
frontDomain
debugMode
analyticsEnabled
isAttachmentPreviewEnabled
support {
supportDriver
supportFrontChatId
}
sentry {
dsn
environment
release
}
captcha {
provider
siteKey
}
api {
mutationMaximumAffectedRecords
}
chromeExtensionId
canManageFeatureFlags
publicFeatureFlags {
key
metadata {
label
description
imagePath
}
}
isMicrosoftMessagingEnabled
isMicrosoftCalendarEnabled
isGoogleMessagingEnabled
isGoogleCalendarEnabled
isConfigVariablesInDbEnabled
calendarBookingPageId
}
}
`;

View File

@ -1,6 +1,6 @@
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { ClientConfig } from '~/generated/graphql';
import { clientConfigApiStatusState } from '../states/clientConfigApiStatusState';
import { getClientConfig } from '../utils/getClientConfig';

View File

@ -1,5 +1,5 @@
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { createState } from 'twenty-ui/utilities';
import { ClientConfig } from '~/generated/graphql';
type ClientConfigApiStatus = {
isLoadedOnce: boolean;

View File

@ -0,0 +1,37 @@
import {
ApiConfig,
AuthProviders,
Billing,
Captcha,
ClientAiModelConfig,
PublicFeatureFlag,
Sentry,
Support,
} from '~/generated-metadata/graphql';
export type ClientConfig = {
aiModels: Array<ClientAiModelConfig>;
analyticsEnabled: boolean;
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
calendarBookingPageId?: string;
canManageFeatureFlags: boolean;
captcha: Captcha;
chromeExtensionId?: string;
debugMode: boolean;
defaultSubdomain?: string;
frontDomain: string;
isAttachmentPreviewEnabled: boolean;
isConfigVariablesInDbEnabled: boolean;
isEmailVerificationRequired: boolean;
isGoogleCalendarEnabled: boolean;
isGoogleMessagingEnabled: boolean;
isMicrosoftCalendarEnabled: boolean;
isMicrosoftMessagingEnabled: boolean;
isMultiWorkspaceEnabled: boolean;
publicFeatureFlags: Array<PublicFeatureFlag>;
sentry: Sentry;
signInPrefilled: boolean;
support: Support;
};

View File

@ -1,5 +1,5 @@
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { ClientConfig } from '~/generated/graphql';
export const getClientConfig = async (): Promise<ClientConfig> => {
const response = await fetch(`${REACT_APP_SERVER_BASE_URL}/client-config`, {

View File

@ -1,4 +1,4 @@
import { ClientConfig } from '~/generated/graphql';
import { ClientConfig } from '@/client-config/types/ClientConfig';
import { createState } from 'twenty-ui/utilities';
export const domainConfigurationState = createState<

View File

@ -25,29 +25,14 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isLabelSyncedWithName
isSearchable
duplicateCriteria
indexMetadatas(paging: { first: 100 }) {
edges {
node {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
indexFieldMetadatas(paging: { first: 100 }) {
edges {
node {
id
createdAt
updatedAt
order
fieldMetadataId
}
}
}
}
}
indexMetadataList {
id
createdAt
updatedAt
name
indexWhereClause
indexType
isUnique
}
fieldsList {
id

View File

@ -11,6 +11,7 @@ export type ObjectMetadataItem = Omit<
| 'indexMetadatas'
| 'labelIdentifierFieldMetadataId'
| 'fieldsList'
| 'indexMetadataList'
> & {
__typename?: string;
fields: FieldMetadataItem[];

View File

@ -1,3 +1,4 @@
import { IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
@ -14,19 +15,21 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
object.node.labelIdentifierFieldMetadataId,
);
const { fieldsList, ...objectWithoutFieldsList } = object.node;
const { fieldsList, indexMetadataList, ...objectWithoutFieldsList } =
object.node;
return {
...objectWithoutFieldsList,
fields: fieldsList,
labelIdentifierFieldMetadataId,
indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({
...index.node,
indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map(
(indexField) => indexField.node,
),
})),
};
indexMetadatas: indexMetadataList.map(
(index) =>
({
...index,
indexFieldMetadatas: [],
}) satisfies IndexMetadataItem,
),
} satisfies ObjectMetadataItem;
}) ?? [];
return formattedObjects;