Custom swagger endpoint for docs (#3869)
* custom swagger endpoint metadata graphql remove /rest from endpoint * fixed pseudo scheme creation * move graphql playground creation to own file, added navbar to change baseurl and token * add schema switcher, fix changing url not applied, add invalid overlay * fix link color * removed path on Graphql Playground, naming fixes subdoc * - fixed overflow issue Rest docs * history replace & goBack * Small fix GraphQL playground broken --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -51,13 +51,14 @@ const sidebars = {
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Core API',
|
||||
href: '/rest-api/',
|
||||
href: '/rest-api/core',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Metadata API',
|
||||
href: '#',
|
||||
className: 'coming-soon',
|
||||
//href: '/rest-api/metadata',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -73,13 +74,12 @@ const sidebars = {
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Core API',
|
||||
href: '/graphql/',
|
||||
href: '/graphql/core',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Metadata API',
|
||||
href: '#',
|
||||
className: 'coming-soon',
|
||||
href: '/graphql/metadata',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -6,19 +6,27 @@ import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import Layout from '@theme/Layout';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
|
||||
import Playground from '../components/playground';
|
||||
import Playground from './playground';
|
||||
|
||||
import explorerCss from '!css-loader!@graphiql/plugin-explorer/dist/style.css';
|
||||
import graphiqlCss from '!css-loader!graphiql/graphiql.css';
|
||||
|
||||
const SubDocToPath = {
|
||||
core: 'graphql',
|
||||
metadata: 'metadata',
|
||||
};
|
||||
|
||||
// Docusaurus does SSR for custom pages, but we need to load GraphiQL in the browser
|
||||
const GraphQlComponent = ({ token }) => {
|
||||
const GraphQlComponent = ({ token, baseUrl, path }) => {
|
||||
const explorer = explorerPlugin({
|
||||
showAttribution: true,
|
||||
});
|
||||
if (!baseUrl || !token) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: 'https://api.twenty.com/graphql',
|
||||
url: baseUrl + '/' + path,
|
||||
});
|
||||
|
||||
// We load graphiql style using useEffect as it breaks remaining docs style
|
||||
@ -50,8 +58,9 @@ const GraphQlComponent = ({ token }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const graphQL = () => {
|
||||
const GraphQlPlayground = ({ subDoc }: { subDoc: 'core' | 'metadata' }) => {
|
||||
const [token, setToken] = useState();
|
||||
const [baseUrl, setBaseUrl] = useState();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
@ -71,7 +80,13 @@ const graphQL = () => {
|
||||
return () => window.removeEventListener('storage', handleThemeChange);
|
||||
}, []);
|
||||
|
||||
const children = <GraphQlComponent token={token} />;
|
||||
const children = (
|
||||
<GraphQlComponent
|
||||
token={token}
|
||||
baseUrl={baseUrl}
|
||||
path={SubDocToPath[subDoc]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
@ -79,9 +94,16 @@ const graphQL = () => {
|
||||
description="GraphQL Playground for Twenty"
|
||||
>
|
||||
<BrowserOnly>
|
||||
{() => <Playground children={children} setToken={setToken} />}
|
||||
{() => (
|
||||
<Playground
|
||||
children={children}
|
||||
setToken={setToken}
|
||||
setBaseUrl={setBaseUrl}
|
||||
subdocName={subDoc}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
export default graphQL;
|
||||
export default GraphQlPlayground;
|
||||
@ -1,27 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TbLoader2 } from 'react-icons/tb';
|
||||
|
||||
import TokenForm, { TokenFormProps } from '../components/token-form';
|
||||
|
||||
const Playground = (
|
||||
{
|
||||
children,
|
||||
setOpenApiJson,
|
||||
setToken
|
||||
}: Partial<React.PropsWithChildren | TokenFormProps>
|
||||
) => {
|
||||
const [isTokenValid, setIsTokenValid] = useState(false)
|
||||
const Playground = ({
|
||||
children,
|
||||
setOpenApiJson,
|
||||
setToken,
|
||||
setBaseUrl,
|
||||
subDoc,
|
||||
}: Partial<React.PropsWithChildren | TokenFormProps> & {
|
||||
subDoc: string;
|
||||
}) => {
|
||||
const [isTokenValid, setIsTokenValid] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<TokenForm
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
setToken={setToken}
|
||||
setBaseUrl={setBaseUrl}
|
||||
isTokenValid={isTokenValid}
|
||||
setIsTokenValid={setIsTokenValid}
|
||||
subDoc={subDoc}
|
||||
setLoadingState={setIsLoading}
|
||||
/>
|
||||
{
|
||||
isTokenValid && children
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{!isTokenValid && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: 'calc(100vh - var(--ifm-navbar-height) - 45px)',
|
||||
top: '45px',
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2,
|
||||
background: 'rgba(23,23,23, 0.2)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '50%',
|
||||
background: 'rgba(23,23,23, 0.8)',
|
||||
color: 'white',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
A token is required as APIs are dynamically generated for each
|
||||
workspace based on their unique metadata. <br /> Generate your token
|
||||
under{' '}
|
||||
<a
|
||||
className="link"
|
||||
href="https://app.twenty.com/settings/developers"
|
||||
>
|
||||
Settings > Developers
|
||||
</a>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="loader-container">
|
||||
<TbLoader2 className="loader" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Playground;
|
||||
|
||||
@ -1,42 +1,90 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90vh;
|
||||
.form-container {
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--ifm-color-secondary-light);
|
||||
position: sticky;
|
||||
top: var(--ifm-navbar-height);
|
||||
padding: 0px 8px;
|
||||
background: var(--ifm-color-secondary-contrast-background);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.form {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
display: flex;
|
||||
height: 45px;
|
||||
gap: 10px;
|
||||
width: 50%;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #16233f;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .link {
|
||||
color: #a3c0f8;
|
||||
&:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 6px;
|
||||
margin: 20px 0 5px 0;
|
||||
margin: 5px 0 5px 0;
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f3f3;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding-left:30px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .input {
|
||||
background-color: #16233f;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
padding: 5px;
|
||||
color: #B3B3B3;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .inputIcon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 6px;
|
||||
margin: 5px 0 5px 0;
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: #f3f3f3;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .select {
|
||||
background-color: #16233f;
|
||||
}
|
||||
|
||||
|
||||
.invalid {
|
||||
border: 1px solid #f83e3e;
|
||||
}
|
||||
@ -75,3 +123,18 @@
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
.backButton {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: 8px;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: #999999;
|
||||
|
||||
&:hover {
|
||||
color: #16233f;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TbLoader2 } from 'react-icons/tb';
|
||||
import { useHistory, useLocation } from '@docusaurus/router';
|
||||
import { TbApi, TbChevronLeft, TbLink } from '@theme/icons';
|
||||
import { parseJson } from 'nx/src/utils/json';
|
||||
|
||||
import tokenForm from '!css-loader!./token-form.css';
|
||||
@ -7,21 +8,38 @@ import tokenForm from '!css-loader!./token-form.css';
|
||||
export type TokenFormProps = {
|
||||
setOpenApiJson?: (json: object) => void;
|
||||
setToken?: (token: string) => void;
|
||||
setBaseUrl?: (baseUrl: string) => void;
|
||||
isTokenValid: boolean;
|
||||
setIsTokenValid: (boolean) => void;
|
||||
setLoadingState: (boolean) => void;
|
||||
subDoc?: string;
|
||||
};
|
||||
|
||||
const TokenForm = ({
|
||||
setOpenApiJson,
|
||||
setToken,
|
||||
setBaseUrl: submitBaseUrl,
|
||||
isTokenValid,
|
||||
setIsTokenValid,
|
||||
subDoc,
|
||||
setLoadingState,
|
||||
}: TokenFormProps) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [baseUrl, setBaseUrl] = useState(
|
||||
parseJson(localStorage.getItem('baseUrl'))?.baseUrl ??
|
||||
'https://api.twenty.com',
|
||||
);
|
||||
const token =
|
||||
parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ??
|
||||
'';
|
||||
|
||||
const updateLoading = (loading: boolean) => {
|
||||
setIsLoading(loading);
|
||||
setLoadingState(loading);
|
||||
};
|
||||
|
||||
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
localStorage.setItem(
|
||||
'TryIt_securitySchemeValues',
|
||||
@ -30,22 +48,33 @@ const TokenForm = ({
|
||||
await submitToken(event.target.value);
|
||||
};
|
||||
|
||||
const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags);
|
||||
const updateBaseUrl = (baseUrl) => {
|
||||
setBaseUrl(baseUrl);
|
||||
submitBaseUrl?.(baseUrl);
|
||||
localStorage.setItem('baseUrl', JSON.stringify({ baseUrl: baseUrl }));
|
||||
};
|
||||
|
||||
const validateToken = (openApiJson) => {
|
||||
setIsTokenValid(!!openApiJson.tags);
|
||||
};
|
||||
|
||||
const getJson = async (token: string) => {
|
||||
setIsLoading(true);
|
||||
updateLoading(true);
|
||||
|
||||
return await fetch('https://api.twenty.com/open-api', {
|
||||
return await fetch(baseUrl + '/open-api/' + (subDoc ?? 'core'), {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((result) => {
|
||||
validateToken(result);
|
||||
setIsLoading(false);
|
||||
updateLoading(false);
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch(() => setIsLoading(false));
|
||||
.catch(() => {
|
||||
updateLoading(false);
|
||||
setIsTokenValid(false);
|
||||
});
|
||||
};
|
||||
|
||||
const submitToken = async (token) => {
|
||||
@ -60,6 +89,7 @@ const TokenForm = ({
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
updateBaseUrl(baseUrl);
|
||||
await submitToken(token);
|
||||
})();
|
||||
}, []);
|
||||
@ -74,46 +104,61 @@ const TokenForm = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
!isTokenValid && (
|
||||
<div>
|
||||
<div className="container">
|
||||
<form className="form">
|
||||
<label>
|
||||
To load your playground schema,{' '}
|
||||
<a
|
||||
className="link"
|
||||
href="https://app.twenty.com/settings/developers"
|
||||
>
|
||||
generate an API key
|
||||
</a>{' '}
|
||||
and paste it here:
|
||||
</label>
|
||||
<p>
|
||||
<input
|
||||
className={token && !isLoading ? 'input invalid' : 'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMD..."
|
||||
defaultValue={token}
|
||||
onChange={updateToken}
|
||||
/>
|
||||
<span
|
||||
className={`token-invalid ${
|
||||
(!token || isLoading) && 'not-visible'
|
||||
}`}
|
||||
>
|
||||
Token invalid
|
||||
</span>
|
||||
<div className="loader-container">
|
||||
<TbLoader2
|
||||
className={`loader ${!isLoading && 'not-visible'}`}
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
</form>
|
||||
<div className="form-container">
|
||||
<form className="form">
|
||||
<div className="backButton" onClick={() => history.goBack()}>
|
||||
<TbChevronLeft size={18} />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
<div className="inputWrapper">
|
||||
<div className="inputIcon" title="Api Key">
|
||||
<TbApi size={20} />
|
||||
</div>
|
||||
<input
|
||||
className={!isTokenValid && !isLoading ? 'input invalid' : 'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
placeholder="API Key"
|
||||
defaultValue={token}
|
||||
onChange={updateToken}
|
||||
/>
|
||||
</div>
|
||||
<div className="inputWrapper">
|
||||
<div className="inputIcon" title="Base URL">
|
||||
<TbLink size={20} />
|
||||
</div>
|
||||
<input
|
||||
className={'input'}
|
||||
type="text"
|
||||
readOnly={isLoading}
|
||||
placeholder="Base URL"
|
||||
defaultValue={baseUrl}
|
||||
onChange={(event) => updateBaseUrl(event.target.value)}
|
||||
onBlur={() => submitToken(token)}
|
||||
/>
|
||||
</div>
|
||||
{!location.pathname.includes('rest-api') && (
|
||||
<div className="inputWrapper" style={{ maxWidth: '100px' }}>
|
||||
<select
|
||||
className="select"
|
||||
onChange={(event) =>
|
||||
history.replace(
|
||||
'/' +
|
||||
location.pathname.split('/').at(-2) +
|
||||
'/' +
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
value={location.pathname.split('/').at(-1)}
|
||||
>
|
||||
<option value="core">Core</option>
|
||||
<option value="metadata">Metadata</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -250,7 +250,7 @@ li.coming-soon a::after {
|
||||
}
|
||||
|
||||
.fullHeightPlayground {
|
||||
height: calc(100vh - var(--ifm-navbar-height));
|
||||
height: calc(100vh - var(--ifm-navbar-height) - 45px);
|
||||
}
|
||||
|
||||
.display-none {
|
||||
|
||||
9
packages/twenty-docs/src/pages/graphql/core.tsx
Normal file
9
packages/twenty-docs/src/pages/graphql/core.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import GraphQlPlayground from '../../components/graphql-playground';
|
||||
|
||||
const CoreGraphql = () => {
|
||||
return <GraphQlPlayground subDoc={'core'} />;
|
||||
};
|
||||
|
||||
export default CoreGraphql;
|
||||
9
packages/twenty-docs/src/pages/graphql/metadata.tsx
Normal file
9
packages/twenty-docs/src/pages/graphql/metadata.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import GraphQlPlayground from '../../components/graphql-playground';
|
||||
|
||||
const CoreGraphql = () => {
|
||||
return <GraphQlPlayground subDoc={'metadata'} />;
|
||||
};
|
||||
|
||||
export default CoreGraphql;
|
||||
@ -1,13 +1,13 @@
|
||||
import Layout from '@theme/Layout';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import { API } from '@stoplight/elements';
|
||||
import Playground from '../components/playground';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
import Playground from '../../components/playground';
|
||||
|
||||
import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css';
|
||||
|
||||
|
||||
const RestApiComponent = ({openApiJson}) => {
|
||||
|
||||
const RestApiComponent = ({ openApiJson }) => {
|
||||
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
||||
useEffect(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
@ -18,31 +18,38 @@ const RestApiComponent = ({openApiJson}) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<API
|
||||
apiDescriptionDocument={JSON.stringify(openApiJson)}
|
||||
router="hash"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<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 [openApiJson, setOpenApiJson] = useState({});
|
||||
|
||||
const children = <RestApiComponent openApiJson={openApiJson} />
|
||||
const children = <RestApiComponent openApiJson={openApiJson} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title="REST API Playground"
|
||||
description="REST API Playground for Twenty"
|
||||
>
|
||||
<BrowserOnly>{
|
||||
() => <Playground
|
||||
children={children}
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
/>
|
||||
}</BrowserOnly>
|
||||
<BrowserOnly>
|
||||
{() => (
|
||||
<Playground
|
||||
children={children}
|
||||
setOpenApiJson={setOpenApiJson}
|
||||
subDoc="core"
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
</Layout>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default restApi;
|
||||
@ -21,6 +21,7 @@ export {
|
||||
TbCheck,
|
||||
TbCheckbox,
|
||||
TbChecklist,
|
||||
TbChevronLeft,
|
||||
TbCircleCheckFilled,
|
||||
TbCircleDot,
|
||||
TbCloud,
|
||||
|
||||
@ -8,9 +8,22 @@ import { OpenApiService } from 'src/core/open-api/open-api.service';
|
||||
export class OpenApiController {
|
||||
constructor(private readonly openApiService: OpenApiService) {}
|
||||
|
||||
@Get()
|
||||
async generateOpenApiSchema(@Req() request: Request, @Res() res: Response) {
|
||||
const data = await this.openApiService.generateSchema(request);
|
||||
@Get('core')
|
||||
async generateOpenApiSchemaCore(
|
||||
@Req() request: Request,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const data = await this.openApiService.generateCoreSchema(request);
|
||||
|
||||
res.send(data);
|
||||
}
|
||||
|
||||
@Get('metadata')
|
||||
async generateOpenApiSchemaMetaData(
|
||||
@Req() request: Request,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const data = await this.openApiService.generateMetaDataSchema();
|
||||
|
||||
res.send(data);
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ export class OpenApiService {
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
) {}
|
||||
|
||||
async generateSchema(request: Request): Promise<OpenAPIV3.Document> {
|
||||
async generateCoreSchema(request: Request): Promise<OpenAPIV3.Document> {
|
||||
const schema = baseSchema();
|
||||
|
||||
let objectMetadataItems;
|
||||
@ -42,8 +42,8 @@ export class OpenApiService {
|
||||
return schema;
|
||||
}
|
||||
schema.paths = objectMetadataItems.reduce((paths, item) => {
|
||||
paths[`/rest/${item.namePlural}`] = computeManyResultPath(item);
|
||||
paths[`/rest/${item.namePlural}/{id}`] = computeSingleResultPath(item);
|
||||
paths[`/${item.namePlural}`] = computeManyResultPath(item);
|
||||
paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item);
|
||||
|
||||
return paths;
|
||||
}, schema.paths as OpenAPIV3.PathsObject);
|
||||
@ -62,4 +62,13 @@ export class OpenApiService {
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
async generateMetaDataSchema(): Promise<OpenAPIV3.Document> {
|
||||
//TODO Add once Rest MetaData api is ready
|
||||
const schema = baseSchema();
|
||||
|
||||
schema.tags = [{ name: 'placeholder' }];
|
||||
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ export const baseSchema = (): OpenAPIV3.Document => {
|
||||
// Testing purposes
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.twenty.com/',
|
||||
url: 'https://api.twenty.com/rest',
|
||||
description: 'Production Development',
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user