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 845ad51d9be1..9e900e3b7e07 100644 --- a/packages/twenty-server/src/engine/api/rest/errors/RestApiException.ts +++ b/packages/twenty-server/src/engine/api/rest/errors/RestApiException.ts @@ -2,14 +2,17 @@ import { BadRequestException } from '@nestjs/common'; import { BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -const formatMessage = (message: BaseGraphQLError) => { - let formattedMessage = message.extensions - ? message.extensions.response.message || message.extensions.response - : message.message; +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 '$data' got i", 'I') + .replace("Variable '$input' got i", 'I'); const regex = /Field '[^']+' is not defined by type .*/; 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 1a9f81c465f8..f53f25b03320 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 @@ -42,7 +42,7 @@ describe('computeSchemaComponents', () => { }, fieldDateTime: { type: 'string', - format: 'date', + format: 'date-time', }, fieldDate: { type: 'string', 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 bdcb75946431..37ab7aef890f 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,7 +12,10 @@ 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'; @@ -20,11 +25,33 @@ type Properties = { [name: string]: 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: @@ -34,6 +61,7 @@ const getFieldProperties = ( 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: @@ -60,16 +88,24 @@ const getFieldProperties = ( } 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; } @@ -95,9 +131,17 @@ const getSchemaComponentsProperties = ( properties: compositeTypeDefinitions .get(field.type) ?.properties?.reduce((properties, property) => { + 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; @@ -166,19 +210,18 @@ const getRequiredFields = (item: ObjectMetadataEntity): string[] => { const computeSchemaComponent = ({ item, - withRequired, + forResponse, }: { item: ObjectMetadataEntity; - withRequired: boolean; + forResponse: boolean; }): OpenAPIV3_1.SchemaObject => { const result = { type: 'object', description: item.description, - properties: getSchemaComponentsProperties(item), - example: {}, + properties: getSchemaComponentsProperties({ item, forResponse }), } as OpenAPIV3_1.SchemaObject; - if (!withRequired) { + if (forResponse) { return result; } @@ -186,14 +229,6 @@ const computeSchemaComponent = ({ if (requiredFields?.length) { result.required = requiredFields; - result.example = requiredFields.reduce( - (example, requiredField) => { - example[requiredField] = ''; - - return example; - }, - {} as Record, - ); } return result; @@ -201,26 +236,25 @@ const computeSchemaComponent = ({ const computeRelationSchemaComponent = ({ item, - withRequired, + forResponse, }: { item: ObjectMetadataEntity; - withRequired: boolean; + forResponse: boolean; }): OpenAPIV3_1.SchemaObject => { const result = { description: item.description, allOf: [ { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}${!withRequired ? ' for Responses' : ''}`, + $ref: `#/components/schemas/${capitalize(item.nameSingular)}${forResponse ? ' for Response' : ''}`, }, { type: 'object', properties: getSchemaComponentsRelationProperties(item), }, ], - example: {}, } as OpenAPIV3_1.SchemaObject; - if (!withRequired) { + if (forResponse) { return result; } @@ -228,14 +262,6 @@ const computeRelationSchemaComponent = ({ if (requiredFields?.length) { result.required = requiredFields; - result.example = requiredFields.reduce( - (example, requiredField) => { - example[requiredField] = ''; - - return example; - }, - {} as Record, - ); } return result; @@ -248,12 +274,12 @@ export const computeSchemaComponents = ( (schemas, item) => { schemas[capitalize(item.nameSingular)] = computeSchemaComponent({ item, - withRequired: true, + forResponse: false, }); - schemas[capitalize(item.nameSingular) + ' for Responses'] = - computeSchemaComponent({ item, withRequired: false }); - schemas[capitalize(item.nameSingular) + ' with Relations for Responses'] = - computeRelationSchemaComponent({ item, withRequired: false }); + schemas[capitalize(item.nameSingular) + ' for Response'] = + computeSchemaComponent({ item, forResponse: true }); + schemas[capitalize(item.nameSingular) + ' with Relations for Response'] = + computeRelationSchemaComponent({ item, forResponse: true }); return schemas; }, @@ -286,7 +312,7 @@ export const computeMetadataSchemaComponents = ( type: 'object', description: `An object`, properties: { - dataSourceId: { type: 'string' }, + dataSourceId: { type: 'string', format: 'uuid' }, nameSingular: { type: 'string' }, namePlural: { type: 'string' }, labelSingular: { type: 'string' }, @@ -297,10 +323,16 @@ export const computeMetadataSchemaComponents = ( 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' }, + labelIdentifierFieldMetadataId: { + type: 'string', + format: 'uuid', + }, + imageIdentifierFieldMetadataId: { + type: 'string', + format: 'uuid', + }, fields: { type: 'object', properties: { @@ -318,7 +350,6 @@ export const computeMetadataSchemaComponents = ( }, }, }, - example: {}, }; schemas[`${capitalize(item.namePlural)}`] = { type: 'array', @@ -326,7 +357,6 @@ export const computeMetadataSchemaComponents = ( items: { $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, }, - example: [{}], }; return schemas; @@ -345,48 +375,47 @@ export const computeMetadataSchemaComponents = ( isActive: { type: 'boolean' }, isSystem: { type: 'boolean' }, isNullable: { type: 'boolean' }, - createdAt: { type: 'string' }, - updatedAt: { type: 'string' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, fromRelationMetadata: { type: 'object', properties: { - id: { type: 'string' }, + id: { type: 'string', format: 'uuid' }, relationType: { type: 'string' }, toObjectMetadata: { type: 'object', properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, nameSingular: { type: 'string' }, namePlural: { type: 'string' }, isSystem: { type: 'boolean' }, }, }, - toFieldMetadataId: { type: 'string' }, + toFieldMetadataId: { type: 'string', format: 'uuid' }, }, }, toRelationMetadata: { type: 'object', properties: { - id: { type: 'string' }, + id: { type: 'string', format: 'uuid' }, relationType: { type: 'string' }, fromObjectMetadata: { type: 'object', properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, nameSingular: { type: 'string' }, namePlural: { type: 'string' }, isSystem: { type: 'boolean' }, }, }, - fromFieldMetadataId: { type: 'string' }, + fromFieldMetadataId: { type: 'string', format: 'uuid' }, }, }, defaultValue: { type: 'object' }, options: { type: 'object' }, }, - example: {}, }; schemas[`${capitalize(item.namePlural)}`] = { type: 'array', @@ -394,7 +423,6 @@ export const computeMetadataSchemaComponents = ( items: { $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, }, - example: [{}], }; return schemas; @@ -408,8 +436,8 @@ export const computeMetadataSchemaComponents = ( fromObjectMetadata: { type: 'object', properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, nameSingular: { type: 'string' }, namePlural: { type: 'string' }, isSystem: { type: 'boolean' }, @@ -419,18 +447,17 @@ export const computeMetadataSchemaComponents = ( toObjectMetadata: { type: 'object', properties: { - id: { type: 'string' }, - dataSourceId: { type: 'string' }, + id: { type: 'string', format: 'uuid' }, + dataSourceId: { type: 'string', format: 'uuid' }, nameSingular: { type: 'string' }, namePlural: { type: 'string' }, isSystem: { type: 'boolean' }, }, }, - toObjectMetadataId: { type: 'string' }, - fromFieldMetadataId: { type: 'string' }, - toFieldMetadataId: { type: 'string' }, + toObjectMetadataId: { type: 'string', format: 'uuid' }, + fromFieldMetadataId: { type: 'string', format: 'uuid' }, + toFieldMetadataId: { type: 'string', format: 'uuid' }, }, - example: {}, }; schemas[`${capitalize(item.namePlural)}`] = { type: 'array', @@ -438,7 +465,6 @@ export const computeMetadataSchemaComponents = ( items: { $ref: `#/components/schemas/${capitalize(item.nameSingular)}`, }, - example: [{}], }; } } 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 27c81899a800..7e3f36493c9b 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, + )}${!fromMetadata ? ' with Relations 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 for Responses' : ' for Responses'}`, + $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, - }, }, }, }, @@ -64,6 +57,10 @@ export const getFindOneResponse200 = ( item: Pick, fromMetadata = false, ) => { + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}${ + !fromMetadata ? ' with Relations for Response' : '' + }`; + return { description: 'Successful operation', content: { @@ -75,20 +72,11 @@ export const getFindOneResponse200 = ( type: 'object', properties: { [item.nameSingular]: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)}${ - !fromMetadata - ? ' with Relations for Responses' - : ' for Responses' - }`, + $ref: schemaRef, }, }, }, }, - example: { - data: { - [item.nameSingular]: `${capitalize(item.nameSingular)}Object`, - }, - }, }, }, }, @@ -100,6 +88,7 @@ export const getCreateOneResponse201 = ( fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}${!fromMetadata ? ' for Response' : ''}`; return { description: 'Successful operation', @@ -112,18 +101,11 @@ export const getCreateOneResponse201 = ( type: 'object', properties: { [`create${one}${capitalize(item.nameSingular)}`]: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Responses`, + $ref: schemaRef, }, }, }, }, - example: { - data: { - [`create${one}${capitalize(item.nameSingular)}`]: `${capitalize( - item.nameSingular, - )}Object`, - }, - }, }, }, }, @@ -133,6 +115,10 @@ export const getCreateOneResponse201 = ( export const getCreateManyResponse201 = ( item: Pick, ) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )} for Response`; + return { description: 'Successful operation', content: { @@ -146,23 +132,12 @@ export const getCreateManyResponse201 = ( [`create${capitalize(item.namePlural)}`]: { type: 'array', items: { - $ref: `#/components/schemas/${capitalize( - item.nameSingular, - )} for Responses`, + $ref: schemaRef, }, }, }, }, }, - example: { - data: { - [`create${capitalize(item.namePlural)}`]: [ - `${capitalize(item.nameSingular)}Object1`, - `${capitalize(item.nameSingular)}Object2`, - '...', - ], - }, - }, }, }, }, @@ -174,6 +149,7 @@ export const getUpdateOneResponse200 = ( fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; + const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}${!fromMetadata ? ' for Response' : ''}`; return { description: 'Successful operation', @@ -186,18 +162,11 @@ export const getUpdateOneResponse200 = ( type: 'object', properties: { [`update${one}${capitalize(item.nameSingular)}`]: { - $ref: `#/components/schemas/${capitalize(item.nameSingular)} for Responses`, + $ref: schemaRef, }, }, }, }, - example: { - data: { - [`update${one}${capitalize(item.nameSingular)}`]: `${capitalize( - item.nameSingular, - )}Object`, - }, - }, }, }, }, @@ -232,13 +201,6 @@ export const getDeleteResponse200 = ( }, }, }, - example: { - data: { - [`delete${one}${capitalize(item.nameSingular)}`]: { - id: 'ffe75ac3-9786-4846-b56f-640685c3631e', - }, - }, - }, }, }, }, @@ -304,6 +266,10 @@ export const getJsonResponse = () => { export const getFindDuplicatesResponse200 = ( item: Pick, ) => { + const schemaRef = `#/components/schemas/${capitalize( + item.nameSingular, + )} for Response`; + return { description: 'Successful operation', content: { @@ -321,16 +287,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, - )} for Responses`, + $ref: schemaRef, }, }, },