mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Move all sync entities in an `entities` key. Rename functions to
logicFunctions
```json
{
application: {
...
},
entities: {
objects: [],
logicFunctions: [],
...
}
}
```
654 lines
26 KiB
Text
654 lines
26 KiB
Text
---
|
|
title: Twenty 앱
|
|
description: Twenty 맞춤설정을 코드로 구축하고 관리하세요.
|
|
---
|
|
|
|
<Warning>
|
|
앱은 현재 알파 테스트 중입니다. 해당 기능은 작동하지만 아직 발전 중입니다.
|
|
</Warning>
|
|
|
|
## 앱이란 무엇인가요?
|
|
|
|
앱을 사용하면 Twenty 맞춤설정을 **코드로** 구축하고 관리할 수 있습니다. 모든 것을 UI에서 구성하는 대신, 데이터 모델과 로직 함수를 코드로 정의합니다 — 이를 통해 더 빠르게 구축·유지 관리하고 여러 워크스페이스에 배포할 수 있습니다.
|
|
|
|
**현재 가능한 작업:**
|
|
|
|
* 사용자 정의 객체와 필드를 코드로 정의하기(관리형 데이터 모델)
|
|
* 사용자 정의 트리거로 로직 함수 구축
|
|
* 동일한 앱을 여러 워크스페이스에 배포
|
|
|
|
**곧 제공 예정:**
|
|
|
|
* 사용자 정의 UI 레이아웃 및 컴포넌트
|
|
|
|
## 사전 준비
|
|
|
|
* Node.js 24+ 및 Yarn 4
|
|
* Twenty 워크스페이스와 API 키(https://app.twenty.com/settings/api-webhooks에서 생성)
|
|
|
|
## 시작하기
|
|
|
|
공식 스캐폴더를 사용해 새 앱을 만든 다음, 인증하고 개발을 시작하세요:
|
|
|
|
```bash filename="Terminal"
|
|
# 새 앱 초기 구조 생성
|
|
npx create-twenty-app@latest my-twenty-app
|
|
cd my-twenty-app
|
|
|
|
# yarn@4를 사용하지 않는 경우
|
|
corepack enable
|
|
yarn install
|
|
|
|
# API 키로 인증합니다(입력하라는 메시지가 표시됩니다)
|
|
yarn auth:login
|
|
|
|
# 개발 모드 시작: 로컬 변경 사항이 워크스페이스와 자동으로 동기화됩니다
|
|
yarn app:dev
|
|
```
|
|
|
|
여기에서 다음 작업을 수행할 수 있습니다:
|
|
|
|
```bash filename="Terminal"
|
|
# Add a new entity to your application (guided)
|
|
yarn entity:add
|
|
|
|
# Generate a typed Twenty client and workspace entity types
|
|
yarn app:generate
|
|
|
|
# Watch your application's function logs
|
|
yarn function:logs
|
|
|
|
# Execute a function by name
|
|
yarn function:execute -n my-function -p '{"name": "test"}'
|
|
|
|
# Uninstall the application from the current workspace
|
|
yarn app:uninstall
|
|
|
|
# Display commands' help
|
|
yarn help
|
|
```
|
|
|
|
참고: [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) 및 [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk)의 CLI 참고 페이지도 확인하세요.
|
|
|
|
## 프로젝트 구조(스캐폴딩됨)
|
|
|
|
`npx create-twenty-app@latest my-twenty-app`를 실행하면, 스캐폴더가 다음을 수행합니다:
|
|
|
|
* `my-twenty-app/`에 최소한의 기본 애플리케이션을 복사합니다
|
|
* 로컬 `twenty-sdk` 종속성과 Yarn 4 구성을 추가합니다
|
|
* `twenty` CLI와 연결된 설정 파일과 스크립트를 생성합니다
|
|
* 기본 애플리케이션 구성과 기본 함수 역할을 생성합니다
|
|
|
|
새로 스캐폴딩된 앱은 다음과 같습니다:
|
|
|
|
```text filename="my-twenty-app/"
|
|
my-twenty-app/
|
|
package.json
|
|
yarn.lock
|
|
.gitignore
|
|
.nvmrc
|
|
.yarnrc.yml
|
|
.yarn/
|
|
install-state.gz
|
|
eslint.config.mjs
|
|
tsconfig.json
|
|
README.md
|
|
public/ # 공개 자산 폴더(이미지, 폰트 등)
|
|
src/
|
|
application.config.ts # 필수 - 기본 애플리케이션 구성
|
|
default-function.role.ts # 서버리스 함수의 기본 역할
|
|
hello-world.function.ts # 서버리스 함수 예제
|
|
hello-world.front-component.tsx # 프런트 컴포넌트 예제
|
|
// 사용자 엔터티 (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
|
```
|
|
|
|
### 설정보다 관례
|
|
|
|
애플리케이션은 파일 접미사로 엔티티를 감지하는 **관례 우선** 접근 방식을 사용합니다. 이를 통해 `src/app/` 폴더 내에서 유연하게 구성할 수 있습니다:
|
|
|
|
| 파일 접미사 | 엔티티 유형 |
|
|
| ----------------------- | ------------ |
|
|
| `*.object.ts` | 사용자 정의 객체 정의 |
|
|
| `*.function.ts` | 서버리스 함수 정의 |
|
|
| `*.front-component.tsx` | 프런트 컴포넌트 정의 |
|
|
| `*.role.ts` | 역할 정의 |
|
|
|
|
### 지원되는 폴더 구성 방식
|
|
|
|
엔티티를 다음 패턴 중 어느 것으로든 구성할 수 있습니다:
|
|
|
|
**전통적(유형별):**
|
|
|
|
```text
|
|
src/
|
|
├── application.config.ts
|
|
├── objects/
|
|
│ └── postCard.object.ts
|
|
├── functions/
|
|
│ └── createPostCard.function.ts
|
|
├── components/
|
|
│ └── card.front-component.tsx
|
|
└── roles/
|
|
└── admin.role.ts
|
|
```
|
|
|
|
**기능 기반:**
|
|
|
|
```text
|
|
src/
|
|
├── application.config.ts
|
|
└── post-card/
|
|
├── postCard.object.ts
|
|
├── createPostCard.function.ts
|
|
├── card.front-component.tsx
|
|
└── postCardAdmin.role.ts
|
|
```
|
|
|
|
**플랫:**
|
|
|
|
```text
|
|
src/
|
|
├── application.config.ts
|
|
├── postCard.object.ts
|
|
├── createPostCard.function.ts
|
|
├── card.front-component.tsx
|
|
└── admin.role.ts
|
|
```
|
|
|
|
개요:
|
|
|
|
* **package.json**: 앱 이름, 버전, 엔진(Node 24+, Yarn 4)을 선언하고, `twenty-sdk`와 함께 `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, `auth:login` 같은 스크립트를 추가합니다. 이 스크립트들은 로컬 `twenty` CLI에 위임됩니다.
|
|
* **.gitignore**: `node_modules`, `.yarn`, `generated/`(타입드 클라이언트), `dist/`, `build/`, 커버리지 폴더, 로그 파일, `.env*` 파일 등의 일반 산출물을 무시합니다.
|
|
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: 프로젝트에서 사용하는 Yarn 4 툴체인을 고정하고 구성합니다.
|
|
* **.nvmrc**: 프로젝트에서 예상하는 Node.js 버전을 고정합니다.
|
|
* **eslint.config.mjs** 및 **tsconfig.json**: 앱의 TypeScript 소스에 대한 린팅 및 TypeScript 구성을 제공합니다.
|
|
* **README.md**: 앱 루트에 기본 안내를 담은 간단한 README입니다.
|
|
* **public/**: 애플리케이션과 함께 제공되는 공개 자산(이미지, 폰트, 정적 파일)을 저장하는 폴더입니다. 여기에 배치된 파일은 동기화 시 업로드되며 런타임에 액세스할 수 있습니다.
|
|
* **src/**: 애플리케이션을 코드로 정의하는 주요 위치:
|
|
* `application.config.ts`: 앱의 전역 구성(메타데이터 및 런타임 연결)입니다. 아래의 "Application config"를 참조하세요.
|
|
* `*.role.ts`: 로직 함수에서 사용하는 역할 정의. 아래의 "Default function role"을 참조하세요.
|
|
* `*.object.ts`: 사용자 정의 객체 정의.
|
|
* `*.function.ts`: 로직 함수 정의.
|
|
* `*.front-component.tsx`: 프런트 컴포넌트 정의.
|
|
|
|
이후 명령을 실행하면 더 많은 파일과 폴더가 추가됩니다:
|
|
|
|
* `yarn app:generate`는 `generated/` 폴더를 생성합니다(타입드 Twenty 클라이언트 + 워크스페이스 타입).
|
|
* `yarn entity:add`는 사용자 정의 객체, 함수, 프런트 컴포넌트 또는 역할에 대한 엔티티 정의 파일을 `src/` 아래에 추가합니다.
|
|
|
|
## 인증
|
|
|
|
처음 `yarn auth:login`을 실행하면 다음을 입력하라는 프롬프트가 표시됩니다:
|
|
|
|
* API URL(기본값은 http://localhost:3000 또는 현재 워크스페이스 프로필)
|
|
* API 키
|
|
|
|
자격 증명은 사용자별로 `~/.twenty/config.json`에 저장됩니다. 여러 프로필을 유지하고 프로필 간에 전환할 수 있습니다.
|
|
|
|
### 작업 공간 관리
|
|
|
|
```bash filename="Terminal"
|
|
# Login interactively (recommended)
|
|
yarn auth:login
|
|
|
|
# Login to a specific workspace profile
|
|
yarn auth:login --workspace my-custom-workspace
|
|
|
|
# List all configured workspaces
|
|
yarn auth:list
|
|
|
|
# Switch the default workspace (interactive)
|
|
yarn auth:switch
|
|
|
|
# Switch to a specific workspace
|
|
yarn auth:switch production
|
|
|
|
# Check current authentication status
|
|
yarn auth:status
|
|
```
|
|
|
|
한 번 `auth:switch`로 작업 공간을 전환하면, 이후의 모든 명령은 기본적으로 해당 작업 공간을 사용합니다. 여전히 `--workspace <name>`로 일시적으로 재정의할 수 있습니다.
|
|
|
|
## SDK 리소스(타입 및 구성) 사용
|
|
|
|
twenty-sdk는 앱 내부에서 사용하는 타입드 빌딩 블록과 헬퍼 함수를 제공합니다. 가장 자주 사용하게 될 핵심 요소는 다음과 같습니다.
|
|
|
|
### 헬퍼 함수
|
|
|
|
SDK는 앱 엔티티를 정의할 때 사용할 수 있는, 내장 검증이 포함된 네 가지 헬퍼 함수를 제공합니다:
|
|
|
|
| 함수 | 목적 |
|
|
| ------------------ | ------------------- |
|
|
| `defineApplication()` | 애플리케이션 메타데이터 구성 |
|
|
| `defineObject()` | 필드가 있는 사용자 정의 객체 정의 |
|
|
| `defineFunction()` | 핸들러가 있는 로직 함수 정의 |
|
|
| `defineRole()` | 역할 권한과 객체 접근 구성 |
|
|
|
|
이 함수들은 런타임에 구성을 검증하고, 더 나은 IDE 자동 완성과 타입 안정성을 제공합니다.
|
|
|
|
### 객체 정의하기
|
|
|
|
사용자 정의 객체는 워크스페이스의 레코드에 대한 스키마와 동작을 모두 정의합니다. `defineObject()`를 사용해 내장 검증과 함께 객체를 정의하세요:
|
|
|
|
```typescript
|
|
// src/app/postCard.object.ts
|
|
import { defineObject, FieldType } from 'twenty-sdk';
|
|
|
|
enum PostCardStatus {
|
|
DRAFT = 'DRAFT',
|
|
SENT = 'SENT',
|
|
DELIVERED = 'DELIVERED',
|
|
RETURNED = 'RETURNED',
|
|
}
|
|
|
|
export default defineObject({
|
|
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
|
nameSingular: 'postCard',
|
|
namePlural: 'postCards',
|
|
labelSingular: 'Post Card',
|
|
labelPlural: 'Post Cards',
|
|
description: 'A post card object',
|
|
icon: 'IconMail',
|
|
fields: [
|
|
{
|
|
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
|
name: 'content',
|
|
type: FieldType.TEXT,
|
|
label: 'Content',
|
|
description: "Postcard's content",
|
|
icon: 'IconAbc',
|
|
},
|
|
{
|
|
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
|
name: 'recipientName',
|
|
type: FieldType.FULL_NAME,
|
|
label: 'Recipient name',
|
|
icon: 'IconUser',
|
|
},
|
|
{
|
|
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
|
name: 'recipientAddress',
|
|
type: FieldType.ADDRESS,
|
|
label: 'Recipient address',
|
|
icon: 'IconHome',
|
|
},
|
|
{
|
|
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
|
name: 'status',
|
|
type: FieldType.SELECT,
|
|
label: 'Status',
|
|
icon: 'IconSend',
|
|
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
|
options: [
|
|
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
|
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
|
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
|
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
|
],
|
|
},
|
|
{
|
|
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
|
name: 'deliveredAt',
|
|
type: FieldType.DATE_TIME,
|
|
label: 'Delivered at',
|
|
icon: 'IconCheck',
|
|
isNullable: true,
|
|
defaultValue: null,
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
핵심 요점:
|
|
|
|
* 내장 검증과 더 나은 IDE 지원을 위해 `defineObject()`를 사용하세요.
|
|
* `universalIdentifier`는 배포 전반에서 고유하고 안정적이어야 합니다.
|
|
* 각 필드는 `name`, `type`, `label` 및 고유하고 안정적인 `universalIdentifier`가 필요합니다.
|
|
* `fields` 배열은 선택 사항입니다. 사용자 정의 필드 없이도 객체를 정의할 수 있습니다.
|
|
* `yarn entity:add`를 사용하여 새 객체를 스캐폴딩할 수 있으며, 이름, 필드, 관계 설정 과정을 안내합니다.
|
|
|
|
<Note>
|
|
**기본 필드는 자동으로 생성됩니다.** 사용자 정의 객체를 정의하면 Twenty가 `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, `deletedAt` 등의 표준 필드를 자동으로 추가합니다. 이 필드들은 `fields` 배열에 정의할 필요가 없습니다. 사용자 정의 필드만 추가하세요.
|
|
</Note>
|
|
|
|
### 애플리케이션 구성(application.config.ts)
|
|
|
|
모든 앱에는 다음을 설명하는 단일 `application.config.ts` 파일이 있습니다:
|
|
|
|
* **앱에 대한 정보**: 식별자, 표시 이름, 설명.
|
|
* **함수가 실행되는 방식**: 권한을 위해 사용하는 역할.
|
|
* **(선택 사항) 변수**: 함수에 환경 변수로 노출되는 키–값 쌍.
|
|
|
|
`defineApplication()`을 사용해 애플리케이션 구성을 정의하세요:
|
|
|
|
```typescript
|
|
// src/app/application.config.ts
|
|
import { defineApplication } from 'twenty-sdk';
|
|
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
|
|
|
export default defineApplication({
|
|
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
|
displayName: 'My Twenty App',
|
|
description: 'My first Twenty app',
|
|
icon: 'IconWorld',
|
|
applicationVariables: {
|
|
DEFAULT_RECIPIENT_NAME: {
|
|
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
|
description: 'Default recipient name for postcards',
|
|
value: 'Jane Doe',
|
|
isSecret: false,
|
|
},
|
|
},
|
|
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
|
});
|
|
```
|
|
|
|
노트:
|
|
|
|
* `universalIdentifier` 필드는 고유하고 결정적인 ID입니다. 한 번 생성한 후 동기화 전반에 걸쳐 안정적으로 유지하세요.
|
|
* `applicationVariables`는 함수의 환경 변수가 됩니다(예: `DEFAULT_RECIPIENT_NAME`는 `process.env.DEFAULT_RECIPIENT_NAME`로 사용 가능).
|
|
* `defaultRoleUniversalIdentifier`는 `*.role.ts` 파일에서 정의한 역할과 일치해야 합니다(아래 참조).
|
|
|
|
#### 역할 및 권한
|
|
|
|
애플리케이션은 워크스페이스의 객체와 작업에 대한 권한을 캡슐화하는 역할을 정의할 수 있습니다. `application.config.ts`의 `defaultRoleUniversalIdentifier` 필드는 앱의 로직 함수가 사용하는 기본 역할을 지정합니다.
|
|
|
|
* `TWENTY_API_KEY`로 주입되는 런타임 API 키는 이 기본 함수 역할에서 파생됩니다.
|
|
* 타입드 클라이언트는 해당 역할에 부여된 권한으로 제한됩니다.
|
|
* 최소 권한 원칙을 따르세요. 함수에 필요한 권한만 가진 전용 역할을 만들고, 해당 역할의 universal identifier를 참조하세요.
|
|
|
|
##### 기본 함수 역할(\*.role.ts)
|
|
|
|
새 앱을 스캐폴딩하면, CLI가 기본 역할 파일도 생성합니다. `defineRole()`을 사용해 내장 검증과 함께 역할을 정의하세요:
|
|
|
|
```typescript
|
|
// src/app/default-function.role.ts
|
|
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
|
|
|
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
|
'b648f87b-1d26-4961-b974-0908fd991061';
|
|
|
|
export default defineRole({
|
|
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
|
label: 'Default function role',
|
|
description: 'Default role for function Twenty client',
|
|
canReadAllObjectRecords: false,
|
|
canUpdateAllObjectRecords: false,
|
|
canSoftDeleteAllObjectRecords: false,
|
|
canDestroyAllObjectRecords: false,
|
|
canUpdateAllSettings: false,
|
|
canBeAssignedToAgents: false,
|
|
canBeAssignedToUsers: false,
|
|
canBeAssignedToApiKeys: false,
|
|
objectPermissions: [
|
|
{
|
|
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
|
canReadObjectRecords: true,
|
|
canUpdateObjectRecords: true,
|
|
canSoftDeleteObjectRecords: false,
|
|
canDestroyObjectRecords: false,
|
|
},
|
|
],
|
|
fieldPermissions: [
|
|
{
|
|
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
|
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
|
canReadFieldValue: false,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
permissionFlags: [PermissionFlag.APPLICATIONS],
|
|
});
|
|
```
|
|
|
|
이 역할의 `universalIdentifier`는 `application.config.ts`에서 `defaultRoleUniversalIdentifier`로 참조됩니다. 다시 말해:
|
|
|
|
* **\*.role.ts**는 기본 함수 역할이 수행할 수 있는 작업을 정의합니다.
|
|
* **application.config.ts**는 해당 역할을 가리키므로, 함수는 그 권한을 상속받습니다.
|
|
|
|
노트:
|
|
|
|
* 스캐폴딩된 역할에서 시작하여, 최소 권한 원칙에 따라 점진적으로 제한하세요.
|
|
* `objectPermissions`와 `fieldPermissions`를 함수에 필요한 객체/필드로 교체하세요.
|
|
* `permissionFlags`는 플랫폼 수준 기능에 대한 액세스를 제어합니다. 최소한으로 유지하고, 필요한 것만 추가하세요.
|
|
* 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).
|
|
|
|
### 로직 함수 구성과 엔트리포인트
|
|
|
|
각 함수 파일은 `defineFunction()`을 사용해 핸들러와 선택적 트리거가 포함된 구성을 내보냅니다. 자동 감지를 위해 `*.function.ts` 파일 접미사를 사용하세요.
|
|
|
|
```typescript
|
|
// src/app/createPostCard.function.ts
|
|
import { defineFunction } from 'twenty-sdk';
|
|
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
|
import Twenty, { type Person } from '~/generated';
|
|
|
|
const handler = async (params: RoutePayload) => {
|
|
const client = new Twenty(); // generated typed client
|
|
const name = 'name' in params.queryStringParameters
|
|
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
|
: 'Hello world';
|
|
|
|
const result = await client.mutation({
|
|
createPostCard: {
|
|
__args: { data: { name } },
|
|
id: true,
|
|
name: true,
|
|
},
|
|
});
|
|
return result;
|
|
};
|
|
|
|
export default defineFunction({
|
|
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
|
name: 'create-new-post-card',
|
|
timeoutSeconds: 2,
|
|
handler,
|
|
triggers: [
|
|
// Public HTTP route trigger '/s/post-card/create'
|
|
{
|
|
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
|
type: 'route',
|
|
path: '/post-card/create',
|
|
httpMethod: 'GET',
|
|
isAuthRequired: false,
|
|
},
|
|
// Cron trigger (CRON pattern)
|
|
// {
|
|
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
|
// type: 'cron',
|
|
// pattern: '0 0 1 1 *',
|
|
// },
|
|
// Database event trigger
|
|
// {
|
|
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
|
// type: 'databaseEvent',
|
|
// eventName: 'person.updated',
|
|
// updatedFields: ['name'],
|
|
// },
|
|
],
|
|
});
|
|
```
|
|
|
|
일반적인 트리거 유형:
|
|
|
|
* **route**: **`/s/` 엔드포인트** 아래에서 HTTP 경로와 메서드로 함수를 노출합니다:
|
|
|
|
> 예: `path: '/post-card/create',` -> `<APP_URL>/s/post-card/create`에서 호출
|
|
|
|
* **cron**: CRON 식을 사용하여 예약된 일정으로 함수를 실행합니다.
|
|
* **databaseEvent**: 워크스페이스 객체 라이프사이클 이벤트에서 실행됩니다. 이벤트 작업이 `updated`인 경우, 수신할 특정 필드를 `updatedFields` 배열에 지정할 수 있습니다. 정의하지 않거나 비워두면, 어떤 업데이트든 함수가 트리거됩니다.
|
|
|
|
> 예: `person.updated`
|
|
|
|
노트:
|
|
|
|
* `triggers` 배열은 선택 사항입니다. 트리거가 없는 함수는 다른 함수에서 호출되는 유틸리티 함수로 사용할 수 있습니다.
|
|
* 하나의 함수에서 여러 트리거 유형을 혼합할 수 있습니다.
|
|
|
|
### 라우트 트리거 페이로드
|
|
|
|
<Warning>
|
|
**호환성 파괴적 변경(v1.16, 2026년 1월):** 라우트 트리거 페이로드 형식이 변경되었습니다. v1.16 이전에는 쿼리 매개변수, 경로 매개변수, 그리고 본문이 페이로드로 직접 전송되었습니다. v1.16부터는 이들이 구조화된 `RoutePayload` 객체 내부에 중첩됩니다.
|
|
|
|
**v1.16 이전:**
|
|
|
|
```typescript
|
|
const handler = async (params) => {
|
|
const { param1, param2 } = params; // Direct access
|
|
};
|
|
```
|
|
|
|
**v1.16 이후:**
|
|
|
|
```typescript
|
|
const handler = async (event: RoutePayload) => {
|
|
const { param1, param2 } = event.body; // Access via .body
|
|
const { queryParam } = event.queryStringParameters;
|
|
const { id } = event.pathParameters;
|
|
};
|
|
```
|
|
|
|
**기존 함수 마이그레이션 방법:** 핸들러에서 params 객체에서 직접 구조 분해하는 대신 `event.body`, `event.queryStringParameters`, 또는 `event.pathParameters`에서 구조 분해하도록 업데이트하세요.
|
|
</Warning>
|
|
|
|
라우트 트리거가 로직 함수를 호출하면, AWS HTTP API v2 형식을 따르는 `RoutePayload` 객체를 받습니다. `twenty-sdk`에서 해당 타입을 임포트하세요:
|
|
|
|
```typescript
|
|
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
|
|
|
const handler = async (event: RoutePayload) => {
|
|
// Access request data
|
|
const { headers, queryStringParameters, pathParameters, body } = event;
|
|
|
|
// HTTP method and path are available in requestContext
|
|
const { method, path } = event.requestContext.http;
|
|
|
|
return { message: 'Success' };
|
|
};
|
|
```
|
|
|
|
`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` | 원시 요청 경로 |
|
|
|
|
### HTTP 헤더 전달
|
|
|
|
기본적으로 보안상의 이유로 들어오는 요청의 HTTP 헤더는 로직 함수로 **전달되지 않습니다**. 특정 헤더에 접근하려면 `forwardedRequestHeaders` 배열에 명시적으로 나열하세요:
|
|
|
|
```typescript
|
|
export default defineFunction({
|
|
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
|
name: 'webhook-handler',
|
|
handler,
|
|
triggers: [
|
|
{
|
|
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
|
type: 'route',
|
|
path: '/webhook',
|
|
httpMethod: 'POST',
|
|
isAuthRequired: false,
|
|
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
핸들러에서 다음과 같이 해당 헤더에 접근할 수 있습니다:
|
|
|
|
```typescript
|
|
const handler = async (event: RoutePayload) => {
|
|
const signature = event.headers['x-webhook-signature'];
|
|
const contentType = event.headers['content-type'];
|
|
|
|
// Validate webhook signature...
|
|
return { received: true };
|
|
};
|
|
```
|
|
|
|
<Note>
|
|
헤더 이름은 소문자로 정규화됩니다. 소문자 키를 사용해 접근하세요(예: `event.headers['content-type']`).
|
|
</Note>
|
|
|
|
새 함수를 만드는 방법은 두 가지입니다:
|
|
|
|
* **스캐폴딩**: `yarn entity:add`를 실행하고 새 함수를 추가하는 옵션을 선택하세요. 이렇게 하면 핸들러와 구성이 포함된 시작 파일이 생성됩니다.
|
|
* **수동**: 새 `*.function.ts` 파일을 만들고 동일한 패턴에 따라 `defineFunction()`을 사용하세요.
|
|
|
|
### 생성된 타입드 클라이언트
|
|
|
|
워크스페이스 스키마를 기반으로 generated/에 로컬 타입드 클라이언트를 생성하려면 yarn app:generate를 실행하세요. 함수에서 사용하세요:
|
|
|
|
```typescript
|
|
import Twenty from '~/generated';
|
|
|
|
const client = new Twenty();
|
|
const { me } = await client.query({ me: { id: true, displayName: true } });
|
|
```
|
|
|
|
클라이언트는 `yarn app:generate`로 다시 생성됩니다. 객체를 변경한 후 또는 새 워크스페이스에 온보딩할 때 다시 실행하세요.
|
|
|
|
#### 로직 함수의 런타임 자격 증명
|
|
|
|
함수가 Twenty에서 실행될 때, 플랫폼은 코드가 실행되기 전에 자격 증명을 환경 변수로 주입합니다:
|
|
|
|
* `TWENTY_API_URL`: 앱이 대상으로 하는 Twenty API의 기본 URL.
|
|
* `TWENTY_API_KEY`: 애플리케이션의 기본 함수 역할 범위로 제한된 단기 키.
|
|
|
|
노트:
|
|
|
|
* 생성된 클라이언트에 URL이나 API 키를 전달할 필요가 없습니다. 런타임에 process.env에서 `TWENTY_API_URL`과 `TWENTY_API_KEY`를 읽습니다.
|
|
* API 키의 권한은 `application.config.ts`에서 `defaultRoleUniversalIdentifier`를 통해 참조된 역할에 의해 결정됩니다. 이는 애플리케이션의 로직 함수에서 사용하는 기본 역할입니다.
|
|
* 애플리케이션은 최소 권한 원칙을 따르도록 역할을 정의할 수 있습니다. 함수에 필요한 권한만 부여하고, `defaultRoleUniversalIdentifier`를 해당 역할의 universal identifier로 지정하세요.
|
|
|
|
### Hello World 예제
|
|
|
|
객체, 함수, 여러 트리거를 보여주는 최소한의 엔드투엔드 예제를 [여기](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world)에서 살펴보세요:
|
|
|
|
## 수동 설정(스캐폴더 없이)
|
|
|
|
최적의 시작 경험을 위해 `create-twenty-app` 사용을 권장하지만, 프로젝트를 수동으로 설정할 수도 있습니다. CLI를 전역으로 설치하지 마세요. 대신 `twenty-sdk`를 로컬 종속성으로 추가하고 package.json에 스크립트를 연결하세요:
|
|
|
|
```bash filename="Terminal"
|
|
yarn add -D twenty-sdk
|
|
```
|
|
|
|
그런 다음 다음과 같은 스크립트를 추가하세요:
|
|
|
|
```json filename="package.json"
|
|
{
|
|
"scripts": {
|
|
"auth:login": "twenty auth:login",
|
|
"auth:logout": "twenty auth:logout",
|
|
"auth:status": "twenty auth:status",
|
|
"auth:switch": "twenty auth:switch",
|
|
"auth:list": "twenty auth:list",
|
|
"app:dev": "twenty app:dev",
|
|
"app:generate": "twenty app:generate",
|
|
"app:uninstall": "twenty app:uninstall",
|
|
"entity:add": "twenty entity:add",
|
|
"function:logs": "twenty function:logs",
|
|
"function:execute": "twenty function:execute",
|
|
"help": "twenty help"
|
|
}
|
|
}
|
|
```
|
|
|
|
이제 Yarn을 통해 동일한 명령을 실행할 수 있습니다. 예: `yarn app:dev`, `yarn app:generate` 등.
|
|
|
|
## 문제 해결
|
|
|
|
* 인증 오류: `yarn auth:login`를 실행하고 API 키에 필요한 권한이 있는지 확인하세요.
|
|
* 서버에 연결할 수 없음: API URL과 Twenty 서버에 접근 가능한지 확인하세요.
|
|
* 타입 또는 클라이언트가 없거나 오래된 경우: `yarn app:generate`를 실행하세요.
|
|
* 개발 모드가 동기화되지 않음: `yarn app:dev`가 실행 중인지, 환경에서 변경 사항을 무시하지 않는지 확인하세요.
|
|
|
|
Discord 도움말 채널: https://discord.com/channels/1130383047699738754/1130386664812982322
|