From 0e561e4ef45fc2343c98c83c669c21bdb0a9535c Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:41:53 +0530 Subject: [PATCH] fix: migrate webhook and API key REST endpoints to core schema (#13318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem After migrating webhooks and API keys from workspace to core level, REST API endpoints were still creating entities in workspace schema (`workspace_*`) instead of core schema, causing webhooks to not fire. ## Solution - Added dedicated REST controllers for webhooks (`/rest/webhooks`) and API keys (`/rest/apiKeys`) - Updated dynamic controller to block workspace-gated entities from being processed - Fixed OpenAPI documentation to exclude these endpoints from playground - Ensured return formats match GraphQL resolvers exactly ## Testing ✅ All endpoints tested with provided auth token - webhooks and API keys now correctly stored in `core` schema --- .../workspace-resolver.factory.ts | 32 ++----- .../api/graphql/workspace-schema.factory.ts | 26 +----- .../core-query-builder.factory.ts | 24 ++++- .../core-query-builder.module.ts | 2 + .../core-modules/api-key/api-key.module.ts | 12 ++- .../api-key/controllers/api-key.controller.ts | 89 +++++++++++++++++++ .../core-modules/open-api/open-api.module.ts | 5 +- .../open-api/open-api.service.spec.ts | 6 ++ .../core-modules/open-api/open-api.service.ts | 38 ++++++-- .../webhook/controllers/webhook.controller.ts | 78 ++++++++++++++++ .../core-modules/webhook/webhook.module.ts | 17 +++- .../decorators/workspace-gate.decorator.ts | 4 +- .../twenty-orm/interfaces/gate.interface.ts | 2 +- .../utils/is-gate-and-not-enabled.util.ts | 8 +- .../should-exclude-from-workspace-api.util.ts | 23 +++++ .../api-key.workspace-entity.ts | 2 +- .../webhook.workspace-entity.ts | 2 +- 17 files changed, 302 insertions(+), 68 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts create mode 100644 packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util.ts diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index 6591d0b93..03118258f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -17,10 +17,9 @@ import { import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; +import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; import { CreateOneResolverFactory } from './factories/create-one-resolver.factory'; @@ -100,27 +99,14 @@ export class WorkspaceResolverFactory { for (const objectMetadata of Object.values(objectMetadataMaps.byId).filter( isDefined, )) { - const workspaceEntity = standardObjectMetadataDefinitions.find( - (entity) => { - const entityMetadata = metadataArgsStorage.filterEntities(entity); - - return entityMetadata?.standardId === objectMetadata.standardId; - }, - ); - - if (workspaceEntity) { - const entityMetadata = - metadataArgsStorage.filterEntities(workspaceEntity); - - if ( - isGatedAndNotEnabled( - entityMetadata?.gate, - workspaceFeatureFlagsMap, - 'graphql', - ) - ) { - continue; - } + if ( + shouldExcludeFromWorkspaceApi( + objectMetadata, + standardObjectMetadataDefinitions, + workspaceFeatureFlagsMap, + ) + ) { + continue; } // Generate query resolvers diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 47ab6e982..5d6d4035c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -21,10 +21,9 @@ import { WorkspaceMetadataCacheExceptionCode, } from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; +import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; @Injectable() export class WorkspaceSchemaFactory { @@ -86,27 +85,10 @@ export class WorkspaceSchemaFactory { indexes: objectMetadataItem.indexMetadatas, })) .filter((objectMetadata) => { - // Find the corresponding workspace entity for this object metadata - const workspaceEntity = standardObjectMetadataDefinitions.find( - (entity) => { - const entityMetadata = metadataArgsStorage.filterEntities(entity); - - return entityMetadata?.standardId === objectMetadata.standardId; - }, - ); - - if (!workspaceEntity) { - return true; // Include non-workspace entities (custom objects, etc.) - } - - const entityMetadata = - metadataArgsStorage.filterEntities(workspaceEntity); - - // Filter out entities that are GraphQL-gated and not enabled - return !isGatedAndNotEnabled( - entityMetadata?.gate, + return !shouldExcludeFromWorkspaceApi( + objectMetadata, + standardObjectMetadataDefinitions, workspaceFeatureFlagsMap, - 'graphql', ); }); diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts index b5dec7f43..df10443e4 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts @@ -12,25 +12,29 @@ import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path import { Query } from 'src/engine/api/rest/core/types/query.type'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; +import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; @Injectable() export class CoreQueryBuilderFactory { constructor( private readonly createManyQueryFactory: CreateManyQueryFactory, - private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory, private readonly createVariablesFactory: CreateVariablesFactory, + private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory, private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory, private readonly accessTokenService: AccessTokenService, private readonly domainManagerService: DomainManagerService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, + private readonly featureFlagService: FeatureFlagService, ) {} async getObjectMetadata( @@ -94,6 +98,22 @@ export class CoreQueryBuilderFactory { ); } + const workspaceFeatureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); + + // Check if this entity is workspace-gated and should be blocked from workspace API + if ( + shouldExcludeFromWorkspaceApi( + objectMetadataItem, + standardObjectMetadataDefinitions, + workspaceFeatureFlagsMap, + ) + ) { + throw new BadRequestException( + `object '${parsedObject}' not found. ${parsedObject} is not available via REST API.`, + ); + } + return { objectMetadataMaps, objectMetadataMapItem: objectMetadataItem, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts index ec8386ffe..b927f1761 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts @@ -4,6 +4,7 @@ import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/ import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -11,6 +12,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ imports: [ AuthModule, DomainManagerModule, + FeatureFlagModule, WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, ], diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts index b849aa241..9cde88b9a 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts @@ -4,11 +4,21 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity'; import { ApiKeyResolver } from 'src/engine/core-modules/api-key/api-key.resolver'; import { ApiKeyService } from 'src/engine/core-modules/api-key/api-key.service'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; + +import { ApiKeyController } from './controllers/api-key.controller'; @Module({ - imports: [TypeOrmModule.forFeature([ApiKey], 'core'), JwtModule], + imports: [ + TypeOrmModule.forFeature([ApiKey], 'core'), + JwtModule, + AuthModule, + WorkspaceCacheStorageModule, + ], providers: [ApiKeyService, ApiKeyResolver], + controllers: [ApiKeyController], exports: [ApiKeyService, TypeOrmModule], }) export class ApiKeyModule {} diff --git a/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts new file mode 100644 index 000000000..8b23f0414 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/api-key/controllers/api-key.controller.ts @@ -0,0 +1,89 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + UseFilters, + UseGuards, +} from '@nestjs/common'; + +import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; +import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity'; +import { ApiKeyService } from 'src/engine/core-modules/api-key/api-key.service'; +import { CreateApiKeyDTO } from 'src/engine/core-modules/api-key/dtos/create-api-key.dto'; +import { UpdateApiKeyDTO } from 'src/engine/core-modules/api-key/dtos/update-api-key.dto'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +/** + * rest/apiKeys is deprecated, use rest/metadata/apiKeys instead + * rest/apiKeys will be removed in the future + */ +@Controller(['rest/apiKeys', 'rest/metadata/apiKeys']) +@UseGuards(JwtAuthGuard, WorkspaceAuthGuard) +@UseFilters(RestApiExceptionFilter) +export class ApiKeyController { + constructor(private readonly apiKeyService: ApiKeyService) {} + + @Get() + async findAll(@AuthWorkspace() workspace: Workspace): Promise { + return this.apiKeyService.findActiveByWorkspaceId(workspace.id); + } + + @Get(':id') + async findOne( + @Param('id') id: string, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.apiKeyService.findById(id, workspace.id); + } + + @Post() + async create( + @Body() createApiKeyDto: CreateApiKeyDTO, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.apiKeyService.create({ + name: createApiKeyDto.name, + expiresAt: new Date(createApiKeyDto.expiresAt), + revokedAt: createApiKeyDto.revokedAt + ? new Date(createApiKeyDto.revokedAt) + : undefined, + workspaceId: workspace.id, + }); + } + + @Patch(':id') + async update( + @Param('id') id: string, + @Body() updateApiKeyDto: UpdateApiKeyDTO, + @AuthWorkspace() workspace: Workspace, + ): Promise { + const updateData: Partial = {}; + + if (updateApiKeyDto.name !== undefined) + updateData.name = updateApiKeyDto.name; + if (updateApiKeyDto.expiresAt !== undefined) + updateData.expiresAt = new Date(updateApiKeyDto.expiresAt); + if (updateApiKeyDto.revokedAt !== undefined) { + updateData.revokedAt = updateApiKeyDto.revokedAt + ? new Date(updateApiKeyDto.revokedAt) + : undefined; + } + + return this.apiKeyService.update(id, workspace.id, updateData); + } + + @Delete(':id') + async remove( + @Param('id') id: string, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.apiKeyService.revoke(id, workspace.id); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts index 69cc16f97..1666013cf 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { OpenApiController } from 'src/engine/core-modules/open-api/open-api.controller'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; -import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; @Module({ - imports: [ObjectMetadataModule, AuthModule], + imports: [ObjectMetadataModule, AuthModule, FeatureFlagModule], controllers: [OpenApiController], providers: [OpenApiService], }) diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts index 3da44988c..4b704044e 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; @@ -24,11 +25,16 @@ describe('OpenApiService', () => { provide: TwentyConfigService, useValue: {}, }, + { + provide: FeatureFlagService, + useValue: {}, + }, ], }).compile(); service = module.get(OpenApiService); }); + it('should be defined', () => { expect(service).toBeDefined(); }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index 5ee25e7f7..442b498a4 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -6,6 +6,7 @@ import { capitalize } from 'twenty-shared/utils'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { baseSchema } from 'src/engine/core-modules/open-api/utils/base-schema.utils'; import { computeMetadataSchemaComponents, @@ -36,9 +37,11 @@ import { getUpdateOneResponse200, } from 'src/engine/core-modules/open-api/utils/responses.utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { getServerUrl } from 'src/utils/get-server-url'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; +import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; +import { getServerUrl } from 'src/utils/get-server-url'; @Injectable() export class OpenApiService { @@ -46,6 +49,7 @@ export class OpenApiService { private readonly accessTokenService: AccessTokenService, private readonly twentyConfigService: TwentyConfigService, private readonly objectMetadataService: ObjectMetadataService, + private readonly featureFlagService: FeatureFlagService, ) {} async generateCoreSchema(request: Request): Promise { @@ -57,11 +61,13 @@ export class OpenApiService { const schema = baseSchema('core', baseUrl); let objectMetadataItems; + let workspace; try { - const { workspace } = + const authResult = await this.accessTokenService.validateTokenByRequest(request); + workspace = authResult.workspace; workspaceValidator.assertIsDefinedOrThrow(workspace); objectMetadataItems = @@ -77,7 +83,19 @@ export class OpenApiService { if (!objectMetadataItems.length) { return schema; } - schema.paths = objectMetadataItems.reduce((paths, item) => { + + const workspaceFeatureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); + + const filteredObjectMetadataItems = objectMetadataItems.filter((item) => { + return !shouldExcludeFromWorkspaceApi( + item, + standardObjectMetadataDefinitions, + workspaceFeatureFlagsMap, + ); + }); + + schema.paths = filteredObjectMetadataItems.reduce((paths, item) => { paths[`/${item.namePlural}`] = computeManyResultPath(item); paths[`/batch/${item.namePlural}`] = computeBatchPath(item); paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item); @@ -120,7 +138,7 @@ export class OpenApiService { schema.components = { ...schema.components, // components.securitySchemes is defined in base Schema - schemas: computeSchemaComponents(objectMetadataItems), + schemas: computeSchemaComponents(filteredObjectMetadataItems), parameters: computeParameterComponents(), responses: { '400': get400ErrorResponses(), @@ -128,6 +146,8 @@ export class OpenApiService { }, }; + schema.tags = computeSchemaTags(filteredObjectMetadataItems); + return schema; } @@ -152,6 +172,14 @@ export class OpenApiService { nameSingular: 'field', namePlural: 'fields', }, + { + nameSingular: 'webhook', + namePlural: 'webhooks', + }, + { + nameSingular: 'apikey', + namePlural: 'apiKeys', + }, ]; schema.paths = metadata.reduce((path, item) => { diff --git a/packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts b/packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts new file mode 100644 index 000000000..a34a53951 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/webhook/controllers/webhook.controller.ts @@ -0,0 +1,78 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + UseFilters, + UseGuards, +} from '@nestjs/common'; + +import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter'; +import { CreateWebhookDTO } from 'src/engine/core-modules/webhook/dtos/create-webhook.dto'; +import { UpdateWebhookDTO } from 'src/engine/core-modules/webhook/dtos/update-webhook.dto'; +import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity'; +import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +/** + * rest/webhooks is deprecated, use rest/metadata/webhooks instead + * rest/webhooks will be removed in the future + */ +@Controller(['rest/webhooks', 'rest/metadata/webhooks']) +@UseGuards(JwtAuthGuard, WorkspaceAuthGuard) +@UseFilters(RestApiExceptionFilter) +export class WebhookController { + constructor(private readonly webhookService: WebhookService) {} + + @Get() + async findAll(@AuthWorkspace() workspace: Workspace): Promise { + return this.webhookService.findByWorkspaceId(workspace.id); + } + + @Get(':id') + async findOne( + @Param('id') id: string, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.webhookService.findById(id, workspace.id); + } + + @Post() + async create( + @Body() createWebhookDto: CreateWebhookDTO, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.webhookService.create({ + targetUrl: createWebhookDto.targetUrl, + operations: createWebhookDto.operations || ['*.*'], + description: createWebhookDto.description, + secret: createWebhookDto.secret, + workspaceId: workspace.id, + }); + } + + @Patch(':id') + async update( + @Param('id') id: string, + @Body() updateWebhookDto: UpdateWebhookDTO, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.webhookService.update(id, workspace.id, updateWebhookDto); + } + + @Delete(':id') + async remove( + @Param('id') id: string, + @AuthWorkspace() workspace: Workspace, + ): Promise { + const result = await this.webhookService.delete(id, workspace.id); + + return result !== null; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/webhook/webhook.module.ts b/packages/twenty-server/src/engine/core-modules/webhook/webhook.module.ts index 2dfa413aa..a16b4e4d0 100644 --- a/packages/twenty-server/src/engine/core-modules/webhook/webhook.module.ts +++ b/packages/twenty-server/src/engine/core-modules/webhook/webhook.module.ts @@ -1,13 +1,22 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Webhook } from './webhook.entity'; -import { WebhookResolver } from './webhook.resolver'; -import { WebhookService } from './webhook.service'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity'; +import { WebhookResolver } from 'src/engine/core-modules/webhook/webhook.resolver'; +import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; + +import { WebhookController } from './controllers/webhook.controller'; @Module({ - imports: [TypeOrmModule.forFeature([Webhook], 'core')], + imports: [ + TypeOrmModule.forFeature([Webhook], 'core'), + AuthModule, + WorkspaceCacheStorageModule, + ], providers: [WebhookService, WebhookResolver], + controllers: [WebhookController], exports: [WebhookService, TypeOrmModule], }) export class WebhookModule {} diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-gate.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-gate.decorator.ts index 2a757df96..c11c84c5d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-gate.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-gate.decorator.ts @@ -5,7 +5,7 @@ import { TypedReflect } from 'src/utils/typed-reflect'; export interface WorkspaceGateOptions { featureFlag: string; excludeFromDatabase?: boolean; - excludeFromGraphQL?: boolean; + excludeFromWorkspaceApi?: boolean; } export function WorkspaceGate(options: WorkspaceGateOptions) { @@ -21,7 +21,7 @@ export function WorkspaceGate(options: WorkspaceGateOptions) { const gateOptions = { featureFlag: options.featureFlag, excludeFromDatabase: options.excludeFromDatabase ?? true, - excludeFromGraphQL: options.excludeFromGraphQL ?? true, + excludeFromWorkspaceApi: options.excludeFromWorkspaceApi ?? true, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/gate.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/gate.interface.ts index 9581a2413..9485e4aa5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/gate.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/gate.interface.ts @@ -1,5 +1,5 @@ export interface Gate { featureFlag: string; excludeFromDatabase?: boolean; - excludeFromGraphQL?: boolean; + excludeFromWorkspaceApi?: boolean; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util.ts index 442224611..894c82639 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util.ts @@ -1,6 +1,6 @@ import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; -export type GateContext = 'database' | 'graphql'; +export type GateContext = 'database' | 'workspaceApi'; export const isGatedAndNotEnabled = ( gate: Gate | undefined, @@ -19,9 +19,9 @@ export const isGatedAndNotEnabled = ( return false; // Not gated for database } break; - case 'graphql': - if (gate.excludeFromGraphQL === false) { - return false; // Not gated for GraphQL + case 'workspaceApi': + if (gate.excludeFromWorkspaceApi === false) { + return false; // Not gated for workspace API } break; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util.ts new file mode 100644 index 000000000..14ecf6a8b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util.ts @@ -0,0 +1,23 @@ +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; +import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; + +export const shouldExcludeFromWorkspaceApi = ( + objectMetadataItem: { standardId?: string | null | undefined }, + standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], + workspaceFeatureFlagsMap: Record, +): boolean => { + const entityMetadata = standardObjectMetadataDefinitions + .map((entity) => metadataArgsStorage.filterEntities(entity)) + .find((meta) => meta?.standardId === objectMetadataItem.standardId); + + if (!entityMetadata) { + return false; // Don't exclude non-workspace entities + } + + return isGatedAndNotEnabled( + entityMetadata?.gate, + workspaceFeatureFlagsMap, + 'workspaceApi', + ); +}; diff --git a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts index 5578c823b..a3c0b6fe6 100644 --- a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts +++ b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts @@ -24,7 +24,7 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync @WorkspaceGate({ featureFlag: 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED', excludeFromDatabase: false, - excludeFromGraphQL: true, + excludeFromWorkspaceApi: true, }) export class ApiKeyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ diff --git a/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts b/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts index 796dcd0d5..8411521b0 100644 --- a/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts +++ b/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts @@ -25,7 +25,7 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync @WorkspaceGate({ featureFlag: 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED', excludeFromDatabase: false, - excludeFromGraphQL: true, + excludeFromWorkspaceApi: true, }) export class WebhookWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({