Files
twenty_crm/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
Charles Bochet d5c974054d 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
2025-06-23 21:06:17 +02:00

149 lines
4.1 KiB
TypeScript

import { FieldMetadataType } from 'twenty-shared/types';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@Entity('fieldMetadata')
@Unique('IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE', [
'name',
'objectMetadataId',
'workspaceId',
])
@Index('IDX_FIELD_METADATA_RELATION_TARGET_FIELD_METADATA_ID', [
'relationTargetFieldMetadataId',
])
@Index('IDX_FIELD_METADATA_RELATION_TARGET_OBJECT_METADATA_ID', [
'relationTargetObjectMetadataId',
])
@Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID_WORKSPACE_ID', [
'objectMetadataId',
'workspaceId',
])
export class FieldMetadataEntity<
T extends FieldMetadataType = FieldMetadataType,
> {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'uuid' })
standardId: string | null;
@Column({ nullable: false, type: 'uuid' })
objectMetadataId: string;
@ManyToOne(() => ObjectMetadataEntity, (object) => object.fields, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'objectMetadataId' })
@Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID', ['objectMetadataId'])
object: Relation<ObjectMetadataEntity>;
@Column({
nullable: false,
type: 'varchar',
})
type: T;
@Column({ nullable: false })
name: string;
@Column({ nullable: false })
label: string;
@Column({ nullable: true, type: 'jsonb' })
defaultValue: FieldMetadataDefaultValue<T>;
@Column({ nullable: true, type: 'text' })
description: string;
@Column({ nullable: true })
icon: string;
@Column({ type: 'jsonb', nullable: true })
standardOverrides?: FieldStandardOverridesDTO;
@Column('jsonb', { nullable: true })
options: FieldMetadataOptions<T>;
@Column('jsonb', { nullable: true })
settings?: FieldMetadataSettings<T>;
@Column({ default: false })
isCustom: boolean;
@Column({ default: false })
isActive: boolean;
@Column({ default: false })
isSystem: boolean;
@Column({ nullable: true, default: true })
isNullable: boolean;
@Column({ nullable: true, default: false })
isUnique: boolean;
@Column({ nullable: false, type: 'uuid' })
@Index('IDX_FIELD_METADATA_WORKSPACE_ID', ['workspaceId'])
workspaceId: string;
@Column({ default: false })
isLabelSyncedWithName: boolean;
@Column({ nullable: true, type: 'uuid' })
relationTargetFieldMetadataId: string;
@OneToOne(
() => FieldMetadataEntity,
(fieldMetadata: FieldMetadataEntity) =>
fieldMetadata.relationTargetFieldMetadataId,
)
@JoinColumn({ name: 'relationTargetFieldMetadataId' })
relationTargetFieldMetadata: Relation<FieldMetadataEntity>;
@Column({ nullable: true, type: 'uuid' })
relationTargetObjectMetadataId: string;
@ManyToOne(
() => ObjectMetadataEntity,
(objectMetadata: ObjectMetadataEntity) =>
objectMetadata.targetRelationFields,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'relationTargetObjectMetadataId' })
relationTargetObjectMetadata: Relation<ObjectMetadataEntity>;
@OneToMany(
() => IndexFieldMetadataEntity,
(indexFieldMetadata: IndexFieldMetadataEntity) =>
indexFieldMetadata.indexMetadata,
{
cascade: true,
},
)
indexFieldMetadatas: Relation<IndexFieldMetadataEntity>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}