fix api mismatch on rest metadata (#13680)

## Context

This PR fixes a REST API metadata schema mismatch that occurred after
the webhook and apiKey migration from workspace entities to metadata
level.

## The Issue

After PR #13576 deleted the webhook and apiKey workspace entities, the
metadata OpenAPI specification ended up in a broken state:
- The `/metadata` endpoints still had paths for `/webhooks` and
`/apiKeys`
- But the schema definitions were missing (they were previously pulled
from workspace entities)
- This created dangling references in the OpenAPI document

## The Fix

Added proper schema definitions for webhook and apiKey directly in
`computeMetadataSchemaComponents`:
- `Webhook`, `WebhookForUpdate`, `WebhookForResponse`
- `ApiKey`, `ApiKeyForUpdate`, `ApiKeyForResponse`

## Why the CI is Failing

The CI breaking changes detection is comparing:
- **Main branch**: Has a broken OpenAPI document with endpoints that
reference non-existent schemas
- **This branch**: Has the fixed OpenAPI document with proper schemas

The OpenAPI diff tool can't even parse the main branch's document due to
the missing schema references, hence the error -- tested locally
This commit is contained in:
nitin
2025-08-06 16:15:05 +05:30
committed by GitHub
parent 8169ba10a5
commit 60257a9701
3 changed files with 126 additions and 13 deletions
@@ -37,12 +37,13 @@ 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable()
export class OpenApiService {
@@ -264,18 +265,9 @@ export class OpenApiService {
return path;
}, schema.paths as OpenAPIV3_1.PathsObject);
const objectMetadataItems = await this.getObjectMetadataItems(workspace);
const webhookAndApiKeyObjectMetadataItems = objectMetadataItems.filter(
({ nameSingular }) => ['webhook', 'apiKey'].includes(nameSingular),
);
schema.components = {
...schema.components, // components.securitySchemes is defined in base Schema
schemas: {
...computeMetadataSchemaComponents(metadata),
...computeSchemaComponents(webhookAndApiKeyObjectMetadataItems),
},
schemas: computeMetadataSchemaComponents(metadata),
parameters: computeParameterComponents(true),
responses: {
'400': get400ErrorResponses(),
@@ -283,7 +275,12 @@ export class OpenApiService {
},
};
schema.tags = computeSchemaTags(webhookAndApiKeyObjectMetadataItems);
schema.tags = computeSchemaTags(
metadata.map((item) => ({
nameSingular: item.nameSingular,
namePlural: item.namePlural,
})) as ObjectMetadataEntity[],
);
return schema;
}
@@ -382,6 +382,117 @@ export const computeMetadataSchemaComponents = (
},
};
return schemas;
}
case 'webhook': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
description: `A webhook`,
properties: {
targetUrl: { type: 'string' },
operations: {
type: 'array',
items: { type: 'string' },
default: ['*.*'],
},
description: { type: 'string' },
secret: { type: 'string' },
},
required: ['targetUrl'],
};
schemas[`${capitalize(item.namePlural)}`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
};
schemas[`${capitalize(item.nameSingular)}ForUpdate`] = {
type: 'object',
description: `A webhook for update`,
properties: {
targetUrl: { type: 'string' },
operations: {
type: 'array',
items: { type: 'string' },
},
description: { type: 'string' },
},
};
schemas[`${capitalize(item.nameSingular)}ForResponse`] = {
type: 'object',
description: `A webhook`,
properties: {
id: { type: 'string', format: 'uuid' },
targetUrl: { type: 'string' },
operations: {
type: 'array',
items: { type: 'string' },
},
description: { type: 'string' },
workspaceId: { type: 'string', format: 'uuid' },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
deletedAt: { type: 'string', format: 'date-time' },
},
};
schemas[`${capitalize(item.namePlural)}ForResponse`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`,
},
};
return schemas;
}
case 'apiKey': {
schemas[`${capitalize(item.nameSingular)}`] = {
type: 'object',
description: `An API key`,
properties: {
name: { type: 'string' },
expiresAt: { type: 'string', format: 'date-time' },
},
required: ['name', 'expiresAt'],
};
schemas[`${capitalize(item.namePlural)}`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}`,
},
};
schemas[`${capitalize(item.nameSingular)}ForUpdate`] = {
type: 'object',
description: `An API key for update`,
properties: {
name: { type: 'string' },
expiresAt: { type: 'string', format: 'date-time' },
revokedAt: { type: 'string', format: 'date-time' },
},
};
schemas[`${capitalize(item.nameSingular)}ForResponse`] = {
type: 'object',
description: `An API key`,
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
expiresAt: { type: 'string', format: 'date-time' },
revokedAt: { type: 'string', format: 'date-time' },
workspaceId: { type: 'string', format: 'uuid' },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
};
schemas[`${capitalize(item.namePlural)}ForResponse`] = {
type: 'array',
description: `A list of ${item.namePlural}`,
items: {
$ref: `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`,
},
};
return schemas;
}
}