Optimize EntityMetadata caching in GlobalWorkspaceDataSource (#16146)

## Context
EntityMetadata was being rebuilt from scratch on every
findMetadata()/getMetadata() call (~20 times per request). This involved
running EntitySchemaTransformer.transform() and
EntityMetadataBuilder.build() repeatedly, causing unnecessary CPU
overhead.

## Implementation
Cache entityMetadatas in ORMWorkspaceContext: Build EntityMetadata once
during workspace context initialization instead of on every metadata
lookup
Remove redundant entitySchemas caching: Since flatMetadata is already
cached, the additional Redis cache for entitySchemaOptions was
unnecessary overhead
Remove WorkspaceEntitiesStorage: Replaced with direct lookup from
FlatObjectMetadataMap
Simplify getObjectMetadataFromEntityTarget: Now only accepts string
targets, using flat metadata maps directly

Also:
Removed unused injections in some services
This commit is contained in:
Weiko
2025-11-28 10:09:52 +01:00
committed by GitHub
parent fd9ea2f5ee
commit 9620a4b0ba
11 changed files with 45 additions and 148 deletions
@@ -1,7 +1,7 @@
import { AsyncLocalStorage } from 'async_hooks';
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import { type EntitySchema } from 'typeorm';
import { type EntityMetadata } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
@@ -11,22 +11,21 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export type WorkspaceContext = {
export type ORMWorkspaceContext = {
authContext: WorkspaceAuthContext;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
objectIdByNameSingular: Record<string, string>;
metadataVersion: number;
featureFlagsMap: Record<FeatureFlagKey, boolean>;
permissionsPerRoleId: ObjectsPermissionsByRoleId;
entitySchemas: EntitySchema[];
entityMetadatas: EntityMetadata[];
};
export const workspaceContextStorage =
new AsyncLocalStorage<WorkspaceContext>();
new AsyncLocalStorage<ORMWorkspaceContext>();
export const getWorkspaceContext = (): WorkspaceContext => {
export const getWorkspaceContext = (): ORMWorkspaceContext => {
const context = workspaceContextStorage.getStore();
if (!context) {
@@ -39,12 +38,12 @@ export const getWorkspaceContext = (): WorkspaceContext => {
};
export const withWorkspaceContext = <T>(
context: WorkspaceContext,
context: ORMWorkspaceContext,
fn: () => T | Promise<T>,
): T | Promise<T> => {
return workspaceContextStorage.run(context, fn);
};
export const setWorkspaceContext = (context: WorkspaceContext): void => {
export const setWorkspaceContext = (context: ORMWorkspaceContext): void => {
workspaceContextStorage.enterWith(context);
};
@@ -1,32 +0,0 @@
import { type EntitySchema } from 'typeorm';
export class WorkspaceEntitiesStorage {
private static workspaceEntities = new Map<
string,
Map<string, EntitySchema>
>();
static setEntitySchema(
workspaceId: string,
objectMetadataName: string,
schema: EntitySchema,
): void {
if (!this.workspaceEntities.has(workspaceId)) {
this.workspaceEntities.set(workspaceId, new Map<string, EntitySchema>());
}
const workspace = this.workspaceEntities.get(workspaceId);
workspace?.set(objectMetadataName, schema);
}
static getObjectMetadataName(
workspaceId: string,
target: EntitySchema,
): string | undefined {
const workspace = this.workspaceEntities.get(workspaceId);
return Array.from(workspace?.entries() || []).find(
([, schema]) => schema.options.name === target.options.name,
)?.[0];
}
}