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:
27
packages/twenty-docs/src/components/playground.tsx
Normal file
27
packages/twenty-docs/src/components/playground.tsx
Normal 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;
|
||||
@ -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);
|
||||
103
packages/twenty-docs/src/components/token-form.tsx
Normal file
103
packages/twenty-docs/src/components/token-form.tsx
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -339,6 +339,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
'fields',
|
||||
'fields.fromRelationMetadata',
|
||||
'fields.toRelationMetadata',
|
||||
'fields.fromRelationMetadata.toObjectMetadata',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user