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:
@ -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
|
||||
|
||||
@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
],
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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<ApiKey[]> {
|
||||
return this.apiKeyService.findActiveByWorkspaceId(workspace.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ApiKey | null> {
|
||||
return this.apiKeyService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() createApiKeyDto: CreateApiKeyDTO,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ApiKey> {
|
||||
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<ApiKey | null> {
|
||||
const updateData: Partial<ApiKey> = {};
|
||||
|
||||
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<ApiKey | null> {
|
||||
return this.apiKeyService.revoke(id, workspace.id);
|
||||
}
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
@ -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<Webhook[]> {
|
||||
return this.webhookService.findByWorkspaceId(workspace.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<Webhook | null> {
|
||||
return this.webhookService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() createWebhookDto: CreateWebhookDTO,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<Webhook> {
|
||||
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<Webhook | null> {
|
||||
return this.webhookService.update(id, workspace.id, updateWebhookDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<boolean> {
|
||||
const result = await this.webhookService.delete(id, workspace.id);
|
||||
|
||||
return result !== null;
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface Gate {
|
||||
featureFlag: string;
|
||||
excludeFromDatabase?: boolean;
|
||||
excludeFromGraphQL?: boolean;
|
||||
excludeFromWorkspaceApi?: boolean;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<string, boolean>,
|
||||
): 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',
|
||||
);
|
||||
};
|
||||
@ -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({
|
||||
|
||||
@ -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({
|
||||
|
||||
Reference in New Issue
Block a user