11311 object create webhook should be triggered when object is created via rest api (#11336)

Fixes https://github.com/twentyhq/twenty/issues/11311
This commit is contained in:
martmull
2025-04-02 10:22:22 +02:00
committed by GitHub
parent 9a2f7d8b71
commit 6e92b19e01
3 changed files with 42 additions and 14 deletions

View File

@ -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;

View File

@ -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<ObjectRecord>(
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,
};
}
}

View File

@ -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],
})