From 6e92b19e0142e9f2b83d920919d194c7d3ddbd59 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 2 Apr 2025 10:22:22 +0200 Subject: [PATCH] 11311 object create webhook should be triggered when object is created via rest api (#11336) Fixes https://github.com/twentyhq/twenty/issues/11311 --- packages/twenty-server/@types/express.d.ts | 2 +- .../api/rest/core/rest-api-core-v2.service.ts | 45 ++++++++++++++++--- .../src/engine/api/rest/rest-api.module.ts | 9 +--- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/twenty-server/@types/express.d.ts b/packages/twenty-server/@types/express.d.ts index 6bf3987b0..e3df6b10d 100644 --- a/packages/twenty-server/@types/express.d.ts +++ b/packages/twenty-server/@types/express.d.ts @@ -6,7 +6,7 @@ declare module 'express-serve-static-core' { interface Request { user?: User | null; apiKey?: ApiKeyWorkspaceEntity | null; - workspace?: Workspace; + workspace: Workspace; workspaceId?: string; workspaceMetadataVersion?: number; workspaceMemberId?: string; diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts index 6604f557e..53a09f770 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts @@ -4,6 +4,8 @@ import { Request } from 'express'; import { capitalize } from 'twenty-shared/utils'; import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from 'typeorm'; +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory'; @@ -14,13 +16,14 @@ import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-i import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory'; import { OrderByInputFactory } from 'src/engine/api/rest/input-factories/order-by-input.factory'; import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; +import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; interface FindManyMeta { hasNextPage: boolean; @@ -44,10 +47,10 @@ export class RestApiCoreServiceV2 { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly limitInputFactory: LimitInputFactory, private readonly filterInputFactory: FilterInputFactory, - private readonly featureFlagService: FeatureFlagService, private readonly orderByInputFactory: OrderByInputFactory, private readonly startingAfterInputFactory: StartingAfterInputFactory, private readonly endingBeforeInputFactory: EndingBeforeInputFactory, + protected readonly apiEventEmitterService: ApiEventEmitterService, ) {} async delete(request: Request) { @@ -57,7 +60,7 @@ export class RestApiCoreServiceV2 { throw new BadRequestException('Record ID not found'); } - const { objectMetadataNameSingular, repository } = + const { objectMetadataNameSingular, objectMetadata, repository } = await this.getRepositoryAndMetadataOrFail(request); const recordToDelete = await repository.findOneOrFail({ where: { id: recordId }, @@ -65,6 +68,12 @@ export class RestApiCoreServiceV2 { await repository.delete(recordId); + this.apiEventEmitterService.emitDeletedEvents( + [recordToDelete], + this.getAuthContextFromRequest(request), + objectMetadata.objectMetadataMapItem, + ); + return this.formatResult({ operation: 'delete', objectNameSingular: objectMetadataNameSingular, @@ -77,10 +86,16 @@ export class RestApiCoreServiceV2 { async createOne(request: Request) { const { body } = request; - const { objectMetadataNameSingular, repository } = + const { objectMetadataNameSingular, objectMetadata, repository } = await this.getRepositoryAndMetadataOrFail(request); const createdRecord = await repository.save(body); + this.apiEventEmitterService.emitCreateEvents( + [createdRecord], + this.getAuthContextFromRequest(request), + objectMetadata.objectMetadataMapItem, + ); + return this.formatResult({ operation: 'create', objectNameSingular: objectMetadataNameSingular, @@ -95,7 +110,7 @@ export class RestApiCoreServiceV2 { throw new BadRequestException('Record ID not found'); } - const { objectMetadataNameSingular, repository } = + const { objectMetadataNameSingular, objectMetadata, repository } = await this.getRepositoryAndMetadataOrFail(request); const recordToUpdate = await repository.findOneOrFail({ @@ -107,6 +122,14 @@ export class RestApiCoreServiceV2 { ...request.body, }); + this.apiEventEmitterService.emitUpdateEvents( + [recordToUpdate], + [updatedRecord], + Object.keys(request.body), + this.getAuthContextFromRequest(request), + objectMetadata.objectMetadataMapItem, + ); + return this.formatResult({ operation: 'update', objectNameSingular: objectMetadataNameSingular, @@ -441,7 +464,7 @@ export class RestApiCoreServiceV2 { ); const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspace.id, objectMetadataNameSingular, ); @@ -465,4 +488,14 @@ export class RestApiCoreServiceV2 { id: isForwardPagination ? { gt: cursorData.id } : { lt: cursorData.id }, }; } + + private getAuthContextFromRequest(request: Request): AuthContext { + return { + user: request.user, + workspace: request.workspace, + apiKey: request.apiKey, + workspaceMemberId: request.workspaceMemberId, + userWorkspaceId: request.userWorkspaceId, + }; + } } diff --git a/packages/twenty-server/src/engine/api/rest/rest-api.module.ts b/packages/twenty-server/src/engine/api/rest/rest-api.module.ts index 36ead9493..8334b9802 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api.module.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api.module.ts @@ -1,6 +1,5 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller'; import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller'; @@ -17,11 +16,9 @@ import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service'; import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; @Module({ imports: [ @@ -31,8 +28,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ AuthModule, HttpModule, TwentyORMModule, - TypeOrmModule.forFeature([FeatureFlag], 'core'), - FeatureFlagModule, ], controllers: [ RestApiMetadataController, @@ -44,12 +39,12 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ RestApiCoreService, RestApiCoreServiceV2, RestApiService, - FeatureFlagService, StartingAfterInputFactory, EndingBeforeInputFactory, LimitInputFactory, FilterInputFactory, OrderByInputFactory, + ApiEventEmitterService, ], exports: [RestApiMetadataService], })