Improve REST API Docs (#13931)
Various improvements to the REST API docs as we often get questions (misconceptions on how to use filters, how to use with LLMs, etc.)
This commit is contained in:
@@ -46,7 +46,7 @@ export const SettingsApiKeys = () => {
|
||||
<StyledContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Playground`}
|
||||
title={t`Documentation`}
|
||||
description={t`Try our REST or GraphQL API playgrounds.`}
|
||||
/>
|
||||
<StyledSettingsApiPlaygroundCoverImage />
|
||||
|
||||
@@ -84,7 +84,12 @@ export class OpenApiService {
|
||||
`${request.protocol}://${request.get('host')}`,
|
||||
);
|
||||
|
||||
const schema = baseSchema('core', baseUrl);
|
||||
const tokenFromQuery = request.query.token;
|
||||
const schema = baseSchema(
|
||||
'core',
|
||||
baseUrl,
|
||||
typeof tokenFromQuery === 'string' ? tokenFromQuery : undefined,
|
||||
);
|
||||
|
||||
const workspace = await this.getWorkspaceFromRequest(request);
|
||||
|
||||
@@ -171,7 +176,12 @@ export class OpenApiService {
|
||||
`${request.protocol}://${request.get('host')}`,
|
||||
);
|
||||
|
||||
const schema = baseSchema('metadata', baseUrl);
|
||||
const tokenFromQuery = request.query.token;
|
||||
const schema = baseSchema(
|
||||
'metadata',
|
||||
baseUrl,
|
||||
typeof tokenFromQuery === 'string' ? tokenFromQuery : undefined,
|
||||
);
|
||||
|
||||
const workspace = await this.getWorkspaceFromRequest(request);
|
||||
|
||||
|
||||
+7
-26
@@ -1,9 +1,5 @@
|
||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
|
||||
import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
|
||||
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
import {
|
||||
computeDepthParameters,
|
||||
computeEndingBeforeParameters,
|
||||
@@ -36,12 +32,8 @@ describe('computeParameters', () => {
|
||||
expect(computeOrderByParameters()).toEqual({
|
||||
name: 'order_by',
|
||||
in: 'query',
|
||||
description: `Sorts objects returned.
|
||||
Should have the following shape: **field_name_1,field_name_2[DIRECTION_2],...**
|
||||
Available directions are **${Object.values(OrderByDirection).join(
|
||||
'**, **',
|
||||
)}**.
|
||||
Default direction is **${DEFAULT_ORDER_DIRECTION}**`,
|
||||
description: `Format: **field_name_1,field_name_2[DIRECTION_2]
|
||||
Refer to the filter section at the top of the page for more details.`,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
@@ -65,9 +57,9 @@ describe('computeParameters', () => {
|
||||
name: 'depth',
|
||||
in: 'query',
|
||||
description: `Determines the level of nested related objects to include in the response.
|
||||
- 0: Returns only the primary object's information.
|
||||
- 1: Returns the primary object along with its directly related objects (with no additional nesting for related objects).
|
||||
- 2: Returns the primary object, its directly related objects, and the related objects of those related objects.`,
|
||||
- 0: Primary object only
|
||||
- 1: Primary object + direct relations
|
||||
- 2: Primary object + direct relations + nested relations`,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
@@ -82,19 +74,8 @@ describe('computeParameters', () => {
|
||||
expect(computeFilterParameters()).toEqual({
|
||||
name: 'filter',
|
||||
in: 'query',
|
||||
description: `Filters objects returned.
|
||||
Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2...
|
||||
To filter on composite type fields use **field.subField[COMPARATOR]:value_1
|
||||
**
|
||||
Available comparators are **${Object.values(FilterComparators).join(
|
||||
'**, **',
|
||||
)}**.
|
||||
You can create more complex filters using conjunctions **${Object.values(
|
||||
Conjunctions,
|
||||
).join('**, **')}**.
|
||||
Default root conjunction is **${DEFAULT_CONJUNCTION}**.
|
||||
To filter **null** values use **field[is]:NULL** or **field[is]:NOT_NULL**
|
||||
To filter using **boolean** values use **field[eq]:true** or **field[eq]:false**`,
|
||||
description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2
|
||||
Refer to the filter section at the top of the page for more details.`,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
|
||||
+107
-1
@@ -7,12 +7,118 @@ export const API_Version = 'v0.1';
|
||||
export const baseSchema = (
|
||||
schemaName: 'core' | 'metadata',
|
||||
serverUrl: string,
|
||||
token?: string,
|
||||
): OpenAPIV3_1.Document => {
|
||||
return {
|
||||
openapi: '3.1.1',
|
||||
info: {
|
||||
title: 'Twenty Api',
|
||||
description: `This is a **Twenty REST/API** playground based on the **OpenAPI 3.1 specification**.`,
|
||||
description: `Use this page to explore and call the **REST API**.
|
||||
|
||||
## Authentication
|
||||
|
||||
Send a Bearer token with each request:
|
||||
|
||||
\`\`\`http
|
||||
Authorization: Bearer <token>
|
||||
\`\`\`
|
||||
|
||||
Example cURL:
|
||||
|
||||
\`\`\`bash
|
||||
curl -H 'Authorization: Bearer <token>' <server>/rest/core/companies
|
||||
\`\`\`
|
||||
|
||||
Tokens can be generated in Settings → Playground and are workspace-scoped.
|
||||
|
||||
|
||||
## Filters
|
||||
|
||||
Use the \`filter\` query parameter to narrow results.
|
||||
|
||||
- Format: \`field[COMPARATOR]:value\`
|
||||
- Multiple conditions: \`field1[eq]:1,field2[gte]:10\` (root conjunction is AND)
|
||||
- Composite fields: \`field.subField[COMPARATOR]:value\`
|
||||
- Common comparators: \`eq\`, \`neq\`, \`in\`, \`containsAny\`, \`is\`, \`gt\`, \`gte\`, \`lt\`, \`lte\`, \`startsWith\`, \`like\`, \`ilike\`
|
||||
|
||||
Examples:
|
||||
|
||||
\`\`\`text
|
||||
filter=status[eq]:"open"
|
||||
filter=createdAt[gte]:"2024-01-01"
|
||||
filter=owner.name[ilike]:"%smith%"
|
||||
filter=id[in]:["id-1","id-2"]
|
||||
filter=deletedAt[is]:NULL
|
||||
filter=isActive[eq]:true
|
||||
\`\`\`
|
||||
|
||||
Advanced (optional): \`and(...)\`, \`or(...)\`, \`not(...)\` (\`not\` wraps one condition)
|
||||
|
||||
\`\`\`text
|
||||
filter=or(status[eq]:"open",assigneeId[is]:NULL)
|
||||
\`\`\`
|
||||
|
||||
Notes: Strings and dates are quoted; numbers are not.
|
||||
|
||||
## Pagination and ordering
|
||||
|
||||
All list endpoints use cursor-based pagination.
|
||||
|
||||
- Use **limit** to cap page size (default: 60, max: 60).
|
||||
- Use **starting_after** to fetch the next page (forward).
|
||||
- Use **ending_before** to fetch the previous page (backward).
|
||||
- Responses include **pageInfo** with \`hasNextPage\`, \`startCursor\`, and \`endCursor\`.
|
||||
|
||||
Examples:
|
||||
|
||||
\`\`\`bash
|
||||
# First page
|
||||
curl -H 'Authorization: Bearer <token>' \\
|
||||
'<server>/rest/core/companies?limit=60'
|
||||
|
||||
# Next page
|
||||
curl -H 'Authorization: Bearer <token>' \\
|
||||
'<server>/rest/core/companies?limit=60&starting_after=<endCursorFromPreviousPage>'
|
||||
|
||||
# Previous page
|
||||
curl -H 'Authorization: Bearer <token>' \\
|
||||
'<server>/rest/core/companies?limit=60&ending_before=<startCursorFromCurrentPage>'
|
||||
\`\`\`
|
||||
|
||||
You can combine pagination with filters and ordering.
|
||||
|
||||
Ordering with \`order_by\`:
|
||||
- Shape: \`field1,field2[DIRECTION2]\`
|
||||
- Directions: AscNullsFirst, AscNullsLast, DescNullsFirst, DescNullsLast
|
||||
- Default per-field direction: AscNullsFirst
|
||||
|
||||
Examples:
|
||||
\`\`\`text
|
||||
order_by=createdAt
|
||||
order_by=id[AscNullsFirst],createdAt[DescNullsLast]
|
||||
\`\`\`
|
||||
|
||||
## Usage with LLMs
|
||||
|
||||
You can use AI to generate code based on the OpenAPI schema with the following URLs:
|
||||
|
||||
\`\`\`text
|
||||
Core: ${serverUrl}/rest/open-api/core?token=${token ?? '<your_token>'}
|
||||
Metadata: ${serverUrl}/rest/open-api/metadata?token=${token ?? '<your_token>'}
|
||||
\`\`\`
|
||||
|
||||
Quick prompt example (Cursor or any agent):
|
||||
|
||||
\`\`\`text
|
||||
Here is an OpenAPI schema for the Twenty REST API:\n${serverUrl}/rest/open-api/core?token=${token ?? '<your_token>'}
|
||||
|
||||
Use it to list companies created after 2024-01-01, ordered by createdAt desc, and include only 20 results.
|
||||
\`\`\`
|
||||
|
||||
Notes:
|
||||
- Treat the token like a secret; prefer a short-lived Playground token.
|
||||
- Most editors can fetch and process the schema even if it's large.
|
||||
`,
|
||||
termsOfService:
|
||||
'https://github.com/twentyhq/twenty?tab=coc-ov-file#readme',
|
||||
contact: {
|
||||
|
||||
@@ -2,11 +2,6 @@ import { type OpenAPIV3_1 } from 'openapi-types';
|
||||
|
||||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
|
||||
import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
|
||||
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
|
||||
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory';
|
||||
|
||||
export const computeLimitParameters = (
|
||||
fromMetadata = false,
|
||||
): OpenAPIV3_1.ParameterObject => {
|
||||
@@ -28,12 +23,8 @@ export const computeOrderByParameters = (): OpenAPIV3_1.ParameterObject => {
|
||||
return {
|
||||
name: 'order_by',
|
||||
in: 'query',
|
||||
description: `Sorts objects returned.
|
||||
Should have the following shape: **field_name_1,field_name_2[DIRECTION_2],...**
|
||||
Available directions are **${Object.values(OrderByDirection).join(
|
||||
'**, **',
|
||||
)}**.
|
||||
Default direction is **${DEFAULT_ORDER_DIRECTION}**`,
|
||||
description: `Format: **field_name_1,field_name_2[DIRECTION_2]
|
||||
Refer to the filter section at the top of the page for more details.`,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
@@ -56,9 +47,9 @@ export const computeDepthParameters = (): OpenAPIV3_1.ParameterObject => {
|
||||
name: 'depth',
|
||||
in: 'query',
|
||||
description: `Determines the level of nested related objects to include in the response.
|
||||
- 0: Returns only the primary object's information.
|
||||
- 1: Returns the primary object along with its directly related objects (with no additional nesting for related objects).
|
||||
- 2: Returns the primary object, its directly related objects, and the related objects of those related objects.`,
|
||||
- 0: Primary object only
|
||||
- 1: Primary object + direct relations
|
||||
- 2: Primary object + direct relations + nested relations`,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
@@ -72,20 +63,8 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => {
|
||||
return {
|
||||
name: 'filter',
|
||||
in: 'query',
|
||||
description: `Filters objects returned.
|
||||
Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2...
|
||||
To filter on composite type fields use **field.subField[COMPARATOR]:value_1
|
||||
**
|
||||
Available comparators are **${Object.values(FilterComparators).join(
|
||||
'**, **',
|
||||
)}**.
|
||||
You can create more complex filters using conjunctions **${Object.values(
|
||||
Conjunctions,
|
||||
).join('**, **')}**.
|
||||
Default root conjunction is **${DEFAULT_CONJUNCTION}**.
|
||||
To filter **null** values use **field[is]:NULL** or **field[is]:NOT_NULL**
|
||||
To filter using **boolean** values use **field[eq]:true** or **field[eq]:false**`,
|
||||
|
||||
description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2
|
||||
Refer to the filter section at the top of the page for more details.`,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
|
||||
@@ -164,6 +164,8 @@ export const CodeEditor = ({
|
||||
onValidate?.(markers);
|
||||
}}
|
||||
options={{
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
overviewRulerLanes: 0,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
|
||||
Reference in New Issue
Block a user