fix: migrate webhook and API key REST endpoints to core schema (#13318)

## 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
This commit is contained in:
nitin
2025-07-23 18:41:53 +05:30
committed by GitHub
parent 05a09d7a73
commit 0e561e4ef4
17 changed files with 302 additions and 68 deletions
@@ -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],
})
@@ -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>(OpenApiService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
@@ -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<OpenAPIV3_1.Document> {
@@ -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) => {