From ed7bd0ba260ac467f5b48b78233347962423e519 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 20 Dec 2023 12:01:55 +0100 Subject: [PATCH] 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 --- .../twenty-docs/src/components/playground.tsx | 27 ++++ .../token-form.css} | 18 ++- .../twenty-docs/src/components/token-form.tsx | 103 +++++++++++++ packages/twenty-docs/src/pages/graphql.tsx | 86 +++++++---- packages/twenty-docs/src/pages/rest-api.tsx | 142 ++++-------------- .../src/core/open-api/open-api.service.ts | 4 +- .../core/open-api/utils/base-schema.utils.ts | 4 +- .../core/open-api/utils/components.utils.ts | 23 +-- .../object-metadata.service.ts | 1 + 9 files changed, 250 insertions(+), 158 deletions(-) create mode 100644 packages/twenty-docs/src/components/playground.tsx rename packages/twenty-docs/src/{pages/rest-api.css => components/token-form.css} (79%) create mode 100644 packages/twenty-docs/src/components/token-form.tsx diff --git a/packages/twenty-docs/src/components/playground.tsx b/packages/twenty-docs/src/components/playground.tsx new file mode 100644 index 000000000..5f908ef83 --- /dev/null +++ b/packages/twenty-docs/src/components/playground.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import TokenForm, { TokenFormProps } from '../components/token-form'; + +const Playground = ( + { + children, + setOpenApiJson, + setToken + }: Partial +) => { + const [isTokenValid, setIsTokenValid] = useState(false) + return ( + <> + + { + isTokenValid && children + } + + ) +} + +export default Playground; diff --git a/packages/twenty-docs/src/pages/rest-api.css b/packages/twenty-docs/src/components/token-form.css similarity index 79% rename from packages/twenty-docs/src/pages/rest-api.css rename to packages/twenty-docs/src/components/token-form.css index ad68c5679..9defb0263 100644 --- a/packages/twenty-docs/src/pages/rest-api.css +++ b/packages/twenty-docs/src/components/token-form.css @@ -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); diff --git a/packages/twenty-docs/src/components/token-form.tsx b/packages/twenty-docs/src/components/token-form.tsx new file mode 100644 index 000000000..b39f5603c --- /dev/null +++ b/packages/twenty-docs/src/components/token-form.tsx @@ -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) => { + 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 && ( +
+
+
+ +

+ + Token invalid +

+ +
+

+
+
+
+ ) +} + +export default TokenForm; diff --git a/packages/twenty-docs/src/pages/graphql.tsx b/packages/twenty-docs/src/pages/graphql.tsx index a5c29c235..23dd8adcb 100644 --- a/packages/twenty-docs/src/pages/graphql.tsx +++ b/packages/twenty-docs/src/pages/graphql.tsx @@ -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 (
- ; +
- ); + ) +} + +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 = + + return ( + + { + () => + } + + ) }; -const graphQL = () => ( - - {() => } - -); - export default graphQL; diff --git a/packages/twenty-docs/src/pages/rest-api.tsx b/packages/twenty-docs/src/pages/rest-api.tsx index 9d0873b07..de1c989d3 100644 --- a/packages/twenty-docs/src/pages/rest-api.tsx +++ b/packages/twenty-docs/src/pages/rest-api.tsx @@ -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) => { - 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 && ( -
-
-
- -

- -

Token invalid

-
- -
-

-
-
-
- ) -} - -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 && ( - <> - - { - isTokenValid && ( - - ) - } - + return ( + ) } -const restApi = () => ( - - {() => } - -); +const restApi = () => { + const [openApiJson, setOpenApiJson] = useState({}) + + const children = + + return ( + + { + () => + } + + ) +}; export default restApi; 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 4d3bfc0fb..80409e429 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 @@ -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 { - const schema = baseSchema(this.environmentService.getFrontBaseUrl()); + const schema = baseSchema(); let objectMetadataItems; 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 640879acf..2b07d60ca 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,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', 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 fc686963b..731114955 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 @@ -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); diff --git a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts index 80c3db808..e5192e81d 100644 --- a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts @@ -339,6 +339,7 @@ export class ObjectMetadataService extends TypeOrmQueryService