diff --git a/.github/workflows/ci-breaking-changes.yaml b/.github/workflows/ci-breaking-changes.yaml index 4d8b1073e6..b9a291d0da 100644 --- a/.github/workflows/ci-breaking-changes.yaml +++ b/.github/workflows/ci-breaking-changes.yaml @@ -1,7 +1,10 @@ name: GraphQL and OpenAPI Breaking Changes Detection on: - pull_request: + # Using pull_request_target instead of pull_request to have access to secrets for external contributors + # Security note: This is safe because we're only analyzing API schemas and posting comments, + # not running untrusted code from the PR + pull_request_target: types: [opened, synchronize, edited] branches: - main @@ -18,6 +21,7 @@ permissions: contents: read pull-requests: write checks: write + issues: write jobs: changed-files-check: @@ -581,6 +585,7 @@ jobs: if: always() uses: actions/github-script@v7 with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); let hasChanges = false; 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 719edec5b6..631b9827e3 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 @@ -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; } diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index d15cff09ce..5a608aa3f9 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -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; } }