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',
+ },
+ },
},
},
},