feat: adding metadata open-api endpoints and updating docs (#4170)

* initialise metadata schema for open-api

* remove "soon" label on metadata rest-api

* open-api fetch paths

* remove parameter type for metadata schema

* add REST module to open-api

* metadata schema components

* metadata paths

* refactor and /open-api route fix
This commit is contained in:
Aditya Pimpalkar 2024-03-05 10:37:16 +00:00 committed by GitHub
parent a9f4a66c4f
commit caa4dcf893
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 366 additions and 13 deletions

View File

@ -56,9 +56,7 @@ const sidebars = {
{
type: 'link',
label: 'Metadata API',
href: '#',
className: 'coming-soon',
//href: '/rest-api/metadata',
href: '/rest-api/metadata',
},
],
},

View File

@ -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 (
<div
style={{
height: 'calc(100vh - var(--ifm-navbar-height) - 45px)',
overflow: 'auto',
}}
>
<API apiDescriptionDocument={JSON.stringify(openApiJson)} router="hash" />
</div>
);
};
const restApi = () => {
const [openApiJson, setOpenApiJson] = useState({});
const children = <RestApiComponent openApiJson={openApiJson} />;
return (
<Layout
title="REST API Playground"
description="REST API Playground for Twenty"
>
<BrowserOnly>
{() => (
<Playground
children={children}
setOpenApiJson={setOpenApiJson}
subDoc="metadata"
/>
)}
</BrowserOnly>
</Layout>
);
};
export default restApi;

View File

@ -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 {}

View File

@ -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<OpenAPIV3_1.Document> {
const schema = baseSchema();
const schema = baseSchema('core');
let objectMetadataItems;
@ -80,10 +88,98 @@ export class OpenApiService {
async generateMetaDataSchema(): Promise<OpenAPIV3_1.Document> {
//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;
}
}

View File

@ -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() },
};
};

View File

@ -150,3 +150,146 @@ export const computeParameterComponents = (): Record<
limit: computeLimitParameters(),
};
};
export const computeMetadataSchemaComponents = (
metadataSchema: { nameSingular: string; namePlural: string }[],
): Record<string, OpenAPIV3_1.SchemaObject> => {
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<string, OpenAPIV3_1.SchemaObject>,
);
};

View File

@ -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(),
},

View File

@ -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<ObjectMetadataEntity, 'nameSingular'>,
) => {
return {
description: 'body',
required: true,

View File

@ -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<ObjectMetadataEntity, 'nameSingular' | 'namePlural'>,
) => {
return {
description: 'Successful operation',
content: {
@ -37,7 +39,9 @@ export const getManyResultResponse200 = (item: ObjectMetadataEntity) => {
};
};
export const getSingleResultSuccessResponse = (item: ObjectMetadataEntity) => {
export const getSingleResultSuccessResponse = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
return {
description: 'Successful operation',
content: {
@ -60,7 +64,9 @@ export const getSingleResultSuccessResponse = (item: ObjectMetadataEntity) => {
};
};
export const getDeleteResponse200 = (item) => {
export const getDeleteResponse200 = (
item: Pick<ObjectMetadataEntity, 'nameSingular'>,
) => {
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',
},
},
},
},
},