diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsApiKeys.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsApiKeys.tsx index 4f2aa21d56..6b0d4e09a1 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsApiKeys.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsApiKeys.tsx @@ -46,7 +46,7 @@ export const SettingsApiKeys = () => {
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index 455ae82f9c..1cac24584c 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -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); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts index 0f3198ce62..6c3974b41c 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts @@ -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', diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts index 3f9cacf424..ad3c11cf53 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/base-schema.utils.ts @@ -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 +\`\`\` + +Example cURL: + +\`\`\`bash +curl -H 'Authorization: Bearer ' /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 ' \\ + '/rest/core/companies?limit=60' + +# Next page +curl -H 'Authorization: Bearer ' \\ + '/rest/core/companies?limit=60&starting_after=' + +# Previous page +curl -H 'Authorization: Bearer ' \\ + '/rest/core/companies?limit=60&ending_before=' +\`\`\` + +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 ?? ''} +Metadata: ${serverUrl}/rest/open-api/metadata?token=${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 ?? ''} + +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: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index 1b4a3dfacb..01709e1260 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -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', diff --git a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx index 28d05fbe41..d6f559c2a1 100644 --- a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx @@ -164,6 +164,8 @@ export const CodeEditor = ({ onValidate?.(markers); }} options={{ + formatOnPaste: true, + formatOnType: true, overviewRulerLanes: 0, scrollbar: { vertical: 'hidden',