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 { 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, ) {} private normalizeTargetUrl(targetUrl: string): string { try { const url = new URL(targetUrl); return url.toString(); } catch { return targetUrl; } } 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 applicationIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(applications); return webhooks .map((webhookEntity) => fromWebhookEntityToFlatWebhook({ entity: webhookEntity, applicationIdToUniversalIdentifierMap, }), ) .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'], }), ]); if (!isDefined(webhook)) { return null; } const applicationIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(applications); return fromFlatWebhookToWebhookDto( fromWebhookEntityToFlatWebhook({ entity: webhook, applicationIdToUniversalIdentifierMap, }), ); } async create( input: CreateWebhookInput, workspaceId: string, ): Promise { const normalizedTargetUrl = this.normalizeTargetUrl(input.targetUrl); const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ); const flatWebhookToCreate = fromCreateWebhookInputToFlatWebhookToCreate({ createWebhookInput: { ...input, targetUrl: normalizedTargetUrl, }, workspaceId, flatApplication: workspaceCustomFlatApplication, }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { webhook: { flatEntityToCreate: [flatWebhookToCreate], flatEntityToDelete: [], flatEntityToUpdate: [], }, }, workspaceId, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating webhook', ); } const { flatWebhookMaps: recomputedFlatWebhookMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatWebhookMaps'], }, ); return fromFlatWebhookToWebhookDto( findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId: flatWebhookToCreate.id, flatEntityMaps: recomputedFlatWebhookMaps, }), ); } async update( input: UpdateWebhookInput, workspaceId: string, ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ); const normalizedInput = { ...input, update: { ...input.update, ...(isDefined(input.update.targetUrl) && { targetUrl: this.normalizeTargetUrl(input.update.targetUrl), }), }, }; const { flatWebhookMaps: existingFlatWebhookMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatWebhookMaps'], }, ); const flatWebhookToUpdate = fromUpdateWebhookInputToFlatWebhookToUpdateOrThrow({ flatWebhookMaps: existingFlatWebhookMaps, updateWebhookInput: normalizedInput, }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { webhook: { flatEntityToCreate: [], flatEntityToDelete: [], flatEntityToUpdate: [flatWebhookToUpdate], }, }, workspaceId, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating webhook', ); } const { flatWebhookMaps: recomputedFlatWebhookMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatWebhookMaps'], }, ); return fromFlatWebhookToWebhookDto( findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId: input.id, flatEntityMaps: recomputedFlatWebhookMaps, }), ); } async delete(id: string, workspaceId: string): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ); const { flatWebhookMaps: existingFlatWebhookMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatWebhookMaps'], }, ); const flatWebhookToDelete = fromDeleteWebhookInputToFlatWebhookOrThrow({ flatWebhookMaps: existingFlatWebhookMaps, webhookId: id, }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { webhook: { flatEntityToCreate: [], flatEntityToDelete: [flatWebhookToDelete], flatEntityToUpdate: [], }, }, workspaceId, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while deleting webhook', ); } return fromFlatWebhookToWebhookDto(flatWebhookToDelete); } }