Add entitySchemas to workspace context and cache (#15966)

This commit is contained in:
Weiko
2025-11-21 15:07:46 +01:00
committed by GitHub
parent f77fcdcc63
commit a21d58f43d
4 changed files with 61 additions and 114 deletions
@@ -43,7 +43,6 @@ export class GlobalWorkspaceDataSourceService
},
},
this.workspaceEventEmitter,
this.entitySchemaFactory,
);
await this.globalWorkspaceDataSource.initialize();
@@ -23,9 +23,7 @@ import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { type EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/workspace-context.storage';
@@ -36,29 +34,17 @@ type CreateQueryBuilderOptions = {
calledByWorkspaceEntityManager?: boolean;
};
const ENTITY_METADATA_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
type CachedEntityMetadata = {
entityMetadataMap: Map<EntityTarget<ObjectLiteral>, EntityMetadata>;
timestamp: number;
};
export class GlobalWorkspaceDataSource extends DataSource {
private entityMetadataCache: Map<string, CachedEntityMetadata>;
private eventEmitterService: WorkspaceEventEmitter;
private entitySchemaFactory: EntitySchemaFactory;
private _isConstructing = true;
dataSourceWithOverridenCreateQueryBuilder: GlobalWorkspaceDataSource;
constructor(
options: DataSourceOptions,
eventEmitterService: WorkspaceEventEmitter,
entitySchemaFactory: EntitySchemaFactory,
) {
super(options);
this.eventEmitterService = eventEmitterService;
this.entitySchemaFactory = entitySchemaFactory;
this.entityMetadataCache = new Map();
this._isConstructing = false;
Object.defineProperty(this, 'manager', {
@@ -97,17 +83,11 @@ export class GlobalWorkspaceDataSource extends DataSource {
target: EntityTarget<ObjectLiteral>,
): EntityMetadata | undefined {
const context = getWorkspaceContext();
const { authContext, metadataVersion } = context;
const workspaceId = authContext.workspace.id;
const cacheKey = `${workspaceId}-${metadataVersion}`;
const { entitySchemas } = context;
const cachedEntityMetadata = this.getCachedEntityMetadata(cacheKey);
const entityMetadata = this.buildMetadatasFromSchemas(entitySchemas);
if (!cachedEntityMetadata) {
return undefined;
}
return cachedEntityMetadata.entityMetadataMap.get(target);
return entityMetadata.find((metadata) => metadata.target === target);
}
override getMetadata(target: EntityTarget<ObjectLiteral>): EntityMetadata {
@@ -282,53 +262,6 @@ export class GlobalWorkspaceDataSource extends DataSource {
return super.query(query, parameters, queryRunner);
}
async buildWorkspaceMetadata(
workspaceId: string,
metadataVersion: number,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<void> {
const cacheKey = `${workspaceId}-${metadataVersion}`;
if (this.getCachedEntityMetadata(cacheKey)) {
return;
}
this.clearWorkspaceEntityMetadataCache(workspaceId);
const entitySchemas = await Promise.all(
Object.values(objectMetadataMaps.byId)
.filter(isDefined)
.map((objectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
objectMetadata,
objectMetadataMaps,
),
),
);
const entityMetadatas = this.buildMetadatasFromSchemas(entitySchemas);
const metadataMap = new Map<EntityTarget<ObjectLiteral>, EntityMetadata>();
for (const metadata of entityMetadatas) {
metadataMap.set(metadata.target, metadata);
}
this.entityMetadataCache.set(cacheKey, {
entityMetadataMap: metadataMap,
timestamp: Date.now(),
});
}
private clearWorkspaceEntityMetadataCache(workspaceId: string): void {
for (const key of this.entityMetadataCache.keys()) {
if (key.startsWith(`${workspaceId}-`)) {
this.entityMetadataCache.delete(key);
}
}
}
private buildMetadatasFromSchemas(
entitySchemas: EntitySchema[],
): EntityMetadata[] {
@@ -344,31 +277,4 @@ export class GlobalWorkspaceDataSource extends DataSource {
return entityMetadatas;
}
hasWorkspaceEntityMetadataCacheForVersion(
workspaceId: string,
metadataVersion: number,
): boolean {
const cacheKey = `${workspaceId}-${metadataVersion}`;
return !!this.getCachedEntityMetadata(cacheKey);
}
private getCachedEntityMetadata(
cacheKey: string,
): CachedEntityMetadata | undefined {
const cached = this.entityMetadataCache.get(cacheKey);
if (!cached) {
return undefined;
}
if (Date.now() - cached.timestamp > ENTITY_METADATA_CACHE_TTL_MS) {
this.entityMetadataCache.delete(cacheKey);
return undefined;
}
return cached;
}
}
@@ -1,12 +1,15 @@
import { Injectable, type Type } from '@nestjs/common';
import { type ObjectLiteral } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { EntitySchema, type ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import {
@@ -14,6 +17,7 @@ import {
withWorkspaceContext,
} from 'src/engine/twenty-orm/storage/workspace-context.storage';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
@Injectable()
@@ -23,6 +27,8 @@ export class GlobalWorkspaceOrmManager {
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly entitySchemaFactory: EntitySchemaFactory,
) {}
async getRepository<T extends ObjectLiteral>(
@@ -70,21 +76,6 @@ export class GlobalWorkspaceOrmManager {
fn: () => T | Promise<T>,
): Promise<T> {
const context = await this.loadWorkspaceContext(authContext);
const globalDataSource =
this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource();
if (
!globalDataSource.hasWorkspaceEntityMetadataCacheForVersion(
authContext.workspace.id,
context.metadataVersion,
)
) {
await globalDataSource.buildWorkspaceMetadata(
authContext.workspace.id,
context.metadataVersion,
context.objectMetadataMaps,
);
}
return withWorkspaceContext(context, fn);
}
@@ -110,12 +101,61 @@ export class GlobalWorkspaceOrmManager {
workspaceId,
});
const entitySchemas = await this.buildEntitySchemas(
workspaceId,
metadataVersion,
objectMetadataMaps,
);
return {
authContext,
objectMetadataMaps,
metadataVersion,
featureFlagsMap,
permissionsPerRoleId,
entitySchemas,
};
}
private async buildEntitySchemas(
workspaceId: string,
dataSourceMetadataVersion: number,
objectMetadataMaps: ObjectMetadataMaps,
) {
const cachedEntitySchemaOptions =
await this.workspaceCacheStorageService.getORMEntitySchema(
workspaceId,
dataSourceMetadataVersion,
);
let cachedEntitySchemas: EntitySchema[];
if (cachedEntitySchemaOptions) {
cachedEntitySchemas = cachedEntitySchemaOptions.map(
(option) => new EntitySchema(option),
);
} else {
const entitySchemas = await Promise.all(
Object.values(objectMetadataMaps.byId)
.filter(isDefined)
.map((objectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
objectMetadata,
objectMetadataMaps,
),
),
);
await this.workspaceCacheStorageService.setORMEntitySchema(
workspaceId,
dataSourceMetadataVersion,
entitySchemas.map((entitySchema) => entitySchema.options),
);
cachedEntitySchemas = entitySchemas;
}
return cachedEntitySchemas;
}
}
@@ -1,6 +1,7 @@
import { AsyncLocalStorage } from 'async_hooks';
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import { type EntitySchema } from 'typeorm';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
@@ -13,6 +14,7 @@ export type WorkspaceContext = {
metadataVersion: number;
featureFlagsMap: Record<FeatureFlagKey, boolean>;
permissionsPerRoleId: ObjectsPermissionsByRoleId;
entitySchemas: EntitySchema[];
};
export const workspaceContextStorage =