2914 graphql api documentation (#3065)

* Remove dead code

* Create playground component

* Remove useless call to action

* Fix graphiql theme

* Fix style

* Split components

* Move headers to headers form

* Fix nodes in open-api components

* Remove useless check

* Clean code

* Fix css differences

* Keep carret when fetching schema
This commit is contained in:
martmull
2023-12-20 12:01:55 +01:00
committed by GitHub
parent d2666dc667
commit ed7bd0ba26
9 changed files with 250 additions and 158 deletions

View File

@ -0,0 +1,27 @@
import React, { useState } from 'react';
import TokenForm, { TokenFormProps } from '../components/token-form';
const Playground = (
{
children,
setOpenApiJson,
setToken
}: Partial<React.PropsWithChildren | TokenFormProps>
) => {
const [isTokenValid, setIsTokenValid] = useState(false)
return (
<>
<TokenForm
setOpenApiJson={setOpenApiJson}
setToken={setToken}
isTokenValid={isTokenValid}
setIsTokenValid={setIsTokenValid}
/>
{
isTokenValid && children
}
</>
)
}
export default Playground;

View File

@ -18,8 +18,12 @@
transition: color 0.3s ease;
}
[data-theme='dark'] .link {
color: #a3c0f8;
}
.input {
padding: 4px;
padding: 6px;
margin: 20px 0 5px 0;
max-width: 460px;
width: 100%;
@ -27,17 +31,19 @@
background-color: #f3f3f3;
border: 1px solid #ddd;
border-radius: 4px;
}
[data-theme='dark'] .input {
background-color: #16233f;
}
.invalid {
border: 1px solid red;
border: 1px solid #f83e3e;
}
.token-invalid {
color: red;
color: #f83e3e;
font-size: 12px;
text-align: left;
}
.not-visible {
@ -50,6 +56,10 @@
animation: animate 2s infinite;
}
[data-theme='dark'] .loader {
color: #a3c0f8;
}
@keyframes animate {
0% {
transform: rotate(0deg);

View File

@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import { parseJson } from 'nx/src/utils/json';
import tokenForm from '!css-loader!./token-form.css';
import { TbLoader2 } from 'react-icons/tb';
export type TokenFormProps = {
setOpenApiJson?: (json: object) => void,
setToken?: (token: string) => void,
isTokenValid: boolean,
setIsTokenValid: (boolean) => void,
}
const TokenForm = (
{
setOpenApiJson,
setToken,
isTokenValid,
setIsTokenValid,
}: TokenFormProps
) => {
const [isLoading, setIsLoading] = useState(false)
const token = parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ?? ''
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
localStorage.setItem(
'TryIt_securitySchemeValues',
JSON.stringify({bearerAuth: event.target.value}),
)
await submitToken(event.target.value)
}
const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags)
const getJson = async (token: string ) => {
setIsLoading(true)
return await fetch(
'https://api.twenty.com/open-api',
{headers: {Authorization: `Bearer ${token}`}}
)
.then((res)=> res.json())
.then((result)=> {
validateToken(result)
setIsLoading(false)
return result
})
.catch(() => setIsLoading(false))
}
const submitToken = async (token) => {
if (isLoading) return
const json = await getJson(token)
setToken && setToken(token)
setOpenApiJson && setOpenApiJson(json)
}
useEffect(()=> {
(async ()=> {
await submitToken(token)
})()
},[])
// We load playground style using useEffect as it breaks remaining docs style
useEffect(() => {
const styleElement = document.createElement('style');
styleElement.innerHTML = tokenForm.toString();
document.head.append(styleElement);
return () => styleElement.remove();
}, []);
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/api-keys'>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>
</div>
)
}
export default TokenForm;

View File

