From 8481c76bfb9da0ec85ab2aaac42b7fb15590b494 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 28 Jul 2026 09:53:29 +0200 Subject: [PATCH] perf: use cache for webhook reads (#23382) ## Context Webhook reads queried both the webhook and application tables, then rebuilt the same flat webhook representation already maintained by the workspace cache. This affected REST, GraphQL, and the webhook listing tool. ## What changed - Read `findAll` and `findById` from `flatWebhookMaps` - Keep `findById` as a keyed ID lookup - Preserve `findAll` ordering by `createdAt` - Remove unused webhook and application repository wiring The flat webhook cache contains only active webhooks and already includes the application universal identifier needed by the DTO conversion. ## Expected impact On a warm workspace cache, webhook reads avoid queries to both the webhook and application tables. `findAll` still iterates over every returned webhook, matching the original query's result cardinality, while `findById` uses a keyed lookup. ## Validation - Typecheck reports no errors in the changed files - `findAll` ordering and `findById` missing-record behavior are preserved Review in cubic --- .../webhook/webhook.module.ts | 7 -- .../webhook/webhook.service.ts | 68 +++++-------------- 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts index 13aa003e66..34f6d41e0a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts @@ -1,26 +1,20 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { FlatWebhookModule } from 'src/engine/metadata-modules/flat-webhook/flat-webhook.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WebhookController } from 'src/engine/metadata-modules/webhook/controllers/webhook.controller'; -import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; import { WebhookGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/webhook/interceptors/webhook-graphql-api-exception.interceptor'; import { WebhookToolWorkspaceService } from 'src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service'; import { WebhookResolver } from 'src/engine/metadata-modules/webhook/webhook.resolver'; import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service'; -import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ imports: [ - TypeOrmModule.forFeature([ApplicationEntity, WebhookEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationModule, WorkspaceCacheStorageModule, @@ -36,7 +30,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WebhookGraphqlApiExceptionInterceptor, WorkspaceMigrationGraphqlApiExceptionInterceptor, WebhookToolWorkspaceService, - provideWorkspaceScopedRepository(WebhookEntity), ], exports: [WebhookService, WebhookToolWorkspaceService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts index 2f71ba0bff..dbd9a772ef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts @@ -1,35 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - import { isDefined } from 'twenty-shared/utils'; -import { IsNull, Repository } from 'typeorm'; -import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; 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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { fromCreateWebhookInputToFlatWebhookToCreate } from 'src/engine/metadata-modules/flat-webhook/utils/from-create-webhook-input-to-flat-webhook-to-create.util'; import { fromDeleteWebhookInputToFlatWebhookOrThrow } from 'src/engine/metadata-modules/flat-webhook/utils/from-delete-webhook-input-to-flat-webhook-or-throw.util'; import { fromFlatWebhookToWebhookDto } from 'src/engine/metadata-modules/flat-webhook/utils/from-flat-webhook-to-webhook-dto.util'; import { fromUpdateWebhookInputToFlatWebhookToUpdateOrThrow } from 'src/engine/metadata-modules/flat-webhook/utils/from-update-webhook-input-to-flat-webhook-to-update-or-throw.util'; -import { fromWebhookEntityToFlatWebhook } from 'src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util'; -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'; import { type CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/create-webhook.input'; import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input'; import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto'; -import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; -import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @Injectable() export class WebhookService { constructor( - @InjectWorkspaceScopedRepository(WebhookEntity) - private readonly webhookRepository: WorkspaceScopedRepository, - @InjectRepository(ApplicationEntity) - private readonly applicationRepository: Repository, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly applicationService: ApplicationService, @@ -46,54 +34,32 @@ export class WebhookService { } async findAll(workspaceId: string): Promise { - const [webhooks, applications] = await Promise.all([ - this.webhookRepository.find(workspaceId, { - where: { deletedAt: IsNull() }, - order: { createdAt: 'ASC' }, - }), - this.applicationRepository.find({ - where: { workspaceId }, - select: ['id', 'universalIdentifier'], - }), - ]); + const { flatWebhookMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatWebhookMaps'] }, + ); - const applicationIdToUniversalIdentifierMap = - createIdToUniversalIdentifierMap(applications); - - return webhooks - .map((webhookEntity) => - fromWebhookEntityToFlatWebhook({ - entity: webhookEntity, - applicationIdToUniversalIdentifierMap, - }), - ) + return Object.values(flatWebhookMaps.byUniversalIdentifier) + .filter(isDefined) + .sort((a, b) => a.createdAt.localeCompare(b.createdAt)) .map(fromFlatWebhookToWebhookDto); } async findById(id: string, workspaceId: string): Promise { - const [webhook, applications] = await Promise.all([ - this.webhookRepository.findOne(workspaceId, { - where: { id, deletedAt: IsNull() }, - }), - this.applicationRepository.find({ - where: { workspaceId }, - select: ['id', 'universalIdentifier'], - }), - ]); + const { flatWebhookMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, flatMapsKeys: ['flatWebhookMaps'] }, + ); + const webhook = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatWebhookMaps, + }); if (!isDefined(webhook)) { return null; } - const applicationIdToUniversalIdentifierMap = - createIdToUniversalIdentifierMap(applications); - - return fromFlatWebhookToWebhookDto( - fromWebhookEntityToFlatWebhook({ - entity: webhook, - applicationIdToUniversalIdentifierMap, - }), - ); + return fromFlatWebhookToWebhookDto(webhook); } async create(