From ae0ffb1373ca92349db86ece2aacf6da5eccc2f4 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 28 Jul 2026 09:54:24 +0200 Subject: [PATCH] perf: use cache for view entity lookups (#23384) ## Context View child mutation guards resolve a parent view before checking access. The lookup service queried PostgreSQL for a single `viewId`, even though the same relationship already exists in the workspace flat-map cache. With 15 guards using this service, each guarded mutation could add an unnecessary database round trip. ## What changed - Replace the five workspace-scoped repositories with `WorkspaceManyOrAllFlatEntityMapsCacheService` - Load only the flat map matching the requested child kind - Resolve view fields, filters, filter groups, groups, and sorts by ID - Preserve the existing `null` behavior for missing entities Each lookup is keyed by entity ID, no workspace-wide filtering is introduced. ## Expected impact On a warm workspace cache, permission guards resolve the parent `viewId` without querying PostgreSQL. Cold caches retain the normal workspace cache recomputation behavior. ## Validation - Typecheck reports no errors in the changed file - Existing lookup semantics are preserved for all supported entity kinds and missing IDs Review in cubic --- .../services/view-entity-lookup.service.ts | 104 +++++++++--------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-entity-lookup.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-entity-lookup.service.ts index df53f0271d..dfbad08e66 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-entity-lookup.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-entity-lookup.service.ts @@ -1,27 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; -import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; -import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; -import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type ViewChildEntityKind } from 'src/engine/metadata-modules/view-permissions/types/view-permissions.types'; -import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; -import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; -import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; @Injectable() export class ViewEntityLookupService { constructor( - @InjectWorkspaceScopedRepository(ViewFieldEntity) - private readonly viewFieldRepository: WorkspaceScopedRepository, - @InjectWorkspaceScopedRepository(ViewFilterEntity) - private readonly viewFilterRepository: WorkspaceScopedRepository, - @InjectWorkspaceScopedRepository(ViewFilterGroupEntity) - private readonly viewFilterGroupRepository: WorkspaceScopedRepository, - @InjectWorkspaceScopedRepository(ViewGroupEntity) - private readonly viewGroupRepository: WorkspaceScopedRepository, - @InjectWorkspaceScopedRepository(ViewSortEntity) - private readonly viewSortRepository: WorkspaceScopedRepository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} async findViewIdByEntityIdAndKind( @@ -31,58 +17,76 @@ export class ViewEntityLookupService { ): Promise { switch (kind) { case 'viewField': { - const row = await this.viewFieldRepository.findOne(workspaceId, { - where: { id: entityId }, - select: ['viewId'], - }); + const { flatViewFieldMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatViewFieldMaps'] }, + ); - if (row) return row.viewId; - break; + return ( + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: entityId, + flatEntityMaps: flatViewFieldMaps, + })?.viewId ?? null + ); } case 'viewFilter': { - const row = await this.viewFilterRepository.findOne(workspaceId, { - where: { id: entityId }, - select: ['viewId'], - }); + const { flatViewFilterMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatViewFilterMaps'] }, + ); - if (row) return row.viewId; - break; + return ( + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: entityId, + flatEntityMaps: flatViewFilterMaps, + })?.viewId ?? null + ); } case 'viewFilterGroup': { - const row = await this.viewFilterGroupRepository.findOne(workspaceId, { - where: { id: entityId }, - select: ['viewId'], - }); + const { flatViewFilterGroupMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatViewFilterGroupMaps'] }, + ); - if (row) return row.viewId; - break; + return ( + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: entityId, + flatEntityMaps: flatViewFilterGroupMaps, + })?.viewId ?? null + ); } case 'viewGroup': { - const row = await this.viewGroupRepository.findOne(workspaceId, { - where: { id: entityId }, - select: ['viewId'], - }); + const { flatViewGroupMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatViewGroupMaps'] }, + ); - if (row) return row.viewId; - break; + return ( + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: entityId, + flatEntityMaps: flatViewGroupMaps, + })?.viewId ?? null + ); } case 'viewSort': { - const row = await this.viewSortRepository.findOne(workspaceId, { - where: { id: entityId }, - select: ['viewId'], - }); + const { flatViewSortMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatViewSortMaps'] }, + ); - if (row) return row.viewId; - break; + return ( + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: entityId, + flatEntityMaps: flatViewSortMaps, + })?.viewId ?? null + ); } default: - break; + return null; } - - return null; } }