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;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .link {
|
||||||
|
color: #a3c0f8;
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
padding: 4px;
|
padding: 6px;
|
||||||
margin: 20px 0 5px 0;
|
margin: 20px 0 5px 0;
|
||||||
max-width: 460px;
|
max-width: 460px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -27,17 +31,19 @@
|
|||||||
background-color: #f3f3f3;
|
background-color: #f3f3f3;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .input {
|
||||||
|
background-color: #16233f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid {
|
.invalid {
|
||||||
border: 1px solid red;
|
border: 1px solid #f83e3e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-invalid {
|
.token-invalid {
|
||||||
color: red;
|
color: #f83e3e;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.not-visible {
|
.not-visible {
|
||||||
@ -50,6 +56,10 @@
|
|||||||
animation: animate 2s infinite;
|
animation: animate 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .loader {
|
||||||
|
color: #a3c0f8;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes animate {
|
@keyframes animate {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
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 { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||||
import { GraphiQL } from "graphiql";
|
import { GraphiQL } from 'graphiql';
|
||||||
import React from "react";
|
import React, { useEffect, useState } from 'react';
|
||||||
import Layout from "@theme/Layout";
|
import Layout from '@theme/Layout';
|
||||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
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
|
// Docusaurus does SSR for custom pages, but we need to load GraphiQL in the browser
|
||||||
const GraphiQLComponent = () => {
|
const GraphQlComponent = ({token}) => {
|
||||||
if (
|
const fetcher = createGraphiQLFetcher({ url: "https://api.twenty.com/graphql" });
|
||||||
!window.localStorage.getItem("graphiql:theme") &&
|
|
||||||
window.localStorage.getItem("theme") != "dark"
|
// We load graphiql style using useEffect as it breaks remaining docs style
|
||||||
) {
|
useEffect(()=> {
|
||||||
window.localStorage.setItem("graphiql:theme", "light");
|
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 (
|
return (
|
||||||
<div className="fullHeightPlayground">
|
<div className="fullHeightPlayground">
|
||||||
<GraphiQL fetcher={fetcher} />;
|
<GraphiQL
|
||||||
|
fetcher={fetcher}
|
||||||
|
defaultHeaders={JSON.stringify({Authorization: `Bearer ${token}`})}
|
||||||
|
/>
|
||||||
</div>
|
</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;
|
export default graphQL;
|
||||||
|
|||||||
@ -1,25 +1,14 @@
|
|||||||
import Layout from "@theme/Layout";
|
import Layout from '@theme/Layout';
|
||||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from 'react';
|
||||||
import { API } from '@stoplight/elements';
|
import { API } from '@stoplight/elements';
|
||||||
|
import Playground from '../components/playground';
|
||||||
import spotlightTheme from '!css-loader!@stoplight/elements/styles.min.css';
|
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 RestApiComponent = ({openApiJson}) => {
|
||||||
const updateToken = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
localStorage.setItem('TryIt_securitySchemeValues', JSON.stringify({bearerAuth: event.target.value}))
|
|
||||||
onSubmit(event.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const styleElement = document.createElement('style');
|
const styleElement = document.createElement('style');
|
||||||
styleElement.innerHTML = spotlightTheme.toString();
|
styleElement.innerHTML = spotlightTheme.toString();
|
||||||
@ -28,103 +17,32 @@ const TokenForm = ({onSubmit, isTokenValid, token, isLoading}: TokenFormProps)=>
|
|||||||
return () => styleElement.remove();
|
return () => styleElement.remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
const styleElement = document.createElement('style');
|
<API
|
||||||
styleElement.innerHTML = restApiCss.toString();
|
apiDescriptionDocument={JSON.stringify(openApiJson)}
|
||||||
document.head.append(styleElement);
|
router="hash"
|
||||||
|
/>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const restApi = () => (
|
const restApi = () => {
|
||||||
<Layout
|
const [openApiJson, setOpenApiJson] = useState({})
|
||||||
title="REST API Playground"
|
|
||||||
description="REST API Playground for Twenty"
|
const children = <RestApiComponent openApiJson={openApiJson} />
|
||||||
>
|
|
||||||
<BrowserOnly>{() => <RestApiComponent />}</BrowserOnly>
|
return (
|
||||||
</Layout>
|
<Layout
|
||||||
);
|
title="REST API Playground"
|
||||||
|
description="REST API Playground for Twenty"
|
||||||
|
>
|
||||||
|
<BrowserOnly>{
|
||||||
|
() => <Playground
|
||||||
|
children={children}
|
||||||
|
setOpenApiJson={setOpenApiJson}
|
||||||
|
/>
|
||||||
|
}</BrowserOnly>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
export default restApi;
|
export default restApi;
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { OpenAPIV3 } from 'openapi-types';
|
|||||||
|
|
||||||
import { TokenService } from 'src/core/auth/services/token.service';
|
import { TokenService } from 'src/core/auth/services/token.service';
|
||||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.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 { baseSchema } from 'src/core/open-api/utils/base-schema.utils';
|
||||||
import {
|
import {
|
||||||
computeManyResultPath,
|
computeManyResultPath,
|
||||||
@ -23,11 +22,10 @@ export class OpenApiService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly objectMetadataService: ObjectMetadataService,
|
private readonly objectMetadataService: ObjectMetadataService,
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateSchema(request: Request): Promise<OpenAPIV3.Document> {
|
async generateSchema(request: Request): Promise<OpenAPIV3.Document> {
|
||||||
const schema = baseSchema(this.environmentService.getFrontBaseUrl());
|
const schema = baseSchema();
|
||||||
|
|
||||||
let objectMetadataItems;
|
let objectMetadataItems;
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import { OpenAPIV3 } from 'openapi-types';
|
|||||||
|
|
||||||
import { computeOpenApiPath } from 'src/core/open-api/utils/path.utils';
|
import { computeOpenApiPath } from 'src/core/open-api/utils/path.utils';
|
||||||
|
|
||||||
export const baseSchema = (frontBaseUrl: string): OpenAPIV3.Document => {
|
export const baseSchema = (): OpenAPIV3.Document => {
|
||||||
return {
|
return {
|
||||||
openapi: '3.0.3',
|
openapi: '3.0.3',
|
||||||
info: {
|
info: {
|
||||||
title: 'Twenty Api',
|
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',
|
termsOfService: 'https://github.com/twentyhq/twenty?tab=coc-ov-file',
|
||||||
contact: {
|
contact: {
|
||||||
email: 'felix@twenty.com',
|
email: 'felix@twenty.com',
|
||||||
|
|||||||
@ -42,17 +42,16 @@ const getSchemaComponentsProperties = (
|
|||||||
itemProperty.type = 'boolean';
|
itemProperty.type = 'boolean';
|
||||||
break;
|
break;
|
||||||
case FieldMetadataType.RELATION:
|
case FieldMetadataType.RELATION:
|
||||||
itemProperty = {
|
if (field.fromRelationMetadata?.toObjectMetadata.nameSingular) {
|
||||||
type: 'array',
|
itemProperty = {
|
||||||
items: {
|
type: 'array',
|
||||||
type: 'object',
|
items: {
|
||||||
properties: {
|
$ref: `#/components/schemas/${capitalize(
|
||||||
node: {
|
field.fromRelationMetadata?.toObjectMetadata.nameSingular || '',
|
||||||
type: 'object',
|
)}`,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
break;
|
break;
|
||||||
case FieldMetadataType.LINK:
|
case FieldMetadataType.LINK:
|
||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
@ -74,7 +73,9 @@ const getSchemaComponentsProperties = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
node[field.name] = itemProperty;
|
if (Object.keys(itemProperty).length) {
|
||||||
|
node[field.name] = itemProperty;
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}, {} as Properties);
|
}, {} as Properties);
|
||||||
|
|||||||
@ -339,6 +339,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
'fields',
|
'fields',
|
||||||
'fields.fromRelationMetadata',
|
'fields.fromRelationMetadata',
|
||||||
'fields.toRelationMetadata',
|
'fields.toRelationMetadata',
|
||||||
|
'fields.fromRelationMetadata.toObjectMetadata',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user