diff --git a/packages/twenty-docs/sidebars.js b/packages/twenty-docs/sidebars.js index bc0c76434..99a9a5a03 100644 --- a/packages/twenty-docs/sidebars.js +++ b/packages/twenty-docs/sidebars.js @@ -56,9 +56,7 @@ const sidebars = { { type: 'link', label: 'Metadata API', - href: '#', - className: 'coming-soon', - //href: '/rest-api/metadata', + href: '/rest-api/metadata', }, ], }, diff --git a/packages/twenty-docs/src/pages/rest-api/metadata.tsx b/packages/twenty-docs/src/pages/rest-api/metadata.tsx new file mode 100644 index 000000000..b41356311 --- /dev/null +++ b/packages/twenty-docs/src/pages/rest-api/metadata.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import { API } from '@stoplight/elements'; +import Layout from '@theme/Layout'; + +import Playground from '../../components/playground'; + +import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css'; + +const RestApiComponent = ({ openApiJson }) => { + // We load spotlightTheme style using useEffect as it breaks remaining docs style + useEffect(() => { + const styleElement = document.createElement('style'); + styleElement.innerHTML = spotlightTheme.toString(); + document.head.append(styleElement); + + return () => styleElement.remove(); + }, []); + + return ( +
+ +
+ ); +}; + +const restApi = () => { + const [openApiJson, setOpenApiJson] = useState({}); + + const children = ; + + return ( + + + {() => ( + + )} + + + ); +}; + +export default restApi; diff --git a/packages/twenty-server/src/core/api-rest/api-rest.module.ts b/packages/twenty-server/src/core/api-rest/api-rest.module.ts index bfa91e0eb..36e714d91 100644 --- a/packages/twenty-server/src/core/api-rest/api-rest.module.ts +++ b/packages/twenty-server/src/core/api-rest/api-rest.module.ts @@ -12,5 +12,6 @@ import { ApiRestMetadataService } from 'src/core/api-rest/metadata-rest.service' imports: [ApiRestQueryBuilderModule, AuthModule, HttpModule], controllers: [ApiRestMetadataController, ApiRestController], providers: [ApiRestMetadataService, ApiRestService], + exports: [ApiRestMetadataService], }) export class ApiRestModule {} diff --git a/packages/twenty-server/src/core/open-api/open-api.service.ts b/packages/twenty-server/src/core/open-api/open-api.service.ts index 55f8271b1..80f4ee1ac 100644 --- a/packages/twenty-server/src/core/open-api/open-api.service.ts +++ b/packages/twenty-server/src/core/open-api/open-api.service.ts @@ -12,11 +12,19 @@ import { } from 'src/core/open-api/utils/path.utils'; import { getErrorResponses } from 'src/core/open-api/utils/get-error-responses.utils'; import { + computeMetadataSchemaComponents, computeParameterComponents, computeSchemaComponents, } from 'src/core/open-api/utils/components.utils'; import { computeSchemaTags } from 'src/core/open-api/utils/compute-schema-tags.utils'; import { computeWebhooks } from 'src/core/open-api/utils/computeWebhooks.utils'; +import { capitalize } from 'src/utils/capitalize'; +import { + getDeleteResponse200, + getManyResultResponse200, + getSingleResultSuccessResponse, +} from 'src/core/open-api/utils/responses.utils'; +import { getRequestBody } from 'src/core/open-api/utils/request-body.utils'; @Injectable() export class OpenApiService { @@ -26,7 +34,7 @@ export class OpenApiService { ) {} async generateCoreSchema(request: Request): Promise { - const schema = baseSchema(); + const schema = baseSchema('core'); let objectMetadataItems; @@ -80,10 +88,98 @@ export class OpenApiService { async generateMetaDataSchema(): Promise { //TODO Add once Rest MetaData api is ready - const schema = baseSchema(); + const schema = baseSchema('metadata'); schema.tags = [{ name: 'placeholder' }]; + const metadata = [ + { + nameSingular: 'object', + namePlural: 'objects', + }, + { + nameSingular: 'field', + namePlural: 'fields', + }, + { + nameSingular: 'relation', + namePlural: 'relations', + }, + ]; + + schema.paths = metadata.reduce((path, item) => { + path[`/${item.namePlural}`] = { + get: { + tags: [item.namePlural], + summary: `Find Many ${item.namePlural}`, + parameters: [{ $ref: '#/components/parameters/filter' }], + responses: { + '200': getManyResultResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + post: { + tags: [item.namePlural], + summary: `Create One ${item.nameSingular}`, + operationId: `createOne${capitalize(item.nameSingular)}`, + requestBody: getRequestBody(item), + responses: { + '200': getSingleResultSuccessResponse(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + } as OpenAPIV3_1.PathItemObject; + path[`/${item.namePlural}/{id}`] = { + get: { + tags: [item.namePlural], + summary: `Find One ${item.nameSingular}`, + parameters: [{ $ref: '#/components/parameters/idPath' }], + responses: { + '200': getSingleResultSuccessResponse(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + delete: { + tags: [item.namePlural], + summary: `Delete One ${item.nameSingular}`, + operationId: `deleteOne${capitalize(item.nameSingular)}`, + parameters: [{ $ref: '#/components/parameters/idPath' }], + responses: { + '200': getDeleteResponse200(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + put: { + tags: [item.namePlural], + summary: `Update One ${item.namePlural}`, + operationId: `updateOne${capitalize(item.nameSingular)}`, + parameters: [{ $ref: '#/components/parameters/idPath' }], + requestBody: getRequestBody(item), + responses: { + '200': getSingleResultSuccessResponse(item), + '400': { $ref: '#/components/responses/400' }, + '401': { $ref: '#/components/responses/401' }, + }, + }, + } as OpenAPIV3_1.PathItemObject; + + return path; + }, schema.paths as OpenAPIV3_1.PathsObject); + + schema.components = { + ...schema.components, // components.securitySchemes is defined in base Schema + schemas: computeMetadataSchemaComponents(metadata), + parameters: computeParameterComponents(), + responses: { + '400': getErrorResponses('Invalid request'), + '401': getErrorResponses('Unauthorized'), + }, + }; + return schema; } } diff --git a/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts b/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts index 9f3438ad0..e7d969a7b 100644 --- a/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts +++ b/packages/twenty-server/src/core/open-api/utils/base-schema.utils.ts @@ -2,7 +2,9 @@ import { OpenAPIV3_1 } from 'openapi-types'; import { computeOpenApiPath } from 'src/core/open-api/utils/path.utils'; -export const baseSchema = (): OpenAPIV3_1.Document => { +export const baseSchema = ( + schemaName: 'core' | 'metadata', +): OpenAPIV3_1.Document => { return { openapi: '3.0.3', info: { @@ -21,7 +23,9 @@ export const baseSchema = (): OpenAPIV3_1.Document => { // Testing purposes servers: [ { - url: 'https://api.twenty.com/rest', + url: `https://api.twenty.com/rest/${ + schemaName !== 'core' ? schemaName : '' + }`, description: 'Production Development', }, ], @@ -45,6 +49,6 @@ export const baseSchema = (): OpenAPIV3_1.Document => { description: 'Find out more about **Twenty**', url: 'https://twenty.com', }, - paths: { '/open-api': computeOpenApiPath() }, + paths: { [`/open-api/${schemaName}`]: computeOpenApiPath() }, }; }; diff --git a/packages/twenty-server/src/core/open-api/utils/components.utils.ts b/packages/twenty-server/src/core/open-api/utils/components.utils.ts index 16a572fc6..f66268c6a 100644 --- a/packages/twenty-server/src/core/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/core/open-api/utils/components.utils.ts @@ -150,3 +150,146 @@ export const computeParameterComponents = (): Record< limit: computeLimitParameters(), }; }; + +export const computeMetadataSchemaComponents = ( + metadataSchema: { nameSingular: string; namePlural: string }[], +): Record => { + return metadataSchema.reduce( + (schemas, item) => { + switch (item.nameSingular) { + case 'object': { + schemas[`${capitalize(item.nameSingular)}`] = { + type: 'object', + properties: { + dataSourceId: { type: 'string' }, + nameSingular: { type: 'string' }, + namePlural: { type: 'string' }, + labelSingular: { type: 'string' }, + labelPlural: { type: 'string' }, + description: { type: 'string' }, + icon: { type: 'string' }, + isCustom: { type: 'boolean' }, + isActive: { type: 'boolean' }, + isSystem: { type: 'boolean' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + labelIdentifierFieldMetadataId: { type: 'string' }, + imageIdentifierFieldMetadataId: { type: 'string' }, + fields: { + type: 'object', + properties: { + edges: { + type: 'object', + properties: { + node: { + type: 'array', + items: { + $ref: '#/components/schemas/Field', + }, + }, + }, + }, + }, + }, + }, + }; + + return schemas; + } + case 'field': { + schemas[`${capitalize(item.nameSingular)}`] = { + type: 'object', + properties: { + type: { type: 'string' }, + 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' }, + }, + }; + + return schemas; + } + case 'relation': { + schemas[`${capitalize(item.nameSingular)}`] = { + type: 'object', + properties: { + relationType: { type: 'string' }, + fromObjectMetadata: { + type: 'object', + properties: { + id: { type: 'string' }, + dataSourceId: { type: 'string' }, + nameSingular: { type: 'string' }, + namePlural: { type: 'string' }, + isSystem: { type: 'boolean' }, + }, + }, + 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' }, + }, + }; + } + } + + return schemas; + }, + {} as Record, + ); +}; diff --git a/packages/twenty-server/src/core/open-api/utils/path.utils.ts b/packages/twenty-server/src/core/open-api/utils/path.utils.ts index 04bdc78c0..67bbbf0b2 100644 --- a/packages/twenty-server/src/core/open-api/utils/path.utils.ts +++ b/packages/twenty-server/src/core/open-api/utils/path.utils.ts @@ -101,6 +101,11 @@ export const computeOpenApiPath = (): OpenAPIV3_1.PathItemObject => { tags: ['General'], summary: 'Get Open Api Schema', operationId: 'GetOpenApiSchema', + servers: [ + { + url: 'https://api.twenty.com/', + }, + ], responses: { '200': getJsonResponse(), }, diff --git a/packages/twenty-server/src/core/open-api/utils/request-body.utils.ts b/packages/twenty-server/src/core/open-api/utils/request-body.utils.ts index d79e8c609..77007950b 100644 --- a/packages/twenty-server/src/core/open-api/utils/request-body.utils.ts +++ b/packages/twenty-server/src/core/open-api/utils/request-body.utils.ts @@ -1,7 +1,9 @@ -import { capitalize } from 'src/utils/capitalize'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { capitalize } from 'src/utils/capitalize'; -export const getRequestBody = (item: ObjectMetadataEntity) => { +export const getRequestBody = ( + item: Pick, +) => { return { description: 'body', required: true, diff --git a/packages/twenty-server/src/core/open-api/utils/responses.utils.ts b/packages/twenty-server/src/core/open-api/utils/responses.utils.ts index 3dd683cf1..2a6bc1841 100644 --- a/packages/twenty-server/src/core/open-api/utils/responses.utils.ts +++ b/packages/twenty-server/src/core/open-api/utils/responses.utils.ts @@ -1,7 +1,9 @@ import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { capitalize } from 'src/utils/capitalize'; -export const getManyResultResponse200 = (item: ObjectMetadataEntity) => { +export const getManyResultResponse200 = ( + item: Pick, +) => { return { description: 'Successful operation', content: { @@ -37,7 +39,9 @@ export const getManyResultResponse200 = (item: ObjectMetadataEntity) => { }; }; -export const getSingleResultSuccessResponse = (item: ObjectMetadataEntity) => { +export const getSingleResultSuccessResponse = ( + item: Pick, +) => { return { description: 'Successful operation', content: { @@ -60,7 +64,9 @@ export const getSingleResultSuccessResponse = (item: ObjectMetadataEntity) => { }; }; -export const getDeleteResponse200 = (item) => { +export const getDeleteResponse200 = ( + item: Pick, +) => { return { description: 'Successful operation', content: { @@ -96,6 +102,49 @@ export const getJsonResponse = () => { 'application/json': { schema: { type: 'object', + properties: { + openapi: { type: 'string' }, + info: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + termsOfService: { type: 'string' }, + contact: { + type: 'object', + properties: { email: { type: 'string' } }, + }, + license: { + type: 'object', + properties: { + name: { type: 'string' }, + url: { type: 'string' }, + }, + }, + }, + }, + servers: { + type: 'array', + items: { + url: { type: 'string' }, + description: { type: 'string' }, + }, + }, + components: { + type: 'object', + properties: { + schemas: { type: 'object' }, + parameters: { type: 'object' }, + responses: { type: 'object' }, + }, + }, + paths: { + type: 'object', + }, + tags: { + type: 'object', + }, + }, }, }, },