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 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23382?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:
@@ -1,26 +1,20 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.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 { 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 { FlatWebhookModule } from 'src/engine/metadata-modules/flat-webhook/flat-webhook.module';
|
||||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||||
import { WebhookController } from 'src/engine/metadata-modules/webhook/controllers/webhook.controller';
|
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 { 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 { WebhookToolWorkspaceService } from 'src/engine/metadata-modules/webhook/tools/services/webhook-tool.workspace-service';
|
||||||
import { WebhookResolver } from 'src/engine/metadata-modules/webhook/webhook.resolver';
|
import { WebhookResolver } from 'src/engine/metadata-modules/webhook/webhook.resolver';
|
||||||
import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service';
|
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 { 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 { 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';
|
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([ApplicationEntity, WebhookEntity]),
|
|
||||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||||
WorkspaceMigrationModule,
|
WorkspaceMigrationModule,
|
||||||
WorkspaceCacheStorageModule,
|
WorkspaceCacheStorageModule,
|
||||||
@@ -36,7 +30,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
|||||||
WebhookGraphqlApiExceptionInterceptor,
|
WebhookGraphqlApiExceptionInterceptor,
|
||||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||||
WebhookToolWorkspaceService,
|
WebhookToolWorkspaceService,
|
||||||
provideWorkspaceScopedRepository(WebhookEntity),
|
|
||||||
],
|
],
|
||||||
exports: [WebhookService, WebhookToolWorkspaceService],
|
exports: [WebhookService, WebhookToolWorkspaceService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 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 UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
|
||||||
import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto';
|
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 { 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';
|
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebhookService {
|
export class WebhookService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectWorkspaceScopedRepository(WebhookEntity)
|
|
||||||
private readonly webhookRepository: WorkspaceScopedRepository<WebhookEntity>,
|
|
||||||
@InjectRepository(ApplicationEntity)
|
|
||||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
|
||||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||||
private readonly applicationService: ApplicationService,
|
private readonly applicationService: ApplicationService,
|
||||||
@@ -46,54 +34,32 @@ export class WebhookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findAll(workspaceId: string): Promise<WebhookDTO[]> {
|
async findAll(workspaceId: string): Promise<WebhookDTO[]> {
|
||||||
const [webhooks, applications] = await Promise.all([
|
const { flatWebhookMaps } =
|
||||||
this.webhookRepository.find(workspaceId, {
|
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||||
where: { deletedAt: IsNull() },
|
{ workspaceId, flatMapsKeys: ['flatWebhookMaps'] },
|
||||||
order: { createdAt: 'ASC' },
|
);
|
||||||
}),
|
|
||||||
this.applicationRepository.find({
|
|
||||||
where: { workspaceId },
|
|
||||||
select: ['id', 'universalIdentifier'],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const applicationIdToUniversalIdentifierMap =
|
return Object.values(flatWebhookMaps.byUniversalIdentifier)
|
||||||
createIdToUniversalIdentifierMap(applications);
|
.filter(isDefined)
|
||||||
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
||||||
return webhooks
|
|
||||||
.map((webhookEntity) =>
|
|
||||||
fromWebhookEntityToFlatWebhook({
|
|
||||||
entity: webhookEntity,
|
|
||||||
applicationIdToUniversalIdentifierMap,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.map(fromFlatWebhookToWebhookDto);
|
.map(fromFlatWebhookToWebhookDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string, workspaceId: string): Promise<WebhookDTO | null> {
|
async findById(id: string, workspaceId: string): Promise<WebhookDTO | null> {
|
||||||
const [webhook, applications] = await Promise.all([
|
const { flatWebhookMaps } =
|
||||||
this.webhookRepository.findOne(workspaceId, {
|
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||||
where: { id, deletedAt: IsNull() },
|
{ workspaceId, flatMapsKeys: ['flatWebhookMaps'] },
|
||||||
}),
|
);
|
||||||
this.applicationRepository.find({
|
const webhook = findFlatEntityByIdInFlatEntityMaps({
|
||||||
where: { workspaceId },
|
flatEntityId: id,
|
||||||
select: ['id', 'universalIdentifier'],
|
flatEntityMaps: flatWebhookMaps,
|
||||||
}),
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
if (!isDefined(webhook)) {
|
if (!isDefined(webhook)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationIdToUniversalIdentifierMap =
|
return fromFlatWebhookToWebhookDto(webhook);
|
||||||
createIdToUniversalIdentifierMap(applications);
|
|
||||||
|
|
||||||
return fromFlatWebhookToWebhookDto(
|
|
||||||
fromWebhookEntityToFlatWebhook({
|
|
||||||
entity: webhook,
|
|
||||||
applicationIdToUniversalIdentifierMap,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
|
|||||||
Reference in New Issue
Block a user