Compare commits

..

60 Commits

Author SHA1 Message Date
Félix Malfait f5b2c71943 Fix 2026-03-31 18:28:16 +02:00
Félix Malfait 74e6e20921 Merge branch 'feat/agent-chat-message-queue' of github.com:twentyhq/twenty into pr/19118 2026-03-31 17:57:07 +02:00
Félix Malfait c3ee312d26 Fix client SDK generated types: make turnId nullable
The DTO exposes turnId as nullable for queued messages, update
generated schema to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 17:45:52 +02:00
Félix Malfait 6cdca784fd Merge branch 'main' into feat/agent-chat-message-queue 2026-03-31 17:31:36 +02:00
Baptiste Devessier 2612145436 Fix sdk tests (#19172) 2026-03-31 15:08:21 +00:00
github-actions[bot] 4c97642258 i18n - translations (#19171)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 16:33:53 +02:00
nitin 08f019e9c9 [AI] More tab new design (#19165)
closes
https://discord.com/channels/1130383047699738754/1480981616666087687

<img width="1596" height="1318" alt="CleanShot 2026-03-31 at 18 06 37"
src="https://github.com/user-attachments/assets/0d87610e-e4ce-4f3d-8047-97d242f230c5"
/>
2026-03-31 16:26:40 +02:00
Marie 888fa271f0 [Apps SDK] Fix rich app link in documentation (#19007)
- Fix link to rich app for LLMS
- Add example of extension of existing object in rich app (post card
app)
2026-03-31 13:35:57 +00:00
github-actions[bot] bb5c64952c i18n - translations (#19169)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 15:29:13 +02:00
github-actions[bot] 6dedb35a1f i18n - translations (#19168)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 15:22:30 +02:00
Weiko 436333f110 Add see records link to data model (#19163)
## Context
Add a link to the INDEX view page of an object to list all its records
directly from the data model page of the object.

<img width="556" height="460" alt="Screenshot 2026-03-31 at 14 04 20"
src="https://github.com/user-attachments/assets/3daa9ee5-ad09-42b5-a406-088ce8c53be4"
/>
2026-03-31 13:12:43 +00:00
Abdul Rahman ec283b8f2d Fix dropping workspace nav items at the bottom of the list (#18989)
https://github.com/user-attachments/assets/64abd955-892e-48c8-bf7b-d4d8645cf33e

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-31 13:06:31 +00:00
github-actions[bot] c3b969ab74 i18n - translations (#19166)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 14:55:02 +02:00
Weiko 287fe90ce9 Add link to workspace members index view page from the settings (#19164)
## Context
Fixes https://github.com/twentyhq/core-team-issues/issues/2039

<img width="608" height="519" alt="Screenshot 2026-03-31 at 14 28 00"
src="https://github.com/user-attachments/assets/939e47c6-84c2-471c-8da5-b76b161f086a"
/>
2026-03-31 12:39:22 +00:00
Thomas Trompette d10c0a6439 Fix: composite update events not received (#19053)
Database batch events (update / insert / soft-delete / hard-delete) were
building recordsBefore / recordsAfter after formatResult ran twice: once
inside WorkspaceSelectQueryBuilder.getMany() / getOne(), and again in
the CUD query builders. On the second pass, already-shaped composite
fields (e.g. emails) went through formatFieldMetadataValue and kept the
same object references as the live TypeORM row, so recordsBefore could
change when the entity was updated—breaking workflow triggers and diffs.
2026-03-31 11:58:12 +00:00
BugIsGod 176e81cd76 fix: clear navigation stack immediately in goBackFromSidePanel (#19153)
Fixes: #19152 

## Summary
goBackFromSidePanel returned early without writing the new (empty) stack
to the store, leaving stale navigation state. And it caused
openRecordInSidePanel to think the record was already open and skip
reopening.
<img width="587" height="405" alt="image"
src="https://github.com/user-attachments/assets/eb36f4c6-43ca-4c08-891a-c592ff673837"
/>

## Before


https://github.com/user-attachments/assets/03d1e24a-6d1e-4efc-93c1-72be0bc31a89


## After
When close the side panel with Escape, now it can be opened by clicking
arrow button again.




https://github.com/user-attachments/assets/ae700d41-4f81-4d1a-af9a-f97f31f489a6

---------

Co-authored-by: Devessier <baptiste@devessier.fr>
2026-03-31 11:29:03 +00:00
Charles Bochet 1ce4da5b67 Fix upgrade commands 2 (#19157) 2026-03-31 12:47:01 +02:00
github-actions[bot] ef66d6b337 i18n - translations (#19158)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 12:23:19 +02:00
BugIsGod bcca5d0002 fix: show disabled file chip when file no longer exists in timeline (#19045)
Fixes: #18943 
Follow-up pr: #19001

## Summary:                                                             
- Timeline activity shows file upload history, but deleted files had no
signed URL and were still rendered as clickable — clicking did nothing
- grab the fileId from properties.diff.after, look it up in the current
record's files field: if present, use live signed URL; if absent, mark
as deleted
- Deleted file chips show line-through label, not-allowed cursor, and
"File no longer exists" tooltip on hover
  


https://github.com/user-attachments/assets/5df6a675-0003-4fd1-ad57-a07e4338923f

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-03-31 10:07:19 +00:00
Weiko e0630b8653 Fix TransactionNotStartedError (#19155)
## Context
Checked in the codebase where we are trying to rollback a non-active
transaction.


packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts
seemed to be the only place where it happens.

We could enforce this with a lint rule in the future 🤔
2026-03-31 10:02:50 +00:00
Félix Malfait 9573e11879 Fix front typecheck and update client SDK generated types
- Cast status to union type in mapDBMessagesToUIMessages
- Update client SDK generated schema/types to include status field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 09:57:51 +02:00
Paul Rastoin 61a27984e8 0.8.0.canary.7 bump (#19150)
Already published on npm
2026-03-31 09:50:28 +02:00
Félix Malfait 717989d36d Fix circular dependency and improve code quality
- Extract STREAM_AGENT_CHAT_JOB_NAME and StreamAgentChatJobData to
  separate types file to break circular dependency between
  stream-agent-chat.job.ts and agent-chat-streaming.service.ts
- Optimize flushNextQueuedMessage to check queued messages first
  (lightweight query) before loading full conversation
- Add missing state field to reasoning parts in server-side mapper
- Use entity status value in controller response instead of string literal
- Narrow ExtendedUIMessage.status to 'queued' | 'sent' union type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 09:45:09 +02:00
Félix Malfait e6cfa46998 fix: typecheck errors for status field and reasoning part
- Add optional status field to ExtendedUIMessage in twenty-shared
- Remove invalid providerOptions from reasoning part mapper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 09:34:03 +02:00
Félix Malfait d4acf77338 fix: migration schema prefix, review fixes
- Add "core" schema prefix to migration SQL (fixes CI)
- Remove unused modelId param from queueMessage
- Scope promoteQueuedMessage to QUEUED status only
- Use AgentMessageStatus enum instead of string literals
- Create proper mapDBPartsToUIMessageParts server-side mapper
  (fixes lossy mapping that dropped tool calls, reasoning, etc.)
- Merge getQueuedMessages + getMessagesForThread into single query
- Pass hasTitle from job data to avoid redundant thread lookup
- Remove modelId from frontend queue POST body

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 09:28:18 +02:00
github-actions[bot] fd21d0c6ca i18n - docs translations (#19142)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-31 00:23:38 +02:00
Félix Malfait 220b32b19b feat: server-side message queue for agent chat
Replace localStorage-based frontend queue with server-side persistence.
When a user sends a message while streaming, the message is stored in
the database with status='queued'. When streaming finishes, the worker
automatically picks up the next queued message and starts processing it.

- Add `status` column (queued/sent) and make `turnId` nullable on AgentMessage
- Add REST endpoints POST/:threadId/queue and DELETE/:threadId/queue/:messageId
- Add auto-flush logic in stream job's finally block
- Frontend reads queued messages from GraphQL instead of localStorage
- Delete queued messages via REST DELETE endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 22:40:50 +02:00
github-actions[bot] 8d539f0e49 i18n - docs translations (#19139)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 22:30:14 +02:00
Félix Malfait f8d92f9626 feat: queue messages sent while AI is streaming
When the user sends a message while the AI is still streaming a
response, instead of silently dropping it, queue it per-thread.
The queue is persisted to localStorage so it survives page refreshes.
When streaming completes, the next queued message is automatically
sent.

- New Jotai state: agentChatQueuedMessagesByThreadIdState (localStorage-backed)
- New UI component: AIChatQueuedMessages showing queued items with remove buttons
- Modified handleSendMessage to push to queue when streaming is active
- Added flushNextQueuedMessage to auto-send after stream completion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 22:28:16 +02:00
github-actions[bot] a6cecdbd49 i18n - docs translations (#19137)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 20:38:05 +02:00
Raphaël Bosi 383935d0d9 Fix widgets drag handles (#19133)
- Remove drag handles and cursor resize for the placeholder
- Fix the drag handles no longer appearing on hover and always present
drag handle for north and south

This was due to the linaria migration: adding a minus sign or px after a
css variable isn't working, we have to use calc.

## Before


https://github.com/user-attachments/assets/cba398ab-92c8-4924-ba3c-1a28fb4fd163

## After


https://github.com/user-attachments/assets/aec675cd-b809-434d-a8a7-edb12ce37364
2026-03-30 17:07:49 +00:00
Paul Rastoin 37908114fc [SDK] Extract twenty-front-component-renderer outside of twenty-sdk ( 2.8MB ) (#19021)
Followup https://github.com/twentyhq/twenty/pull/19010

## Dependency diagram

```
┌─────────────────────┐
│     twenty-front    │
│   (React frontend)  │
└─────────┬───────────┘
          │ imports runtime:
          │   FrontComponentRenderer
          │   FrontComponentRendererWithSdkClient
          │   useFrontComponentExecutionContext
          ▼
┌──────────────────────────────────┐         ┌─────────────────────────┐
│ twenty-front-component-renderer  │────────▶│       twenty-sdk        │
│   (remote-dom host + worker)     │         │  (app developer SDK)    │
│                                  │         │                         │
│  imports from twenty-sdk:        │         │  Public API:            │
│   • types only:                  │         │   defineFrontComponent  │
│     FrontComponentExecutionContext│         │   navigate, closeSide…  │
│     NavigateFunction             │         │   useFrontComponent…    │
│     CloseSidePanelFunction       │         │   Command components    │
│     CommandConfirmation…         │         │   conditional avail.    │
│     OpenCommandConfirmation…     │         │                         │
│     EnqueueSnackbarFunction      │         │  Internal only:         │
│     etc.                         │         │   frontComponentHost…   │
│                                  │         │   front-component-build │
│  owns locally:                   │         │   esbuild plugins       │
│   • ALLOWED_HTML_ELEMENTS        │         │                         │
│   • EVENT_TO_REACT               │         └────────────┬────────────┘
│   • HTML_TAG_TO_CUSTOM_ELEMENT…  │                      │
│   • SerializedEventData          │                      │ types
│   • PropertySchema               │                      ▼
│   • frontComponentHostComm…      │         ┌─────────────────────────┐
│     (local ref to globalThis)    │         │     twenty-shared       │
│   • setFrontComponentExecution…  │         │  (common types/utils)   │
│     (local impl, same keys)      │         │   AppPath, SidePanelP…  │
│                                  │         │   EnqueueSnackbarParams │
└──────────────────────────────────┘         │   isDefined, …          │
          │                                  └─────────────────────────┘
          │ also depends on
          ▼
    twenty-shared (types)
    @remote-dom/* (runtime)
    @quilted/threads (runtime)
    react (runtime)
```

**Key points:**

- **`twenty-front`** depends on the renderer, **not** on `twenty-sdk`
directly (for rendering)
- **`twenty-front-component-renderer`** depends on `twenty-sdk` for
**types only** (function signatures, `FrontComponentExecutionContext`).
The runtime bridge (`frontComponentHostCommunicationApi`) is shared via
`globalThis` keys, not module imports
- **`twenty-sdk`** has no dependency on the renderer — clean one-way
dependency
- The renderer owns all remote-dom infrastructure (element schemas,
event mappings, custom element tags) that was previously leaking through
the SDK's public API
- The SDK's `./build` entry point was removed entirely (unused)
2026-03-30 17:06:06 +00:00
github-actions[bot] 369ae2862f i18n - docs translations (#19135)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 18:40:35 +02:00
github-actions[bot] 5bde41ebbb i18n - translations (#19134)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 18:30:38 +02:00
Félix Malfait 8fa3962e1c feat: add resumable stream support for agent chat (#19107)
## Overview
Add resumable stream support for agent chat to allow clients to
reconnect and resume streaming responses if the connection is
interrupted (e.g., during page refresh).

## Changes

### Backend (Twenty Server)
- Add `activeStreamId` column to `AgentChatThreadEntity` to track
ongoing streams
- Create `AgentChatResumableStreamService` to manage Redis-backed
resumable streams using the `resumable-stream` library with ioredis
- Extend `AgentChatController` with:
  - `GET /:threadId/stream` endpoint to resume an existing stream
  - `DELETE /:threadId/stream` endpoint to stop an active stream
- Update `AgentChatStreamingService` to store streams in Redis and track
active stream IDs
- Add `resumable-stream@^2.2.12` dependency to package.json

### Frontend (Twenty Front)
- Update `useAgentChat` hook to:
- Use a persistent transport with `prepareReconnectToStreamRequest` for
resumable streams
  - Export `resumeStream` function from useChat
  - Add `handleStop` callback to clear active stream on DELETE endpoint
- Use thread ID as stable message ID instead of including message count
- Add stream resumption logic in `AgentChatAiSdkStreamEffect` component
to automatically call `resumeStream()` when switching threads

## Database Migration
New migration `1774003611071-add-active-stream-id-to-agent-chat-thread`
adds the `activeStreamId` column to store the current resumable stream
identifier.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 18:19:25 +02:00
Weiko ca00e8dece Fix databaseSchema migration (#19132) 2026-03-30 18:01:19 +02:00
Thomas des Francs 2263e14394 clean paddings in favorites section (#19112)
## Summary
- hide the Favorites section wrapper when there are no top-level
favorites and no folder creation in progress
- remove the extra top padding from the Favorites list so the section
title-to-content gap matches other navigation sections
- prevent the empty animated container from adding stray space in the
navbar

## screens

Before
<img width="926" height="820" alt="CleanShot 2026-03-30 at 12 02 11@2x"
src="https://github.com/user-attachments/assets/eac53f86-1108-42ac-986f-328327fbe947"
/>

<img width="1602" height="1168" alt="CleanShot 2026-03-30 at 12 02
29@2x"
src="https://github.com/user-attachments/assets/6249c7f9-9ac5-46c8-a709-dfc1904b37c7"
/>

(arrows = paddings too big)

After
<img width="1270" height="1074" alt="CleanShot 2026-03-30 at 12 01
24@2x"
src="https://github.com/user-attachments/assets/16e4155a-5591-4387-9675-f5c7be58ce32"
/>
2026-03-30 15:37:35 +00:00
Etienne ecf8161d0e Fix - Remove signFileUrl method (#19121)
`signFileUrl` is the old way to resolve file url. Replaced by
`signFileByIdUrl` which needs `FileFolder` and `fileId`.

Fix also avatar for person and workspaceMember. To be continued with
imageIdentifier refactor.


Fix : 
- https://discord.com/channels/1130383047699738754/1486723854910099576
- https://discord.com/channels/1130383047699738754/1484566445584285870
- https://discord.com/channels/1130383047699738754/1487124612889313420

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-30 15:14:54 +00:00
Etienne 3d1c53ec9d Gql direct execution - Improvements (#18972)
#### Direct Execution  __typename & null backfill
##### __typename filling
The direct execution path now correctly derives __typename at every
level of the GraphQL response:
Connection types — CompanyConnection, CompanyEdge, Company, PageInfo
GroupBy types — TaskGroupByConnection (was incorrectly producing
TaskConnection)
Composite fields — Links, FullName, Currency, etc. handled by a
dedicated formatter (was inheriting the parent object's typename)
Previously, __typename was derived from the object's universal
identifier (a UUID), producing broken values like
20202020B3744779A56180086Cb2E17FConnection.
##### Null backfill
Selected fields missing from the resolver result are backfilled with
null, matching the standard Yoga schema behavior.
##### Integration test
A new test runs the same findMany query (with __typename at all
structural levels) through both paths — standard Yoga schema and direct
execution — and asserts identical output via toStrictEqual.
2026-03-30 17:20:25 +02:00
github-actions[bot] 2a5aab0c44 i18n - translations (#19129)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 17:19:21 +02:00
martmull 8985dfbc5d Improve apps (#19120)
fixes
https://discord.com/channels/1130383047699738754/1488094970241089586
2026-03-30 15:03:23 +00:00
github-actions[bot] 40abe1e6d0 i18n - docs translations (#19128)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 16:49:57 +02:00
Raphaël Bosi 6c128a35dd Fix cropped calendar event name (#19126)
## Before
<img width="3024" height="1480" alt="CleanShot 2026-03-30 at 16 17 13
2@2x"
src="https://github.com/user-attachments/assets/0ec774a6-5e28-440a-93d5-1e91ea9c4c5a"
/>

## After
<img width="3024" height="1482" alt="CleanShot 2026-03-30 at 16 16
56@2x"
src="https://github.com/user-attachments/assets/0a15a6a4-be6f-4e22-b00b-548df9e73e2d"
/>
2026-03-30 14:35:39 +00:00
Paul Rastoin c0086646fd [APP] Stricter API assertions (#19114)
# Introduction

Improved the ci assertions to be sure the default logic function worked
fully

## what's next
About to introduce a s3 + lambda e2e test to cover the lambda driver too
in addition of the local driver
2026-03-30 14:18:45 +00:00
Charles Bochet 08b041633a chore: remove 1-19 upgrade commands and navigation menu item feature flags (#19083)
## Summary
- Deletes the entire `1-19` upgrade version command directory (7 files,
~1500 lines) and removes all references from the upgrade module and
command runner.
- Removes `IS_NAVIGATION_MENU_ITEM_ENABLED` and
`IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flags from the enum,
seed data, generated schema files, and test mocks. These flags had no
remaining feature-gated code — navigation menu items are now always
enabled.
2026-03-30 14:15:46 +00:00
neo773 2fccd29ec6 messaging cleanup (#19124) 2026-03-30 16:21:00 +02:00
github-actions[bot] 30bdc24bf8 i18n - translations (#19125)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 16:08:36 +02:00
Etienne d2cf05f4f4 Fix - Not shared message on record index (#19103)
quality-feedbacks :
https://discord.com/channels/1130383047699738754/1486711185436053514/1486711185436053514
<img width="1000" height="500" alt="Screenshot 2026-03-30 at 09 42 48"
src="https://github.com/user-attachments/assets/fe9027fe-0056-4fec-af51-5b39bb467bb5"
/>
<img width="1000" height="500" alt="Screenshot 2026-03-30 at 09 42 34"
src="https://github.com/user-attachments/assets/8cd05ea0-7eb2-4490-b87e-3a7dcd57add2"
/>

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-03-30 13:53:35 +00:00
Raphaël Bosi 696a202bb9 Fix navigation menu edition (#19115)
# Description

Fix "Already in navbar" false positive in the sidebar object picker:
`getObjectMetadataIdsInDraft` was collecting `objectMetadataId` from all
navigation menu item types (OBJECT, VIEW, RECORD), so having a pinned
view or record for an object type would incorrectly block re-adding that
object. Now only OBJECT-type items contribute to the duplicate check.

# Video QA

## Before


https://github.com/user-attachments/assets/7b853a55-6d7a-444c-92ee-bf6a75d9927c

## After


https://github.com/user-attachments/assets/65e651e2-9df8-45fd-872d-00782d6b6490

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-30 13:50:52 +00:00
Raphaël Bosi b8374f5531 Fix export view (#19117)
# PR Description

**Export view** command sent { id: { in: [] } } to the server, which
rejects empty arrays. It now falls back to the view's filters instead
when there is no selection, which is the intended behavior.

# Video QA

## Before


https://github.com/user-attachments/assets/3e6263f9-9c66-4f67-a81e-e0571652147d

## After


https://github.com/user-attachments/assets/977a366d-1936-457f-96a1-a5d4fb8ac98e
2026-03-30 13:07:24 +00:00
Charles Bochet 1109b89cd7 fix: use Redis metadata version for GraphQL response cache key (#19111)
## Summary

- Fix stale `ObjectMetadataItems` GraphQL response cache after field
creation by using `request.workspaceMetadataVersion` (sourced from
Redis) instead of `workspace.metadataVersion` (from the potentially
stale CoreEntityCacheService)
- Make the E2E kanban view test selector more robust with a regex match

## Root Cause

The `useCachedMetadata` GraphQL plugin keys cached responses using
`workspace.metadataVersion` from the `CoreEntityCacheService`. When a
field is created:

1. The migration runner increments `metadataVersion` in DB and Redis
2. But the `CoreEntityCacheService` for `WorkspaceEntity` is **not**
invalidated
3. So `request.workspace.metadataVersion` still has the old version
4. The cache key resolves to the old cached response
5. The frontend gets stale metadata without the newly created field

This breaks E2E tests (and likely affects users) - after creating a
custom field, the metadata isn't visible until the workspace entity
cache refreshes.

## Fix

Use `request.workspaceMetadataVersion` (populated from Redis by the
middleware, always up-to-date) as the primary version for cache keys,
falling back to the entity cache version.

## Test plan

- [ ] E2E `create-kanban-view` tests should pass (creating a Select
field and immediately using it in a Kanban view)
- [ ] Verify `ObjectMetadataItems` returns fresh data after field
creation (no stale cache)


Made with [Cursor](https://cursor.com)
2026-03-30 15:01:54 +02:00
github-actions[bot] 107914b437 i18n - docs translations (#19119)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 14:39:38 +02:00
Raphaël Bosi cb85a1b5a3 Fix record name hover (#19109)
## Before


https://github.com/user-attachments/assets/1972f3ac-17b2-4531-a64e-22f2672de71c

## After


https://github.com/user-attachments/assets/0fffdac6-0fcb-4446-9b7f-1791ec08e2df
2026-03-30 13:15:02 +02:00
neo773 ef8789fad6 fix: convert empty parentFolderId to null in messageFolder core dual-write (#19110) 2026-03-30 13:11:38 +02:00
Raphaël Bosi ee479001a1 Fix workspace dropdown (#19106)
## Before


https://github.com/user-attachments/assets/2ebc682d-f868-4a8b-8a1a-fe304222652e

## After


https://github.com/user-attachments/assets/e6fddab2-b5a1-4941-b60f-f45ad2bfaadd
2026-03-30 12:56:23 +02:00
github-actions[bot] 6af7e32c54 i18n - docs translations (#19113)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 12:45:45 +02:00
Abdullah. d246b16063 More parts of the new website. (#19094)
This PR brings in a few more components and some refactor of the
existing components for the website due to design requirements.
2026-03-30 12:00:32 +02:00
github-actions[bot] 0b2f435b3e i18n - translations (#19108)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 11:00:52 +02:00
martmull 039a04bc2f Fix warning about ../../tsconfig.base.json not found (#19105)
as title
2026-03-30 10:56:59 +02:00
martmull fe1377f18b Provide applicatiion assets (#18973)
- improve backend
- improve frontend

<img width="1293" height="824" alt="image"
src="https://github.com/user-attachments/assets/7a4633f1-85cd-4126-b058-dbeae6ba2218"
/>
2026-03-30 10:53:31 +02:00
629 changed files with 23271 additions and 9651 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "twenty-front",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "start", "twenty-front"],
"port": 3001
},
{
"name": "twenty-server",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "start", "twenty-server"],
"port": 3000
},
{
"name": "twenty-worker",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "run", "twenty-server:worker"],
"port": 0
}
]
}
+24 -4
View File
@@ -86,6 +86,8 @@ jobs:
run: |
echo "Attempting to merge main into current branch..."
git config user.email "ci@twenty.com"
git config user.name "CI"
git fetch origin main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
@@ -99,8 +101,8 @@ jobs:
echo "❌ Merge failed due to conflicts"
echo "⚠️ Falling back to comparing current branch against main without merge"
# Abort the failed merge
git merge --abort
# Abort the failed merge (may not exist if merge never started)
git merge --abort 2>/dev/null || true
echo "merged=false" >> $GITHUB_OUTPUT
echo "BRANCH_STATE=conflicts" >> $GITHUB_ENV
@@ -402,6 +404,23 @@ jobs:
# Clean up temp directory
rm -rf /tmp/current-branch-files
- name: Validate downloaded schema files
id: validate-schemas
run: |
valid=true
for file in main-schema-introspection.json current-schema-introspection.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json \
main-rest-api.json current-rest-api.json \
main-rest-metadata-api.json current-rest-metadata-api.json; do
if [ ! -f "$file" ] || ! jq empty "$file" 2>/dev/null; then
echo "::warning::Invalid or missing schema file: $file"
valid=false
fi
done
echo "valid=$valid" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -414,6 +433,7 @@ jobs:
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
npm install -g @graphql-inspector/cli
@@ -424,7 +444,6 @@ jobs:
echo "Checking GraphQL schema for changes..."
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
@@ -442,7 +461,6 @@ jobs:
echo "Checking GraphQL metadata schema for changes..."
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL metadata schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
@@ -461,6 +479,7 @@ jobs:
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
@@ -529,6 +548,7 @@ jobs:
fi
- name: Check REST Metadata API Breaking Changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
+1 -1
View File
@@ -188,7 +188,7 @@ jobs:
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Created company"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
env:
@@ -0,0 +1,114 @@
name: CI Front Component Renderer
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
yarn.lock
packages/twenty-front-component-renderer/**
packages/twenty-sdk/**
packages/twenty-shared/**
!packages/twenty-sdk/package.json
renderer-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
task: [build, typecheck, lint]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run ${{ matrix.task }}
run: npx nx ${{ matrix.task }} twenty-front-component-renderer
renderer-sb-build:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build storybook
run: npx nx storybook:build twenty-front-component-renderer
- name: Upload storybook build
uses: actions/upload-artifact@v4
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
retention-days: 1
renderer-sb-test:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: renderer-sb-build
env:
STORYBOOK_URL: http://localhost:6008
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-sdk
- name: Download storybook build
uses: actions/download-artifact@v4
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
- name: Install Playwright
run: |
cd packages/twenty-front-component-renderer
npx playwright install
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6008 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-front-component-renderer
ci-front-component-renderer-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs:
[
changed-files-check,
renderer-task,
renderer-sb-build,
renderer-sb-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+2 -1
View File
@@ -25,6 +25,7 @@ jobs:
package.json
yarn.lock
packages/twenty-front/**
packages/twenty-front-component-renderer/**
packages/twenty-ui/**
packages/twenty-shared/**
packages/twenty-sdk/**
@@ -93,7 +94,7 @@ jobs:
run: |
npx nx build twenty-shared
npx nx build twenty-ui
npx nx build twenty-sdk
npx nx build twenty-front-component-renderer
- name: Download storybook build
uses: actions/download-artifact@v4
with:
+1 -4
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
task: [lint, typecheck, test:unit, test:integration]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -41,9 +41,6 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Install Playwright
if: contains(matrix.task, 'storybook')
run: npx playwright install chromium
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
+1
View File
@@ -208,6 +208,7 @@
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"packages/twenty-sdk",
"packages/twenty-front-component-renderer",
"packages/twenty-client-sdk",
"packages/twenty-apps",
"packages/twenty-cli",
+1 -1
View File
@@ -110,7 +110,7 @@ npx create-twenty-app@latest my-app -m
## Local server
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker on port 2020). These commands only apply to the Docker-based dev server — they do not manage a Twenty instance started from source (e.g. `npx nx start twenty-server` on port 3000). You can also manage it manually:
```bash
yarn twenty server start # Start (pulls image if needed)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.8.0-canary.5",
"version": "0.8.0-canary.7",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -1,7 +1,7 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
## UUID requirement
@@ -10,6 +10,7 @@ import * as path from 'path';
import { basename } from 'path';
import {
authLoginOAuth,
detectLocalServer,
serverStart,
type ServerStartResult,
} from 'twenty-sdk/cli';
@@ -62,15 +63,19 @@ export class CreateAppCommand {
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
});
const shouldStartServer = await this.shouldStartServer();
if (startResult.success) {
serverResult = startResult.data;
await this.connectToLocal(serverResult.url);
} else {
console.log(chalk.yellow(`\n${startResult.error.message}`));
if (shouldStartServer) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
});
if (startResult.success) {
serverResult = startResult.data;
await this.promptConnectToLocal(serverResult.url);
} else {
console.log(chalk.yellow(`\n${startResult.error.message}`));
}
}
}
@@ -201,7 +206,46 @@ export class CreateAppCommand {
);
}
private async connectToLocal(serverUrl: string): Promise<void> {
private async shouldStartServer(): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
name: 'startDocker',
message:
'No running Twenty instance found. Would you like to start one using Docker?',
default: true,
},
]);
return startDocker;
}
private async promptConnectToLocal(serverUrl: string): Promise<void> {
const { shouldAuthenticate } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldAuthenticate',
message: `Would you like to authenticate to the local Twenty instance (${serverUrl})?`,
default: true,
},
]);
if (!shouldAuthenticate) {
console.log(
chalk.gray(
'Authentication skipped. Run `yarn twenty remote add` manually.',
),
);
return;
}
try {
const result = await authLoginOAuth({
apiUrl: serverUrl,
@@ -211,14 +255,14 @@ export class CreateAppCommand {
if (!result.success) {
console.log(
chalk.yellow(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
'Authentication failed. Run `yarn twenty remote add` manually.',
),
);
}
} catch {
console.log(
chalk.yellow(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
'Authentication failed. Run `yarn twenty remote add` manually.',
),
);
}
@@ -236,7 +280,7 @@ export class CreateAppCommand {
if (!serverResult) {
console.log(
chalk.gray(
'- yarn twenty remote add --local # Authenticate with Twenty',
'- yarn twenty remote add # Authenticate with Twenty',
),
);
}
@@ -425,8 +425,12 @@ const handler = async (): Promise<{ message: string }> => {
},
});
if (!createCompany?.id || !createCompany?.name) {
throw new Error('Failed to create company: missing id or name in response');
}
return {
message: \`Created company "\${createCompany?.name}" with id \${createCompany?.id}\`,
message: \`Created company "\${createCompany.name}" with id \${createCompany.id}\`,
};
};
@@ -794,6 +798,7 @@ const createPackageJson = async ({
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
keywords: ['twenty-app'],
packageManager: 'yarn@4.9.2',
scripts,
devDependencies,
@@ -1,7 +1,7 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
## UUID requirement
- All generated UUIDs must be valid UUID v4.
@@ -9,22 +9,11 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"remote:add": "twenty remote add --local",
"remote:status": "twenty remote status",
"remote:switch": "twenty remote switch",
"remote:list": "twenty remote list",
"remote:remove": "twenty remote remove",
"dev": "twenty dev",
"add": "twenty add",
"logs": "twenty logs",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"help": "twenty help",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
"twenty": "twenty"
},
"dependencies": {
"twenty-sdk": "latest"
"twenty-sdk": "latest",
"twenty-client-sdk": "latest"
},
"devDependencies": {
"typescript": "^5.9.3",
@@ -0,0 +1,18 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
// Field on existing company object
export default defineField({
universalIdentifier: 'f922fdb8-10a9-4f11-a1d0-992a779f6dff',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.BOOLEAN,
name: 'canReceivePostcards',
label: 'Can Receive Postcards',
description: 'Whether the company can receive postcards',
icon: 'IconMailbox',
defaultValue: false,
});
@@ -6,6 +6,9 @@ export const RECIPIENT_UNIVERSAL_IDENTIFIER =
export const RECIPIENT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
'd2a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d';
export const RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d';
export default defineObject({
universalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
nameSingular: 'recipient',
@@ -23,7 +26,7 @@ export default defineObject({
icon: 'IconMail',
},
{
universalIdentifier: 'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier: RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER,
type: FieldType.ADDRESS,
label: 'Mailing Address',
name: 'mailingAddress',
@@ -1,13 +1,16 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card-recipient.object';
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
export const ALL_POST_CARD_RECIPIENTS_VIEW_ID =
'b1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d';
export const POST_CARD_RECIPIENT_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER =
'fd959c6f-3465-4a3a-b7ad-3f4004fffc9a';
export default defineView({
universalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
name: 'All Post Card Recipients',
@@ -17,7 +20,8 @@ export default defineView({
position: 2,
fields: [
{
universalIdentifier: 'bf1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
universalIdentifier:
POST_CARD_RECIPIENT_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
fieldMetadataUniversalIdentifier: SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
position: 0,
isVisible: true,
@@ -1,9 +1,10 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER,
RECIPIENT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
RECIPIENT_UNIVERSAL_IDENTIFIER,
} from '../objects/recipient.object';
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
export const ALL_RECIPIENTS_VIEW_ID = 'b1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d';
@@ -23,5 +24,13 @@ export default defineView({
isVisible: true,
size: 200,
},
{
universalIdentifier: 'bf1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d',
fieldMetadataUniversalIdentifier:
RECIPIENT_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER,
position: 1,
isVisible: true,
size: 150,
},
],
});
+1 -1
View File
@@ -1,7 +1,7 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
## UUID requirement
@@ -20,8 +20,8 @@
"@types/react": "^18.2.0",
"oxlint": "^0.16.0",
"react": "^18.2.0",
"twenty-client-sdk": "portal:../../twenty-client-sdk",
"twenty-sdk": "portal:../../twenty-sdk",
"twenty-client-sdk": "0.8.0-canary.5",
"twenty-sdk": "0.8.0-canary.5",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
@@ -16,8 +16,12 @@ const handler = async (): Promise<{ message: string }> => {
},
});
if (!createCompany?.id || !createCompany?.name) {
throw new Error('Failed to create company: missing id or name in response');
}
return {
message: `Created company "${createCompany?.name}" with id ${createCompany?.id}`,
message: `Created company "${createCompany.name}" with id ${createCompany.id}`,
};
};
@@ -14,7 +14,8 @@ export default defineObject({
labelPlural: 'Example items',
description: 'A sample custom object',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
labelIdentifierFieldMetadataUniversalIdentifier:
NAME_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
+123 -107
View File
@@ -5,13 +5,13 @@ __metadata:
version: 8
cacheKey: 10c0
"@alcalzone/ansi-tokenize@npm:^0.1.3":
version: 0.1.3
resolution: "@alcalzone/ansi-tokenize@npm:0.1.3"
"@alcalzone/ansi-tokenize@npm:^0.2.4":
version: 0.2.5
resolution: "@alcalzone/ansi-tokenize@npm:0.2.5"
dependencies:
ansi-styles: "npm:^6.2.1"
is-fullwidth-code-point: "npm:^4.0.0"
checksum: 10c0/b88c5708271bb64ce132fc80dac8d5b87fc1699bf3abfdf10ecae40dbb56ab82460818f5746ecdd9870a00be9854e039d5cb3a121b808d0ff6f5bc7d0146cb38
is-fullwidth-code-point: "npm:^5.0.0"
checksum: 10c0/dd8622288426b5b7dbf8b68d51d4b930cea591d0e3b9dbc3f523131464d78ac922c165fcb7ba5307f133d35dbcaa0e6648a1c3fb6a0dc2725546d6f867b70af4
languageName: node
linkType: hard
@@ -2655,7 +2655,7 @@ __metadata:
languageName: node
linkType: hard
"ansi-escapes@npm:^7.0.0":
"ansi-escapes@npm:^7.3.0":
version: 7.3.0
resolution: "ansi-escapes@npm:7.3.0"
dependencies:
@@ -2717,7 +2717,7 @@ __metadata:
languageName: node
linkType: hard
"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.2.1":
"ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3":
version: 6.2.3
resolution: "ansi-styles@npm:6.2.3"
checksum: 10c0/23b8a4ce14e18fb854693b95351e286b771d23d8844057ed2e7d083cd3e708376c3323707ec6a24365f7d7eda3ca00327fe04092e29e551499ec4c8b7bfac868
@@ -2948,7 +2948,7 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^5.3.0":
"chalk@npm:^5.3.0, chalk@npm:^5.6.0":
version: 5.6.2
resolution: "chalk@npm:5.6.2"
checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976
@@ -3020,13 +3020,13 @@ __metadata:
languageName: node
linkType: hard
"cli-truncate@npm:^4.0.0":
version: 4.0.0
resolution: "cli-truncate@npm:4.0.0"
"cli-truncate@npm:^5.1.1":
version: 5.2.0
resolution: "cli-truncate@npm:5.2.0"
dependencies:
slice-ansi: "npm:^5.0.0"
string-width: "npm:^7.0.0"
checksum: 10c0/d7f0b73e3d9b88cb496e6c086df7410b541b56a43d18ade6a573c9c18bd001b1c3fba1ad578f741a4218fdc794d042385f8ac02c25e1c295a2d8b9f3cb86eb4c
slice-ansi: "npm:^8.0.0"
string-width: "npm:^8.2.0"
checksum: 10c0/0d4ec94702ca85b64522ac93633837fb5ea7db17b79b1322a60f6045e6ae2b8cd7bd4c1d19ac7d1f9e10e3bbda1112e172e439b68c02b785ee00da8d6a5c5471
languageName: node
linkType: hard
@@ -3306,7 +3306,7 @@ __metadata:
languageName: node
linkType: hard
"es-toolkit@npm:^1.22.0":
"es-toolkit@npm:^1.39.10":
version: 1.45.1
resolution: "es-toolkit@npm:1.45.1"
dependenciesMeta:
@@ -3727,7 +3727,7 @@ __metadata:
languageName: node
linkType: hard
"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1":
"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0":
version: 1.5.0
resolution: "get-east-asian-width@npm:1.5.0"
checksum: 10c0/bff8bbc8d81790b9477f7aa55b1806b9f082a8dc1359fff7bd8b96939622c86b729685afc2bfeb22def1fc6ef1e5228e4d87dd4e6da60bc43a5edfb03c4ee167
@@ -3906,8 +3906,8 @@ __metadata:
"@types/react": "npm:^18.2.0"
oxlint: "npm:^0.16.0"
react: "npm:^18.2.0"
twenty-client-sdk: "portal:../../twenty-client-sdk"
twenty-sdk: "portal:../../twenty-sdk"
twenty-client-sdk: "npm:0.8.0-canary.5"
twenty-sdk: "npm:0.8.0-canary.5"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -4030,44 +4030,45 @@ __metadata:
languageName: node
linkType: hard
"ink@npm:^5.1.1":
version: 5.2.1
resolution: "ink@npm:5.2.1"
"ink@npm:^6.8.0":
version: 6.8.0
resolution: "ink@npm:6.8.0"
dependencies:
"@alcalzone/ansi-tokenize": "npm:^0.1.3"
ansi-escapes: "npm:^7.0.0"
"@alcalzone/ansi-tokenize": "npm:^0.2.4"
ansi-escapes: "npm:^7.3.0"
ansi-styles: "npm:^6.2.1"
auto-bind: "npm:^5.0.1"
chalk: "npm:^5.3.0"
chalk: "npm:^5.6.0"
cli-boxes: "npm:^3.0.0"
cli-cursor: "npm:^4.0.0"
cli-truncate: "npm:^4.0.0"
cli-truncate: "npm:^5.1.1"
code-excerpt: "npm:^4.0.0"
es-toolkit: "npm:^1.22.0"
es-toolkit: "npm:^1.39.10"
indent-string: "npm:^5.0.0"
is-in-ci: "npm:^1.0.0"
is-in-ci: "npm:^2.0.0"
patch-console: "npm:^2.0.0"
react-reconciler: "npm:^0.29.0"
scheduler: "npm:^0.23.0"
react-reconciler: "npm:^0.33.0"
scheduler: "npm:^0.27.0"
signal-exit: "npm:^3.0.7"
slice-ansi: "npm:^7.1.0"
slice-ansi: "npm:^8.0.0"
stack-utils: "npm:^2.0.6"
string-width: "npm:^7.2.0"
type-fest: "npm:^4.27.0"
widest-line: "npm:^5.0.0"
string-width: "npm:^8.1.1"
terminal-size: "npm:^4.0.1"
type-fest: "npm:^5.4.1"
widest-line: "npm:^6.0.0"
wrap-ansi: "npm:^9.0.0"
ws: "npm:^8.18.0"
yoga-layout: "npm:~3.2.1"
peerDependencies:
"@types/react": ">=18.0.0"
react: ">=18.0.0"
react-devtools-core: ^4.19.1
"@types/react": ">=19.0.0"
react: ">=19.0.0"
react-devtools-core: ">=6.1.2"
peerDependenciesMeta:
"@types/react":
optional: true
react-devtools-core:
optional: true
checksum: 10c0/0e2607712783353fbe99438ca4220db3d5874c7ae1a07e2b232f7539489b13a9db929b3046eedeccf318a3ffa222a572287dbe2307c848b8e7f6ec4bba39e550
checksum: 10c0/50500e547fdf6a1f1d836d6befbd4770e3ab649ef0be1884500a6da411fb68a90e22dd7dcc9c404911d30e9f87506b3b9d8e997c6c6ceac85ee054b4dadefaff
languageName: node
linkType: hard
@@ -4140,14 +4141,7 @@ __metadata:
languageName: node
linkType: hard
"is-fullwidth-code-point@npm:^4.0.0":
version: 4.0.0
resolution: "is-fullwidth-code-point@npm:4.0.0"
checksum: 10c0/df2a717e813567db0f659c306d61f2f804d480752526886954a2a3e2246c7745fd07a52b5fecf2b68caf0a6c79dcdace6166fdf29cc76ed9975cc334f0a018b8
languageName: node
linkType: hard
"is-fullwidth-code-point@npm:^5.0.0":
"is-fullwidth-code-point@npm:^5.0.0, is-fullwidth-code-point@npm:^5.1.0":
version: 5.1.0
resolution: "is-fullwidth-code-point@npm:5.1.0"
dependencies:
@@ -4165,12 +4159,12 @@ __metadata:
languageName: node
linkType: hard
"is-in-ci@npm:^1.0.0":
version: 1.0.0
resolution: "is-in-ci@npm:1.0.0"
"is-in-ci@npm:^2.0.0":
version: 2.0.0
resolution: "is-in-ci@npm:2.0.0"
bin:
is-in-ci: cli.js
checksum: 10c0/98f9cec4c35aece4cf731abf35ebf28359a9b0324fac810da05b842515d9ccb33b8999c1d9a679f0362e1a4df3292065c38d7f86327b1387fa667bc0150f4898
checksum: 10c0/1e1d1056939a681e8206035de5ad84e0404556eaa7622bb55f0f1868b9788bff3df427bc0b1ed5a172623154a90fcb1e759a230817cd73d09435543ae3c71feb
languageName: node
linkType: hard
@@ -5014,15 +5008,14 @@ __metadata:
languageName: node
linkType: hard
"react-dom@npm:^18.2.0":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
"react-dom@npm:^19.0.0":
version: 19.2.4
resolution: "react-dom@npm:19.2.4"
dependencies:
loose-envify: "npm:^1.1.0"
scheduler: "npm:^0.23.2"
scheduler: "npm:^0.27.0"
peerDependencies:
react: ^18.3.1
checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85
react: ^19.2.4
checksum: 10c0/f0c63f1794dedb154136d4d0f59af00b41907f4859571c155940296808f4b94bf9c0c20633db75b5b2112ec13d8d7dd4f9bf57362ed48782f317b11d05a44f35
languageName: node
linkType: hard
@@ -5033,15 +5026,14 @@ __metadata:
languageName: node
linkType: hard
"react-reconciler@npm:^0.29.0":
version: 0.29.2
resolution: "react-reconciler@npm:0.29.2"
"react-reconciler@npm:^0.33.0":
version: 0.33.0
resolution: "react-reconciler@npm:0.33.0"
dependencies:
loose-envify: "npm:^1.1.0"
scheduler: "npm:^0.23.2"
scheduler: "npm:^0.27.0"
peerDependencies:
react: ^18.3.1
checksum: 10c0/94f48ddc348a974256cf13c859f5a94efdb0cd72e04c51b1a4d5c72a8b960ccd35df2196057ee6a4cbcb26145e12b01e3f9ba3b183fddb901414db36a07cbf43
react: ^19.2.0
checksum: 10c0/3f7b27ea8d0ff4c8bf0e402a285e1af9b7d0e6f4c1a70a28f4384938bc1130bc82a90a31df0b79ef5e380e2e55e2598bd90b4dbf802b1203d735ba0355817d3a
languageName: node
linkType: hard
@@ -5054,6 +5046,13 @@ __metadata:
languageName: node
linkType: hard
"react@npm:^19.0.0":
version: 19.2.4
resolution: "react@npm:19.2.4"
checksum: 10c0/cd2c9ff67a720799cc3b38a516009986f7fc4cb8d3e15716c6211cf098d1357ee3e348ab05ad0600042bbb0fd888530ba92e329198c92eafa0994f5213396596
languageName: node
linkType: hard
"readdirp@npm:^4.0.1":
version: 4.1.2
resolution: "readdirp@npm:4.1.2"
@@ -5298,12 +5297,10 @@ __metadata:
languageName: node
linkType: hard
"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2":
version: 0.23.2
resolution: "scheduler@npm:0.23.2"
dependencies:
loose-envify: "npm:^1.1.0"
checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78
"scheduler@npm:^0.27.0":
version: 0.27.0
resolution: "scheduler@npm:0.27.0"
checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452
languageName: node
linkType: hard
@@ -5406,23 +5403,13 @@ __metadata:
languageName: node
linkType: hard
"slice-ansi@npm:^5.0.0":
version: 5.0.0
resolution: "slice-ansi@npm:5.0.0"
"slice-ansi@npm:^8.0.0":
version: 8.0.0
resolution: "slice-ansi@npm:8.0.0"
dependencies:
ansi-styles: "npm:^6.0.0"
is-fullwidth-code-point: "npm:^4.0.0"
checksum: 10c0/2d4d40b2a9d5cf4e8caae3f698fe24ae31a4d778701724f578e984dcb485ec8c49f0c04dab59c401821e80fcdfe89cace9c66693b0244e40ec485d72e543914f
languageName: node
linkType: hard
"slice-ansi@npm:^7.1.0":
version: 7.1.2
resolution: "slice-ansi@npm:7.1.2"
dependencies:
ansi-styles: "npm:^6.2.1"
is-fullwidth-code-point: "npm:^5.0.0"
checksum: 10c0/36742f2eb0c03e2e81a38ed14d13a64f7b732fe38c3faf96cce0599788a345011e840db35f1430ca606ea3f8db2abeb92a8d25c2753a819e3babaa10c2e289a2
ansi-styles: "npm:^6.2.3"
is-fullwidth-code-point: "npm:^5.1.0"
checksum: 10c0/0ce4aa91febb7cea4a00c2c27bb820fa53b6d2862ce0f80f7120134719f7914fc416b0ed966cf35250a3169e152916392f35917a2d7cad0fcc5d8b841010fa9a
languageName: node
linkType: hard
@@ -5532,7 +5519,7 @@ __metadata:
languageName: node
linkType: hard
"string-width@npm:^7.0.0, string-width@npm:^7.2.0":
"string-width@npm:^7.0.0":
version: 7.2.0
resolution: "string-width@npm:7.2.0"
dependencies:
@@ -5543,6 +5530,16 @@ __metadata:
languageName: node
linkType: hard
"string-width@npm:^8.1.0, string-width@npm:^8.1.1, string-width@npm:^8.2.0":
version: 8.2.0
resolution: "string-width@npm:8.2.0"
dependencies:
get-east-asian-width: "npm:^1.5.0"
strip-ansi: "npm:^7.1.2"
checksum: 10c0/d8915428b43519b0f494da6590dbe4491857d8a12e40250e50fc01fbb616ffd8400a436bbe25712255ee129511fe0414c49d3b6b9627e2bc3a33dcec1d2eda02
languageName: node
linkType: hard
"strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1":
version: 3.0.1
resolution: "strip-ansi@npm:3.0.1"
@@ -5570,7 +5567,7 @@ __metadata:
languageName: node
linkType: hard
"strip-ansi@npm:^7.1.0":
"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2":
version: 7.2.0
resolution: "strip-ansi@npm:7.2.0"
dependencies:
@@ -5640,6 +5637,13 @@ __metadata:
languageName: node
linkType: hard
"tagged-tag@npm:^1.0.0":
version: 1.0.0
resolution: "tagged-tag@npm:1.0.0"
checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1
languageName: node
linkType: hard
"tar@npm:^7.5.4":
version: 7.5.11
resolution: "tar@npm:7.5.11"
@@ -5653,6 +5657,13 @@ __metadata:
languageName: node
linkType: hard
"terminal-size@npm:^4.0.1":
version: 4.0.1
resolution: "terminal-size@npm:4.0.1"
checksum: 10c0/89afd9d816dd9dbfe4499da9aeea70491bbde4ff4592226a9c8ac71074a7580afead6a78e95ecc35f6d42e09087b55ffcb1019302cd55e0cc957b6ce5c4847e8
languageName: node
linkType: hard
"tinybench@npm:^2.9.0":
version: 2.9.0
resolution: "tinybench@npm:2.9.0"
@@ -5751,20 +5762,21 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@portal:../../twenty-client-sdk::locator=hello-world%40workspace%3A.":
version: 0.0.0-use.local
resolution: "twenty-client-sdk@portal:../../twenty-client-sdk::locator=hello-world%40workspace%3A."
"twenty-client-sdk@npm:0.8.0-canary.5":
version: 0.8.0-canary.5
resolution: "twenty-client-sdk@npm:0.8.0-canary.5"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
checksum: 10c0/754b92b732c8e03c9779f6557324710afe4c07f7e6efbe766bd8ff3b6dd8a00c0d9f3fecce2098d68ef9556cafa722b2189e490473e420fdb9cf30c56beb3619
languageName: node
linkType: soft
linkType: hard
"twenty-sdk@portal:../../twenty-sdk::locator=hello-world%40workspace%3A.":
version: 0.0.0-use.local
resolution: "twenty-sdk@portal:../../twenty-sdk::locator=hello-world%40workspace%3A."
"twenty-sdk@npm:0.8.0-canary.5":
version: 0.8.0-canary.5
resolution: "twenty-sdk@npm:0.8.0-canary.5"
dependencies:
"@chakra-ui/react": "npm:^3.33.0"
"@emotion/react": "npm:^11.14.0"
@@ -5782,13 +5794,14 @@ __metadata:
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
graphql-sse: "npm:^2.5.4"
ink: "npm:^5.1.1"
ink: "npm:^6.8.0"
inquirer: "npm:^10.0.0"
jsonc-parser: "npm:^3.2.0"
preact: "npm:^10.28.3"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:0.8.0-canary.5"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
@@ -5796,8 +5809,9 @@ __metadata:
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/47d0970c88f59c092e8eca34dca38bf88d9d52678997349068ca8a8e5cebed2ea71dbd4e8b7299f40b99b5419d4cecd53b31f821b65959267e8d97eb91d2ddde
languageName: node
linkType: soft
linkType: hard
"type-fest@npm:^0.21.3":
version: 0.21.3
@@ -5806,10 +5820,12 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^4.27.0":
version: 4.41.0
resolution: "type-fest@npm:4.41.0"
checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4
"type-fest@npm:^5.4.1":
version: 5.5.0
resolution: "type-fest@npm:5.5.0"
dependencies:
tagged-tag: "npm:^1.0.0"
checksum: 10c0/60bf79a8df45abf99490e3204eceb5cf7f915413f8a69fb578c75cab37ddcb7d29ee21f185f0e1617323ac0b2a441e001b8dc691e220d0b087e9c29ea205538c
languageName: node
linkType: hard
@@ -6123,12 +6139,12 @@ __metadata:
languageName: node
linkType: hard
"widest-line@npm:^5.0.0":
version: 5.0.0
resolution: "widest-line@npm:5.0.0"
"widest-line@npm:^6.0.0":
version: 6.0.0
resolution: "widest-line@npm:6.0.0"
dependencies:
string-width: "npm:^7.0.0"
checksum: 10c0/6bd6cca8cda502ef50e05353fd25de0df8c704ffc43ada7e0a9cf9a5d4f4e12520485d80e0b77cec8a21f6c3909042fcf732aa9281e5dbb98cc9384a138b2578
string-width: "npm:^8.1.0"
checksum: 10c0/735f1fdcd97fe765a07bb8b5e73c020bed8e53ab34e83ce0ef01693ba3c914d9e7977fe5f5facf0d0b670297a82dd5e376d3efa0896860dfcdaf7cd6924c0fb7
languageName: node
linkType: hard
@@ -1,7 +1,7 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/fixtures/postcard-app
## Common Pitfalls
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "0.8.0-canary.5",
"version": "0.8.0-canary.7",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -38,9 +38,6 @@ type ApplicationRegistration {
id: UUID!
universalIdentifier: String!
name: String!
description: String
logoUrl: String
author: String
oAuthClientId: String!
oAuthRedirectUris: [String!]!
oAuthScopes: [String!]!
@@ -48,8 +45,6 @@ type ApplicationRegistration {
sourceType: ApplicationRegistrationSourceType!
sourcePackage: String
latestAvailableVersion: String
websiteUrl: String
termsUrl: String
isListed: Boolean!
isFeatured: Boolean!
createdAt: DateTime!
@@ -1731,8 +1726,6 @@ enum FeatureFlagKey {
IS_EMAILING_DOMAIN_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_COMMAND_MENU_ITEM_ENABLED
IS_NAVIGATION_MENU_ITEM_ENABLED
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED
IS_DRAFT_EMAIL_ENABLED
IS_USAGE_ANALYTICS_ENABLED
IS_RICH_TEXT_V1_MIGRATED
@@ -2421,91 +2414,30 @@ type File {
createdAt: DateTime!
}
type MarketplaceAppField {
name: String!
type: String!
label: String!
description: String
icon: String
objectUniversalIdentifier: String
universalIdentifier: String
}
type MarketplaceAppObject {
universalIdentifier: String!
nameSingular: String!
namePlural: String!
labelSingular: String!
labelPlural: String!
description: String
icon: String
fields: [MarketplaceAppField!]!
}
type MarketplaceAppLogicFunction {
name: String!
description: String
timeoutSeconds: Int
}
type MarketplaceAppFrontComponent {
name: String!
description: String
}
type MarketplaceAppRoleObjectPermission {
objectUniversalIdentifier: String!
canReadObjectRecords: Boolean
canUpdateObjectRecords: Boolean
canSoftDeleteObjectRecords: Boolean
canDestroyObjectRecords: Boolean
}
type MarketplaceAppRoleFieldPermission {
objectUniversalIdentifier: String!
fieldUniversalIdentifier: String!
canReadFieldValue: Boolean
canUpdateFieldValue: Boolean
}
type MarketplaceAppDefaultRole {
id: String!
label: String!
description: String
canReadAllObjectRecords: Boolean!
canUpdateAllObjectRecords: Boolean!
canSoftDeleteAllObjectRecords: Boolean!
canDestroyAllObjectRecords: Boolean!
canUpdateAllSettings: Boolean!
canAccessAllTools: Boolean!
objectPermissions: [MarketplaceAppRoleObjectPermission!]!
fieldPermissions: [MarketplaceAppRoleFieldPermission!]!
permissionFlags: [String!]!
}
type MarketplaceApp {
id: String!
name: String!
description: String!
icon: String!
version: String!
author: String!
category: String!
logo: String
screenshots: [String!]!
aboutDescription: String!
providers: [String!]!
websiteUrl: String
termsUrl: String
objects: [MarketplaceAppObject!]!
fields: [MarketplaceAppField!]!
logicFunctions: [MarketplaceAppLogicFunction!]!
frontComponents: [MarketplaceAppFrontComponent!]!
defaultRole: MarketplaceAppDefaultRole
sourcePackage: String
isFeatured: Boolean!
}
type MarketplaceAppDetail {
universalIdentifier: String!
id: String!
name: String!
sourceType: ApplicationRegistrationSourceType!
sourcePackage: String
latestAvailableVersion: String
isListed: Boolean!
isFeatured: Boolean!
manifest: JSON
}
type PublicDomain {
id: UUID!
domain: String!
@@ -2855,10 +2787,12 @@ type AgentChatThread {
type AgentMessage {
id: UUID!
threadId: UUID!
turnId: UUID!
turnId: UUID
agentId: UUID
role: String!
status: String!
parts: [AgentMessagePart!]!
processedAt: DateTime
createdAt: DateTime!
}
@@ -3249,6 +3183,7 @@ type Query {
findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats!
findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariable!]!
applicationRegistrationTarballUrl(id: String!): String
getApplicationShareLink(id: String!): String!
currentUser: User!
currentWorkspace: Workspace!
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
@@ -3274,7 +3209,7 @@ type Query {
findManyPublicDomains: [PublicDomain!]!
getEmailingDomains: [EmailingDomain!]!
findManyMarketplaceApps: [MarketplaceApp!]!
findOneMarketplaceApp(universalIdentifier: String!): MarketplaceApp!
findMarketplaceAppDetail(universalIdentifier: String!): MarketplaceAppDetail!
findManyApplications: [Application!]!
findOneApplication(id: UUID, universalIdentifier: UUID): Application!
}
@@ -3596,6 +3531,7 @@ type Mutation {
verifyEmailingDomain(id: String!): EmailingDomain!
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean!
syncMarketplaceCatalog: Boolean!
installApplication(appRegistrationId: String!, version: String): Boolean!
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
uninstallApplication(universalIdentifier: String!): Boolean!
@@ -4441,14 +4377,9 @@ input GetAuthorizationUrlForSSOInput {
input CreateApplicationRegistrationInput {
name: String!
description: String
logoUrl: String
author: String
universalIdentifier: String
oAuthRedirectUris: [String!]
oAuthScopes: [String!]
websiteUrl: String
termsUrl: String
}
input UpdateApplicationRegistrationInput {
@@ -4458,13 +4389,8 @@ input UpdateApplicationRegistrationInput {
input UpdateApplicationRegistrationPayload {
name: String
description: String
logoUrl: String
author: String
oAuthRedirectUris: [String!]
oAuthScopes: [String!]
websiteUrl: String
termsUrl: String
isListed: Boolean
}
@@ -42,9 +42,6 @@ export interface ApplicationRegistration {
id: Scalars['UUID']
universalIdentifier: Scalars['String']
name: Scalars['String']
description?: Scalars['String']
logoUrl?: Scalars['String']
author?: Scalars['String']
oAuthClientId: Scalars['String']
oAuthRedirectUris: Scalars['String'][]
oAuthScopes: Scalars['String'][]
@@ -52,8 +49,6 @@ export interface ApplicationRegistration {
sourceType: ApplicationRegistrationSourceType
sourcePackage?: Scalars['String']
latestAvailableVersion?: Scalars['String']
websiteUrl?: Scalars['String']
termsUrl?: Scalars['String']
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
createdAt: Scalars['DateTime']
@@ -1427,7 +1422,7 @@ export interface PublicFeatureFlag {
__typename: 'PublicFeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_ENABLED' | 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_GRAPHQL_QUERY_TIMING_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
export interface ClientConfig {
appVersion?: Scalars['String']
@@ -2120,99 +2115,32 @@ export interface File {
__typename: 'File'
}
export interface MarketplaceAppField {
name: Scalars['String']
type: Scalars['String']
label: Scalars['String']
description?: Scalars['String']
icon?: Scalars['String']
objectUniversalIdentifier?: Scalars['String']
universalIdentifier?: Scalars['String']
__typename: 'MarketplaceAppField'
}
export interface MarketplaceAppObject {
universalIdentifier: Scalars['String']
nameSingular: Scalars['String']
namePlural: Scalars['String']
labelSingular: Scalars['String']
labelPlural: Scalars['String']
description?: Scalars['String']
icon?: Scalars['String']
fields: MarketplaceAppField[]
__typename: 'MarketplaceAppObject'
}
export interface MarketplaceAppLogicFunction {
name: Scalars['String']
description?: Scalars['String']
timeoutSeconds?: Scalars['Int']
__typename: 'MarketplaceAppLogicFunction'
}
export interface MarketplaceAppFrontComponent {
name: Scalars['String']
description?: Scalars['String']
__typename: 'MarketplaceAppFrontComponent'
}
export interface MarketplaceAppRoleObjectPermission {
objectUniversalIdentifier: Scalars['String']
canReadObjectRecords?: Scalars['Boolean']
canUpdateObjectRecords?: Scalars['Boolean']
canSoftDeleteObjectRecords?: Scalars['Boolean']
canDestroyObjectRecords?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleObjectPermission'
}
export interface MarketplaceAppRoleFieldPermission {
objectUniversalIdentifier: Scalars['String']
fieldUniversalIdentifier: Scalars['String']
canReadFieldValue?: Scalars['Boolean']
canUpdateFieldValue?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleFieldPermission'
}
export interface MarketplaceAppDefaultRole {
id: Scalars['String']
label: Scalars['String']
description?: Scalars['String']
canReadAllObjectRecords: Scalars['Boolean']
canUpdateAllObjectRecords: Scalars['Boolean']
canSoftDeleteAllObjectRecords: Scalars['Boolean']
canDestroyAllObjectRecords: Scalars['Boolean']
canUpdateAllSettings: Scalars['Boolean']
canAccessAllTools: Scalars['Boolean']
objectPermissions: MarketplaceAppRoleObjectPermission[]
fieldPermissions: MarketplaceAppRoleFieldPermission[]
permissionFlags: Scalars['String'][]
__typename: 'MarketplaceAppDefaultRole'
}
export interface MarketplaceApp {
id: Scalars['String']
name: Scalars['String']
description: Scalars['String']
icon: Scalars['String']
version: Scalars['String']
author: Scalars['String']
category: Scalars['String']
logo?: Scalars['String']
screenshots: Scalars['String'][]
aboutDescription: Scalars['String']
providers: Scalars['String'][]
websiteUrl?: Scalars['String']
termsUrl?: Scalars['String']
objects: MarketplaceAppObject[]
fields: MarketplaceAppField[]
logicFunctions: MarketplaceAppLogicFunction[]
frontComponents: MarketplaceAppFrontComponent[]
defaultRole?: MarketplaceAppDefaultRole
sourcePackage?: Scalars['String']
isFeatured: Scalars['Boolean']
__typename: 'MarketplaceApp'
}
export interface MarketplaceAppDetail {
universalIdentifier: Scalars['String']
id: Scalars['String']
name: Scalars['String']
sourceType: ApplicationRegistrationSourceType
sourcePackage?: Scalars['String']
latestAvailableVersion?: Scalars['String']
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
manifest?: Scalars['JSON']
__typename: 'MarketplaceAppDetail'
}
export interface PublicDomain {
id: Scalars['UUID']
domain: Scalars['String']
@@ -2520,10 +2448,12 @@ export interface AgentChatThread {
export interface AgentMessage {
id: Scalars['UUID']
threadId: Scalars['UUID']
turnId: Scalars['UUID']
turnId?: Scalars['UUID']
agentId?: Scalars['UUID']
role: Scalars['String']
status: Scalars['String']
parts: AgentMessagePart[]
processedAt?: Scalars['DateTime']
createdAt: Scalars['DateTime']
__typename: 'AgentMessage'
}
@@ -2805,6 +2735,7 @@ export interface Query {
findApplicationRegistrationStats: ApplicationRegistrationStats
findApplicationRegistrationVariables: ApplicationRegistrationVariable[]
applicationRegistrationTarballUrl?: Scalars['String']
getApplicationShareLink: Scalars['String']
currentUser: User
currentWorkspace: Workspace
getPublicWorkspaceDataByDomain: PublicWorkspaceData
@@ -2830,7 +2761,7 @@ export interface Query {
findManyPublicDomains: PublicDomain[]
getEmailingDomains: EmailingDomain[]
findManyMarketplaceApps: MarketplaceApp[]
findOneMarketplaceApp: MarketplaceApp
findMarketplaceAppDetail: MarketplaceAppDetail
findManyApplications: Application[]
findOneApplication: Application
__typename: 'Query'
@@ -3047,6 +2978,7 @@ export interface Mutation {
verifyEmailingDomain: EmailingDomain
createOneAppToken: AppToken
installMarketplaceApp: Scalars['Boolean']
syncMarketplaceCatalog: Scalars['Boolean']
installApplication: Scalars['Boolean']
runWorkspaceMigration: Scalars['Boolean']
uninstallApplication: Scalars['Boolean']
@@ -3114,9 +3046,6 @@ export interface ApplicationRegistrationGenqlSelection{
id?: boolean | number
universalIdentifier?: boolean | number
name?: boolean | number
description?: boolean | number
logoUrl?: boolean | number
author?: boolean | number
oAuthClientId?: boolean | number
oAuthRedirectUris?: boolean | number
oAuthScopes?: boolean | number
@@ -3124,8 +3053,6 @@ export interface ApplicationRegistrationGenqlSelection{
sourceType?: boolean | number
sourcePackage?: boolean | number
latestAvailableVersion?: boolean | number
websiteUrl?: boolean | number
termsUrl?: boolean | number
isListed?: boolean | number
isFeatured?: boolean | number
createdAt?: boolean | number
@@ -5314,107 +5241,34 @@ export interface FileGenqlSelection{
__scalar?: boolean | number
}
export interface MarketplaceAppFieldGenqlSelection{
name?: boolean | number
type?: boolean | number
label?: boolean | number
description?: boolean | number
icon?: boolean | number
objectUniversalIdentifier?: boolean | number
universalIdentifier?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppObjectGenqlSelection{
universalIdentifier?: boolean | number
nameSingular?: boolean | number
namePlural?: boolean | number
labelSingular?: boolean | number
labelPlural?: boolean | number
description?: boolean | number
icon?: boolean | number
fields?: MarketplaceAppFieldGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppLogicFunctionGenqlSelection{
name?: boolean | number
description?: boolean | number
timeoutSeconds?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppFrontComponentGenqlSelection{
name?: boolean | number
description?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleObjectPermissionGenqlSelection{
objectUniversalIdentifier?: boolean | number
canReadObjectRecords?: boolean | number
canUpdateObjectRecords?: boolean | number
canSoftDeleteObjectRecords?: boolean | number
canDestroyObjectRecords?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleFieldPermissionGenqlSelection{
objectUniversalIdentifier?: boolean | number
fieldUniversalIdentifier?: boolean | number
canReadFieldValue?: boolean | number
canUpdateFieldValue?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppDefaultRoleGenqlSelection{
id?: boolean | number
label?: boolean | number
description?: boolean | number
canReadAllObjectRecords?: boolean | number
canUpdateAllObjectRecords?: boolean | number
canSoftDeleteAllObjectRecords?: boolean | number
canDestroyAllObjectRecords?: boolean | number
canUpdateAllSettings?: boolean | number
canAccessAllTools?: boolean | number
objectPermissions?: MarketplaceAppRoleObjectPermissionGenqlSelection
fieldPermissions?: MarketplaceAppRoleFieldPermissionGenqlSelection
permissionFlags?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppGenqlSelection{
id?: boolean | number
name?: boolean | number
description?: boolean | number
icon?: boolean | number
version?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
screenshots?: boolean | number
aboutDescription?: boolean | number
providers?: boolean | number
websiteUrl?: boolean | number
termsUrl?: boolean | number
objects?: MarketplaceAppObjectGenqlSelection
fields?: MarketplaceAppFieldGenqlSelection
logicFunctions?: MarketplaceAppLogicFunctionGenqlSelection
frontComponents?: MarketplaceAppFrontComponentGenqlSelection
defaultRole?: MarketplaceAppDefaultRoleGenqlSelection
sourcePackage?: boolean | number
isFeatured?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppDetailGenqlSelection{
universalIdentifier?: boolean | number
id?: boolean | number
name?: boolean | number
sourceType?: boolean | number
sourcePackage?: boolean | number
latestAvailableVersion?: boolean | number
isListed?: boolean | number
isFeatured?: boolean | number
manifest?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface PublicDomainGenqlSelection{
id?: boolean | number
domain?: boolean | number
@@ -5746,7 +5600,9 @@ export interface AgentMessageGenqlSelection{
turnId?: boolean | number
agentId?: boolean | number
role?: boolean | number
status?: boolean | number
parts?: AgentMessagePartGenqlSelection
processedAt?: boolean | number
createdAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -6042,6 +5898,7 @@ export interface QueryGenqlSelection{
findApplicationRegistrationStats?: (ApplicationRegistrationStatsGenqlSelection & { __args: {id: Scalars['String']} })
findApplicationRegistrationVariables?: (ApplicationRegistrationVariableGenqlSelection & { __args: {applicationRegistrationId: Scalars['String']} })
applicationRegistrationTarballUrl?: { __args: {id: Scalars['String']} }
getApplicationShareLink?: { __args: {id: Scalars['String']} }
currentUser?: UserGenqlSelection
currentWorkspace?: WorkspaceGenqlSelection
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
@@ -6067,7 +5924,7 @@ export interface QueryGenqlSelection{
findManyPublicDomains?: PublicDomainGenqlSelection
getEmailingDomains?: EmailingDomainGenqlSelection
findManyMarketplaceApps?: MarketplaceAppGenqlSelection
findOneMarketplaceApp?: (MarketplaceAppGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
findMarketplaceAppDetail?: (MarketplaceAppDetailGenqlSelection & { __args: {universalIdentifier: Scalars['String']} })
findManyApplications?: ApplicationGenqlSelection
findOneApplication?: (ApplicationGenqlSelection & { __args?: {id?: (Scalars['UUID'] | null), universalIdentifier?: (Scalars['UUID'] | null)} })
__typename?: boolean | number
@@ -6303,6 +6160,7 @@ export interface MutationGenqlSelection{
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
syncMarketplaceCatalog?: boolean | number
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
@@ -6597,11 +6455,11 @@ export interface UpdateSkillInput {id: Scalars['UUID'],name?: (Scalars['String']
export interface GetAuthorizationUrlForSSOInput {identityProviderId: Scalars['UUID'],workspaceInviteHash?: (Scalars['String'] | null)}
export interface CreateApplicationRegistrationInput {name: Scalars['String'],description?: (Scalars['String'] | null),logoUrl?: (Scalars['String'] | null),author?: (Scalars['String'] | null),universalIdentifier?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),websiteUrl?: (Scalars['String'] | null),termsUrl?: (Scalars['String'] | null)}
export interface CreateApplicationRegistrationInput {name: Scalars['String'],universalIdentifier?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null)}
export interface UpdateApplicationRegistrationInput {id: Scalars['String'],update: UpdateApplicationRegistrationPayload}
export interface UpdateApplicationRegistrationPayload {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),logoUrl?: (Scalars['String'] | null),author?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),websiteUrl?: (Scalars['String'] | null),termsUrl?: (Scalars['String'] | null),isListed?: (Scalars['Boolean'] | null)}
export interface UpdateApplicationRegistrationPayload {name?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),isListed?: (Scalars['Boolean'] | null)}
export interface CreateApplicationRegistrationVariableInput {applicationRegistrationId: Scalars['String'],key: Scalars['String'],value: Scalars['String'],description?: (Scalars['String'] | null),isSecret?: (Scalars['Boolean'] | null)}
@@ -8295,62 +8153,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const MarketplaceAppField_possibleTypes: string[] = ['MarketplaceAppField']
export const isMarketplaceAppField = (obj?: { __typename?: any } | null): obj is MarketplaceAppField => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppField"')
return MarketplaceAppField_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppObject_possibleTypes: string[] = ['MarketplaceAppObject']
export const isMarketplaceAppObject = (obj?: { __typename?: any } | null): obj is MarketplaceAppObject => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppObject"')
return MarketplaceAppObject_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppLogicFunction_possibleTypes: string[] = ['MarketplaceAppLogicFunction']
export const isMarketplaceAppLogicFunction = (obj?: { __typename?: any } | null): obj is MarketplaceAppLogicFunction => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppLogicFunction"')
return MarketplaceAppLogicFunction_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppFrontComponent_possibleTypes: string[] = ['MarketplaceAppFrontComponent']
export const isMarketplaceAppFrontComponent = (obj?: { __typename?: any } | null): obj is MarketplaceAppFrontComponent => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppFrontComponent"')
return MarketplaceAppFrontComponent_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleObjectPermission_possibleTypes: string[] = ['MarketplaceAppRoleObjectPermission']
export const isMarketplaceAppRoleObjectPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleObjectPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleObjectPermission"')
return MarketplaceAppRoleObjectPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleFieldPermission_possibleTypes: string[] = ['MarketplaceAppRoleFieldPermission']
export const isMarketplaceAppRoleFieldPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleFieldPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleFieldPermission"')
return MarketplaceAppRoleFieldPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppDefaultRole_possibleTypes: string[] = ['MarketplaceAppDefaultRole']
export const isMarketplaceAppDefaultRole = (obj?: { __typename?: any } | null): obj is MarketplaceAppDefaultRole => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppDefaultRole"')
return MarketplaceAppDefaultRole_possibleTypes.includes(obj.__typename)
}
const MarketplaceApp_possibleTypes: string[] = ['MarketplaceApp']
export const isMarketplaceApp = (obj?: { __typename?: any } | null): obj is MarketplaceApp => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceApp"')
@@ -8359,6 +8161,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const MarketplaceAppDetail_possibleTypes: string[] = ['MarketplaceAppDetail']
export const isMarketplaceAppDetail = (obj?: { __typename?: any } | null): obj is MarketplaceAppDetail => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppDetail"')
return MarketplaceAppDetail_possibleTypes.includes(obj.__typename)
}
const PublicDomain_possibleTypes: string[] = ['PublicDomain']
export const isPublicDomain = (obj?: { __typename?: any } | null): obj is PublicDomain => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicDomain"')
@@ -9147,8 +8957,6 @@ export const enumFeatureFlagKey = {
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
IS_NAVIGATION_MENU_ITEM_ENABLED: 'IS_NAVIGATION_MENU_ITEM_ENABLED' as const,
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED: 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED' as const,
IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const,
IS_USAGE_ANALYTICS_ENABLED: 'IS_USAGE_ANALYTICS_ENABLED' as const,
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -1,6 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compileOnSave": false,
"compilerOptions": {
"rootDir": ".",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
+2
View File
@@ -16,6 +16,7 @@ COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-front-component-renderer/package.json /app/packages/twenty-front-component-renderer/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
COPY ./packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
@@ -51,6 +52,7 @@ FROM common-deps AS twenty-front-build
ARG REACT_APP_SERVER_BASE_URL
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-front-component-renderer /app/packages/twenty-front-component-renderer
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
@@ -19,15 +19,17 @@ The SDK provides helper functions for defining your app entities. As described i
|----------|---------|
| `defineApplication` | Configure application metadata (required, one per app) |
| `defineObject` | Define custom objects with fields |
| `defineField` | Extend existing objects with additional fields or define standalone relation fields |
| `defineLogicFunction` | Define logic functions with handlers |
| `definePreInstallLogicFunction` | Define a pre-install logic function (one per app) |
| `definePostInstallLogicFunction` | Define a post-install logic function (one per app) |
| `defineFrontComponent` | Define front components for custom UI |
| `defineRole` | Configure role permissions and object access |
| `defineField` | Extend existing objects with additional fields |
| `defineView` | Define saved views for objects |
| `defineNavigationMenuItem` | Define sidebar navigation links |
| `defineSkill` | Define AI agent skills |
| `defineAgent` | Define AI agents |
| `definePageLayout` | Define custom page layouts |
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
@@ -36,7 +38,7 @@ These functions validate your configuration at build time and provide IDE autoco
Custom objects describe both schema and behavior for records in your workspace. Use `defineObject()` to define objects with built-in validation:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ Key points:
- The `universalIdentifier` must be unique and stable across deployments.
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
- The `fields` array is optional — you can define objects without custom fields.
- You can scaffold new objects using `yarn twenty entity:add`, which guides you through naming, fields, and relationships.
- You can scaffold new objects using `yarn twenty add`, which guides you through naming, fields, and relationships.
<Note>
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields
@@ -120,6 +122,195 @@ Key points:
but this is not recommended.
</Note>
### Defining fields on existing objects
Use `defineField()` to add fields to objects you don't own — such as standard Twenty objects (Person, Company, etc.) or objects from other apps. Unlike inline fields in `defineObject()`, standalone fields require an `objectUniversalIdentifier` to specify which object they extend:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Key points:
- `objectUniversalIdentifier` identifies the target object. For standard objects, use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exported from `twenty-sdk`.
- When defining fields inline in `defineObject()`, you do **not** need `objectUniversalIdentifier` — it's inherited from the parent object.
- `defineField()` is the only way to add fields to objects you didn't create with `defineObject()`.
### Relations
Relations connect objects together. In Twenty, relations are always **bidirectional** — you define both sides, and each side references the other.
There are two relation types:
| Relation type | Description | Has foreign key? |
|---------------|-------------|-----------------|
| `MANY_TO_ONE` | Many records of this object point to one record of the target | Yes (`joinColumnName`) |
| `ONE_TO_MANY` | One record of this object has many records of the target | No (inverse side) |
#### How relations work
Every relation requires **two fields** that reference each other:
1. The **MANY_TO_ONE** side — lives on the object that holds the foreign key
2. The **ONE_TO_MANY** side — lives on the object that owns the collection
Both fields use `FieldType.RELATION` and cross-reference each other via `relationTargetFieldMetadataUniversalIdentifier`.
#### Example: Post Card has many Recipients
Suppose a `PostCard` can be sent to many `PostCardRecipient` records. Each recipient belongs to exactly one post card.
**Step 1: Define the ONE_TO_MANY side on PostCard** (the "one" side):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Circular imports:** Both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file, and import them in the other file. The build system resolves these at compile time.
</Note>
#### Relating to standard objects
To create a relation with a built-in Twenty object (Person, Company, etc.), use `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Relation field properties
| Property | Required | Description |
|----------|----------|-------------|
| `type` | Yes | Must be `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object |
| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object |
| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) |
#### Inline relation fields in defineObject
You can also define relation fields directly inside `defineObject()`. In that case, omit `objectUniversalIdentifier` — it's inherited from the parent object:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Application config (application-config.ts)
@@ -161,13 +352,29 @@ Notes:
- `defaultRoleUniversalIdentifier` must match the role file (see below).
- Pre-install and post-install functions are automatically detected during the manifest build. See [Pre-install functions](#pre-install-functions) and [Post-install functions](#post-install-functions).
#### Marketplace metadata
If you plan to [publish your app](/developers/extend/apps/publishing), these optional fields control how your app appears in the marketplace:
| Field | Description |
|-------|-------------|
| `author` | Author or company name |
| `category` | App category for marketplace filtering |
| `logoUrl` | Path to your app logo (relative to `./assets/`) |
| `screenshots` | Array of screenshot paths (relative to `./assets/`) |
| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm |
| `websiteUrl` | Link to your website |
| `termsUrl` | Link to terms of service |
| `emailSupport` | Support email address |
| `issueReportUrl` | Link to issue tracker |
#### Roles and permissions
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions.
- The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role.
- The typed client will be restricted to the permissions granted to that role.
- Follow leastprivilege: create a dedicated role with only the permissions your functions need, then reference its universal identifier.
- Follow least-privilege: create a dedicated role with only the permissions your functions need, then reference its universal identifier.
##### Default function role (*.role.ts)
@@ -219,7 +426,7 @@ The `universalIdentifier` of this role is then referenced in `application-config
- **application-config.ts** points to that role so your functions inherit its permissions.
Notes:
- Start from the scaffolded role, then progressively restrict it following leastprivilege.
- Start from the scaffolded role, then progressively restrict it following least-privilege.
- Replace the `objectPermissions` and `fieldPermissions` with the objects/fields your functions need.
- `permissionFlags` control access to platform-level capabilities. Keep them minimal; add only what you need.
- See a working example in the Hello World app: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
@@ -229,7 +436,7 @@ Notes:
Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -318,7 +525,7 @@ export default definePreInstallLogicFunction({
You can also manually execute the pre-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Key points:
@@ -327,7 +534,7 @@ Key points:
- Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
- Pre-install functions do not need triggers — they are invoked by the platform before installation or manually via `function:execute --preInstall`.
- Pre-install functions do not need triggers — they are invoked by the platform before installation or manually via `exec --preInstall`.
### Post-install functions
@@ -355,7 +562,7 @@ export default definePostInstallLogicFunction({
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Key points:
@@ -364,7 +571,7 @@ Key points:
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
- The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `exec --postInstall`.
### Route trigger payload
@@ -412,7 +619,7 @@ The `RoutePayload` type has the following structure:
|----------|------|-------------|
| `headers` | `Record<string, string \| undefined>` | HTTP headers (only those listed in `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) |
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern (e.g., `/users/:id` `{ id: '123' }`) |
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern (e.g., `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Parsed request body (JSON) |
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded |
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) |
@@ -458,7 +665,7 @@ const handler = async (event: RoutePayload) => {
You can create new functions in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
- **Scaffolded**: Run `yarn twenty add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern.
### Marking a logic function as a tool
@@ -470,7 +677,7 @@ To mark a logic function as a tool, set `isTool: true` and provide a `toolInputS
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -558,7 +765,7 @@ Key points:
You can create new front components in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Scaffolded**: Run `yarn twenty add` and choose the option to add a new front component.
- **Manual**: Create a new `.tsx` file and use `defineFrontComponent()`, following the same pattern.
### Skills
@@ -592,46 +799,122 @@ Key points:
You can create new skills in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
- **Scaffolded**: Run `yarn twenty add` and choose the option to add a new skill.
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Generated typed clients
### Typed API clients (`twenty-client-sdk`)
Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema:
The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components:
- **`CoreApiClient`** — queries the `/graphql` endpoint for workspace data
- **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
| Client | Import | Endpoint | Generated? |
|--------|--------|----------|------------|
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built |
#### CoreApiClient
`CoreApiClient` is the main client for querying and mutating workspace data. It is **generated from your workspace schema** during `yarn twenty dev` or `yarn twenty build`, so it's fully typed to match your objects and fields.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Both clients are re-generated automatically by `yarn twenty dev` whenever your objects or fields change.
The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema.
#### Runtime credentials in logic functions
<Note>
**CoreApiClient is generated at dev/build time.** If you try to use it without running `yarn twenty dev` or `yarn twenty build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema, generates a typed client using `@genql/cli`, writes the generated sources to `node_modules/twenty-client-sdk/dist/core/generated/`, and replaces the stubs in `node_modules/twenty-client-sdk/dist/core.mjs` and `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
When your function runs on Twenty, the platform injects credentials as environment variables before your code executes:
#### Using CoreSchema for type annotations
- `TWENTY_API_URL`: Base URL of the Twenty API your app targets.
- `TWENTY_API_KEY`: Shortlived key scoped to your application's default function role.
`CoreSchema` provides TypeScript types matching your workspace objects, useful for typing component state or function parameters:
Notes:
- You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime.
- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. This is the default role used by logic functions of your application.
- Applications can define roles to follow leastprivilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier.
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` ships pre-built with the SDK (no generation required). It queries the `/metadata` endpoint for workspace configuration, applications, and file uploads:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Runtime credentials
When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables:
- `TWENTY_API_URL` — Base URL of the Twenty API
- `TWENTY_API_KEY` — Short-lived key scoped to your application's default function role
You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role referenced in `defaultRoleUniversalIdentifier` in your `application-config.ts`.
#### Uploading files
The generated `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
`MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields. It implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -641,25 +924,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
The method signature:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `fileBuffer` | `Buffer` | The raw file contents |
@@ -668,8 +940,7 @@ uploadFile(
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
Key points:
- The `uploadFile` method is available on `MetadataApiClient` because the upload mutation is resolved by the `/metadata` endpoint.
- It uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed — consistent with how apps reference fields everywhere else.
- Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed.
- The returned `url` is a signed URL you can use to access the uploaded file.
### Hello World example
@@ -9,17 +9,18 @@ Apps are currently in alpha testing. The feature is functional but still evolvin
Apps let you extend Twenty with custom objects, fields, logic functions, AI skills, and UI components — all managed as code.
**What you can do today:**
- Define custom objects and fields as code (managed data model)
- Build logic functions with custom triggers (HTTP routes, cron, database events)
- Define skills for AI agents
- Build front components that render inside Twenty's UI
- Deploy the same app across multiple workspaces
**What you can build:**
- Custom objects, fields, views, and navigation items to shape your data model
- Logic functions triggered by HTTP routes, cron schedules, or database events
- Front components that render directly inside Twenty's UI
- Skills that extend Twenty's AI agents
- Deploy an app across multiple workspaces
## Prerequisites
- Node.js 24+ and Yarn 4
- Docker (for the local Twenty dev server)
- Node.js 24+
- Yarn 4
- Docker (or a running local Twenty instance)
## Getting Started
@@ -28,27 +29,15 @@ Create a new app using the official scaffolder, then authenticate and start deve
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
The scaffolder supports two modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Use `--minimal` option to scaffold a minimal installation
From here you can:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -122,7 +111,7 @@ With `--minimal`, only the core files are created (`application-config.ts`, `rol
At a high level:
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus a `twenty` script that delegates to the local `twenty` CLI. Run `yarn twenty help` to list all available commands.
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, coverage folders, log files, and `.env*` files.
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
- **.nvmrc**: Pins the Node.js version expected by the project.
- **.oxlintrc.json** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
@@ -165,8 +154,8 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty dev` will auto-generate two typed API clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (for workspace data via `/graphql`) and `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
- `yarn twenty dev` will auto-generate the typed `CoreApiClient` (for workspace data via `/graphql`) into `node_modules/twenty-client-sdk/`. The `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built and is available immediately. Import them from `twenty-client-sdk/core` and `twenty-client-sdk/metadata` respectively.
- `yarn twenty add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -12,7 +12,25 @@ Apps are currently in alpha testing. The feature is functional but still evolvin
Once your app is [built and tested locally](/developers/extend/apps/building), you have two paths for distributing it:
- **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install.
- **Push a tarball** — deploy your app to a specific Twenty server for internal use without making it publicly available.
- **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use.
Both paths start from the same **build** step.
## Building your app
The `build` command compiles your TypeScript sources, transpiles logic functions and front components, and generates a `manifest.json` that describes your app's contents:
```bash filename="Terminal"
yarn twenty build
```
The output is written to `.twenty/output/`. This directory contains everything needed for distribution: compiled code, assets, the manifest, and a copy of your `package.json`.
To also create a `.tgz` tarball (used by the deploy command internally, or for manual distribution):
```bash filename="Terminal"
yarn twenty build --tarball
```
## Publishing to npm
@@ -21,29 +39,76 @@ Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twe
### Requirements
- An [npm](https://www.npmjs.com) account
- Your package name **must** use the `twenty-app-` prefix (e.g., `twenty-app-postcard-sender`)
- The `twenty-app` keyword **must** be listed in your `package.json` `keywords` array
### Adding the required keyword
The Twenty marketplace discovers apps by searching the npm registry for packages with the `twenty-app` keyword. Add it to your `package.json`:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
The marketplace searches for `keywords:twenty-app` on the npm registry. Without this keyword, your package won't appear in the marketplace even if it has the `twenty-app-` name prefix.
</Note>
### Steps
1. **Build your app** — the CLI compiles your TypeScript sources and generates the application manifest:
1. **Build your app:**
```bash filename="Terminal"
yarn twenty build
```
2. **Publish to npm** — push the built package to the npm registry:
2. **Publish to npm:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Auto-discovery
This runs `npm publish` from the `.twenty/output/` directory.
Packages with the `twenty-app-` prefix are automatically discovered by the Twenty marketplace catalog. Once published, your app appears in the marketplace within a few minutes — no manual registration or approval required.
To publish under a specific dist-tag (e.g., `beta` or `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### How marketplace discovery works
The Twenty server syncs its marketplace catalog from the npm registry **every hour**:
1. It searches for all npm packages with the `keywords:twenty-app` keyword
2. For each package, it fetches the `manifest.json` from the npm CDN
3. The app's metadata (name, description, author, logo, screenshots, category) is extracted from the manifest and displayed in the marketplace
After publishing, your app can take up to one hour to appear in the marketplace. To trigger the sync immediately instead of waiting for the next hourly run:
```bash filename="Terminal"
yarn twenty catalog-sync
```
To target a specific remote:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
The metadata shown in the marketplace comes from your `defineApplication()` call in your app source code — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
<Note>
If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`.
</Note>
### CI publishing
The scaffolded project includes a GitHub Actions workflow that publishes on every release. It runs `app:build`, then `npm publish --provenance` from the build output:
The scaffolded project includes a GitHub Actions workflow that publishes on every release:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `npx twenty build`, then `npm publish` from `.twenty/output`.
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`.
<Tip>
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
</Tip>
## Internal distribution
## Deploying to a server (tarball)
For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can push a tarball directly to a Twenty server.
For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server.
### Push a tarball
### Prerequisites
Build your app and deploy it to a specific server in one step:
Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`.
Add a remote:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Any workspace on that server can then install and upgrade the app from the **Applications** settings page.
For a local development server:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
You can also authenticate with an API key for non-interactive environments:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Manage your remotes:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Deploying
Build and upload your app to the server in one step:
```bash filename="Terminal"
yarn twenty deploy
```
This builds the app with `--tarball`, then uploads the tarball to the default remote via a GraphQL multipart upload.
To deploy to a specific remote:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Sharing a deployed app
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. To share a deployed app:
1. Go to **Settings > Applications > Registrations** and open your app
2. In the **Distribution** tab, click **Copy share link**
3. Share this link with users on other workspaces — it takes them directly to the app's install page
The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server.
### Version management
To release an update:
1. Bump the `version` field in your `package.json`
2. Push a new tarball with `npx twenty publish --server <server-url>`
3. Workspaces on that server will see the upgrade available in their settings
2. Run `yarn twenty deploy` (or `yarn twenty deploy -r production`)
3. Workspaces that have the app installed will see the upgrade available in their settings
<Note>
Internal apps are scoped to the server they're pushed to. They won't appear in the public marketplace and can't be installed by workspaces on other servers.
</Note>
## Installing apps
## App categories
Once an app is published (npm) or deployed (tarball), workspaces install it through the UI:
```bash filename="Terminal"
yarn twenty install
```
Or from the **Settings > Applications** page in the Twenty UI, where both marketplace and tarball-deployed apps can be browsed and installed.
## App distribution categories
Twenty organizes apps into three categories based on how they're distributed:
| Category | How it works | Visible in marketplace? |
|----------|-------------|------------------------|
| **Development** | Local dev mode apps running via `yarn twenty dev`. Used for building and testing. | No |
| **Published** | Apps published to npm with the `twenty-app-` prefix. Listed in the marketplace for any workspace to install. | Yes |
| **Internal** | Apps deployed via tarball to a specific server. Available only to workspaces on that server. | No |
| **Published (npm)** | Apps published to npm with the `twenty-app` keyword. Listed in the marketplace for any workspace to install. | Yes |
| **Internal (tarball)** | Apps deployed via tarball to a specific server. Available only to workspaces on that server via a share link. | No |
<Tip>
Start in **Development** mode while building your app. When it's ready, choose **Published** (npm) for broad distribution or **Internal** (tarball) for private deployment.
</Tip>
## CLI reference
| Command | Description | Key flags |
|---------|-------------|-----------|
| `yarn twenty build` | Compile app and generate manifest | `--tarball` — also create a `.tgz` package |
| `yarn twenty publish` | Build and publish to npm | `--tag <tag>` — npm dist-tag (e.g., `beta`, `next`) |
| `yarn twenty deploy` | Build and upload tarball to a server | `-r, --remote <name>` — target remote |
| `yarn twenty catalog-sync` | Trigger marketplace catalog sync on the server | `-r, --remote <name>` — target remote |
| `yarn twenty install` | Install a deployed app on a workspace | `-r, --remote <name>` — target remote |
| `yarn twenty dev` | Watch and sync local changes | Uses default remote |
| `yarn twenty remote add` | Add a server connection | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | List configured remotes | — |
| `yarn twenty remote switch` | Set default remote | — |
| `yarn twenty remote status` | Show connection status | — |
| `yarn twenty remote remove` | Remove a remote | — |
@@ -37,7 +37,7 @@ yarn twenty dev
### Local Server Management
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker on port 2020). These commands only apply to the Docker-based dev server — they do not manage a Twenty instance started from source (e.g. `npx nx start twenty-server` on port 3000):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
@@ -15,19 +15,21 @@ description: عرّف الكائنات، والدوال المنطقية، وم
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](/l/ar/developers/extend/apps/getting-started#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
| دالة | الغرض |
| -------------------------------- | ---------------------------------------------------- |
| `defineApplication` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
| `defineObject` | تعريف كائنات مخصصة مع حقول |
| `defineLogicFunction` | تعريف وظائف منطقية مع معالجات |
| `definePreInstallLogicFunction` | تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق) |
| `definePostInstallLogicFunction` | تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق) |
| `defineFrontComponent` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
| `defineRole` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
| `defineField` | وسّع الكائنات الموجودة بحقول إضافية |
| `defineView` | تعريف العروض المحفوظة للكائنات |
| `defineNavigationMenuItem` | تعريف روابط التنقل في الشريط الجانبي |
| `defineSkill` | عرّف مهارات وكيل الذكاء الاصطناعي |
| دالة | الغرض |
| -------------------------------- | -------------------------------------------------------------- |
| `defineApplication` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
| `defineObject` | تعريف كائنات مخصصة مع حقول |
| `defineField` | وسّع الكائنات الموجودة بحقول إضافية أو عرّف حقول علاقات مستقلة |
| `defineLogicFunction` | تعريف وظائف منطقية مع معالجات |
| `definePreInstallLogicFunction` | تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق) |
| `definePostInstallLogicFunction` | تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق) |
| `defineFrontComponent` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
| `defineRole` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
| `defineView` | تعريف العروض المحفوظة للكائنات |
| `defineNavigationMenuItem` | تعريف روابط التنقل في الشريط الجانبي |
| `defineSkill` | عرّف مهارات وكيل الذكاء الاصطناعي |
| `defineAgent` | عرّف وكلاء الذكاء الاصطناعي |
| `definePageLayout` | عرّف تخطيطات صفحات مخصّصة |
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
@@ -36,7 +38,7 @@ description: عرّف الكائنات، والدوال المنطقية، وم
تصف الكائنات المخصصة كلًا من المخطط والسلوك للسجلات في مساحة عملك. استخدم `defineObject()` لتعريف كائنات مع تحقق مدمج:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ export default defineObject({
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
<Note>
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية
@@ -120,6 +122,197 @@ export default defineObject({
لكن هذا غير مستحسن.
</Note>
### تعريف الحقول على الكائنات الموجودة
استخدم `defineField()` لإضافة حقول إلى كائنات لا تملكها — مثل كائنات Twenty القياسية (Person, Company, etc.) أو كائنات من تطبيقات أخرى. على خلاف الحقول المضمّنة في `defineObject()`، تتطلّب الحقول المستقلة `objectUniversalIdentifier` لتحديد الكائن الذي تقوم بتوسيعه:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
النقاط الرئيسية:
* `objectUniversalIdentifier` يحدّد الكائن الهدف. بالنسبة للكائنات القياسية، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` المُصدَّر من `twenty-sdk`.
* عند تعريف الحقول بشكل مضمّن في `defineObject()`، **لا** تحتاج إلى `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب.
* `defineField()` هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام `defineObject()`.
### العلاقات
تربط العلاقات الكائنات معًا. في Twenty، تكون العلاقات دائمًا **ثنائية الاتجاه** — حيث تعرّف الجانبين، ويشير كل جانب إلى الآخر.
هناك نوعان من العلاقات:
| نوع العلاقة | الوصف | هل لديه مفتاح خارجي؟ |
| ------------- | ------------------------------------------------------ | ---------------------- |
| `MANY_TO_ONE` | تشير العديد من سجلات هذا الكائن إلى سجل واحد من الهدف | نعم (`joinColumnName`) |
| `ONE_TO_MANY` | يحتوي سجل واحد من هذا الكائن على العديد من سجلات الهدف | لا (الجانب العكسي) |
#### كيف تعمل العلاقات
تتطلّب كل علاقة **حقلين** يشيران إلى بعضهما البعض:
1. جانب **MANY_TO_ONE** — يوجد على الكائن الذي يحمل المفتاح الخارجي
2. جانب **ONE_TO_MANY** — يوجد على الكائن الذي يملك المجموعة
يستخدم كلا الحقلين `FieldType.RELATION` ويُحيل كلٌ منهما إلى الآخر عبر `relationTargetFieldMetadataUniversalIdentifier`.
#### مثال: البطاقة البريدية لديها العديد من المستلمين
افترض أن `PostCard` يمكن إرسالها إلى العديد من سجلات `PostCardRecipient`. ينتمي كل مستلم إلى بطاقة بريدية واحدة بالضبط.
**الخطوة 1: عرّف جانب ONE_TO_MANY على PostCard** (جانب "الواحد"):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**الخطوة 2: عرّف جانب MANY_TO_ONE على PostCardRecipient** (جانب "العديد" — يحمل المفتاح الخارجي):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**الاستيرادات الدائرية:** كلا حقلي العلاقة يُحيل كلٌ منهما إلى `universalIdentifier` الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدّر معرّفات الحقول كثوابت مسمّاة من كل ملف، واستوردها في الملف الآخر. يقوم نظام البناء بحلّها في وقت الترجمة.
</Note>
#### الربط مع الكائنات القياسية
لإنشاء علاقة مع كائن Twenty مضمّن (Person, Company, etc.)، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### خصائص حقل العلاقة
| الخاصية | مطلوب | الوصف |
| ------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------- |
| `type` | نعم | يجب أن يكون `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للكائن الهدف |
| `relationTargetFieldMetadataUniversalIdentifier` | نعم | قيمة `universalIdentifier` للحقل المطابق على الكائن الهدف |
| `universalSettings.relationType` | نعم | `RelationType.MANY_TO_ONE` أو `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE فقط | ماذا يحدث عند حذف السجل المشار إليه: `CASCADE`، `SET_NULL`، `RESTRICT`، أو `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE فقط | اسم عمود قاعدة البيانات للمفتاح الخارجي (مثل `postCardId`) |
#### حقول العلاقات المضمّنة في defineObject
يمكنك أيضًا تعريف حقول العلاقات مباشرةً داخل `defineObject()`. في هذه الحالة، احذف `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### تكوين التطبيق (application-config.ts)
كل تطبيق لديه ملف واحد `application-config.ts` يصف:
@@ -161,6 +354,22 @@ export default defineApplication({
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
* يتم اكتشاف دوال ما قبل التثبيت وما بعد التثبيت تلقائيًا أثناء إنشاء ملف البيان. راجع [دوال ما قبل التثبيت](#pre-install-functions) و[دوال ما بعد التثبيت](#post-install-functions).
#### بيانات التعريف لسوق التطبيقات
إذا كنت تخطط لـ [نشر تطبيقك](/l/ar/developers/extend/apps/publishing)، فإن هذه الحقول الاختيارية تتحكّم في كيفية ظهور تطبيقك في السوق:
| الحقل | الوصف |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `author` | اسم المؤلف أو الشركة |
| `category` | فئة التطبيق لتصفية سوق التطبيقات |
| `logoUrl` | المسار إلى شعار تطبيقك (نسبيًا إلى `./assets/`) |
| `screenshots` | مصفوفة لمسارات لقطات الشاشة (نسبيًا إلى `./assets/`) |
| `aboutDescription` | وصف ماركداون أطول لعلامة التبويب "حول". إذا لم يتم تضمينه، يستخدم السوق ملف `README.md` الخاص بالحزمة من npm |
| `websiteUrl` | رابط إلى موقعك الإلكتروني |
| `termsUrl` | رابط إلى شروط الخدمة |
| `emailSupport` | عنوان البريد الإلكتروني للدعم |
| `issueReportUrl` | رابط إلى متتبّع المشاكل |
#### الأدوار والصلاحيات
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك.
@@ -230,7 +439,7 @@ export default defineRole({
كل ملف وظيفة يستخدم `defineLogicFunction()` لتصدير تكوين مع معالج ومشغّلات اختيارية.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
النقاط الرئيسية:
@@ -334,7 +543,7 @@ yarn twenty function:execute --preInstall
* يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `preInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
* تم ضبط المهلة الافتراضية على 300 ثانية (5 دقائق) للسماح بمهام التحضير الأطول.
* لا تحتاج دوال ما قبل التثبيت إلى مُشغِّلات — إذ يستدعيها النظام الأساسي قبل التثبيت أو يدويًا عبر `function:execute --preInstall`.
* لا تحتاج دوال ما قبل التثبيت إلى مشغّلات — إذ يستدعيها النظام الأساسي قبل التثبيت أو يدويًا عبر `exec --preInstall`.
### دوال ما بعد التثبيت
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
النقاط الرئيسية:
@@ -372,7 +581,7 @@ yarn twenty function:execute --postInstall
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `postInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
* لا تحتاج دوال ما بعد التثبيت إلى مُشغِّلات — حيث يستدعيها النظام الأساسي أثناء التثبيت أو يدويًا عبر `function:execute --postInstall`.
* لا تحتاج دوال ما بعد التثبيت إلى مشغّلات — حيث يستدعيها النظام الأساسي أثناء التثبيت أو يدويًا عبر `exec --postInstall`.
### حمولة مشغل المسار
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
يحتوي نوع `RoutePayload` على البنية التالية:
| الخاصية | النوع | الوصف |
| ---------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) |
| `pathParameters` | `Record<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار (على سبيل المثال، `/users/:id` `{ id: '123' }`) |
| `المحتوى` | `object \| null` | جسم الطلب المُحلَّل (JSON) |
| `isBase64Encoded` | `قيمة منطقية` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 |
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | المسار الخام للطلب |
| الخاصية | النوع | الوصف |
| ---------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | رؤوس HTTP (فقط تلك المدرجة في `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | معلمات سلسلة الاستعلام (تُضمّ القيم المتعددة باستخدام فواصل) |
| `pathParameters` | `Record<string, string \| undefined>` | معلمات المسار المستخرجة من نمط المسار (على سبيل المثال، `/users/:id` -> `{ id: '123' }`) |
| `المحتوى` | `object \| null` | جسم الطلب المُحلَّل (JSON) |
| `isBase64Encoded` | `قيمة منطقية` | ما إذا كان جسم الطلب مُرمَّزًا بترميز base64 |
| `requestContext.http.method` | `string` | طريقة HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | المسار الخام للطلب |
### تمرير رؤوس HTTP
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
يمكنك إنشاء وظائف جديدة بطريقتين:
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة دالة منطقية جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
* **مُنشأ بالقالب**: شغّل `yarn twenty add` واختر خيار إضافة وظيفة منطقية جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
* **يدوي**: أنشئ ملفًا جديدًا `*.logic-function.ts` واستخدم `defineLogicFunction()` مع اتباع النمط نفسه.
### تمييز دالة منطقية كأداة
@@ -478,7 +687,7 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ export default defineFrontComponent({
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
* **مُنشأ بالقالب**: شغّل `yarn twenty add` واختر خيار إضافة مكوّن أمامي جديد.
* **يدوي**: أنشئ ملفًا جديدًا `.tsx` واستخدم `defineFrontComponent()` مع اتباع النمط نفسه.
### المهارات
@@ -602,47 +811,122 @@ export default defineSkill({
يمكنك إنشاء مهارات جديدة بطريقتين:
* **مُنشأ بالقالب**: شغِّل `yarn twenty entity:add` واختر خيار إضافة مهارة جديدة.
* **مُنشأ بالقالب**: شغّل `yarn twenty add` واختر خيار إضافة مهارة جديدة.
* **يدوي**: أنشئ ملفًا جديدًا واستخدم `defineSkill()` مع اتباع النمط نفسه.
### عملاء مُولَّدون مضبوطو الأنواع
### عملاء واجهة برمجة التطبيقات ذات أنواع ثابتة (`twenty-client-sdk`)
يتم توليد عميلين مضبوطي الأنواع تلقائيًا بواسطة `yarn twenty dev` وتخزينهما في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك:
توفر حزمة `twenty-client-sdk` عميلين لـ GraphQL ذوي أنواع ثابتة للتفاعل مع واجهة Twenty البرمجية من وظائفك المنطقية ومكوّنات الواجهة الأمامية:
* **`CoreApiClient`** — يُجري استعلامات إلى نقطة النهاية `/graphql` للحصول على بيانات مساحة العمل
* **`MetadataApiClient`** — يستعلم عن نقطة النهاية `/metadata` لتكوين مساحة العمل وتحميل الملفات.
| العميل | استيراد | نقطة النهاية | مُولَّد؟ |
| ------------------- | ---------------------------- | --------------------------------------------------- | -------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — بيانات مساحة العمل (السجلات، الكائنات) | نعم، في وقت التطوير/البناء |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — تكوين مساحة العمل، رفع الملفات | لا، يأتي مُجهزًا مسبقًا |
#### CoreApiClient
`CoreApiClient` هو العميل الرئيسي للاستعلام وتعديل بيانات مساحة العمل. يتم توليده من مخطط مساحة العمل الخاصة بك أثناء `yarn twenty dev` أو `yarn twenty build`، لذا فهو مكتوب الأنواع بالكامل ليتوافق مع كائناتك وحقولك.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
يُعاد توليد كلا العميلين تلقائيًا بواسطة `yarn twenty dev` كلما تغيّرت كائناتك أو حقولك.
يستخدم العميل صياغة مجموعة اختيار: مرِّر `true` لتضمين حقل، واستخدم `__args` للوسيطات، وعشّش الكائنات للعلاقات. ستحصل على إكمال تلقائي كامل وفحص للأنواع يعتمد على مخطط مساحة العمل لديك.
#### بيانات الاعتماد وقت التشغيل في الدوال المنطقية
<Note>
**يتم توليد CoreApiClient في وقت التطوير/البناء.** إذا حاولت استخدامه دون تشغيل `yarn twenty dev` أو `yarn twenty build` أولًا، فسوف ينتج خطأ. تتم عملية التوليد تلقائيًا — حيث يقوم CLI بفحص مخطط GraphQL الخاص بمساحة العمل لديك، ويولّد عميلًا مكتوب الأنواع باستخدام `@genql/cli`، ويكتب المصادر المُولَّدة إلى `node_modules/twenty-client-sdk/dist/core/generated/`، ويستبدل الأجزاء الوهمية في `node_modules/twenty-client-sdk/dist/core.mjs` و`node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
عندما تعمل دالتك على Twenty، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئة قبل تنفيذ كودك:
#### استخدام CoreSchema للتعليقات التوضيحية للأنواع
* `TWENTY_API_URL`: عنوان URL الأساسي لواجهة Twenty البرمجية التي يستهدفها تطبيقك.
* `TWENTY_API_KEY`: مفتاح قصير العمر ذو نطاق يقتصر على الدور الافتراضي لوظيفة تطبيقك.
يوفّر `CoreSchema` أنواع TypeScript المطابقة لكائنات مساحة العمل لديك، وهو مفيد لتعيين أنواع حالة المكوّن أو معاملات الدوال:
الملاحظات:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* لا تحتاج إلى تمرير عنوان URL أو مفتاح واجهة برمجة التطبيقات إلى العميل المُولَّد. يقوم بقراءة `TWENTY_API_URL` و`TWENTY_API_KEY` من process.env وقت التشغيل.
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application-config.ts` عبر `defaultRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الدوال المنطقية في تطبيقك.
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `defaultRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
يأتي `MetadataApiClient` مُجهّزًا مسبقًا مع SDK (لا حاجة للتوليد). يستعلم عن نقطة النهاية `/metadata` للحصول على تكوين مساحة العمل والتطبيقات ورفع الملفات:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### بيانات الاعتماد أثناء وقت التشغيل
عند تشغيل كودك على Twenty (وظائف منطقية أو مكوّنات أمامية)، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئية:
* `TWENTY_API_URL` — عنوان URL الأساسي لواجهة Twenty البرمجية
* `TWENTY_API_KEY` — مفتاح قصير العمر ذو نطاق يقتصر على الدور الافتراضي لوظيفة تطبيقك
لست **بحاجة** إلى تمرير هذه القيم إلى العملاء — فهي تُقرأ تلقائيًا من `process.env`. تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `defaultRoleUniversalIdentifier` ضمن `application-config.ts`.
#### رفع الملفات
يتضمن `MetadataApiClient` المُولَّد طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع ملف ضمن كائنات مساحة العمل الخاصة بك. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
يتضمن `MetadataApiClient` طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع الملف. يطبّق [مواصفة طلب GraphQL متعددة الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
توقيع الطريقة:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| المعلمة | النوع | الوصف |
| ---------------------------------- | -------- | ---------------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | المحتوى الخام للملف |
@@ -680,8 +953,7 @@ uploadFile(
النقاط الرئيسية:
* تتوفر طريقة `uploadFile` على `MetadataApiClient` لأن عملية الـ mutation الخاصة بالرفع تُعالَج عبر نقطة النهاية `/metadata`.
* تستخدم `universalIdentifier` الخاص بالحقل (وليس المعرّف الخاص بمساحة العمل)، ليعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك — بما يتماشى مع كيفية إشارة التطبيقات إلى الحقول في كل مكان آخر.
* يستخدم `universalIdentifier` الخاص بالحقل (وليس معرّفه الخاص بمساحة العمل)، بحيث يعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك.
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
### مثال Hello World
@@ -9,18 +9,19 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول ووظائف منطقية ومهارات ذكاء اصطناعي ومكونات واجهة مستخدم مخصصة — جميعها تُدار ككود.
**ما الذي يمكنك فعله اليوم:**
**ما الذي يمكنك بناؤه:**
* عرِّف كائنات وحقولًا مخصصة على شكل كود (نموذج بيانات مُدار)
* أنشئ وظائف منطقية مع مشغلات مخصصة (مسارات HTTP، cron، أحداث قاعدة البيانات)
* حدد المهارات لوكلاء الذكاء الاصطناعي
* أنشئ مكونات واجهية تُعرَض داخل واجهة مستخدم Twenty
* انشر التطبيق نفسه عبر مساحات عمل متعددة
* كائنات مخصّصة، وحقول، وطرق عرض، وعناصر تنقّل لتشكيل نموذج بياناتك
* دوال منطقية يتم تشغيلها عبر مسارات HTTP، وجداول cron، أو أحداث قاعدة البيانات
* مكوّنات واجهة أمامية تُعرَض مباشرة داخل واجهة مستخدم Twenty
* مهارات توسّع قدرات وكلاء الذكاء الاصطناعي في Twenty
* انشر تطبيقاً عبر مساحات عمل متعددة
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* Docker (لخادم تطوير Twenty المحلي)
* Node.js 24+
* Yarn 4
* Docker (أو مثيل Twenty محلي قيد التشغيل)
## البدء
@@ -29,27 +30,15 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
يدعم المُهيئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> استخدم الخيار `--minimal` لتهيئة تثبيت مصغّر
من هنا يمكنك:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ my-twenty-app/
بشكل عام:
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` بالإضافة إلى نص برمجي `twenty` يفوِّض إلى `twenty` CLI المحلي. شغِّل `yarn twenty help` لعرض جميع الأوامر المتاحة.
* **.gitignore**: يتجاهل العناصر الشائعة مثل `node_modules` و`.yarn` و`generated/` (عميل مضبوط الأنواع) و`dist/` و`build/` ومجلدات التغطية وملفات السجلات وملفات `.env*`.
* **.gitignore**: يتجاهل العناصر الشائعة مثل `node_modules` و`.yarn` و`.twenty/` و`dist/` و`build/` ومجلدات التغطية وملفات السجلات وملفات `.env*`.
* **yarn.lock**، **.yarnrc.yml**، **.yarn/**: تقوم بقفل وتكوين حزمة أدوات Yarn 4 المستخدمة في المشروع.
* **.nvmrc**: يثبّت إصدار Node.js المتوقع للمشروع.
* **.oxlintrc.json** و **tsconfig.json**: يقدّمان إعدادات الفحص والتهيئة لـ TypeScript لمصادر TypeScript في تطبيقك.
@@ -167,8 +156,8 @@ export default defineObject({
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
* سيقوم `yarn twenty dev` بتوليد عميلين API مضبوطي الأنواع تلقائيًا في `node_modules/twenty-sdk/generated`: `CoreApiClient` (لبيانات مساحة العمل عبر `/graphql`) و`MetadataApiClient` (لتكوين مساحة العمل وتحميل الملفات عبر `/metadata`).
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات ضمن `src/` لكائناتك المخصّصة، والوظائف، ومكوّنات الواجهة الأمامية، والأدوار، والمهارات، وغير ذلك.
* `yarn twenty dev` سيولّد تلقائياً `CoreApiClient` مضبوط الأنواع (لبيانات مساحة العمل عبر `/graphql`) داخل `node_modules/twenty-client-sdk/`. `MetadataApiClient` (لتهيئة مساحة العمل ورفع الملفات عبر `/metadata`) يأتي مُبنًى مسبقاً ومتاحاً على الفور. استوردْهما من `twenty-client-sdk/core` و`twenty-client-sdk/metadata` على الترتيب.
* `yarn twenty add` سيضيف ملفات تعريف الكيانات ضمن `src/` لكائناتك المخصّصة، والوظائف، ومكوّنات الواجهة الأمامية، والأدوار، والمهارات، وغير ذلك.
## المصادقة
@@ -12,7 +12,25 @@ description: وزّع تطبيق Twenty الخاص بك على سوق Twenty أ
بمجرد أن يكون تطبيقك [مبنيًا ومختبرًا محليًا](/l/ar/developers/extend/apps/building)، لديك مساران لتوزيعه:
* **النشر على npm** — أدرج تطبيقك في سوق Twenty ليتسنى لأي مساحة عمل اكتشافه وتثبيته.
* **إرسال tarball** — انشر تطبيقك إلى خادم Twenty معيّن للاستخدام الداخلي من دون جعله متاحًا للعامة.
* **نشر أرشيف tar** — ارفع تطبيقك مباشرةً إلى خادم Twenty محدد للاستخدام الداخلي أو الخاص.
كلا المسارين يبدآن من نفس خطوة **build**.
## بناء تطبيقك
يقوم الأمر `build` بتجميع مصادر TypeScript الخاصة بك، وتحويل دوال المنطق ومكوّنات الواجهة الأمامية، وإنشاء ملف `manifest.json` يصف محتويات تطبيقك:
```bash filename="Terminal"
yarn twenty build
```
يتم حفظ المخرجات في `.twenty/output/`. يحتوي هذا الدليل على كل ما يلزم للتوزيع: الكود المُجمَّع، والأصول، وملف manifest، ونسخة من `package.json` الخاص بك.
لإنشاء حزمة tarball بصيغة `.tgz` أيضًا (تُستخدم داخليًا بواسطة أمر النشر، أو للتوزيع اليدوي):
```bash filename="Terminal"
yarn twenty build --tarball
```
## النشر على npm
@@ -21,29 +39,76 @@ description: وزّع تطبيق Twenty الخاص بك على سوق Twenty أ
### المتطلبات
* حساب على [npm](https://www.npmjs.com)
* يجب أن يستخدم اسم الحزمة البادئة `twenty-app-` (مثلًا، `twenty-app-postcard-sender`)
* الكلمة المفتاحية `twenty-app` **يجب** أن تُدرج في مصفوفة `keywords` في `package.json` الخاص بك
### إضافة الكلمة المفتاحية المطلوبة
يعثر سوق Twenty على التطبيقات من خلال البحث في سجل npm عن الحزم التي تحتوي على الكلمة المفتاحية `twenty-app`. أضِفها إلى `package.json` الخاص بك:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
يبحث السوق عن `keywords:twenty-app` في سجل npm. من دون هذه الكلمة المفتاحية، لن تظهر حزمتك في السوق حتى وإن كانت تحمل بادئة الاسم `twenty-app-`.
</Note>
### الخطوات
1. **قم ببناء تطبيقك** — تقوم أداة CLI بتجميع مصادر TypeScript الخاصة بك وإنشاء ملف بيان التطبيق:
1. **بناء تطبيقك:**
```bash filename="Terminal"
yarn twenty build
```
2. **النشر على npm** — ادفع الحزمة المبنية إلى سجل npm:
2. **النشر على npm:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### الاكتشاف التلقائي
هذا يُشغِّل `npm publish` من دليل `.twenty/output/`.
تُكتشف الحِزم التي تحمل البادئة `twenty-app-` تلقائيًا بواسطة فهرس سوق Twenty. بعد نشره، سيظهر تطبيقك في السوق خلال بضع دقائق — من دون الحاجة إلى تسجيل يدوي أو موافقة يدوية.
للنشر تحت dist-tag معيّن (مثلًا: `beta` أو `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### كيف تعمل آلية الاكتشاف في السوق
يقوم خادم Twenty بمزامنة كتالوج السوق من سجل npm **كل ساعة**:
1. يبحث عن جميع حزم npm التي تحتوي على الكلمة المفتاحية `keywords:twenty-app`
2. ولكل حزمة، يجلب ملف `manifest.json` من شبكة CDN الخاصة بـ npm
3. يتم استخراج بيانات التعريف الخاصة بالتطبيق (الاسم، الوصف، المؤلف، الشعار، لقطات الشاشة، الفئة) من ملف manifest وعرضها في السوق
بعد النشر، قد يستغرق ظهور تطبيقك في السوق ما يصل إلى ساعة واحدة. لتشغيل المزامنة فورًا بدلًا من انتظار التشغيل التالي كل ساعة:
```bash filename="Terminal"
yarn twenty catalog-sync
```
لاستهداف remote معيّن:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
تأتي بيانات التعريف المعروضة في السوق من استدعائك لـ `defineApplication()` في الشيفرة المصدرية لتطبيقك — حقول مثل `displayName` و`description` و`author` و`category` و`logoUrl` و`screenshots` و`aboutDescription` و`websiteUrl` و`termsUrl`.
<Note>
إذا لم يحدد تطبيقك `aboutDescription` في `defineApplication()`، فسيستخدم السوق تلقائيًا ملف `README.md` الخاص بحزمتك من npm كمحتوى لصفحة حول. هذا يعني أنه يمكنك الاحتفاظ بملف README واحد لكل من npm وسوق Twenty. إذا كنت تريد وصفًا مختلفًا في السوق، فقم بتعيين `aboutDescription` بشكل صريح.
</Note>
### النشر عبر CI
يتضمن المشروع المُولَّد سير عمل GitHub Actions يقوم بالنشر عند كل إصدار. يشغِّل `app:build`، ثم ينفِّذ `npm publish --provenance` من مخرجات البناء:
يتضمن المشروع المُولَّد سير عمل GitHub Actions يقوم بالنشر عند كل إصدار:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
بالنسبة لأنظمة CI الأخرى (GitLab CI، CircleCI، إلخ)، تنطبق الأوامر الثلاثة نفسها: `yarn install`، ثم `npx twenty build`، ثم `npm publish` من `.twenty/output`.
بالنسبة لأنظمة CI الأخرى (GitLab CI، وCircleCI، إلخ)، تنطبق الأوامر الثلاثة نفسها: `yarn install`، ثم `yarn twenty build`، ثم `npm publish` من `.twenty/output`.
<Tip>
**npm provenance** اختياري ولكنه موصى به. يضيف النشر باستخدام `--provenance` شارة ثقة إلى إدراجك على npm، مما يتيح للمستخدمين التحقق من أن الحزمة تم بناؤها من التزام محدد ضمن خط أنابيب CI عام. راجع [وثائق npm provenance](https://docs.npmjs.com/generating-provenance-statements) للحصول على تعليمات الإعداد.
</Tip>
## التوزيع الداخلي
## النشر إلى خادم (tarball)
بالنسبة للتطبيقات التي لا تريد إتاحتها للعامة — مثل الأدوات المملوكة، أو عمليات التكامل الخاصة بالمؤسسات فقط، أو الإصدارات التجريبية — يمكنك إرسال tarball مباشرةً إلى خادم Twenty.
بالنسبة للتطبيقات التي لا تريد إتاحتها للعامة — مثل الأدوات المملوكة، أو عمليات التكامل الخاصة بالمؤسسات فقط، أو الإصدارات التجريبية — يمكنك نشر tarball مباشرةً إلى خادم Twenty.
### إرسال tarball
### المتطلبات الأساسية
قم ببناء تطبيقك وانشره إلى خادم محدد في خطوة واحدة:
قبل النشر، تحتاج إلى remote مُعدّ يشير إلى خادم الهدف. تُخزّن remotes عنوان URL للخادم وبيانات اعتماد المصادقة محليًا في `~/.twenty/config.json`.
أضِف remote:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
يمكن لأي مساحة عمل على ذلك الخادم بعدها تثبيت التطبيق وترقيته من صفحة الإعدادات **التطبيقات**.
لخادم تطوير محلي:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
يمكنك أيضًا إجراء المصادقة باستخدام مفتاح API للبيئات غير التفاعلية:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
إدارة remotes الخاصة بك:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### النشر
بناء تطبيقك ورفعه إلى الخادم في خطوة واحدة:
```bash filename="Terminal"
yarn twenty deploy
```
يُنشئ هذا التطبيق باستخدام `--tarball`، ثم يرفع ملف tarball إلى الـ remote الافتراضي عبر رفع متعدد الأجزاء لـ GraphQL.
لنشره إلى remote معيّن:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### مشاركة تطبيق منشور
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. لمشاركة تطبيق منشور:
1. اذهب إلى **الإعدادات > التطبيقات > التسجيلات** وافتح تطبيقك
2. في علامة التبويب **التوزيع**، انقر **نسخ رابط المشاركة**
3. شارك هذا الرابط مع المستخدمين في مساحات عمل أخرى — سيأخذهم مباشرةً إلى صفحة تثبيت التطبيق
يستخدم رابط المشاركة عنوان URL الأساسي للخادم (من دون أي نطاق فرعي لمساحة عمل)، لذا يعمل مع أي مساحة عمل على الخادم.
### إدارة الإصدارات
لطرح تحديث:
1. ارفع قيمة الحقل `version` في ملف `package.json`
2. أرسل tarball جديدًا باستخدام `npx twenty publish --server <server-url>`
3. سترى مساحات العمل على ذلك الخادم الترقية متاحة في إعداداتها
2. شغّل `yarn twenty deploy` (أو `yarn twenty deploy -r production`)
3. سترى مساحات العمل التي ثبّتت التطبيق الترقية متاحة في إعداداتها
<Note>
التطبيقات الداخلية مقتصرة على الخادم الذي تُرسل إليه. لن تظهر في السوق العام ولا يمكن لمساحات العمل على خوادم أخرى تثبيتها.
</Note>
## تثبيت التطبيقات
## فئات التطبيقات
بعد نشر التطبيق (npm) أو نشره إلى الخادم (tarball)، تقوم مساحات العمل بتثبيته عبر واجهة المستخدم:
```bash filename="Terminal"
yarn twenty install
```
أو من صفحة **الإعدادات > التطبيقات** في واجهة Twenty، حيث يمكن استعراض التطبيقات من السوق ومن النشر عبر tarball وتثبيتها.
## فئات توزيع التطبيقات
تُنظِّم Twenty التطبيقات في ثلاث فئات استنادًا إلى طريقة توزيعها:
| الفئة | كيف يعمل | مرئي في سوق Twenty؟ |
| ----------- | ---------------------------------------------------------------------------------------------------- | ------------------- |
| **التطوير** | تطبيقات وضع التطوير المحلي التي تعمل عبر `yarn twenty dev`. تُستخدم للبناء والاختبار. | لا |
| **منشور** | تطبيقات منشورة على npm مع البادئة `twenty-app-`. مدرجة في سوق Twenty لتتمكن أي مساحة عمل من تثبيتها. | نعم |
| **داخلي** | تطبيقات منشورة عبر tarball إلى خادم محدد. متاحة فقط لمساحات العمل على ذلك الخادم. | لا |
| الفئة | كيف يعمل | مرئي في سوق Twenty؟ |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------- |
| **التطوير** | تطبيقات وضع التطوير المحلي التي تعمل عبر `yarn twenty dev`. تُستخدم للبناء والاختبار. | لا |
| **منشور (npm)** | تطبيقات منشورة على npm تحتوي على الكلمة المفتاحية `twenty-app`. مدرجة في سوق Twenty لتتمكن أي مساحة عمل من تثبيتها. | نعم |
| **داخلي (tarball)** | تطبيقات منشورة عبر tarball إلى خادم محدد. متاحة فقط لمساحات العمل على ذلك الخادم عبر رابط مشاركة. | لا |
<Tip>
ابدأ في وضع **التطوير** أثناء بناء تطبيقك. عندما يصبح جاهزًا، اختر **منشور** (npm) للتوزيع الواسع أو **داخلي** (tarball) للنشر الخاص.
</Tip>
## مرجع CLI
| أمر | الوصف | الأعلام الرئيسية |
| --------------------------- | ------------------------------------ | ----------------------------------------------------- |
| `yarn twenty build` | تجميع التطبيق وإنشاء manifest | `--tarball` — يقوم أيضًا بإنشاء حزمة `.tgz` |
| `yarn twenty publish` | بناء التطبيق ونشره إلى npm | `--tag <tag>` — وسم توزيع npm (مثلًا: `beta`، `next`) |
| `yarn twenty deploy` | بناء ورفع tarball إلى خادم | `-r, --remote <name>` — الـ remote المستهدف |
| `yarn twenty catalog-sync` | تشغيل مزامنة كتالوج السوق على الخادم | `-r, --remote <name>` — الـ remote المستهدف |
| `yarn twenty install` | تثبيت تطبيق منشور على مساحة عمل | `-r, --remote <name>` — الـ remote المستهدف |
| `yarn twenty dev` | مراقبة ومزامنة التغييرات المحلية | يستخدم الـ remote الافتراضي |
| `yarn twenty remote add` | إضافة اتصال بخادم | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | عرض الـ remotes المُكوَّنة | — |
| `yarn twenty remote switch` | تعيين الـ remote الافتراضي | — |
| `yarn twenty remote status` | عرض حالة الاتصال | — |
| `yarn twenty remote remove` | إزالة remote | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### إدارة الخادم المحلي
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل على المنفذ 2020). تنطبق هذه الأوامر فقط على خادم التطوير المستند إلى Docker — ولا تُدير مثيل Twenty المُشغَّل من المصدر (مثل `npx nx start twenty-server` على المنفذ 3000):
```bash filename="Terminal"
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
@@ -15,19 +15,21 @@ twenty-sdk poskytuje typované stavební bloky a pomocné funkce, které použí
SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je popsáno v [Detekce entit](/l/cs/developers/extend/apps/getting-started#entity-detection), musíte použít `export default define<Entity>({...})`, aby byly vaše entity detekovány:
| Funkce | Účel |
| -------------------------------- | ----------------------------------------------------------------- |
| `defineApplication` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
| `defineObject` | Definice vlastních objektů s poli |
| `defineLogicFunction` | Definice logických funkcí s obslužnými funkcemi |
| `definePreInstallLogicFunction` | Definujte předinstalační logickou funkci (jedna na aplikaci) |
| `definePostInstallLogicFunction` | Definujte postinstalační logickou funkci (jedna na aplikaci) |
| `defineFrontComponent` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
| `defineRole` | Konfigurace oprávnění rolí a přístupu k objektům |
| `defineField` | Rozšiřte existující objekty o další pole |
| `defineView` | Definujte uložená zobrazení pro objekty |
| `defineNavigationMenuItem` | Definujte odkazy postranní navigace |
| `defineSkill` | Definujte dovednosti agenta AI |
| Funkce | Účel |
| -------------------------------- | ------------------------------------------------------------------------------- |
| `defineApplication` | Nakonfigurujte metadata aplikace (povinné, jedno na aplikaci) |
| `defineObject` | Definice vlastních objektů s poli |
| `defineField` | Rozšiřte existující objekty o další pole nebo definujte samostatná relační pole |
| `defineLogicFunction` | Definice logických funkcí s obslužnými funkcemi |
| `definePreInstallLogicFunction` | Definujte předinstalační logickou funkci (jedna na aplikaci) |
| `definePostInstallLogicFunction` | Definujte postinstalační logickou funkci (jedna na aplikaci) |
| `defineFrontComponent` | Definujte frontendové komponenty pro vlastní uživatelské rozhraní |
| `defineRole` | Konfigurace oprávnění rolí a přístupu k objektům |
| `defineView` | Definujte uložená zobrazení pro objekty |
| `defineNavigationMenuItem` | Definujte odkazy postranní navigace |
| `defineSkill` | Definujte dovednosti agenta AI |
| `defineAgent` | Definujte agenty AI |
| `definePageLayout` | Definujte vlastní rozvržení stránek |
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
@@ -36,7 +38,7 @@ Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automati
Vlastní objekty popisují jak schéma, tak chování záznamů ve vašem pracovním prostoru. K definování objektů s vestavěnou validací použijte `defineObject()`:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ Hlavní body:
* Hodnota `universalIdentifier` musí být jedinečná a stabilní napříč nasazeními.
* Každé pole vyžaduje `name`, `type`, `label` a svůj vlastní stabilní `universalIdentifier`.
* Pole `fields` je volitelné — objekty můžete definovat i bez vlastních polí.
* Nové objekty můžete vygenerovat pomocí `yarn twenty entity:add`, který vás provede pojmenováním, poli a vztahy.
* Nové objekty můžete vygenerovat pomocí `yarn twenty add`, který vás provede pojmenováním, poli a vztahy.
<Note>
**Základní pole jsou vytvořena automaticky.** Když definujete vlastní objekt, Twenty automaticky přidá standardní pole
@@ -120,6 +122,197 @@ Výchozí pole můžete přepsat definováním pole se stejným názvem v poli `
ale to se nedoporučuje.
</Note>
### Definování polí u existujících objektů
Pomocí `defineField()` přidejte pole k objektům, které nevlastníte — například ke standardním objektům Twenty (Person, Company atd.). nebo k objektům z jiných aplikací. Na rozdíl od inline polí v `defineObject()` vyžadují samostatná pole `objectUniversalIdentifier` k určení, který objekt rozšiřují:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Hlavní body:
* `objectUniversalIdentifier` identifikuje cílový objekt. Pro standardní objekty použijte `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exportovaný z `twenty-sdk`.
* Při definování polí inline v `defineObject()` `objectUniversalIdentifier` nepotřebujete — dědí se z nadřazeného objektu.
* `defineField()` je jediný způsob, jak přidat pole k objektům, které jste nevytvořili pomocí `defineObject()`.
### Vztahy
Relace propojují objekty. Ve Twenty jsou relace vždy obousměrné — definujete obě strany a každá strana odkazuje na tu druhou.
Existují dva typy relací:
| Typ vztahu | Popis | Má cizí klíč? |
| ------------- | --------------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Mnoho záznamů tohoto objektu ukazuje na jeden záznam cílového objektu | Ano (`joinColumnName`) |
| `ONE_TO_MANY` | Jeden záznam tohoto objektu má mnoho záznamů cílového objektu | Ne (inverzní strana) |
#### Jak fungují relace
Každá relace vyžaduje dvě pole, která na sebe vzájemně odkazují:
1. Strana MANY_TO_ONE — je na objektu, který drží cizí klíč
2. Strana ONE_TO_MANY — je na objektu, který vlastní kolekci
Obě pole používají `FieldType.RELATION` a vzájemně se odkazují prostřednictvím `relationTargetFieldMetadataUniversalIdentifier`.
#### Příklad: Pohlednice má mnoho příjemců
Předpokládejme, že `PostCard` lze odeslat mnoha záznamům `PostCardRecipient`. Každý příjemce náleží přesně jedné pohlednici.
**Krok 1: Definujte stranu ONE_TO_MANY na PostCard** (strana "one"):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Krok 2: Definujte stranu MANY_TO_ONE na PostCardRecipient** (strana "many" — drží cizí klíč):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Cyklické importy:** Obě relační pole odkazují na `universalIdentifier` toho druhého. Abyste předešli problémům s cyklickými importy, exportujte ID polí jako pojmenované konstanty z každého souboru a v druhém souboru je importujte. Build systém je vyřeší v době kompilace.
</Note>
#### Vazby na standardní objekty
Chcete-li vytvořit relaci s vestavěným objektem Twenty (Person, Company atd.), použijte `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Vlastnosti relačních polí
| Vlastnost | Povinné | Popis |
| ------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------- |
| `type` | Ano | Musí být `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Ano | `universalIdentifier` cílového objektu |
| `relationTargetFieldMetadataUniversalIdentifier` | Ano | `universalIdentifier` odpovídajícího pole na cílovém objektu |
| `universalSettings.relationType` | Ano | `RelationType.MANY_TO_ONE` nebo `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Pouze MANY_TO_ONE | Co se stane, když je smazán odkazovaný záznam: `CASCADE`, `SET_NULL`, `RESTRICT` nebo `NO_ACTION` |
| `universalSettings.joinColumnName` | Pouze MANY_TO_ONE | Název databázového sloupce pro cizí klíč (např. `postCardId`) |
#### Vložená relační pole v defineObject
Relační pole můžete také definovat přímo uvnitř `defineObject()`. V takovém případě vynechejte `objectUniversalIdentifier` — dědí se z nadřazeného objektu:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Konfigurace aplikace (application-config.ts)
Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
@@ -161,6 +354,22 @@ Poznámky:
* `defaultRoleUniversalIdentifier` se musí shodovat se souborem role (viz níže).
* Předinstalační a postinstalační funkce jsou při sestavování manifestu automaticky detekovány. Viz [Předinstalační funkce](#pre-install-functions) a [Postinstalační funkce](#post-install-functions).
#### Metadata Marketplace
Pokud plánujete [zveřejnit svou aplikaci](/l/cs/developers/extend/apps/publishing), tato volitelná pole určují, jak se vaše aplikace zobrazuje na Marketplace:
| Pole | Popis |
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
| `author` | Jméno autora nebo název společnosti |
| `category` | Kategorie aplikace pro filtrování na Marketplace |
| `logoUrl` | Cesta k logu vaší aplikace (relativně k `./assets/`) |
| `screenshots` | Pole cest ke snímkům obrazovky (relativně k `./assets/`) |
| `aboutDescription` | Delší popis v Markdownu pro kartu "O aplikaci". Pokud je vynecháno, tržiště použije `README.md` balíčku z npm |
| `websiteUrl` | Odkaz na váš web |
| `termsUrl` | Odkaz na Podmínky služby |
| `emailSupport` | E-mailová adresa podpory |
| `issueReportUrl` | Odkaz na nástroj pro sledování problémů |
#### Role a oprávnění
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `defaultRoleUniversalIdentifier` v `application-config.ts` určuje výchozí roli používanou logickými funkcemi vaší aplikace.
@@ -220,7 +429,7 @@ Na `universalIdentifier` této role se poté odkazuje v `application-config.ts`
Poznámky:
* Začněte rolí vytvořenou scaffolderem a postupně ji omezujte podle principu nejmenších oprávnění.
* Začněte vygenerovanou rolí a postupně ji omezujte podle principu nejmenších oprávnění.
* Nahraďte `objectPermissions` a `fieldPermissions` objekty/poli, která vaše funkce potřebují.
* `permissionFlags` řídí přístup k schopnostem na úrovni platformy. Držte je na minimu; přidávejte pouze to, co potřebujete.
* Podívejte se na funkční příklad v aplikaci Hello World: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
@@ -230,7 +439,7 @@ Poznámky:
Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Hlavní body:
@@ -334,7 +543,7 @@ Hlavní body:
* Na jednu aplikaci je povolena pouze jedna předinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `preInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší přípravné úlohy.
* Předinstalační funkce nepotřebují spouštěče — platforma je vyvolává před instalací nebo je lze spustit ručně pomocí `function:execute --preInstall`.
* Předinstalační funkce nepotřebují spouštěče — platforma je vyvolává před instalací nebo je lze spustit ručně pomocí `exec --preInstall`.
### Postinstalační funkce
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
Postinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Hlavní body:
@@ -372,7 +581,7 @@ Hlavní body:
* Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `postInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
* Postinstalační funkce nepotřebují spouštěče — jsou spouštěny platformou během instalace nebo ručně pomocí `function:execute --postInstall`.
* Postinstalační funkce nepotřebují spouštěče — jsou spouštěny platformou během instalace nebo ručně pomocí `exec --postInstall`.
### Payload spouštěče trasy
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
Typ `RoutePayload` má následující strukturu:
| Vlastnost | Typ | Popis |
| ---------------------------- | ------------------------------------- | --------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy (např. `/users/:id` `{ id: '123' }`) |
| `body` | `object \| null` | Parsované tělo požadavku (JSON) |
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku |
| Vlastnost | Typ | Popis |
| ---------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | Záhlaví HTTP (pouze ta uvedená v `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametry query stringu (více hodnot spojených čárkami) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametry cesty extrahované ze vzoru trasy (např. `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Parsované tělo požadavku (JSON) |
| `isBase64Encoded` | `boolean` | Zda je tělo kódováno base64 |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Nezpracovaná cesta požadavku |
### Přeposílání záhlaví HTTP
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
Nové funkce můžete vytvářet dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou logickou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
* **Vygenerované**: Spusťte `yarn twenty add` a zvolte možnost přidat novou logickou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
* **Ruční**: Vytvořte nový soubor `*.logic-function.ts` a použijte `defineLogicFunction()` podle stejného vzoru.
### Označení logické funkce jako nástroje
@@ -478,7 +687,7 @@ Chcete-li označit logickou funkci jako nástroj, nastavte `isTool: true` a posk
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ Hlavní body:
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou frontendovou komponentu.
* **Vygenerované**: Spusťte `yarn twenty add` a zvolte možnost přidat novou frontendovou komponentu.
* **Ruční**: Vytvořte nový soubor `.tsx` a použijte `defineFrontComponent()`, podle stejného vzoru.
### Dovednosti
@@ -602,47 +811,122 @@ Hlavní body:
Nové dovednosti můžete vytvářet dvěma způsoby:
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou dovednost.
* **Vygenerované**: Spusťte `yarn twenty add` a zvolte možnost přidat novou dovednost.
* **Ruční**: Vytvořte nový soubor a použijte `defineSkill()` podle stejného vzoru.
### Generované typované klienty
### Typovaní klienti API (`twenty-client-sdk`)
Dva typovaní klienti jsou automaticky generováni pomocí `yarn twenty dev` a ukládají se do `node_modules/twenty-sdk/generated` podle schématu vašeho pracovního prostoru:
Balíček `twenty-client-sdk` poskytuje dva typované klienty GraphQL pro práci s Twenty API z vašich logických funkcí a frontendových komponent:
* **`CoreApiClient`** — provádí dotazy na endpoint `/graphql` za účelem získání dat pracovního prostoru
* **`MetadataApiClient`** — odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru a nahrávání souborů
| Klient | Importovat | Koncový bod | Generováno? |
| ------------------- | ---------------------------- | ---------------------------------------------------------------- | ------------------------------ |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — data pracovního prostoru (záznamy, objekty) | Ano, při vývoji/sestavení |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — konfigurace pracovního prostoru, nahrávání souborů | Ne, dodává se předem sestavený |
#### CoreApiClient
`CoreApiClient` je hlavní klient pro dotazování a mutace dat pracovního prostoru. Generuje se z vašeho schématu pracovního prostoru během `yarn twenty dev` nebo `yarn twenty build`, takže je plně typovaný tak, aby odpovídal vašim objektům a polím.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Oba klienti se automaticky znovu generují pomocí `yarn twenty dev` kdykoli se změní vaše objekty nebo pole.
Klient používá syntaxi výběrové sady (selection-set): předáním `true` zahrnete pole, pro argumenty použijte `__args` a pro relace vnořujte objekty. Získáte plné automatické doplňování a kontrolu typů založené na schématu vašeho pracovního prostoru.
#### Běhové přihlašovací údaje v logických funkcích
<Note>
**CoreApiClient je generován při vývoji/sestavení.** Pokud se jej pokusíte použít bez předchozího spuštění `yarn twenty dev` nebo `yarn twenty build`, vyvolá chybu. Generování probíhá automaticky — CLI prozkoumá GraphQL schéma vašeho pracovního prostoru, vygeneruje typovaného klienta pomocí `@genql/cli`, zapíše vygenerované zdrojové soubory do `node_modules/twenty-client-sdk/dist/core/generated/` a nahradí zástupné soubory v `node_modules/twenty-client-sdk/dist/core.mjs` a `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
Když vaše funkce běží na Twenty, platforma před spuštěním kódu vloží přihlašovací údaje jako proměnné prostředí:
#### Použití CoreSchema pro anotace typů
* `TWENTY_API_URL`: Základní URL Twenty API, na které vaše aplikace cílí.
* `TWENTY_API_KEY`: Krátkodobý klíč s rozsahem omezeným na výchozí roli funkce vaší aplikace.
`CoreSchema` poskytuje typy TypeScriptu odpovídající objektům vašeho pracovního prostoru; je užitečný pro typování stavu komponent nebo parametrů funkcí:
Poznámky:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* Není nutné předávat URL ani klíč API vygenerovanému klientovi. Za běhu čte `TWENTY_API_URL` a `TWENTY_API_KEY` z process.env.
* Oprávnění klíče API jsou určena rolí odkazovanou ve vašem `application-config.ts` prostřednictvím `defaultRoleUniversalIdentifier`. Toto je výchozí role používaná logickými funkcemi vaší aplikace.
* Aplikace mohou definovat role podle principu nejmenších oprávnění. Udělte pouze oprávnění, která vaše funkce potřebují, a poté nastavte `defaultRoleUniversalIdentifier` na univerzální identifikátor této role.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` je součástí SDK již předem sestavený (není vyžadována žádná generace). Odesílá dotazy na endpoint `/metadata` pro konfiguraci pracovního prostoru, aplikace a nahrávání souborů:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Běhové přihlašovací údaje
Když váš kód běží na Twenty (logické funkce nebo frontendové komponenty), platforma vloží přihlašovací údaje jako proměnné prostředí:
* `TWENTY_API_URL` — Základní URL Twenty API
* `TWENTY_API_KEY` — Krátkodobý klíč s rozsahem omezeným na výchozí roli funkce vaší aplikace
Není nutné je předávat klientům — čtou je automaticky z `process.env`. Oprávnění API klíče jsou určena rolí uvedenou v `defaultRoleUniversalIdentifier` ve vašem `application-config.ts`.
#### Nahrávání souborů
Vygenerovaný `MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru u objektů ve vašem pracovním prostoru. Protože standardní klienti GraphQL nativně nepodporují nahrávání souborů pomocí multipart, klient poskytuje tuto speciální metodu, která interně implementuje [specifikaci multipart požadavků GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
`MetadataApiClient` obsahuje metodu `uploadFile` pro připojování souborů k polím typu souboru. Implementuje [specifikaci GraphQL multipart request](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
Signatura metody:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parametr | Typ | Popis |
| ---------------------------------- | -------- | --------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Surový obsah souboru |
@@ -680,8 +953,7 @@ uploadFile(
Hlavní body:
* Metoda `uploadFile` je k dispozici v `MetadataApiClient`, protože mutaci nahrávání obsluhuje endpoint `/metadata`.
* Používá `universalIdentifier` pole (nikoli jeho ID specifické pro pracovní prostor), takže váš kód pro nahrávání funguje ve všech pracovních prostorech, kde je vaše aplikace nainstalována — v souladu s tím, jak aplikace odkazují na pole všude jinde.
* Používá `universalIdentifier` pole (nikoli jeho ID specifické pro pracovní prostor), takže váš kód pro nahrávání funguje v jakémkoli pracovním prostoru, kde je vaše aplikace nainstalována.
* Vrácená hodnota `url` je podepsaná adresa URL, kterou můžete použít k přístupu k nahranému souboru.
### Příklad Hello World
@@ -9,18 +9,19 @@ Aplikace jsou aktuálně v alfa testování. Tato funkce je funkční, ale stál
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, AI schopnosti a komponenty uživatelského rozhraní — vše je spravováno jako kód.
**Co můžete dělat už dnes:**
**Co můžete vytvořit:**
* Definujte vlastní objekty a pole jako kód (spravovaný datový model)
* Vytvářejte logické funkce s vlastními spouštěči (HTTP routy, cron, databázové události)
* Definujte dovednosti agentů AI
* Vytvářejte frontendové komponenty, které se vykreslují uvnitř uživatelského rozhraní Twenty
* Nasazujte stejnou aplikaci do více pracovních prostorů
* Vlastní objekty, pole, zobrazení a položky navigace pro utváření vašeho datového modelu
* Logické funkce spouštěné trasami HTTP, plánovačem cron nebo událostmi databáze
* Frontendové komponenty, které se vykreslují přímo uvnitř uživatelského rozhraní Twenty
* Dovednosti, které rozšiřují možnosti AI agentů Twenty
* Nasazení aplikace napříč více pracovními prostory
## Předpoklady
* Node.js 24+ a Yarn 4
* Docker (pro místní vývojový server Twenty)
* Node.js 24+
* Yarn 4
* Docker (nebo běžící lokální instance Twenty)
## Začínáme
@@ -29,27 +30,15 @@ Vytvořte novou aplikaci pomocí oficiálního scaffolderu, poté se ověřte a
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
Nástroj pro generování kostry podporuje dva režimy pro řízení toho, které ukázkové soubory jsou zahrnuty:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Použijte volbu `--minimal` k vygenerování minimální instalace
Odtud můžete:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ S volbou `--minimal` se vytvoří pouze základní soubory (`application-config.
V kostce:
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skript `twenty`, který deleguje na lokální `twenty` CLI. Spusťte `yarn twenty help` pro výpis všech dostupných příkazů.
* **.gitignore**: Ignoruje běžné artefakty jako `node_modules`, `.yarn`, `generated/` (typovaný klient), `dist/`, `build/`, složky s coverage, logy a soubory `.env*`.
* **.gitignore**: Ignoruje běžné artefakty jako `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, složky s coverage, soubory s logy a soubory `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Zamykají a konfigurují nástrojový řetězec Yarn 4 používaný projektem.
* **.nvmrc**: Fixuje verzi Node.js požadovanou projektem.
* **.oxlintrc.json** a **tsconfig.json**: Poskytují lintování a konfiguraci TypeScriptu pro zdrojové soubory vaší aplikace v TypeScriptu.
@@ -167,8 +156,8 @@ export default defineObject({
Pozdější příkazy přidají další soubory a složky:
* `yarn twenty dev` automaticky vygeneruje dva typované API klienty v `node_modules/twenty-sdk/generated`: `CoreApiClient` (pro data pracovního prostoru přes `/graphql`) a `MetadataApiClient` (pro konfiguraci pracovního prostoru a nahrávání souborů přes `/metadata`).
* `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty, role, dovednosti a další.
* `yarn twenty dev` automaticky vygeneruje typovaný `CoreApiClient` (pro data pracovního prostoru přes `/graphql`) do `node_modules/twenty-client-sdk/`. `MetadataApiClient` (pro konfiguraci pracovního prostoru a nahrávání souborů přes `/metadata`) je dodáván předpřipravený a je okamžitě k dispozici. Importujte je z `twenty-client-sdk/core` a `twenty-client-sdk/metadata` v uvedeném pořadí.
* `yarn twenty add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty, role, dovednosti a další.
## Ověření
@@ -12,7 +12,25 @@ Aplikace jsou aktuálně v alfa testování. Tato funkce je funkční, ale stál
Jakmile je vaše aplikace [sestavena a otestována lokálně](/l/cs/developers/extend/apps/building), máte dvě cesty, jak ji distribuovat:
* **Publish to npm** — uveďte svou aplikaci v Marketplace Twenty, aby ji mohl kterýkoli pracovní prostor objevit a nainstalovat.
* **Odeslat tarball** — nasaďte svou aplikaci na konkrétní server Twenty pro interní použití, aniž by byla veřejně dostupná.
* **Nasaďte tarball** — nahrajte svou aplikaci přímo na konkrétní server Twenty pro interní nebo soukromé použití.
Obě cesty začínají stejným krokem **build**.
## Sestavení vaší aplikace
Příkaz `build` zkompiluje vaše zdrojové soubory TypeScriptu, transpiluje logické funkce a frontendové komponenty a vygeneruje soubor `manifest.json`, který popisuje obsah vaší aplikace:
```bash filename="Terminal"
yarn twenty build
```
Výstup se zapisuje do `.twenty/output/`. Tento adresář obsahuje vše potřebné pro distribuci: zkompilovaný kód, statické soubory, manifest a kopii souboru `package.json`.
Chcete-li také vytvořit tarball `.tgz` (používaný interně příkazem deploy nebo pro ruční distribuci):
```bash filename="Terminal"
yarn twenty build --tarball
```
## Publikování na npm
@@ -21,29 +39,76 @@ Publikování na npm zajistí, že bude vaše aplikace dohledatelná v Marketpla
### Požadavky
* Účet na [npm](https://www.npmjs.com)
* Název vašeho balíčku **musí** používat předponu `twenty-app-` (např. `twenty-app-postcard-sender`)
* Klíčové slovo `twenty-app` **musí** být uvedeno v poli `keywords` vašeho `package.json`
### Přidání požadovaného klíčového slova
Tržiště Twenty objevuje aplikace hledáním balíčků v registru npm s klíčovým slovem `twenty-app`. Přidejte jej do svého `package.json`:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
Tržiště vyhledává v registru npm výraz `keywords:twenty-app`. Bez tohoto klíčového slova se váš balíček v tržišti neobjeví, i když má jmennou předponu `twenty-app-`.
</Note>
### Postup
1. **Sestavte svou aplikaci** — CLI zkompiluje vaše zdrojové soubory TypeScript a vygeneruje manifest aplikace:
1. **Sestavení vaší aplikace:**
```bash filename="Terminal"
yarn twenty build
```
2. **Publikujte na npm** — odešlete sestavený balíček do registru npm:
2. **Publikování na npm:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Automatické rozpoznání
Tímto se spustí `npm publish` z adresáře `.twenty/output/`.
Balíčky s předponou `twenty-app-` jsou automaticky rozpoznávány katalogem Marketplace Twenty. Po publikování se vaše aplikace během několika minut objeví v Marketplace — nevyžaduje žádnou ruční registraci ani schvalování.
Chcete-li publikovat pod konkrétním dist-tagem (např. `beta` nebo `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### Jak funguje objevování v tržišti
Server Twenty synchronizuje svůj katalog tržiště z registru npm **každou hodinu**:
1. Vyhledá všechny balíčky na npm s klíčovým slovem `keywords:twenty-app`
2. Pro každý balíček stáhne `manifest.json` z CDN npm
3. Metadata aplikace (název, popis, autor, logo, snímky obrazovky, kategorie) se získají z manifestu a zobrazí se v tržišti
Po publikování se vaše aplikace může v tržišti objevit až za jednu hodinu. Chcete-li spustit synchronizaci okamžitě místo čekání na další hodinové spuštění:
```bash filename="Terminal"
yarn twenty catalog-sync
```
Chcete-li zacílit na konkrétní vzdálený cíl:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
Metadata zobrazená v tržišti pocházejí z volání `defineApplication()` ve zdrojovém kódu vaší aplikace — z polí jako `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` a `termsUrl`.
<Note>
Pokud vaše aplikace nedefinuje `aboutDescription` v `defineApplication()`, tržiště automaticky použije soubor `README.md` vašeho balíčku z npm jako obsah stránky O aplikaci. To znamená, že můžete spravovat jediný soubor README jak pro npm, tak pro tržiště Twenty. Pokud chcete v tržišti jiný popis, explicitně nastavte `aboutDescription`.
</Note>
### Publikování pomocí CI
Vygenerovaný projekt obsahuje pracovní postup GitHub Actions, který publikuje při každém vydání. Spouští `app:build`, a poté `npm publish --provenance` z výstupu buildu:
Vygenerovaný projekt obsahuje pracovní postup GitHub Actions, který publikuje při každém vydání:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Pro jiné systémy CI (GitLab CI, CircleCI atd.) platí stejné tři příkazy: `yarn install`, `npx twenty build` a poté `npm publish` z `.twenty/output`.
Pro jiné systémy CI (GitLab CI, CircleCI atd.) platí stejné tři příkazy: `yarn install`, `yarn twenty build` a poté `npm publish` z `.twenty/output`.
<Tip>
**npm provenance** je volitelné, ale doporučené. Publikování s `--provenance` přidá k vašemu záznamu na npm odznak důvěryhodnosti a umožní uživatelům ověřit, že balíček byl sestaven z konkrétního commitu ve veřejné CI pipeline. Pokyny k nastavení najdete v [dokumentaci k npm provenance](https://docs.npmjs.com/generating-provenance-statements).
</Tip>
## Interní distribuce
## Nasazení na server (tarball)
Pro aplikace, které nechcete zpřístupnit veřejně — proprietární nástroje, integrace pouze pro enterprise nebo experimentální buildy — můžete odeslat tarball přímo na server Twenty.
U aplikací, které nechcete zpřístupnit veřejně — proprietární nástroje, integrace pouze pro enterprise nebo experimentální buildy — můžete nasadit tarball přímo na server Twenty.
### Odeslat tarball
### Předpoklady
Sestavte svou aplikaci a nasaďte ji na konkrétní server v jednom kroku:
Před nasazením potřebujete nakonfigurovaný vzdálený cíl směřující na cílový server. Vzdálené cíle ukládají adresu URL serveru a přihlašovací údaje lokálně v `~/.twenty/config.json`.
Přidat vzdálený cíl:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Jakýkoli pracovní prostor na tomto serveru pak může aplikaci instalovat a aktualizovat ze stránky nastavení **Applications**.
Pro lokální vývojový server:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
Pro neinteraktivní prostředí se můžete ověřit také pomocí klíče API:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Spravujte své vzdálené servery:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Nasazení
Sestavte a nahrajte svou aplikaci na server v jednom kroku:
```bash filename="Terminal"
yarn twenty deploy
```
Tímto se aplikace sestaví s `--tarball` a poté se tarball nahraje na výchozí vzdálený server prostřednictvím vícedílného (multipart) nahrávání GraphQL.
Chcete-li nasadit na konkrétní vzdálený server:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Sdílení nasazené aplikace
Aplikace ve formě tarball nejsou uvedeny ve veřejném tržišti, takže je ostatní pracovní prostory na tomtéž serveru procházením neobjeví. Chcete-li sdílet nasazenou aplikaci:
1. Přejděte do **Nastavení > Aplikace > Registrace** a otevřete svou aplikaci
2. Na kartě **Distribuce** klikněte na **Zkopírovat odkaz ke sdílení**
3. Sdílejte tento odkaz s uživateli v jiných pracovních prostorech — zavede je přímo na instalační stránku aplikace
Odkaz ke sdílení používá základní adresu URL serveru (bez jakékoli subdomény pracovního prostoru), takže funguje pro libovolný pracovní prostor na serveru.
### Správa verzí
Chcete-li vydat aktualizaci:
1. Zvyšte hodnotu pole `version` v souboru `package.json`
2. Odešlete nový tarball pomocí `npx twenty publish --server <server-url>`
3. Pracovní prostory na tomto serveru uvidí dostupnou aktualizaci ve svém nastavení
2. Spusťte `yarn twenty deploy` (nebo `yarn twenty deploy -r production`)
3. Pracovní prostory, které mají aplikaci nainstalovanou, uvidí dostupnou aktualizaci ve svém nastavení
<Note>
Interní aplikace jsou omezené na server, na který jsou odeslány. Nezobrazí se ve veřejném Marketplace a nelze je instalovat v pracovních prostorech na jiných serverech.
</Note>
## Instalace aplikací
## Kategorie aplikací
Jakmile je aplikace publikována (npm) nebo nasazena (tarball), pracovní prostory ji instalují prostřednictvím uživatelského rozhraní:
```bash filename="Terminal"
yarn twenty install
```
Nebo ze stránky **Nastavení > Aplikace** v rozhraní Twenty, kde lze procházet a instalovat jak aplikace z tržiště, tak aplikace nasazené jako tarball.
## Kategorie distribuce aplikací
Twenty organizuje aplikace do tří kategorií podle způsobu distribuce:
| Kategorie | Jak to funguje | Viditelné v Marketplace? |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Vývoj** | Aplikace v místním vývojářském režimu spuštěné přes `yarn twenty dev`. Slouží k sestavování a testování. | Ne |
| **Publikováno** | Aplikace publikované na npm s předponou `twenty-app-`. Uvedeny v Marketplace, aby je mohl kterýkoli pracovní prostor nainstalovat. | Ano |
| **Interní** | Aplikace nasazené pomocí tarballu na konkrétní server. Dostupné pouze pro pracovní prostory na tomto serveru. | Ne |
| Kategorie | Jak to funguje | Viditelné v Marketplace? |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Vývoj** | Aplikace v místním vývojářském režimu spuštěné přes `yarn twenty dev`. Slouží k sestavování a testování. | Ne |
| **Publikováno (npm)** | Aplikace publikované na npm s klíčovým slovem `twenty-app`. Uvedeny v Marketplace, aby je mohl kterýkoli pracovní prostor nainstalovat. | Ano |
| **Interní (tarball)** | Aplikace nasazené pomocí tarballu na konkrétní server. Dostupné pouze pro pracovní prostory na tomto serveru prostřednictvím odkazu ke sdílení. | Ne |
<Tip>
Začněte v režimu **Development** při sestavování své aplikace. Až bude připravena, zvolte **Published** (npm) pro širokou distribuci nebo **Internal** (tarball) pro soukromé nasazení.
</Tip>
## Reference CLI
| Příkaz | Popis | Klíčové přepínače |
| --------------------------- | ----------------------------------------------------- | --------------------------------------------------- |
| `yarn twenty build` | Sestaví aplikaci a vygeneruje manifest | `--tarball` — také vytvoří balíček `.tgz` |
| `yarn twenty publish` | Sestaví a publikuje na npm | `--tag <tag>` — npm dist-tag (např. `beta`, `next`) |
| `yarn twenty deploy` | Sestaví a nahraje tarball na server | `-r, --remote <name>` — cílový vzdálený repozitář |
| `yarn twenty catalog-sync` | Spustí synchronizaci katalogu tržiště na serveru | `-r, --remote <name>` — cílový vzdálený repozitář |
| `yarn twenty install` | Nainstaluje nasazenou aplikaci do pracovního prostoru | `-r, --remote <name>` — cílový vzdálený server |
| `yarn twenty dev` | Sleduje a synchronizuje lokální změny | Používá výchozí vzdálený server |
| `yarn twenty remote add` | Přidá připojení k serveru | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | Vypíše nakonfigurované vzdálené servery | — |
| `yarn twenty remote switch` | Nastaví výchozí vzdálený server | — |
| `yarn twenty remote status` | Zobrazí stav připojení | — |
| `yarn twenty remote remove` | Odebere vzdálený server | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### Správa místního serveru
SDK obsahuje příkazy ke správě místního vývojového serveru Twenty (all-in-one obraz Dockeru s PostgreSQL, Redisem, serverem a workerem):
SDK obsahuje příkazy ke správě místního vývojového serveru Twenty (all-in-one obraz Dockeru s PostgreSQL, Redisem, serverem a workerem na portu 2020). Tyto příkazy se vztahují pouze na vývojový server založený na Dockeru — nespravují instanci Twenty spuštěnou ze zdrojového kódu (např. `npx nx start twenty-server` na portu 3000):
```bash filename="Terminal"
# Spusťte místní server (v případě potřeby stáhne obraz)
@@ -15,19 +15,21 @@ Das twenty-sdk stellt typisierte Bausteine und Hilfsfunktionen bereit, die Sie i
Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren. Wie in [Entitätserkennung](/l/de/developers/extend/apps/getting-started#entity-detection) beschrieben, müssen Sie `export default define<Entity>({...})` verwenden, damit Ihre Entitäten erkannt werden:
| Funktion | Zweck |
| -------------------------------- | --------------------------------------------------------------- |
| `defineApplication` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
| `defineObject` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineLogicFunction` | Logikfunktionen mit Handlern definieren |
| `definePreInstallLogicFunction` | Eine Pre-Installations-Logikfunktion definieren (eine pro App) |
| `definePostInstallLogicFunction` | Eine Post-Installations-Logikfunktion definieren (eine pro App) |
| `defineFrontComponent` | Frontend-Komponenten für benutzerdefinierte UI definieren |
| `defineRole` | Rollenberechtigungen und Objektzugriff konfigurieren |
| `defineField` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
| `defineView` | Gespeicherte Views für Objekte definieren |
| `defineNavigationMenuItem` | Seitenleisten-Navigationslinks definieren |
| `defineSkill` | Skills für KI-Agenten definieren |
| Funktion | Zweck |
| -------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `defineApplication` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
| `defineObject` | Benutzerdefinierte Objekte mit Feldern definieren |
| `defineField` | Erweitern Sie bestehende Objekte um zusätzliche Felder oder definieren Sie eigenständige Relationsfelder |
| `defineLogicFunction` | Logikfunktionen mit Handlern definieren |
| `definePreInstallLogicFunction` | Eine Pre-Installations-Logikfunktion definieren (eine pro App) |
| `definePostInstallLogicFunction` | Eine Post-Installations-Logikfunktion definieren (eine pro App) |
| `defineFrontComponent` | Frontend-Komponenten für benutzerdefinierte UI definieren |
| `defineRole` | Rollenberechtigungen und Objektzugriff konfigurieren |
| `defineView` | Gespeicherte Views für Objekte definieren |
| `defineNavigationMenuItem` | Seitenleisten-Navigationslinks definieren |
| `defineSkill` | Skills für KI-Agenten definieren |
| `defineAgent` | KI-Agenten definieren |
| `definePageLayout` | Benutzerdefinierte Seitenlayouts definieren |
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
@@ -36,7 +38,7 @@ Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Aut
Benutzerdefinierte Objekte beschreiben sowohl Schema als auch Verhalten für Datensätze in Ihrem Workspace. Verwenden Sie `defineObject()`, um Objekte mit eingebauter Validierung zu definieren:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ Hauptpunkte:
* Der `universalIdentifier` muss eindeutig und über Deployments hinweg stabil sein.
* Jedes Feld benötigt `name`, `type`, `label` und einen eigenen stabilen `universalIdentifier`.
* Das Array `fields` ist optional — Sie können Objekte ohne benutzerdefinierte Felder definieren.
* Sie können mit `yarn twenty entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
* Sie können mit `yarn twenty add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
<Note>
**Basisfelder werden automatisch erstellt.** Wenn Sie ein benutzerdefiniertes Objekt definieren, fügt Twenty automatisch Standardfelder hinzu
@@ -120,6 +122,197 @@ Sie können Standardfelder überschreiben, indem Sie in Ihrem `fields`-Array ein
dies wird jedoch nicht empfohlen.
</Note>
### Felder für bestehende Objekte definieren
Verwenden Sie `defineField()`, um Objekten, die Ihnen nicht gehören — etwa Standardobjekten von Twenty (Person, Company usw.) — Felder hinzuzufügen oder Objekten aus anderen Apps. Im Gegensatz zu Inline-Feldern in `defineObject()` benötigen eigenständige Felder einen `objectUniversalIdentifier`, um anzugeben, welches Objekt sie erweitern:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Hauptpunkte:
* Der `objectUniversalIdentifier` identifiziert das Zielobjekt. Für Standardobjekte verwenden Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`, die aus `twenty-sdk` exportiert werden.
* Wenn Sie Felder inline in `defineObject()` definieren, benötigen Sie `objectUniversalIdentifier` **nicht** — er wird vom übergeordneten Objekt geerbt.
* `defineField()` ist die einzige Möglichkeit, Felder zu Objekten hinzuzufügen, die Sie nicht mit `defineObject()` erstellt haben.
### Beziehungen
Relationen verbinden Objekte miteinander. In Twenty sind Relationen stets **bidirektional** — Sie definieren beide Seiten, und jede Seite referenziert die andere.
Es gibt zwei Relationstypen:
| Beziehungstyp | Beschreibung | Fremdschlüssel vorhanden? |
| ------------- | ----------------------------------------------------------------------- | ------------------------- |
| `MANY_TO_ONE` | Viele Datensätze dieses Objekts verweisen auf einen Datensatz des Ziels | Ja (`joinColumnName`) |
| `ONE_TO_MANY` | Ein Datensatz dieses Objekts hat viele Datensätze des Ziels | Nein (inverse Seite) |
#### Wie Relationen funktionieren
Jede Relation erfordert **zwei Felder**, die sich gegenseitig referenzieren:
1. Die **MANY_TO_ONE**-Seite — befindet sich auf dem Objekt, das den Fremdschlüssel hält
2. Die **ONE_TO_MANY**-Seite — befindet sich auf dem Objekt, dem die Sammlung gehört
Beide Felder verwenden `FieldType.RELATION` und verweisen über `relationTargetFieldMetadataUniversalIdentifier` gegenseitig aufeinander.
#### Beispiel: Postkarte hat viele Empfänger
Angenommen, eine `PostCard` kann an viele `PostCardRecipient`-Datensätze gesendet werden. Jeder Empfänger gehört genau zu einer Postkarte.
**Schritt 1: Definieren Sie die ONE_TO_MANY-Seite auf PostCard** (die "eine" Seite):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Schritt 2: Definieren Sie die MANY_TO_ONE-Seite auf PostCardRecipient** (die "viele" Seite — hält den Fremdschlüssel):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Zyklische Importe:** Beide Relationsfelder referenzieren gegenseitig den `universalIdentifier` des jeweils anderen. Um Probleme mit zyklischen Importen zu vermeiden, exportieren Sie Ihre Feld-IDs als benannte Konstanten aus jeder Datei und importieren Sie sie in der jeweils anderen Datei. Das Build-System löst dies zur Kompilierzeit auf.
</Note>
#### Relationen zu Standardobjekten
Um eine Relation mit einem integrierten Twenty-Objekt (Person, Company usw.) zu erstellen, verwenden Sie `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Eigenschaften von Relationsfeldern
| Eigenschaft | Erforderlich | Beschreibung |
| ------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `type` | Ja | Muss `FieldType.RELATION` sein |
| `relationTargetObjectMetadataUniversalIdentifier` | Ja | Der `universalIdentifier` des Zielobjekts |
| `relationTargetFieldMetadataUniversalIdentifier` | Ja | Der `universalIdentifier` des entsprechenden Felds auf dem Zielobjekt |
| `universalSettings.relationType` | Ja | `RelationType.MANY_TO_ONE` oder `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Nur für MANY_TO_ONE | Was passiert, wenn der referenzierte Datensatz gelöscht wird: `CASCADE`, `SET_NULL`, `RESTRICT` oder `NO_ACTION` |
| `universalSettings.joinColumnName` | Nur für MANY_TO_ONE | Datenbankspaltenname für den Fremdschlüssel (z. B. `postCardId`) |
#### Inline-Relationsfelder in defineObject
Sie können Relationsfelder auch direkt innerhalb von `defineObject()` definieren. In diesem Fall lassen Sie `objectUniversalIdentifier` weg — er wird vom übergeordneten Objekt geerbt:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Anwendungskonfiguration (application-config.ts)
Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschreibt:
@@ -161,6 +354,22 @@ Notizen:
* `defaultRoleUniversalIdentifier` muss mit der Rollendatei übereinstimmen (siehe unten).
* Pre-Installations- und Post-Installationsfunktionen werden während des Manifest-Builds automatisch erkannt. Siehe [Pre-Installationsfunktionen](#pre-install-functions) und [Post-Installationsfunktionen](#post-install-functions).
#### Marktplatz-Metadaten
Wenn Sie planen, [Ihre App zu veröffentlichen](/l/de/developers/extend/apps/publishing), steuern diese optionalen Felder, wie Ihre App im Marktplatz erscheint:
| Feld | Beschreibung |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Name des Autors oder des Unternehmens |
| `category` | App-Kategorie für die Filterung im Marktplatz |
| `logoUrl` | Pfad zu Ihrem App-Logo (relativ zu `./assets/`) |
| `screenshots` | Array von Screenshot-Pfaden (relativ zu `./assets/`) |
| `aboutDescription` | Längere Markdown-Beschreibung für den Tab "Info". Wenn weggelassen, verwendet der Marketplace die `README.md` des Pakets von npm |
| `websiteUrl` | Link zu Ihrer Website |
| `termsUrl` | Link zu den Nutzungsbedingungen |
| `emailSupport` | Support-E-Mail-Adresse |
| `issueReportUrl` | Link zum Issue-Tracker |
#### Rollen und Berechtigungen
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `defaultRoleUniversalIdentifier` in `application-config.ts` legt die Standardrolle fest, die von den Logikfunktionen Ihrer App verwendet wird.
@@ -230,7 +439,7 @@ Notizen:
Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
Sie können die Pre-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Hauptpunkte:
@@ -334,7 +543,7 @@ Hauptpunkte:
* Pro Anwendung ist nur eine Pre-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
* Der `universalIdentifier` der Funktion wird während des Builds im Anwendungsmanifest automatisch als `preInstallLogicFunctionUniversalIdentifier` gesetzt — Sie müssen ihn nicht in `defineApplication()` referenzieren.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Vorbereitungsvorgänge zu ermöglichen.
* Pre-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform vor der Installation oder manuell über `function:execute --preInstall` aufgerufen.
* Pre-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform vor der Installation oder manuell über `exec --preInstall` aufgerufen.
### Post-Installationsfunktionen
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
Sie können die Post-Installationsfunktion auch jederzeit manuell über die CLI ausführen:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Hauptpunkte:
@@ -372,7 +581,7 @@ Hauptpunkte:
* Pro Anwendung ist nur eine Post-Installationsfunktion zulässig. Der Manifest-Build schlägt fehl, wenn mehr als eine erkannt wird.
* Der `universalIdentifier` der Funktion wird während des Builds im Anwendungsmanifest automatisch als `postInstallLogicFunctionUniversalIdentifier` gesetzt — Sie müssen ihn nicht in `defineApplication()` referenzieren.
* Das standardmäßige Timeout ist auf 300 Sekunden (5 Minuten) festgelegt, um längere Einrichtungsvorgänge wie Daten-Seeding zu ermöglichen.
* Post-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform während der Installation oder manuell über `function:execute --postInstall` aufgerufen.
* Post-Installationsfunktionen benötigen keine Trigger — sie werden von der Plattform während der Installation oder manuell über `exec --postInstall` aufgerufen.
### Routen-Trigger-Payload
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
Der Typ `RoutePayload` hat die folgende Struktur:
| Eigenschaft | Typ | Beschreibung |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) |
| `pathParameters` | `Record<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter (z. B. `/users/:id` `{ id: '123' }`) |
| `body` | `object \| null` | Geparster Request-Body (JSON) |
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist |
| `requestContext.http.method` | `string` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Rohpfad der Anfrage |
| Eigenschaft | Typ | Beschreibung |
| ---------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP-Header (nur die in `forwardedRequestHeaders` aufgelisteten) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Query-String-Parameter (mehrere Werte mit Kommas verbunden) |
| `pathParameters` | `Record<string, string \| undefined>` | Aus dem Routenmuster extrahierte Pfadparameter (z. B. `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Geparster Request-Body (JSON) |
| `isBase64Encoded` | `boolean` | Gibt an, ob der Body Base64-codiert ist |
| `requestContext.http.method` | `string` | HTTP-Methode (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Rohpfad der Anfrage |
### Weiterleiten von HTTP-Headern
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
Sie können neue Funktionen auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Logikfunktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
* **Generiert**: Führen Sie `yarn twenty add` aus und wählen Sie die Option zum Hinzufügen einer neuen Logikfunktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
* **Manuell**: Erstellen Sie eine neue `*.logic-function.ts`-Datei und verwenden Sie `defineLogicFunction()` nach demselben Muster.
### Eine Logikfunktion als Tool markieren
@@ -478,7 +687,7 @@ Um eine Logikfunktion als Tool zu markieren, setzen Sie `isTool: true` und geben
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ Hauptpunkte:
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
* **Generiert**: Führen Sie `yarn twenty add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
* **Manuell**: Erstellen Sie eine neue `.tsx`-Datei und verwenden Sie `defineFrontComponent()` nach demselben Muster.
### Fähigkeiten
@@ -602,47 +811,122 @@ Hauptpunkte:
Sie können neue Skills auf zwei Arten erstellen:
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen eines neuen Skills.
* **Generiert**: Führen Sie `yarn twenty add` aus und wählen Sie die Option zum Hinzufügen eines neuen Skills.
* **Manuell**: Erstellen Sie eine neue Datei und verwenden Sie `defineSkill()` nach demselben Muster.
### Generierte typisierte Clients
### Typisierte API-Clients (`twenty-client-sdk`)
Zwei typisierte Clients werden von `yarn twenty dev` automatisch generiert und basierend auf Ihrem Arbeitsbereichs-Schema in `node_modules/twenty-sdk/generated` gespeichert:
Das Paket `twenty-client-sdk` stellt zwei typisierte GraphQL-Clients bereit, um aus Ihren Logikfunktionen und Frontend-Komponenten mit der Twenty-API zu interagieren:
* **`CoreApiClient`** — fragt den `/graphql`-Endpunkt nach Arbeitsbereichsdaten ab
* **`MetadataApiClient`** — ruft über den Endpunkt `/metadata` die Arbeitsbereichskonfiguration und Datei-Uploads ab.
| Client | Importieren | Endpunkt | Generiert? |
| ------------------- | ---------------------------- | --------------------------------------------------------- | ------------------------------------ |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — Arbeitsbereichsdaten (Datensätze, Objekte) | Ja, zur Entwicklungs-/Build-Zeit |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — Arbeitsbereichskonfiguration, Datei-Uploads | Nein, wird vorgefertigt ausgeliefert |
#### CoreApiClient
Der `CoreApiClient` ist der Haupt-Client zum Abfragen und Ändern von Arbeitsbereichsdaten. Er wird während `yarn twenty dev` oder `yarn twenty build` **aus Ihrem Arbeitsbereichsschema generiert** und ist daher vollständig typisiert, passend zu Ihren Objekten und Feldern.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Beide Clients werden von `yarn twenty dev` automatisch neu generiert, sobald sich Ihre Objekte oder Felder ändern.
Der Client verwendet eine Selection-Set-Syntax: Übergeben Sie `true`, um ein Feld einzuschließen, verwenden Sie `__args` für Argumente, und verschachteln Sie Objekte für Relationen. Sie erhalten vollständige Autovervollständigung und Typprüfung basierend auf Ihrem Arbeitsbereichsschema.
#### Laufzeit-Anmeldedaten in Logikfunktionen
<Note>
**Der CoreApiClient wird zur Entwicklungs-/Build-Zeit generiert.** Wenn Sie versuchen, ihn zu verwenden, ohne zuvor `yarn twenty dev` oder `yarn twenty build` ausgeführt zu haben, wird ein Fehler ausgelöst. Die Generierung erfolgt automatisch — die CLI inspiziert das GraphQL-Schema Ihres Arbeitsbereichs, erzeugt mit `@genql/cli` einen typisierten Client, schreibt die generierten Quellen nach `node_modules/twenty-client-sdk/dist/core/generated/` und ersetzt die Platzhalter in `node_modules/twenty-client-sdk/dist/core.mjs` und `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführung Ihres Codes Anmeldedaten als Umgebungsvariablen:
#### Verwendung von CoreSchema für Typannotationen
* `TWENTY_API_URL`: Basis-URL der Twenty-API, auf die Ihre App abzielt.
* `TWENTY_API_KEY`: Kurzlebiger Schlüssel, der auf die Standard-Funktionsrolle Ihrer Anwendung begrenzt ist.
`CoreSchema` stellt TypeScript-Typen bereit, die Ihren Arbeitsbereichsobjekten entsprechen; nützlich zum Typisieren von Komponentenzustand oder Funktionsparametern:
Notizen:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* Sie müssen dem generierten Client weder URL noch API-Schlüssel übergeben. Er liest `TWENTY_API_URL` und `TWENTY_API_KEY` zur Laufzeit aus process.env.
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application-config.ts` über `defaultRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den Logikfunktionen Ihrer Anwendung verwendet wird.
* Anwendungen können Rollen definieren, um das Least-Privilege-Prinzip einzuhalten. Gewähren Sie nur die Berechtigungen, die Ihre Funktionen benötigen, und verweisen Sie dann mit `defaultRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` ist im SDK bereits vorgefertigt enthalten (keine Generierung erforderlich). Er fragt den Endpunkt `/metadata` nach Arbeitsbereichskonfiguration, Anwendungen und Datei-Uploads ab:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Laufzeit-Anmeldedaten
Wenn Ihr Code auf Twenty ausgeführt wird (Logikfunktionen oder Frontend-Komponenten), injiziert die Plattform Anmeldedaten als Umgebungsvariablen:
* `TWENTY_API_URL` — Basis-URL der Twenty-API
* `TWENTY_API_KEY` — Kurzlebiger Schlüssel, der auf die Standard-Funktionsrolle Ihrer Anwendung begrenzt ist
Sie müssen diese **nicht** an die Clients übergeben — sie lesen automatisch aus `process.env`. Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in `defaultRoleUniversalIdentifier` in Ihrer `application-config.ts` verwiesen wird.
#### Dateien hochladen
Der generierte `MetadataApiClient` enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei in Ihren Arbeitsbereichsobjekten anzuhängen. Da Standard-GraphQL-Clients Multipart-Datei-Uploads nicht nativ unterstützen, stellt der Client diese dedizierte Methode bereit, die unter der Haube die [GraphQL-Multipart-Anfragespezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec) implementiert.
Der `MetadataApiClient` enthält eine Methode `uploadFile`, um Dateien an Felder des Typs Datei anzuhängen. Sie implementiert die [GraphQL-Multipart-Request-Spezifikation](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
Die Methodensignatur:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parameter | Typ | Beschreibung |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Der Rohinhalt der Datei |
@@ -680,8 +953,7 @@ uploadFile(
Hauptpunkte:
* Die Methode `uploadFile` ist auf dem `MetadataApiClient` verfügbar, weil die Upload-Mutation vom Endpunkt `/metadata` aufgelöst wird.
* Sie verwendet den `universalIdentifier` des Feldes (nicht dessen arbeitsbereichsspezifische ID), sodass Ihr Upload-Code in jedem Arbeitsbereich funktioniert, in dem Ihre App installiert ist — im Einklang damit, wie Apps Felder überall sonst referenzieren.
* Sie verwendet den `universalIdentifier` des Feldes (nicht dessen arbeitsbereichsspezifische ID), sodass Ihr Upload-Code in jedem Arbeitsbereich funktioniert, in dem Ihre App installiert ist.
* Die zurückgegebene `url` ist eine signierte URL, mit der Sie auf die hochgeladene Datei zugreifen können.
### Hello-World-Beispiel
@@ -9,18 +9,19 @@ Apps befinden sich derzeit in der Alpha-Testphase. Die Funktion ist funktionsfä
Apps ermöglichen es Ihnen, Twenty mit benutzerdefinierten Objekten, Feldern, Logikfunktionen, KI-Fähigkeiten und UI-Komponenten zu erweitern — alles als Code verwaltet.
**Was Sie heute tun können:**
**Was Sie erstellen können:**
* Benutzerdefinierte Objekte und Felder als Code definieren (verwaltetes Datenmodell)
* Erstellen Sie Logikfunktionen mit benutzerdefinierten Triggern (HTTP-Routen, cron, Datenbankereignisse)
* Fähigkeiten für KI-Agenten definieren
* Erstellen Sie Frontend-Komponenten, die in der Twenty-UI gerendert werden
* Dieselbe App in mehreren Workspaces bereitstellen
* Benutzerdefinierte Objekte, Felder, Ansichten und Navigationselemente, um Ihr Datenmodell zu gestalten
* Logikfunktionen, die durch HTTP-Routen, Cron-Zeitpläne oder Datenbankereignisse ausgelöst werden
* Frontend-Komponenten, die direkt innerhalb der Twenty-UI gerendert werden
* Skills, die die KI-Agenten von Twenty erweitern
* Eine App in mehreren Workspaces bereitstellen
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Docker (für den lokalen Twenty-Dev-Server)
* Node.js 24+
* Yarn 4
* Docker (oder eine lokal laufende Twenty-Instanz)
## Erste Schritte
@@ -29,27 +30,15 @@ Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Verwenden Sie die Option `--minimal`, um eine minimale Installation zu erstellen
Von hier aus können Sie:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ Mit `--minimal` werden nur die Kerndateien erstellt (`application-config.ts`, `r
Auf hoher Ebene:
* **package.json**: Deklariert den App-Namen, die Version und die Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie ein `twenty`-Skript hinzu, das an die lokale `twenty`-CLI delegiert. Führen Sie `yarn twenty help` aus, um alle verfügbaren Befehle aufzulisten.
* **.gitignore**: Ignoriert übliche Artefakte wie `node_modules`, `.yarn`, `generated/` (typisierter Client), `dist/`, `build/`, Coverage-Ordner, Logdateien und `.env*`-Dateien.
* **.gitignore**: Ignoriert übliche Artefakte wie `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, Coverage-Ordner, Logdateien und `.env*`-Dateien.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Fixieren und konfigurieren die vom Projekt verwendete Yarn-4-Toolchain.
* **.nvmrc**: Legt die vom Projekt erwartete Node.js-Version fest.
* **.oxlintrc.json** und **tsconfig.json**: Stellen Linting und TypeScript-Konfiguration für die TypeScript-Quellen Ihrer App bereit.
@@ -167,8 +156,8 @@ export default defineObject({
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
* `yarn twenty dev` generiert automatisch zwei typisierte API-Clients in `node_modules/twenty-sdk/generated`: `CoreApiClient` (für Arbeitsbereichsdaten über `/graphql`) und `MetadataApiClient` (für Arbeitsbereichskonfiguration und Datei-Uploads über `/metadata`).
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
* `yarn twenty dev` generiert den typisierten `CoreApiClient` (für Arbeitsbereichsdaten über `/graphql`) automatisch in `node_modules/twenty-client-sdk/`. Der `MetadataApiClient` (für Arbeitsbereichskonfiguration und Datei-Uploads über `/metadata`) wird vorkompiliert ausgeliefert und ist sofort verfügbar. Importieren Sie sie jeweils aus `twenty-client-sdk/core` und `twenty-client-sdk/metadata`.
* `yarn twenty add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Frontend-Komponenten, Rollen, Skills und mehr hinzu.
## Authentifizierung
@@ -12,7 +12,25 @@ Apps befinden sich derzeit in der Alpha-Testphase. Die Funktion ist funktionsfä
Sobald Ihre App [lokal gebaut und getestet](/l/de/developers/extend/apps/building) wurde, haben Sie zwei Möglichkeiten, sie zu verteilen:
* **Auf npm veröffentlichen** — führen Sie Ihre App im Twenty-Marktplatz auf, damit jeder Arbeitsbereich sie entdecken und installieren kann.
* **Einen Tarball pushen** — stellen Sie Ihre App auf einem bestimmten Twenty-Server für die interne Nutzung bereit, ohne sie öffentlich verfügbar zu machen.
* **Einen Tarball bereitstellen** — Laden Sie Ihre App direkt auf einen bestimmten Twenty-Server für die interne oder private Nutzung hoch.
Beide Pfade beginnen mit demselben **Build**-Schritt.
## Erstellen Ihrer App
Der Befehl `build` kompiliert Ihre TypeScript-Quelltexte, transpiliert Logikfunktionen und Frontend-Komponenten und erzeugt eine `manifest.json`, die die Inhalte Ihrer App beschreibt:
```bash filename="Terminal"
yarn twenty build
```
Die Ausgabe wird in `.twenty/output/` geschrieben. Dieses Verzeichnis enthält alles, was für die Verteilung benötigt wird: kompilierter Code, Assets, das Manifest und eine Kopie Ihrer `package.json`.
Um zusätzlich ein `.tgz`-Tarball zu erstellen (wird intern vom Deploy-Befehl verwendet oder für die manuelle Verteilung):
```bash filename="Terminal"
yarn twenty build --tarball
```
## Auf npm veröffentlichen
@@ -21,29 +39,76 @@ Die Veröffentlichung auf npm macht Ihre App im Twenty-Marktplatz auffindbar. Je
### Anforderungen
* Ein [npm](https://www.npmjs.com)-Konto
* Ihr Paketname **muss** das Präfix `twenty-app-` verwenden (z. B. `twenty-app-postcard-sender`)
* Das Schlüsselwort `twenty-app` **muss** in Ihrem `package.json`-`keywords`-Array aufgeführt sein
### Das erforderliche Schlüsselwort hinzufügen
Der Twenty-Marktplatz entdeckt Apps, indem er die npm-Registry nach Paketen mit dem Schlüsselwort `twenty-app` durchsucht. Fügen Sie es zu Ihrer `package.json` hinzu:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
Der Marktplatz sucht in der npm-Registry nach `keywords:twenty-app`. Ohne dieses Schlüsselwort erscheint Ihr Paket nicht im Marktplatz, selbst wenn es das Namenspräfix `twenty-app-` hat.
</Note>
### Schritte
1. **App erstellen** — die CLI kompiliert Ihre TypeScript-Quellen und erzeugt das Anwendungsmanifest:
1. **Erstellen Ihrer App:**
```bash filename="Terminal"
yarn twenty build
```
2. **Auf npm veröffentlichen** — pushen Sie das gebaute Paket in die npm-Registry:
2. **Auf npm veröffentlichen:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Automatische Erkennung
Dies führt `npm publish` aus dem Verzeichnis `.twenty/output/` aus.
Pakete mit dem Präfix `twenty-app-` werden vom Twenty-Marktplatzkatalog automatisch erkannt. Nach der Veröffentlichung erscheint Ihre App innerhalb weniger Minuten im Marktplatz — keine manuelle Registrierung oder Genehmigung erforderlich.
Um unter einem bestimmten dist-tag zu veröffentlichen (z. B. `beta` oder `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### So funktioniert die Marktplatz-Erkennung
Der Twenty-Server synchronisiert seinen Marktplatzkatalog **stündlich** mit der npm-Registry:
1. Er sucht nach allen npm-Paketen mit dem Schlüsselwort `keywords:twenty-app`
2. Für jedes Paket ruft er die `manifest.json` vom npm-CDN ab
3. Die Metadaten der App (Name, Beschreibung, Autor, Logo, Screenshots, Kategorie) werden aus dem Manifest extrahiert und im Marktplatz angezeigt
Nach der Veröffentlichung kann es bis zu einer Stunde dauern, bis Ihre App im Marktplatz erscheint. Um die Synchronisierung sofort auszulösen, statt auf den nächsten stündlichen Lauf zu warten:
```bash filename="Terminal"
yarn twenty catalog-sync
```
Um ein bestimmtes Remote anzusteuern:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
Die im Marktplatz angezeigten Metadaten stammen aus Ihrem `defineApplication()`-Aufruf im Quellcode Ihrer App — Felder wie `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` und `termsUrl`.
<Note>
Wenn deine App keine `aboutDescription` in `defineApplication()` definiert, verwendet der Marktplatz automatisch die `README.md` deines Pakets von npm als Inhalt der Über-uns-Seite. Das bedeutet, dass du eine einzige README sowohl für npm als auch für den Twenty-Marktplatz pflegen kannst. Wenn du im Marktplatz eine andere Beschreibung möchtest, setze `aboutDescription` explizit.
</Note>
### CI-Veröffentlichung
Das vorgefertigte Projekt enthält einen GitHub-Actions-Workflow, der bei jedem Release eine Veröffentlichung durchführt. Er führt `app:build` aus und danach `npm publish --provenance` aus dem Build-Output:
Das vorgefertigte Projekt enthält einen GitHub-Actions-Workflow, der bei jedem Release eine Veröffentlichung durchführt:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Für andere CI-Systeme (GitLab CI, CircleCI usw.) gelten die gleichen drei Befehle: `yarn install`, `npx twenty build` und anschließend `npm publish` aus `.twenty/output`.
Für andere CI-Systeme (GitLab CI, CircleCI usw.) gelten die gleichen drei Befehle: `yarn install`, `yarn twenty build` und anschließend `npm publish` aus `.twenty/output`.
<Tip>
**npm-Provenance** ist optional, wird jedoch empfohlen. Das Veröffentlichen mit `--provenance` fügt Ihrem npm-Eintrag ein Vertrauensabzeichen hinzu, sodass Nutzer überprüfen können, dass das Paket aus einem bestimmten Commit in einer öffentlichen CI-Pipeline gebaut wurde. Siehe die [npm-Provenance-Dokumentation](https://docs.npmjs.com/generating-provenance-statements) für Einrichtungshinweise.
</Tip>
## Interne Verteilung
## Bereitstellung auf einem Server (Tarball)
Für Apps, die Sie nicht öffentlich verfügbar machen möchten — proprietäre Tools, nur für Unternehmen bestimmte Integrationen oder experimentelle Builds — können Sie einen Tarball direkt auf einen Twenty-Server pushen.
Für Apps, die Sie nicht öffentlich verfügbar machen möchten — proprietäre Tools, ausschließlich für Unternehmen bestimmte Integrationen oder experimentelle Builds — können Sie einen Tarball direkt auf einem Twenty-Server bereitstellen.
### Einen Tarball pushen
### Voraussetzungen
Erstellen Sie Ihre App und stellen Sie sie in einem Schritt auf einem bestimmten Server bereit:
Bevor Sie bereitstellen, benötigen Sie ein konfiguriertes Remote, das auf den Zielserver zeigt. Remotes speichern die Server-URL und Anmeldeinformationen lokal in `~/.twenty/config.json`.
Ein Remote hinzufügen:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Jeder Arbeitsbereich auf diesem Server kann die App anschließend über die Seite **Applications** in den Einstellungen installieren und aktualisieren.
Für einen lokalen Entwicklungsserver:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
Sie können sich in nicht interaktiven Umgebungen auch mit einem API-Schlüssel authentifizieren:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Ihre Remotes verwalten:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Bereitstellen
Bauen und laden Sie Ihre App in einem Schritt auf den Server hoch:
```bash filename="Terminal"
yarn twenty deploy
```
Dies baut die App mit `--tarball` und lädt anschließend den Tarball per GraphQL-Multipart-Upload auf das Standard-Remote hoch.
Um auf ein bestimmtes Remote bereitzustellen:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Eine bereitgestellte App freigeben
Tarball-Apps werden nicht im öffentlichen Marktplatz gelistet, daher entdecken andere Arbeitsbereiche auf demselben Server sie nicht durch Stöbern. So geben Sie eine bereitgestellte App frei:
1. Gehen Sie zu **Einstellungen > Anwendungen > Registrierungen** und öffnen Sie Ihre App
2. Klicken Sie im Tab **Distribution** auf **Freigabelink kopieren**
3. Teilen Sie diesen Link mit Nutzern in anderen Arbeitsbereichen — er führt sie direkt zur Installationsseite der App
Der Freigabelink verwendet die Basis-URL des Servers (ohne Workspace-Subdomain), sodass er für jeden Arbeitsbereich auf dem Server funktioniert.
### Versionsverwaltung
So veröffentlichen Sie ein Update:
1. Erhöhen Sie das Feld `version` in Ihrer `package.json`
2. Pushen Sie einen neuen Tarball mit `npx twenty publish --server <server-url>`
3. Arbeitsbereiche auf diesem Server sehen in ihren Einstellungen, dass ein Upgrade verfügbar ist.
2. Führen Sie `yarn twenty deploy` aus (oder `yarn twenty deploy -r production`)
3. Arbeitsbereiche, die die App installiert haben, sehen in ihren Einstellungen, dass ein Upgrade verfügbar ist.
<Note>
Interne Apps sind auf den Server beschränkt, auf den sie gepusht werden. Sie erscheinen nicht im öffentlichen Marktplatz und können von Arbeitsbereichen auf anderen Servern nicht installiert werden.
</Note>
## Apps installieren
## App-Kategorien
Sobald eine App veröffentlicht (npm) oder bereitgestellt (Tarball) wurde, installieren Arbeitsbereiche sie über die Benutzeroberfläche:
```bash filename="Terminal"
yarn twenty install
```
Oder über die Seite **Einstellungen > Anwendungen** in der Twenty-Oberfläche, wo sowohl Marktplatz- als auch per Tarball bereitgestellte Apps durchsucht und installiert werden können.
## Kategorien der App-Verteilung
Twenty organisiert Apps in drei Kategorien, basierend auf ihrer Vertriebsart:
| Kategorie | Wie es funktioniert | Im Marktplatz sichtbar? |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
| **Entwicklung** | Lokale Apps im Entwicklungsmodus, die über `yarn twenty dev` ausgeführt werden. Zum Erstellen und Testen verwendet. | Nein |
| **Veröffentlicht** | Auf npm veröffentlichte Apps mit dem Präfix `twenty-app-`. Im Marktplatz gelistet, damit jeder Arbeitsbereich sie installieren kann. | Ja |
| **Intern** | Apps, die per Tarball auf einen bestimmten Server bereitgestellt werden. Nur für Arbeitsbereiche auf diesem Server verfügbar. | Nein |
| Kategorie | Wie es funktioniert | Im Marktplatz sichtbar? |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| **Entwicklung** | Lokale Apps im Entwicklungsmodus, die über `yarn twenty dev` ausgeführt werden. Zum Erstellen und Testen verwendet. | Nein |
| **Veröffentlicht (npm)** | Auf npm veröffentlichte Apps mit dem Schlüsselwort `twenty-app`. Im Marktplatz gelistet, damit jeder Arbeitsbereich sie installieren kann. | Ja |
| **Intern (Tarball)** | Apps, die per Tarball auf einen bestimmten Server bereitgestellt werden. Nur für Arbeitsbereiche auf diesem Server per Freigabelink verfügbar. | Nein |
<Tip>
Beginnen Sie im **Entwicklungsmodus**, während Sie Ihre App erstellen. Wenn sie bereit ist, wählen Sie **Veröffentlicht** (npm) für die breite Verteilung oder **Intern** (Tarball) für die private Bereitstellung.
</Tip>
## CLI-Referenz
| Befehl | Beschreibung | Wichtige Flags |
| --------------------------- | --------------------------------------------------------------- | --------------------------------------------------- |
| `yarn twenty build` | App kompilieren und Manifest erzeugen | `--tarball` — zusätzlich ein `.tgz`-Paket erstellen |
| `yarn twenty publish` | Bauen und auf npm veröffentlichen | `--tag <tag>` — npm-dist-tag (z. B. `beta`, `next`) |
| `yarn twenty deploy` | Tarball bauen und auf einen Server hochladen | `-r, --remote <name>` — Ziel-Remote |
| `yarn twenty catalog-sync` | Synchronisierung des Marktplatzkatalogs auf dem Server auslösen | `-r, --remote <name>` — Ziel-Remote |
| `yarn twenty install` | Eine bereitgestellte App in einem Arbeitsbereich installieren | `-r, --remote <name>` — Ziel-Remote |
| `yarn twenty dev` | Lokale Änderungen beobachten und synchronisieren | Verwendet das Standard-Remote |
| `yarn twenty remote add` | Eine Serververbindung hinzufügen | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | Konfigurierte Remotes auflisten | — |
| `yarn twenty remote switch` | Standard-Remote festlegen | — |
| `yarn twenty remote status` | Verbindungsstatus anzeigen | — |
| `yarn twenty remote remove` | Ein Remote entfernen | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### Lokale Serververwaltung
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker auf Port 2020). Diese Befehle gelten nur für den Docker-basierten Dev-Server — sie verwalten keine aus dem Quellcode gestartete Twenty-Instanz (z. B. `npx nx start twenty-server` auf Port 3000):
```bash filename="Terminal"
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
@@ -15,19 +15,21 @@ Il pacchetto twenty-sdk fornisce blocchi tipizzati e funzioni helper da usare ne
L'SDK fornisce funzioni helper per definire le entità della tua app. Come descritto in [Rilevamento delle entità](/l/it/developers/extend/apps/getting-started#entity-detection), devi usare `export default define<Entity>({...})` affinché le tue entità vengano rilevate:
| Funzione | Scopo |
| -------------------------------- | ----------------------------------------------------------------------- |
| `defineApplication` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
| `defineObject` | Definisci oggetti personalizzati con campi |
| `defineLogicFunction` | Definisci funzioni logiche con handler |
| `definePreInstallLogicFunction` | Definisci una funzione logica di pre-installazione (una per app) |
| `definePostInstallLogicFunction` | Definisci una funzione logica di post-installazione (una per app) |
| `defineFrontComponent` | Definisci componenti front-end per un'interfaccia utente personalizzata |
| `defineRole` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| `defineField` | Estendi gli oggetti esistenti con campi aggiuntivi |
| `defineView` | Definisci viste salvate per gli oggetti |
| `defineNavigationMenuItem` | Definisci i link di navigazione della barra laterale |
| `defineSkill` | Definisci le competenze dell'agente IA |
| Funzione | Scopo |
| -------------------------------- | ----------------------------------------------------------------------------------------------- |
| `defineApplication` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
| `defineObject` | Definisci oggetti personalizzati con campi |
| `defineField` | Estendi gli oggetti esistenti con campi aggiuntivi oppure definisci campi di relazione autonomi |
| `defineLogicFunction` | Definisci funzioni logiche con handler |
| `definePreInstallLogicFunction` | Definisci una funzione logica di pre-installazione (una per app) |
| `definePostInstallLogicFunction` | Definisci una funzione logica di post-installazione (una per app) |
| `defineFrontComponent` | Definisci componenti front-end per un'interfaccia utente personalizzata |
| `defineRole` | Configura i permessi dei ruoli e l'accesso agli oggetti |
| `defineView` | Definisci viste salvate per gli oggetti |
| `defineNavigationMenuItem` | Definisci i link di navigazione della barra laterale |
| `defineSkill` | Definisci le competenze dell'agente IA |
| `defineAgent` | Definisci gli agenti IA |
| `definePageLayout` | Definisci layout di pagina personalizzati |
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
@@ -36,7 +38,7 @@ Queste funzioni convalidano la configurazione in fase di build e offrono il comp
Gli oggetti personalizzati descrivono sia lo schema sia il comportamento per i record nel tuo spazio di lavoro. Usa `defineObject()` per definire oggetti con convalida integrata:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ Punti chiave:
* Il `universalIdentifier` deve essere univoco e stabile tra i deployment.
* Ogni campo richiede un `name`, `type`, `label` e il proprio `universalIdentifier` stabile.
* L'array `fields` è facoltativo: puoi definire oggetti senza campi personalizzati.
* Puoi generare nuovi oggetti con `yarn twenty entity:add`, che ti guida nella denominazione, nei campi e nelle relazioni.
* Puoi generare nuovi oggetti con `yarn twenty add`, che ti guida nella denominazione, nei campi e nelle relazioni.
<Note>
**I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard
@@ -120,6 +122,197 @@ Puoi sovrascrivere i campi predefiniti definendo un campo con lo stesso nome nel
ma non è consigliato.
</Note>
### Definire campi sugli oggetti esistenti
Usa `defineField()` per aggiungere campi a oggetti che non possiedi — come gli oggetti standard di Twenty (Person, Company, ecc.) o oggetti di altre app. A differenza dei campi inline in `defineObject()`, i campi autonomi richiedono un `objectUniversalIdentifier` per specificare quale oggetto estendono:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Punti chiave:
* `objectUniversalIdentifier` identifica l'oggetto di destinazione. Per gli oggetti standard, usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` esportati da `twenty-sdk`.
* Quando definisci campi inline in `defineObject()`, **non** hai bisogno di `objectUniversalIdentifier` — viene ereditato dall'oggetto padre.
* `defineField()` è l'unico modo per aggiungere campi a oggetti che non hai creato con `defineObject()`.
### Relazioni
Le relazioni collegano gli oggetti tra loro. In Twenty, le relazioni sono sempre **bidirezionali** — definisci entrambi i lati e ciascun lato fa riferimento all'altro.
Esistono due tipi di relazione:
| Tipo di relazione | Descrizione | Ha una chiave esterna? |
| ----------------- | --------------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Molti record di questo oggetto puntano a un record della destinazione | Sì (`joinColumnName`) |
| `ONE_TO_MANY` | Un record di questo oggetto ha molti record della destinazione | No (lato inverso) |
#### Come funzionano le relazioni
Ogni relazione richiede **due campi** che fanno riferimento l'uno all'altro:
1. Il lato **MANY_TO_ONE** — risiede sull'oggetto che detiene la chiave esterna
2. Il lato **ONE_TO_MANY** — risiede sull'oggetto che possiede la collezione
Entrambi i campi usano `FieldType.RELATION` e si riferiscono reciprocamente tramite `relationTargetFieldMetadataUniversalIdentifier`.
#### Esempio: Post Card ha molti destinatari
Supponiamo che un `PostCard` possa essere inviato a molti record `PostCardRecipient`. Ogni destinatario appartiene esattamente a una sola cartolina.
**Passaggio 1: definisci il lato ONE_TO_MANY su PostCard** (il lato "uno"):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Passaggio 2: definisci il lato MANY_TO_ONE su PostCardRecipient** (il lato "molti" — contiene la chiave esterna):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Importazioni circolari:** Entrambi i campi di relazione fanno riferimento all'`universalIdentifier` dell'altro. Per evitare problemi di importazioni circolari, esporta gli ID dei campi come costanti denominate da ciascun file e importale nell'altro file. Il sistema di build le risolve in fase di compilazione.
</Note>
#### Relazioni con gli oggetti standard
Per creare una relazione con un oggetto Twenty integrato (Person, Company, ecc.), usa `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Proprietà dei campi di relazione
| Proprietà | Obbligatorio | Descrizione |
| ------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `tipo` | Sì | Deve essere `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` dell'oggetto di destinazione |
| `relationTargetFieldMetadataUniversalIdentifier` | Sì | L'`universalIdentifier` del campo corrispondente sull'oggetto di destinazione |
| `universalSettings.relationType` | Sì | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Solo MANY_TO_ONE | Cosa accade quando il record referenziato viene eliminato: `CASCADE`, `SET_NULL`, `RESTRICT` o `NO_ACTION` |
| `universalSettings.joinColumnName` | Solo MANY_TO_ONE | Nome della colonna del database per la chiave esterna (ad es., `postCardId`) |
#### Campi di relazione inline in defineObject
Puoi anche definire i campi di relazione direttamente all'interno di `defineObject()`. In tal caso, ometti `objectUniversalIdentifier` — viene ereditato dall'oggetto padre:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Configurazione dell'applicazione (application-config.ts)
Ogni app ha un singolo file `application-config.ts` che descrive:
@@ -161,6 +354,22 @@ Note:
* `defaultRoleUniversalIdentifier` deve corrispondere al file del ruolo (vedi sotto).
* Le funzioni di pre-installazione e post-installazione vengono rilevate automaticamente durante la build del manifesto. Vedi [Funzioni di pre-installazione](#pre-install-functions) e [Funzioni di post-installazione](#post-install-functions).
#### Metadati del marketplace
Se prevedi di [pubblicare la tua app](/l/it/developers/extend/apps/publishing), questi campi opzionali controllano come la tua app appare nel marketplace:
| Campo | Descrizione |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `autore` | Nome dell'autore o dell'azienda |
| `categoria` | Categoria dell'app per il filtraggio nel marketplace |
| `logoUrl` | Percorso del logo della tua app (relativo a `./assets/`) |
| `screenshots` | Array di percorsi degli screenshot (relativi a `./assets/`) |
| `aboutDescription` | Descrizione markdown più lunga per la scheda "Informazioni". Se omesso, il marketplace utilizza il `README.md` del pacchetto da npm |
| `websiteUrl` | Link al tuo sito web |
| `termsUrl` | Link ai Termini di servizio |
| `emailSupport` | Indirizzo email di supporto |
| `issueReportUrl` | Link al sistema di tracciamento dei problemi |
#### Ruoli e permessi
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `defaultRoleUniversalIdentifier` in `application-config.ts` indica il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
@@ -230,7 +439,7 @@ Note:
Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazione con un handler e trigger opzionali.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
Puoi anche eseguire manualmente la funzione di pre-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Punti chiave:
@@ -334,7 +543,7 @@ Punti chiave:
* È consentita una sola funzione di pre-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `preInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di preparazione più lunghe.
* Le funzioni di pre-installazione non necessitano di trigger — vengono invocate dalla piattaforma prima dell'installazione o manualmente tramite `function:execute --preInstall`.
* Le funzioni di pre-installazione non necessitano di trigger — vengono invocate dalla piattaforma prima dell'installazione o manualmente tramite `exec --preInstall`.
### Funzioni post-installazione
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
Puoi anche eseguire manualmente la funzione di post-installazione in qualsiasi momento utilizzando la CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Punti chiave:
@@ -372,7 +581,7 @@ Punti chiave:
* È consentita una sola funzione di post-installazione per applicazione. La build del manifesto genererà un errore se ne viene rilevata più di una.
* L'`universalIdentifier` della funzione viene impostato automaticamente come `postInstallLogicFunctionUniversalIdentifier` nel manifesto dell'applicazione durante la build — non è necessario farvi riferimento in `defineApplication()`.
* Il timeout predefinito è impostato a 300 secondi (5 minuti) per consentire attività di configurazione più lunghe, come il popolamento dei dati.
* Le funzioni di post-installazione non necessitano di trigger — vengono invocate dalla piattaforma durante l'installazione o manualmente tramite `function:execute --postInstall`.
* Le funzioni di post-installazione non necessitano di trigger — vengono invocate dalla piattaforma durante l'installazione o manualmente tramite `exec --postInstall`.
### Payload del trigger di route
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
Il tipo `RoutePayload` ha la seguente struttura:
| Proprietà | Tipo | Descrizione |
| ---------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route (ad es., `/users/:id` `{ id: '123' }`) |
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) |
| `isBase64Encoded` | `boolean` | Indica se il corpo è codificato in base64 |
| `requestContext.http.method` | `string` | Metodo HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Percorso della richiesta non elaborato |
| Proprietà | Tipo | Descrizione |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `headers` | `Record<string, string \| undefined>` | Intestazioni HTTP (solo quelle elencate in `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametri della query string (valori multipli uniti da virgole) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametri di percorso estratti dal pattern della route (ad es., `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Corpo della richiesta analizzato (JSON) |
| `isBase64Encoded` | `boolean` | Indica se il corpo è codificato in base64 |
| `requestContext.http.method` | `string` | Metodo HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Percorso della richiesta non elaborato |
### Inoltro delle intestazioni HTTP
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
Puoi creare nuove funzioni in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere una nuova funzione logica. Questo genera un file iniziale con un handler e una configurazione.
* **Generata dallo scaffolder**: Esegui `yarn twenty add` e scegli l'opzione per aggiungere una nuova funzione logica. Questo genera un file iniziale con un handler e una configurazione.
* **Manuale**: Crea un nuovo file `*.logic-function.ts` e usa `defineLogicFunction()`, seguendo lo stesso schema.
### Contrassegnare una funzione logica come strumento
@@ -478,7 +687,7 @@ Per contrassegnare una funzione logica come strumento, imposta `isTool: true` e
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ Punti chiave:
Puoi creare nuovi componenti front-end in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere un nuovo componente front-end.
* **Generata dallo scaffolder**: Esegui `yarn twenty add` e scegli l'opzione per aggiungere un nuovo componente front-end.
* **Manuale**: Crea un nuovo file `.tsx` e usa `defineFrontComponent()`, seguendo lo stesso schema.
### Abilità
@@ -602,47 +811,122 @@ Punti chiave:
Puoi creare nuove skill in due modi:
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere una nuova skill.
* **Generata dallo scaffolder**: Esegui `yarn twenty add` e scegli l'opzione per aggiungere una nuova skill.
* **Manuale**: Crea un nuovo file e usa `defineSkill()`, seguendo lo stesso schema.
### Client tipizzati generati
### Client API tipizzati (`twenty-client-sdk`)
Due client tipizzati sono generati automaticamente da `yarn twenty dev` e salvati in `node_modules/twenty-sdk/generated` in base allo schema del tuo spazio di lavoro:
Il pacchetto `twenty-client-sdk` fornisce due client GraphQL tipizzati per interagire con l'API di Twenty dalle tue funzioni logiche e dai componenti front-end:
* **`CoreApiClient`** — interroga l'endpoint `/graphql` per i dati dello spazio di lavoro
* **`MetadataApiClient`** — interroga l'endpoint `/metadata` per la configurazione dello spazio di lavoro e il caricamento dei file
| Client | Importa | Endpoint | Generato? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------------ | -------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — dati dello spazio di lavoro (record, oggetti) | Sì, in fase di dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurazione dello spazio di lavoro, caricamenti di file | No, fornito pronto all'uso |
#### CoreApiClient
`CoreApiClient` è il client principale per interrogare e modificare i dati dello spazio di lavoro. Viene **generato dallo schema del tuo spazio di lavoro** durante `yarn twenty dev` o `yarn twenty build`, quindi è completamente tipizzato per corrispondere ai tuoi oggetti e campi.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Entrambi i client vengono rigenerati automaticamente da `yarn twenty dev` ogni volta che i tuoi oggetti o campi cambiano.
Il client utilizza una sintassi a selection-set: passa `true` per includere un campo, usa `__args` per gli argomenti e annida oggetti per le relazioni. Ottieni completamento automatico e controllo dei tipi completi basati sullo schema del tuo spazio di lavoro.
#### Credenziali di runtime nelle funzioni logiche
<Note>
**CoreApiClient viene generato in fase di dev/build.** Se provi a usarlo senza eseguire prima `yarn twenty dev` o `yarn twenty build`, genererà un errore. La generazione avviene automaticamente — la CLI esegue l'introspezione dello schema GraphQL del tuo spazio di lavoro, genera un client tipizzato usando `@genql/cli`, scrive le sorgenti generate in `node_modules/twenty-client-sdk/dist/core/generated/` e sostituisce gli stub in `node_modules/twenty-client-sdk/dist/core.mjs` e `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
Quando la tua funzione viene eseguita su Twenty, la piattaforma inietta le credenziali come variabili d'ambiente prima dell'esecuzione del tuo codice:
#### Utilizzo di CoreSchema per le annotazioni di tipo
* `TWENTY_API_URL`: URL di base dell'API Twenty a cui punta la tua app.
* `TWENTY_API_KEY`: Chiave a breve durata con ambito al ruolo funzione predefinito della tua applicazione.
`CoreSchema` fornisce tipi TypeScript corrispondenti agli oggetti del tuo spazio di lavoro, utili per tipizzare lo stato dei componenti o i parametri delle funzioni:
Note:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* Non è necessario passare URL o chiave API al client generato. Legge `TWENTY_API_URL` e `TWENTY_API_KEY` da process.env in fase di esecuzione.
* I permessi della chiave API sono determinati dal ruolo referenziato nel tuo `application-config.ts` tramite `defaultRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `defaultRoleUniversalIdentifier` all'identificatore universale di quel ruolo.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` è fornito pronto all'uso con l'SDK (nessuna generazione richiesta). Interroga l'endpoint `/metadata` per la configurazione dello spazio di lavoro, le applicazioni e i caricamenti di file:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Credenziali di runtime
Quando il tuo codice viene eseguito su Twenty (funzioni logiche o componenti front-end), la piattaforma inietta le credenziali come variabili d'ambiente:
* `TWENTY_API_URL` — URL di base dell'API di Twenty
* `TWENTY_API_KEY` — Chiave a breve durata con ambito al ruolo funzione predefinito della tua applicazione
Non è **necessario** passarle ai client — vengono lette automaticamente da `process.env`. I permessi della chiave API sono determinati dal ruolo referenziato in `defaultRoleUniversalIdentifier` nel tuo `application-config.ts`.
#### Caricamento dei file
Il `MetadataApiClient` generato include un metodo `uploadFile` per allegare file ai campi di tipo file sugli oggetti del tuo spazio di lavoro. Poiché i client GraphQL standard non supportano nativamente il caricamento di file multipart, il client fornisce questo metodo dedicato che implementa la [specifica della richiesta GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec) dietro le quinte.
`MetadataApiClient` include un metodo `uploadFile` per allegare file ai campi di tipo file. Implementa la [specifica delle richieste GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
La firma del metodo:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parametro | Tipo | Descrizione |
| ---------------------------------- | -------- | ------------------------------------------------------------------------ |
| `fileBuffer` | `Buffer` | Il contenuto grezzo del file |
@@ -680,8 +953,7 @@ uploadFile(
Punti chiave:
* Il metodo `uploadFile` è disponibile su `MetadataApiClient` perché la mutazione di upload viene risolta dall'endpoint `/metadata`.
* Usa l'`universalIdentifier` del campo (non il suo ID specifico dello spazio di lavoro), quindi il tuo codice di upload funziona in qualsiasi spazio di lavoro in cui la tua app è installata — coerentemente con il modo in cui le app fanno riferimento ai campi altrove.
* Usa l'`universalIdentifier` del campo (non il suo ID specifico dello spazio di lavoro), quindi il tuo codice di upload funziona in qualsiasi spazio di lavoro in cui la tua app è installata.
* L'`url` restituito è un URL firmato che puoi usare per accedere al file caricato.
### Esempio Hello World
@@ -9,18 +9,19 @@ Le app sono attualmente in fase alfa. La funzionalità è funzionante ma ancora
Le app ti permettono di estendere Twenty con oggetti, campi, funzioni logiche, competenze IA e componenti UI personalizzati — il tutto gestito come codice.
**Cosa puoi fare oggi:**
**Cosa puoi creare:**
* Definisci oggetti e campi personalizzati come codice (modello dati gestito)
* Crea funzioni logiche con trigger personalizzati (route HTTP, cron, eventi del database)
* Definisci le abilità per gli agenti IA
* Crea componenti front-end che vengono renderizzati all'interno della UI di Twenty
* Distribuisci la stessa app su più spazi di lavoro
* Oggetti, campi, viste ed elementi di navigazione personalizzati per definire il tuo modello di dati
* Funzioni logiche attivate da route HTTP, pianificazioni cron o eventi del database
* Componenti front-end che vengono renderizzati direttamente all'interno della UI di Twenty
* Abilità che estendono gli agenti IA di Twenty
* Distribuisci un'app su più spazi di lavoro
## Prerequisiti
* Node.js 24+ e Yarn 4
* Docker (per il server di sviluppo locale di Twenty)
* Node.js 24+
* Yarn 4
* Docker (o un'istanza locale di Twenty in esecuzione)
## Per iniziare
@@ -29,27 +30,15 @@ Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e ini
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Usa l'opzione `--minimal` per creare un'installazione minima
Da qui puoi:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ Con `--minimal`, vengono creati solo i file principali (`application-config.ts`,
A livello generale:
* **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk` più uno script `twenty` che delega alla CLI locale `twenty`. Esegui `yarn twenty help` per elencare tutti i comandi disponibili.
* **.gitignore**: Ignora i file generati comuni come `node_modules`, `.yarn`, `generated/` (client tipizzato), `dist/`, `build/`, cartelle di coverage, file di log e file `.env*`.
* **.gitignore**: Ignora i file generati comuni come `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, cartelle di coverage, file di log e file `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloccano e configurano la toolchain Yarn 4 utilizzata dal progetto.
* **.nvmrc**: Fissa la versione di Node.js prevista dal progetto.
* **.oxlintrc.json** e **tsconfig.json**: Forniscono linting e configurazione TypeScript per i sorgenti TypeScript della tua app.
@@ -167,8 +156,8 @@ export default defineObject({
Comandi successivi aggiungeranno altri file e cartelle:
* `yarn twenty dev` genererà automaticamente due client API tipizzati in `node_modules/twenty-sdk/generated`: `CoreApiClient` (per i dati dell'area di lavoro tramite `/graphql`) e `MetadataApiClient` (per la configurazione dell'area di lavoro e il caricamento di file tramite `/metadata`).
* `yarn twenty entity:add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti, funzioni, componenti front-end, ruoli, competenze e altro ancora.
* `yarn twenty dev` genererà automaticamente il `CoreApiClient` tipizzato (per i dati dell'area di lavoro via `/graphql`) in `node_modules/twenty-client-sdk/`. Il `MetadataApiClient` (per la configurazione dell'area di lavoro e il caricamento di file via `/metadata`) è fornito precompilato ed è disponibile immediatamente. Importali da `twenty-client-sdk/core` e `twenty-client-sdk/metadata` rispettivamente.
* `yarn twenty add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti personalizzati, funzioni, componenti front-end, ruoli, competenze e altro ancora.
## Autenticazione
@@ -12,7 +12,25 @@ Le app sono attualmente in fase alfa. La funzionalità è funzionante ma ancora
Una volta che la tua app è stata [compilata e testata localmente](/l/it/developers/extend/apps/building), hai due modalità per distribuirla:
* **Pubblica su npm** — elenca la tua app nel marketplace di Twenty affinché qualsiasi spazio di lavoro possa scoprirla e installarla.
* **Esegui il push di un tarball** — distribuisci la tua app su un server Twenty specifico per uso interno senza renderla pubblicamente disponibile.
* **Distribuisci un tarball** — carica la tua app direttamente su un server Twenty specifico per uso interno o privato.
Entrambi i percorsi partono dalla stessa fase di **build**.
## Compilazione della tua app
Il comando `build` compila i tuoi sorgenti TypeScript, transpila le funzioni di logica e i componenti front-end e genera un `manifest.json` che descrive i contenuti della tua app:
```bash filename="Terminal"
yarn twenty build
```
L'output viene scritto in `.twenty/output/`. Questa directory contiene tutto il necessario per la distribuzione: codice compilato, risorse, il manifest e una copia del tuo `package.json`.
Per creare anche un tarball `.tgz` (usato internamente dal comando di deploy o per la distribuzione manuale):
```bash filename="Terminal"
yarn twenty build --tarball
```
## Pubblicazione su npm
@@ -21,29 +39,76 @@ La pubblicazione su npm rende la tua app scopribile nel marketplace di Twenty. Q
### Requisiti
* Un account [npm](https://www.npmjs.com)
* Il nome del tuo pacchetto **deve** usare il prefisso `twenty-app-` (es. `twenty-app-postcard-sender`)
* La parola chiave `twenty-app` **deve** essere elencata nell'array `keywords` del tuo `package.json`
### Aggiunta della parola chiave richiesta
Il marketplace di Twenty individua le app cercando nel registro npm i pacchetti con la parola chiave `twenty-app`. Aggiungila al tuo `package.json`:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
Il marketplace cerca `keywords:twenty-app` sul registro npm. Senza questa parola chiave, il tuo pacchetto non apparirà nel marketplace anche se ha il prefisso nel nome `twenty-app-`.
</Note>
### Passaggi
1. **Compila la tua app** — la CLI compila i tuoi sorgenti TypeScript e genera il manifest dell'applicazione:
1. **Compila la tua app:**
```bash filename="Terminal"
yarn twenty build
```
2. **Pubblica su npm** — esegui il push del pacchetto compilato al registro npm:
2. **Pubblica su npm:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Rilevamento automatico
Questo esegue `npm publish` dalla directory `.twenty/output/`.
I pacchetti con il prefisso `twenty-app-` vengono rilevati automaticamente dal catalogo del marketplace di Twenty. Una volta pubblicata, la tua app compare nel marketplace entro pochi minuti — non è richiesta alcuna registrazione o approvazione manuale.
Per pubblicare con un dist-tag specifico (ad es. `beta` o `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### Come funziona l'individuazione nel marketplace
Il server Twenty sincronizza il proprio catalogo del marketplace dal registro npm **ogni ora**:
1. Cerca tutti i pacchetti npm con `keywords:twenty-app`
2. Per ogni pacchetto, recupera il `manifest.json` dalla CDN di npm
3. I metadati dell'app (nome, descrizione, autore, logo, screenshot, categoria) vengono estratti dal manifest e visualizzati nel marketplace
Dopo la pubblicazione, la tua app può impiegare fino a un'ora per apparire nel marketplace. Per attivare subito la sincronizzazione invece di attendere la prossima esecuzione oraria:
```bash filename="Terminal"
yarn twenty catalog-sync
```
Per puntare a un remote specifico:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
I metadati mostrati nel marketplace provengono dalla chiamata a `defineApplication()` nel codice sorgente della tua app — campi come `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` e `termsUrl`.
<Note>
Se la tua app non definisce un `aboutDescription` in `defineApplication()`, il marketplace userà automaticamente il `README.md` del tuo pacchetto su npm come contenuto della pagina Informazioni. Questo significa che puoi mantenere un unico README sia per npm sia per il marketplace di Twenty. Se desideri una descrizione diversa nel marketplace, imposta esplicitamente `aboutDescription`.
</Note>
### Pubblicazione con CI
Il progetto generato include un workflow di GitHub Actions che pubblica a ogni release. Esegue `app:build`, quindi `npm publish --provenance` dall'output di build:
Il progetto generato include un workflow di GitHub Actions che pubblica a ogni release:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Per altri sistemi CI (GitLab CI, CircleCI, ecc.), si applicano gli stessi tre comandi: `yarn install`, `npx twenty build`, quindi `npm publish` da `.twenty/output`.
Per altri sistemi CI (GitLab CI, CircleCI, ecc.), si applicano gli stessi tre comandi: `yarn install`, `yarn twenty build`, quindi `npm publish` da `.twenty/output`.
<Tip>
**npm provenance** è opzionale ma consigliata. La pubblicazione con `--provenance` aggiunge un badge di attendibilità alla tua scheda npm, consentendo agli utenti di verificare che il pacchetto sia stato creato a partire da uno specifico commit in una pipeline CI pubblica. Consulta la [documentazione su npm provenance](https://docs.npmjs.com/generating-provenance-statements) per le istruzioni di configurazione.
</Tip>
## Distribuzione interna
## Distribuzione su un server (tarball)
Per le app che non vuoi rendere pubbliche — strumenti proprietari, integrazioni solo per l'azienda o build sperimentali — puoi eseguire il push di un tarball direttamente su un server Twenty.
Per le app che non vuoi rendere pubbliche — strumenti proprietari, integrazioni solo aziendali o build sperimentali — puoi distribuire un tarball direttamente su un server Twenty.
### Push di un tarball
### Prerequisiti
Compila la tua app e distribuiscila su un server specifico in un solo passaggio:
Prima della distribuzione, ti serve un remote configurato che punti al server di destinazione. I remote memorizzano localmente l'URL del server e le credenziali di autenticazione in `~/.twenty/config.json`.
Aggiungi un remote:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Qualsiasi spazio di lavoro su quel server può quindi installare e aggiornare l'app dalla pagina delle impostazioni **Applicazioni**.
Per un server di sviluppo locale:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
Puoi anche autenticarti con una chiave API per ambienti non interattivi:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Gestisci i tuoi remote:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Distribuzione
Compila e carica la tua app sul server in un solo passaggio:
```bash filename="Terminal"
yarn twenty deploy
```
Questo compila l'app con `--tarball`, quindi carica il tarball sul remote predefinito tramite un upload multipart GraphQL.
Per distribuire su un remote specifico:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Condivisione di un'app distribuita
Le app in formato tarball non sono elencate nel marketplace pubblico, quindi altri spazi di lavoro sullo stesso server non le troveranno navigando. Per condividere un'app distribuita:
1. Vai su **Impostazioni > Applicazioni > Registrazioni** e apri la tua app
2. Nella scheda **Distribuzione**, fai clic su **Copia link di condivisione**
3. Condividi questo link con utenti su altri spazi di lavoro — li porterà direttamente alla pagina di installazione dell'app
Il link di condivisione utilizza l'URL di base del server (senza alcun sottodominio dello spazio di lavoro) così funziona per qualsiasi spazio di lavoro sul server.
### Gestione delle versioni
Per rilasciare un aggiornamento:
1. Incrementa il campo `version` nel tuo `package.json`
2. Esegui il push di un nuovo tarball con `npx twenty publish --server <server-url>`
3. Gli spazi di lavoro su quel server vedranno l'aggiornamento disponibile nelle proprie impostazioni
2. Esegui `yarn twenty deploy` (oppure `yarn twenty deploy -r production`)
3. Gli spazi di lavoro che hanno l'app installata vedranno l'aggiornamento disponibile nelle proprie impostazioni
<Note>
Le app interne sono limitate al server su cui vengono caricate. Non compariranno nel marketplace pubblico e non potranno essere installate dagli spazi di lavoro su altri server.
</Note>
## Installazione delle app
## Categorie di app
Una volta che un'app è stata pubblicata (npm) o distribuita (tarball), gli spazi di lavoro la installano tramite l'interfaccia utente:
```bash filename="Terminal"
yarn twenty install
```
Oppure dalla pagina **Impostazioni > Applicazioni** nell'interfaccia di Twenty, dove è possibile sfogliare e installare sia le app del marketplace sia quelle distribuite tramite tarball.
## Categorie di distribuzione delle app
Twenty organizza le app in tre categorie in base a come vengono distribuite:
| Categoria | Come funziona | Visibile nel marketplace? |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| **Sviluppo** | App in modalità di sviluppo locale eseguite tramite `yarn twenty dev`. Usate per la compilazione e i test. | No |
| **Pubblicata** | App pubblicate su npm con il prefisso `twenty-app-`. Elencate nel marketplace per l'installazione da parte di qualsiasi spazio di lavoro. | Sì |
| **Interna** | App distribuite tramite tarball su un server specifico. Disponibili solo per gli spazi di lavoro su quel server. | No |
| Categoria | Come funziona | Visibile nel marketplace? |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
| **Sviluppo** | App in modalità di sviluppo locale eseguite tramite `yarn twenty dev`. Usate per la compilazione e i test. | No |
| **Pubblicate (npm)** | App pubblicate su npm con la parola chiave `twenty-app`. Elencate nel marketplace per l'installazione da parte di qualsiasi spazio di lavoro. | Sì |
| **Interne (tarball)** | App distribuite tramite tarball su un server specifico. Disponibili solo per gli spazi di lavoro su quel server tramite un link di condivisione. | No |
<Tip>
Inizia in modalità **Sviluppo** mentre crei la tua app. Quando è pronta, scegli **Pubblicata** (npm) per un'ampia distribuzione oppure **Interna** (tarball) per una distribuzione privata.
</Tip>
## Riferimento CLI
| Comando | Descrizione | Flag principali |
| --------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------- |
| `yarn twenty build` | Compila l'app e genera il manifest | `--tarball` — crea anche un pacchetto `.tgz` |
| `yarn twenty publish` | Compila e pubblica su npm | `--tag <tag>` — dist-tag npm (ad es. `beta`, `next`) |
| `yarn twenty deploy` | Compila e carica un tarball su un server | `-r, --remote <name>` — remote di destinazione |
| `yarn twenty catalog-sync` | Attiva la sincronizzazione del catalogo del marketplace sul server | `-r, --remote <name>` — remote di destinazione |
| `yarn twenty install` | Installa un'app distribuita su uno spazio di lavoro | `-r, --remote <name>` — remote di destinazione |
| `yarn twenty dev` | Osserva e sincronizza le modifiche locali | Usa il remote predefinito |
| `yarn twenty remote add` | Aggiungi una connessione al server | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | Elenca i remote configurati | — |
| `yarn twenty remote switch` | Imposta il remote predefinito | — |
| `yarn twenty remote status` | Mostra lo stato della connessione | — |
| `yarn twenty remote remove` | Rimuovi un remote | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### Gestione del server locale
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker sulla porta 2020). Questi comandi si applicano solo al server di sviluppo basato su Docker — non gestiscono un'istanza di Twenty avviata dal sorgente (ad es. `npx nx start twenty-server` sulla porta 3000):
```bash filename="Terminal"
# Avvia il server locale (scarica l'immagine se necessario)
@@ -15,19 +15,21 @@ Biblioteca twenty-sdk oferă blocuri de bază tipizate și funcții ajutătoare
SDK-ul oferă funcții ajutătoare pentru definirea entităților aplicației. După cum este descris în [Detectarea entităților](/l/ro/developers/extend/apps/getting-started#entity-detection), trebuie să folosiți `export default define<Entity>({...})` pentru ca entitățile să fie detectate:
| Funcție | Scop |
| -------------------------------- | ---------------------------------------------------------------------- |
| `defineApplication` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
| `defineObject` | Definiți obiecte personalizate cu câmpuri |
| `defineLogicFunction` | Definiți funcții de logică cu handleri |
| `definePreInstallLogicFunction` | Definește o funcție logică de pre-instalare (una per aplicație) |
| `definePostInstallLogicFunction` | Definește o funcție logică post-instalare (una per aplicație) |
| `defineFrontComponent` | Definiți componente Front pentru interfața de utilizator personalizată |
| `defineRole` | Configurați permisiunile rolurilor și accesul la obiecte |
| `defineField` | Extindeți obiectele existente cu câmpuri suplimentare |
| `defineView` | Definește vizualizări salvate pentru obiecte |
| `defineNavigationMenuItem` | Definește linkuri de navigare în bara laterală |
| `defineSkill` | Definiți abilități pentru agentul AI |
| Funcție | Scop |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `defineApplication` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
| `defineObject` | Definiți obiecte personalizate cu câmpuri |
| `defineField` | Extindeți obiectele existente cu câmpuri suplimentare sau definiți câmpuri de relație independente |
| `defineLogicFunction` | Definiți funcții de logică cu handleri |
| `definePreInstallLogicFunction` | Definește o funcție logică de pre-instalare (una per aplicație) |
| `definePostInstallLogicFunction` | Definește o funcție logică post-instalare (una per aplicație) |
| `defineFrontComponent` | Definiți componente Front pentru interfața de utilizator personalizată |
| `defineRole` | Configurați permisiunile rolurilor și accesul la obiecte |
| `defineView` | Definește vizualizări salvate pentru obiecte |
| `defineNavigationMenuItem` | Definește linkuri de navigare în bara laterală |
| `defineSkill` | Definiți abilități pentru agentul AI |
| `defineAgent` | Definiți agenți AI |
| `definePageLayout` | Definiți machete de pagină personalizate |
Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
@@ -36,7 +38,7 @@ Aceste funcții validează configurația în timpul build-ului și oferă comple
Obiectele personalizate descriu atât schema, cât și comportamentul înregistrărilor din spațiul dvs. de lucru. Utilizați `defineObject()` pentru a defini obiecte cu validare încorporată:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ Puncte cheie:
* `universalIdentifier` trebuie să fie unic și stabil între implementări.
* Fiecare câmp necesită un `name`, un `type`, un `label` și propriul `universalIdentifier` stabil.
* Matricea `fields` este opțională — puteți defini obiecte fără câmpuri personalizate.
* Puteți genera obiecte noi folosind `yarn twenty entity:add`, care vă ghidează prin denumire, câmpuri și relații.
* Puteți genera obiecte noi folosind `yarn twenty add`, care vă ghidează prin denumire, câmpuri și relații.
<Note>
**Câmpurile de bază sunt create automat.** Când definiți un obiect personalizat, Twenty adaugă automat câmpuri standard
@@ -120,6 +122,197 @@ Puteți suprascrie câmpurile implicite definind un câmp cu același nume în t
dar acest lucru nu este recomandat.
</Note>
### Definirea câmpurilor pe obiecte existente
Utilizați `defineField()` pentru a adăuga câmpuri la obiecte pe care nu le dețineți — cum ar fi obiectele standard Twenty (Person, Company etc.). sau obiecte din alte aplicații. Spre deosebire de câmpurile inline din `defineObject()`, câmpurile independente necesită un `objectUniversalIdentifier` pentru a specifica obiectul pe care îl extind:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Puncte cheie:
* `objectUniversalIdentifier` identifică obiectul țintă. Pentru obiectele standard, utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` exportați din `twenty-sdk`.
* Atunci când definiți câmpuri inline în `defineObject()`, nu aveți nevoie de `objectUniversalIdentifier` — este moștenit de la obiectul părinte.
* `defineField()` este singura modalitate de a adăuga câmpuri la obiecte pe care nu le-ați creat cu `defineObject()`.
### Relații
Relațiile conectează obiectele între ele. În Twenty, relațiile sunt întotdeauna bidirecționale — definiți ambele părți, iar fiecare parte o referențiază pe cealaltă.
Există două tipuri de relații:
| Tip relație | Descriere | Are cheie străină? |
| ------------- | ---------------------------------------------------------------------------------- | --------------------- |
| `MANY_TO_ONE` | Multe înregistrări ale acestui obiect indică către o singură înregistrare a țintei | Da (`joinColumnName`) |
| `ONE_TO_MANY` | O înregistrare a acestui obiect are multe înregistrări ale țintei | Nu (partea inversă) |
#### Cum funcționează relațiile
Fiecare relație necesită **două câmpuri** care se referențiază reciproc:
1. Partea **MANY_TO_ONE** — se află pe obiectul care deține cheia străină
2. Partea **ONE_TO_MANY** — se află pe obiectul care deține colecția
Ambele câmpuri folosesc `FieldType.RELATION` și se referențiază încrucișat prin `relationTargetFieldMetadataUniversalIdentifier`.
#### Exemplu: Post Card are mulți destinatari
Presupuneți că un `PostCard` poate fi trimis către multe înregistrări `PostCardRecipient`. Fiecare destinatar aparține exact unui Post Card.
**Pasul 1: Definiți partea ONE_TO_MANY pe PostCard** (partea "one"):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Pasul 2: Definiți partea MANY_TO_ONE pe PostCardRecipient** (partea "many" — deține cheia străină):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Importuri circulare:** Ambele câmpuri de relație se referă unul la celălalt prin `universalIdentifier`. Pentru a evita problemele de import circular, exportați ID-urile câmpurilor ca constante denumite din fiecare fișier și importați-le în celălalt fișier. Sistemul de build le rezolvă în timpul compilării.
</Note>
#### Relaționarea cu obiectele standard
Pentru a crea o relație cu un obiect Twenty încorporat (Person, Company etc.), utilizați `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Proprietăți ale câmpului de relație
| Proprietate | Obligatoriu | Descriere |
| ------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `tip` | Da | Trebuie să fie `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Da | `universalIdentifier` al obiectului țintă |
| `relationTargetFieldMetadataUniversalIdentifier` | Da | `universalIdentifier` al câmpului corespunzător de pe obiectul țintă |
| `universalSettings.relationType` | Da | `RelationType.MANY_TO_ONE` sau `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Doar MANY_TO_ONE | Ce se întâmplă atunci când înregistrarea referențiată este ștearsă: `CASCADE`, `SET_NULL`, `RESTRICT` sau `NO_ACTION` |
| `universalSettings.joinColumnName` | Doar MANY_TO_ONE | Numele coloanei din baza de date pentru cheia străină (de ex., `postCardId`) |
#### Câmpuri de relație inline în defineObject
Puteți defini, de asemenea, câmpuri de relație direct în `defineObject()`. În acest caz, omiteți `objectUniversalIdentifier` — este moștenit de la obiectul părinte:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Configurația aplicației (application-config.ts)
Fiecare aplicație are un singur fișier `application-config.ts` care descrie:
@@ -161,6 +354,22 @@ Notițe:
* `defaultRoleUniversalIdentifier` trebuie să corespundă fișierului de rol (vedeți mai jos).
* Funcțiile de pre-instalare și post-instalare sunt detectate automat în timpul construirii manifestului. Vezi [Funcții de pre-instalare](#pre-install-functions) și [Funcții post-instalare](#post-install-functions).
#### Metadate pentru marketplace
Dacă intenționați să [publicați aplicația](/l/ro/developers/extend/apps/publishing), aceste câmpuri opționale controlează modul în care aplicația apare în marketplace:
| Câmp | Descriere |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `autor` | Numele autorului sau al companiei |
| `categorie` | Categoria aplicației pentru filtrarea în marketplace |
| `logoUrl` | Calea către logo-ul aplicației (relativă la `./assets/`) |
| `screenshots` | Listă de căi către capturi de ecran (relative la `./assets/`) |
| `aboutDescription` | Descriere markdown mai lungă pentru fila "About". Dacă este omis, marketplace-ul folosește `README.md` al pachetului de pe npm |
| `websiteUrl` | Link către site-ul dvs. |
| `termsUrl` | Link către termenii de serviciu |
| `emailSupport` | Adresă de e-mail pentru suport |
| `issueReportUrl` | Link către sistemul de urmărire a problemelor |
#### Roluri și permisiuni
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `defaultRoleUniversalIdentifier` din `application-config.ts` desemnează rolul implicit utilizat de funcțiile de logică ale aplicației.
@@ -230,7 +439,7 @@ Notițe:
Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
Poți, de asemenea, să execuți manual funcția de pre-instalare oricând folosind CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Puncte cheie:
@@ -334,7 +543,7 @@ Puncte cheie:
* Este permisă o singură funcție de pre-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una.
* Proprietatea `universalIdentifier` a funcției este setată automat ca `preInstallLogicFunctionUniversalIdentifier` în manifestul aplicației în timpul build-ului — nu este nevoie să o referi în `defineApplication()`.
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de pregătire mai lungi.
* Funcțiile de pre-instalare nu au nevoie de declanșatoare — sunt invocate de platformă înainte de instalare sau manual prin `function:execute --preInstall`.
* Funcțiile de pre-instalare nu au nevoie de declanșatoare — sunt invocate de platformă înainte de instalare sau manual prin `exec --preInstall`.
### Funcții post-instalare
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
Poți, de asemenea, să execuți manual funcția post-instalare oricând folosind CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Puncte cheie:
@@ -372,7 +581,7 @@ Puncte cheie:
* Este permisă o singură funcție de post-instalare per aplicație. Construirea manifestului va genera o eroare dacă este detectată mai mult de una.
* Proprietatea `universalIdentifier` a funcției este setată automat ca `postInstallLogicFunctionUniversalIdentifier` în manifestul aplicației în timpul build-ului — nu este nevoie să o referi în `defineApplication()`.
* Timpul de expirare implicit este setat la 300 de secunde (5 minute) pentru a permite sarcini de configurare mai lungi, cum ar fi popularea datelor.
* Funcțiile post-instalare nu au nevoie de declanșatoare — sunt invocate de platformă în timpul instalării sau manual prin `function:execute --postInstall`.
* Funcțiile post-instalare nu au nevoie de declanșatoare — sunt invocate de platformă în timpul instalării sau manual prin `exec --postInstall`.
### Payload-ul declanșatorului de rută
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
Tipul `RoutePayload` are următoarea structură:
| Proprietate | Tip | Descriere |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
| `headers` | `Record<string, string \| undefined>` | Anteturi HTTP (doar cele listate în `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametri query string (valorile multiple unite cu virgule) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametri de cale extrași din modelul rutei (de ex., `/users/:id` `{ id: '123' }`) |
| `body` | `object \| null` | Corpul cererii analizat (JSON) |
| `isBase64Encoded` | `boolean` | Indică dacă corpul este codificat în base64 |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Calea brută a cererii |
| Proprietate | Tip | Descriere |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | Anteturi HTTP (doar cele listate în `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Parametri query string (valorile multiple unite cu virgule) |
| `pathParameters` | `Record<string, string \| undefined>` | Parametri de cale extrași din modelul rutei (de ex., `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Corpul cererii analizat (JSON) |
| `isBase64Encoded` | `boolean` | Indică dacă corpul este codificat în base64 |
| `requestContext.http.method` | `string` | Metoda HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Calea brută a cererii |
### Transmiterea anteturilor HTTP
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
Puteți crea funcții noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o funcție logică nouă. Aceasta generează un fișier inițial cu un handler și o configurație.
* **Generat**: Rulați `yarn twenty add` și alegeți opțiunea de a adăuga o funcție de logică nouă. Aceasta generează un fișier inițial cu un handler și o configurație.
* **Manual**: Creați un fișier nou `*.logic-function.ts` și folosiți `defineLogicFunction()`, urmând același model.
### Marcarea unei funcții logice drept instrument
@@ -478,7 +687,7 @@ Pentru a marca o funcție logică drept instrument, setați `isTool: true` și f
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ Puncte cheie:
Puteți crea componente Front noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o componentă frontend nouă.
* **Generat**: Rulați `yarn twenty add` și alegeți opțiunea de a adăuga o componentă Front nouă.
* **Manual**: Creați un fișier nou `.tsx` și folosiți `defineFrontComponent()`, urmând același model.
### Abilități
@@ -602,47 +811,122 @@ Puncte cheie:
Puteți crea abilități noi în două moduri:
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o abilitate nouă.
* **Generat**: Rulați `yarn twenty add` și alegeți opțiunea de a adăuga o abilitate nouă.
* **Manual**: Creați un fișier nou și folosiți `defineSkill()`, urmând același model.
### Clienți tipizați generați
### Clienți API tipați (`twenty-client-sdk`)
Doi clienți tipizați sunt generați automat de `yarn twenty dev` și stocați în `node_modules/twenty-sdk/generated`, pe baza schemei spațiului tău de lucru:
Pachetul `twenty-client-sdk` oferă doi clienți GraphQL tipați pentru a interacționa cu API-ul Twenty din funcțiile de logică și componentele Front:
* **`CoreApiClient`** — interoghează endpointul `/graphql` pentru datele spațiului de lucru
* **`MetadataApiClient`** — interoghează endpointul `/metadata` pentru configurarea spațiului de lucru și încărcarea fișierelor
| Client | Importați | Endpoint | Generat? |
| ------------------- | ---------------------------- | ------------------------------------------------------------------- | ---------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — date ale spațiului de lucru (înregistrări, obiecte) | Da, în timpul dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — configurarea spațiului de lucru, încărcări de fișiere | Nu, este livrat preconstruit |
#### CoreApiClient
`CoreApiClient` este clientul principal pentru interogarea și modificarea datelor din spațiul de lucru. Este generat din schema spațiului dvs. de lucru în timpul `yarn twenty dev` sau `yarn twenty build`, astfel încât este complet tipat pentru a corespunde obiectelor și câmpurilor dvs.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Ambii clienți sunt regenerați automat de `yarn twenty dev` ori de câte ori obiectele sau câmpurile tale se schimbă.
Clientul folosește o sintaxă de tip selection-set: transmiteți `true` pentru a include un câmp, folosiți `__args` pentru argumente și imbricați obiecte pentru relații. Obțineți autocompletare și verificare a tipurilor complete, pe baza schemei spațiului dvs. de lucru.
#### Acreditări la runtime în funcțiile de logică
<Note>
**CoreApiClient este generat în timpul dev/build.** Dacă încercați să îl utilizați fără a rula mai întâi `yarn twenty dev` sau `yarn twenty build`, va arunca o eroare. Generarea are loc automat — CLI-ul introspectează schema GraphQL a spațiului dvs. de lucru, generează un client tipat folosind `@genql/cli`, scrie sursele generate în `node_modules/twenty-client-sdk/dist/core/generated/` și înlocuiește stubs-urile din `node_modules/twenty-client-sdk/dist/core.mjs` și `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
Când funcția rulează pe Twenty, platforma injectează acreditări ca variabile de mediu înainte de execuția codului:
#### Folosirea CoreSchema pentru adnotări de tip
* `TWENTY_API_URL`: URL-ul de bază al API-ului Twenty către care țintește aplicația.
* `TWENTY_API_KEY`: Cheie cu durată scurtă, limitată la rolul implicit de funcție al aplicației.
`CoreSchema` oferă tipuri TypeScript care se potrivesc obiectelor din spațiul dvs. de lucru, utile pentru tiparea stării componentelor sau a parametrilor funcțiilor:
Notițe:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* Nu trebuie să transmiteți URL-ul sau cheia API către clientul generat. Acesta citește `TWENTY_API_URL` și `TWENTY_API_KEY` din process.env la runtime.
* Permisiunile cheii API sunt determinate de rolul referențiat în `application-config.ts` prin `defaultRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `defaultRoleUniversalIdentifier` la identificatorul universal al acelui rol.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` este livrat preconstruit împreună cu SDK-ul (nu este necesară generarea). Interoghează endpointul `/metadata` pentru configurarea spațiului de lucru, aplicații și încărcări de fișiere:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Acreditări la rulare
Când codul dvs. rulează pe Twenty (funcții de logică sau componente Front), platforma injectează acreditările ca variabile de mediu:
* `TWENTY_API_URL` — URL-ul de bază al API-ului Twenty
* `TWENTY_API_KEY` — Cheie cu durată scurtă, limitată la rolul implicit de funcție al aplicației
Nu trebuie să le transmiteți clienților — aceștia citesc automat din `process.env`. Permisiunile cheii API sunt determinate de rolul referențiat în `defaultRoleUniversalIdentifier` din `application-config.ts`.
#### Încărcarea fișierelor
Clientul `MetadataApiClient` generat include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier ale obiectelor din spațiul tău de lucru. Deoarece clienții GraphQL standard nu acceptă în mod nativ încărcarea fișierelor multipart, clientul oferă această metodă dedicată care implementează, sub capotă, [specificația cererilor GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec).
`MetadataApiClient` include o metodă `uploadFile` pentru atașarea fișierelor la câmpuri de tip fișier. Implementează [specificația pentru cereri GraphQL multipart](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
Semnătura metodei:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parametru | Tip | Descriere |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | Conținutul brut al fișierului |
@@ -680,8 +953,7 @@ uploadFile(
Puncte cheie:
* Metoda `uploadFile` este disponibilă pe `MetadataApiClient` deoarece mutația de încărcare este rezolvată de endpointul `/metadata`.
* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul tău de încărcare funcționează în orice spațiu de lucru în care aplicația ta este instalată — în concordanță cu modul în care aplicațiile fac referire la câmpuri în rest.
* Folosește `universalIdentifier` al câmpului (nu ID-ul specific spațiului de lucru), astfel încât codul dvs. de încărcare funcționează în orice spațiu de lucru în care aplicația dvs. este instalată.
* `url` returnat este un URL semnat pe care îl poți folosi pentru a accesa fișierul încărcat.
### Exemplu Hello World
@@ -9,18 +9,19 @@ Aplicațiile sunt în prezent în testare alfa. Caracteristica funcționează, d
Aplicațiile vă permit să extindeți Twenty cu obiecte personalizate, câmpuri, funcții logice, abilități IA și componente UI — toate gestionate ca cod.
**Ce puteți face astăzi:**
**Ce puteți construi:**
* Definiți obiecte și câmpuri personalizate sub formă de cod (model de date gestionat)
* Construiți funcții logice cu declanșatoare personalizate (rute HTTP, cron, evenimente ale bazei de date)
* Definiți abilități pentru agenți de IA
* Construiți componente front-end care se afișează în interfața Twenty
* Implementați aceeași aplicație în mai multe spații de lucru
* Obiecte, câmpuri, vizualizări și elemente de navigare personalizate pentru a defini modelul dumneavoastră de date
* Funcții logice declanșate de rute HTTP, programări cron sau evenimente din baza de date
* Componente front-end care se afișează direct în interfața Twenty
* Abilități care extind capabilitățile agenților AI ai Twenty
* Implementați o aplicație în mai multe spații de lucru
## Cerințe
* Node.js 24+ și Yarn 4
* Docker (pentru serverul local de dezvoltare Twenty)
* Node.js 24+
* Yarn 4
* Docker (sau o instanță Twenty locală în execuție)
## Începeți
@@ -29,27 +30,15 @@ Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Folosiți opțiunea `--minimal` pentru a genera o instalare minimă
De aici puteți:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ Cu `--minimal`, sunt create doar fișierele de bază (`application-config.ts`, `
Pe scurt:
* **package.json**: Declară numele aplicației, versiunea, motoarele (Node 24+, Yarn 4) și adaugă `twenty-sdk` plus un script `twenty` care deleagă către CLI-ul local `twenty`. Rulați `yarn twenty help` pentru a lista toate comenzile disponibile.
* **.gitignore**: Ignoră artefacte comune precum `node_modules`, `.yarn`, `generated/` (client tipizat), `dist/`, `build/`, foldere de coverage, fișiere jurnal și fișiere `.env*`.
* **.gitignore**: Ignoră artefacte comune precum `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, foldere de coverage, fișiere jurnal și fișiere `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Blochează și configurează lanțul de instrumente Yarn 4 folosit de proiect.
* **.nvmrc**: Fixează versiunea Node.js așteptată de proiect.
* **.oxlintrc.json** și **tsconfig.json**: Oferă linting și configurație TypeScript pentru fișierele TypeScript ale aplicației.
@@ -167,8 +156,8 @@ export default defineObject({
Comenzile ulterioare vor adăuga mai multe fișiere și foldere:
* `yarn twenty dev` va genera automat doi clienți API tipizați în `node_modules/twenty-sdk/generated`: `CoreApiClient` (pentru datele spațiului de lucru prin `/graphql`) și `MetadataApiClient` (pentru configurarea spațiului de lucru și încărcarea fișierelor prin `/metadata`).
* `yarn twenty entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end, rolurile, abilitățile și altele.
* `yarn twenty dev` va genera automat `CoreApiClient` tipizat (pentru datele spațiului de lucru prin `/graphql`) în `node_modules/twenty-client-sdk/`. `MetadataApiClient` (pentru configurarea spațiului de lucru și încărcarea fișierelor prin `/metadata`) este livrat preconstruit și este disponibil imediat. Importați-le din `twenty-client-sdk/core` și `twenty-client-sdk/metadata`, respectiv.
* `yarn twenty add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele personalizate, funcțiile, componentele front-end, rolurile, abilitățile și altele.
## Autentificare
@@ -12,7 +12,25 @@ Aplicațiile sunt în prezent în testare alfa. Caracteristica funcționează, d
După ce aplicația ta este [construită și testată local](/l/ro/developers/extend/apps/building), ai două căi pentru distribuire:
* **Publică pe npm** — listează aplicația ta în marketplace-ul Twenty pentru ca orice spațiu de lucru să o poată descoperi și instala.
* **Trimite un tarball** — implementează aplicația ta pe un server Twenty specific pentru utilizare internă, fără a o face disponibilă public.
* **Implementați o arhivă tar** — încărcați aplicația direct pe un server Twenty anume pentru uz intern sau privat.
Ambele căi pornesc din aceeași etapă de **build**.
## Construirea aplicației
Comanda `build` compilează sursele TypeScript, transpilează funcțiile de logică și componentele de front-end și generează un `manifest.json` care descrie conținutul aplicației:
```bash filename="Terminal"
yarn twenty build
```
Rezultatul este scris în `.twenty/output/`. Acest director conține tot ce este necesar pentru distribuție: cod compilat, resurse, manifestul și o copie a fișierului tău `package.json`.
Pentru a crea și un pachet `.tgz` (folosit intern de comanda de implementare sau pentru distribuire manuală):
```bash filename="Terminal"
yarn twenty build --tarball
```
## Publicarea pe npm
@@ -21,29 +39,76 @@ Publicarea pe npm face ca aplicația ta să poată fi descoperită în marketpla
### Cerințe
* Un cont [npm](https://www.npmjs.com)
* Numele pachetului tău **trebuie** să folosească prefixul `twenty-app-` (de ex., `twenty-app-postcard-sender`)
* Cuvântul cheie `twenty-app` trebuie să fie listat în array-ul `keywords` din `package.json`-ul tău
### Adăugarea cuvântului cheie necesar
Marketplace-ul Twenty descoperă aplicații căutând în registrul npm pachete cu cuvântul cheie `twenty-app`. Adaugă-l în `package.json`-ul tău:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
Marketplace-ul caută `keywords:twenty-app` în registrul npm. Fără acest cuvânt cheie, pachetul tău nu va apărea în marketplace chiar dacă are prefixul de nume `twenty-app-`.
</Note>
### Pași
1. **Construiește-ți aplicația** — CLI compilează sursele TypeScript și generează manifestul aplicației:
1. **Construiește-ți aplicația:**
```bash filename="Terminal"
yarn twenty build
```
2. **Publică pe npm** — publică pachetul construit în registrul npm:
2. **Publică pe npm:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Descoperire automată
Aceasta rulează `npm publish` din directorul `.twenty/output/`.
Pachetele cu prefixul `twenty-app-` sunt descoperite automat de catalogul marketplace-ului Twenty. După publicare, aplicația ta apare în marketplace în câteva minute — fără înregistrare manuală sau aprobare necesară.
Pentru a publica sub un dist-tag specific (de ex., `beta` sau `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### Cum funcționează descoperirea în marketplace
Serverul Twenty sincronizează catalogul marketplace-ului din registrul npm la fiecare oră:
1. Caută toate pachetele npm cu cuvântul cheie `keywords:twenty-app`
2. Pentru fiecare pachet, preia `manifest.json` din CDN-ul npm
3. Metadatele aplicației (nume, descriere, autor, logo, capturi de ecran, categorie) sunt extrase din manifest și afișate în marketplace
După publicare, poate dura până la o oră ca aplicația ta să apară în marketplace. Pentru a declanșa sincronizarea imediat, în loc să aștepți următoarea rulare orară:
```bash filename="Terminal"
yarn twenty catalog-sync
```
Pentru a viza un remote specific:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
Metadatele afișate în marketplace provin din apelul tău `defineApplication()` din codul sursă al aplicației — câmpuri precum `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` și `termsUrl`.
<Note>
Dacă aplicația ta nu definește un `aboutDescription` în `defineApplication()`, piața va folosi automat fișierul `README.md` al pachetului tău de pe npm drept conținut pentru pagina Despre. Acest lucru înseamnă că poți menține un singur README atât pentru npm, cât și pentru piața Twenty. Dacă vrei o descriere diferită în piață, setează explicit `aboutDescription`.
</Note>
### Publicare CI
Proiectul generat include un workflow GitHub Actions care publică la fiecare lansare. Acesta rulează `app:build`, apoi `npm publish --provenance` din rezultatul build-ului:
Proiectul generat include un workflow GitHub Actions care publică la fiecare lansare:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Pentru alte sisteme CI (GitLab CI, CircleCI etc.), se aplică aceleași trei comenzi: `yarn install`, `npx twenty build`, apoi `npm publish` din `.twenty/output`.
Pentru alte sisteme CI (GitLab CI, CircleCI etc.), se aplică aceleași trei comenzi: `yarn install`, `yarn twenty build`, apoi `npm publish` din `.twenty/output`.
<Tip>
**npm provenance** este opțională, dar recomandată. Publicarea cu `--provenance` adaugă un badge de încredere la listarea ta în npm, permițând utilizatorilor să verifice că pachetul a fost construit dintr-un commit specific într-un pipeline CI public. Vezi [documentația npm provenance](https://docs.npmjs.com/generating-provenance-statements) pentru instrucțiuni de configurare.
</Tip>
## Distribuire internă
## Implementare pe un server (tarball)
Pentru aplicațiile pe care nu le dorești disponibile public — instrumente proprietare, integrări doar pentru enterprise sau build-uri experimentale — poți trimite un tarball direct pe un server Twenty.
Pentru aplicațiile pe care nu le dorești disponibile public — instrumente proprietare, integrări doar pentru enterprise sau build-uri experimentale — poți implementa un tarball direct pe un server Twenty.
### Trimite un tarball
### Cerințe
Construiește-ți aplicația și implementeaz-o pe un server specific, într-un singur pas:
Înainte de implementare, ai nevoie de un remote configurat care să indice serverul țintă. Remote-urile stochează local URL-ul serverului și credențialele de autentificare în `~/.twenty/config.json`.
Adaugă un remote:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Orice spațiu de lucru de pe acel server poate apoi instala și actualiza aplicația din pagina de setări **Applications**.
Pentru un server de dezvoltare local:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
Te poți autentifica și cu o cheie API pentru medii neinteractive:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Gestionează-ți remote-urile:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Implementare
Construiește și încarcă aplicația ta pe server într-un singur pas:
```bash filename="Terminal"
yarn twenty deploy
```
Aceasta construiește aplicația cu `--tarball`, apoi încarcă tarball-ul către remote-ul implicit printr-o încărcare multipart GraphQL.
Pentru a implementa către un remote specific:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Partajarea unei aplicații implementate
Aplicațiile tarball nu sunt listate în marketplace-ul public, astfel încât alte spații de lucru de pe același server nu le vor descoperi prin navigare. Pentru a partaja o aplicație implementată:
1. Mergi la **Setări > Aplicații > Înregistrări** și deschide aplicația ta
2. În fila **Distribuție**, fă clic pe **Copiază linkul de partajare**
3. Partajează acest link cu utilizatori din alte spații de lucru — îi duce direct la pagina de instalare a aplicației
Linkul de partajare folosește URL-ul de bază al serverului (fără niciun subdomeniu de spațiu de lucru), astfel încât funcționează pentru orice spațiu de lucru de pe server.
### Gestionarea versiunilor
Pentru a lansa o actualizare:
1. Actualizează câmpul `version` din `package.json`
2. Trimite un tarball nou cu `npx twenty publish --server <server-url>`
3. Spațiile de lucru de pe acel server vor vedea actualizarea disponibilă în setările lor
2. Rulează `yarn twenty deploy` (sau `yarn twenty deploy -r production`)
3. Spațiile de lucru care au aplicația instalată vor vedea actualizarea disponibilă în setările lor
<Note>
Aplicațiile interne sunt limitate la serverul pe care sunt trimise. Acestea nu vor apărea în marketplace-ul public și nu pot fi instalate de spațiile de lucru de pe alte servere.
</Note>
## Instalarea aplicațiilor
## Categorii de aplicații
După ce o aplicație este publicată (npm) sau implementată (tarball), spațiile de lucru o instalează prin interfața utilizatorului (UI):
```bash filename="Terminal"
yarn twenty install
```
Sau din pagina **Setări > Aplicații** din Twenty UI, unde pot fi navigate și instalate atât aplicațiile din marketplace, cât și cele implementate prin tarball.
## Categorii de distribuție a aplicațiilor
Twenty organizează aplicațiile în trei categorii, în funcție de modul în care sunt distribuite:
| Categorie | Cum funcționează | Vizibilă în marketplace? |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Dezvoltare** | Aplicații în modul de dezvoltare local, rulate prin `yarn twenty dev`. Folosite pentru construire și testare. | Nu |
| **Publicat** | Aplicații publicate pe npm cu prefixul `twenty-app-`. Listate în marketplace pentru ca orice spațiu de lucru să le poată instala. | Da |
| **Intern** | Aplicații implementate prin tarball pe un server specific. Disponibile doar pentru spațiile de lucru de pe acel server. | Nu |
| Categorie | Cum funcționează | Vizibilă în marketplace? |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Dezvoltare** | Aplicații în modul de dezvoltare local, rulate prin `yarn twenty dev`. Folosite pentru construire și testare. | Nu |
| **Publicat (npm)** | Aplicații publicate pe npm cu cuvântul cheie `twenty-app`. Listate în marketplace pentru ca orice spațiu de lucru să le poată instala. | Da |
| **Intern (tarball)** | Aplicații implementate prin tarball pe un server specific. Disponibile doar pentru spațiile de lucru de pe acel server printr-un link de partajare. | Nu |
<Tip>
Pornește în modul **Dezvoltare** în timp ce îți construiești aplicația. Când este gata, alege **Publicat** (npm) pentru distribuire largă sau **Intern** (tarball) pentru implementare privată.
</Tip>
## Referință CLI
| Comandă | Descriere | Opțiuni cheie |
| --------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- |
| `yarn twenty build` | Compilează aplicația și generează manifestul | `--tarball` — creează și un pachet `.tgz` |
| `yarn twenty publish` | Construiește și publică pe npm | `--tag <tag>` — dist-tag npm (de ex., `beta`, `next`) |
| `yarn twenty deploy` | Construiește și încarcă un tarball pe un server | `-r, --remote <name>` — remote țintă |
| `yarn twenty catalog-sync` | Declanșează sincronizarea catalogului marketplace-ului pe server | `-r, --remote <name>` — remote țintă |
| `yarn twenty install` | Instalează o aplicație implementată pe un spațiu de lucru | `-r, --remote <name>` — remote țintă |
| `yarn twenty dev` | Monitorizează și sincronizează modificările locale | Folosește remote-ul implicit |
| `yarn twenty remote add` | Adaugă o conexiune la server | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | Listează remote-urile configurate | — |
| `yarn twenty remote switch` | Setează remote-ul implicit | — |
| `yarn twenty remote status` | Afișează starea conexiunii | — |
| `yarn twenty remote remove` | Elimină un remote | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### Gestionarea serverului local
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker):
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker pe portul 2020). Aceste comenzi se aplică doar serverului de dezvoltare bazat pe Docker — nu gestionează o instanță Twenty pornită din sursă (de exemplu, `npx nx start twenty-server` pe portul 3000):
```bash filename="Terminal"
# Pornește serverul local (descarcă imaginea dacă este necesar)
@@ -15,19 +15,21 @@ description: Определяйте объекты, функции логики,
SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в [Обнаружение сущностей](/l/ru/developers/extend/apps/getting-started#entity-detection), вы должны использовать `export default define<Entity>({...})`, чтобы ваши сущности были обнаружены:
| Функция | Назначение |
| -------------------------------- | ------------------------------------------------------------------------ |
| `defineApplication` | Настройка метаданных приложения (обязательно, по одному на приложение) |
| `defineObject` | Определяет пользовательские объекты с полями |
| `defineLogicFunction` | Определение логических функций с обработчиками |
| `definePreInstallLogicFunction` | Определяет предустановочную логическую функцию (по одной на приложение) |
| `definePostInstallLogicFunction` | Определяет послеустановочную логическую функцию (по одной на приложение) |
| `defineFrontComponent` | Определение фронт-компонентов для настраиваемого интерфейса |
| `defineRole` | Настраивает права роли и доступ к объектам |
| `defineField` | Расширение существующих объектов дополнительными полями |
| `defineView` | Определяйте сохранённые представления для объектов |
| `defineNavigationMenuItem` | Определяйте ссылки боковой панели навигации |
| `defineSkill` | Определение навыков агента ИИ |
| Функция | Назначение |
| -------------------------------- | ----------------------------------------------------------------------------------------------- |
| `defineApplication` | Настройка метаданных приложения (обязательно, по одному на приложение) |
| `defineObject` | Определяет пользовательские объекты с полями |
| `defineField` | Расширяйте существующие объекты дополнительными полями или определяйте отдельные поля отношений |
| `defineLogicFunction` | Определение логических функций с обработчиками |
| `definePreInstallLogicFunction` | Определяет предустановочную логическую функцию (по одной на приложение) |
| `definePostInstallLogicFunction` | Определяет послеустановочную логическую функцию (по одной на приложение) |
| `defineFrontComponent` | Определение фронт-компонентов для настраиваемого интерфейса |
| `defineRole` | Настраивает права роли и доступ к объектам |
| `defineView` | Определяйте сохранённые представления для объектов |
| `defineNavigationMenuItem` | Определяйте ссылки боковой панели навигации |
| `defineSkill` | Определение навыков агента ИИ |
| `defineAgent` | Определяйте агентов ИИ |
| `definePageLayout` | Определяйте пользовательские макеты страниц |
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.
@@ -36,7 +38,7 @@ SDK предоставляет вспомогательные функции д
Пользовательские объекты описывают как схему, так и поведение записей в вашем рабочем пространстве. Используйте `defineObject()` для определения объектов со встроенной валидацией:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ export default defineObject({
* `universalIdentifier` должен быть уникальным и стабильным между развёртываниями.
* Каждому полю требуются `name`, `type`, `label` и собственный стабильный `universalIdentifier`.
* Массив `fields` необязателен — вы можете определять объекты без пользовательских полей.
* Вы можете сгенерировать новые объекты с помощью `yarn twenty entity:add`, который проведёт вас через настройку имени, полей и связей.
* Вы можете сгенерировать новые объекты с помощью `yarn twenty add`, который проведёт вас через выбор именования, полей и связей.
<Note>
**Базовые поля создаются автоматически.** Когда вы определяете пользовательский объект, Twenty автоматически добавляет стандартные поля,
@@ -120,6 +122,197 @@ export default defineObject({
но это не рекомендуется.
</Note>
### Определение полей для существующих объектов
Используйте `defineField()` для добавления полей к объектам, которые вам не принадлежат — например, к стандартным объектам Twenty (Person, Company и т. д.). или к объектам из других приложений. В отличие от встроенных полей в `defineObject()`, отдельные поля требуют `objectUniversalIdentifier`, чтобы указать, какой объект они расширяют:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Основные моменты:
* `objectUniversalIdentifier` определяет целевой объект. Для стандартных объектов используйте `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`, экспортируемые из `twenty-sdk`.
* При определении полей непосредственно в `defineObject()` вам не нужен `objectUniversalIdentifier` — он наследуется от родительского объекта.
* `defineField()` — единственный способ добавить поля к объектам, которые вы не создавали с помощью `defineObject()`.
### Связи
Отношения связывают объекты между собой. В Twenty отношения всегда двунаправленные — вы определяете обе стороны, и каждая сторона ссылается на другую.
Существуют два типа отношений:
| Тип отношения | Описание | Есть внешний ключ? |
| ------------- | --------------------------------------------------------------------- | ---------------------- |
| `MANY_TO_ONE` | Многие записи этого объекта указывают на одну запись целевого объекта | Да (`joinColumnName`) |
| `ONE_TO_MANY` | Одна запись этого объекта имеет много записей целевого объекта | Нет (обратная сторона) |
#### Как работают отношения
Каждое отношение требует **двух полей**, которые ссылаются друг на друга:
1. Сторона **MANY_TO_ONE** — находится в объекте, который содержит внешний ключ
2. Сторона **ONE_TO_MANY** — находится в объекте, которому принадлежит коллекция
Оба поля используют `FieldType.RELATION` и ссылаются друг на друга через `relationTargetFieldMetadataUniversalIdentifier`.
#### Пример: Почтовая открытка имеет много получателей
Предположим, `PostCard` может быть отправлен множству записей `PostCardRecipient`. Каждый получатель относится ровно к одной открытке.
**Шаг 1: Определите сторону ONE_TO_MANY на PostCard** (сторона "one"):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Шаг 2: Определите сторону MANY_TO_ONE на PostCardRecipient** (сторона "many" — содержит внешний ключ):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Циклические импорты:** Оба поля отношений ссылаются на `universalIdentifier` друг друга. Чтобы избежать проблем с циклическими импортами, экспортируйте идентификаторы полей как именованные константы из каждого файла и импортируйте их в другом файле. Система сборки разрешает это на этапе компиляции.
</Note>
#### Связывание со стандартными объектами
Чтобы создать отношение со встроенным объектом Twenty (Person, Company и т. д.), используйте `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### Свойства поля отношения
| Свойство | Обязательно | Описание |
| ------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------- |
| `type` | Да | Должно быть `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Да | `universalIdentifier` целевого объекта |
| `relationTargetFieldMetadataUniversalIdentifier` | Да | `universalIdentifier` соответствующего поля на целевом объекте |
| `universalSettings.relationType` | Да | `RelationType.MANY_TO_ONE` или `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Только для MANY_TO_ONE | Что происходит при удалении связанной записи: `CASCADE`, `SET_NULL`, `RESTRICT` или `NO_ACTION` |
| `universalSettings.joinColumnName` | Только для MANY_TO_ONE | Имя столбца базы данных для внешнего ключа (например, `postCardId`) |
#### Встроенные поля отношений в defineObject
Вы также можете определять поля отношений непосредственно внутри `defineObject()`. В этом случае опустите `objectUniversalIdentifier` — он наследуется от родительского объекта:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Конфигурация приложения (application-config.ts)
У каждого приложения есть единственный файл `application-config.ts`, который описывает:
@@ -161,6 +354,22 @@ export default defineApplication({
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
* Предустановочные и послеустановочные функции автоматически обнаруживаются во время сборки манифеста. См. [Предустановочные функции](#pre-install-functions) и [Послеустановочные функции](#post-install-functions).
#### Метаданные маркетплейса
Если вы планируете [опубликовать приложение](/l/ru/developers/extend/apps/publishing), эти необязательные поля определяют, как ваше приложение отображается в маркетплейсе:
| Поле | Описание |
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `author` | Имя автора или название компании |
| `category` | Категория приложения для фильтрации в маркетплейсе |
| `logoUrl` | Путь к логотипу вашего приложения (относительно `./assets/`) |
| `screenshots` | Массив путей к скриншотам (относительно `./assets/`) |
| `aboutDescription` | Расширенное описание в Markdown для вкладки "About". Если опущено, маркетплейс использует `README.md` пакета из npm |
| `websiteUrl` | Ссылка на ваш сайт |
| `termsUrl` | Ссылка на условия предоставления услуг |
| `emailSupport` | Адрес электронной почты поддержки |
| `issueReportUrl` | Ссылка на систему отслеживания проблем |
#### Роли и разрешения
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `defaultRoleUniversalIdentifier` в `application-config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
@@ -230,7 +439,7 @@ export default defineRole({
Каждый файл функции использует `defineLogicFunction()` для экспорта конфигурации с обработчиком и необязательными триггерами.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
Вы также можете вручную выполнить предустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Основные моменты:
@@ -334,7 +543,7 @@ yarn twenty function:execute --preInstall
* Для каждого приложения допускается только одна предустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
* Параметр `universalIdentifier` функции автоматически устанавливается как `preInstallLogicFunctionUniversalIdentifier` в манифесте приложения во время сборки — вам не нужно ссылаться на него в `defineApplication()`.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы обеспечить выполнение более длительных задач подготовки.
* Предустановочным функциям не нужны триггеры — платформа вызывает их перед установкой или вручную через `function:execute --preInstall`.
* Предустановочным функциям не нужны триггеры — платформа вызывает их перед установкой или вручную через `exec --preInstall`.
### Послеустановочные функции
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
Вы также можете вручную выполнить постустановочную функцию в любое время с помощью CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Основные моменты:
@@ -372,7 +581,7 @@ yarn twenty function:execute --postInstall
* Для каждого приложения допускается только одна послеустановочная функция. Сборка манифеста завершится ошибкой, если будет обнаружено более одной такой функции.
* Параметр `universalIdentifier` функции автоматически устанавливается как `postInstallLogicFunctionUniversalIdentifier` в манифесте приложения во время сборки — вам не нужно ссылаться на него в `defineApplication()`.
* Тайм-аут по умолчанию установлен на 300 секунд (5 минут), чтобы позволить выполнять более длительные задачи настройки, такие как инициализация данных.
* Постустановочным функциям не нужны триггеры — платформа вызывает их во время установки или вручную через `function:execute --postInstall`.
* Постустановочным функциям не нужны триггеры — платформа вызывает их во время установки или вручную через `exec --postInstall`.
### Полезная нагрузка триггера маршрута
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
Тип `RoutePayload` имеет следующую структуру:
| Свойство | Тип | Описание |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ |
| `headers` | `Record<string, string \| undefined>` | HTTP-заголовки (только перечисленные в `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Параметры строки запроса (несколько значений объединяются запятыми) |
| `pathParameters` | `Record<string, string \| undefined>` | Параметры пути, извлечённые из шаблона маршрута (например, `/users/:id` `{ id: '123' }`) |
| `body` | `object \| null` | Разобранное тело запроса (JSON) |
| `isBase64Encoded` | `логический тип` | Является ли тело закодированным в base64 |
| `requestContext.http.method` | `строка` | Метод HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `строка` | Необработанный путь запроса |
| Свойство | Тип | Описание |
| ---------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP-заголовки (только перечисленные в `forwardedRequestHeaders`) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Параметры строки запроса (несколько значений объединяются запятыми) |
| `pathParameters` | `Record<string, string \| undefined>` | Параметры пути, извлечённые из шаблона маршрута (например, `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Разобранное тело запроса (JSON) |
| `isBase64Encoded` | `логический тип` | Является ли тело закодированным в base64 |
| `requestContext.http.method` | `строка` | Метод HTTP (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `строка` | Необработанный путь запроса |
### Проброс HTTP-заголовков
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
Вы можете создать новые функции двумя способами:
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления новой функции логики. Это создаёт стартовый файл с обработчиком и конфигурацией.
* **Сгенерировано**: Запустите `yarn twenty add` и выберите опцию добавления новой логической функции. Это создаёт стартовый файл с обработчиком и конфигурацией.
* **Вручную**: Создайте новый файл `*.logic-function.ts` и используйте `defineLogicFunction()`, следуя тому же шаблону.
### Пометка логической функции как инструмента
@@ -478,7 +687,7 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ export default defineFrontComponent({
Вы можете создать новые фронт-компоненты двумя способами:
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
* **Сгенерировано**: Запустите `yarn twenty add` и выберите опцию добавления нового фронт-компонента.
* **Вручную**: Создайте новый файл `.tsx` и используйте `defineFrontComponent()`, следуя тому же шаблону.
### Навыки
@@ -602,47 +811,122 @@ export default defineSkill({
Вы можете создать новые навыки двумя способами:
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового навыка.
* **Сгенерировано**: Запустите `yarn twenty add` и выберите опцию добавления нового навыка.
* **Вручную**: Создайте новый файл и используйте `defineSkill()`, следуя тому же шаблону.
### Сгенерированные типизированные клиенты
### Типизированные клиенты API (`twenty-client-sdk`)
Два типизированных клиента автоматически генерируются с помощью `yarn twenty dev` и сохраняются в `node_modules/twenty-sdk/generated` на основе схемы вашего рабочего пространства:
Пакет `twenty-client-sdk` предоставляет два типизированных клиента GraphQL для взаимодействия с API Twenty из ваших логических функций и фронт-компонентов:
* **`CoreApiClient`** — выполняет запросы к конечной точке `/graphql` для получения данных рабочего пространства
* **`MetadataApiClient`** — выполняет запросы к эндпоинту `/metadata` для получения конфигурации рабочего пространства и загрузки файлов.
| Клиент | Импорт | Конечная точка | Генерируется? |
| ------------------- | ---------------------------- | ----------------------------------------------------------------- | -------------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — данные рабочего пространства (записи, объекты) | Да, на этапе dev/build |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — конфигурация рабочего пространства, загрузка файлов | Нет, поставляется в готовом виде |
#### CoreApiClient
`CoreApiClient` — основной клиент для запросов и изменений данных рабочего пространства. Он генерируется из схемы вашего рабочего пространства во время `yarn twenty dev` или `yarn twenty build`, поэтому он полностью типизирован в соответствии с вашими объектами и полями.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Оба клиента автоматически перегенерируются с помощью `yarn twenty dev` при изменении ваших объектов или полей.
Клиент использует синтаксис selection-set: передайте `true`, чтобы включить поле, используйте `__args` для аргументов и вкладывайте объекты для отношений. Вы получаете полное автодополнение и проверку типов на основе схемы вашего рабочего пространства.
#### Учётные данные времени выполнения в логических функциях
<Note>
**CoreApiClient генерируется на этапе dev/build.** Если вы попытаетесь использовать его, не запустив сначала `yarn twenty dev` или `yarn twenty build`, он выбросит ошибку. Генерация происходит автоматически — CLI анализирует GraphQL-схему вашего рабочего пространства, генерирует типизированный клиент с помощью `@genql/cli`, записывает сгенерированные исходники в `node_modules/twenty-client-sdk/dist/core/generated/` и заменяет заглушки в `node_modules/twenty-client-sdk/dist/core.mjs` и `node_modules/twenty-client-sdk/dist/core.cjs`.
</Note>
Когда ваша функция запускается на Twenty, платформа подставляет учётные данные как переменные окружения перед выполнением вашего кода:
#### Использование CoreSchema для аннотаций типов
* `TWENTY_API_URL`: Базовый URL API Twenty, на который нацелено ваше приложение.
* `TWENTY_API_KEY`: Краткоживущий ключ, ограниченный ролью функции по умолчанию вашего приложения.
`CoreSchema` предоставляет типы TypeScript, соответствующие объектам вашего рабочего пространства, что полезно для типизации состояния компонентов или параметров функций:
Заметки:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* Вам не нужно передавать URL или ключ API сгенерированному клиенту. Он читает `TWENTY_API_URL` и `TWENTY_API_KEY` из process.env во время выполнения.
* Права ключа API определяются ролью, на которую ссылается ваш `application-config.ts` через `defaultRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Предоставляйте только те права, которые нужны вашим функциям, затем укажите в `defaultRoleUniversalIdentifier` универсальный идентификатор этой роли.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` поставляется в готовом виде вместе с SDK (генерация не требуется). Он выполняет запросы к эндпоинту `/metadata` для получения конфигурации рабочего пространства, приложений и загрузки файлов:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Учётные данные времени выполнения
Когда ваш код выполняется на Twenty (логические функции или фронт-компоненты), платформа предоставляет учётные данные в виде переменных окружения:
* `TWENTY_API_URL` — базовый URL API Twenty
* `TWENTY_API_KEY` — краткоживущий ключ, ограниченный ролью функции по умолчанию вашего приложения
Вам не нужно передавать их клиентам — они автоматически читаются из `process.env`. Права ключа API определяются ролью, указанной в `defaultRoleUniversalIdentifier` в вашем `application-config.ts`.
#### Загрузка файлов
Сгенерированный `MetadataApiClient` включает метод `uploadFile` для прикрепления файлов к полям типа «файл» в объектах вашего рабочего пространства. Поскольку стандартные клиенты GraphQL изначально не поддерживают многочастовую загрузку файлов, клиент предоставляет специальный метод, который под капотом реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec).
`MetadataApiClient` включает метод `uploadFile` для прикрепления файлов к полям типа файла. Он реализует [спецификацию многочастных запросов GraphQL](https://github.com/jaydenseric/graphql-multipart-request-spec):
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
Сигнатура метода:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Параметр | Тип | Описание |
| ---------------------------------- | -------- | ------------------------------------------------------------------------ |
| `fileBuffer` | `Buffer` | Необработанное содержимое файла |
@@ -680,8 +953,7 @@ uploadFile(
Основные моменты:
* Метод `uploadFile` доступен в `MetadataApiClient`, потому что мутация загрузки обрабатывается эндпоинтом `/metadata`.
* Он использует `universalIdentifier` поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение — в соответствии с тем, как приложения ссылаются на поля повсюду.
* Он использует `universalIdentifier` поля (а не его идентификатор, специфичный для рабочего пространства), поэтому ваш код загрузки будет работать в любом рабочем пространстве, где установлено ваше приложение.
* Возвращаемый `url` — это подписанный URL, который можно использовать для доступа к загруженному файлу.
### Пример Hello World
@@ -9,18 +9,19 @@ description: Создайте своё первое приложение Twenty
Приложения позволяют расширять Twenty с помощью пользовательских объектов, полей, логических функций, навыков ИИ и UI-компонентов — всё это управляется как код.
**Что вы можете делать уже сегодня:**
**Что вы можете создать:**
* Определяйте пользовательские объекты и поля в виде кода (управляемая модель данных)
* Создавайте логические функции с пользовательскими триггерами (HTTP-маршруты, cron, события базы данных)
* Определяйте навыки для ИИ-агентов
* Создавайте фронтенд-компоненты, которые отображаются внутри интерфейса Twenty
* Развёртывайте одно и то же приложение в нескольких рабочих пространствах
* Пользовательские объекты, поля, представления и элементы навигации для формирования вашей модели данных
* Логические функции, запускаемые маршрутами HTTP, расписаниями cron или событиями базы данных
* Фронтенд-компоненты, которые непосредственно отображаются внутри интерфейса Twenty
* Навыки, расширяющие возможности ИИ-агентов Twenty
* Разверните приложение в нескольких рабочих пространствах
## Требования
* Node.js 24+ и Yarn 4
* Docker (для локального сервера разработки Twenty)
* Node.js 24+
* Yarn 4
* Docker (или запущенный локальный экземпляр Twenty)
## Начало работы
@@ -29,27 +30,15 @@ description: Создайте своё первое приложение Twenty
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Используйте параметр `--minimal`, чтобы создать минимальную установку
Отсюда вы можете:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ my-twenty-app/
В общих чертах:
* **package.json**: Объявляет имя приложения, версию, движки (Node 24+, Yarn 4) и добавляет `twenty-sdk`, а также скрипт `twenty`, который делегирует выполнение локальному CLI `twenty`. Выполните `yarn twenty help`, чтобы вывести список всех доступных команд.
* **.gitignore**: Игнорирует распространённые артефакты, такие как `node_modules`, `.yarn`, `generated/` (типизированный клиент), `dist/`, `build/`, каталоги coverage, файлы журналов и файлы `.env*`.
* **.gitignore**: Игнорирует распространённые артефакты, такие как `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, каталоги coverage, файлы журналов и файлы `.env*`.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Фиксируют и настраивают используемый в проекте инструментарий Yarn 4.
* **.nvmrc**: Фиксирует версию Node.js, ожидаемую проектом.
* **.oxlintrc.json** и **tsconfig.json**: Обеспечивают линтинг и конфигурацию TypeScript для исходников вашего приложения на TypeScript.
@@ -167,8 +156,8 @@ export default defineObject({
Позднее команды добавят больше файлов и папок:
* `yarn twenty dev` автоматически сгенерирует два типизированных клиента API в `node_modules/twenty-sdk/generated`: `CoreApiClient` (для данных рабочего пространства через `/graphql`) и `MetadataApiClient` (для конфигурации рабочего пространства и загрузки файлов через `/metadata`).
* `yarn twenty entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов, ролей, навыков и многого другого.
* `yarn twenty dev` автоматически сгенерирует типизированный `CoreApiClient` (для данных рабочего пространства через `/graphql`) в `node_modules/twenty-client-sdk/`. `MetadataApiClient` (для конфигурации рабочего пространства и загрузки файлов через `/metadata`) поставляется в предсобранном виде и доступен сразу. Импортируйте их из `twenty-client-sdk/core` и `twenty-client-sdk/metadata` соответственно.
* `yarn twenty add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов, ролей, навыков и многого другого.
## Аутентификация
@@ -12,7 +12,25 @@ description: Распространяйте своё приложение Twenty
После того как ваше приложение [собрано и протестировано локально](/l/ru/developers/extend/apps/building), у вас есть два пути для его распространения:
* **Опубликовать в npm** — разместите ваше приложение в маркетплейсе Twenty, чтобы любое рабочее пространство могло его найти и установить.
* **Отправить tarball** — разверните приложение на конкретном сервере Twenty для внутреннего использования, не делая его общедоступным.
* **Разверните tar-архив** — загрузите своё приложение напрямую на конкретный сервер Twenty для внутреннего или частного использования.
Оба пути начинаются с одного и того же шага **build**.
## Сборка вашего приложения
Команда `build` компилирует ваши исходники TypeScript, транспилирует функции логики и фронтенд-компоненты и генерирует `manifest.json`, который описывает содержимое вашего приложения:
```bash filename="Terminal"
yarn twenty build
```
Выходные данные записываются в `.twenty/output/`. Этот каталог содержит всё необходимое для распространения: скомпилированный код, ресурсы, манифест и копию вашего `package.json`.
Чтобы также создать tarball `.tgz` (который внутренне используется командой deploy или для ручного распространения):
```bash filename="Terminal"
yarn twenty build --tarball
```
## Публикация в npm
@@ -21,29 +39,76 @@ description: Распространяйте своё приложение Twenty
### Требования
* Учётная запись [npm](https://www.npmjs.com)
* Название вашего пакета **должно** использовать префикс `twenty-app-` (например, `twenty-app-postcard-sender`)
* Ключевое слово `twenty-app` **обязательно** должно быть указано в массиве `keywords` вашего `package.json`
### Добавление обязательного ключевого слова
Маркетплейс Twenty находит приложения, ища в реестре npm пакеты с ключевым словом `twenty-app`. Добавьте его в ваш `package.json`:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
Маркетплейс ищет в реестре npm по `keywords:twenty-app`. Без этого ключевого слова ваш пакет не появится в маркетплейсе, даже если в его имени есть префикс `twenty-app-`.
</Note>
### Шаги
1. **Соберите приложение** — CLI компилирует исходные файлы TypeScript и генерирует манифест приложения:
1. **Сборка вашего приложения:**
```bash filename="Terminal"
yarn twenty build
```
2. **Опубликуйте в npm** — отправьте собранный пакет в реестр npm:
2. **Публикация в npm:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Автоматическое обнаружение
Это выполняет `npm publish` из каталога `.twenty/output/`.
Пакеты с префиксом `twenty-app-` автоматически обнаруживаются каталогом маркетплейса Twenty. После публикации ваше приложение появится в маркетплейсе в течение нескольких минут — ручная регистрация или одобрение не требуются.
Чтобы опубликовать с определённым dist-tag (например, `beta` или `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### Как работает обнаружение приложений в маркетплейсе
Сервер Twenty синхронизирует каталог маркетплейса из реестра npm **каждый час**:
1. Он ищет все пакеты npm с ключевым словом `keywords:twenty-app`
2. Для каждого пакета он извлекает `manifest.json` с CDN npm
3. Метаданные приложения (name, description, author, logo, screenshots, category) извлекаются из манифеста и отображаются в маркетплейсе
После публикации может пройти до одного часа, прежде чем ваше приложение появится в маркетплейсе. Чтобы запустить синхронизацию немедленно, не дожидаясь следующего почасового запуска:
```bash filename="Terminal"
yarn twenty catalog-sync
```
Чтобы указать конкретный remote:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
Метаданные, отображаемые в маркетплейсе, берутся из вызова `defineApplication()` в исходном коде вашего приложения — из таких полей, как `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` и `termsUrl`.
<Note>
Если ваше приложение не определяет `aboutDescription` в `defineApplication()`, маркетплейс автоматически использует `README.md` вашего пакета из npm в качестве содержимого страницы «О приложении». Это означает, что вы можете поддерживать единый README как для npm, так и для маркетплейса Twenty. Если вы хотите другое описание в маркетплейсе, явно задайте `aboutDescription`.
</Note>
### Публикация через CI
Сгенерированный шаблоном проект включает workflow GitHub Actions, который выполняет публикацию при каждом релизе. Он запускает `app:build`, затем `npm publish --provenance` из результатов сборки:
Сгенерированный шаблоном проект включает рабочий процесс GitHub Actions, который выполняет публикацию при каждом релизе:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Для других CI-систем (GitLab CI, CircleCI и др.) применимы те же три команды: `yarn install`, `npx twenty build`, затем `npm publish` из `.twenty/output`.
Для других CI-систем (GitLab CI, CircleCI и др.) применимы те же три команды: `yarn install`, `yarn twenty build`, затем `npm publish` из `.twenty/output`.
<Tip>
**npm provenance** — опционально, но рекомендуется. Публикация с флагом `--provenance` добавляет к вашему пакету в npm значок доверия, позволяя пользователям проверить, что пакет был собран из конкретного коммита в общедоступном конвейере CI. См. инструкции по настройке в [документации по npm provenance](https://docs.npmjs.com/generating-provenance-statements).
</Tip>
## Внутреннее распространение
## Развертывание на сервер (tarball)
Для приложений, которые вы не хотите делать общедоступными — собственные инструменты, интеграции только для предприятия или экспериментальные сборки — вы можете отправить tarball напрямую на сервер Twenty.
Для приложений, которые вы не хотите делать общедоступными — собственные инструменты, интеграции только для предприятий или экспериментальные сборки — вы можете развернуть tarball напрямую на сервер Twenty.
### Отправка tarball
### Требования
Соберите приложение и разверните его на конкретном сервере одним шагом:
Перед развертыванием вам нужен настроенный remote, указывающий на целевой сервер. Remotes локально хранят URL сервера и учётные данные аутентификации в `~/.twenty/config.json`.
Добавьте remote:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Любое рабочее пространство на этом сервере сможет установить и обновить приложение на странице настроек **Applications**.
Для локального сервера разработки:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
Вы также можете аутентифицироваться с помощью API-ключа в неинтерактивных средах:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Управляйте remotes:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Развертывание
Соберите и загрузите ваше приложение на сервер в одном шаге:
```bash filename="Terminal"
yarn twenty deploy
```
Это собирает приложение с флагом `--tarball`, затем загружает tarball на remote по умолчанию через GraphQL multipart upload.
Чтобы развернуть на конкретный remote:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Общий доступ к развернутому приложению
Приложения в формате tarball не отображаются в публичном маркетплейсе, поэтому другие рабочие пространства на том же сервере не найдут их при просмотре. Чтобы поделиться развернутым приложением:
1. Перейдите в **Настройки > Приложения > Регистрации** и откройте ваше приложение
2. На вкладке **Распространение** нажмите **Копировать ссылку для общего доступа**
3. Поделитесь этой ссылкой с пользователями в других рабочих пространствах — она ведёт их прямо на страницу установки приложения
Ссылка общего доступа использует базовый URL сервера (без какого-либо поддомена рабочего пространства), поэтому она работает для любого рабочего пространства на сервере.
### Управление версиями
Чтобы выпустить обновление:
1. Обновите значение поля `version` в файле `package.json`
2. Отправьте новый tarball командой `npx twenty publish --server <server-url>`
3. Рабочие пространства на этом сервере увидят доступное обновление в своих настройках
2. Выполните `yarn twenty deploy` (или `yarn twenty deploy -r production`)
3. Рабочие пространства, в которых установлено приложение, увидят доступное обновление в своих настройках
<Note>
Внутренние приложения ограничены сервером, на который они отправлены. Они не появятся в публичном маркетплейсе и не могут быть установлены рабочими пространствами на других серверах.
</Note>
## Установка приложений
## Категории приложений
После публикации приложения (npm) или его развертывания (tarball) рабочие пространства устанавливают его через интерфейс:
```bash filename="Terminal"
yarn twenty install
```
Или со страницы **Настройки > Приложения** в интерфейсе Twenty, где можно просматривать и устанавливать как приложения из маркетплейса, так и развернутые через tarball.
## Категории распространения приложений
Twenty группирует приложения в три категории в зависимости от способа их распространения:
| Категория | Как это работает | Отображается в маркетплейсе? |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| **Разработка** | Локальные приложения в режиме разработки, запущенные через `yarn twenty dev`. Используются для сборки и тестирования. | Нет |
| **Опубликовано** | Приложения, опубликованные в npm с префиксом `twenty-app-`. Отображаются в маркетплейсе, доступные для установки любому рабочему пространству. | Да |
| **Внутренний** | Приложения, развернутые через tarball на конкретном сервере. Доступны только рабочим пространствам на этом сервере. | Нет |
| Категория | Как это работает | Отображается в маркетплейсе? |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| **Разработка** | Локальные приложения в режиме разработки, запущенные через `yarn twenty dev`. Используются для сборки и тестирования. | Нет |
| **Опубликовано (npm)** | Приложения, опубликованные в npm с ключевым словом `twenty-app`. Отображаются в маркетплейсе, доступные для установки любому рабочему пространству. | Да |
| **Внутренние (tarball)** | Приложения, развернутые через tarball на конкретном сервере. Доступны только рабочим пространствам на этом сервере по ссылке общего доступа. | Нет |
<Tip>
Начните в режиме **Разработка** во время создания приложения. Когда будет готово, выберите **Опубликовано** (npm) для широкого распространения или **Внутренний** (tarball) для приватного развертывания.
</Tip>
## Справочник по CLI
| Команда | Описание | Основные флаги |
| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------- |
| `yarn twenty build` | Скомпилировать приложение и сгенерировать манифест | `--tarball` — также создать пакет `.tgz` |
| `yarn twenty publish` | Собрать и опубликовать в npm | `--tag <tag>` — dist-tag npm (например, `beta`, `next`) |
| `yarn twenty deploy` | Собрать и загрузить tarball на сервер | `-r, --remote <name>` — целевой удалённый репозиторий |
| `yarn twenty catalog-sync` | Запустить на сервере синхронизацию каталога маркетплейса | `-r, --remote <name>` — целевой удалённый репозиторий |
| `yarn twenty install` | Установить развернутое приложение в рабочем пространстве | `-r, --remote <name>` — целевой remote |
| `yarn twenty dev` | Отслеживать и синхронизировать локальные изменения | Использует remote по умолчанию |
| `yarn twenty remote add` | Добавить подключение к серверу | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | Показать настроенные remotes | — |
| `yarn twenty remote switch` | Установить remote по умолчанию | — |
| `yarn twenty remote status` | Показать статус подключения | — |
| `yarn twenty remote remove` | Удалить remote | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### Управление локальным сервером
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером):
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером на порту 2020). Эти команды применимы только к серверу разработки на базе Docker — они не управляют экземпляром Twenty, запущенным из исходного кода (например, `npx nx start twenty-server` на порту 3000):
```bash filename="Terminal"
# Запустить локальный сервер (при необходимости будет загружен образ)
@@ -15,19 +15,21 @@ twenty-sdk, uygulamanız içinde kullandığınız türlendirilmiş yapı taşla
SDK, uygulama varlıklarınızı tanımlamak için yardımcı fonksiyonlar sağlar. [Varlık algılama](/l/tr/developers/extend/apps/getting-started#entity-detection) bölümünde açıklandığı gibi, varlıklarınızın algılanması için `export default define<Entity>({...})` kullanmalısınız:
| Fonksiyon | Amaç |
| -------------------------------- | ------------------------------------------------------------------------- |
| `defineApplication` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
| `defineObject` | Alanlara sahip özel nesneler tanımlayın |
| `defineLogicFunction` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `definePreInstallLogicFunction` | Bir kurulum öncesi mantık işlevi tanımlayın (uygulama başına bir adet) |
| `definePostInstallLogicFunction` | Bir kurulum sonrası mantık işlevi tanımlayın (uygulama başına bir adet) |
| `defineFrontComponent` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
| `defineRole` | Rol izinlerini ve nesne erişimini yapılandırın |
| `defineField` | Mevcut nesneleri ek alanlarla genişletin |
| `defineView` | Nesneler için kaydedilmiş görünümler tanımlayın |
| `defineNavigationMenuItem` | Kenar çubuğu gezinme bağlantılarını tanımlayın |
| `defineSkill` | Yapay zekâ ajanı yeteneklerini tanımlayın |
| Fonksiyon | Amaç |
| -------------------------------- | ---------------------------------------------------------------------------------- |
| `defineApplication` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
| `defineObject` | Alanlara sahip özel nesneler tanımlayın |
| `defineField` | Mevcut nesneleri ek alanlarla genişletin veya bağımsız ilişki alanları tanımlayın. |
| `defineLogicFunction` | İşleyicilerle mantık fonksiyonları tanımlayın |
| `definePreInstallLogicFunction` | Bir kurulum öncesi mantık işlevi tanımlayın (uygulama başına bir adet) |
| `definePostInstallLogicFunction` | Bir kurulum sonrası mantık işlevi tanımlayın (uygulama başına bir adet) |
| `defineFrontComponent` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
| `defineRole` | Rol izinlerini ve nesne erişimini yapılandırın |
| `defineView` | Nesneler için kaydedilmiş görünümler tanımlayın |
| `defineNavigationMenuItem` | Kenar çubuğu gezinme bağlantılarını tanımlayın |
| `defineSkill` | Yapay zekâ ajanı yeteneklerini tanımlayın |
| `defineAgent` | Yapay zekâ ajanlarını tanımlayın. |
| `definePageLayout` | Özel sayfa düzenlerini tanımlayın. |
Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
@@ -36,7 +38,7 @@ Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomat
Özel nesneler, çalışma alanınızdaki kayıtlar için hem şemayı hem de davranışı tanımlar. Yerleşik doğrulamayla nesneler tanımlamak için `defineObject()` kullanın:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ export default defineObject({
* `universalIdentifier` dağıtımlar arasında benzersiz ve kararlı olmalıdır.
* Her alan bir `name`, `type`, `label` ve kendi kararlı `universalIdentifier` değerini gerektirir.
* `fields` dizisi isteğe bağlıdır — özel alanlar olmadan da nesneler tanımlayabilirsiniz.
* `yarn twenty entity:add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
* `yarn twenty add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
<Note>
**Temel alanlar otomatik olarak oluşturulur.** Özel bir nesne tanımladığınızda Twenty, standart alanları otomatik olarak ekler
@@ -120,6 +122,197 @@ Bunları `fields` dizinizde tanımlamanız gerekmez — yalnızca özel alanlar
ancak bu önerilmez.
</Note>
### Mevcut nesneler üzerinde alanları tanımlama
Sahibi olmadığınız nesnelere alan eklemek için `defineField()` kullanın — standart Twenty nesneleri (Person, Company, vb.) gibi. veya diğer uygulamalardaki nesneler. `defineObject()` içindeki satır içi alanların aksine, bağımsız alanlar hangi nesneyi genişlettiklerini belirtmek için bir `objectUniversalIdentifier` gerektirir:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
Önemli noktalar:
* `objectUniversalIdentifier` hedef nesneyi tanımlar. Standart nesneler için, `twenty-sdk`'den dışa aktarılan `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`'ı kullanın.
* Alanları `defineObject()` içinde satır içi tanımlarken, `objectUniversalIdentifier`'a ihtiyacınız yoktur — üst nesneden devralınır.
* `defineField()`, `defineObject()` ile oluşturmadığınız nesnelere alan eklemenin tek yoludur.
### İlişkiler
İlişkiler nesneleri birbirine bağlar. Twenty'de ilişkiler her zaman **çift yönlüdür** — her iki tarafı da tanımlarsınız ve her taraf diğerine başvurur.
İki ilişki türü vardır:
| İlişki türü | Açıklama | Yabancı anahtar var mı? |
| ------------- | --------------------------------------------------------- | ----------------------- |
| `MANY_TO_ONE` | Bu nesnenin birçok kaydı, hedefin bir kaydını işaret eder | Evet (`joinColumnName`) |
| `ONE_TO_MANY` | Bu nesnenin bir kaydı, hedefin birçok kaydına sahiptir | Hayır (ters taraf) |
#### İlişkiler nasıl çalışır
Her ilişki, birbirine referans veren iki alan gerektirir:
1. **MANY_TO_ONE** tarafı — yabancı anahtarı tutan nesne üzerinde bulunur
2. **ONE_TO_MANY** tarafı — koleksiyona sahip olan nesne üzerinde bulunur
Her iki alan da `FieldType.RELATION` kullanır ve `relationTargetFieldMetadataUniversalIdentifier` aracılığıyla birbirine karşılıklı referans verir.
#### Örnek: Posta Kartı'nın birçok Alıcısı vardır
Bir `PostCard`'ın birçok `PostCardRecipient` kaydına gönderilebildiğini varsayalım. Her alıcı tam olarak bir posta kartına aittir.
**Adım 1: PostCard üzerinde ONE_TO_MANY tarafını tanımlayın** ("bir" taraf):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**Adım 2: PostCardRecipient üzerinde MANY_TO_ONE tarafını tanımlayın** ("çok" taraf — yabancı anahtarı tutar):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
**Döngüsel içe aktarmalar:** Her iki ilişki alanı da birbirlerinin `universalIdentifier` değerine referans verir. Döngüsel içe aktarma sorunlarından kaçınmak için, alan kimliklerinizi her dosyadan adlandırılmış sabitler olarak dışa aktarın ve diğer dosyada içe aktarın. Derleme sistemi bunları derleme zamanında çözer.
</Note>
#### Standart nesnelerle ilişkilendirme
Yerleşik bir Twenty nesnesiyle (Person, Company, vb.) ilişki oluşturmak için `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` kullanın:
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### İlişki alanı özellikleri
| Özellik | Zorunlu | Açıklama |
| ------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
| `type` | Evet | `FieldType.RELATION` olmalıdır |
| `relationTargetObjectMetadataUniversalIdentifier` | Evet | Hedef nesnenin `universalIdentifier` değeri |
| `relationTargetFieldMetadataUniversalIdentifier` | Evet | Hedef nesnedeki eşleşen alanın `universalIdentifier` değeri |
| `universalSettings.relationType` | Evet | `RelationType.MANY_TO_ONE` veya `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | Yalnızca MANY_TO_ONE | Başvurulan kayıt silindiğinde ne olacağı: `CASCADE`, `SET_NULL`, `RESTRICT` veya `NO_ACTION` |
| `universalSettings.joinColumnName` | Yalnızca MANY_TO_ONE | Yabancı anahtar için veritabanı sütun adı (örn. `postCardId`) |
#### defineObject içinde satır içi ilişki alanları
İlişki alanlarını doğrudan `defineObject()` içinde de tanımlayabilirsiniz. Bu durumda, `objectUniversalIdentifier`'ı atlayın — üst nesneden devralınır:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### Uygulama yapılandırması (application-config.ts)
Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` dosyası vardır:
@@ -161,6 +354,22 @@ Notlar:
* `defaultRoleUniversalIdentifier`, rol dosyasıyla eşleşmelidir (aşağıya bakın).
* Kurulum öncesi ve kurulum sonrası işlevler, manifest oluşturma sırasında otomatik olarak algılanır. Bkz. [Kurulum öncesi işlevler](#pre-install-functions) ve [Kurulum sonrası işlevler](#post-install-functions).
#### Pazaryeri meta verileri
Eğer [uygulamanızı yayımlamayı](/l/tr/developers/extend/apps/publishing) planlıyorsanız, bu isteğe bağlı alanlar uygulamanızın pazaryerinde nasıl görüneceğini kontrol eder:
| Alan | Açıklama |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Yazar veya şirket adı |
| `category` | Pazaryerinde filtreleme için uygulama kategorisi |
| `logoUrl` | Uygulama logonuzun yolu (`./assets/` dizinine göre) |
| `screenshots` | Ekran görüntüsü yollarının dizisi (`./assets/` dizinine göre) |
| `aboutDescription` | "Hakkında" sekmesi için daha uzun bir markdown açıklaması. Belirtilmezse, pazaryeri npm'deki paketin `README.md` dosyasını kullanır |
| `websiteUrl` | Web sitenize bağlantı |
| `termsUrl` | Hizmet Koşulları'na bağlantı |
| `emailSupport` | Destek e-posta adresi |
| `issueReportUrl` | Sorun izleyicisine bağlantı |
#### Roller ve izinler
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application-config.ts` içindeki `defaultRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonlarının kullandığı varsayılan rolü belirtir.
@@ -220,7 +429,7 @@ Bu rolün `universalIdentifier` değeri daha sonra `application-config.ts` için
Notlar:
* Oluşturulan rol ile başlayın ve en az ayrıcalık ilkesini izleyerek aşamalı olarak kısıtlayın.
* Oluşturulan rolden başlayın ve en az ayrıcalık ilkesini izleyerek bunu aşamalı olarak kısıtlayın.
* `objectPermissions` ve `fieldPermissions` değerlerini, fonksiyonlarınızın ihtiyaç duyduğu nesneler/alanlarla değiştirin.
* `permissionFlags`, platform düzeyindeki yeteneklere erişimi kontrol eder. Minimumda tutun; yalnızca ihtiyacınız olanları ekleyin.
* Çalışan bir örneği Hello World uygulamasında görün: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
@@ -230,7 +439,7 @@ Notlar:
Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren bir yapılandırmayı dışa aktarmak için `defineLogicFunction()` kullanır.
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
Ayrıca kurulum öncesi işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
Önemli noktalar:
@@ -334,7 +543,7 @@ yarn twenty function:execute --preInstall
* Uygulama başına yalnızca bir kurulum öncesi işlevine izin verilir. Birden fazla tespit edilirse manifest oluşturma hataya düşer.
* İşlevin `universalIdentifier` değeri, oluşturma sırasında uygulama manifestinde otomatik olarak `preInstallLogicFunctionUniversalIdentifier` olarak ayarlanır — `defineApplication()` içinde buna atıfta bulunmanıza gerek yoktur.
* Varsayılan zaman aşımı, daha uzun hazırlık görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
* Kurulum öncesi işlevlerin tetikleyicilere ihtiyacı yoktur — kurulumdan önce platform tarafından veya `function:execute --preInstall` aracılığıyla manuel olarak çağrılırlar.
* Kurulum öncesi işlevlerin tetikleyicilere ihtiyacı yoktur — kurulumdan önce platform tarafından veya `exec --preInstall` aracılığıyla manuel olarak çağrılırlar.
### Kurulum sonrası işlevler
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
Ayrıca kurulum sonrası işlevi istediğiniz zaman CLI kullanarak manuel olarak çalıştırabilirsiniz:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
Önemli noktalar:
@@ -372,7 +581,7 @@ yarn twenty function:execute --postInstall
* Uygulama başına yalnızca bir kurulum sonrası işlevine izin verilir. Birden fazla tespit edilirse manifest oluşturma hataya düşer.
* İşlevin `universalIdentifier` değeri, oluşturma sırasında uygulama manifestinde otomatik olarak `postInstallLogicFunctionUniversalIdentifier` olarak ayarlanır — `defineApplication()` içinde buna atıfta bulunmanıza gerek yoktur.
* Varsayılan zaman aşımı, veri tohumlama gibi daha uzun kurulum görevlerine izin vermek için 300 saniye (5 dakika) olarak ayarlanmıştır.
* Kurulum sonrası işlevlerin tetikleyicilere ihtiyacı yoktur — kurulum sırasında platform tarafından veya `function:execute --postInstall` aracılığıyla manuel olarak çağrılırlar.
* Kurulum sonrası işlevlerin tetikleyicilere ihtiyacı yoktur — kurulum sırasında platform tarafından veya `exec --postInstall` aracılığıyla manuel olarak çağrılırlar.
### Rota tetikleyicisi yükü
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
`RoutePayload` türünün yapısı şu şekildedir:
| Özellik | Tür | Açıklama |
| ---------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP başlıkları (`forwardedRequestHeaders` içinde listelenenlerle sınırlı) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Sorgu dizesi parametreleri (birden çok değer virgülle birleştirilir) |
| `pathParameters` | `Record<string, string \| undefined>` | Rota deseninden çıkarılan yol parametreleri (örn., `/users/:id` `{ id: '123' }`) |
| `body` | `object \| null` | Ayrıştırılmış istek gövdesi (JSON) |
| `isBase64Encoded` | `boolean` | Gövdenin base64 ile kodlanıp kodlanmadığı |
| `requestContext.http.method` | `string` | HTTP yöntemi (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Ham istek yolu |
| Özellik | Tür | Açıklama |
| ---------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP başlıkları (`forwardedRequestHeaders` içinde listelenenlerle sınırlı) |
| `queryStringParameters` | `Record<string, string \| undefined>` | Sorgu dizesi parametreleri (birden çok değer virgülle birleştirilir) |
| `pathParameters` | `Record<string, string \| undefined>` | Rota deseninden çıkarılan yol parametreleri (örn., `/users/:id` -> `{ id: '123' }`) |
| `body` | `object \| null` | Ayrıştırılmış istek gövdesi (JSON) |
| `isBase64Encoded` | `boolean` | Gövdenin base64 ile kodlanıp kodlanmadığı |
| `requestContext.http.method` | `string` | HTTP yöntemi (GET, POST, PUT, PATCH, DELETE) |
| `requestContext.http.path` | `string` | Ham istek yolu |
### HTTP başlıklarını iletme
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
Yeni fonksiyonları iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir mantık fonksiyonu ekleme seçeneğini seçin. Bu, bir işleyici ve yapılandırma içeren bir başlangıç dosyası oluşturur.
* **Şablondan**: `yarn twenty add` çalıştırın ve yeni bir mantık fonksiyonu ekleme seçeneğini seçin. Bu, bir işleyici ve yapılandırma içeren bir başlangıç dosyası oluşturur.
* **Manuel**: Yeni bir `*.logic-function.ts` dosyası oluşturun ve aynı deseni izleyerek `defineLogicFunction()` kullanın.
### Bir mantık işlevini araç olarak işaretleme
@@ -478,7 +687,7 @@ Bir mantık işlevini bir araç olarak işaretlemek için `isTool: true` olarak
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ export default defineFrontComponent({
Yeni ön uç bileşenlerini iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir ön uç bileşeni ekleme seçeneğini seçin.
* **Şablondan**: `yarn twenty add` çalıştırın ve yeni bir ön uç bileşeni ekleme seçeneğini seçin.
* **Manuel**: Aynı deseni izleyerek yeni bir `.tsx` dosyası oluşturun ve `defineFrontComponent()` kullanın.
### Beceriler
@@ -602,47 +811,122 @@ export default defineSkill({
Yeni yetenekleri iki şekilde oluşturabilirsiniz:
* **Şablondan**: `yarn twenty entity:add` komutunu çalıştırın ve yeni bir yetenek ekleme seçeneğini seçin.
* **Şablondan**: `yarn twenty add` çalıştırın ve yeni bir yetenek ekleme seçeneğini seçin.
* **Manuel**: Yeni bir dosya oluşturun ve aynı deseni izleyerek `defineSkill()` kullanın.
### Oluşturulan tiplendirilmiş istemciler
### Tipli API istemcileri (`twenty-client-sdk`)
Çalışma alanı şemanıza göre `yarn twenty dev` tarafından iki tiplendirilmiş istemci otomatik olarak oluşturulur ve `node_modules/twenty-sdk/generated` içine kaydedilir:
`twenty-client-sdk` paketi, mantık fonksiyonlarınızdan ve ön uç bileşenlerinizden Twenty API ile etkileşim kurmak için iki tipli GraphQL istemcisi sağlar:
* **`CoreApiClient`** — çalışma alanı verileri için `/graphql` uç noktasını sorgular
* **`MetadataApiClient`** — çalışma alanı yapılandırması ve dosya yüklemeleri için `/metadata` uç noktasını sorgular.
| İstemci | İçe Aktar | Uç nokta | Oluşturuldu mu? |
| ------------------- | ---------------------------- | ------------------------------------------------------------- | --------------------------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — çalışma alanı verileri (kayıtlar, nesneler) | Evet, geliştirme/derleme zamanında |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — çalışma alanı yapılandırması, dosya yüklemeleri | Hayır, önceden hazırlanmış olarak gelir |
#### CoreApiClient
`CoreApiClient`, çalışma alanı verilerini sorgulamak ve değiştirmek için ana istemcidir. `yarn twenty dev` veya `yarn twenty build` sırasında çalışma alanı şemanızdan oluşturulur; bu nedenle nesnelerinize ve alanlarınıza uyacak şekilde tamamen tiplendirilmiştir.
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
Her iki istemci de, nesneleriniz veya alanlarınız değiştiğinde, `yarn twenty dev` tarafından otomatik olarak yeniden oluşturulur.
İstemci bir seçim kümesi sözdizimi kullanır: Bir alanı dahil etmek için `true` geçin, bağımsız değişkenler için `__args` kullanın ve ilişkiler için nesneleri iç içe yerleştirin. Çalışma alanı şemanıza göre tam otomatik tamamlama ve tip denetimi elde edersiniz.
#### Mantık fonksiyonlarında çalışma zamanı kimlik bilgileri
<Note>
**CoreApiClient geliştirme/derleme zamanında oluşturulur.** Bunu önce `yarn twenty dev` veya `yarn twenty build` çalıştırmadan kullanmaya çalışırsanız, hata verir. Oluşturma otomatik olarak gerçekleşir — CLI, çalışma alanınızın GraphQL şemasını inceler, `@genql/cli` kullanarak tipli bir istemci üretir, üretilen kaynakları `node_modules/twenty-client-sdk/dist/core/generated/` dizinine yazar ve `node_modules/twenty-client-sdk/dist/core.mjs` ile `node_modules/twenty-client-sdk/dist/core.cjs` içindeki taslakları değiştirir.
</Note>
Fonksiyonunuz Twenty üzerinde çalıştığında, platform kodunuz yürütülmeden önce kimlik bilgilerini ortam değişkenleri olarak enjekte eder:
#### Tür açıklamaları için CoreSchema'yı kullanma
* `TWENTY_API_URL`: Uygulamanızın hedeflediği Twenty API'nin temel URLsi.
* `TWENTY_API_KEY`: Uygulamanızın varsayılan fonksiyon rolü kapsamına sahip kısa ömürlü anahtar.
`CoreSchema`, çalışma alanı nesnelerinize uyan TypeScript türleri sağlar; bileşen durumunu veya işlev parametrelerini tiplemek için kullanışlıdır:
Notlar:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* Oluşturulan istemciye URL veya API anahtarı geçirmeniz gerekmez. Çalışma zamanında `TWENTY_API_URL` ve `TWENTY_API_KEY` değerlerini process.env üzerinden okur.
* API anahtarının izinleri, `application-config.ts` içinde `defaultRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `defaultRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient`, SDK ile birlikte önceden hazırlanmış olarak gelir (oluşturma gerektirmez). Çalışma alanı yapılandırması, uygulamalar ve dosya yüklemeleri için `/metadata` uç noktasını sorgular:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### Çalışma zamanı kimlik bilgileri
Kodunuz Twenty üzerinde çalıştığında (mantık işlevleri veya ön uç bileşenleri), platform kimlik bilgilerini ortam değişkenleri olarak enjekte eder:
* `TWENTY_API_URL` — Twenty API'nin temel URL'si
* `TWENTY_API_KEY` — Uygulamanızın varsayılan fonksiyon rolü kapsamına sahip kısa ömürlü anahtar
Bunları istemcilere iletmeniz gerekmez — otomatik olarak `process.env`'den okurlar. API anahtarının izinleri, `application-config.ts` içinde `defaultRoleUniversalIdentifier` ile referans verilen role göre belirlenir.
#### Dosya yükleme
Oluşturulan `MetadataApiClient`, çalışma alanı nesnelerinizdeki dosya türündeki alanlara dosya eklemek için bir `uploadFile` yöntemi içerir. Standart GraphQL istemcileri çok parçalı dosya yüklemelerini yerel olarak desteklemediğinden, istemci arka planda [GraphQL çok parçalı istek belirtimi](https://github.com/jaydenseric/graphql-multipart-request-spec) uygulayan bu özel yöntemi sağlar.
`MetadataApiClient`, dosya türü alanlara dosya eklemek için bir `uploadFile` yöntemi içerir. [GraphQL çok parçalı istek spesifikasyonunu](https://github.com/jaydenseric/graphql-multipart-request-spec) uygular:
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
Yöntem imzası:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| Parametre | Tür | Açıklama |
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------------ |
| `fileBuffer` | `Buffer` | Dosyanın ham içeriği |
@@ -680,8 +953,7 @@ uploadFile(
Önemli noktalar:
* `uploadFile` yöntemi, yükleme mutasyonu `/metadata` uç noktası tarafından çözümlendiği için `MetadataApiClient` üzerinde mevcuttur.
* Alan için `universalIdentifier` kullanılır (çalışma alanına özgü kimliği değil), böylece yükleme kodunuz uygulamanızın yüklü olduğu herhangi bir çalışma alanında çalışır — uygulamaların başka her yerde alanlara nasıl atıfta bulunduğuyla tutarlıdır.
* Alan için `universalIdentifier` kullanır (çalışma alanına özgü kimliği değil), böylece yükleme kodunuz uygulamanızın yüklü olduğu herhangi bir çalışma alanında çalışır.
* Döndürülen `url`, yüklenen dosyaya erişmek için kullanabileceğiniz imzalı bir URL'dir.
### Hello World örneği
@@ -9,18 +9,19 @@ Uygulamalar şu anda alfa testinde. Özellik işlevsel ancak hâlâ gelişmekte.
Uygulamalar, Twenty'yi özel nesneler, alanlar, mantık işlevleri, Yapay Zeka yetenekleri ve UI bileşenleriyle genişletmenizi sağlar — tümü kod olarak yönetilir.
**Bugün Yapabilecekleriniz:**
**Oluşturabilecekleriniz:**
* Özel nesneleri ve alanları kod olarak tanımlayın (yönetilen veri modeli)
* Özel tetikleyicilerle mantık işlevleri oluşturun (HTTP routes, cron, database events)
* Yapay zekâ ajanları için becerileri tanımlayın
* Twenty'nin UI'si içinde görüntülenen ön bileşenler oluşturun
* Aynı uygulamayı birden çok çalışma alanına dağıtın
* Veri modelinizi şekillendirmek için özel nesneler, alanlar, görünümler ve gezinti öğeleri
* HTTP rotaları, cron zamanlamaları veya veritabanı olayları tarafından tetiklenen mantık işlevleri
* Twenty'nin UI'si içinde doğrudan görüntülenen ön uç bileşenleri
* Twenty'nin yapay zeka ajanlarını genişleten beceriler
* Bir uygulamayı birden çok çalışma alanına dağıtın
## Ön Gereksinimler
* Node.js 24+ ve Yarn 4
* Docker (yerel Twenty geliştirme sunucusu için)
* Node.js 24+
* Yarn 4
* Docker (veya çalışan yerel bir Twenty örneği)
## Başlarken
@@ -29,27 +30,15 @@ Resmi iskelet oluşturucu aracını kullanarak yeni bir uygulama oluşturun, ard
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> Minimal bir kurulum iskeleti oluşturmak için `--minimal` seçeneğini kullanın
Buradan şunları yapabilirsiniz:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ my-twenty-app/
Genel hatlarıyla:
* **package.json**: Uygulama adını, sürümünü, motorları (Node 24+, Yarn 4) bildirir ve `twenty-sdk` ile yerel `twenty` CLI'sine yetki devreden bir `twenty` betiği ekler. Tüm mevcut komutları listelemek için `yarn twenty help` komutunu çalıştırın.
* **.gitignore**: `node_modules`, `.yarn`, `generated/` (türlendirilmiş istemci), `dist/`, `build/`, kapsam klasörleri, günlük dosyaları ve `.env*` dosyaları gibi yaygın artifaktları yok sayar.
* **.gitignore**: `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, kapsam klasörleri, günlük dosyaları ve `.env*` dosyaları gibi yaygın artifaktları yok sayar.
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Proje tarafından kullanılan Yarn 4 araç zincirini kilitler ve yapılandırır.
* **.nvmrc**: Projenin beklediği Node.js sürümünü sabitler.
* **.oxlintrc.json** ve **tsconfig.json**: Uygulamanızın TypeScript kaynakları için linting ve TypeScript yapılandırması sağlar.
@@ -167,8 +156,8 @@ export default defineObject({
İlerideki komutlar daha fazla dosya ve klasör ekleyecektir:
* `yarn twenty dev`, `node_modules/twenty-sdk/generated` içinde iki tiplendirilmiş API istemcisini otomatik olarak oluşturur: `CoreApiClient` (`/graphql` üzerinden çalışma alanı verileri için) ve `MetadataApiClient` (çalışma alanı yapılandırması ve `/metadata` üzerinden dosya yüklemeleri için).
* `yarn twenty entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz, rolleriniz, yetenekleriniz ve daha fazlası için `src/` altında varlık tanım dosyaları ekler.
* `yarn twenty dev`, türlendirilmiş `CoreApiClient`'i (çalışma alanı verileri için `/graphql` aracılığıyla) `node_modules/twenty-client-sdk/` içine otomatik olarak oluşturur. `MetadataApiClient` (çalışma alanı yapılandırması ve dosya yüklemeleri için `/metadata` aracılığıyla) önceden derlenmiş olarak gelir ve hemen kullanılabilir. Bunları sırasıyla `twenty-client-sdk/core` ve `twenty-client-sdk/metadata` inden içe aktarın.
* `yarn twenty add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz, rolleriniz, yetenekleriniz ve daha fazlası için `src/` altında varlık tanım dosyaları ekler.
## Kimlik Doğrulama
@@ -12,7 +12,25 @@ Uygulamalar şu anda alfa testinde. Özellik işlevsel ancak hâlâ gelişmekte.
Uygulamanız [yerelde derlenip test edildikten sonra](/l/tr/developers/extend/apps/building), dağıtım için iki yolunuz vardır:
* **npmye yayımlama** — uygulamanızı Twenty pazaryerinde listeleyin; böylece herhangi bir çalışma alanı keşfedip yükleyebilir.
* **Bir tarball gönderin** — uygulamanızı herkese açık yapmadan, dahili kullanım için belirli bir Twenty sunucusuna dağıtın.
* **Bir tar arşivi dağıtın** — uygulamanızı dahili veya özel kullanım için doğrudan belirli bir Twenty sunucusuna yükleyin.
Her iki yol da aynı **build** adımından başlar.
## Uygulamanızı derleme
`build` komutu TypeScript kaynaklarınızı derler, mantık işlevlerini ve ön uç bileşenlerini transpile eder ve uygulamanızın içeriğini açıklayan bir `manifest.json` üretir:
```bash filename="Terminal"
yarn twenty build
```
Çıktı `.twenty/output/` dizinine yazılır. Bu dizin, dağıtım için gereken her şeyi içerir: derlenmiş kod, varlıklar, manifest ve `package.json` dosyanızın bir kopyası.
Ayrıca bir `.tgz` tarball oluşturmak için (deploy komutu tarafından dahili olarak kullanılır veya el ile dağıtım için):
```bash filename="Terminal"
yarn twenty build --tarball
```
## npmye yayımlama
@@ -21,29 +39,76 @@ npmye yayımlamak, uygulamanızın Twenty pazaryerinde keşfedilebilir olmas
### Gereksinimler
* Bir [npm](https://www.npmjs.com) hesabı
* Paket adınız **mutlaka** `twenty-app-` önekini kullanmalıdır (ör. `twenty-app-postcard-sender`)
* `twenty-app` anahtar kelimesi `package.json` dosyanızdaki `keywords` dizisinde **mutlaka** listelenmelidir
### Gerekli anahtar kelimeyi ekleme
Twenty pazar yeri, npm kayıt defterinde `twenty-app` anahtar kelimesine sahip paketleri arayarak uygulamaları keşfeder. Bunu `package.json` dosyanıza ekleyin:
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
Pazar yeri, npm kayıt defterinde `keywords:twenty-app` araması yapar. Bu anahtar kelime olmadan, adında `twenty-app-` öneki bulunsa bile paketiniz pazar yerinde görünmez.
</Note>
### Adımlar
1. **Uygulamanızı derleyin** — CLI, TypeScript kaynaklarınızı derler ve uygulama manifestini oluşturur:
1. **Uygulamanızı derleyin:**
```bash filename="Terminal"
yarn twenty build
```
2. **npmye yayımlayın** — derlenmiş paketi npm registryye gönderin:
2. **npmye yayımlayın:**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### Otomatik keşif
Bu, `.twenty/output/` dizininden `npm publish` komutunu çalıştırır.
`twenty-app-` önekine sahip paketler, Twenty pazaryeri kataloğu tarafından otomatik olarak keşfedilir. Yayımlandıktan sonra, uygulamanız birkaç dakika içinde pazaryerinde görünür — manuel kayıt veya onay gerekmez.
Belirli bir dist-tag altında yayımlamak için (ör. `beta` veya `next`):
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### Pazar yerinde keşif nasıl çalışır
Twenty sunucusu pazar yeri kataloğunu npm kayıt defterinden **her saat** eşitler:
1. `keywords:twenty-app` anahtar kelimesine sahip tüm npm paketlerini arar
2. Her paket için `manifest.json` dosyasını npm CDNinden getirir
3. Uygulamanın meta verileri (ad, açıklama, yazar, logo, ekran görüntüleri, kategori) manifest dosyasından çıkarılır ve pazar yerinde görüntülenir
Yayımladıktan sonra, uygulamanızın pazar yerinde görünmesi bir saate kadar sürebilir. Bir sonraki saatlik çalışmayı beklemek yerine eşitlemeyi hemen tetiklemek için:
```bash filename="Terminal"
yarn twenty catalog-sync
```
Belirli bir remoteu hedeflemek için:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
Pazar yerinde gösterilen meta veriler, uygulamanızın kaynak kodundaki `defineApplication()` çağrısından gelir — `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl` ve `termsUrl` gibi alanlar.
<Note>
Uygulamanız `defineApplication()` içinde bir `aboutDescription` tanımlamıyorsa, pazaryeri, hakkında sayfasının içeriği olarak paketinizin npm'deki `README.md` dosyasını otomatik olarak kullanır. Bu, hem npm hem de Twenty pazaryeri için tek bir README dosyası kullanabileceğiniz anlamına gelir. Pazaryerinde farklı bir açıklama istiyorsanız, `aboutDescription` değerini açıkça ayarlayın.
</Note>
### CI üzerinden yayımlama
İskelet proje, her sürümde yayımlama yapan bir GitHub Actions iş akışını içerir. Önce `app:build` çalıştırır, ardından derleme çıktısından `npm publish --provenance` çalıştırır:
İskelet proje, her sürümde yayımlayan bir GitHub Actions iş akışını içerir:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
Diğer CI sistemleri (GitLab CI, CircleCI, vb.) için de aynı üç komut geçerlidir: `yarn install`, `npx twenty build` ve ardından `.twenty/output` dizininden `npm publish`.
Diğer CI sistemleri (GitLab CI, CircleCI, vb.) için de aynı üç komut geçerlidir: `yarn install`, `yarn twenty build` ve ardından `.twenty/output` dizininden `npm publish`.
<Tip>
**npm provenance** isteğe bağlıdır ancak önerilir. `--provenance` ile yayımlamak, npm listenize bir güven rozeti ekler ve kullanıcıların paketin herkese açık bir CI ardışık düzenindeki belirli bir committen oluşturulduğunu doğrulamasını sağlar. Kurulum talimatları için [npm provenance belgelerine](https://docs.npmjs.com/generating-provenance-statements) bakın.
</Tip>
## Dahili dağıtım
## Sunucuya dağıtım (tarball)
Genel kullanıma açık olmasını istemediğiniz uygulamalar — sahipli araçlar, yalnızca kurumsal entegrasyonlar veya deneysel yapılar — için bir tarball’ı doğrudan bir Twenty sunucusuna gönderebilirsiniz.
Genel kullanıma açık olmasını istemediğiniz uygulamalar — sahipli araçlar, yalnızca kurumsal entegrasyonlar veya deneysel derlemeler — için bir tarball’ı doğrudan bir Twenty sunucusuna dağıtabilirsiniz.
### Bir tarball gönderin
### Ön Gereksinimler
Uygulamanızı derleyin ve tek adımda belirli bir sunucuya dağıtın:
Dağıtmadan önce, hedef sunucuyu işaret eden yapılandırılmış bir remotea ihtiyacınız vardır. Remotelar sunucu URLsini ve kimlik doğrulama bilgilerini yerel olarak `~/.twenty/config.json` içinde saklar.
Bir remote ekleyin:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
Daha sonra, bu sunucudaki herhangi bir çalışma alanı uygulamayı **Applications** ayarları sayfasından yükleyip güncelleyebilir.
Yerel bir geliştirme sunucusu için:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
Etkileşimli olmayan ortamlar için bir API anahtarıyla da kimlik doğrulayabilirsiniz:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Remotelarınızı yönetin:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Dağıtım
Uygulamanızı tek adımda derleyip sunucuya yükleyin:
```bash filename="Terminal"
yarn twenty deploy
```
Bu, uygulamayı `--tarball` ile derler ve ardından tarball’ı varsayılan remotea GraphQL çok parçalı yükleme ile yükler.
Belirli bir remotea dağıtmak için:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Dağıtılmış bir uygulamayı paylaşma
Tarball uygulamaları genel pazar yerinde listelenmez; bu nedenle aynı sunucudaki diğer çalışma alanları gezinerek onları keşfedemez. Dağıtılmış bir uygulamayı paylaşmak için:
1. **Ayarlar > Uygulamalar > Kayıtlar** bölümüne gidin ve uygulamanızı açın
2. **Dağıtım** sekmesinde, **Paylaşım bağlantısını kopyala**ya tıklayın
3. Bu bağlantıyı diğer çalışma alanlarındaki kullanıcılarla paylaşın — onları doğrudan uygulamanın yükleme sayfasına götürür
Paylaşım bağlantısı, sunucunun temel URLsini (herhangi bir çalışma alanı alt alan adı olmadan) kullanır; böylece sunucudaki herhangi bir çalışma alanı için çalışır.
### Sürüm yönetimi
Bir güncelleme yayımlamak için:
1. `package.json` içindeki `version` alanını artırın
2. `npx twenty publish --server <server-url>` ile yeni bir tarball gönderin
3. O sunucudaki çalışma alanları, ayarlarında kullanılabilir güncellemeyi görecektir.
2. `yarn twenty deploy` (veya `yarn twenty deploy -r production`) komutunu çalıştırın
3. Uygulamayı kurmuş olan çalışma alanları, ayarlarında mevcut güncellemeyi görecektir
<Note>
Dahili uygulamalar, gönderildikleri sunucu ile sınırlıdır. Genel pazaryerinde görünmezler ve diğer sunuculardaki çalışma alanları tarafından yüklenemezler.
</Note>
## Uygulamaları yükleme
## Uygulama kategorileri
Bir uygulama yayımlandığında (npm) veya dağıtıldığında (tarball), çalışma alanları onu kullanıcı arayüzü (UI) aracılığıyla yükler:
```bash filename="Terminal"
yarn twenty install
```
Veya Twenty kullanıcı arayüzündeki **Ayarlar > Uygulamalar** sayfasından; burada hem pazar yerindeki hem de tarball ile dağıtılmış uygulamalar görüntülenip yüklenebilir.
## Uygulama dağıtım kategorileri
Twenty, uygulamaları nasıl dağıtıldıklarına göre üç kategoriye ayırır:
| Kategori | Nasıl Çalışır | Pazaryerinde görünür mü? |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Geliştirme** | `yarn twenty dev` ile çalışan yerel geliştirme modu uygulamaları. Derleme ve test için kullanılır. | Hayır |
| **Yayımlanmış** | `twenty-app-` önekiyle npmye yayımlanan uygulamalar. Herhangi bir çalışma alanının yükleyebilmesi için pazaryerinde listelenir. | Evet |
| **Dahili** | Bir tarball aracılığıyla belirli bir sunucuya dağıtılan uygulamalar. Yalnızca o sunucudaki çalışma alanlarının kullanımına açıktır. | Hayır |
| Kategori | Nasıl Çalışır | Pazaryerinde görünür mü? |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| **Geliştirme** | `yarn twenty dev` ile çalışan yerel geliştirme modu uygulamaları. Derleme ve test için kullanılır. | Hayır |
| **Yayımlanmış (npm)** | `twenty-app` anahtar kelimesiyle npmye yayımlanan uygulamalar. Herhangi bir çalışma alanının yükleyebilmesi için pazaryerinde listelenir. | Evet |
| **Dahili (tarball)** | Bir tarball aracılığıyla belirli bir sunucuya dağıtılan uygulamalar. Yalnızca o sunucudaki çalışma alanları için bir paylaşım bağlantısı aracılığıyla kullanılabilir. | Hayır |
<Tip>
Uygulamanızı geliştirirken **Geliştirme** modunda başlayın. Hazır olduğunda, geniş dağıtım için **Yayımlanmış** (npm) ya da özel dağıtım için **Dahili** (tarball) seçeneğini tercih edin.
</Tip>
## CLI başvurusu
| Komut | Açıklama | Temel bayraklar |
| --------------------------- | --------------------------------------------------- | -------------------------------------------------- |
| `yarn twenty build` | Uygulamayı derleyin ve manifest oluşturun | `--tarball` — ayrıca bir `.tgz` paket oluşturur |
| `yarn twenty publish` | Derleyin ve npmye yayımlayın | `--tag <tag>` — npm dist-tag (örn. `beta`, `next`) |
| `yarn twenty deploy` | Derleyin ve tarball’ı bir sunucuya yükleyin | `-r, --remote <name>` — hedef remote |
| `yarn twenty catalog-sync` | Sunucuda pazar yeri katalog eşitlemesini tetikleyin | `-r, --remote <name>` — hedef remote |
| `yarn twenty install` | Dağıtılmış bir uygulamayı bir çalışma alanına kurun | `-r, --remote <name>` — hedef remote |
| `yarn twenty dev` | Yerel değişiklikleri izleyin ve eşitleyin | Varsayılan remoteu kullanır |
| `yarn twenty remote add` | Bir sunucu bağlantısı ekleyin | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | Yapılandırılmış remoteları listeleyin | — |
| `yarn twenty remote switch` | Varsayılan remoteu ayarlayın | — |
| `yarn twenty remote status` | Bağlantı durumunu gösterin | — |
| `yarn twenty remote remove` | Bir remoteu kaldırın | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### Yerel Sunucu Yönetimi
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı):
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı, 2020 numaralı portta). Bu komutlar yalnızca Docker tabanlı geliştirme sunucusu için geçerlidir — kaynak koddan başlatılan bir Twenty örneğini yönetmezler (örn. 3000 numaralı portta `npx nx start twenty-server`):
```bash filename="Terminal"
# Yerel sunucuyu başlatın (gerekirse imajı indirir)
@@ -15,19 +15,21 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](/l/zh/developers/extend/apps/getting-started#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
| 函数 | 目的 |
| -------------------------------- | ------------------- |
| `defineApplication` | 配置应用元数据(必需,每个应用一个) |
| `defineObject` | 定义带字段的自定义对象 |
| `defineLogicFunction` | 定义带处理程序的逻辑函数 |
| `definePreInstallLogicFunction` | 定义一个安装前逻辑函数(每个应用一个) |
| `definePostInstallLogicFunction` | 定义一个安装逻辑函数(每个应用一个) |
| `defineFrontComponent` | 为自定义 UI 定义前端组件 |
| `defineRole` | 配置角色权限和对象访问 |
| `defineField` | 为现有对象扩展额外字段 |
| `defineView` | 为对象定义已保存的视图 |
| `defineNavigationMenuItem` | 定义侧边栏导航链接 |
| `defineSkill` | 定义 AI 智能体技能 |
| 函数 | 目的 |
| -------------------------------- | ------------------------ |
| `defineApplication` | 配置应用元数据(必需,每个应用一个) |
| `defineObject` | 定义带字段的自定义对象 |
| `defineField` | 使用附加字段扩展现有对象,或定义独立的关系字段。 |
| `defineLogicFunction` | 定义带处理程序的逻辑函数 |
| `definePreInstallLogicFunction` | 定义一个安装逻辑函数(每个应用一个) |
| `definePostInstallLogicFunction` | 定义一个安装后逻辑函数(每个应用一个) |
| `defineFrontComponent` | 为自定义 UI 定义前端组件 |
| `defineRole` | 配置角色权限和对象访问 |
| `defineView` | 为对象定义已保存的视图 |
| `defineNavigationMenuItem` | 定义侧边栏导航链接 |
| `defineSkill` | 定义 AI 智能体技能 |
| `defineAgent` | 定义 AI 智能体 |
| `definePageLayout` | 定义自定义页面布局 |
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
@@ -36,7 +38,7 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
自定义对象同时描述工作空间中记录的架构与行为。 使用 `defineObject()` 以内置校验定义对象:
```typescript
// src/app/postCard.object.ts
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
@@ -110,7 +112,7 @@ export default defineObject({
* `universalIdentifier` 必须在各次部署间保持唯一且稳定。
* 每个字段都需要 `name`、`type`、`label` 以及其自身稳定的 `universalIdentifier`。
* `fields` 数组是可选的——你可以定义没有自定义字段的对象。
* 你可以使用 `yarn twenty entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
* 你可以使用 `yarn twenty add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
<Note>
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加标准字段
@@ -120,6 +122,197 @@ export default defineObject({
但不建议这样做。
</Note>
### 在现有对象上定义字段
使用 `defineField()` 向你不拥有的对象添加字段——例如标准的 Twenty 对象(Person、Company 等)。 或来自其他应用的对象。 与在 `defineObject()` 中的内联字段不同,独立字段需要一个 `objectUniversalIdentifier` 来指定它们要扩展的对象:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
关键点:
* `objectUniversalIdentifier` 用于标识目标对象。 对于标准对象,请使用从 `twenty-sdk` 导出的 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`。
* 在 `defineObject()` 中以内联方式定义字段时,你不需要 `objectUniversalIdentifier`——它会从父对象继承。
* `defineField()` 是为非通过 `defineObject()` 创建的对象添加字段的唯一方式。
### 关系
关系用于将对象彼此连接。 在 Twenty 中,关系始终是双向的——你需要定义两侧,每一侧都引用另一侧。
关系有两种类型:
| 关系类型 | 描述 | 是否有外键? |
| ------------- | ------------------- | ------------------- |
| `MANY_TO_ONE` | 该对象的多条记录指向目标对象的一条记录 | 是(`joinColumnName` |
| `ONE_TO_MANY` | 该对象的一条记录拥有目标对象的多条记录 | 否(反向侧) |
#### 关系如何工作
每个关系都需要两个相互引用的字段:
1. **MANY_TO_ONE** 侧——位于持有外键的对象上
2. **ONE_TO_MANY** 侧——位于拥有集合的对象上
两个字段都使用 `FieldType.RELATION`,并通过 `relationTargetFieldMetadataUniversalIdentifier` 相互交叉引用。
#### 示例:Post Card 拥有多个收件人
假设一个 `PostCard` 可以发送到多个 `PostCardRecipient` 记录。 每个收件人只隶属于一张 Post Card。
**步骤 1:在 PostCard 上定义 ONE_TO_MANY 侧**(“一”侧):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**步骤 2:在 PostCardRecipient 上定义 MANY_TO_ONE 侧**(“多”侧——持有外键):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
\*\*循环导入:\*\*两个关系字段相互引用彼此的 `universalIdentifier`。 为避免循环导入问题,请在各自文件中将字段 ID 作为具名常量导出,并在另一个文件中导入它们。 构建系统会在编译时解析这些引用。
</Note>
#### 与标准对象建立关系
要与内置的 Twenty 对象(Person、Company 等)建立关系,请使用 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### 关系字段属性
| 属性 | 必填 | 描述 |
| ------------------------------------------------- | ---------------- | -------------------------------------------------------------- |
| `类型` | 是 | 必须为 `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | 是 | 目标对象的 `universalIdentifier` |
| `relationTargetFieldMetadataUniversalIdentifier` | 是 | 目标对象上匹配字段的 `universalIdentifier` |
| `universalSettings.relationType` | 是 | `RelationType.MANY_TO_ONE` 或 `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | 仅适用于 MANY_TO_ONE | 当被引用的记录被删除时的处理方式:`CASCADE`、`SET_NULL`、`RESTRICT` 或 `NO_ACTION` |
| `universalSettings.joinColumnName` | 仅适用于 MANY_TO_ONE | 外键的数据库列名(例如,`postCardId` |
#### 在 defineObject 中内联关系字段
你也可以直接在 `defineObject()` 内定义关系字段。 在这种情况下,省略 `objectUniversalIdentifier`——它会从父对象继承:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### 应用配置(application-config.ts
每个应用都有一个 `application-config.ts` 文件,用于描述:
@@ -161,13 +354,29 @@ export default defineApplication({
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
* 清单构建期间会自动检测安装前和安装后函数。 参见 [安装前函数](#pre-install-functions) 和 [安装后函数](#post-install-functions)。
#### 应用市场元数据
如果你计划[发布你的应用](/l/zh/developers/extend/apps/publishing),这些可选字段将控制你的应用在应用市场中的展示:
| 字段 | 描述 |
| ------------------ | -------------------------------------------------------------- |
| `作者` | 作者或公司名称 |
| `类别` | 用于应用市场筛选的应用类别 |
| `logoUrl` | 你的应用徽标的路径(相对于 `./assets/` |
| `screenshots` | 屏幕截图路径数组(相对于 `./assets/` |
| `aboutDescription` | 用于“关于”选项卡的更长的 Markdown 描述。 如果省略,市场将使用该软件包在 npm 上的 `README.md`。 |
| `websiteUrl` | 你的网站链接 |
| `termsUrl` | 服务条款链接 |
| `emailSupport` | 支持电子邮件地址 |
| `issueReportUrl` | 问题跟踪器链接 |
#### 角色和权限
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application-config.ts` 中的 `defaultRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
* 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。
* 类型化客户端将受限于该角色授予的权限。
* 遵循最小权限原则:仅授予函数所需权限来创建一个专用角色,然后引用其通用标识符。
* 遵循最小权限原则:创建一个仅包含你的函数所需权限专用角色,然后引用其通用标识符。
##### 默认函数角色(*.role.ts
@@ -230,7 +439,7 @@ export default defineRole({
每个函数文件都使用 `defineLogicFunction()` 导出包含处理程序和可选触发器的配置。
```typescript
// src/app/createPostCard.logic-function.ts
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
@@ -324,7 +533,7 @@ export default definePreInstallLogicFunction({
你也可以随时使用 CLI 手动执行安装前函数:
```bash filename="Terminal"
yarn twenty function:execute --preInstall
yarn twenty exec --preInstall
```
关键点:
@@ -334,7 +543,7 @@ yarn twenty function:execute --preInstall
* 每个应用仅允许一个安装前函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `preInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的准备任务。
* 安装前函数不需要触发器——它们会在安装前由平台调用,或通过 `function:execute --preInstall` 手动调用。
* 安装前函数不需要触发器——它们会在安装前由平台调用,或通过 `exec --preInstall` 手动调用。
### 安装后函数
@@ -362,7 +571,7 @@ export default definePostInstallLogicFunction({
你也可以随时使用 CLI 手动执行安装后函数:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
yarn twenty exec --postInstall
```
关键点:
@@ -372,7 +581,7 @@ yarn twenty function:execute --postInstall
* 每个应用仅允许一个安装后函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `postInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的设置任务,如数据填充。
* 安装后函数不需要触发器——它们会在安装过程中由平台调用,或通过 `function:execute --postInstall` 手动调用。
* 安装后函数不需要触发器——它们会在安装过程中由平台调用,或通过 `exec --postInstall` 手动调用。
### 路由触发器负载
@@ -416,15 +625,15 @@ const handler = async (event: RoutePayload) => {
`RoutePayload` 类型具有以下结构:
| 属性 | 类型 | 描述 |
| ---------------------------- | ------------------------------------- | ------------------------------------------------ |
| `headers` | `Record<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) |
| `queryStringParameters` | `Record<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) |
| `pathParameters` | `Record<string, string \| undefined>` | 从路由模式中提取的路径参数(例如,`/users/:id` `{ id: '123' }` |
| `body` | `object \| null` | 已解析的请求体(JSON) |
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 |
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE |
| `requestContext.http.path` | `string` | 原始请求路径 |
| 属性 | 类型 | 描述 |
| ---------------------------- | ------------------------------------- | ------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) |
| `queryStringParameters` | `Record<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) |
| `pathParameters` | `Record<string, string \| undefined>` | 从路由模式中提取的路径参数(例如,`/users/:id` -> `{ id: '123' }` |
| `body` | `object \| null` | 已解析的请求体(JSON) |
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 |
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE |
| `requestContext.http.path` | `string` | 原始请求路径 |
### 转发 HTTP 请求头
@@ -466,7 +675,7 @@ const handler = async (event: RoutePayload) => {
你可以通过两种方式创建新函数:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新逻辑函数的选项。 这将生成一个包含处理程序和配置的入门文件。
* **脚手架生成**:运行 `yarn twenty add` 并选择添加新逻辑函数的选项。 这将生成一个包含处理程序和配置的入门文件。
* **手动**:创建一个新的 `*.logic-function.ts` 文件,并使用 `defineLogicFunction()`,遵循相同的模式。
### 将逻辑函数标记为工具
@@ -478,7 +687,7 @@ const handler = async (event: RoutePayload) => {
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
@@ -567,7 +776,7 @@ export default defineFrontComponent({
你可以通过两种方式创建新的前端组件:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新前端组件的选项。
* **脚手架生成**:运行 `yarn twenty add` 并选择添加新前端组件的选项。
* **手动**:创建一个新的 `.tsx` 文件,并使用 `defineFrontComponent()`,遵循相同的模式。
### 技能
@@ -602,47 +811,122 @@ export default defineSkill({
你可以通过两种方式创建新技能:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新技能的选项。
* **脚手架生成**:运行 `yarn twenty add` 并选择添加新技能的选项。
* **手动**:创建一个新文件,并使用 `defineSkill()`,遵循相同的模式。
### 生成的类型化客户端
### 类型化 API 客户端(`twenty-client-sdk`
两个类型化客户端会由 `yarn twenty dev` 基于你的工作区架构自动生成,并存放在 `node_modules/twenty-sdk/generated`
`twenty-client-sdk` 包提供了两个类型化的 GraphQL 客户端,供你的逻辑函数和前端组件与 Twenty API 交互
* **`CoreApiClient`** — 查询 `/graphql` 端点以获取工作区数据
* **`MetadataApiClient`** — 查询 `/metadata` 端点以获取工作区配置并处理文件上传
| 客户端 | 导入 | 端点 | 是否生成? |
| ------------------- | ---------------------------- | ------------------------ | --------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql`——工作区数据(记录、对象) | 是,在开发/构建时 |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata`——工作区配置、文件上传 | 否,已预构建提供 |
#### CoreApiClient
`CoreApiClient` 是用于查询和变更工作区数据的主要客户端。 它会在执行 `yarn twenty dev` 或 `yarn twenty build` 时根据你的工作区架构生成,因此能完全类型化以匹配你的对象和字段。
```typescript
import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/generated';
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
每当你的对象或字段发生变化时,`yarn twenty dev` 都会自动重新生成这两个客户端
该客户端使用选择集语法:传入 `true` 以包含某字段,使用 `__args` 传递参数,并通过嵌套对象表示关系。 你将基于工作区架构获得完整的自动补全和类型检查
#### 逻辑函数中的运行时凭据
<Note>
**CoreApiClient 在开发/构建时生成。** 如果在未先运行 `yarn twenty dev` 或 `yarn twenty build` 的情况下尝试使用它,将会抛出错误。 生成过程是自动完成的——CLI 会自省你的工作区 GraphQL 架构,使用 `@genql/cli` 生成类型化客户端,将生成的源码写入 `node_modules/twenty-client-sdk/dist/core/generated/`,并替换 `node_modules/twenty-client-sdk/dist/core.mjs` 和 `node_modules/twenty-client-sdk/dist/core.cjs` 中的存根。
</Note>
当你的函数在 Twenty 上运行时,平台会在代码执行前将凭据作为环境变量注入:
#### 使用 CoreSchema 进行类型标注
* `TWENTY_API_URL`:你的应用所针对的 Twenty API 的基础 URL。
* `TWENTY_API_KEY`:作用域限定于你的应用默认函数角色的短期密钥。
`CoreSchema` 提供与工作区对象相匹配的 TypeScript 类型,可用于为组件状态或函数参数进行类型标注:
备注:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
* 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。
* API 密钥的权限由 `application-config.ts` 中通过 `defaultRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `defaultRoleUniversalIdentifier` 指向该角色的通用标识符。
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` 随 SDK 一并提供,已预构建(无需生成)。 它会查询 `/metadata` 端点以获取工作区配置、应用以及文件上传:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### 运行时凭据
当你的代码在 Twenty 上运行(逻辑函数或前端组件)时,平台会以环境变量的形式注入凭据:
* `TWENTY_API_URL`——Twenty API 的基础 URL
* `TWENTY_API_KEY`——作用域限定于你的应用默认函数角色的短期密钥
你无需将这些值传递给客户端——它们会自动从 `process.env` 读取。 API 密钥的权限由你的 `application-config.ts` 中 `defaultRoleUniversalIdentifier` 引用的角色决定。
#### 上传文件
生成的 `MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到你的工作区对象的文件类型字段。 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)
`MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到文件类型字段。 它实现了[GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)
```typescript
import { MetadataApiClient } from 'twenty-sdk/generated';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
@@ -652,25 +936,14 @@ const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
方法签名:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| 参数 | 类型 | 描述 |
| ---------------------------------- | -------- | ------------------------------------------------ |
| `fileBuffer` | `Buffer` | 原始文件内容 |
@@ -680,8 +953,7 @@ uploadFile(
关键点:
* `uploadFile` 方法可在 `MetadataApiClient` 上使用,因为上传 mutation 由 `/metadata` 端点解析
* 它使用该字段的 `universalIdentifier`(而不是其工作区特定的 ID),因此你的上传代码可以在安装了你的应用的任何工作区中使用——这与应用在其他地方引用字段的方式保持一致。
* 使用字段的 `universalIdentifier`(而不是其工作区特定的 ID),因此你的上传代码可在安装了你的应用的任何工作区中运行
* 返回的 `url` 是一个签名 URL,你可以用它来访问已上传的文件。
### Hello World 示例
@@ -9,18 +9,19 @@ description: 几分钟内创建你的第一个 Twenty 应用。
应用可通过自定义对象、字段、逻辑函数、AI 技能和 UI 组件来扩展 Twenty——全部以代码进行管理。
**你现在可以做什么:**
**你可以构建的内容:**
* 以代码定义自定义对象字段(受管理的数据模型
* 使用自定义触发器(HTTP 路由、cron数据库事件)构建逻辑函数
* 为 AI 智能体定义技能
* 构建在 Twenty 的 UI 中渲染的前端组件
* 自定义对象字段、视图和导航项,以塑造你的数据模型
* HTTP 路由、cron 调度或数据库事件触发的逻辑函数
* 在 Twenty 的 UI 中直接渲染的前端组件
* 用于扩展 Twenty 的 AI 代理的技能
* 将同一个应用部署到多个工作空间
## 先决条件
* Node.js 24+ 和 Yarn 4
* Docker (用于本地 Twenty 开发服务器)
* Node.js 24+
* Yarn 4
* Docker (或正在运行的本地 Twenty 实例)
## 开始使用
@@ -29,27 +30,15 @@ description: 几分钟内创建你的第一个 Twenty 应用。
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill, agent)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
npx create-twenty-app@latest my-app --minimal
```
> 使用 `--minimal` 选项生成最简安装脚手架
从这里您可以:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty entity:add
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
@@ -123,7 +112,7 @@ my-twenty-app/
总体来说:
* **package.json**:声明应用名称、版本、引擎(Node 24+、Yarn 4),并添加 `twenty-sdk` 以及一个 `twenty` 脚本,该脚本会委托给本地的 `twenty` CLI。 运行 `yarn twenty help` 以列出所有可用命令。
* **.gitignore**:忽略常见产物,如 `node_modules`、`.yarn`、`generated/`(类型化客户端)、`dist/`、`build/`、覆盖率文件夹、日志文件以及 `.env*` 文件。
* **.gitignore**:忽略常见产物,如 `node_modules`、`.yarn`、`.twenty/`、`dist/`、`build/`、覆盖率文件夹、日志文件以及 `.env*` 文件。
* **yarn.lock**、**.yarnrc.yml**、**.yarn/**:锁定并配置项目使用的 Yarn 4 工具链。
* **.nvmrc**:固定项目期望的 Node.js 版本。
* **.oxlintrc.json** 和 **tsconfig.json**:为应用的 TypeScript 源码提供 Lint 与 TypeScript 配置。
@@ -167,8 +156,8 @@ export default defineObject({
后续命令将添加更多文件和文件夹:
* `yarn twenty dev` 会在 `node_modules/twenty-sdk/generated` 中自动生成两个类型化 API 客户端:`CoreApiClient`(通过 `/graphql` 获取工作区数据) `MetadataApiClient`(通过 `/metadata` 处理工作区配置和文件上传)。
* `yarn twenty entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件、角色、技能等添加实体定义文件。
* `yarn twenty dev` 会自动生成类型化 `CoreApiClient`(通过 `/graphql` 获取工作区数据),并写入 `node_modules/twenty-client-sdk/`。 `MetadataApiClient`(通过 `/metadata` 处理工作区配置和文件上传)为预构建版本,可立即使用。 分别从 `twenty-client-sdk/core` 和 `twenty-client-sdk/metadata` 导入它们
* `yarn twenty add` 会在 `src/` 下为你的自定义对象、函数、前端组件、角色、技能等添加实体定义文件。
## 身份验证
@@ -12,7 +12,25 @@ description: 将你的 Twenty 应用分发到应用市场,或进行内部部
一旦你的应用已[在本地构建并完成测试](/l/zh/developers/extend/apps/building),你可以通过两种方式进行分发:
* **发布到 npm** — 将你的应用在 Twenty 应用市场上架,供任何工作区发现并安装。
* **推送 tar 包** — 将你的应用部署到特定的 Twenty 服务器供内部使用,而无需公开发布
* **部署 tar 包** — 直接将你的应用上传到特定的 Twenty 服务器,以供内部或私有使用。
两种路径都从同一个**构建**步骤开始。
## 构建你的应用
`build` 命令会编译你的 TypeScript 源码,转译逻辑函数和前端组件,并生成一个描述你应用内容的 `manifest.json`
```bash filename="Terminal"
yarn twenty build
```
输出将写入 `.twenty/output/`。 此目录包含分发所需的一切:已编译的代码、资源、清单,以及你的 `package.json` 副本。
要同时创建一个 `.tgz` 压缩包(由部署命令在内部使用,或用于手动分发):
```bash filename="Terminal"
yarn twenty build --tarball
```
## 发布到 npm
@@ -21,29 +39,76 @@ description: 将你的 Twenty 应用分发到应用市场,或进行内部部
### 要求
* 一个 [npm](https://www.npmjs.com) 账户
* 你的包名**必须**使用 `twenty-app-` 前缀(例如,`twenty-app-postcard-sender`
* 你的 `package.json` 的 `keywords` 数组中**必须**包含 `twenty-app` 关键字
### 添加所需关键字
Twenty 市场通过在 npm 注册表中搜索带有 `twenty-app` 关键字的包来发现应用。 将其添加到你的 `package.json`
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
}
```
<Note>
该市场会在 npm 注册表中搜索 `keywords:twenty-app`。 没有此关键字,即使包名带有 `twenty-app-` 前缀,你的包也不会出现在市场中。
</Note>
### 步骤
1. **构建你的应用** — CLI 会编译你的 TypeScript 源码并生成应用清单:
1. **构建你的应用**
```bash filename="Terminal"
yarn twenty build
```
2. **发布到 npm** — 将构建好的包推送到 npm 注册表:
2. **发布到 npm**
```bash filename="Terminal"
npx twenty publish
yarn twenty publish
```
### 自动发现
这会在 `.twenty/output/` 目录下运行 `npm publish`。
Twenty 应用市场目录会自动发现带有 `twenty-app-` 前缀的包。 发布后,你的应用会在几分钟内出现在应用市场中 — 无需手动注册或审批。
要在特定的 dist-tag(例如 `beta` 或 `next`)下发布:
```bash filename="Terminal"
yarn twenty publish --tag beta
```
### 应用市场的发现机制如何运作
Twenty 服务器会**每小时**从 npm 注册表同步其市场目录:
1. 它会搜索所有带有 `keywords:twenty-app` 关键字的 npm 包
2. 对于每个包,它会从 npm CDN 获取 `manifest.json`
3. 应用的元数据(名称、描述、作者、徽标、屏幕截图、类别)将从清单中提取,并显示在市场中
发布后,你的应用最多可能需要一小时才会出现在市场中。 要立即触发同步,而无需等待下一次每小时同步:
```bash filename="Terminal"
yarn twenty catalog-sync
```
要指定特定的远程:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
市场中显示的元数据来自你在应用源代码中调用的 `defineApplication()` —— 诸如 `displayName`、`description`、`author`、`category`、`logoUrl`、`screenshots`、`aboutDescription`、`websiteUrl` 和 `termsUrl` 等字段。
<Note>
如果您的应用未在 `defineApplication()` 中定义 `aboutDescription`,市场将自动使用 npm 上您的软件包的 `README.md` 作为关于页面内容。 这意味着您可以为 npm 和 Twenty 市场维护同一个 README。 如果您希望在市场中使用不同的描述,请显式设置 `aboutDescription`。
</Note>
### CI 发布
脚手架项目包含一个 GitHub Actions 工作流,会在每次发版时自动发布。 它会先运行 `app:build`,然后在构建输出目录中执行 `npm publish --provenance`
脚手架项目包含一个 GitHub Actions 工作流,会在每次发版时自动发布:
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -72,48 +137,117 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
对于其他 CI 系统(GitLab CI、CircleCI 等),同样适用以下三条命令:`yarn install`、`npx twenty build`,然后在 `.twenty/output` 目录下执行 `npm publish`。
对于其他 CI 系统(GitLab CI、CircleCI 等),同样适用以下三条命令:`yarn install`、`yarn twenty build`,然后在 `.twenty/output` 目录下执行 `npm publish`。
<Tip>
**npm provenance** 可选,但建议启用。 使用 `--provenance` 发布会在你的 npm 列表中添加可信徽章,使用户可以验证该包是由公共 CI 流水线中的特定提交构建的。 有关设置说明,请参见 [npm provenance 文档](https://docs.npmjs.com/generating-provenance-statements)。
</Tip>
## 内部分发
## 部署到服务器(tar 包)
对于你不希望公开的应用 — 例如专有工具、仅供企业使用的集成或实验性构建你可以将 tar 包直接推送到某台 Twenty 服务器。
对于你不希望公开的应用专有工具、仅供企业使用的集成或实验性构建),你可以将 tar 包直接部署到某台 Twenty 服务器。
### 推送 tar 包
### 先决条件
一步中构建你的应用并将其部署到特定服务器:
部署之前,你需要配置一个指向目标服务器的远程。 远程会将服务器 URL 和身份验证凭据本地存储在 `~/.twenty/config.json` 中。
添加远程:
```bash filename="Terminal"
npx twenty publish --server <server-url>
yarn twenty remote add --url https://your-twenty-server.com --as production
```
该服务器上的任何工作区随后都可以在**应用程序**设置页面安装和升级该应用。
对于本地开发服务器:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
对于非交互式环境,你也可以使用 API 密钥进行身份验证:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
管理你的远程:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### 部署
一步构建并将你的应用上传到服务器:
```bash filename="Terminal"
yarn twenty deploy
```
这会使用 `--tarball` 构建应用,然后通过 GraphQL 多部分上传将该 tar 包上传到默认远程。
部署到特定远程:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### 共享已部署的应用
通过 tar 包分发的应用不会出现在公共市场中,因此同一服务器上的其他工作区无法通过浏览发现它们。 要共享已部署的应用:
1. 前往 **Settings > Applications > Registrations** 并打开你的应用
2. 在 **Distribution** 选项卡中,点击 **Copy share link**
3. 将此链接分享给其他工作区的用户 — 它会将他们直接带到该应用的安装页面
该分享链接使用服务器的基础 URL(不包含任何工作区子域),因此适用于该服务器上的任意工作区。
### 版本管理
要发布更新:
1. 更新 `package.json` 中的 `version` 字段
2. 使用 `npx twenty publish --server <server-url>` 推送新的 tar 包
3. 该服务器上的工作区会在其设置中看到可用的升级
2. 运行 `yarn twenty deploy`(或 `yarn twenty deploy -r production`
3. 已安装该应用的工作区会在其设置中看到可用的升级
<Note>
内部应用的作用范围仅限于它们被推送到的服务器。 它们不会出现在公共应用市场中,其他服务器上的工作区也无法安装。
</Note>
## 安装应用
## 应用类别
一旦应用已发布(npm)或已部署(tar 包),各工作区即可通过 UI 进行安装:
```bash filename="Terminal"
yarn twenty install
```
或者在 Twenty UI 的 **Settings > Applications** 页面中浏览并安装来自市场或通过 tar 包部署的应用。
## 应用分发类别
Twenty 会根据分发方式将应用归为三类:
| 类别 | 工作原理 | 在应用市场中可见? |
| ------- | ------------------------------------------------- | --------- |
| **开发** | 通过 `yarn twenty dev` 运行的本地开发模式应用。 用于构建和测试。 | 否 |
| **已发布** | 使用 `twenty-app-` 前缀发布到 npm 的应用。 在应用市场上架,供任何工作区安装。 | 是 |
| **内部** | 通过 tar 包部署到特定服务器的应用。 仅对该服务器上的工作区可用。 | 否 |
| 类别 | 工作原理 | 在应用市场中可见? |
| ------------- | -------------------------------------------------- | --------- |
| **开发** | 通过 `yarn twenty dev` 运行的本地开发模式应用。 用于构建和测试。 | 否 |
| **已发布npm** | 发布到 npm 且包含 `twenty-app` 关键字的应用。 在应用市场上架,供任何工作区安装。 | 是 |
| **内部tar 包)** | 通过 tar 包部署到特定服务器的应用。 仅通过分享链接对该服务器上的工作区可用。 | 否 |
<Tip>
在构建你的应用时,从**开发**模式开始。 准备就绪后,选择用于广泛分发的**已发布**(npm),或用于私有部署的**内部**(tar 包)。
</Tip>
## CLI 参考
| 命令 | 描述 | 关键选项 |
| --------------------------- | ---------------- | ------------------------------------------- |
| `yarn twenty build` | 编译应用并生成清单 | `--tarball` — 同时创建一个 `.tgz` 包 |
| `yarn twenty publish` | 构建并发布到 npm | `--tag <tag>` — npm 分发标签(例如 `beta`、`next` |
| `yarn twenty deploy` | 构建并将 tar 包上传到服务器 | `-r, --remote <name>` — 目标远程 |
| `yarn twenty catalog-sync` | 在服务器上触发市场目录同步 | `-r, --remote <name>` — 目标远程 |
| `yarn twenty install` | 在某个工作区安装已部署的应用 | `-r, --remote <name>` — 目标远程 |
| `yarn twenty dev` | 监听并同步本地更改 | 使用默认远程 |
| `yarn twenty remote add` | 添加服务器连接 | `--url``--token``--as``--local``--port` |
| `yarn twenty remote list` | 列出已配置的远程 | — |
| `yarn twenty remote switch` | 设置默认远程 | — |
| `yarn twenty remote status` | 显示连接状态 | — |
| `yarn twenty remote remove` | 移除远程 | — |
@@ -38,7 +38,7 @@ yarn twenty dev
### 本地服务器管理
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程,监听 2020 端口)。 这些命令仅适用于基于 Docker 的开发服务器——它们不会管理从源代码启动的 Twenty 实例(例如通过 `npx nx start twenty-server` 启动的实例,运行在 3000 端口):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
@@ -24,7 +24,7 @@ test('Create Industry Select Field', async ({ page }) => {
test('Create Kanban View from Industry Select Field', async ({ page }) => {
await page.getByRole('link', { name: 'Opportunities' }).click();
await page.getByRole('button', { name: 'All Opportunities ·' }).click();
await page.getByRole('button', { name: /All Opportunities/ }).click();
await page.getByText('Add view').click();
await page.getByRole('textbox').press('ControlOrMeta+a');
await page.getByRole('textbox').fill('By industry');
@@ -32,12 +32,11 @@ test('Create Kanban View from Industry Select Field', async ({ page }) => {
await page.getByText('Kanban').click();
await page.locator('[aria-controls="view-picker-kanban-field-options"]').click();
await page.getByRole('option', { name: 'Industry' }).click();
// Use exact: true to ensure we only click the button with the label "Create"
await page.getByRole('button', { name: 'Create new view' }).click();
await expect(page.getByText('Food')).toBeVisible();
await expect(page.getByText('Food')).toBeVisible({ timeout: 30000 });
await expect(page.getByText('Tech')).toBeVisible();
await expect(page.getByText('Travel')).toBeVisible();
await expect(page.getByText('No value')).toBeVisible();
await expect(page.getByText('No Value')).toBeVisible();
const byIndustryElements = await page.locator('text=By industry').all();
expect(byIndustryElements.length).toBeGreaterThanOrEqual(1);
for (const element of byIndustryElements) {
@@ -50,6 +49,6 @@ test('Create Kanban View from Industry Select Field', async ({ page }) => {
return req.url().includes('/metadata') &&
req.method() === 'POST';
})]);
await expect(page.getByText('No value')).not.toBeVisible();
await expect(page.getByText('No Value')).not.toBeVisible();
});
})
@@ -0,0 +1,4 @@
node_modules
storybook-static
src/__stories__/example-sources-built
src/__stories__/example-sources-built-preact
@@ -0,0 +1,57 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": "off",
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",
"react/display-name": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-useless-fragment": "off",
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
@@ -8,10 +8,10 @@ const dirname =
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
const sdkRoot = path.resolve(dirname, '..');
const packageRoot = path.resolve(dirname, '..');
const config: StorybookConfig = {
stories: ['../src/front-component-renderer/**/*.stories.@(js|jsx|ts|tsx)'],
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-vitest'],
@@ -23,11 +23,11 @@ const config: StorybookConfig = {
staticDirs: [
{
from: '../src/front-component-renderer/__stories__/example-sources-built',
from: '../src/__stories__/example-sources-built',
to: '/built',
},
{
from: '../src/front-component-renderer/__stories__/example-sources-built-preact',
from: '../src/__stories__/example-sources-built-preact',
to: '/built-preact',
},
],
@@ -57,7 +57,7 @@ const config: StorybookConfig = {
},
plugins: [
...(viteConfig.plugins ?? []),
tsconfigPaths({ root: sdkRoot }),
tsconfigPaths({ root: packageRoot }),
],
optimizeDeps: {
...viteConfig.optimizeDeps,
@@ -0,0 +1,58 @@
{
"name": "twenty-front-component-renderer",
"version": "0.8.0-canary.5",
"private": true,
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist",
"package.json"
],
"scripts": {
"build": "npx rimraf dist && npx vite build"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"license": "AGPL-3.0",
"dependencies": {
"@quilted/threads": "^4.0.1",
"@remote-dom/core": "^1.10.1",
"@remote-dom/react": "^1.2.2",
"@sniptt/guards": "^0.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zod": "^4.1.11"
},
"devDependencies": {
"@chakra-ui/react": "^3.33.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^7.3.8",
"@storybook/addon-vitest": "^10.2.13",
"@storybook/react-vite": "^10.2.13",
"@types/node": "^24.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.56.1",
"storybook": "^10.2.13",
"styled-components": "^6.1.0",
"ts-morph": "^25.0.0",
"tsx": "^4.7.0",
"twenty-sdk": "workspace:*",
"twenty-shared": "workspace:*",
"twenty-ui": "workspace:*",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": "^24.5.0",
"yarn": "^4.0.2"
}
}
@@ -0,0 +1,112 @@
{
"name": "twenty-front-component-renderer",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-front-component-renderer/src",
"projectType": "library",
"tags": ["scope:frontend"],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "^production"],
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"],
"options": {
"cwd": "{projectRoot}",
"commands": [
"npx rimraf dist && npx vite build -c vite.config.ts",
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
],
"parallel": false
}
},
"typecheck": {},
"lint": {},
"generate-remote-dom-elements": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["^build"],
"inputs": [
"{projectRoot}/scripts/remote-dom/**/*",
"{projectRoot}/src/constants/**/*",
"{workspaceRoot}/packages/twenty-ui/src/**/index.ts",
"{workspaceRoot}/packages/twenty-ui/src/**/*.tsx"
],
"outputs": [
"{projectRoot}/src/host/generated/*",
"{projectRoot}/src/remote/generated/*"
],
"options": {
"cwd": "packages/twenty-front-component-renderer",
"command": "tsx -r tsconfig-paths/register scripts/remote-dom/generate-remote-dom-elements.ts"
},
"configurations": {
"verbose": {
"command": "tsx -r tsconfig-paths/register scripts/remote-dom/generate-remote-dom-elements.ts --verbose"
}
}
},
"storybook:prebuild": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": [
"generate-remote-dom-elements",
{
"target": "build:sdk",
"projects": "twenty-sdk"
},
{
"target": "build:individual",
"projects": "twenty-ui"
},
{
"target": "build:individual",
"projects": "twenty-shared"
}
],
"inputs": [
"{projectRoot}/scripts/front-component-stories/**/*",
"{projectRoot}/src/__stories__/example-sources/*",
"{workspaceRoot}/packages/twenty-sdk/src/cli/utilities/build/**/*"
],
"outputs": ["{projectRoot}/src/__stories__/example-sources-built/*"],
"options": {
"command": "tsx {projectRoot}/scripts/front-component-stories/build-source-examples.ts"
}
},
"storybook:build": {
"dependsOn": ["storybook:prebuild"],
"configurations": {
"test": {}
}
},
"storybook:serve:dev": {
"executor": "nx:run-commands",
"options": {
"port": 6008
}
},
"storybook:serve:static": {
"options": {
"buildTarget": "twenty-front-component-renderer:storybook:build",
"port": 6008
},
"configurations": {
"test": {}
}
},
"storybook:test": {
"dependsOn": ["storybook:prebuild"],
"options": {
"command": "vitest run --coverage --config vitest.storybook.config.ts --shard={args.shard}"
}
},
"storybook:test:no-coverage": {
"dependsOn": ["storybook:prebuild"],
"options": {
"command": "vitest run --config vitest.storybook.config.ts --shard={args.shard}"
}
},
"storybook:coverage": {}
}
}
@@ -3,20 +3,20 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getFrontComponentBuildPlugins } from '../../src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
import { getFrontComponentBuildPlugins } from 'twenty-sdk/front-component-renderer/build';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const exampleSourcesDir = path.resolve(
dirname,
'../../src/front-component-renderer/__stories__/example-sources',
'../../src/__stories__/example-sources',
);
const exampleSourcesBuiltDir = path.resolve(
dirname,
'../../src/front-component-renderer/__stories__/example-sources-built',
'../../src/__stories__/example-sources-built',
);
const exampleSourcesBuiltPreactDir = path.resolve(
dirname,
'../../src/front-component-renderer/__stories__/example-sources-built-preact',
'../../src/__stories__/example-sources-built-preact',
);
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
@@ -26,7 +26,10 @@ const twentyUiIndividualIndex = path.resolve(
'../../../twenty-ui/dist/individual/individual-entry.js',
);
const sdkIndividualIndex = path.resolve(dirname, '../../dist/sdk/index.js');
const sdkIndividualIndex = path.resolve(
dirname,
'../../../twenty-sdk/dist/sdk/index.js',
);
const twentySharedIndividualDir = path.resolve(
dirname,
@@ -4,9 +4,9 @@ import * as path from 'path';
import { IndentationText, Project, QuoteKind } from 'ts-morph';
import { fileURLToPath } from 'url';
import { ALLOWED_HTML_ELEMENTS } from '../../src/sdk/front-component-api/constants/AllowedHtmlElements';
import { COMMON_HTML_EVENTS } from '../../src/sdk/front-component-api/constants/CommonHtmlEvents';
import { HTML_COMMON_PROPERTIES } from '../../src/sdk/front-component-api/constants/HtmlCommonProperties';
import { ALLOWED_HTML_ELEMENTS } from '../../src/constants/AllowedHtmlElements';
import { COMMON_HTML_EVENTS } from '../../src/constants/CommonHtmlEvents';
import { HTML_COMMON_PROPERTIES } from '../../src/constants/HtmlCommonProperties';
import {
type ComponentSchema,
@@ -19,15 +19,9 @@ import {
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PACKAGE_PATH = path.resolve(SCRIPT_DIR, '../..');
const FRONT_COMPONENT_PATH = path.join(
PACKAGE_PATH,
'src/front-component-renderer',
);
const HOST_GENERATED_DIR = path.join(FRONT_COMPONENT_PATH, 'host/generated');
const REMOTE_GENERATED_DIR = path.join(
FRONT_COMPONENT_PATH,
'remote/generated',
);
const SRC_PATH = path.join(PACKAGE_PATH, 'src');
const HOST_GENERATED_DIR = path.join(SRC_PATH, 'host/generated');
const REMOTE_GENERATED_DIR = path.join(SRC_PATH, 'remote/generated');
const extractHtmlTag = (tag: string): string => tag.slice(5);
@@ -1,6 +1,6 @@
import type { Project, SourceFile } from 'ts-morph';
import { EVENT_TO_REACT } from '../../../src/sdk/front-component-api/constants/EventToReact';
import { EVENT_TO_REACT } from '../../../src/constants/EventToReact';
import { type ComponentSchema } from './schemas';
import { addExportedConst } from './utils';
@@ -336,8 +336,7 @@ export const generateRemoteElements = (
});
sourceFile.addImportDeclaration({
moduleSpecifier:
'../../../sdk/front-component-api/constants/SerializedEventData',
moduleSpecifier: '@/constants/SerializedEventData',
namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }],
});

Some files were not shown because too many files have changed in this diff Show More