@ -1,35 +1,69 @@
import { createGraphiQLFetcher } from "@graphiql/toolkit";
import { GraphiQL } from "graphiql";
import React from "react";
import Layout from "@theme/Layout";
import BrowserOnly from "@docusaurus/BrowserOnly";
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql';
import React, { useEffect, useState } from 'react';
import Layout from '@theme/Layout';
import BrowserOnly from '@docusaurus/BrowserOnly';
import { useTheme, Theme } from '@graphiql/react';
import Playground from '../components/playground';
import graphiqlCss from '!css-loader!graphiql/graphiql.css';
import "graphiql/graphiql.css";
// Docusaurus does SSR for custom pages but we need to load GraphiQL in the browser
const GraphiQLComponent = () => {
if (
!window.localStorage.getItem("graphiql:theme") &&
window.localStorage.getItem("theme") != "dark"
) {
window.localStorage.setItem("graphiql:theme", "light");
}
// Docusaurus does SSR for custom pages, but we need to load GraphiQL in the browser
const GraphQlComponent = ({token}) => {
const fetcher = createGraphiQLFetcher({ url: "https://api.twenty.com/graphql" });
// We load graphiql style using useEffect as it breaks remaining docs style
useEffect(()=> {
const styleElement = document.createElement('style')
styleElement.innerHTML = graphiqlCss.toString()
document.head.append(styleElement)
return ()=> styleElement.remove();
}, [])
const fetcher = createGraphiQLFetcher({url: "https://api.twenty.com/graphql"});
return (
<div className="fullHeightPlayground">
<GraphiQL fetcher={fetcher} />;
<GraphiQL
fetcher={fetcher}
defaultHeaders={JSON.stringify({Authorization: `Bearer ${token}`})}
/>
</div>
);
)
}
const graphQL = () => {
const [token , setToken] = useState()
const { setTheme } = useTheme();
useEffect(()=> {
window.localStorage.setItem("graphiql:theme", window.localStorage.getItem("theme") || 'light');
const handleThemeChange = (ev) => {
if(ev.key === 'theme') {
setTheme(ev.newValue as Theme);
}
}
window.addEventListener('storage', handleThemeChange)
return () => window.removeEventListener('storage', handleThemeChange)
}, [])
const children = <GraphQlComponent token={token} />
return (
<Layout
title="GraphQL Playground"
description="GraphQL Playground for Twenty"
>
<BrowserOnly>{
() => <Playground
children={children}
setToken={setToken}
/>
}</BrowserOnly>
</Layout>
)
};
const graphQL = () => (
<Layout
title="GraphQL Playground"
description="GraphQL Playground for Twenty"
>
<BrowserOnly>{() => <GraphiQLComponent />}</BrowserOnly>
</Layout>
);
export default graphQL;

View File

@ -1,25 +1,14 @@
import Layout from "@theme/Layout";
import BrowserOnly from "@docusaurus/BrowserOnly";
import React, { useEffect, useState } from "react";
import Layout from '@theme/Layout';
import BrowserOnly from '@docusaurus/BrowserOnly';
import React, { useEffect, useState } from 'react';
import { API } from '@stoplight/elements';
import Playground from '../components/playground';
import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css';
import restApiCss from '!css-loader!./rest-api.css';
import { parseJson } from "nx/src/utils/json";
import { TbLoader2 } from "react-icons/tb";
type TokenFormProps = {
onSubmit: (token: string) => void,
isTokenValid: boolean,
isLoading: boolean,
token: string,
}
const TokenForm = ({onSubmit, isTokenValid, token, isLoading}: TokenFormProps)=> {
const updateToken = (event: React.ChangeEvent<HTMLInputElement>) => {
localStorage.setItem('TryIt_securitySchemeValues', JSON.stringify({bearerAuth: event.target.value}))
onSubmit(event.target.value)
}
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();
@ -28,103 +17,32 @@ const TokenForm = ({onSubmit, isTokenValid, token, isLoading}: TokenFormProps)=>
return () => styleElement.remove();
}, []);
useEffect(() => {
const styleElement = document.createElement('style');
styleElement.innerHTML = restApiCss.toString();
document.head.append(styleElement);
return () => styleElement.remove();
}, []);
return !isTokenValid && (
<div>
<div className='container'>
<form className="form">
<label>
To load your REST API schema, <a className='link' href='https://app.twenty.com/settings/developers/api-keys'>generate an API key</a> and paste it here:
</label>
<p>
<input
className={(token && !isLoading) ? "input invalid" : "input"}
type='text'
disabled={isLoading}
placeholder='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMD...'
defaultValue={token}
onChange={updateToken}
/>
<p className={`token-invalid ${(!token || isLoading )&& 'not-visible'}`}>Token invalid</p>
<div className='loader-container'>
<TbLoader2 className={`loader ${!isLoading && 'not-visible'}`} />
</div>
</p>
</form>
</div>
</div>
)
}
const RestApiComponent = () => {
const [openApiJson, setOpenApiJson] = useState({})
const [isTokenValid, setIsTokenValid] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const storedToken = parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ?? ''
const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags)
const getJson = async (token: string ) => {
setIsLoading(true)
return await fetch(
"https://api.twenty.com/open-api",
{headers: {Authorization: `Bearer ${token}`}}
)
.then((res)=> res.json())
.then((result)=> {
validateToken(result)
setIsLoading(false)
return result
})
.catch(() => setIsLoading(false))
}
const submitToken = async (token) => {
if (isLoading) return
const json = await getJson(token)
setOpenApiJson(json)
}
useEffect(()=> {
(async ()=> {
await submitToken(storedToken)
})()
},[])
return isTokenValid !== undefined && (
<>
<TokenForm
onSubmit={submitToken}
isTokenValid={isTokenValid}
isLoading={isLoading}
token={storedToken}
/>
{
isTokenValid && (
<API
apiDescriptionDocument={JSON.stringify(openApiJson)}
router="hash"
/>
)
}
</>
return (
<API
apiDescriptionDocument={JSON.stringify(openApiJson)}
router="hash"
/>
)
}
const restApi = () => (
<Layout
title="REST API Playground"
description="REST API Playground for Twenty"
>
<BrowserOnly>{() => <RestApiComponent />}</BrowserOnly>
</Layout>
);
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}
/>
}</BrowserOnly>
</Layout>
)
};
export default restApi;

