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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23384?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Weiko
2026-07-28 09:54:24 +02:00
committed by GitHub
parent 19903f89e5
commit ae0ffb1373
@@ -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<ViewFieldEntity>,
@InjectWorkspaceScopedRepository(ViewFilterEntity)
private readonly viewFilterRepository: WorkspaceScopedRepository<ViewFilterEntity>,
@InjectWorkspaceScopedRepository(ViewFilterGroupEntity)
private readonly viewFilterGroupRepository: WorkspaceScopedRepository<ViewFilterGroupEntity>,
@InjectWorkspaceScopedRepository(ViewGroupEntity)
private readonly viewGroupRepository: WorkspaceScopedRepository<ViewGroupEntity>,
@InjectWorkspaceScopedRepository(ViewSortEntity)
private readonly viewSortRepository: WorkspaceScopedRepository<ViewSortEntity>,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async findViewIdByEntityIdAndKind(
@@ -31,58 +17,76 @@ export class ViewEntityLookupService {
): Promise<string | null> {
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;
}
}