From c55dfbde6e3e2065c15e00aed7fd7fe6698ad796 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 4 Sep 2024 17:25:59 +0200 Subject: [PATCH] Fix unauthorized error handling (#6835) from @BOHEUS comments in #6640 - fix bad 500 error when authentication invalid - remove "id", "createdAt", "updatedAt", etc from creation and update paths schema - improve error message - remove "id" from test body - improve secondaryLink schema description - improve depth parameter description - remove required from response body - improve examples - improve error message formatting - fix filter by position - answered to negative float position @BOHEUS comment Also: - fix secondary field openapi field description - remove schema display in playground Screenshots ![image](https://github.com/user-attachments/assets/a5d52afd-ab10-49f3-8806-ee41b04bc775) ![image](https://github.com/user-attachments/assets/33f985bb-ff75-42f6-a0bb-741bd32a1d08) --- .../__mocks__/object-metadata-item.mock.ts | 18 +- .../rest-api-core-batch.controller.ts | 4 +- .../controllers/rest-api-core.controller.ts | 3 + .../filter-utils/format-field-values.utils.ts | 14 +- .../api/rest/errors/RestApiException.ts | 28 +- .../utils/fetch-metadata-fields.utils.ts | 63 ++- .../core-modules/open-api/open-api.service.ts | 9 +- .../utils/__tests__/components.utils.spec.ts | 456 ++++++++++++++--- .../utils/__tests__/parameters.utils.spec.ts | 5 +- .../open-api/utils/components.utils.ts | 462 ++++++++++++------ .../utils/get-error-responses.utils.ts | 2 +- .../open-api/utils/parameters.utils.ts | 5 +- .../core-modules/open-api/utils/path.utils.ts | 3 +- .../open-api/utils/request-body.utils.ts | 18 +- .../open-api/utils/responses.utils.ts | 107 ++-- .../playground/rest-api-wrapper.tsx | 6 +- 16 files changed, 863 insertions(+), 340 deletions(-) diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index ab348dd176..399f096155 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -156,7 +156,23 @@ const fieldRatingMock = { name: 'fieldRating', type: FieldMetadataType.RATING, isNullable: true, - defaultValue: null, + defaultValue: 'RATING_1', + options: [ + { + id: '9a519a86-422b-4598-88ae-78751353f683', + color: 'red', + label: 'Opt 1', + value: 'RATING_1', + position: 0, + }, + { + id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', + color: 'purple', + label: 'Opt 2', + value: 'RATING_2', + position: 1, + }, + ], }; const fieldPositionMock = { diff --git a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core-batch.controller.ts b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core-batch.controller.ts index e77ae4c891..b43ef55dd2 100644 --- a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core-batch.controller.ts +++ b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core-batch.controller.ts @@ -1,11 +1,13 @@ -import { Controller, Post, Req, Res } from '@nestjs/common'; +import { Controller, Post, Req, Res, UseGuards } from '@nestjs/common'; import { Request, Response } from 'express'; import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service'; import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; @Controller('rest/batch/*') +@UseGuards(JwtAuthGuard) export class RestApiCoreBatchController { constructor(private readonly restApiCoreService: RestApiCoreService) {} diff --git a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts index 766f823522..b23e9ab6f6 100644 --- a/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts +++ b/packages/twenty-server/src/engine/api/rest/core/controllers/rest-api-core.controller.ts @@ -7,14 +7,17 @@ import { Put, Req, Res, + UseGuards, } from '@nestjs/common'; import { Request, Response } from 'express'; import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service'; import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; @Controller('rest/*') +@UseGuards(JwtAuthGuard) export class RestApiCoreController { constructor(private readonly restApiCoreService: RestApiCoreService) {} diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts index 79e351f941..e884ebf4b6 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts @@ -23,12 +23,16 @@ export const formatFieldValue = ( if (comparator === 'is') { return value; } - if (fieldType === FieldMetadataType.NUMBER) { - return parseInt(value); - } - if (fieldType === FieldMetadataType.BOOLEAN) { - return value.toLowerCase() === 'true'; + switch (fieldType) { + case FieldMetadataType.NUMERIC: + return parseInt(value); + case FieldMetadataType.NUMBER: + case FieldMetadataType.POSITION: + return parseFloat(value); + case FieldMetadataType.BOOLEAN: + return value.toLowerCase() === 'true'; } + if ( (value[0] === '"' || value[0] === "'") && (value.charAt(value.length - 1) === '"' || diff --git a/packages/twenty-server/src/engine/api/rest/errors/RestApiException.ts b/packages/twenty-server/src/engine/api/rest/errors/RestApiException.ts index 2cc0b571b7..9e900e3b7e 100644 --- a/packages/twenty-server/src/engine/api/rest/errors/RestApiException.ts +++ b/packages/twenty-server/src/engine/api/rest/errors/RestApiException.ts @@ -2,22 +2,34 @@ import { BadRequestException } from '@nestjs/common'; import { BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -const formatMessage = (message: BaseGraphQLError) => { - if (message.extensions) { - return message.extensions.response.message || message.extensions.response; +const formatMessage = (error: BaseGraphQLError) => { + let formattedMessage = error.extensions + ? error.extensions.response?.error || + error.extensions.response || + error.message + : error.error; + + formattedMessage = formattedMessage + .replace(/"/g, "'") + .replace("Variable '$data' got i", 'I') + .replace("Variable '$input' got i", 'I'); + + const regex = /Field '[^']+' is not defined by type .*/; + + const match = formattedMessage.match(regex); + + if (match) { + formattedMessage = match[0]; } - return message.message; + return formattedMessage; }; export class RestApiException extends BadRequestException { constructor(errors: BaseGraphQLError[]) { super({ statusCode: 400, - message: - errors.length === 1 - ? formatMessage(errors[0]) - : JSON.stringify(errors.map((error) => formatMessage(error))), + messages: errors.map((error) => formatMessage(error)), error: 'Bad Request', }); } diff --git a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts index 61899096d2..718a21e6ad 100644 --- a/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/metadata/query-builder/utils/fetch-metadata-fields.utils.ts @@ -1,4 +1,28 @@ export const fetchMetadataFields = (objectNamePlural: string) => { + const fromRelations = ` + toObjectMetadata { + id + dataSourceId + nameSingular + namePlural + isSystem + isRemote + } + toFieldMetadataId + `; + + const toRelations = ` + fromObjectMetadata { + id + dataSourceId + nameSingular + namePlural + isSystem + isRemote + } + fromFieldMetadataId + `; + const fields = ` type name @@ -14,26 +38,12 @@ export const fetchMetadataFields = (objectNamePlural: string) => { fromRelationMetadata { id relationType - toObjectMetadata { - id - dataSourceId - nameSingular - namePlural - isSystem - } - toFieldMetadataId + ${fromRelations} } toRelationMetadata { id relationType - fromObjectMetadata { - id - dataSourceId - nameSingular - namePlural - isSystem - } - fromFieldMetadataId + ${toRelations} } defaultValue options @@ -69,25 +79,10 @@ export const fetchMetadataFields = (objectNamePlural: string) => { return fields; case 'relations': return ` + id relationType - fromObjectMetadata { - id - dataSourceId - nameSingular - namePlural - isSystem - } - fromObjectMetadataId - toObjectMetadata { - id - dataSourceId - nameSingular - namePlural - isSystem - } - toObjectMetadataId - fromFieldMetadataId - toFieldMetadataId + ${fromRelations} + ${toRelations} `; } }; 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 0c73f73ae2..bdcd710fa4 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 @@ -22,7 +22,10 @@ import { computeManyResultPath, computeSingleResultPath, } from 'src/engine/core-modules/open-api/utils/path.utils'; -import { getRequestBody } from 'src/engine/core-modules/open-api/utils/request-body.utils'; +import { + getRequestBody, + getUpdateRequestBody, +} from 'src/engine/core-modules/open-api/utils/request-body.utils'; import { getCreateOneResponse201, getDeleteResponse200, @@ -165,7 +168,7 @@ export class OpenApiService { summary: `Find One ${item.nameSingular}`, parameters: [{ $ref: '#/components/parameters/idPath' }], responses: { - '200': getFindOneResponse200(item, true), + '200': getFindOneResponse200(item), '400': { $ref: '#/components/responses/400' }, '401': { $ref: '#/components/responses/401' }, }, @@ -187,7 +190,7 @@ export class OpenApiService { summary: `Update One ${item.nameSingular}`, operationId: `updateOne${capitalize(item.nameSingular)}`, parameters: [{ $ref: '#/components/parameters/idPath' }], - requestBody: getRequestBody(capitalize(item.nameSingular)), + requestBody: getUpdateRequestBody(capitalize(item.nameSingular)), responses: { '200': getUpdateOneResponse200(item, true), '400': { $ref: '#/components/responses/400' }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index e481a23438..acabfb71fe 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -22,9 +22,6 @@ describe('computeSchemaComponents', () => { ).toEqual({ ObjectName: { type: 'object', - description: undefined, - required: ['fieldNumber'], - example: { fieldNumber: '' }, properties: { fieldUuid: { type: 'string', @@ -40,9 +37,20 @@ describe('computeSchemaComponents', () => { type: 'string', format: 'email', }, + fieldEmails: { + type: 'object', + properties: { + primaryEmail: { + type: 'string', + }, + additionalEmails: { + type: 'object', + }, + }, + }, fieldDateTime: { type: 'string', - format: 'date', + format: 'date-time', }, fieldDate: { type: 'string', @@ -58,21 +66,44 @@ describe('computeSchemaComponents', () => { type: 'number', }, fieldLinks: { - properties: { - primaryLinkLabel: { type: 'string' }, - primaryLinkUrl: { type: 'string' }, - secondaryLinks: { type: 'object' }, - }, type: 'object', + properties: { + primaryLinkLabel: { + type: 'string', + }, + primaryLinkUrl: { + type: 'string', + }, + secondaryLinks: { + type: 'array', + items: { + type: 'object', + description: 'A secondary link', + properties: { + url: { + type: 'string', + }, + label: { + type: 'string', + }, + }, + }, + }, + }, }, fieldCurrency: { - properties: { - amountMicros: { type: 'number' }, - currencyCode: { type: 'string' }, - }, type: 'object', + properties: { + amountMicros: { + type: 'number', + }, + currencyCode: { + type: 'string', + }, + }, }, fieldFullName: { + type: 'object', properties: { firstName: { type: 'string', @@ -81,10 +112,10 @@ describe('computeSchemaComponents', () => { type: 'string', }, }, - type: 'object', }, fieldRating: { - type: 'number', + type: 'string', + enum: ['RATING_1', 'RATING_2'], }, fieldSelect: { type: 'string', @@ -98,10 +129,23 @@ describe('computeSchemaComponents', () => { type: 'number', }, fieldAddress: { + type: 'object', properties: { + addressStreet1: { + type: 'string', + }, + addressStreet2: { + type: 'string', + }, addressCity: { type: 'string', }, + addressPostcode: { + type: 'string', + }, + addressState: { + type: 'string', + }, addressCountry: { type: 'string', }, @@ -111,20 +155,7 @@ describe('computeSchemaComponents', () => { addressLng: { type: 'number', }, - addressPostcode: { - type: 'string', - }, - addressState: { - type: 'string', - }, - addressStreet1: { - type: 'string', - }, - addressStreet2: { - type: 'string', - }, }, - type: 'object', }, fieldRawJson: { type: 'object', @@ -133,9 +164,341 @@ describe('computeSchemaComponents', () => { type: 'string', }, fieldActor: { + type: 'object', properties: { source: { type: 'string', + enum: [ + 'EMAIL', + 'CALENDAR', + 'WORKFLOW', + 'API', + 'IMPORT', + 'MANUAL', + ], + }, + }, + }, + }, + required: ['fieldNumber'], + }, + 'ObjectName for Update': { + type: 'object', + properties: { + fieldUuid: { + type: 'string', + format: 'uuid', + }, + fieldText: { + type: 'string', + }, + fieldPhone: { + type: 'string', + }, + fieldEmail: { + type: 'string', + format: 'email', + }, + fieldEmails: { + type: 'object', + properties: { + primaryEmail: { + type: 'string', + }, + additionalEmails: { + type: 'object', + }, + }, + }, + fieldDateTime: { + type: 'string', + format: 'date-time', + }, + fieldDate: { + type: 'string', + format: 'date', + }, + fieldBoolean: { + type: 'boolean', + }, + fieldNumber: { + type: 'integer', + }, + fieldNumeric: { + type: 'number', + }, + fieldLinks: { + type: 'object', + properties: { + primaryLinkLabel: { + type: 'string', + }, + primaryLinkUrl: { + type: 'string', + }, + secondaryLinks: { + type: 'array', + items: { + type: 'object', + description: 'A secondary link', + properties: { + url: { + type: 'string', + }, + label: { + type: 'string', + }, + }, + }, + }, + }, + }, + fieldCurrency: { + type: 'object', + properties: { + amountMicros: { + type: 'number', + }, + currencyCode: { + type: 'string', + }, + }, + }, + fieldFullName: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + lastName: { + type: 'string', + }, + }, + }, + fieldRating: { + type: 'string', + enum: ['RATING_1', 'RATING_2'], + }, + fieldSelect: { + type: 'string', + enum: ['OPTION_1', 'OPTION_2'], + }, + fieldMultiSelect: { + type: 'string', + enum: ['OPTION_1', 'OPTION_2'], + }, + fieldPosition: { + type: 'number', + }, + fieldAddress: { + type: 'object', + properties: { + addressStreet1: { + type: 'string', + }, + addressStreet2: { + type: 'string', + }, + addressCity: { + type: 'string', + }, + addressPostcode: { + type: 'string', + }, + addressState: { + type: 'string', + }, + addressCountry: { + type: 'string', + }, + addressLat: { + type: 'number', + }, + addressLng: { + type: 'number', + }, + }, + }, + fieldRawJson: { + type: 'object', + }, + fieldRichText: { + type: 'string', + }, + fieldActor: { + type: 'object', + properties: { + source: { + type: 'string', + enum: [ + 'EMAIL', + 'CALENDAR', + 'WORKFLOW', + 'API', + 'IMPORT', + 'MANUAL', + ], + }, + }, + }, + }, + }, + 'ObjectName for Response': { + type: 'object', + properties: { + fieldUuid: { + type: 'string', + format: 'uuid', + }, + fieldText: { + type: 'string', + }, + fieldPhone: { + type: 'string', + }, + fieldEmail: { + type: 'string', + format: 'email', + }, + fieldEmails: { + type: 'object', + properties: { + primaryEmail: { + type: 'string', + }, + additionalEmails: { + type: 'object', + }, + }, + }, + fieldDateTime: { + type: 'string', + format: 'date-time', + }, + fieldDate: { + type: 'string', + format: 'date', + }, + fieldBoolean: { + type: 'boolean', + }, + fieldNumber: { + type: 'integer', + }, + fieldNumeric: { + type: 'number', + }, + fieldLinks: { + type: 'object', + properties: { + primaryLinkLabel: { + type: 'string', + }, + primaryLinkUrl: { + type: 'string', + }, + secondaryLinks: { + type: 'array', + items: { + type: 'object', + description: 'A secondary link', + properties: { + url: { + type: 'string', + }, + label: { + type: 'string', + }, + }, + }, + }, + }, + }, + fieldCurrency: { + type: 'object', + properties: { + amountMicros: { + type: 'number', + }, + currencyCode: { + type: 'string', + }, + }, + }, + fieldFullName: { + type: 'object', + properties: { + firstName: { + type: 'string', + }, + lastName: { + type: 'string', + }, + }, + }, + fieldRating: { + type: 'string', + enum: ['RATING_1', 'RATING_2'], + }, + fieldSelect: { + type: 'string', + enum: ['OPTION_1', 'OPTION_2'], + }, + fieldMultiSelect: { + type: 'string', + enum: ['OPTION_1', 'OPTION_2'], + }, + fieldPosition: { + type: 'number', + }, + fieldAddress: { + type: 'object', + properties: { + addressStreet1: { + type: 'string', + }, + addressStreet2: { + type: 'string', + }, + addressCity: { + type: 'string', + }, + addressPostcode: { + type: 'string', + }, + addressState: { + type: 'string', + }, + addressCountry: { + type: 'string', + }, + addressLat: { + type: 'number', + }, + addressLng: { + type: 'number', + }, + }, + }, + fieldRawJson: { + type: 'object', + }, + fieldRichText: { + type: 'string', + }, + fieldActor: { + type: 'object', + properties: { + source: { + type: 'string', + enum: [ + 'EMAIL', + 'CALENDAR', + 'WORKFLOW', + 'API', + 'IMPORT', + 'MANUAL', + ], }, workspaceMemberId: { type: 'string', @@ -145,44 +508,15 @@ describe('computeSchemaComponents', () => { type: 'string', }, }, - type: 'object', }, - fieldEmails: { - properties: { - primaryEmail: { - type: 'string', - }, - additionalEmails: { - type: 'object', - }, + fieldRelation: { + type: 'array', + items: { + $ref: '#/components/schemas/ToObjectMetadataName for Response', }, - type: 'object', }, }, }, - 'ObjectName with Relations': { - allOf: [ - { - $ref: '#/components/schemas/ObjectName', - }, - { - properties: { - fieldRelation: { - type: 'array', - items: { - $ref: '#/components/schemas/ToObjectMetadataName', - }, - }, - }, - type: 'object', - }, - ], - description: undefined, - example: { - fieldNumber: '', - }, - required: ['fieldNumber'], - }, }); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts index a4f6b6fcc0..0a15ff0fd1 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts @@ -64,7 +64,10 @@ describe('computeParameters', () => { expect(computeDepthParameters()).toEqual({ name: 'depth', in: 'query', - description: 'Limits the depth objects returned.', + description: `Determines the level of nested related objects to include in the response. + - 0: Returns only the primary object's information. + - 1: Returns the primary object along with its directly related objects (with no additional nesting for related objects). + - 2: Returns the primary object, its directly related objects, and the related objects of those related objects.`, required: false, schema: { type: 'integer', 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 cc5c91065f..4b9d7fc496 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 @@ -1,5 +1,7 @@ import { OpenAPIV3_1 } from 'openapi-types'; +import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; + import { computeDepthParameters, computeEndingBeforeParameters, @@ -10,9 +12,13 @@ import { computeStartingAfterParameters, } from 'src/engine/core-modules/open-api/utils/parameters.utils'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { capitalize } from 'src/utils/capitalize'; +import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; type Property = OpenAPIV3_1.SchemaObject; @@ -20,8 +26,33 @@ type Properties = { [name: string]: Property; }; -const getFieldProperties = (type: FieldMetadataType): Property => { +const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => { + if (forResponse) { + return true; + } + switch (field.name) { + case 'id': + case 'createdAt': + case 'updatedAt': + case 'deletedAt': + return false; + default: + return true; + } +}; + +const getFieldProperties = ( + type: FieldMetadataType, + propertyName?: string, + options?: FieldMetadataOptions, +): Property => { switch (type) { + case FieldMetadataType.SELECT: + case FieldMetadataType.MULTI_SELECT: + return { + type: 'string', + enum: options?.map((option: { value: string }) => option.value), + }; case FieldMetadataType.UUID: return { type: 'string', format: 'uuid' }; case FieldMetadataType.TEXT: @@ -31,28 +62,55 @@ const getFieldProperties = (type: FieldMetadataType): Property => { case FieldMetadataType.EMAIL: return { type: 'string', format: 'email' }; case FieldMetadataType.DATE_TIME: + return { type: 'string', format: 'date-time' }; case FieldMetadataType.DATE: return { type: 'string', format: 'date' }; case FieldMetadataType.NUMBER: return { type: 'integer' }; - case FieldMetadataType.NUMERIC: case FieldMetadataType.RATING: + return { + type: 'string', + enum: options?.map((option: { value: string }) => option.value), + }; + case FieldMetadataType.NUMERIC: case FieldMetadataType.POSITION: return { type: 'number' }; case FieldMetadataType.BOOLEAN: return { type: 'boolean' }; case FieldMetadataType.RAW_JSON: + if (propertyName === 'secondaryLinks') { + return { + type: 'array', + items: { + type: 'object', + description: `A secondary link`, + properties: { + url: { type: 'string' }, + label: { type: 'string' }, + }, + }, + }; + } + return { type: 'object' }; + default: return { type: 'string' }; } }; -const getSchemaComponentsProperties = ( - item: ObjectMetadataEntity, -): Properties => { +const getSchemaComponentsProperties = ({ + item, + forResponse, +}: { + item: ObjectMetadataEntity; + forResponse: boolean; +}): Properties => { return item.fields.reduce((node, field) => { - if (field.type == FieldMetadataType.RELATION) { + if ( + !isFieldAvailable(field, forResponse) || + field.type === FieldMetadataType.RELATION + ) { return node; } @@ -66,6 +124,12 @@ const getSchemaComponentsProperties = ( enum: field.options.map((option: { value: string }) => option.value), }; break; + case FieldMetadataType.RATING: + itemProperty = { + type: 'string', + enum: field.options.map((option: { value: string }) => option.value), + }; + break; case FieldMetadataType.LINK: case FieldMetadataType.LINKS: case FieldMetadataType.CURRENCY: @@ -78,7 +142,18 @@ const getSchemaComponentsProperties = ( properties: compositeTypeDefinitions .get(field.type) ?.properties?.reduce((properties, property) => { - properties[property.name] = getFieldProperties(property.type); + if ( + property.hidden === true || + (property.hidden === 'input' && !forResponse) || + (property.hidden === 'output' && forResponse) + ) { + return properties; + } + properties[property.name] = getFieldProperties( + property.type, + property.name, + property.options, + ); return properties; }, {} as Properties), @@ -105,19 +180,21 @@ const getSchemaComponentsRelationProperties = ( item: ObjectMetadataEntity, ): Properties => { return item.fields.reduce((node, field) => { + if (field.type !== FieldMetadataType.RELATION) { + return node; + } + let itemProperty = {} as Property; - if (field.type == FieldMetadataType.RELATION) { - if (field.fromRelationMetadata?.toObjectMetadata.nameSingular) { - itemProperty = { - type: 'array', - items: { - $ref: `#/components/schemas/${capitalize( - field.fromRelationMetadata?.toObjectMetadata.nameSingular || '', - )}`, - }, - }; - } + if (field.fromRelationMetadata?.toObjectMetadata.nameSingular) { + itemProperty = { + type: 'array', + items: { + $ref: `#/components/schemas/${capitalize( + field.fromRelationMetadata?.toObjectMetadata.nameSingular, + )} for Response`, + }, + }; } if (field.description) { @@ -144,62 +221,38 @@ const getRequiredFields = (item: ObjectMetadataEntity): string[] => { }, [] as string[]); }; -const computeSchemaComponent = ( - item: ObjectMetadataEntity, -): OpenAPIV3_1.SchemaObject => { +const computeSchemaComponent = ({ + item, + withRequiredFields, + forResponse, + withRelations, +}: { + item: ObjectMetadataEntity; + withRequiredFields: boolean; + forResponse: boolean; + withRelations: boolean; +}): OpenAPIV3_1.SchemaObject => { const result = { type: 'object', description: item.description, - properties: getSchemaComponentsProperties(item), - example: {}, + properties: getSchemaComponentsProperties({ item, forResponse }), } as OpenAPIV3_1.SchemaObject; - const requiredFields = getRequiredFields(item); - - if (requiredFields?.length) { - result.required = requiredFields; - result.example = requiredFields.reduce( - (example, requiredField) => { - example[requiredField] = ''; - - return example; - }, - {} as Record, - ); + if (withRelations) { + result.properties = { + ...result.properties, + ...getSchemaComponentsRelationProperties(item), + }; } - return result; -}; - -const computeRelationSchemaComponent = ( - item: ObjectMetadataEntity, -): OpenAPIV3_1.SchemaObject => { - const result = { - description: item.description, - allOf: [ - { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, - }, - { - type: 'object', - properties: getSchemaComponentsRelationProperties(item), - }, - ], - example: {}, - } as OpenAPIV3_1.SchemaObject; + if (!withRequiredFields) { + return result; + } const requiredFields = getRequiredFields(item); if (requiredFields?.length) { result.required = requiredFields; - result.example = requiredFields.reduce( - (example, requiredField) => { - example[requiredField] = ''; - - return example; - }, - {} as Record, - ); } return result; @@ -210,9 +263,26 @@ export const computeSchemaComponents = ( ): Record => { return objectMetadataItems.reduce( (schemas, item) => { - schemas[capitalize(item.nameSingular)] = computeSchemaComponent(item); - schemas[capitalize(item.nameSingular) + ' with Relations'] = - computeRelationSchemaComponent(item); + schemas[capitalize(item.nameSingular)] = computeSchemaComponent({ + item, + withRequiredFields: true, + forResponse: false, + withRelations: false, + }); + schemas[capitalize(item.nameSingular) + ' for Update'] = + computeSchemaComponent({ + item, + withRequiredFields: false, + forResponse: false, + withRelations: false, + }); + schemas[capitalize(item.nameSingular) + ' for Response'] = + computeSchemaComponent({ + item, + withRequiredFields: false, + forResponse: true, + withRelations: true, + }); return schemas; }, @@ -245,21 +315,47 @@ export const computeMetadataSchemaComponents = ( type: 'object', description: `An object`, properties: { - dataSourceId: { type: 'string' }, nameSingular: { type: 'string' }, namePlural: { type: 'string' }, labelSingular: { type: 'string' }, labelPlural: { type: 'string' }, description: { type: 'string' }, icon: { type: 'string' }, + labelIdentifierFieldMetadataId: { + type: 'string', + format: 'uuid', + }, + imageIdentifierFieldMetadataId: { + type: 'string', + format: 'uuid', + }, + }, + }; + schemas[`${capitalize(item.namePlural)}`] = { + type: 'array', + description: `A list of ${item.namePlural}`, + items: { + $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, + }, + }; + schemas[`${capitalize(item.nameSingular)} for Update`] = { + type: 'object', + description: `An object`, + properties: { + isActive: { type: 'boolean' }, + }, + }; + schemas[`${capitalize(item.nameSingular)} for Response`] = { + ...schemas[`${capitalize(item.nameSingular)}`], + properties: { + ...schemas[`${capitalize(item.nameSingular)}`].properties, + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, isCustom: { type: 'boolean' }, - isRemote: { type: 'boolean' }, isActive: { type: 'boolean' }, isSystem: { type: 'boolean' }, - createdAt: { type: 'string' }, - updatedAt: { type: 'string' }, - labelIdentifierFieldMetadataId: { type: 'string' }, - imageIdentifierFieldMetadataId: { type: 'string' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, fields: { type: 'object', properties: { @@ -269,7 +365,7 @@ export const computeMetadataSchemaComponents = ( node: { type: 'array', items: { - $ref: '#/components/schemas/Field', + $ref: '#/components/schemas/Field for Response', }, }, }, @@ -277,15 +373,13 @@ export const computeMetadataSchemaComponents = ( }, }, }, - example: {}, }; - schemas[`${capitalize(item.namePlural)}`] = { + schemas[`${capitalize(item.namePlural)} for Response`] = { type: 'array', description: `A list of ${item.namePlural}`, items: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, + $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Response`, }, - example: [{}], }; return schemas; @@ -295,57 +389,17 @@ export const computeMetadataSchemaComponents = ( type: 'object', description: `A field`, properties: { - type: { type: 'string' }, + type: { + type: 'string', + enum: Object.keys(FieldMetadataType), + }, name: { type: 'string' }, label: { type: 'string' }, description: { type: 'string' }, icon: { type: 'string' }, - isCustom: { type: 'boolean' }, - isActive: { type: 'boolean' }, - isSystem: { type: 'boolean' }, isNullable: { type: 'boolean' }, - createdAt: { type: 'string' }, - updatedAt: { type: 'string' }, - fromRelationMetadata: { - type: 'object', - properties: { - id: { type: 'string' }, - relationType: { type: 'string' }, - toObjectMetadata: { - type: 'object', - properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, - nameSingular: { type: 'string' }, - namePlural: { type: 'string' }, - isSystem: { type: 'boolean' }, - }, - }, - toFieldMetadataId: { type: 'string' }, - }, - }, - toRelationMetadata: { - type: 'object', - properties: { - id: { type: 'string' }, - relationType: { type: 'string' }, - fromObjectMetadata: { - type: 'object', - properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, - nameSingular: { type: 'string' }, - namePlural: { type: 'string' }, - isSystem: { type: 'boolean' }, - }, - }, - fromFieldMetadataId: { type: 'string' }, - }, - }, - defaultValue: { type: 'object' }, - options: { type: 'object' }, + objectMetadataId: { type: 'string', format: 'uuid' }, }, - example: {}, }; schemas[`${capitalize(item.namePlural)}`] = { type: 'array', @@ -353,7 +407,93 @@ export const computeMetadataSchemaComponents = ( items: { $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, }, - example: [{}], + }; + schemas[`${capitalize(item.nameSingular)} for Update`] = { + type: 'object', + description: `An object`, + properties: { + description: { type: 'string' }, + icon: { type: 'string' }, + isActive: { type: 'boolean' }, + isCustom: { type: 'boolean' }, + isNullable: { type: 'boolean' }, + isSystem: { type: 'boolean' }, + label: { type: 'string' }, + name: { type: 'string' }, + }, + }; + schemas[`${capitalize(item.nameSingular)} for Response`] = { + ...schemas[`${capitalize(item.nameSingular)}`], + properties: { + type: { + type: 'string', + enum: Object.keys(FieldMetadataType), + }, + name: { type: 'string' }, + label: { type: 'string' }, + description: { type: 'string' }, + icon: { type: 'string' }, + isNullable: { type: 'boolean' }, + id: { type: 'string', format: 'uuid' }, + isCustom: { type: 'boolean' }, + isActive: { type: 'boolean' }, + isSystem: { type: 'boolean' }, + defaultValue: { type: 'object' }, + options: { type: 'object' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, + fromRelationMetadata: { + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + relationType: { + type: 'string', + enum: Object.keys(RelationMetadataType), + }, + toObjectMetadata: { + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, + nameSingular: { type: 'string' }, + namePlural: { type: 'string' }, + isSystem: { type: 'boolean' }, + isRemote: { type: 'boolean' }, + }, + }, + toFieldMetadataId: { type: 'string', format: 'uuid' }, + }, + }, + toRelationMetadata: { + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + relationType: { + type: 'string', + enum: Object.keys(RelationMetadataType), + }, + fromObjectMetadata: { + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, + nameSingular: { type: 'string' }, + namePlural: { type: 'string' }, + isSystem: { type: 'boolean' }, + isRemote: { type: 'boolean' }, + }, + }, + fromFieldMetadataId: { type: 'string', format: 'uuid' }, + }, + }, + }, + }; + schemas[`${capitalize(item.namePlural)} for Response`] = { + type: 'array', + description: `A list of ${item.namePlural}`, + items: { + $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Response`, + }, }; return schemas; @@ -363,33 +503,17 @@ export const computeMetadataSchemaComponents = ( type: 'object', description: 'A relation', properties: { - relationType: { type: 'string' }, - fromObjectMetadata: { - type: 'object', - properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, - nameSingular: { type: 'string' }, - namePlural: { type: 'string' }, - isSystem: { type: 'boolean' }, - }, + relationType: { + type: 'string', + enum: Object.keys(RelationMetadataType), }, - fromObjectMetadataId: { type: 'string' }, - toObjectMetadata: { - type: 'object', - properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, - nameSingular: { type: 'string' }, - namePlural: { type: 'string' }, - isSystem: { type: 'boolean' }, - }, - }, - toObjectMetadataId: { type: 'string' }, - fromFieldMetadataId: { type: 'string' }, - toFieldMetadataId: { type: 'string' }, + fromObjectMetadataId: { type: 'string', format: 'uuid' }, + toObjectMetadataId: { type: 'string', format: 'uuid' }, + fromName: { type: 'string' }, + fromLabel: { type: 'string' }, + toName: { type: 'string' }, + toLabel: { type: 'string' }, }, - example: {}, }; schemas[`${capitalize(item.namePlural)}`] = { type: 'array', @@ -397,7 +521,47 @@ export const computeMetadataSchemaComponents = ( items: { $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, }, - example: [{}], + }; + schemas[`${capitalize(item.nameSingular)} for Response`] = { + ...schemas[`${capitalize(item.nameSingular)}`], + properties: { + relationType: { + type: 'string', + enum: Object.keys(RelationMetadataType), + }, + id: { type: 'string', format: 'uuid' }, + fromFieldMetadataId: { type: 'string', format: 'uuid' }, + toFieldMetadataId: { type: 'string', format: 'uuid' }, + fromObjectMetadata: { + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, + nameSingular: { type: 'string' }, + namePlural: { type: 'string' }, + isSystem: { type: 'boolean' }, + isRemote: { type: 'boolean' }, + }, + }, + toObjectMetadata: { + type: 'object', + properties: { + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, + nameSingular: { type: 'string' }, + namePlural: { type: 'string' }, + isSystem: { type: 'boolean' }, + isRemote: { type: 'boolean' }, + }, + }, + }, + }; + schemas[`${capitalize(item.namePlural)} for Response`] = { + type: 'array', + description: `A list of ${item.namePlural}`, + items: { + $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Response`, + }, }; } } diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/get-error-responses.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/get-error-responses.utils.ts index 13f48a6823..b7026b460e 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/get-error-responses.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/get-error-responses.utils.ts @@ -9,7 +9,7 @@ export const get400ErrorResponses = (): OpenAPIV3_1.ResponseObject => { type: 'object', properties: { statusCode: { type: 'number' }, - message: { type: 'string' }, + messages: { type: 'array', items: { type: 'string' } }, error: { type: 'string' }, }, example: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index 954e567e85..ca009395f3 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -55,7 +55,10 @@ export const computeDepthParameters = (): OpenAPIV3_1.ParameterObject => { return { name: 'depth', in: 'query', - description: 'Limits the depth objects returned.', + description: `Determines the level of nested related objects to include in the response. + - 0: Returns only the primary object's information. + - 1: Returns the primary object along with its directly related objects (with no additional nesting for related objects). + - 2: Returns the primary object, its directly related objects, and the related objects of those related objects.`, required: false, schema: { type: 'integer', diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts index 8dcf96e093..87892fa550 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts @@ -4,6 +4,7 @@ import { getArrayRequestBody, getFindDuplicatesRequestBody, getRequestBody, + getUpdateRequestBody, } from 'src/engine/core-modules/open-api/utils/request-body.utils'; import { getCreateManyResponse201, @@ -113,7 +114,7 @@ export const computeSingleResultPath = ( { $ref: '#/components/parameters/idPath' }, { $ref: '#/components/parameters/depth' }, ], - requestBody: getRequestBody(capitalize(item.nameSingular)), + requestBody: getUpdateRequestBody(capitalize(item.nameSingular)), responses: { '200': getUpdateOneResponse200(item), '400': { $ref: '#/components/responses/400' }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts index 52fa223cb5..e5f46c2b3a 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/request-body.utils.ts @@ -12,6 +12,20 @@ export const getRequestBody = (name: string) => { }; }; +export const getUpdateRequestBody = (name: string) => { + return { + description: 'body', + required: true, + content: { + 'application/json': { + schema: { + $ref: `#/components/schemas/${name} for Update`, + }, + }, + }, + }; +}; + export const getArrayRequestBody = (name: string) => { return { required: true, @@ -45,10 +59,6 @@ export const getFindDuplicatesRequestBody = (name: string) => { }, ids: { type: 'array', - items: { - type: 'string', - format: 'uuid', - }, }, }, }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts index d79c46a841..1d46a332b2 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts @@ -5,6 +5,10 @@ export const getFindManyResponse200 = ( item: Pick, fromMetadata = false, ) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )} for Response`; + return { description: 'Successful operation', content: { @@ -18,9 +22,7 @@ export const getFindManyResponse200 = ( [item.namePlural]: { type: 'array', items: { - $ref: `#/components/schemas/${capitalize( - item.nameSingular, - )}${!fromMetadata ? ' with Relations' : ''}`, + $ref: schemaRef, }, }, }, @@ -29,8 +31,14 @@ export const getFindManyResponse200 = ( type: 'object', properties: { hasNextPage: { type: 'boolean' }, - startCursor: { type: 'string' }, - endCursor: { type: 'string' }, + startCursor: { + type: 'string', + format: 'uuid', + }, + endCursor: { + type: 'string', + format: 'uuid', + }, }, }, ...(!fromMetadata && { @@ -39,21 +47,6 @@ export const getFindManyResponse200 = ( }, }), }, - example: { - data: { - [item.namePlural]: [ - `${capitalize(item.nameSingular)}Object1`, - `${capitalize(item.nameSingular)}Object2`, - '...', - ], - }, - pageInfo: { - hasNextPage: true, - startCursor: '56f411fb-0900-4ffb-b942-d7e8d6709eff', - endCursor: '93adf3c6-6cf7-4a86-adcd-75f77857ba67', - }, - totalCount: 132, - }, }, }, }, @@ -62,8 +55,9 @@ export const getFindManyResponse200 = ( export const getFindOneResponse200 = ( item: Pick, - fromMetadata = false, ) => { + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)} for Response`; + return { description: 'Successful operation', content: { @@ -75,18 +69,11 @@ export const getFindOneResponse200 = ( type: 'object', properties: { [item.nameSingular]: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}${ - !fromMetadata ? ' with Relations' : '' - }`, + $ref: schemaRef, }, }, }, }, - example: { - data: { - [item.nameSingular]: `${capitalize(item.nameSingular)}Object`, - }, - }, }, }, }, @@ -98,6 +85,7 @@ export const getCreateOneResponse201 = ( fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)} for Response`; return { description: 'Successful operation', @@ -110,18 +98,11 @@ export const getCreateOneResponse201 = ( type: 'object', properties: { [`create${one}${capitalize(item.nameSingular)}`]: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, + $ref: schemaRef, }, }, }, }, - example: { - data: { - [`create${one}${capitalize(item.nameSingular)}`]: `${capitalize( - item.nameSingular, - )}Object`, - }, - }, }, }, }, @@ -131,6 +112,10 @@ export const getCreateOneResponse201 = ( export const getCreateManyResponse201 = ( item: Pick, ) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )} for Response`; + return { description: 'Successful operation', content: { @@ -144,23 +129,12 @@ export const getCreateManyResponse201 = ( [`create${capitalize(item.namePlural)}`]: { type: 'array', items: { - $ref: `#/components/schemas/${capitalize( - item.nameSingular, - )}`, + $ref: schemaRef, }, }, }, }, }, - example: { - data: { - [`create${capitalize(item.namePlural)}`]: [ - `${capitalize(item.nameSingular)}Object1`, - `${capitalize(item.nameSingular)}Object2`, - '...', - ], - }, - }, }, }, }, @@ -172,6 +146,7 @@ export const getUpdateOneResponse200 = ( fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)} for Response`; return { description: 'Successful operation', @@ -184,18 +159,11 @@ export const getUpdateOneResponse200 = ( type: 'object', properties: { [`update${one}${capitalize(item.nameSingular)}`]: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, + $ref: schemaRef, }, }, }, }, - example: { - data: { - [`update${one}${capitalize(item.nameSingular)}`]: `${capitalize( - item.nameSingular, - )}Object`, - }, - }, }, }, }, @@ -230,13 +198,6 @@ export const getDeleteResponse200 = ( }, }, }, - example: { - data: { - [`delete${one}${capitalize(item.nameSingular)}`]: { - id: 'ffe75ac3-9786-4846-b56f-640685c3631e', - }, - }, - }, }, }, }, @@ -302,6 +263,10 @@ export const getJsonResponse = () => { export const getFindDuplicatesResponse200 = ( item: Pick, ) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )} for Response`; + return { description: 'Successful operation', content: { @@ -319,16 +284,20 @@ export const getFindDuplicatesResponse200 = ( type: 'object', properties: { hasNextPage: { type: 'boolean' }, - startCursor: { type: 'string' }, - endCursor: { type: 'string' }, + startCursor: { + type: 'string', + format: 'uuid', + }, + endCursor: { + type: 'string', + format: 'uuid', + }, }, }, companyDuplicates: { type: 'array', items: { - $ref: `#/components/schemas/${capitalize( - item.nameSingular, - )}`, + $ref: schemaRef, }, }, }, diff --git a/packages/twenty-website/src/app/_components/playground/rest-api-wrapper.tsx b/packages/twenty-website/src/app/_components/playground/rest-api-wrapper.tsx index cd2da8ea8e..390fd0cf63 100644 --- a/packages/twenty-website/src/app/_components/playground/rest-api-wrapper.tsx +++ b/packages/twenty-website/src/app/_components/playground/rest-api-wrapper.tsx @@ -23,7 +23,11 @@ export const RestApiWrapper = ({ openApiJson }: { openApiJson: any }) => { overflow: 'auto', }} > - + ); };