View File

@ -5,7 +5,6 @@ import { OpenAPIV3 } from 'openapi-types';
import { TokenService } from 'src/core/auth/services/token.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { baseSchema } from 'src/core/open-api/utils/base-schema.utils';
import {
computeManyResultPath,
@ -23,11 +22,10 @@ export class OpenApiService {
constructor(
private readonly tokenService: TokenService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly environmentService: EnvironmentService,
) {}
async generateSchema(request: Request): Promise<OpenAPIV3.Document> {
const schema = baseSchema(this.environmentService.getFrontBaseUrl());
const schema = baseSchema();
let objectMetadataItems;

View File

@ -2,12 +2,12 @@ import { OpenAPIV3 } from 'openapi-types';
import { computeOpenApiPath } from 'src/core/open-api/utils/path.utils';
export const baseSchema = (frontBaseUrl: string): OpenAPIV3.Document => {
export const baseSchema = (): OpenAPIV3.Document => {
return {
openapi: '3.0.3',
info: {
title: 'Twenty Api',
description: `This is a **Twenty REST/API** playground based on the **OpenAPI 3.0 specification**.\n\nTo use the Playground, please log to your twenty account and generate an API key here: ${frontBaseUrl}/settings/developers/api-keys`,
description: `This is a **Twenty REST/API** playground based on the **OpenAPI 3.0 specification**.`,
termsOfService: 'https://github.com/twentyhq/twenty?tab=coc-ov-file',
contact: {
email: 'felix@twenty.com',

View File

@ -42,17 +42,16 @@ const getSchemaComponentsProperties = (
itemProperty.type = 'boolean';
break;
case FieldMetadataType.RELATION:
itemProperty = {
type: 'array',
items: {
type: 'object',
properties: {
node: {
type: 'object',
},
if (field.fromRelationMetadata?.toObjectMetadata.nameSingular) {
itemProperty = {
type: 'array',
items: {
$ref: `#/components/schemas/${capitalize(
field.fromRelationMetadata?.toObjectMetadata.nameSingular || '',
)}`,
},
},
};
};
}
break;
case FieldMetadataType.LINK:
case FieldMetadataType.CURRENCY:
@ -74,7 +73,9 @@ const getSchemaComponentsProperties = (
break;
}
node[field.name] = itemProperty;
if (Object.keys(itemProperty).length) {
node[field.name] = itemProperty;
}
return node;
}, {} as Properties);

View File

@ -339,6 +339,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
'fields.fromRelationMetadata.toObjectMetadata',
],
});
}