Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d68868df5 | ||
|
|
4dfb4a8fcb | ||
|
|
c5be4d2d60 | ||
|
|
70aae670f4 | ||
|
|
668cd7593b | ||
|
|
4180ba969b | ||
|
|
5065d75a6c | ||
|
|
715eed85be | ||
|
|
03651cab6c | ||
|
|
493e6a7f8f | ||
|
|
c4775450f5 | ||
|
|
cb6957fa51 | ||
|
|
9633740132 |
@@ -22,51 +22,24 @@ Examples of existing syncable entities: `skill`, `agent`, `view`, `viewField`, `
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Creating a New Syncable Entity](#creating-a-new-syncable-entity)
|
||||
- [What is a Syncable Entity?](#what-is-a-syncable-entity)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Overview](#overview)
|
||||
- [Key Design Principle: Separation of Concerns](#key-design-principle-separation-of-concerns)
|
||||
- [File Structure](#file-structure)
|
||||
- [Step-by-Step Implementation](#step-by-step-implementation)
|
||||
- [Step 1: Add Metadata Name Constant (twenty-shared)](#step-1-add-metadata-name-constant-twenty-shared)
|
||||
- [Step 2: Create TypeORM Entity](#step-2-create-typeorm-entity)
|
||||
- [Step 2b: Using JsonbProperty and SerializedRelation Types](#step-2b-using-jsonbproperty-and-serializedrelation-types)
|
||||
- [JsonbProperty Wrapper](#jsonbproperty-wrapper)
|
||||
- [SerializedRelation Type](#serializedrelation-type)
|
||||
- [Complete Example](#complete-example)
|
||||
- [Step 3: Define Flat Entity Type](#step-3-define-flat-entity-type)
|
||||
- [Step 4: Define Editable Properties](#step-4-define-editable-properties)
|
||||
- [Step 5: Register in Central Constants](#step-5-register-in-central-constants)
|
||||
- [5a. All Flat Entity Types Registry](#5a-all-flat-entity-types-registry)
|
||||
- [5b. Properties to Compare and Stringify](#5b-properties-to-compare-and-stringify)
|
||||
- [5c. Metadata Relations](#5c-metadata-relations)
|
||||
- [5d. Required Metadata for Validation](#5d-required-metadata-for-validation)
|
||||
- [Step 6: Create Cache Service](#step-6-create-cache-service)
|
||||
- [Step 7: Create Flat Entity Module](#step-7-create-flat-entity-module)
|
||||
- [Step 8: Define Action Types](#step-8-define-action-types)
|
||||
- [Step 9: Create Validator Service](#step-9-create-validator-service)
|
||||
- [Step 10: Create Builder Service](#step-10-create-builder-service)
|
||||
- [Step 11: Create Action Handlers (Runner)](#step-11-create-action-handlers-runner)
|
||||
- [Step 12: Wire in Orchestrator Service (CRITICAL)](#step-12-wire-in-orchestrator-service-critical)
|
||||
- [Step 13: Register in Modules](#step-13-register-in-modules)
|
||||
- [13a. Builder Module](#13a-builder-module)
|
||||
- [13b. Validators Module](#13b-validators-module)
|
||||
- [13c. Action Handlers Module](#13c-action-handlers-module)
|
||||
- [Using the Entity in Services](#using-the-entity-in-services)
|
||||
- [Integration Tests](#integration-tests)
|
||||
- [Checklist](#checklist)
|
||||
- [Syncable Entity Requirements](#syncable-entity-requirements)
|
||||
- [JSONB Properties and Serialized Relations](#jsonb-properties-and-serialized-relations)
|
||||
- [Registration (twenty-shared)](#registration-twenty-shared)
|
||||
- [Flat Entity Definition](#flat-entity-definition)
|
||||
- [Central Constants Registration](#central-constants-registration)
|
||||
- [Cache Layer](#cache-layer)
|
||||
- [Migration Builder](#migration-builder)
|
||||
- [Migration Runner](#migration-runner)
|
||||
- [Orchestrator Wiring (⚠️ COMMONLY FORGOTTEN)](#orchestrator-wiring-️-commonly-forgotten)
|
||||
- [Module Registration](#module-registration)
|
||||
- [Testing](#testing)
|
||||
1. [Overview](#overview)
|
||||
2. [File Structure](#file-structure)
|
||||
3. [Step-by-Step Implementation](#step-by-step-implementation)
|
||||
- [Step 1: Add Metadata Name Constant](#step-1-add-metadata-name-constant-twenty-shared)
|
||||
- [Step 2: Create TypeORM Entity](#step-2-create-typeorm-entity)
|
||||
- [Step 3: Define Flat Entity Type](#step-3-define-flat-entity-type)
|
||||
- [Step 4: Define Editable Properties](#step-4-define-editable-properties)
|
||||
- [Step 5: Register in Central Constants](#step-5-register-in-central-constants)
|
||||
- [Step 6: Create Cache Service](#step-6-create-cache-service)
|
||||
- [Step 7: Create Flat Entity Module](#step-7-create-flat-entity-module)
|
||||
- [Step 8: Define Action Types](#step-8-define-action-types)
|
||||
- [Step 9: Create Validator Service](#step-9-create-validator-service)
|
||||
- [Step 10: Create Builder Service](#step-10-create-builder-service)
|
||||
- [Step 11: Create Action Handlers](#step-11-create-action-handlers-runner)
|
||||
- [Step 12: Wire in Orchestrator Service](#step-12-wire-in-orchestrator-service-critical)
|
||||
- [Step 13: Register in Modules](#step-13-register-in-modules)
|
||||
4. [Using the Entity in Services](#using-the-entity-in-services)
|
||||
5. [Integration Tests](#integration-tests)
|
||||
|
||||
---
|
||||
|
||||
@@ -203,6 +176,9 @@ export class MyEntityEntity
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
standardId: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@@ -261,157 +237,6 @@ export abstract class WorkspaceRelatedEntity {
|
||||
|
||||
---
|
||||
|
||||
### Step 2b: Using JsonbProperty and SerializedRelation Types
|
||||
|
||||
When your entity has JSONB columns or stores foreign key references inside JSONB structures, you must use the branded type wrappers to enable automatic universal identifier mapping.
|
||||
|
||||
#### JsonbProperty Wrapper
|
||||
|
||||
Wrap all JSONB column types with `JsonbProperty<T>` to mark them for the universal entity transformation system:
|
||||
|
||||
```typescript
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
@Entity('myEntity')
|
||||
export class MyEntityEntity extends SyncableEntity {
|
||||
// Simple JSONB column - wrap the type
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
settings: JsonbProperty<MyEntitySettings> | null;
|
||||
|
||||
// JSONB column with complex type
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<MyEntityConfiguration>;
|
||||
|
||||
// Array stored as JSONB
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
tags: JsonbProperty<string[]> | null;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use `JsonbProperty<T>`:**
|
||||
- Any column with `type: 'jsonb'` that stores an object or array
|
||||
- Configuration objects, settings, metadata blobs
|
||||
- Any structured data stored as JSON in the database
|
||||
|
||||
**What it enables:**
|
||||
- The type system can identify which properties are JSONB columns
|
||||
- Automatic transformation of serialized relations within JSONB structures
|
||||
- Type-safe universal entity mapping
|
||||
|
||||
#### SerializedRelation Type
|
||||
|
||||
Use `SerializedRelation` for properties **inside JSONB structures** that store foreign key references (entity IDs):
|
||||
|
||||
```typescript
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// Define the JSONB structure type
|
||||
type MyEntityConfiguration = {
|
||||
name: string;
|
||||
// This stores a reference to another field's ID - use SerializedRelation
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
// This stores a reference to an object's ID
|
||||
sourceObjectMetadataId: SerializedRelation;
|
||||
// Regular string - NOT a foreign key reference
|
||||
displayFormat: string;
|
||||
};
|
||||
|
||||
@Entity('myEntity')
|
||||
export class MyEntityEntity extends SyncableEntity {
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<MyEntityConfiguration>;
|
||||
}
|
||||
```
|
||||
|
||||
**When to use `SerializedRelation`:**
|
||||
- Properties inside JSONB that store UUIDs referencing other entities
|
||||
- Foreign key relationships that can't use TypeORM relations (because they're in JSONB)
|
||||
- Any `*Id` property inside a JSONB structure that references another metadata entity
|
||||
|
||||
**What it enables:**
|
||||
- Automatic renaming from `*Id` to `*UniversalIdentifier` in universal entities
|
||||
- Type-safe extraction of serialized relation properties
|
||||
- Proper handling during workspace sync/migration
|
||||
|
||||
#### Complete Example
|
||||
|
||||
```typescript
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
// JSONB structure with serialized relations
|
||||
type WidgetConfiguration = {
|
||||
title: string;
|
||||
// Foreign keys stored in JSONB - use SerializedRelation
|
||||
fieldMetadataId: SerializedRelation;
|
||||
objectMetadataId: SerializedRelation;
|
||||
// Optional foreign key
|
||||
viewId?: SerializedRelation;
|
||||
// Regular properties (not foreign keys)
|
||||
displayMode: 'compact' | 'expanded';
|
||||
maxItems: number;
|
||||
};
|
||||
|
||||
type GridPosition = {
|
||||
row: number;
|
||||
column: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
@Entity('widget')
|
||||
export class WidgetEntity extends SyncableEntity implements Required<WidgetEntity> {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
// JSONB column with serialized relations - wrap with JsonbProperty
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
configuration: JsonbProperty<WidgetConfiguration>;
|
||||
|
||||
// JSONB column without serialized relations - still wrap with JsonbProperty
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
gridPosition: JsonbProperty<GridPosition>;
|
||||
|
||||
@Column({ default: false })
|
||||
isCustom: boolean;
|
||||
|
||||
// ... other columns
|
||||
}
|
||||
```
|
||||
|
||||
**Result in Universal Entity:**
|
||||
|
||||
When transformed to a universal entity, the `configuration` property will have its `SerializedRelation` fields automatically renamed:
|
||||
|
||||
```typescript
|
||||
// Original (in database/flat entity)
|
||||
{
|
||||
fieldMetadataId: "abc-123",
|
||||
objectMetadataId: "def-456",
|
||||
viewId: "ghi-789",
|
||||
displayMode: "compact",
|
||||
maxItems: 10,
|
||||
}
|
||||
|
||||
// Transformed (in universal entity)
|
||||
{
|
||||
fieldMetadataUniversalIdentifier: "abc-123",
|
||||
objectMetadataUniversalIdentifier: "def-456",
|
||||
viewUniversalIdentifier: "ghi-789",
|
||||
displayMode: "compact",
|
||||
maxItems: 10,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Define Flat Entity Type
|
||||
|
||||
**File:** `src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts`
|
||||
@@ -692,7 +517,7 @@ import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
|
||||
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
|
||||
import { MyEntityExceptionCode } from 'src/engine/metadata-modules/my-entity/my-entity.exception';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
@@ -760,7 +585,7 @@ export class FlatMyEntityValidatorService {
|
||||
}
|
||||
|
||||
// Prevent deletion of standard entities unless it's a system build
|
||||
if (!buildOptions.isSystemBuild && belongsToTwentyStandardApp(existingEntity)) {
|
||||
if (!buildOptions.isSystemBuild && isStandardMetadata(existingEntity)) {
|
||||
validationResult.errors.push({
|
||||
code: MyEntityExceptionCode.MY_ENTITY_IS_STANDARD,
|
||||
message: t`Cannot delete standard entity`,
|
||||
@@ -913,7 +738,7 @@ export class WorkspaceMigrationMyEntityActionsBuilderService extends WorkspaceEn
|
||||
type: 'update',
|
||||
metadataName: 'myEntity',
|
||||
entityId: flatEntityId,
|
||||
update: flatEntityUpdates,
|
||||
updates: flatEntityUpdates,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -1318,11 +1143,6 @@ Before considering your syncable entity complete, verify:
|
||||
- [ ] Entity has `isCustom` boolean column
|
||||
- [ ] Entity-to-flat transform sets `universalIdentifier` correctly (`standardId || id`)
|
||||
|
||||
### JSONB Properties and Serialized Relations
|
||||
- [ ] All JSONB columns are wrapped with `JsonbProperty<T>`
|
||||
- [ ] Foreign key references inside JSONB structures use `SerializedRelation` type
|
||||
- [ ] JSONB structure types are properly defined with `SerializedRelation` for `*Id` properties
|
||||
|
||||
### Registration (twenty-shared)
|
||||
- [ ] Metadata name added to `ALL_METADATA_NAME`
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -14,8 +14,8 @@ concurrency:
|
||||
|
||||
env:
|
||||
# restore-cache action adds 'v4-' prefix and '-<branch>-<sha>' suffix to the key
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-ubuntu-latest-8-cores-runner
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-ubuntu-latest-8-cores-runner-${{ github.ref_name }}-${{ github.sha }}
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-depot-ubuntu-24.04-8-runner-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -27,7 +27,6 @@ jobs:
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-sdk/**
|
||||
changed-files-check-e2e:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
@@ -39,7 +38,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
steps:
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -86,7 +85,6 @@ jobs:
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-ui
|
||||
npx nx build twenty-sdk
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
@@ -140,7 +138,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
if: false
|
||||
needs: front-sb-build
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
@@ -206,7 +204,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
steps:
|
||||
|
||||
@@ -25,7 +25,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/[email protected]
|
||||
@@ -39,62 +39,64 @@ 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:
|
||||
tag: scope:sdk
|
||||
tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: [changed-files-check, sdk-test]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
NODE_ENV: test
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Server / Create Test DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: SDK / Run e2e Tests
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: test:e2e
|
||||
# TODO: Re-enable sdk-e2e-test once application sync is stable
|
||||
# sdk-e2e-test:
|
||||
# timeout-minutes: 30
|
||||
# runs-on: depot-ubuntu-24.04-8
|
||||
# needs: [changed-files-check, sdk-test]
|
||||
# if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
# services:
|
||||
# postgres:
|
||||
# image: twentycrm/twenty-postgres-spilo
|
||||
# env:
|
||||
# PGUSER_SUPERUSER: postgres
|
||||
# PGPASSWORD_SUPERUSER: postgres
|
||||
# ALLOW_NOSSL: 'true'
|
||||
# SPILO_PROVIDER: 'local'
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# redis:
|
||||
# image: redis
|
||||
# ports:
|
||||
# - 6379:6379
|
||||
# env:
|
||||
# NODE_ENV: test
|
||||
# steps:
|
||||
# - name: Fetch custom Github Actions and base branch history
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: Install dependencies
|
||||
# uses: ./.github/actions/yarn-install
|
||||
# - name: Server / Append billing config to .env.test
|
||||
# working-directory: packages/twenty-server
|
||||
# run: |
|
||||
# echo "" >> .env.test
|
||||
# echo "IS_BILLING_ENABLED=true" >> .env.test
|
||||
# echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
|
||||
# echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
# echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
|
||||
# echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
|
||||
# - name: Server / Create Test DB
|
||||
# run: |
|
||||
# PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
# - name: SDK / Run E2E Tests
|
||||
# run: npx nx test:e2e twenty-sdk
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, sdk-test, sdk-e2e-test]
|
||||
needs: [changed-files-check, sdk-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: server-setup
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: server-setup
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
repository_dispatch:
|
||||
types: [claude-core-team-issues]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
claude_args: '--max-turns 200 --model opus --allowedTools "Edit,Write,WebFetch,Bash(bash packages/twenty-utils/setup-dev-env.sh),Bash(npx nx *),Bash(npx jest *),Bash(yarn *),Bash(git *),Bash(gh *),Bash(sed *),Bash(python3 *),Bash(rm *),Bash(find *),Bash(grep *),Bash(cat *),Bash(ls *),Bash(head *),Bash(tail *),Bash(wc *),Bash(sort *),Bash(uniq *),Bash(mkdir *),Bash(cp *),Bash(mv *),Bash(touch *),Bash(chmod *),Bash(echo *),Bash(curl *),Bash(cd *),Bash(pwd *),Bash(diff *),Bash(xargs *),Bash(awk *),Bash(cut *),Bash(tee *),Bash(tr *)"'
|
||||
settings: |
|
||||
{
|
||||
"env": {
|
||||
"PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default"
|
||||
}
|
||||
}
|
||||
- name: Post Create-PR link if Claude ran out of turns
|
||||
if: failure()
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH=$(git branch --show-current)
|
||||
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "" ]; then
|
||||
exit 0
|
||||
fi
|
||||
AHEAD=$(git rev-list --count main.."$BRANCH" 2>/dev/null || echo "0")
|
||||
if [ "$AHEAD" = "0" ]; then
|
||||
exit 0
|
||||
fi
|
||||
EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
ISSUE_NUMBER="${{ github.event.issue.number || github.event.pull_request.number }}"
|
||||
ENCODED_BRANCH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$BRANCH', safe=''))")
|
||||
PR_URL="https://github.com/${{ github.repository }}/compare/main...${ENCODED_BRANCH}?quick_pull=1"
|
||||
BODY="⚠️ Claude ran out of turns before creating a PR. Work has been pushed to [\`$BRANCH\`](https://github.com/${{ github.repository }}/tree/$ENCODED_BRANCH).\n\n[**Create PR →**]($PR_URL)"
|
||||
if [ -n "$ISSUE_NUMBER" ]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$(echo -e "$BODY")"
|
||||
fi
|
||||
|
||||
claude-cross-repo:
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build prompt from dispatch payload
|
||||
id: prompt
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const p = context.payload.client_payload;
|
||||
let prompt;
|
||||
if (p.comment_body) {
|
||||
prompt = `You are responding to a comment on issue #${p.issue_number} ("${p.issue_title}") in the ${p.repo_full_name} repository.\n\nThe comment by @${p.sender} says:\n\n${p.comment_body}\n\nIssue body:\n\n${p.issue_body}\n\nPlease help with this request. The code you are working with is the twenty codebase (this repository).`;
|
||||
} else {
|
||||
prompt = `You are responding to issue #${p.issue_number} ("${p.issue_title}") in the ${p.repo_full_name} repository, opened by @${p.sender}.\n\nIssue body:\n\n${p.issue_body}\n\nPlease help with this request. The code you are working with is the twenty codebase (this repository).`;
|
||||
}
|
||||
core.setOutput('prompt', prompt);
|
||||
core.setOutput('repo', p.repo_full_name);
|
||||
core.setOutput('issue_number', p.issue_number);
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: ${{ steps.prompt.outputs.prompt }}
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
claude_args: '--max-turns 200 --model opus --allowedTools "Edit,Write,WebFetch,Bash(bash packages/twenty-utils/setup-dev-env.sh),Bash(npx nx *),Bash(npx jest *),Bash(yarn *),Bash(git *),Bash(gh *),Bash(sed *),Bash(python3 *),Bash(rm *),Bash(find *),Bash(grep *),Bash(cat *),Bash(ls *),Bash(head *),Bash(tail *),Bash(wc *),Bash(sort *),Bash(uniq *),Bash(mkdir *),Bash(cp *),Bash(mv *),Bash(touch *),Bash(chmod *),Bash(echo *),Bash(curl *),Bash(cd *),Bash(pwd *),Bash(diff *),Bash(xargs *),Bash(awk *),Bash(cut *),Bash(tee *),Bash(tr *)"'
|
||||
settings: |
|
||||
{
|
||||
"env": {
|
||||
"PG_DATABASE_URL": "postgres://postgres:postgres@localhost:5432/default"
|
||||
}
|
||||
}
|
||||
- name: Post response to source issue
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.TWENTY_DISPATCH_TOKEN }}
|
||||
script: |
|
||||
const [owner, repo] = '${{ steps.prompt.outputs.repo }}'.split('/');
|
||||
const issueNumber = parseInt('${{ steps.prompt.outputs.issue_number }}', 10);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: `Claude finished processing this request. [See workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
|
||||
});
|
||||
Vendored
-22
@@ -22,28 +22,6 @@
|
||||
"close": false
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "twenty-server - run unit test file",
|
||||
"type": "shell",
|
||||
"command": "npx nx run twenty-server:jest -- --config ./jest.config.mjs ${relativeFile} --silent=false ${input:watchMode} ${input:updateSnapshot}",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"env": {
|
||||
"NODE_ENV": "test",
|
||||
"NODE_OPTIONS": "--max-old-space-size=12288 --import tsx/esm"
|
||||
},
|
||||
"shell": {
|
||||
"executable": "/bin/zsh",
|
||||
"args": ["-l", "-c"]
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"close": false
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -21,31 +21,30 @@ npx nx run twenty-server:worker # Start background worker
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Preferred: run a single test file (fast)
|
||||
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
|
||||
|
||||
# Run all tests for a package
|
||||
# Run tests
|
||||
npx nx test twenty-front # Frontend unit tests
|
||||
npx nx test twenty-server # Backend unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
|
||||
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front
|
||||
npx nx storybook:test twenty-front
|
||||
npx nx storybook:build twenty-front # Build Storybook
|
||||
npx nx storybook:test twenty-front # Run Storybook tests
|
||||
|
||||
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
|
||||
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Linting (diff with main - fastest, always prefer this)
|
||||
npx nx lint:diff-with-main twenty-front
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
|
||||
# Linting (diff with main - fastest)
|
||||
npx nx lint:diff-with-main twenty-front # Lint only files changed vs main
|
||||
npx nx lint:diff-with-main twenty-server # Lint only files changed vs main
|
||||
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix files changed vs main
|
||||
|
||||
# Linting (full project - slower, use only when needed)
|
||||
npx nx lint twenty-front
|
||||
npx nx lint twenty-server
|
||||
# Linting (full project)
|
||||
npx nx lint twenty-front # Lint all files in frontend
|
||||
npx nx lint twenty-server # Lint all files in backend
|
||||
npx nx lint twenty-front --fix # Auto-fix all linting issues
|
||||
|
||||
# Type checking
|
||||
npx nx typecheck twenty-front
|
||||
@@ -58,8 +57,7 @@ npx nx fmt twenty-server
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build packages (twenty-shared must be built first)
|
||||
npx nx build twenty-shared
|
||||
# Build packages
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
@@ -71,7 +69,7 @@ npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
|
||||
# Generate migration (replace [name] with kebab-case descriptive name)
|
||||
# Generate migration
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
|
||||
# Sync metadata
|
||||
@@ -80,9 +78,8 @@ npx nx run twenty-server:command workspace:sync-metadata
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
# Generate GraphQL types
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
@@ -110,36 +107,13 @@ packages/
|
||||
- **Named exports only** (no default exports)
|
||||
- **Types over interfaces** (except when extending third-party interfaces)
|
||||
- **String literals over enums** (except for GraphQL enums)
|
||||
- **No 'any' type allowed** — strict TypeScript enforced
|
||||
- **No 'any' type allowed**
|
||||
- **Event handlers preferred over useEffect** for state updates
|
||||
- **Props down, events up** — unidirectional data flow
|
||||
- **Composition over inheritance**
|
||||
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
|
||||
|
||||
### Naming Conventions
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
|
||||
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
|
||||
- **TypeScript generics**: descriptive names (`TData` not `T`)
|
||||
|
||||
### File Structure
|
||||
- Components under 300 lines, services under 500 lines
|
||||
- Components in their own directories with tests and stories
|
||||
- Use `index.ts` barrel exports for clean imports
|
||||
- Import order: external libraries first, then internal (`@/`), then relative
|
||||
|
||||
### Comments
|
||||
- Use short-form comments (`//`), not JSDoc blocks
|
||||
- Explain WHY (business logic), not WHAT
|
||||
- Do not comment obvious code
|
||||
- Multi-line comments use multiple `//` lines, not `/** */`
|
||||
|
||||
### State Management
|
||||
- **Recoil** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
|
||||
- **Recoil** for global state management
|
||||
- Component-specific state with React hooks
|
||||
- GraphQL cache managed by Apollo Client
|
||||
- Use functional state updates: `setState(prev => prev + 1)`
|
||||
|
||||
### Backend Architecture
|
||||
- **NestJS modules** for feature organization
|
||||
@@ -148,54 +122,36 @@ packages/
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Migrations
|
||||
### Database
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **TypeORM migrations** for schema management
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- Always generate migrations when changing entity files
|
||||
- Migration names must be kebab-case (e.g. `add-agent-turn-evaluation`)
|
||||
- Include both `up` and `down` logic in migrations
|
||||
- Never delete or rewrite committed migrations
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
|
||||
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure database migrations are generated for entity changes
|
||||
1. Always run linting and type checking after code changes
|
||||
2. Test changes with relevant test suites
|
||||
3. Ensure database migrations are properly structured
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Emotion** for styling with styled-components pattern
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
- Components should be in their own directories with tests and stories
|
||||
|
||||
### Testing Strategy
|
||||
- **Test behavior, not implementation** — focus on user perspective
|
||||
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
|
||||
- Query by user-visible elements (text, roles, labels) over test IDs
|
||||
- Use `@testing-library/user-event` for realistic interactions
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## CI Environment (GitHub Actions)
|
||||
|
||||
When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up.
|
||||
|
||||
- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh`
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
- The script is idempotent and safe to run multiple times.
|
||||
- **Unit tests** with Jest for both frontend and backend
|
||||
- **Integration tests** for critical backend workflows
|
||||
- **Storybook** for component development and testing
|
||||
- **E2E tests** with Playwright for critical user flows
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Detailed development guidelines and best practices
|
||||
- `.cursor/rules/` - Development guidelines and best practices
|
||||
|
||||
+2
-8
@@ -11,9 +11,7 @@ import unicornPlugin from 'eslint-plugin-unicorn';
|
||||
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
|
||||
import jsoncParser from 'jsonc-eslint-parser';
|
||||
|
||||
const twentyRules = await nxPlugin.loadWorkspaceRules(
|
||||
'packages/twenty-eslint-rules',
|
||||
);
|
||||
const twentyRules = await nxPlugin.loadWorkspaceRules('packages/twenty-eslint-rules');
|
||||
|
||||
export default [
|
||||
// Base JavaScript configuration
|
||||
@@ -67,10 +65,6 @@ export default [
|
||||
sourceTag: 'scope:sdk',
|
||||
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:create-app',
|
||||
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
@@ -203,7 +197,7 @@ export default [
|
||||
plugins: {
|
||||
...mdxPlugin.flat.plugins,
|
||||
'@nx': nxPlugin,
|
||||
twenty: { rules: twentyRules },
|
||||
'twenty': { rules: twentyRules },
|
||||
},
|
||||
},
|
||||
mdxPlugin.flatCodeBlocks,
|
||||
|
||||
@@ -15,12 +15,9 @@
|
||||
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
|
||||
|
||||
- Zero‑config project bootstrap
|
||||
- Preconfigured scripts for auth, dev mode (watch & sync), generate, uninstall, and function management
|
||||
- Preconfigured scripts for auth, generate, dev sync, one‑off sync, uninstall
|
||||
- Strong TypeScript support and typed client generation
|
||||
|
||||
## Documentation
|
||||
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
|
||||
## Prerequisites
|
||||
- Node.js 24+ (recommended) and Yarn 4
|
||||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
@@ -35,7 +32,7 @@ cd my-twenty-app
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Get help
|
||||
# Get Help
|
||||
yarn run help
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
@@ -47,33 +44,29 @@ yarn entity:add
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn function:logs
|
||||
# Or run a one‑time sync
|
||||
yarn app:sync
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn function:execute -n my-function -p '{"key": "value"}'
|
||||
# Watch your application's functions logs
|
||||
yarn function:logs
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
```
|
||||
|
||||
## What gets scaffolded
|
||||
- A minimal app structure ready for Twenty with example files:
|
||||
- `application-config.ts` - Application metadata configuration
|
||||
- `roles/default-role.ts` - Default role for logic functions
|
||||
- `logic-functions/hello-world.ts` - Example logic function with HTTP trigger
|
||||
- `front-components/hello-world.tsx` - Example front component
|
||||
- A minimal app structure ready for Twenty
|
||||
- TypeScript configuration
|
||||
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
|
||||
- Example placeholders to help you add entities, actions, and sync logic
|
||||
|
||||
## Next steps
|
||||
- Use `yarn auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn entity:add` (logic functions, front components, objects, roles).
|
||||
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- Explore the generated project and add your first entity with `yarn entity:add`.
|
||||
- Keep your types up‑to‑date using `yarn app:generate`.
|
||||
- Use `yarn app:dev` while you iterate to see changes instantly in your workspace.
|
||||
|
||||
|
||||
## Publish your application
|
||||
|
||||
@@ -1,20 +1,111 @@
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
js.configs.recommended,
|
||||
{
|
||||
ignores: ['**/dist/**'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js globals
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
// Browser globals that Node.js also has
|
||||
URL: 'readonly',
|
||||
URLSearchParams: 'readonly',
|
||||
// Node.js types
|
||||
NodeJS: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
rules: {
|
||||
...typescriptEslint.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
},
|
||||
},
|
||||
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js globals
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
// Jest globals
|
||||
describe: 'readonly',
|
||||
it: 'readonly',
|
||||
test: 'readonly',
|
||||
expect: 'readonly',
|
||||
jest: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
afterEach: 'readonly',
|
||||
beforeAll: 'readonly',
|
||||
afterAll: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
rules: {
|
||||
...typescriptEslint.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**'],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.5.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"package.json"
|
||||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build",
|
||||
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
|
||||
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
|
||||
"build": "npx rimraf dist && npx vite build"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty",
|
||||
@@ -38,7 +34,6 @@
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"twenty-shared": "workspace:*",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,7 +43,6 @@
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
|
||||
@@ -14,6 +14,12 @@ Then, start development mode to sync your app and watch for changes:
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Or run a one-time sync:
|
||||
|
||||
```bash
|
||||
yarn app:sync
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
@@ -23,12 +29,11 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
yarn auth:login # Authenticate with Twenty
|
||||
yarn auth:logout # Remove credentials
|
||||
yarn auth:status # Check auth status
|
||||
yarn auth:switch # Switch default workspace
|
||||
yarn auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn app:dev # Start dev mode (watch, build, and sync)
|
||||
yarn entity:add # Add a new entity (function, front-component, object, role)
|
||||
yarn app:dev # Start dev mode (sync + watch)
|
||||
yarn app:sync # One-time sync
|
||||
yarn entity:add # Add a new entity (function, object, role)
|
||||
yarn app:generate # Generate typed Twenty client
|
||||
yarn function:logs # Stream function logs
|
||||
yarn function:execute # Execute a function with JSON payload
|
||||
|
||||
@@ -12,9 +12,6 @@ jest.mock('fs-extra', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const APPLICATION_FILE_NAME = 'application-config.ts';
|
||||
const DEFAULT_ROLE_FILE_NAME = 'default-role.ts';
|
||||
|
||||
describe('copyBaseApplicationProject', () => {
|
||||
let testAppDirectory: string;
|
||||
|
||||
@@ -35,7 +32,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should create the correct folder structure with src/', async () => {
|
||||
it('should create the correct folder structure with src/app/', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
@@ -43,16 +40,16 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
// Verify src/ folder exists
|
||||
// Verify src/app/ folder exists
|
||||
const srcAppPath = join(testAppDirectory, 'src');
|
||||
expect(await fs.pathExists(srcAppPath)).toBe(true);
|
||||
|
||||
// Verify application-config.ts exists in src/
|
||||
const appConfigPath = join(srcAppPath, APPLICATION_FILE_NAME);
|
||||
// Verify application.config.ts exists in src/app/
|
||||
const appConfigPath = join(srcAppPath, 'application.config.ts');
|
||||
expect(await fs.pathExists(appConfigPath)).toBe(true);
|
||||
|
||||
// Verify default-role.ts exists in src/
|
||||
const roleConfigPath = join(srcAppPath, 'roles', DEFAULT_ROLE_FILE_NAME);
|
||||
// Verify default-function.role.ts exists in src/app/
|
||||
const roleConfigPath = join(srcAppPath, 'default-function.role.ts');
|
||||
expect(await fs.pathExists(roleConfigPath)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -70,7 +67,8 @@ describe('copyBaseApplicationProject', () => {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
expect(packageJson.name).toBe('my-test-app');
|
||||
expect(packageJson.version).toBe('0.1.0');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('0.5.0');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('0.3.1');
|
||||
expect(packageJson.scripts['app:sync']).toBe('twenty app:sync');
|
||||
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
|
||||
});
|
||||
|
||||
@@ -105,7 +103,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
expect(yarnLockContent).toContain('yarn lockfile v1');
|
||||
});
|
||||
|
||||
it('should create application-config.ts with defineApplication and correct values', async () => {
|
||||
it('should create application.config.ts with defineApp and correct values', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
@@ -113,18 +111,22 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
|
||||
const appConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'application.config.ts',
|
||||
);
|
||||
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
|
||||
|
||||
// Verify it uses defineApplication
|
||||
// Verify it uses defineApp
|
||||
expect(appConfigContent).toContain(
|
||||
"import { defineApplication } from 'twenty-sdk'",
|
||||
"import { defineApp } from 'twenty-sdk'",
|
||||
);
|
||||
expect(appConfigContent).toContain('export default defineApplication({');
|
||||
expect(appConfigContent).toContain('export default defineApp({');
|
||||
|
||||
// Verify it imports the role identifier
|
||||
expect(appConfigContent).toContain(
|
||||
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
|
||||
"import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role'",
|
||||
);
|
||||
|
||||
// Verify display name and description
|
||||
@@ -138,11 +140,11 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Verify it references the role
|
||||
expect(appConfigContent).toContain(
|
||||
'defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
|
||||
'functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
});
|
||||
|
||||
it('should create default-role.ts with defineRole and correct values', async () => {
|
||||
it('should create default-function.role.ts with defineRole and correct values', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
@@ -153,8 +155,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
const roleConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'roles',
|
||||
DEFAULT_ROLE_FILE_NAME,
|
||||
'default-function.role.ts',
|
||||
);
|
||||
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
|
||||
|
||||
@@ -166,7 +167,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Verify it exports the universal identifier constant
|
||||
expect(roleConfigContent).toContain(
|
||||
'export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER',
|
||||
'export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
|
||||
// Verify role label includes app name
|
||||
@@ -182,7 +183,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Verify it has a universalIdentifier (UUID format)
|
||||
expect(roleConfigContent).toMatch(
|
||||
/universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER/,
|
||||
/universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -210,7 +211,11 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
|
||||
const appConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'application.config.ts',
|
||||
);
|
||||
const appConfigContent = await fs.readFile(appConfigPath, 'utf8');
|
||||
|
||||
expect(appConfigContent).toContain("description: ''");
|
||||
@@ -239,11 +244,11 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Read both app configs
|
||||
const firstAppConfig = await fs.readFile(
|
||||
join(firstAppDir, 'src', APPLICATION_FILE_NAME),
|
||||
join(firstAppDir, 'src', 'application.config.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const secondAppConfig = await fs.readFile(
|
||||
join(secondAppDir, 'src', APPLICATION_FILE_NAME),
|
||||
join(secondAppDir, 'src', 'application.config.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
@@ -279,19 +284,19 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: secondAppDir,
|
||||
});
|
||||
|
||||
// Read both role configs
|
||||
const firstRoleConfig = await fs.readFile(
|
||||
join(firstAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
|
||||
join(firstAppDir, 'src', 'default-function.role.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const secondRoleConfig = await fs.readFile(
|
||||
join(secondAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
|
||||
join(secondAppDir, 'src', 'default-function.role.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// Extract UUIDs using regex
|
||||
const uuidRegex =
|
||||
/DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
||||
/DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
||||
const firstUuid = firstRoleConfig.match(uuidRegex)?.[1];
|
||||
const secondUuid = secondRoleConfig.match(uuidRegex)?.[1];
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
|
||||
const SRC_FOLDER = 'src';
|
||||
const APP_FOLDER = 'src';
|
||||
|
||||
export const copyBaseApplicationProject = async ({
|
||||
appName,
|
||||
@@ -22,45 +21,32 @@ export const copyBaseApplicationProject = async ({
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
await createPublicAssetDirectory(appDirectory);
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
|
||||
const appFolderPath = join(appDirectory, APP_FOLDER);
|
||||
|
||||
await fs.ensureDir(sourceFolderPath);
|
||||
await fs.ensureDir(appFolderPath);
|
||||
|
||||
await createDefaultRoleConfig({
|
||||
await createDefaultServerlessFunctionRoleConfig({
|
||||
displayName: appDisplayName,
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'roles',
|
||||
fileName: 'default-role.ts',
|
||||
appDirectory: appFolderPath,
|
||||
});
|
||||
|
||||
await createDefaultFrontComponent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'front-components',
|
||||
fileName: 'hello-world.tsx',
|
||||
appDirectory: appFolderPath,
|
||||
});
|
||||
|
||||
await createDefaultFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
fileName: 'hello-world.ts',
|
||||
appDirectory: appFolderPath,
|
||||
});
|
||||
|
||||
await createApplicationConfig({
|
||||
displayName: appDisplayName,
|
||||
description: appDescription,
|
||||
appDirectory: sourceFolderPath,
|
||||
fileName: 'application-config.ts',
|
||||
appDirectory: appFolderPath,
|
||||
});
|
||||
};
|
||||
|
||||
const createPublicAssetDirectory = async (appDirectory: string) => {
|
||||
await fs.ensureDir(join(appDirectory, ASSETS_DIR));
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
@@ -85,9 +71,7 @@ generated
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty/*
|
||||
!.twenty/output/
|
||||
.twenty
|
||||
|
||||
# production
|
||||
/build
|
||||
@@ -112,26 +96,22 @@ yarn-error.log*
|
||||
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
|
||||
};
|
||||
|
||||
const createDefaultRoleConfig = async ({
|
||||
const createDefaultServerlessFunctionRoleConfig = async ({
|
||||
displayName,
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
displayName: string;
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineRole } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: '${displayName} default function role',
|
||||
description: '${displayName} default function role',
|
||||
canReadAllObjectRecords: true,
|
||||
@@ -141,18 +121,13 @@ export default defineRole({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
await fs.writeFile(join(appDirectory, 'default-function.role.ts'), content);
|
||||
};
|
||||
|
||||
const createDefaultFrontComponent = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
@@ -175,72 +150,68 @@ export default defineFrontComponent({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
await fs.writeFile(
|
||||
join(appDirectory, 'hello-world.front-component.tsx'),
|
||||
content,
|
||||
);
|
||||
};
|
||||
|
||||
const createDefaultFunction = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
const triggerUniversalIdentifier = v4();
|
||||
|
||||
const content = `import { defineLogicFunction } from 'twenty-sdk';
|
||||
const content = `import { defineFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (): Promise<{ message: string }> => {
|
||||
return { message: 'Hello, World!' };
|
||||
};
|
||||
|
||||
// Logic function handler - rename and implement your logic
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'hello-world-logic-function',
|
||||
description: 'A simple logic function',
|
||||
name: 'hello-world-function',
|
||||
description: 'A sample serverless function',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/hello-world-logic-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '${triggerUniversalIdentifier}',
|
||||
type: 'route',
|
||||
path: '/hello-world-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
await fs.writeFile(join(appDirectory, 'hello-world.function.ts'), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const content = `import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
const content = `import { defineApp } from 'twenty-sdk';
|
||||
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
export default defineApp({
|
||||
universalIdentifier: '${v4()}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description ?? ''}',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
await fs.writeFile(join(appDirectory, 'application.config.ts'), content);
|
||||
};
|
||||
|
||||
const createPackageJson = async ({
|
||||
@@ -267,6 +238,8 @@ const createPackageJson = async ({
|
||||
'auth:switch': 'twenty auth:switch',
|
||||
'auth:list': 'twenty auth:list',
|
||||
'app:dev': 'twenty app:dev',
|
||||
'app:build': 'twenty app:build',
|
||||
'app:sync': 'twenty app:sync',
|
||||
'entity:add': 'twenty entity:add',
|
||||
'app:generate': 'twenty app:generate',
|
||||
'function:logs': 'twenty function:logs',
|
||||
@@ -277,13 +250,13 @@ const createPackageJson = async ({
|
||||
'lint:fix': 'eslint --fix',
|
||||
},
|
||||
dependencies: {
|
||||
'twenty-sdk': '0.5.0',
|
||||
'twenty-sdk': '0.3.1',
|
||||
},
|
||||
devDependencies: {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
'@types/react': '^19.0.2',
|
||||
react: '^19.0.2',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
},
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"types": ["jest", "node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"jsx": "react"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||
@@ -13,7 +13,7 @@ const config: ApplicationConfig = {
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -95,7 +95,7 @@ export class PostCard {
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
|
||||
@@ -14,7 +14,7 @@ export const functionRole: RoleConfig = {
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectNameSingular: 'postCard',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -23,8 +23,8 @@ export const functionRole: RoleConfig = {
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectNameSingular: 'postCard',
|
||||
fieldName: 'content',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
import { defineApp } from 'twenty-sdk';
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
export default defineApp({
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
|
||||
displayName: 'Self Hosting',
|
||||
description: 'Used to manage billing and telemetry of self-hosted instances',
|
||||
@@ -16,4 +16,6 @@ export default defineApp({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -8,25 +8,8 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"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:sync": "twenty app sync",
|
||||
"entity:add": "twenty entity add",
|
||||
"app:generate": "twenty app generate",
|
||||
"function:logs": "twenty function logs",
|
||||
"function:execute": "twenty function execute",
|
||||
"app:uninstall": "twenty app uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.3.1"
|
||||
"twenty-sdk": "0.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { FieldType, defineObject } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
|
||||
nameSingular: 'selfHostingUser',
|
||||
namePlural: 'selfHostingUsers',
|
||||
labelSingular: 'Self Hosting User',
|
||||
labelPlural: 'Self Hosting Users',
|
||||
fields: [
|
||||
{
|
||||
type: FieldType.EMAILS,
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
description: 'The email of the self hosting user',
|
||||
universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FieldMetadata, FieldMetadataType, ObjectMetadata } from 'twenty-sdk/application';
|
||||
|
||||
@ObjectMetadata({
|
||||
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
|
||||
nameSingular: 'selfHostingUser',
|
||||
namePlural: 'selfHostingUsers',
|
||||
labelSingular: 'Self Hosting User',
|
||||
labelPlural: 'Self Hosting Users',
|
||||
})
|
||||
export class SelfHostingUser {
|
||||
@FieldMetadata({
|
||||
type: FieldMetadataType.EMAILS,
|
||||
label: 'Email',
|
||||
description: 'The email of the self hosting user',
|
||||
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
|
||||
})
|
||||
email: object;
|
||||
}
|
||||
+8
-26
@@ -1,20 +1,5 @@
|
||||
import { defineFunction } from 'twenty-sdk';
|
||||
import { createClient } from '../../generated';
|
||||
|
||||
// TODO: import from twenty-sdk when 0.4.0 is deployed
|
||||
type ServerlessFunctionEvent<TBody = object> = {
|
||||
headers: Record<string, string | undefined>;
|
||||
queryStringParameters: Record<string, string | undefined>;
|
||||
pathParameters: Record<string, string | undefined>;
|
||||
body: TBody | null;
|
||||
isBase64Encoded: boolean;
|
||||
requestContext: {
|
||||
http: {
|
||||
method: string;
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
import { createClient } from '../generated';
|
||||
|
||||
type TelemetryEventPayload = {
|
||||
action: string;
|
||||
@@ -37,10 +22,10 @@ type TelemetryEventPayload = {
|
||||
};
|
||||
|
||||
export const main = async (
|
||||
params: ServerlessFunctionEvent<TelemetryEventPayload>,
|
||||
params: TelemetryEventPayload,
|
||||
): Promise<{ success: boolean; message: string; error?: string }> => {
|
||||
try {
|
||||
const { action, payload } = params.body || {};
|
||||
const { action, payload } = params;
|
||||
|
||||
if (action !== 'user_signup') {
|
||||
return {
|
||||
@@ -84,10 +69,7 @@ export const main = async (
|
||||
createSelfHostingUser: {
|
||||
__args: {
|
||||
data: {
|
||||
name:
|
||||
payload?.payload?.events?.[0]?.userFirstName +
|
||||
' ' +
|
||||
payload?.payload?.events?.[0]?.userLastName,
|
||||
name: payload?.payload?.events?.[0]?.userFirstName + ' ' + payload?.payload?.events?.[0]?.userLastName,
|
||||
email: {
|
||||
primaryEmail: userEmail,
|
||||
additionalEmails: null,
|
||||
@@ -115,11 +97,10 @@ export const main = async (
|
||||
}
|
||||
};
|
||||
|
||||
export default defineFunction({
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
|
||||
name: 'telemetry-webhook',
|
||||
timeoutSeconds: 5,
|
||||
handler: main,
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '7c8e3f5a-9b4c-4d1e-8f2a-1b3c4d5e6f7a',
|
||||
@@ -129,4 +110,5 @@ export default defineFunction({
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,11 +20,7 @@
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,50 +42,25 @@
|
||||
{{- regexFind "\\|(.+)$" . | trimPrefix "|" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Check if using external secret for database password */}}
|
||||
{{- define "twenty.db.useExternalSecret" -}}
|
||||
{{- if and (not .Values.db.enabled) .Values.db.external.secretName .Values.db.external.passwordKey -}}
|
||||
true
|
||||
{{- else -}}
|
||||
false
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Database URL secret name */}}
|
||||
{{- define "twenty.dbUrl.secretName" -}}
|
||||
{{- printf "%s-db-url" (include "twenty.fullname" .) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Database password secret name */}}
|
||||
{{- define "twenty.dbPassword.secretName" -}}
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" -}}
|
||||
{{- .Values.db.external.secretName -}}
|
||||
{{- else -}}
|
||||
{{- include "twenty.dbUrl.secretName" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Database password secret key */}}
|
||||
{{- define "twenty.dbPassword.secretKey" -}}
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" -}}
|
||||
{{- .Values.db.external.passwordKey -}}
|
||||
{{/* Compose DB connection URL */}}
|
||||
{{- define "twenty.dbUrl" -}}
|
||||
{{- if .Values.server.env.PG_DATABASE_URL -}}
|
||||
{{- .Values.server.env.PG_DATABASE_URL -}}
|
||||
{{- else if .Values.db.enabled -}}
|
||||
appPassword
|
||||
{{- $host := printf "%s-db" (include "twenty.fullname" .) -}}
|
||||
{{- $user := .Values.db.internal.appUser | default "twenty_app_user" -}}
|
||||
{{- $pass := .Values.db.internal.appPassword | default (randAlphaNum 32) -}}
|
||||
{{- $db := .Values.db.internal.database | default "twenty" -}}
|
||||
{{- printf "postgres://%s:%s@%s.%s.svc.cluster.local/%s" $user $pass $host (include "twenty.namespace" .) $db -}}
|
||||
{{- else -}}
|
||||
password
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Database URL template for external secret (will be evaluated at runtime) */}}
|
||||
{{- define "twenty.dbUrl.template" -}}
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" -}}
|
||||
{{- $scheme := "postgres" -}}
|
||||
{{- $host := .Values.db.external.host -}}
|
||||
{{- $port := .Values.db.external.port | default 5432 -}}
|
||||
{{- $user := .Values.db.external.user | default "postgres" -}}
|
||||
{{- $pass := .Values.db.external.password | default "postgres" -}}
|
||||
{{- $db := .Values.db.external.database | default "twenty" -}}
|
||||
{{- $qs := ternary "?sslmode=require" "" (eq .Values.db.external.ssl true) -}}
|
||||
{{- printf "%s://%s:$(DB_PASSWORD)@%s:%v/%s%s" $scheme $user $host $port $db $qs -}}
|
||||
{{- printf "%s://%s:%s@%s:%v/%s%s" $scheme $user $pass $host $port $db $qs -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -103,11 +78,9 @@ password
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Compose Server URL from override, ingress, or service */}}
|
||||
{{/* Compose Server URL from ingress, else service */}}
|
||||
{{- define "twenty.serverUrl" -}}
|
||||
{{- if .Values.server.env.SERVER_URL -}}
|
||||
{{- .Values.server.env.SERVER_URL -}}
|
||||
{{- else if and .Values.server.ingress.enabled (gt (len .Values.server.ingress.hosts) 0) -}}
|
||||
{{- if and .Values.server.ingress.enabled (gt (len .Values.server.ingress.hosts) 0) -}}
|
||||
{{- $host := (index .Values.server.ingress.hosts 0).host -}}
|
||||
{{- $tls := gt (len .Values.server.ingress.tls) 0 -}}
|
||||
{{- $scheme := ternary "https" "http" $tls -}}
|
||||
|
||||
@@ -116,21 +116,11 @@ spec:
|
||||
- >-
|
||||
npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource
|
||||
env:
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbPassword.secretName" . }}
|
||||
key: {{ include "twenty.dbPassword.secretKey" . }}
|
||||
- name: PG_DATABASE_URL
|
||||
value: {{ include "twenty.dbUrl.template" . | quote }}
|
||||
{{- else }}
|
||||
- name: PG_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
name: {{ include "twenty.fullname" . }}-db-url
|
||||
key: url
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: server
|
||||
{{- $img := include "twenty.server.image" . }}
|
||||
@@ -139,21 +129,11 @@ spec:
|
||||
env:
|
||||
- name: SERVER_URL
|
||||
value: {{ include "twenty.serverUrl" . | quote }}
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbPassword.secretName" . }}
|
||||
key: {{ include "twenty.dbPassword.secretKey" . }}
|
||||
- name: PG_DATABASE_URL
|
||||
value: {{ include "twenty.dbUrl.template" . | quote }}
|
||||
{{- else }}
|
||||
- name: PG_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
name: {{ include "twenty.fullname" . }}-db-url
|
||||
key: url
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: SIGN_IN_PREFILLED
|
||||
|
||||
@@ -52,21 +52,11 @@ spec:
|
||||
env:
|
||||
- name: SERVER_URL
|
||||
value: {{ include "twenty.serverUrl" . | quote }}
|
||||
{{- if eq (include "twenty.db.useExternalSecret" .) "true" }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbPassword.secretName" . }}
|
||||
key: {{ include "twenty.dbPassword.secretKey" . }}
|
||||
- name: PG_DATABASE_URL
|
||||
value: {{ include "twenty.dbUrl.template" . | quote }}
|
||||
{{- else }}
|
||||
- name: PG_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "twenty.dbUrl.secretName" . }}
|
||||
name: {{ include "twenty.fullname" . }}-db-url
|
||||
key: url
|
||||
{{- end }}
|
||||
- name: REDIS_URL
|
||||
value: {{ include "twenty.redisUrl" . | quote }}
|
||||
- name: STORAGE_TYPE
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $secretName := printf "%s-db-url" (include "twenty.fullname" .) -}}
|
||||
{{- if .Values.db.enabled -}}
|
||||
{{- $existingSecret := lookup "v1" "Secret" (include "twenty.namespace" .) $secretName -}}
|
||||
{{- $appPassword := "" -}}
|
||||
{{- if $existingSecret -}}
|
||||
@@ -21,25 +20,3 @@ type: Opaque
|
||||
stringData:
|
||||
url: {{ printf "postgres://%s:%s@%s-db.%s.svc.cluster.local/%s" (urlquery $appUser) (urlquery $appPassword) (include "twenty.fullname" .) (include "twenty.namespace" .) (.Values.db.internal.database | default "twenty") | quote }}
|
||||
appPassword: {{ $appPassword | quote }}
|
||||
{{- else if not .Values.db.external.secretName -}}
|
||||
{{- $scheme := "postgres" -}}
|
||||
{{- $host := .Values.db.external.host -}}
|
||||
{{- $port := .Values.db.external.port | default 5432 -}}
|
||||
{{- $user := .Values.db.external.user | default "postgres" -}}
|
||||
{{- $pass := .Values.db.external.password | default "" -}}
|
||||
{{- $db := .Values.db.external.database | default "twenty" -}}
|
||||
{{- $qs := ternary "?sslmode=require" "" (eq .Values.db.external.ssl true) -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ $secretName }}
|
||||
namespace: {{ include "twenty.namespace" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "twenty.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: server
|
||||
type: Opaque
|
||||
stringData:
|
||||
url: {{ printf "%s://%s:%s@%s:%v/%s%s" $scheme (urlquery $user) (urlquery $pass) $host $port $db $qs | quote }}
|
||||
password: {{ $pass | quote }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"env": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"SERVER_URL": { "type": "string", "description": "Override for the server URL (e.g., https://crm.example.com). If not set, derived from ingress or service." },
|
||||
"NODE_PORT": { "type": "integer", "minimum": 1 },
|
||||
"PG_DATABASE_URL": { "type": "string" },
|
||||
"REDIS_URL": { "type": "string" },
|
||||
|
||||
@@ -19,13 +19,8 @@ storage:
|
||||
bucket: ""
|
||||
region: ""
|
||||
endpoint: ""
|
||||
# Option A: direct values
|
||||
accessKeyId: ""
|
||||
secretAccessKey: ""
|
||||
# Option B: reference a Secret
|
||||
# secretName: my-s3-creds
|
||||
# accessKeyIdKey: accessKeyId
|
||||
# secretAccessKeyKey: secretAccessKey
|
||||
|
||||
# Auth tokens (random if not provided)
|
||||
secrets:
|
||||
@@ -142,8 +137,6 @@ db:
|
||||
password: ""
|
||||
database: twenty
|
||||
ssl: false
|
||||
secretName: ""
|
||||
passwordKey: ""
|
||||
|
||||
# Redis
|
||||
redisInternal:
|
||||
|
||||
@@ -14,7 +14,6 @@ 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-sdk/package.json /app/packages/twenty-sdk/
|
||||
|
||||
# Install all dependencies
|
||||
RUN yarn && yarn cache clean && npx nx reset
|
||||
@@ -40,7 +39,6 @@ ARG REACT_APP_SERVER_BASE_URL
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
RUN npx nx build twenty-front
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@ setup_and_migrate_db() {
|
||||
yarn database:migrate:prod
|
||||
fi
|
||||
|
||||
yarn command:prod cache:flush
|
||||
yarn command:prod upgrade
|
||||
yarn command:prod cache:flush
|
||||
|
||||
echo "Successfully migrated DB!"
|
||||
}
|
||||
|
||||
|
||||
@@ -159,12 +159,6 @@ You should run all commands in the following steps from the root of the project.
|
||||
CREATE ROLE postgres WITH SUPERUSER LOGIN;
|
||||
```
|
||||
This creates a superuser role named `postgres` with login access.
|
||||
```bash
|
||||
Role name | Attributes | Member of
|
||||
-----------+-------------+-----------
|
||||
postgres | Superuser | {}
|
||||
john | Superuser | {}
|
||||
```
|
||||
|
||||
**Option 2:** If you have docker installed:
|
||||
```bash
|
||||
|
||||
@@ -9,13 +9,16 @@ Apps are currently in alpha testing. The feature is functional but still evolvin
|
||||
|
||||
## What Are Apps?
|
||||
|
||||
Apps let you build and manage Twenty customizations **as code**. Instead of configuring everything through the UI, you define your data model and logic functions in code — making it faster to build, maintain, and roll out to multiple workspaces.
|
||||
Apps let you build and manage Twenty customizations **as code**. Instead of configuring everything through the UI, you define your data model and serverless functions in code — making it faster to build, maintain, and roll out to multiple workspaces.
|
||||
|
||||
**What you can do today:**
|
||||
- Define custom objects and fields as code (managed data model)
|
||||
- Build logic functions with custom triggers
|
||||
- Build serverless functions with custom triggers
|
||||
- Deploy the same app across multiple workspaces
|
||||
|
||||
**Coming soon:**
|
||||
- Custom UI layouts and components
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+ and Yarn 4
|
||||
@@ -45,12 +48,15 @@ From here you can:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
yarn app:create-entity
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# Watch your application's function logs
|
||||
# Run a one‑time sync (instead of watch mode)
|
||||
yarn app:sync
|
||||
|
||||
# Watch your application's functions logs
|
||||
yarn function:logs
|
||||
|
||||
# Execute a function by name
|
||||
@@ -60,7 +66,7 @@ yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn help
|
||||
yarn app:help
|
||||
```
|
||||
|
||||
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -88,60 +94,79 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Example logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
app/
|
||||
application.config.ts # Required - main application configuration
|
||||
default-function.role.ts # Default role for serverless functions
|
||||
// your entities (*.object.ts, *.function.ts, *.role.ts)
|
||||
utils/ # Optional - handler implementations & utilities
|
||||
```
|
||||
|
||||
### Convention-over-configuration
|
||||
|
||||
Applications use a **convention-over-configuration** approach where entities are detected by their file suffix. This allows flexible organization within the `src/app/` folder:
|
||||
|
||||
| File suffix | Entity type |
|
||||
|-------------|-------------|
|
||||
| `*.object.ts` | Custom object definitions |
|
||||
| `*.function.ts` | Serverless function definitions |
|
||||
| `*.role.ts` | Role definitions |
|
||||
|
||||
### Supported folder organizations
|
||||
|
||||
You can organize your entities in any of these patterns:
|
||||
|
||||
**Traditional (by type):**
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Feature-based:**
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Flat:**
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
At a high level:
|
||||
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and authentication commands that delegate to the local `twenty` CLI.
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall`, and `auth` that delegate to the local `twenty` CLI.
|
||||
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `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.
|
||||
- **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
|
||||
- **README.md**: A short README in the app root with basic instructions.
|
||||
- **public/**: A folder for storing public assets (images, fonts, static files) that will be served with your application. Files placed here are uploaded during sync and accessible at runtime.
|
||||
- **src/**: The main place where you define your application-as-code
|
||||
|
||||
### Entity detection
|
||||
|
||||
The SDK detects entities by parsing your TypeScript files for **`export default define<Entity>({...})`** calls. Each entity type has a corresponding helper function exported from `twenty-sdk`:
|
||||
|
||||
| Helper function | Entity type |
|
||||
|-----------------|-------------|
|
||||
| `defineObject()` | Custom object definitions |
|
||||
| `defineLogicFunction()` | Logic function definitions |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | Role definitions |
|
||||
| `defineField()` | Field extensions for existing objects |
|
||||
|
||||
<Note>
|
||||
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
|
||||
</Note>
|
||||
|
||||
Example of a detected entity:
|
||||
```typescript
|
||||
// This file can be named anything and placed anywhere in src/
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCard',
|
||||
// ... rest of config
|
||||
});
|
||||
```
|
||||
- **src/app/**: The main place where you define your application-as-code:
|
||||
- `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See "Application config" below.
|
||||
- `*.role.ts`: Role definitions used by your serverless functions. See "Default function role" below.
|
||||
- `*.object.ts`: Custom object definitions.
|
||||
- `*.function.ts`: Serverless function definitions.
|
||||
- **src/utils/**: Optional folder for handler implementations and utilities.
|
||||
|
||||
Later commands will add more files and folders:
|
||||
|
||||
- `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
|
||||
- `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
|
||||
- `yarn app:create-entity` will add entity definition files under `src/app/` for your custom objects, functions, or roles.
|
||||
l
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -182,18 +207,16 @@ The twenty-sdk provides typed building blocks and helper functions you use insid
|
||||
|
||||
### Helper functions
|
||||
|
||||
The SDK provides helper functions for defining your app entities. As described in [Entity detection](#entity-detection), you must use `export default define<Entity>({...})` for your entities to be detected:
|
||||
The SDK provides four helper functions with built-in validation for defining your app entities:
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `defineApplication()` | Configure application metadata (required, one per app) |
|
||||
| `defineApp()` | Configure application metadata |
|
||||
| `defineObject()` | Define custom objects with fields |
|
||||
| `defineLogicFunction()` | Define logic functions with handlers |
|
||||
| `defineFrontComponent()` | Define front components for custom UI |
|
||||
| `defineFunction()` | Define serverless functions with handlers |
|
||||
| `defineRole()` | Configure role permissions and object access |
|
||||
| `defineField()` | Extend existing objects with additional fields |
|
||||
|
||||
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
|
||||
These functions validate your configuration at runtime and provide better IDE autocompletion and type safety.
|
||||
|
||||
### Defining objects
|
||||
|
||||
@@ -274,29 +297,80 @@ 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 entity:add`, which guides you through naming, fields, and relationships.
|
||||
- You can scaffold new objects using `yarn app:create-entity`, 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 such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
|
||||
</Note>
|
||||
|
||||
<Accordion title="Alternative: Decorator-based syntax">
|
||||
You can also define objects using TypeScript decorators. This approach uses class-based syntax with `@Object`, `@Field`, and `@Relation` decorators:
|
||||
|
||||
### Application config (application-config.ts)
|
||||
```typescript
|
||||
import {
|
||||
type AddressField,
|
||||
Field,
|
||||
FieldType,
|
||||
type FullNameField,
|
||||
Object,
|
||||
OnDeleteAction,
|
||||
Relation,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
import { type Note } from '../../generated';
|
||||
|
||||
Every app has a single `application-config.ts` file that describes:
|
||||
@Object({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post card',
|
||||
labelPlural: 'Post cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
export class PostCard {
|
||||
@Field({
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
})
|
||||
content: string;
|
||||
|
||||
@Relation({
|
||||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
}
|
||||
```
|
||||
|
||||
Note: The decorator approach requires `experimentalDecorators` in your TypeScript config.
|
||||
</Accordion>
|
||||
|
||||
|
||||
### Application config (application.config.ts)
|
||||
|
||||
Every app has a single `application.config.ts` file that describes:
|
||||
|
||||
- **Who the app is**: identifiers, display name, and description.
|
||||
- **How its functions run**: which role they use for permissions.
|
||||
- **(Optional) variables**: key–value pairs exposed to your functions as environment variables.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
Use `defineApp()` to define your application configuration:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
// src/app/application.config.ts
|
||||
import { defineApp } from 'twenty-sdk';
|
||||
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
export default defineApp({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
@@ -309,18 +383,18 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
|
||||
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
- `defaultRoleUniversalIdentifier` must match the role file (see below).
|
||||
- `functionRoleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||||
|
||||
#### 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.
|
||||
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's serverless 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.
|
||||
@@ -331,14 +405,14 @@ Applications can define roles that encapsulate permissions on your workspace's o
|
||||
When you scaffold a new app, the CLI also creates a default role file. Use `defineRole()` to define roles with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: false,
|
||||
@@ -351,7 +425,7 @@ export default defineRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectNameSingular: 'postCard',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -360,8 +434,8 @@ export default defineRole({
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectNameSingular: 'postCard',
|
||||
fieldName: 'content',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
@@ -370,10 +444,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
The `universalIdentifier` of this role is then referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`. In other words:
|
||||
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `functionRoleUniversalIdentifier`. In other words:
|
||||
|
||||
- **\*.role.ts** defines what the default function role can do.
|
||||
- **application-config.ts** points to that role so your functions inherit its permissions.
|
||||
- **application.config.ts** points to that role so your functions inherit its permissions.
|
||||
|
||||
Notes:
|
||||
- Start from the scaffolded role, then progressively restrict it following least‑privilege.
|
||||
@@ -381,17 +455,22 @@ Notes:
|
||||
- `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).
|
||||
|
||||
### Logic function config and entrypoint
|
||||
### Serverless function config and entrypoint
|
||||
|
||||
Each function file uses `defineLogicFunction()` to export a configuration with a handler and optional triggers.
|
||||
Each function file uses `defineFunction()` to export a configuration with a handler and optional triggers. Use the `*.function.ts` file suffix for automatic detection.
|
||||
|
||||
```typescript
|
||||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
// 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';
|
||||
import Twenty, { type Person } from '../../generated';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const handler = async (
|
||||
params:
|
||||
| RoutePayload
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||||
| CronPayload,
|
||||
) => {
|
||||
const client = new Twenty(); // generated typed client
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
@@ -407,7 +486,7 @@ const handler = async (params: RoutePayload) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
@@ -422,18 +501,18 @@ export default defineLogicFunction({
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
{
|
||||
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'],
|
||||
// },
|
||||
{
|
||||
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['name'],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -473,10 +552,10 @@ const handler = async (event: RoutePayload) => {
|
||||
**To migrate existing functions:** Update your handler to destructure from `event.body`, `event.queryStringParameters`, or `event.pathParameters` instead of directly from the params object.
|
||||
</Warning>
|
||||
|
||||
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`:
|
||||
When a route trigger invokes your function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -503,10 +582,10 @@ The `RoutePayload` type has the following structure:
|
||||
|
||||
### Forwarding HTTP headers
|
||||
|
||||
By default, HTTP headers from incoming requests are **not** passed to your logic function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array:
|
||||
By default, HTTP headers from incoming requests are **not** passed to your serverless function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -541,59 +620,23 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
You can create new functions in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn entity: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.
|
||||
|
||||
### Front components
|
||||
|
||||
Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/my-widget.front-component.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const MyWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>My Custom Widget</h1>
|
||||
<p>This is a custom front component for Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'my-widget',
|
||||
description: 'A custom widget component',
|
||||
component: MyWidget,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Front components are React components that render in isolated contexts within Twenty.
|
||||
- Use the `*.front-component.tsx` file suffix for automatic detection.
|
||||
- The `component` field references your React component.
|
||||
- Components are built and synced automatically during `yarn app:dev`.
|
||||
|
||||
You can create new front components in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new front component.
|
||||
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
|
||||
- **Scaffolded**: Run `yarn app:create-entity` and choose the option to add a new function. This generates a starter file with a handler and config.
|
||||
- **Manual**: Create a new `*.function.ts` file and use `defineFunction()`, following the same pattern.
|
||||
|
||||
### Generated typed client
|
||||
|
||||
Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import Twenty from './generated';
|
||||
|
||||
const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
The client is re-generated by `yarn app:generate`. Re-run after changing your objects or when onboarding to a new workspace.
|
||||
The client is re-generated by `yarn app:generate`. Re-run after changing your objects and `yarn app:sync` or when onboarding to a new workspace.
|
||||
|
||||
#### Runtime credentials in logic functions
|
||||
#### Runtime credentials in serverless functions
|
||||
|
||||
When your function runs on Twenty, the platform injects credentials as environment variables before your code executes:
|
||||
|
||||
@@ -602,13 +645,13 @@ When your function runs on Twenty, the platform injects credentials as environme
|
||||
|
||||
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 least‑privilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier.
|
||||
- The API key's permissions are determined by the role referenced in your `application.config.ts` via `functionRoleUniversalIdentifier`. This is the default role used by serverless functions of your application.
|
||||
- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that role's universal identifier.
|
||||
|
||||
|
||||
### Hello World example
|
||||
|
||||
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
@@ -623,29 +666,25 @@ Then add scripts like these:
|
||||
```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"
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
|
||||
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:sync`, etc.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Authentication errors: run `yarn auth:login` and ensure your API key has the required permissions.
|
||||
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
|
||||
- Types or client missing/outdated: run `yarn app:generate`.
|
||||
- Types or client missing/outdated: run `yarn app:generate` and then `yarn app:dev`.
|
||||
- Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment.
|
||||
|
||||
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -289,19 +289,19 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
|
||||
## Logic Functions
|
||||
## Serverless Functions
|
||||
|
||||
Twenty supports logic functions for workflows and custom logic. The execution environment is configured via the `SERVERLESS_TYPE` environment variable.
|
||||
Twenty supports serverless functions for workflows and custom logic. The execution environment is configured via the `SERVERLESS_TYPE` environment variable.
|
||||
|
||||
<Warning>
|
||||
**Security Notice:** The local driver (`SERVERLESS_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, we highly recommend using `SERVERLESS_TYPE=LAMBDA` or `SERVERLESS_TYPE=DISABLED`.
|
||||
**Security Notice:** The local serverless driver (`SERVERLESS_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, we highly recommend using `SERVERLESS_TYPE=LAMBDA` or `SERVERLESS_TYPE=DISABLED`.
|
||||
</Warning>
|
||||
|
||||
### Available Drivers
|
||||
|
||||
| Driver | Environment Variable | Use Case | Security Level |
|
||||
|--------|---------------------|----------|----------------|
|
||||
| Disabled | `SERVERLESS_TYPE=DISABLED` | Disable logic functions entirely | N/A |
|
||||
| Disabled | `SERVERLESS_TYPE=DISABLED` | Disable serverless functions entirely | N/A |
|
||||
| Local | `SERVERLESS_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
|
||||
|
||||
@@ -321,11 +321,11 @@ SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**To disable logic functions:**
|
||||
**To disable serverless functions:**
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
<Note>
|
||||
When using `SERVERLESS_TYPE=DISABLED`, any attempt to execute a logic function will return an error. This is useful if you want to run Twenty without logic function capabilities.
|
||||
When using `SERVERLESS_TYPE=DISABLED`, any attempt to execute a serverless function will return an error. This is useful if you want to run Twenty without serverless function capabilities.
|
||||
</Note>
|
||||
|
||||
+307
-307
File diff suppressed because it is too large
Load Diff
@@ -170,13 +170,6 @@ cd twenty
|
||||
|
||||
يقوم هذا بإنشاء دور مشرف نظام باسم `postgres` مع إمكانية تسجيل الدخول.
|
||||
|
||||
```bash
|
||||
اسم الدور | الخصائص | عضو في
|
||||
-----------+-------------+-----------
|
||||
postgres | مشرف نظام | {}
|
||||
john | مشرف نظام | {}
|
||||
```
|
||||
|
||||
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -9,14 +9,18 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
|
||||
## ما هي التطبيقات؟
|
||||
|
||||
تتيح لك التطبيقات إنشاء وإدارة تخصيصات Twenty **ككود**. بدلًا من تكوين كل شيء عبر واجهة المستخدم، تُعرِّف نموذج بياناتك ووظائف المنطق في الكود — مما يجعل البناء والصيانة والنشر إلى مساحات عمل متعددة أسرع.
|
||||
تتيح لك التطبيقات إنشاء وإدارة تخصيصات Twenty **ككود**. بدلًا من تكوين كل شيء عبر واجهة المستخدم، تُعرِّف نموذج بياناتك ووظائف بلا خادم في الكود — مما يجعل الإنشاء والصيانة والنشر إلى مساحات عمل متعددة أسرع.
|
||||
|
||||
**ما الذي يمكنك فعله اليوم:**
|
||||
|
||||
* عرِّف كائنات وحقولًا مخصصة على شكل كود (نموذج بيانات مُدار)
|
||||
* أنشئ وظائف منطقية مع مشغلات مخصصة
|
||||
* أنشئ وظائف بلا خادم مع مشغلات مخصصة
|
||||
* انشر التطبيق نفسه عبر مساحات عمل متعددة
|
||||
|
||||
**قريبًا:**
|
||||
|
||||
* تخطيطات ومكونات واجهة مستخدم مخصصة
|
||||
|
||||
## المتطلبات الأساسية
|
||||
|
||||
* Node.js 24+ وYarn 4
|
||||
@@ -46,12 +50,15 @@ yarn app:dev
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
yarn app:create-entity
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# Watch your application's function logs
|
||||
# Run a one‑time sync (instead of watch mode)
|
||||
yarn app:sync
|
||||
|
||||
# Watch your application's functions logs
|
||||
yarn function:logs
|
||||
|
||||
# Execute a function by name
|
||||
@@ -61,7 +68,7 @@ yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn help
|
||||
yarn app:help
|
||||
```
|
||||
|
||||
راجع أيضًا: صفحات مرجع CLI لـ [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) و[twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -89,61 +96,82 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
|
||||
src/
|
||||
├── application-config.ts # مطلوب - التكوين الرئيسي للتطبيق
|
||||
├── roles/
|
||||
│ └── default-role.ts # الدور الافتراضي لوظائف المنطق
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # مثال لوظيفة منطقية
|
||||
└── front-components/
|
||||
└── hello-world.tsx # مثال لمكوّن الواجهة الأمامية
|
||||
app/
|
||||
application.config.ts # مطلوب - التكوين الرئيسي للتطبيق
|
||||
default-function.role.ts # الدور الافتراضي للوظائف بدون خادم
|
||||
// الكيانات الخاصة بك (*.object.ts, *.function.ts, *.role.ts)
|
||||
utils/ # اختياري - تنفيذات المُعالِجات والأدوات المساعدة
|
||||
```
|
||||
|
||||
### الاتفاقية فوق التهيئة
|
||||
|
||||
تستخدم التطبيقات نهج **الاتفاقية فوق التهيئة** حيث تُكتشف الكيانات عبر لاحقة اسم الملف. يتيح ذلك تنظيمًا مرنًا داخل مجلد `src/app/`:
|
||||
|
||||
| لاحقة الملف | نوع الكيان |
|
||||
| --------------- | ---------------------- |
|
||||
| `*.object.ts` | تعريفات كائنات مخصصة |
|
||||
| `*.function.ts` | تعريفات وظائف بلا خادم |
|
||||
| `*.role.ts` | تعريفات الأدوار |
|
||||
|
||||
### طرق تنظيم المجلدات المدعومة
|
||||
|
||||
يمكنك تنظيم الكيانات بأي من الأنماط التالية:
|
||||
|
||||
**تقليدي (حسب النوع):**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**حسب الميزة:**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**مسطح:**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
بشكل عام:
|
||||
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` فضلًا عن نصوص مثل `app:dev` و`app:generate` و`entity:add` و`function:logs` و`function:execute` و`app:uninstall` وأوامر المصادقة التي تُفوِّض إلى `twenty` CLI المحلي.
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` فضلًا عن نصوص مثل `dev` و`sync` و`generate` و`create-entity` و`logs` و`uninstall` و`auth` التي تفوِّض إلى `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/**: المكان الرئيسي حيث تعرّف تطبيقك ككود
|
||||
|
||||
### اكتشاف الكيانات
|
||||
|
||||
يكتشف SDK الكيانات عبر تحليل ملفات TypeScript الخاصة بك بحثًا عن استدعاءات **`export default define<Entity>({...})`**. يحتوي كل نوع كيان على دالة مساعدة مقابلة يتم تصديرها من `twenty-sdk`:
|
||||
|
||||
| دالة مساعدة | نوع الكيان |
|
||||
| ------------------------ | --------------------------------- |
|
||||
| `defineObject()` | تعريفات كائنات مخصصة |
|
||||
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | تعريفات الأدوار |
|
||||
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
|
||||
|
||||
<Note>
|
||||
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
|
||||
</Note>
|
||||
|
||||
مثال على كيان تم اكتشافه:
|
||||
|
||||
```typescript
|
||||
// This file can be named anything and placed anywhere in src/
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCard',
|
||||
// ... rest of config
|
||||
});
|
||||
```
|
||||
* **src/app/**: المكان الرئيسي حيث تعرّف تطبيقك ككود:
|
||||
* `application.config.ts`: التكوين العام لتطبيقك (بيانات وصفية وربط وقت التشغيل). انظر "تكوين التطبيق" أدناه.
|
||||
* `*.role.ts`: تعريفات الأدوار المستخدمة بواسطة وظائفك بلا خادم. انظر "الدور الافتراضي للوظيفة" أدناه.
|
||||
* `*.object.ts`: تعريفات كائنات مخصصة.
|
||||
* `*.function.ts`: تعريفات وظائف بلا خادم.
|
||||
* **src/utils/**: مجلد اختياري لتنفيذات المعالجات والأدوات المساعدة.
|
||||
|
||||
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
|
||||
|
||||
* `yarn app:generate` سيُنشئ مجلدًا `generated/` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
|
||||
* `yarn entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار.
|
||||
* `yarn app:create-entity` سيضيف ملفات تعريف الكيانات تحت `src/app/` لكائناتك المخصصة أو الوظائف أو الأدوار.
|
||||
l
|
||||
|
||||
## المصادقة
|
||||
|
||||
@@ -184,18 +212,16 @@ Once you've switched workspaces with `auth:switch`, all subsequent commands will
|
||||
|
||||
### دوال مساعدة
|
||||
|
||||
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
|
||||
يوفّر SDK أربع دوال مساعدة مع تحقق مدمج لتعريف كيانات تطبيقك:
|
||||
|
||||
| دالة | الغرض |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
|
||||
| دالة | الغرض |
|
||||
| ------------------ | ---------------------------------------- |
|
||||
| `defineApp()` | تهيئة بيانات التطبيق الوصفية |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineFunction()` | تعريف وظائف بلا خادم مع معالجات |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
|
||||
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
تتحقق هذه الدوال من تكوينك في وقت التشغيل وتوفر إكمالًا تلقائيًا أفضل في بيئة التطوير وأمان أنواع أعلى.
|
||||
|
||||
### تعريف الكائنات
|
||||
|
||||
@@ -276,28 +302,79 @@ export default defineObject({
|
||||
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
|
||||
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
|
||||
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn app:create-entity`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
|
||||
<Note>
|
||||
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية مثل `name` و`createdAt` و`updatedAt` و`createdBy` و`position` و`deletedAt`. لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
|
||||
</Note>
|
||||
|
||||
### تكوين التطبيق (application-config.ts)
|
||||
<Accordion title="بديل: صياغة قائمة على المزيّنات">
|
||||
يمكنك أيضًا تعريف كائنات باستخدام مزيّنات TypeScript. يستخدم هذا النهج صياغة معتمدة على الأصناف مع مزيّنات `@Object` و`@Field` و`@Relation`:
|
||||
|
||||
كل تطبيق لديه ملف واحد `application-config.ts` يصف:
|
||||
```typescript
|
||||
import {
|
||||
type AddressField,
|
||||
Field,
|
||||
FieldType,
|
||||
type FullNameField,
|
||||
Object,
|
||||
OnDeleteAction,
|
||||
Relation,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
import { type Note } from '../../generated';
|
||||
|
||||
@Object({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post card',
|
||||
labelPlural: 'Post cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
export class PostCard {
|
||||
@Field({
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
})
|
||||
content: string;
|
||||
|
||||
@Relation({
|
||||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
}
|
||||
```
|
||||
|
||||
ملاحظة: يتطلب نهج المزيّنات `experimentalDecorators` في تهيئة TypeScript لديك.
|
||||
</Accordion>
|
||||
|
||||
### تكوين التطبيق (application.config.ts)
|
||||
|
||||
كل تطبيق لديه ملف واحد `application.config.ts` يصف:
|
||||
|
||||
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
|
||||
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
|
||||
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
استخدم `defineApp()` لتعريف تهيئة تطبيقك:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
// src/app/application.config.ts
|
||||
import { defineApp } from 'twenty-sdk';
|
||||
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
export default defineApp({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
@@ -310,7 +387,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +395,11 @@ export default defineApplication({
|
||||
|
||||
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
|
||||
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
|
||||
* `functionRoleUniversalIdentifier` يجب أن يطابق الدور الذي تعرّفه في ملف `*.role.ts` (انظر أدناه).
|
||||
|
||||
#### الأدوار والصلاحيات
|
||||
|
||||
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك.
|
||||
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `functionRoleUniversalIdentifier` في `application.config.ts` الدور الافتراضي الذي تستخدمه الوظائف بلا خادم في تطبيقك.
|
||||
|
||||
* مفتاح واجهة البرمجة في وقت التشغيل المحقون باسم `TWENTY_API_KEY` مستمد من دور الوظيفة الافتراضي هذا.
|
||||
* سيُقيَّد العميل مضبوط الأنواع بالأذونات الممنوحة لذلك الدور.
|
||||
@@ -333,14 +410,14 @@ export default defineApplication({
|
||||
عند توليد تطبيق جديد بالقالب، ينشئ CLI أيضًا ملف دور افتراضي. استخدم `defineRole()` لتعريف أدوار مع تحقق مدمج:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: false,
|
||||
@@ -353,7 +430,7 @@ export default defineRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectNameSingular: 'postCard',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -362,8 +439,8 @@ export default defineRole({
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectNameSingular: 'postCard',
|
||||
fieldName: 'content',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
@@ -372,10 +449,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application-config.ts` باسم `defaultRoleUniversalIdentifier`. بعبارة أخرى:
|
||||
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `functionRoleUniversalIdentifier`. بعبارة أخرى:
|
||||
|
||||
* **\\*.role.ts** يحدد ما يمكن أن يفعله الدور الافتراضي للوظيفة.
|
||||
* **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
|
||||
* **application.config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك صلاحياته.
|
||||
|
||||
الملاحظات:
|
||||
|
||||
@@ -384,17 +461,22 @@ export default defineRole({
|
||||
* `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).
|
||||
|
||||
### تكوين الوظيفة المنطقية ونقطة الدخول
|
||||
### تكوين وظيفة بلا خادم ونقطة الدخول
|
||||
|
||||
كل ملف وظيفة يستخدم `defineLogicFunction()` لتصدير تكوين مع معالج ومشغّلات اختيارية.
|
||||
كل ملف وظيفة يستخدم `defineFunction()` لتصدير تكوين مع معالج ومشغلات اختيارية. استخدم لاحقة الملف `*.function.ts` للاكتشاف التلقائي.
|
||||
|
||||
```typescript
|
||||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
// 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';
|
||||
import Twenty, { type Person } from '../../generated';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const handler = async (
|
||||
params:
|
||||
| RoutePayload
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||||
| CronPayload,
|
||||
) => {
|
||||
const client = new Twenty(); // generated typed client
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
@@ -410,7 +492,7 @@ const handler = async (params: RoutePayload) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
@@ -425,18 +507,18 @@ export default defineLogicFunction({
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
{
|
||||
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'],
|
||||
// },
|
||||
{
|
||||
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['name'],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -483,10 +565,10 @@ export default defineLogicFunction({
|
||||
**لترحيل الدوال الحالية:** حدّث المعالج لديك لفكّ البنية من `event.body` أو `event.queryStringParameters` أو `event.pathParameters` بدلاً من القراءة مباشرةً من كائن params.
|
||||
</Warning>
|
||||
|
||||
عندما يستدعي مشغّل المسار وظيفتك المنطقية، يتلقى كائنًا من النوع `RoutePayload` يتبع تنسيق AWS HTTP API v2. استورد النوع من `twenty-sdk`:
|
||||
عندما يستدعي مشغّل المسار دالتك، فإنه يتلقى كائنًا من النوع `RoutePayload` يتبع تنسيق AWS HTTP API v2. استورد النوع من `twenty-sdk`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -513,10 +595,10 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
### تمرير رؤوس HTTP
|
||||
|
||||
افتراضيًا، **لا** تُمرَّر رؤوس HTTP من الطلبات الواردة إلى وظيفتك المنطقية لأسباب أمنية. للوصول إلى رؤوس محددة، قم بإدراجها صراحةً في مصفوفة `forwardedRequestHeaders`:
|
||||
افتراضيًا، لا يتم تمرير رؤوس HTTP من الطلبات الواردة إلى دالتك بدون خادم لأسباب أمنية. للوصول إلى رؤوس محددة، قم بإدراجها صراحةً في مصفوفة `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,60 +633,23 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
يمكنك إنشاء وظائف جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغّل `yarn entity:add` واختر خيار إضافة وظيفة منطقية جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.logic-function.ts` واستخدم `defineLogicFunction()` مع اتباع النمط نفسه.
|
||||
|
||||
### المكوّنات الأمامية
|
||||
|
||||
تتيح لك المكوّنات الأمامية إنشاء مكوّنات React مخصّصة تُعرَض داخل واجهة مستخدم Twenty. استخدم `defineFrontComponent()` لتعريف مكوّنات مع تحقّق مدمج:
|
||||
|
||||
```typescript
|
||||
// src/my-widget.front-component.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const MyWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>My Custom Widget</h1>
|
||||
<p>This is a custom front component for Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'my-widget',
|
||||
description: 'A custom widget component',
|
||||
component: MyWidget,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* المكوّنات الأمامية هي مكوّنات React تُعرَض ضمن سياقات معزولة داخل Twenty.
|
||||
* استخدم لاحقة الملف `*.front-component.tsx` للاكتشاف التلقائي.
|
||||
* يشير الحقل `component` إلى مكوّن React الخاص بك.
|
||||
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn app:dev`.
|
||||
|
||||
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغّل `yarn entity:add` واختر خيار إضافة مكوّن أمامي جديد.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.front-component.tsx` واستخدم `defineFrontComponent()`.
|
||||
* **مُنشأ بالقالب**: شغّل `yarn app:create-entity` واختر خيار إضافة وظيفة جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.function.ts` واستخدم `defineFunction()` مع اتباع النمط نفسه.
|
||||
|
||||
### عميل مُولَّد مضبوط الأنواع
|
||||
|
||||
شغّل yarn app:generate لإنشاء عميل محلي مضبوط الأنواع في generated/ استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import Twenty from './generated';
|
||||
|
||||
const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
يُعاد توليد العميل بواسطة `yarn app:generate`. أعِد التشغيل بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة.
|
||||
يُعاد توليد العميل بواسطة `yarn app:generate`. أعِد تشغيله بعد تغيير كائناتك وتشغيل `yarn app:sync` أو عند الانضمام إلى مساحة عمل جديدة.
|
||||
|
||||
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
|
||||
#### بيانات الاعتماد في وقت التشغيل في الوظائف بلا خادم
|
||||
|
||||
عندما تعمل وظيفتك على Twenty، يقوم النظام الأساسي بحقن بيانات الاعتماد كمتغيرات بيئة قبل تنفيذ كودك:
|
||||
|
||||
@@ -614,12 +659,12 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
الملاحظات:
|
||||
|
||||
* لا تحتاج إلى تمرير عنوان URL أو مفتاح واجهة برمجة التطبيقات إلى العميل المُولَّد. يقوم بقراءة `TWENTY_API_URL` و`TWENTY_API_KEY` من process.env وقت التشغيل.
|
||||
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application-config.ts` عبر `defaultRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
|
||||
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `defaultRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
|
||||
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application.config.ts` عبر `functionRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف بلا خادم في تطبيقك.
|
||||
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `functionRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
|
||||
|
||||
### مثال Hello World
|
||||
|
||||
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف المنطقية والمكوّنات الأمامية ومشغّلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف ومشغلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## إعداد يدوي (بدون المهيئ)
|
||||
|
||||
@@ -634,29 +679,25 @@ 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"
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
يمكنك الآن تشغيل الأوامر نفسها عبر Yarn، مثل `yarn app:dev` و`yarn app:generate`، إلخ.
|
||||
يمكنك الآن تشغيل الأوامر نفسها عبر Yarn، مثل `yarn app:dev` و`yarn app:sync`، إلخ.
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
* أخطاء المصادقة: شغّل `yarn auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
|
||||
* يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة البرمجة وأن خادم Twenty قابل للوصول.
|
||||
* الأنواع أو العميل مفقود/قديم: شغّل `yarn app:generate`.
|
||||
* الأنواع أو العميل مفقود/قديم: شغّل `yarn app:generate` ثم `yarn app:dev`.
|
||||
* وضع التطوير لا يزامن: تأكد من أن `yarn app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
|
||||
|
||||
قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -292,19 +292,19 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
</Warning>
|
||||
|
||||
## الوظائف المنطقية
|
||||
## وظائف بلا خادم
|
||||
|
||||
تدعم Twenty الوظائف المنطقية لعمليات سير العمل والمنطق المخصص. يتم تكوين بيئة التنفيذ عبر متغير البيئة `SERVERLESS_TYPE`.
|
||||
تدعم Twenty وظائف بلا خادم لعمليات سير العمل والمنطق المخصص. يتم تكوين بيئة التنفيذ عبر متغير البيئة `SERVERLESS_TYPE`.
|
||||
|
||||
<Warning>
|
||||
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
|
||||
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي لوظائف بلا خادم (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
|
||||
</Warning>
|
||||
|
||||
### برامج التشغيل المتاحة
|
||||
|
||||
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
|
||||
| -------------- | -------------------------- | ------------------------------- | ----------------------------- |
|
||||
| معطل | `SERVERLESS_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
|
||||
| معطل | `SERVERLESS_TYPE=DISABLED` | تعطيل وظائف بلا خادم بالكامل | غير متاح |
|
||||
| محلي | `SERVERLESS_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
|
||||
|
||||
@@ -326,12 +326,12 @@ SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**لتعطيل الوظائف المنطقية:**
|
||||
**لتعطيل وظائف بلا خادم:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
<Note>
|
||||
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة منطقية إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون إمكانات الوظائف المنطقية.
|
||||
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة بلا خادم إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون قدرات وظائف بلا خادم.
|
||||
</Note>
|
||||
|
||||
@@ -59,4 +59,4 @@ description: ميزات مدعومة بالذكاء الاصطناعي قادم
|
||||
سنحدّث هذا القسم عندما تصبح ميزات الذكاء الاصطناعي متاحة. وفي هذه الأثناء:
|
||||
|
||||
* تابع [GitHub](https://github.com/twentyhq/twenty) للاطّلاع على تحديثات التطوير
|
||||
* انضم إلى [Discord](https://discord.gg/UfGNZJfAG6) لمشاركة الملاحظات وطلبات الميزات
|
||||
* انضم إلى [Discord](https://discord.gg/twenty) لمشاركة الملاحظات وطلبات الميزات
|
||||
|
||||
@@ -39,15 +39,11 @@ description: اربط السجلات عبر كائنات مختلفة باستخ
|
||||
|
||||
**مثال:** يمكن ربط العديد من الأشخاص بالعديد من المشاريع، والعكس صحيح.
|
||||
|
||||
تستخدم العلاقات من نوع متعدد إلى متعدد نمط **كائن ربط**: كائن وسيط يربط بين الجانبين. باستخدام ميزة علاقة الربط، تعرض Twenty السجلات المرتبطة النهائية مباشرةً، مع إخفاء الكائن الوسيط من واجهة المستخدم.
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-diagram.png" style={{width:'100%'}} />
|
||||
|
||||
<Warning>
|
||||
**ميزة المختبر**: يجب تمكين علاقات الربط في **الإعدادات → التحديثات → المختبر** قبل الاستخدام.
|
||||
</Warning>
|
||||
**متعدد-إلى-متعدد غير مدعوم بعد.**
|
||||
|
||||
راجع [كيفية إنشاء علاقات متعدد-إلى-متعدد](/l/ar/user-guide/data-model/how-tos/create-many-to-many-relations) للحصول على دليل كامل خطوة بخطوة.
|
||||
هذا النوع من العلاقات مُخطّط للنصف الأول من عام 2026. كحل بديل، أنشئ كائنًا وسيطًا "junction" (مثال: "Project Assignments") لديه علاقات من نوع متعدد-إلى-واحد مع كلا الكائنين.
|
||||
</Warning>
|
||||
|
||||
## إنشاء حقل علاقة
|
||||
|
||||
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
---
|
||||
title: إنشاء علاقات متعدد-إلى-متعدد
|
||||
description: اربط السجلات حيث يمكن ربط عناصر كثيرة على كلا الجانبين معًا باستخدام كائنات الربط.
|
||||
---
|
||||
|
||||
تتيح علاقات متعدد-إلى-متعدد ربط سجلات متعددة على كلا الجانبين. على سبيل المثال: يمكن للعديد من الأشخاص العمل على العديد من المشاريع، ويمكن لكل مشروع أن يضم العديد من الأشخاص.
|
||||
|
||||
<Warning>
|
||||
**ميزة المختبر**: علاقات الربط متاحة حاليًا في المختبر. فعِّلها في **الإعدادات → التحديثات → المختبر** قبل اتباع هذا الدليل.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
تتطلب هذه الميزة أيضًا تفعيل **الوضع المتقدم** (زر التبديل في أسفل يمين صفحة الإعدادات).
|
||||
</Note>
|
||||
|
||||
## متى نستخدم علاقات متعدد-إلى-متعدد
|
||||
|
||||
استخدم علاقات متعدد-إلى-متعدد عندما يمكن لكل جانب من العلاقة أن يحتوي على عدة ارتباطات:
|
||||
|
||||
| العلاقة | مثال |
|
||||
| ------------------ | --------------------------------------------------------------------- |
|
||||
| الأشخاص ↔ المشاريع | قد يعمل الشخص على عدة مشاريع؛ ويضم المشروع عدة أعضاء فريق |
|
||||
| الشركات ↔ الوسوم | يمكن أن تحتوي الشركة على عدة وسوم؛ ويمكن أن ينطبق الوسم على عدة شركات |
|
||||
| المنتجات ↔ الطلبات | يمكن أن يوجد المنتج في عدة طلبات؛ ويحتوي الطلب على عدة منتجات |
|
||||
|
||||
## كيف يعمل
|
||||
|
||||
تستخدم Twenty نمط **كائن الربط** لعلاقات متعدد-إلى-متعدد. يوضع كائن الربط بين كائنين ويحتفظ بالارتباطات:
|
||||
|
||||
```
|
||||
People ←→ Project Assignments ←→ Projects
|
||||
```
|
||||
|
||||
يحتوي كائن **تعيينات المشروع** (كائن ربط) على:
|
||||
|
||||
* علاقة مع الأشخاص (متعدد-إلى-واحد)
|
||||
* علاقة مع المشاريع (متعدد-إلى-واحد)
|
||||
|
||||
عند تفعيل مفتاح علاقة الربط، تعرض Twenty السجلات المرتبطة مباشرةً بدلًا من إظهار سجلات كائن الربط الوسيطة.
|
||||
|
||||
## المتطلبات الأساسية
|
||||
|
||||
1. **تفعيل علاقات الربط في المختبر**: انتقل إلى **الإعدادات → التحديثات → المختبر** وفعِّل **علاقات الربط**
|
||||
2. **فعِّل الوضع المتقدم**: شغِّل **الوضع المتقدم** من أسفل يمين الشريط الجانبي لصفحة الإعدادات
|
||||
3. خطِّط نموذج البيانات الخاص بك:
|
||||
* ما الكائنان اللذان ستربطهما؟
|
||||
* ما الاسم الذي ينبغي أن يُطلق على كائن الربط؟
|
||||
|
||||
## الخطوة 1: إنشاء كائن الربط
|
||||
|
||||
أولًا، أنشئ الكائن الوسيط الذي سيحتفظ بالارتباطات.
|
||||
|
||||
1. اذهب إلى **الإعدادات → نموذج البيانات**
|
||||
2. انقر **+ كائن جديد**
|
||||
3. سمِّه تسمية وصفية (مثلًا: "تعيين مشروع"، "عضو فريق"، "طلب منتج")
|
||||
4. انقر على **حفظ**
|
||||
|
||||
<Tip>
|
||||
**اتفاقية التسمية**: استخدم اسمًا يصف العلاقة، مثل "تعيين مشروع" أو "عضوية الفريق". هذا يجعل نموذج البيانات أسهل في الفهم.
|
||||
</Tip>
|
||||
|
||||
## الخطوة 2: إنشاء علاقات من كائن الربط
|
||||
|
||||
أضِف حقول علاقة من كائن الربط إلى كلا الكائنين اللذين تريد ربطهما.
|
||||
|
||||
### العلاقة الأولى (كائن الربط → الكائن A)
|
||||
|
||||
1. حدِّد كائن الربط في **الإعدادات → نموذج البيانات**
|
||||
2. انقر **+ إضافة حقل**
|
||||
3. اختر **العلاقة** كنوع الحقل
|
||||
4. اختر الكائن الأول (مثلًا، "الأشخاص")
|
||||
5. عيِّن نوع العلاقة إلى **متعدد-إلى-واحد** (يمكن لعديد من التعيينات الارتباط بشخص واحد)
|
||||
6. قم بتسمية الحقول:
|
||||
* الحقل على كائن الربط: مثلًا، "شخص"
|
||||
* الحقل على الأشخاص: مثلًا، "تعيينات المشروع"
|
||||
7. انقر على **حفظ**
|
||||
|
||||
### العلاقة الثانية (كائن الربط → الكائن B)
|
||||
|
||||
1. وأنت ما زلت في كائن الربط، انقر **+ إضافة حقل**
|
||||
2. اختر **العلاقة** كنوع الحقل
|
||||
3. اختر الكائن الثاني (مثلًا، "المشاريع")
|
||||
4. عيِّن نوع العلاقة إلى **متعدد-إلى-واحد**
|
||||
5. قم بتسمية الحقول:
|
||||
* الحقل على كائن الربط: مثلًا، "مشروع"
|
||||
* الحقل على المشاريع: مثلًا، "أعضاء الفريق"
|
||||
6. انقر على **حفظ**
|
||||
|
||||
## الخطوة 3: ضبط عرض علاقة الربط
|
||||
|
||||
قم الآن بضبط كائنات المصدر لعرض السجلات المرتبطة مباشرةً، مع تجاوز كائن الربط الوسيط.
|
||||
|
||||
1. اذهب إلى **الإعدادات → نموذج البيانات**
|
||||
2. اختر الكائن الأول (مثلًا، "الأشخاص")
|
||||
3. اعثر على حقل العلاقة الذي يشير إلى كائن الربط (مثلًا، "تعيينات المشروع")
|
||||
4. انقر لتحرير الحقل
|
||||
5. فعّل **"هذه علاقة بكائن ربط"**
|
||||
6. حدِّد **العلاقة الهدف** (مثلًا، "مشروع" — الحقل على كائن الربط الذي يشير إلى الجانب الآخر)
|
||||
7. انقر على **حفظ**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
كرِّر على الكائن الآخر:
|
||||
|
||||
1. اختر "المشاريع" في نموذج البيانات
|
||||
2. حرِّر حقل العلاقة "أعضاء الفريق"
|
||||
3. فعّل مفتاح الربط
|
||||
4. حدِّد "شخص" كالعلاقة الهدف
|
||||
5. حفظ
|
||||
|
||||
## النتيجة
|
||||
|
||||
بعد التكوين:
|
||||
|
||||
* في سجل **شخص**، يعرض حقل "تعيينات المشروع" **المشاريع** مباشرةً (وليس سجلات التعيين)
|
||||
* في سجل **مشروع**، يعرض حقل "أعضاء الفريق" **الأشخاص** مباشرةً
|
||||
|
||||
لا يزال كائن الربط موجودًا ويخزّن الارتباطات، لكن واجهة المستخدم تقدّم عرضًا أوضح لعلاقات متعدد-إلى-متعدد.
|
||||
|
||||
## مثال: الأشخاص ↔ المشاريع
|
||||
|
||||
إليك شرحًا كاملًا خطوة بخطوة:
|
||||
|
||||
### إنشاء كائن الربط
|
||||
|
||||
* الاسم: **تعيين مشروع**
|
||||
* الوصف: "يربط الأشخاص بالمشاريع التي يعملون عليها"
|
||||
|
||||
### إضافة علاقات
|
||||
|
||||
1. **تعيين مشروع → الأشخاص**
|
||||
* النوع: متعدد-إلى-واحد
|
||||
* الحقل على التعيين: "شخص"
|
||||
* الحقل على الأشخاص: "تعيينات المشروع"
|
||||
|
||||
2. **تعيين مشروع → المشاريع**
|
||||
* النوع: متعدد-إلى-واحد
|
||||
* الحقل على التعيين: "مشروع"
|
||||
* الحقل على المشاريع: "أعضاء الفريق"
|
||||
|
||||
### ضبط عرض علاقة الربط
|
||||
|
||||
1. على كائن **الأشخاص**:
|
||||
* حرِّر حقل "تعيينات المشروع"
|
||||
* فعّل مفتاح الربط
|
||||
* الهدف: "مشروع"
|
||||
|
||||
2. على كائن **المشاريع**:
|
||||
* حرِّر حقل "أعضاء الفريق"
|
||||
* فعّل مفتاح الربط
|
||||
* الهدف: "شخص"
|
||||
|
||||
### استخدمه
|
||||
|
||||
* افتح سجل شخص → سترى مشاريعه مباشرةً
|
||||
* افتح سجل مشروع → سترى أعضاء الفريق مباشرةً
|
||||
* أنشئ ارتباطات جديدة من أي جانب
|
||||
|
||||
## إضافة بيانات إضافية إلى الارتباطات
|
||||
|
||||
نظرًا لأن كائن الربط كائن حقيقي، يمكنك إضافة حقول مخصصة لتخزين معلومات حول العلاقة:
|
||||
|
||||
* **الدور**: "مطوّر"، "مصمّم"، "مدير"
|
||||
* **تاريخ البدء**: متى انضمّوا إلى المشروع
|
||||
* **الساعات المخصّصة**: عدد الساعات الأسبوعية على هذا المشروع
|
||||
|
||||
للوصول إلى هذه البيانات، انتقل إلى كائن الربط مباشرةً أو استعلم عنها عبر واجهة API.
|
||||
|
||||
## القيود
|
||||
|
||||
* **استيراد/تصدير CSV**: لا يُدعم استيراد علاقات متعدد-إلى-متعدد مباشرةً. بدلًا من ذلك، استورد السجلات إلى كائن الربط.
|
||||
* **عوامل التصفية**: قد تكون خيارات التصفية حسب علاقات متعدد-إلى-متعدد محدودة.
|
||||
|
||||
## ذات صلة
|
||||
|
||||
* [حقول العلاقات](/l/ar/user-guide/data-model/capabilities/relation-fields) — شرح أنواع العلاقات
|
||||
* [إنشاء كائنات مخصصة](/l/ar/user-guide/data-model/how-tos/create-custom-objects) — كيفية إنشاء الكائنات
|
||||
* [إنشاء حقول العلاقات](/l/ar/user-guide/data-model/how-tos/create-relation-fields) — إعداد العلاقات الأساسي
|
||||
@@ -9,7 +9,7 @@ description: تعرّف على المصطلحات الأساسية المستخ
|
||||
|
||||
## التطبيقات
|
||||
|
||||
التطبيقات هي امتدادات مخصّصة مُنشأة على شكل شيفرة يمكنها تعريف نماذج البيانات والوظائف المنطقية. تمكّن المطورين من إنشاء تخصيصات قابلة لإعادة الاستخدام يمكن نشرها عبر مساحات عمل متعددة.
|
||||
التطبيقات هي امتدادات مخصّصة مُنشأة على شكل شيفرة يمكنها تعريف نماذج البيانات ووظائف بدون خوادم. تمكّن المطورين من إنشاء تخصيصات قابلة لإعادة الاستخدام يمكن نشرها عبر مساحات عمل متعددة.
|
||||
|
||||
## إجراءات الكود
|
||||
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ image: /images/user-guide/workflows/workflow.png
|
||||
| **تأكيدات الفعاليات** | تفاصيل الفعالية أو جدول الأعمال |
|
||||
|
||||
<Note>
|
||||
المرفقات ثابتة—يتم إرسال الملف نفسه إلى جميع المستلمين. بالنسبة للمستندات الديناميكية (مثل عروض الأسعار المخصصة)، أنشئ الملفات وأرفقها باستخدام [وظيفة منطقية](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty).
|
||||
المرفقات ثابتة—يتم إرسال الملف نفسه إلى جميع المستلمين. بالنسبة للمستندات الديناميكية (مثل عروض الأسعار المخصصة)، أنشئ الملفات وأرفقها باستخدام [وظيفة بدون خادم](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty).
|
||||
</Note>
|
||||
|
||||
## أفضل الممارسات
|
||||
|
||||
@@ -256,7 +256,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
* اختبر الكود مباشرة في الخطوة
|
||||
|
||||
<Note>
|
||||
إذا كنت بحاجة إلى استخدام مفاتيح API خارجية في كودك، فيجب إدخالها مباشرةً في جسم الدالة. لا يمكنك تكوين مفاتيح API في مكان آخر والإشارة إليها في الدالة المنطقية.
|
||||
إذا كنت بحاجة إلى استخدام مفاتيح API خارجية في كودك، فيجب إدخالها مباشرةً في جسم الدالة. لا يمكنك تكوين مفاتيح API في مكان آخر والإشارة إليها في الدالة عديمة الخادم.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
|
||||
+9
-9
@@ -7,7 +7,7 @@ description: أنشئ سير عمل لإنشاء ملف PDF (مثل عرض سع
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
يستخدم سير العمل هذا **المحفز اليدوي** بحيث يتمكن المستخدمون من إنشاء ملف PDF عند الطلب لأي سجل محدد. تتولى **الوظيفة المنطقية** ما يلي:
|
||||
يستخدم سير العمل هذا **المحفز اليدوي** بحيث يتمكن المستخدمون من إنشاء ملف PDF عند الطلب لأي سجل محدد. **وظيفة بلا خادم** تتولى ما يلي:
|
||||
|
||||
1. تنزيل ملف PDF من عنوان URL (من خدمة إنشاء PDF)
|
||||
2. رفع الملف إلى Twenty
|
||||
@@ -17,7 +17,7 @@ description: أنشئ سير عمل لإنشاء ملف PDF (مثل عرض سع
|
||||
|
||||
قبل إعداد سير العمل:
|
||||
|
||||
1. **أنشئ مفتاح API**: انتقل إلى **الإعدادات → واجهات برمجة التطبيقات** ثم أنشئ مفتاح API جديدًا. ستحتاج إلى هذا الرمز المميز للوظيفة المنطقية.
|
||||
1. **أنشئ مفتاح API**: انتقل إلى **الإعدادات → واجهات برمجة التطبيقات** ثم أنشئ مفتاح API جديدًا. ستحتاج إلى هذا الرمز المميز للوظيفة بلا خادم.
|
||||
2. **قم بإعداد خدمة إنشاء PDF** (اختياري): إذا كنت تريد إنشاء ملفات PDF ديناميكيًا (مثل عروض الأسعار)، فاستخدم خدمة مثل Carbone أو PDFMonkey أو DocuSeal لإنشاء ملف PDF والحصول على رابط تنزيل.
|
||||
|
||||
## إعداد خطوة بخطوة
|
||||
@@ -32,9 +32,9 @@ description: أنشئ سير عمل لإنشاء ملف PDF (مثل عرض سع
|
||||
باستخدام المحفز اليدوي، يمكن للمستخدمين تشغيل سير العمل هذا عبر زر يظهر في أعلى اليمين عند تحديد سجل، وذلك لإنشاء ملف PDF وإرفاقه.
|
||||
</Tip>
|
||||
|
||||
### الخطوة 2: إضافة وظيفة منطقية
|
||||
### الخطوة 2: إضافة وظيفة بلا خادم
|
||||
|
||||
1. أضف إجراء **Code** (وظيفة منطقية)
|
||||
1. أضف إجراء **وظيفة بلا خادم**
|
||||
2. أنشئ وظيفة جديدة باستخدام الكود أدناه
|
||||
3. قم بتهيئة معلمات الإدخال
|
||||
|
||||
@@ -45,10 +45,10 @@ description: أنشئ سير عمل لإنشاء ملف PDF (مثل عرض سع
|
||||
| `companyId` | `{{trigger.object.id}}` |
|
||||
|
||||
<Note>
|
||||
إذا كنت تُرفِقه بكائن مختلف (شخص، فرصة، إلخ)، فأعد تسمية المعلمة وفقًا لذلك (مثلًا، `personId`، `opportunityId`) وحدّث الوظيفة المنطقية.
|
||||
إذا كنت تُرفق إلى كائن مختلف (شخص، فرصة، إلخ)، فأعد تسمية المعلمة وفقًا لذلك (مثلًا، `personId`، `opportunityId`) وحدث الوظيفة بلا خادم.
|
||||
</Note>
|
||||
|
||||
#### كود الوظيفة المنطقية
|
||||
#### كود الوظيفة بلا خادم
|
||||
|
||||
```typescript
|
||||
export const main = async (
|
||||
@@ -172,7 +172,7 @@ export const main = async (
|
||||
إذا كنت تستخدم خدمة إنشاء PDF، يمكنك:
|
||||
|
||||
1. أولًا، أنشئ إجراء طلب HTTP لإنشاء ملف PDF
|
||||
2. مرّر رابط ملف PDF المُعاد إلى الوظيفة المنطقية كمعلمة
|
||||
2. مرّر رابط ملف PDF المُعاد إلى الوظيفة بلا خادم كمعلمة
|
||||
|
||||
```typescript
|
||||
export const main = async (
|
||||
@@ -211,7 +211,7 @@ export const main = async (
|
||||
* **DocuSeal** - منصة أتمتة المستندات
|
||||
* **Documint** - إنشاء مستندات يعتمد على واجهة برمجة التطبيقات أولًا
|
||||
|
||||
توفر كل خدمة واجهة برمجة تطبيقات تُرجع رابط ملف PDF، ويمكنك بعدها تمريره إلى الوظيفة المنطقية.
|
||||
توفر كل خدمة واجهة برمجة تطبيقات تُرجع رابط ملف PDF، ويمكنك بعدها تمريره إلى الوظيفة بلا خادم.
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
@@ -224,5 +224,5 @@ export const main = async (
|
||||
## ذات صلة
|
||||
|
||||
* [مشغلات سير العمل](/l/ar/user-guide/workflows/capabilities/workflow-triggers)
|
||||
* [الوظائف المنطقية](/l/ar/user-guide/workflows/capabilities/workflow-actions#code)
|
||||
* [وظائف بلا خادم](/l/ar/user-guide/workflows/capabilities/workflow-actions#serverless-function)
|
||||
* [إنشاء عرض سعر أو فاتورة من Twenty](/l/ar/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ description: الأسئلة الشائعة حول سير العمل في Twenty.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="ما أقصى وقت تنفيذ لإجراءات Code؟">
|
||||
إجراءات Code (دوال منطقية) لديها **مهلة افتراضية قدرها 5 دقائق** (300 ثانية).
|
||||
إجراءات Code (دوال بلا خادم) لديها **مهلة افتراضية قدرها 5 دقائق** (300 ثانية).
|
||||
|
||||
أقصى مهلة يمكن ضبطها هي **15 دقيقة** (900 ثانية).
|
||||
|
||||
|
||||
@@ -169,13 +169,6 @@ Všechny příkazy v následujících krocích byste měli provádět z kořene
|
||||
|
||||
Tím vytvoříte superuživatelskou roli pojmenovanou `postgres` s přístupovými právy.
|
||||
|
||||
```bash
|
||||
Jméno role | Vlastnosti | Členem
|
||||
-----------+-------------+-----------
|
||||
postgres | Superuživatel | {}
|
||||
john | Superuživatel | {}
|
||||
```
|
||||
|
||||
**Možnost 2:** Pokud máte nainstalován docker:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -9,14 +9,18 @@ description: Vytvářejte a spravujte přizpůsobení Twenty jako kód.
|
||||
|
||||
## Co jsou aplikace?
|
||||
|
||||
Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako kód**. Místo konfigurace všeho přes uživatelské rozhraní definujete v kódu svůj datový model a logické funkce — což zrychluje vývoj, údržbu i nasazování do více pracovních prostorů.
|
||||
Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako kód**. Místo konfigurace všeho přes uživatelské rozhraní definujete v kódu svůj datový model a serverless funkce — což zrychluje vývoj, údržbu i nasazování do více pracovních prostorů.
|
||||
|
||||
**Co můžete dělat už dnes:**
|
||||
|
||||
* Definujte vlastní objekty a pole jako kód (spravovaný datový model)
|
||||
* Vytvářejte logické funkce s vlastními spouštěči
|
||||
* Vytvářejte serverless funkce s vlastními spouštěči
|
||||
* Nasazujte stejnou aplikaci do více pracovních prostorů
|
||||
|
||||
**Již brzy:**
|
||||
|
||||
* Vlastní rozvržení a komponenty uživatelského rozhraní
|
||||
|
||||
## Předpoklady
|
||||
|
||||
* Node.js 24+ a Yarn 4
|
||||
@@ -45,23 +49,26 @@ yarn app:dev
|
||||
Odtud můžete:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Přidejte do vaší aplikace novou entitu (s průvodcem)
|
||||
yarn entity:add
|
||||
# Add a new entity to your application (guided)
|
||||
yarn app:create-entity
|
||||
|
||||
# Vygenerujte typovaného klienta Twenty a typy entit pracovního prostoru
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# Sledujte logy funkcí vaší aplikace
|
||||
# Run a one‑time sync (instead of watch mode)
|
||||
yarn app:sync
|
||||
|
||||
# Watch your application's functions logs
|
||||
yarn function:logs
|
||||
|
||||
# Spusťte funkci podle názvu
|
||||
# Execute a function by name
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Odinstalujte aplikaci z aktuálního pracovního prostoru
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
|
||||
# Zobrazte nápovědu k příkazům
|
||||
yarn help
|
||||
# Display commands' help
|
||||
yarn app:help
|
||||
```
|
||||
|
||||
Viz také: referenční stránky CLI pro [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) a [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -89,61 +96,82 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Složka s veřejnými prostředky (obrázky, písma apod.)
|
||||
src/
|
||||
├── application-config.ts # Povinné – hlavní konfigurace aplikace
|
||||
├── roles/
|
||||
│ └── default-role.ts # Výchozí role pro logické funkce
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Ukázková logická funkce
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Ukázková front-endová komponenta
|
||||
app/
|
||||
application.config.ts # Povinné - hlavní konfigurace aplikace
|
||||
default-function.role.ts # Výchozí role pro serverless funkce
|
||||
// vaše entity (*.object.ts, *.function.ts, *.role.ts)
|
||||
utils/ # Volitelné - implementace handlerů a nástroje
|
||||
```
|
||||
|
||||
### Konvence před konfigurací
|
||||
|
||||
Aplikace používají přístup **konvence před konfigurací**, kde jsou entity detekovány podle přípony souboru. To umožňuje flexibilní organizaci ve složce `src/app/`:
|
||||
|
||||
| Přípona souboru | Typ entity |
|
||||
| --------------- | -------------------------- |
|
||||
| `*.object.ts` | Definice vlastních objektů |
|
||||
| `*.function.ts` | Definice serverless funkcí |
|
||||
| `*.role.ts` | Definice rolí |
|
||||
|
||||
### Podporované uspořádání složek
|
||||
|
||||
Entity můžete uspořádat podle některého z těchto vzorů:
|
||||
|
||||
**Tradiční (podle typu):**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Podle funkcí:**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Plochá:**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
V kostce:
|
||||
|
||||
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skripty jako `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` a autentizační příkazy, které delegují na lokální `twenty` CLI.
|
||||
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skripty jako `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall` a `auth`, které delegují na lokální `twenty` CLI.
|
||||
* **.gitignore**: Ignoruje běžné artefakty jako `node_modules`, `.yarn`, `generated/` (typovaný klient), `dist/`, `build/`, složky s coverage, 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.
|
||||
* **eslint.config.mjs** a **tsconfig.json**: Poskytují lintování a konfiguraci TypeScriptu pro zdrojové soubory vaší aplikace v TypeScriptu.
|
||||
* **README.md**: Krátké README v kořeni aplikace se základními pokyny.
|
||||
* **public/**: Složka pro ukládání veřejných prostředků (obrázky, písma, statické soubory), které bude vaše aplikace poskytovat. Soubory umístěné zde se během synchronizace nahrají a jsou za běhu dostupné.
|
||||
* **src/**: Hlavní místo, kde definujete svou aplikaci jako kód
|
||||
|
||||
### Detekce entit
|
||||
|
||||
SDK detekuje entity analýzou vašich souborů TypeScript a hledá volání **`export default define<Entity>({...})`**. Každý typ entity má odpovídající pomocnou funkci exportovanou z `twenty-sdk`:
|
||||
|
||||
| Pomocná funkce | Typ entity |
|
||||
| ------------------------ | ------------------------------------- |
|
||||
| `defineObject()` | Definice vlastních objektů |
|
||||
| `defineLogicFunction()` | Definice logických funkcí |
|
||||
| `defineFrontComponent()` | Definice frontendových komponent |
|
||||
| `defineRole()` | Definice rolí |
|
||||
| `defineField()` | Rozšíření polí u existujících objektů |
|
||||
|
||||
<Note>
|
||||
**Pojmenování souborů je flexibilní.** Detekce entit je založená na AST — SDK prochází vaše zdrojové soubory a hledá vzor `export default define<Entity>({...})`. Soubory a složky můžete organizovat, jak chcete. Seskupování podle typu entity (např. `logic-functions/`, `roles/`) je pouze konvence pro organizaci kódu, nikoli požadavek.
|
||||
</Note>
|
||||
|
||||
Příklad detekované entity:
|
||||
|
||||
```typescript
|
||||
// This file can be named anything and placed anywhere in src/
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCard',
|
||||
// ... rest of config
|
||||
});
|
||||
```
|
||||
* **src/app/**: Hlavní místo, kde definujete svou aplikaci jako kód:
|
||||
* `application.config.ts`: Globální konfigurace vaší aplikace (metadata a napojení za běhu). Viz „Konfigurace aplikace“ níže.
|
||||
* `*.role.ts`: Definice rolí používané vašimi serverless funkcemi. Viz „Výchozí role funkce“ níže.
|
||||
* `*.object.ts`: Definice vlastních objektů.
|
||||
* `*.function.ts`: Definice serverless funkcí.
|
||||
* **src/utils/**: Volitelná složka pro implementace obslužných funkcí a pomocné nástroje.
|
||||
|
||||
Pozdější příkazy přidají další soubory a složky:
|
||||
|
||||
* `yarn app:generate` vytvoří složku `generated/` (typovaný klient Twenty + typy pracovního prostoru).
|
||||
* `yarn entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty nebo role.
|
||||
* `yarn app:create-entity` přidá soubory s definicemi entit do `src/app/` pro vaše vlastní objekty, funkce nebo role.
|
||||
l
|
||||
|
||||
## Ověření
|
||||
|
||||
@@ -184,18 +212,16 @@ twenty-sdk poskytuje typované stavební bloky a pomocné funkce, které použí
|
||||
|
||||
### Pomocné funkce
|
||||
|
||||
SDK poskytuje pomocné funkce pro definování entit vaší aplikace. Jak je popsáno v [Detekce entit](#entity-detection), musíte použít `export default define<Entity>({...})`, aby byly vaše entity detekovány:
|
||||
SDK poskytuje čtyři pomocné funkce s vestavěnou validací pro definování entit vaší aplikace:
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
| Funkce | Účel |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| `defineApp()` | Konfigurace metadat aplikace |
|
||||
| `defineObject()` | Definice vlastních objektů s poli |
|
||||
| `defineFunction()` | Definice serverless funkcí s obslužnými funkcemi |
|
||||
| `defineRole()` | Konfigurace oprávnění rolí a přístupu k objektům |
|
||||
|
||||
Tyto funkce validují vaši konfiguraci v době sestavení a poskytují automatické doplňování v IDE a typovou bezpečnost.
|
||||
Tyto funkce validují vaši konfiguraci za běhu a poskytují lepší automatické doplňování v IDE a lepší typovou bezpečnost.
|
||||
|
||||
### Definování objektů
|
||||
|
||||
@@ -276,28 +302,79 @@ 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 entity:add`, který vás provede pojmenováním, poli a vztahy.
|
||||
* Nové objekty můžete vygenerovat pomocí `yarn app:create-entity`, 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 jako `name`, `createdAt`, `updatedAt`, `createdBy`, `position` a `deletedAt`. Nemusíte je definovat v poli `fields` — přidejte pouze svá vlastní pole.
|
||||
</Note>
|
||||
|
||||
### Konfigurace aplikace (application-config.ts)
|
||||
<Accordion title="Alternativa: Syntaxe založená na dekorátorech">
|
||||
Objekty můžete definovat také pomocí dekorátorů TypeScriptu. Tento přístup používá třídovou syntaxi s dekorátory `@Object`, `@Field` a `@Relation`:
|
||||
|
||||
Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
|
||||
```typescript
|
||||
import {
|
||||
type AddressField,
|
||||
Field,
|
||||
FieldType,
|
||||
type FullNameField,
|
||||
Object,
|
||||
OnDeleteAction,
|
||||
Relation,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
import { type Note } from '../../generated';
|
||||
|
||||
@Object({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post card',
|
||||
labelPlural: 'Post cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
export class PostCard {
|
||||
@Field({
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
})
|
||||
content: string;
|
||||
|
||||
@Relation({
|
||||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
}
|
||||
```
|
||||
|
||||
Poznámka: Přístup s dekorátory vyžaduje `experimentalDecorators` v konfiguraci TypeScriptu.
|
||||
</Accordion>
|
||||
|
||||
### Konfigurace aplikace (application.config.ts)
|
||||
|
||||
Každá aplikace má jeden soubor `application.config.ts`, který popisuje:
|
||||
|
||||
* **Identitu aplikace**: identifikátory, zobrazovaný název a popis.
|
||||
* **Jak běží její funkce**: kterou roli používají pro oprávnění.
|
||||
* **(Volitelné) proměnné**: dvojice klíč–hodnota zpřístupněné vašim funkcím jako proměnné prostředí.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
K definování konfigurace aplikace použijte `defineApp()`:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
// src/app/application.config.ts
|
||||
import { defineApp } from 'twenty-sdk';
|
||||
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
export default defineApp({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
@@ -310,7 +387,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +395,11 @@ Poznámky:
|
||||
|
||||
* Pole `universalIdentifier` jsou deterministická ID, která vlastníte; vygenerujte je jednou a udržujte je stabilní napříč synchronizacemi.
|
||||
* `applicationVariables` se stanou proměnnými prostředí pro vaše funkce (například `DEFAULT_RECIPIENT_NAME` je dostupné jako `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` se musí shodovat se souborem role (viz níže).
|
||||
* `functionRoleUniversalIdentifier` se musí shodovat s rolí, kterou definujete ve svém souboru `*.role.ts` (viz níže).
|
||||
|
||||
#### 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.
|
||||
Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a akcím ve vašem pracovním prostoru. Pole `functionRoleUniversalIdentifier` v `application.config.ts` určuje výchozí roli používanou serverless funkcemi vaší aplikace.
|
||||
|
||||
* Běhový klíč API vložený jako `TWENTY_API_KEY` je odvozen z této výchozí role funkcí.
|
||||
* Typovaný klient bude omezen oprávněními udělenými této roli.
|
||||
@@ -333,14 +410,14 @@ Aplikace mohou definovat role, které zapouzdřují oprávnění k objektům a a
|
||||
Když vygenerujete novou aplikaci, CLI také vytvoří výchozí soubor role. K definování rolí s vestavěnou validací použijte `defineRole()`:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: false,
|
||||
@@ -353,7 +430,7 @@ export default defineRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectNameSingular: 'postCard',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -362,8 +439,8 @@ export default defineRole({
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectNameSingular: 'postCard',
|
||||
fieldName: 'content',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
@@ -372,10 +449,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
Na `universalIdentifier` této role se poté odkazuje v `application-config.ts` jako na `defaultRoleUniversalIdentifier`. Jinými slovy:
|
||||
Na `universalIdentifier` této role se poté odkazuje v `application.config.ts` jako na `functionRoleUniversalIdentifier`. Jinými slovy:
|
||||
|
||||
* **\*.role.ts** definuje, co může výchozí role funkce dělat.
|
||||
* **application-config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění.
|
||||
* **application.config.ts** ukazuje na tuto roli, aby vaše funkce zdědily její oprávnění.
|
||||
|
||||
Poznámky:
|
||||
|
||||
@@ -384,17 +461,22 @@ Poznámky:
|
||||
* `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).
|
||||
|
||||
### Konfigurace logických funkcí a vstupní bod
|
||||
### Konfigurace serverless funkcí a vstupní bod
|
||||
|
||||
Každý soubor funkce používá `defineLogicFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči.
|
||||
Každý soubor funkce používá `defineFunction()` k exportu konfigurace s obslužnou funkcí (handlerem) a volitelnými spouštěči. Pro automatickou detekci použijte příponu souboru `*.function.ts`.
|
||||
|
||||
```typescript
|
||||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
// 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';
|
||||
import Twenty, { type Person } from '../../generated';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const handler = async (
|
||||
params:
|
||||
| RoutePayload
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||||
| CronPayload,
|
||||
) => {
|
||||
const client = new Twenty(); // generated typed client
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
@@ -410,7 +492,7 @@ const handler = async (params: RoutePayload) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
@@ -425,18 +507,18 @@ export default defineLogicFunction({
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
{
|
||||
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'],
|
||||
// },
|
||||
{
|
||||
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['name'],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -483,10 +565,10 @@ Poznámky:
|
||||
**Jak migrovat existující funkce:** Aktualizujte svůj handler tak, aby destrukturoval z `event.body`, `event.queryStringParameters` nebo `event.pathParameters` místo přímo z objektu params.
|
||||
</Warning>
|
||||
|
||||
Když spouštěč trasy vyvolá vaši logickou funkci, ta obdrží objekt `RoutePayload`, který odpovídá formátu AWS HTTP API v2. Importujte typ z `twenty-sdk`:
|
||||
Když spouštěč trasy vyvolá vaši funkci, ta obdrží objekt `RoutePayload`, který odpovídá formátu AWS HTTP API v2. Importujte typ z `twenty-sdk`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -513,10 +595,10 @@ Typ `RoutePayload` má následující strukturu:
|
||||
|
||||
### Přeposílání záhlaví HTTP
|
||||
|
||||
Ve výchozím nastavení se záhlaví HTTP z příchozích požadavků z bezpečnostních důvodů do vaší logické funkce **ne** předávají. Chcete-li zpřístupnit konkrétní záhlaví, výslovně je uveďte v poli `forwardedRequestHeaders`:
|
||||
Ve výchozím nastavení se záhlaví HTTP z příchozích požadavků z bezpečnostních důvodů do vaší serverless funkce **ne** předávají. Chcete-li zpřístupnit konkrétní záhlaví, výslovně je uveďte v poli `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,60 +633,23 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Nové funkce můžete vytvářet dvěma způsoby:
|
||||
|
||||
* **Vygenerované**: Spusťte `yarn entity: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.
|
||||
|
||||
### Frontendové komponenty
|
||||
|
||||
Frontendové komponenty vám umožňují vytvářet vlastní React komponenty, které se vykreslují v rozhraní Twenty. K definování komponent s vestavěnou validací použijte `defineFrontComponent()`:
|
||||
|
||||
```typescript
|
||||
// src/my-widget.front-component.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const MyWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>My Custom Widget</h1>
|
||||
<p>This is a custom front component for Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'my-widget',
|
||||
description: 'A custom widget component',
|
||||
component: MyWidget,
|
||||
});
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
|
||||
* Frontendové komponenty jsou React komponenty, které se vykreslují v izolovaných kontextech v rámci Twenty.
|
||||
* Pro automatickou detekci použijte příponu souboru `*.front-component.tsx`.
|
||||
* Pole `component` odkazuje na vaši React komponentu.
|
||||
* Komponenty se během `yarn app:dev` automaticky sestaví a synchronizují.
|
||||
|
||||
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
|
||||
|
||||
* **Vygenerované**: Spusťte `yarn entity:add` a zvolte možnost přidat novou frontendovou komponentu.
|
||||
* **Ruční**: Vytvořte nový soubor `*.front-component.tsx` a použijte `defineFrontComponent()`.
|
||||
* **Vygenerované**: Spusťte `yarn app:create-entity` a zvolte možnost přidat novou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
|
||||
* **Ruční**: Vytvořte nový soubor `*.function.ts` a použijte `defineFunction()` podle stejného vzoru.
|
||||
|
||||
### Generovaný typovaný klient
|
||||
|
||||
Spusťte yarn app:generate a vytvořte lokálního typovaného klienta v generated/ na základě schématu vašeho pracovního prostoru. Použijte jej ve svých funkcích:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import Twenty from './generated';
|
||||
|
||||
const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Klient je znovu generován příkazem `yarn app:generate`. Spusťte znovu po změně vašich objektů nebo při připojování k novému pracovnímu prostoru.
|
||||
Klient je znovu generován příkazem `yarn app:generate`. Spusťte jej znovu po změně objektů a po `yarn app:sync`, případně při připojení k novému pracovnímu prostoru.
|
||||
|
||||
#### Běhové přihlašovací údaje v logických funkcích
|
||||
#### Běhové přihlašovací údaje v serverless funkcích
|
||||
|
||||
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í:
|
||||
|
||||
@@ -614,12 +659,12 @@ Když vaše funkce běží na Twenty, platforma před spuštěním kódu vloží
|
||||
Poznámky:
|
||||
|
||||
* 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.
|
||||
* Oprávnění API klíče jsou určena rolí odkazovanou v `application.config.ts` prostřednictvím `functionRoleUniversalIdentifier`. Toto je výchozí role používaná serverless 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 `functionRoleUniversalIdentifier` na univerzální identifikátor této role.
|
||||
|
||||
### Příklad Hello World
|
||||
|
||||
Prozkoumejte minimalistický end-to-end příklad, který demonstruje objekty, logické funkce, frontendové komponenty a více spouštěčů [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Prozkoumejte minimalistický end-to-end příklad, který demonstruje objekty, funkce a více spouštěčů [zde](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Ruční nastavení (bez scaffolderu)
|
||||
|
||||
@@ -634,29 +679,25 @@ Poté přidejte skripty jako tyto:
|
||||
```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"
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nyní můžete spouštět stejné příkazy přes Yarn, např. `yarn app:dev`, `yarn app:generate` atd.
|
||||
Nyní můžete spouštět stejné příkazy přes Yarn, např. `yarn app:dev`, `yarn app:sync` atd.
|
||||
|
||||
## Řešení potíží
|
||||
|
||||
* Chyby ověření: spusťte `yarn auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění.
|
||||
* Nelze se připojit k serveru: ověřte URL API a že je server Twenty dosažitelný.
|
||||
* Typy nebo klient chybí nebo jsou zastaralé: spusťte `yarn app:generate`.
|
||||
* Typy nebo klient chybí/jsou zastaralé: spusťte `yarn app:generate` a poté `yarn app:dev`.
|
||||
* Režim vývoje nesynchronizuje: ujistěte se, že běží `yarn app:dev` a že vaše prostředí změny neignoruje.
|
||||
|
||||
Kanál podpory na Discordu: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -292,19 +292,19 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**Režim pouze s prostředím:** Pokud nastavíte `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, přidejte tyto proměnné do souboru `.env`
|
||||
</Warning>
|
||||
|
||||
## Logické funkce
|
||||
## Serverless funkce
|
||||
|
||||
Twenty podporuje logické funkce pro pracovní postupy a vlastní logiku. Běhové prostředí se konfiguruje pomocí proměnné prostředí `SERVERLESS_TYPE`.
|
||||
Twenty podporuje serverless funkce pro pracovní postupy a vlastní logiku. Běhové prostředí se konfiguruje pomocí proměnné prostředí `SERVERLESS_TYPE`.
|
||||
|
||||
<Warning>
|
||||
**Upozornění na zabezpečení:** Místní ovladač (`SERVERLESS_TYPE=LOCAL`) spouští kód přímo na hostiteli v procesu Node.js bez sandboxu. Měl by být používán pouze pro důvěryhodný kód při vývoji. Pro produkční nasazení, která pracují s nedůvěryhodným kódem, důrazně doporučujeme použít `SERVERLESS_TYPE=LAMBDA` nebo `SERVERLESS_TYPE=DISABLED`.
|
||||
**Upozornění na zabezpečení:** Místní serverless ovladač (`SERVERLESS_TYPE=LOCAL`) spouští kód přímo na hostiteli v procesu Node.js bez sandboxu. Měl by být používán pouze pro důvěryhodný kód při vývoji. Pro produkční nasazení, která pracují s nedůvěryhodným kódem, důrazně doporučujeme použít `SERVERLESS_TYPE=LAMBDA` nebo `SERVERLESS_TYPE=DISABLED`.
|
||||
</Warning>
|
||||
|
||||
### Dostupné ovladače
|
||||
|
||||
| Ovladač | Proměnná prostředí | Případ použití | Úroveň zabezpečení |
|
||||
| --------- | -------------------------- | ------------------------------------------ | ----------------------------------- |
|
||||
| Neaktivní | `SERVERLESS_TYPE=DISABLED` | Úplně zakázat logické funkce | N/A |
|
||||
| Neaktivní | `SERVERLESS_TYPE=DISABLED` | Úplně zakázat serverless funkce | N/A |
|
||||
| Lokální | `SERVERLESS_TYPE=LOCAL` | Vývojová a důvěryhodná prostředí | Nízká (bez sandboxu) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produkční prostředí s nedůvěryhodným kódem | Vysoká (izolace na úrovni hardwaru) |
|
||||
|
||||
@@ -326,12 +326,12 @@ SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**Pro zakázání logických funkcí:**
|
||||
**Pro zakázání serverless funkcí:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
<Note>
|
||||
Při použití `SERVERLESS_TYPE=DISABLED` skončí každý pokus o spuštění logické funkce chybou. To je užitečné, pokud chcete provozovat Twenty bez podpory logických funkcí.
|
||||
Při použití `SERVERLESS_TYPE=DISABLED` skončí každý pokus o spuštění serverless funkce chybou. To je užitečné, pokud chcete provozovat Twenty bez podpory serverless funkcí.
|
||||
</Note>
|
||||
|
||||
@@ -59,4 +59,4 @@ To zajistí, že AI agenti budou respektovat vaše zásady správy dat a budou p
|
||||
Tuto sekci budeme aktualizovat, jakmile budou funkce AI k dispozici. Mezitím:
|
||||
|
||||
* Sledujte náš [GitHub](https://github.com/twentyhq/twenty) pro novinky o vývoji
|
||||
* Přidejte se na náš [Discord](https://discord.gg/UfGNZJfAG6) a podělte se o zpětnou vazbu a návrhy na nové funkce
|
||||
* Přidejte se na náš [Discord](https://discord.gg/twenty) a podělte se o zpětnou vazbu a návrhy na nové funkce
|
||||
|
||||
@@ -39,15 +39,11 @@ Mnoho záznamů v Objektu A může být propojeno s mnoha záznamy v Objektu B.
|
||||
|
||||
**Příklad:** Mnoho lidí může být propojeno s mnoha projekty a naopak.
|
||||
|
||||
Vztahy typu mnoho-na-mnoho používají vzor zvaný **spojovací objekt**: zprostředkující objekt, který propojuje obě strany. S funkcí spojovacího vztahu zobrazuje Twenty přímo výsledné propojené záznamy a zprostředkující objekt v UI skrývá.
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-diagram.png" style={{width:'100%'}} />
|
||||
|
||||
<Warning>
|
||||
**Experimentální funkce**: Vztahy přes spojovací objekt je před použitím nutné povolit v **Settings → Updates → Lab**.
|
||||
</Warning>
|
||||
**Mnoho k mnoha zatím není podporováno.**
|
||||
|
||||
Podívejte se na [Jak vytvořit vztahy typu mnoho k mnoha](/l/cs/user-guide/data-model/how-tos/create-many-to-many-relations) pro úplný návod krok za krokem.
|
||||
Tento typ relace je plánován na 1. pololetí 2026. Jako dočasné řešení vytvořte prostřední "spojovací" objekt (např. "Project Assignments"), který má relace typu mnoho k jednomu k oběma objektům.
|
||||
</Warning>
|
||||
|
||||
## Vytvoření relačního pole
|
||||
|
||||
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
---
|
||||
title: Vytváření vztahů mnoho-na-mnoho
|
||||
description: Propojte záznamy, kde lze mnoho položek na obou stranách vzájemně propojit pomocí spojovacích objektů.
|
||||
---
|
||||
|
||||
Vztahy mnoho-na-mnoho vám umožňují propojit více záznamů na obou stranách. Například: mnoho lidí může pracovat na mnoha projektech a každý projekt může mít mnoho lidí.
|
||||
|
||||
<Warning>
|
||||
**Lab Feature**: Vztahy přes spojovací objekt jsou momentálně v Lab. Povolte je v **Settings → Updates → Lab** předtím, než budete pokračovat podle tohoto návodu.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Tato funkce také vyžaduje, aby byl povolen **Advanced mode** (přepínač je vpravo dole v Settings).
|
||||
</Note>
|
||||
|
||||
## Kdy použít mnoho-na-mnoho
|
||||
|
||||
Použijte mnoho-na-mnoho, když obě strany vztahu mohou mít více propojení:
|
||||
|
||||
| Vztah | Příklad |
|
||||
| --------------------- | --------------------------------------------------------------------------- |
|
||||
| Lidé ↔ Projekty | Osoba pracuje na více projektech; projekt má více členů týmu |
|
||||
| Společnosti ↔ Štítky | Společnost může mít více štítků; štítek může platit pro více společností |
|
||||
| Produkty ↔ Objednávky | Produkt může být v několika objednávkách; objednávka obsahuje více produktů |
|
||||
|
||||
## Jak to funguje
|
||||
|
||||
Twenty používá u vztahů mnoho-na-mnoho vzor **spojovacího objektu**. Spojovací objekt leží mezi dvěma objekty a uchovává propojení:
|
||||
|
||||
```
|
||||
People ←→ Project Assignments ←→ Projects
|
||||
```
|
||||
|
||||
Objekt **Project Assignments** (spojovací) má:
|
||||
|
||||
* Vztah na People (mnoho k jednomu)
|
||||
* Vztah na Projects (mnoho k jednomu)
|
||||
|
||||
Když povolíte přepínač pro vztah přes spojovací objekt, Twenty zobrazí propojené záznamy přímo místo zobrazení prostředních záznamů spojovacího objektu.
|
||||
|
||||
## Předpoklady
|
||||
|
||||
1. **Povolte Junction Relations v Lab**: Přejděte do **Settings → Updates → Lab** a povolte **Junction Relations**
|
||||
2. **Povolte Advanced mode**: Zapněte **Advanced mode** vpravo dole na postranním panelu Settings
|
||||
3. Naplánujte svůj datový model:
|
||||
* Které dva objekty propojujete?
|
||||
* Jak by se měl jmenovat spojovací objekt?
|
||||
|
||||
## Krok 1: Vytvořte spojovací objekt
|
||||
|
||||
Nejprve vytvořte mezilehlý objekt, který bude uchovávat propojení.
|
||||
|
||||
1. Přejděte do **Nastavení → Datový model**
|
||||
2. Klikněte na **+ New object**
|
||||
3. Pojmenujte jej výstižně (např. "Project Assignment", "Team Member", "Product Order")
|
||||
4. Klikněte na **Uložit**
|
||||
|
||||
<Tip>
|
||||
**Konvence pojmenování**: Použijte název, který popisuje vztah, například "Project Assignment" nebo "Team Membership". Tím bude datový model srozumitelnější.
|
||||
</Tip>
|
||||
|
||||
## Krok 2: Vytvořte vztahy ze spojovacího objektu
|
||||
|
||||
Přidejte relační pole ze spojovacího objektu do obou objektů, které chcete propojit.
|
||||
|
||||
### První vztah (Junction → Objekt A)
|
||||
|
||||
1. Vyberte svůj spojovací objekt v **Settings → Data Model**
|
||||
2. Klikněte na **+ Add Field**
|
||||
3. Zvolte **Relation** jako typ pole
|
||||
4. Vyberte první objekt (např. "People")
|
||||
5. Nastavte typ vztahu na **Many-to-One** (mnoho přiřazení může odkazovat na jednu osobu)
|
||||
6. Pojmenujte pole:
|
||||
* Pole na spojovacím objektu: např. "Person"
|
||||
* Pole na People: např. "Project Assignments"
|
||||
7. Klikněte na **Uložit**
|
||||
|
||||
### Druhý vztah (Junction → Objekt B)
|
||||
|
||||
1. Stále na spojovacím objektu klikněte na **+ Add Field**
|
||||
2. Zvolte **Relation** jako typ pole
|
||||
3. Vyberte druhý objekt (např. "Projects")
|
||||
4. Nastavte typ vztahu na **Many-to-One**
|
||||
5. Pojmenujte pole:
|
||||
* Pole na spojovacím objektu: např. "Project"
|
||||
* Pole na Projects: např. "Team Members"
|
||||
6. Klikněte na **Uložit**
|
||||
|
||||
## Krok 3: Nakonfigurujte zobrazení vztahu přes spojovací objekt
|
||||
|
||||
Nyní nakonfigurujte zdrojové objekty tak, aby zobrazovaly propojené záznamy přímo a přeskočily mezilehlý spojovací objekt.
|
||||
|
||||
1. Přejděte do **Nastavení → Datový model**
|
||||
2. Vyberte první objekt (např. "People")
|
||||
3. Najděte relační pole směřující na spojovací objekt (např. "Project Assignments")
|
||||
4. Klikněte pro úpravu pole
|
||||
5. Povolte **"This is a relation to a Junction Object"**
|
||||
6. Vyberte **Target relation** (např. "Project" — pole na spojovacím objektu, které ukazuje na druhou stranu)
|
||||
7. Klikněte na **Uložit**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
Opakujte pro druhý objekt:
|
||||
|
||||
1. Vyberte "Projects" v Data Model
|
||||
2. Upravte relační pole "Team Members"
|
||||
3. Povolte přepínač pro vztah přes spojovací objekt
|
||||
4. Vyberte "Person" jako cílový vztah
|
||||
5. Uložit
|
||||
|
||||
## Výsledek
|
||||
|
||||
Po konfiguraci:
|
||||
|
||||
* Na záznamu **Person** pole "Project Assignments" zobrazuje přímo **Projects** (nikoli záznamy přiřazení)
|
||||
* Na záznamu **Project** pole "Team Members" zobrazuje přímo **People**
|
||||
|
||||
Spojovací objekt stále existuje a ukládá propojení, ale rozhraní zobrazuje čistší pohled mnoho-na-mnoho.
|
||||
|
||||
## Příklad: Lidé ↔ Projekty
|
||||
|
||||
Zde je kompletní postup:
|
||||
|
||||
### Vytvořte spojovací objekt
|
||||
|
||||
* Název: **Project Assignment**
|
||||
* Popis: "Propojuje lidi s projekty, na kterých pracují"
|
||||
|
||||
### Přidejte vztahy
|
||||
|
||||
1. **Project Assignment → People**
|
||||
* Typ: Many-to-One
|
||||
* Pole na Assignment: "Person"
|
||||
* Pole na People: "Project Assignments"
|
||||
|
||||
2. **Project Assignment → Projects**
|
||||
* Typ: Many-to-One
|
||||
* Pole na Assignment: "Project"
|
||||
* Pole na Projects: "Team Members"
|
||||
|
||||
### Nakonfigurujte zobrazení spojovacího objektu
|
||||
|
||||
1. Na objektu **People**:
|
||||
* Upravte pole "Project Assignments"
|
||||
* Povolte přepínač pro vztah přes spojovací objekt
|
||||
* Cíl: "Project"
|
||||
|
||||
2. Na objektu **Projects**:
|
||||
* Upravte pole "Team Members"
|
||||
* Povolte přepínač pro vztah přes spojovací objekt
|
||||
* Cíl: "Person"
|
||||
|
||||
### Použití
|
||||
|
||||
* Otevřete záznam osoby → Uvidíte její projekty přímo
|
||||
* Otevřete záznam projektu → Uvidíte členy týmu přímo
|
||||
* Vytvářejte nová propojení z obou stran
|
||||
|
||||
## Přidávání doplňkových dat do propojení
|
||||
|
||||
Protože spojovací objekt je skutečný objekt, můžete přidat vlastní pole k ukládání informací o vztahu:
|
||||
|
||||
* **Role**: "Developer", "Designer", "Manager"
|
||||
* **Start Date**: Kdy se k projektu připojili
|
||||
* **Hours Allocated**: Týdenní počet hodin na tomto projektu
|
||||
|
||||
Pro přístup k těmto datům přejděte přímo na spojovací objekt nebo jej dotazujte přes API.
|
||||
|
||||
## Omezení
|
||||
|
||||
* **CSV Import/Export**: Přímý import vztahů mnoho-na-mnoho není podporován. Místo toho importujte záznamy do spojovacího objektu.
|
||||
* **Filters**: Filtrování podle vztahů mnoho-na-mnoho může mít omezené možnosti.
|
||||
|
||||
## Související
|
||||
|
||||
* [Relační pole](/l/cs/user-guide/data-model/capabilities/relation-fields) — vysvětlení typů vztahů
|
||||
* [Vytváření vlastních objektů](/l/cs/user-guide/data-model/how-tos/create-custom-objects) — jak vytvářet objekty
|
||||
* [Vytváření relačních polí](/l/cs/user-guide/data-model/how-tos/create-relation-fields) — základní nastavení vztahů
|
||||
@@ -9,7 +9,7 @@ API (rozhraní pro programování aplikací) umožňuje propojení Twenty s dal
|
||||
|
||||
## Aplikace
|
||||
|
||||
Aplikace jsou vlastní rozšíření vytvořená jako kód, která mohou definovat datové modely a logické funkce. Umožňují vývojářům vytvářet znovupoužitelné přizpůsobení, které lze nasadit do více pracovních prostorů.
|
||||
Aplikace jsou vlastní rozšíření vytvořená jako kód, která mohou definovat datové modely a bezserverové funkce. Umožňují vývojářům vytvářet znovupoužitelné přizpůsobení, které lze nasadit do více pracovních prostorů.
|
||||
|
||||
## Akce kódu
|
||||
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ K e-mailům odesílaným z pracovních postupů můžete přikládat soubory. P
|
||||
| **Potvrzení událostí** | Podrobnosti o události nebo program |
|
||||
|
||||
<Note>
|
||||
Přílohy jsou statické—všem příjemcům se posílá stejný soubor. U dynamických dokumentů (například personalizovaných nabídek) soubory vygenerujte a přiložte pomocí [Logic funkce](/l/cs/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty).
|
||||
Přílohy jsou statické—všem příjemcům se posílá stejný soubor. U dynamických dokumentů (například personalizovaných nabídek) soubory vygenerujte a přiložte pomocí [Serverless funkce](/l/cs/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty).
|
||||
</Note>
|
||||
|
||||
## Osvědčené postupy
|
||||
|
||||
@@ -256,7 +256,7 @@ Spouští vlastní JavaScript ve vašem pracovním postupu.
|
||||
* Testovat kód přímo ve kroku
|
||||
|
||||
<Note>
|
||||
Pokud potřebujete ve svém kódu použít externí klíče API, musíte je zadat přímo do těla funkce. You cannot configure API keys elsewhere and reference them in the logic function.
|
||||
Pokud potřebujete ve svém kódu použít externí klíče API, musíte je zadat přímo do těla funkce. Klíče API nelze nakonfigurovat jinde a odkazovat na ně v bezserverové funkci.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
|
||||
+9
-9
@@ -7,7 +7,7 @@ Automaticky vygenerujte nebo získejte PDF a připojte ho k záznamu v Twenty. T
|
||||
|
||||
## Přehled
|
||||
|
||||
Tento pracovní postup používá **Ruční spouštěč**, takže uživatelé mohou na požádání vygenerovat PDF pro libovolný vybraný záznam. O zpracování se stará **logická funkce**:
|
||||
Tento pracovní postup používá **Ruční spouštěč**, takže uživatelé mohou na požádání vygenerovat PDF pro libovolný vybraný záznam. O zpracování se stará **Serverless funkce**:
|
||||
|
||||
1. Stažení PDF z adresy URL (ze služby pro generování PDF)
|
||||
2. Nahrání souboru do Twenty
|
||||
@@ -17,7 +17,7 @@ Tento pracovní postup používá **Ruční spouštěč**, takže uživatelé mo
|
||||
|
||||
Než nastavíte pracovní postup:
|
||||
|
||||
1. **Vytvořte klíč API**: Přejděte do **Nastavení → API** a vytvořte nový klíč API. Tento token budete potřebovat pro logickou funkci.
|
||||
1. **Vytvořte klíč API**: Přejděte do **Nastavení → API** a vytvořte nový klíč API. Tento token budete potřebovat pro serverless funkci.
|
||||
2. **Nastavte službu pro generování PDF** (volitelné): Pokud chcete dynamicky generovat PDF (např. nabídky), použijte službu jako Carbone, PDFMonkey nebo DocuSeal k vytvoření PDF a získání adresy URL pro stažení.
|
||||
|
||||
## Nastavení krok za krokem
|
||||
@@ -32,9 +32,9 @@ Než nastavíte pracovní postup:
|
||||
S **Ručním spouštěčem** mohou uživatelé spustit tento pracovní postup pomocí tlačítka, které se zobrazí vpravo nahoře po výběru záznamu, aby vygenerovali a připojili PDF.
|
||||
</Tip>
|
||||
|
||||
### Krok 2: Přidejte logickou funkci
|
||||
### Krok 2: Přidejte serverless funkci
|
||||
|
||||
1. Přidejte akci **Code** (logická funkce)
|
||||
1. Přidejte akci **Serverless funkce**
|
||||
2. Vytvořte novou funkci pomocí kódu níže
|
||||
3. Nakonfigurujte vstupní parametry
|
||||
|
||||
@@ -45,10 +45,10 @@ Než nastavíte pracovní postup:
|
||||
| `companyId` | `{{trigger.object.id}}` |
|
||||
|
||||
<Note>
|
||||
Pokud připojujete k jinému objektu (Osoba, Příležitost apod.), přejmenujte parametr podle toho (např. `personId`, `opportunityId`) a aktualizujte logickou funkci.
|
||||
Pokud připojujete k jinému objektu (Osoba, Příležitost apod.), přejmenujte parametr podle toho (např. `personId`, `opportunityId`) a aktualizujte serverless funkci.
|
||||
</Note>
|
||||
|
||||
#### Kód logické funkce
|
||||
#### Kód serverless funkce
|
||||
|
||||
```typescript
|
||||
export const main = async (
|
||||
@@ -172,7 +172,7 @@ Aktualizujte jak parametr funkce, tak objekt `variables.data` v mutaci přílohy
|
||||
Pokud používáte službu pro generování PDF, můžete:
|
||||
|
||||
1. Nejprve proveďte akci HTTP Request pro vygenerování PDF
|
||||
2. Předejte vrácenou adresu URL PDF logické funkci jako parametr
|
||||
2. Předejte vrácenou adresu URL PDF serverless funkci jako parametr
|
||||
|
||||
```typescript
|
||||
export const main = async (
|
||||
@@ -211,7 +211,7 @@ Pro vytváření dynamických nabídek nebo faktur:
|
||||
* **DocuSeal** - Platforma pro automatizaci dokumentů
|
||||
* **Documint** - Generování dokumentů primárně přes API
|
||||
|
||||
Každá služba poskytuje API, které vrací adresu URL PDF, kterou pak můžete předat logické funkci.
|
||||
Každá služba poskytuje API, které vrací adresu URL PDF, kterou pak můžete předat serverless funkci.
|
||||
|
||||
## Řešení potíží
|
||||
|
||||
@@ -224,5 +224,5 @@ Každá služba poskytuje API, které vrací adresu URL PDF, kterou pak můžete
|
||||
## Související
|
||||
|
||||
* [Spouštěče pracovních postupů](/l/cs/user-guide/workflows/capabilities/workflow-triggers)
|
||||
* [Logické funkce](/l/cs/user-guide/workflows/capabilities/workflow-actions#code)
|
||||
* [Serverless funkce](/l/cs/user-guide/workflows/capabilities/workflow-actions#serverless-function)
|
||||
* [Vygenerujte nabídku nebo fakturu z Twenty](/l/cs/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ description: Často kladené otázky k pracovním postupům v Twenty.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Jaký je maximální čas spuštění pro akce Code?">
|
||||
Akce Code (logické funkce) mají **výchozí časový limit 5 minut** (300 sekund).
|
||||
Akce Code (serverless funkce) mají **výchozí časový limit 5 minut** (300 sekund).
|
||||
|
||||
Maximální nastavitelný časový limit je **15 minut** (900 sekund).
|
||||
|
||||
|
||||
@@ -170,13 +170,6 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
|
||||
|
||||
Dadurch wird eine Superuser-Rolle namens `postgres` mit Anmeldezugriff erstellt.
|
||||
|
||||
```bash
|
||||
Rollenname | Attribute | Mitglied von
|
||||
-----------+-------------+-----------
|
||||
postgres | Superuser | {}
|
||||
john | Superuser | {}
|
||||
```
|
||||
|
||||
**Option 2:** Wenn Sie Docker installiert haben:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -9,14 +9,18 @@ description: Twenty-Anpassungen als Code erstellen und verwalten.
|
||||
|
||||
## Was sind Apps?
|
||||
|
||||
Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. Anstatt alles über die UI zu konfigurieren, definieren Sie Ihr Datenmodell und Logikfunktionen im Code — das beschleunigt Entwicklung, Wartung und den Rollout auf mehrere Workspaces.
|
||||
Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. Anstatt alles über die UI zu konfigurieren, definieren Sie Ihr Datenmodell und serverlose Funktionen im Code — das beschleunigt Entwicklung, Wartung und Rollout auf mehrere Workspaces.
|
||||
|
||||
**Was Sie heute tun können:**
|
||||
|
||||
* Benutzerdefinierte Objekte und Felder als Code definieren (verwaltetes Datenmodell)
|
||||
* Logikfunktionen mit benutzerdefinierten Triggern erstellen
|
||||
* Serverlose Funktionen mit benutzerdefinierten Triggern erstellen
|
||||
* Dieselbe App in mehreren Workspaces bereitstellen
|
||||
|
||||
**Bald verfügbar:**
|
||||
|
||||
* Benutzerdefinierte UI-Layouts und Komponenten
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
* Node.js 24+ und Yarn 4
|
||||
@@ -45,23 +49,26 @@ yarn app:dev
|
||||
Von hier aus können Sie:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Eine neue Entität zu deiner Anwendung hinzufügen (geführt)
|
||||
yarn entity:add
|
||||
# Add a new entity to your application (guided)
|
||||
yarn app:create-entity
|
||||
|
||||
# Einen typisierten Twenty-Client und Entitätstypen für den Arbeitsbereich generieren
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# Die Funktionsprotokolle deiner Anwendung überwachen
|
||||
# Run a one‑time sync (instead of watch mode)
|
||||
yarn app:sync
|
||||
|
||||
# Watch your application's functions logs
|
||||
yarn function:logs
|
||||
|
||||
# Eine Funktion anhand ihres Namens ausführen
|
||||
# Execute a function by name
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
|
||||
# Hilfe zu Befehlen anzeigen
|
||||
yarn help
|
||||
# Display commands' help
|
||||
yarn app:help
|
||||
```
|
||||
|
||||
Siehe auch: die CLI-Referenzseiten für [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) und [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -89,61 +96,82 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Example logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
app/
|
||||
application.config.ts # Required - main application configuration
|
||||
default-function.role.ts # Default role for serverless functions
|
||||
// your entities (*.object.ts, *.function.ts, *.role.ts)
|
||||
utils/ # Optional - handler implementations & utilities
|
||||
```
|
||||
|
||||
### Konvention vor Konfiguration
|
||||
|
||||
Anwendungen verwenden einen Ansatz **Konvention vor Konfiguration**, bei dem Entitäten anhand ihrer Dateiendung erkannt werden. Dies ermöglicht eine flexible Organisation im Ordner `src/app/`:
|
||||
|
||||
| Dateiendung | Entitätstyp |
|
||||
| --------------- | ------------------------------------- |
|
||||
| `*.object.ts` | Benutzerdefinierte Objektdefinitionen |
|
||||
| `*.function.ts` | Definitionen serverloser Funktionen |
|
||||
| `*.role.ts` | Rollendefinitionen |
|
||||
|
||||
### Unterstützte Ordnerorganisationen
|
||||
|
||||
Sie können Ihre Entitäten nach einem der folgenden Muster organisieren:
|
||||
|
||||
**Traditionell (nach Typ):**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Feature-basiert:**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Flach:**
|
||||
|
||||
```text
|
||||
src/app/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
Auf hoher Ebene:
|
||||
|
||||
* **package.json**: Deklariert den App-Namen, die Version, Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie Skripte wie `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` sowie Authentifizierungsbefehle hinzu, die an die lokale `twenty`-CLI delegieren.
|
||||
* **package.json**: Deklariert App-Name, Version, Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie Skripte wie `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall` und `auth` hinzu, die an die lokale `twenty`-CLI delegieren.
|
||||
* **.gitignore**: Ignoriert übliche Artefakte wie `node_modules`, `.yarn`, `generated/` (typisierter Client), `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.
|
||||
* **eslint.config.mjs** und **tsconfig.json**: Stellen Linting und TypeScript-Konfiguration für die TypeScript-Quellen Ihrer App bereit.
|
||||
* **README.md**: Ein kurzes README im App-Root mit grundlegenden Anweisungen.
|
||||
* **public/**: Ein Ordner zum Speichern öffentlicher Assets (Bilder, Schriftarten, statische Dateien), die zusammen mit Ihrer Anwendung bereitgestellt werden. Hier abgelegte Dateien werden während der Synchronisierung hochgeladen und sind zur Laufzeit zugänglich.
|
||||
* **src/**: Der Hauptort, an dem Sie Ihre Anwendung als Code definieren
|
||||
|
||||
### Entitätserkennung
|
||||
|
||||
Das SDK erkennt Entitäten, indem es Ihre TypeScript-Dateien nach Aufrufen von **`export default define<Entity>({...})`** parst. Für jeden Entitätstyp gibt es eine entsprechende Hilfsfunktion, die aus `twenty-sdk` exportiert wird:
|
||||
|
||||
| Hilfsfunktion | Entitätstyp |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| `defineObject()` | Benutzerdefinierte Objektdefinitionen |
|
||||
| `defineLogicFunction()` | Definitionen von Logikfunktionen |
|
||||
| `defineFrontComponent()` | Definitionen von Frontend-Komponenten |
|
||||
| `defineRole()` | Rollendefinitionen |
|
||||
| `defineField()` | Felderweiterungen für bestehende Objekte |
|
||||
|
||||
<Note>
|
||||
**Dateibenennung ist flexibel.** Die Entitätserkennung ist AST-basiert — das SDK durchsucht Ihre Quelldateien nach dem Muster `export default define<Entity>({...})`. Sie können Ihre Dateien und Ordner nach Belieben organisieren. Die Gruppierung nach Entitätstyp (z. B. `logic-functions/`, `roles/`) ist lediglich eine Konvention zur Codeorganisation, keine Voraussetzung.
|
||||
</Note>
|
||||
|
||||
Beispiel für eine erkannte Entität:
|
||||
|
||||
```typescript
|
||||
// This file can be named anything and placed anywhere in src/
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '...',
|
||||
nameSingular: 'postCard',
|
||||
// ... rest of config
|
||||
});
|
||||
```
|
||||
* **src/app/**: Der Hauptort, an dem Sie Ihre Anwendung als Code definieren:
|
||||
* `application.config.ts`: Globale Konfiguration für Ihre App (Metadaten und Laufzeit-Anbindung). Siehe unten „Anwendungskonfiguration“.
|
||||
* `*.role.ts`: Rollendefinitionen, die von Ihren serverlosen Funktionen verwendet werden. Siehe unten „Standard-Funktionsrolle“.
|
||||
* `*.object.ts`: Benutzerdefinierte Objektdefinitionen.
|
||||
* `*.function.ts`: Definitionen serverloser Funktionen.
|
||||
* **src/utils/**: Optionaler Ordner für Handler-Implementierungen und Utilities.
|
||||
|
||||
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
|
||||
|
||||
* `yarn app:generate` erstellt einen `generated/`-Ordner (typisierter Twenty-Client + Workspace-Typen).
|
||||
* `yarn entity:add` fügt unter `src/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen, Front-Komponenten oder Rollen hinzu.
|
||||
* `yarn app:create-entity` fügt unter `src/app/` Entitätsdefinitionsdateien für Ihre benutzerdefinierten Objekte, Funktionen oder Rollen hinzu.
|
||||
l
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
@@ -184,18 +212,16 @@ Das twenty-sdk stellt typisierte Bausteine und Hilfsfunktionen bereit, die Sie i
|
||||
|
||||
### Hilfsfunktionen
|
||||
|
||||
Das SDK stellt Hilfsfunktionen bereit, um die Entitäten Ihrer App zu definieren. Wie in [Entitätserkennung](#entity-detection) beschrieben, müssen Sie `export default define<Entity>({...})` verwenden, damit Ihre Entitäten erkannt werden:
|
||||
Das SDK stellt vier Hilfsfunktionen mit eingebauter Validierung bereit, um Ihre App-Entitäten zu definieren:
|
||||
|
||||
| Funktion | Zweck |
|
||||
| ------------------------ | -------------------------------------------------------------- |
|
||||
| `defineApplication()` | Anwendungsmetadaten konfigurieren (erforderlich, eine pro App) |
|
||||
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
|
||||
| `defineLogicFunction()` | Logikfunktionen mit Handlern definieren |
|
||||
| `defineFrontComponent()` | Frontend-Komponenten für benutzerdefinierte UI definieren |
|
||||
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
|
||||
| `defineField()` | Bestehende Objekte mit zusätzlichen Feldern erweitern |
|
||||
| Funktion | Zweck |
|
||||
| ------------------ | ---------------------------------------------------- |
|
||||
| `defineApp()` | Anwendungsmetadaten konfigurieren |
|
||||
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
|
||||
| `defineFunction()` | Serverlose Funktionen mit Handlern definieren |
|
||||
| `defineRole()` | Rollenberechtigungen und Objektzugriff konfigurieren |
|
||||
|
||||
Diese Funktionen validieren Ihre Konfiguration zur Build-Zeit und bieten IDE-Autovervollständigung sowie Typsicherheit.
|
||||
Diese Funktionen validieren Ihre Konfiguration zur Laufzeit und bieten bessere IDE-Autovervollständigung sowie Typsicherheit.
|
||||
|
||||
### Objekte definieren
|
||||
|
||||
@@ -276,28 +302,79 @@ 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 entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
|
||||
* Sie können mit `yarn app:create-entity` 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 wie `name`, `createdAt`, `updatedAt`, `createdBy`, `position` und `deletedAt` hinzu. Sie müssen diese nicht in Ihrem `fields`-Array definieren — fügen Sie nur Ihre benutzerdefinierten Felder hinzu.
|
||||
</Note>
|
||||
|
||||
### Anwendungskonfiguration (application-config.ts)
|
||||
<Accordion title="Alternative: Dekoratorbasierte Syntax">
|
||||
Sie können Objekte auch mit TypeScript-Dekoratoren definieren. Dieser Ansatz verwendet klassenbasierte Syntax mit den Dekoratoren `@Object`, `@Field` und `@Relation`:
|
||||
|
||||
Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschreibt:
|
||||
```typescript
|
||||
import {
|
||||
type AddressField,
|
||||
Field,
|
||||
FieldType,
|
||||
type FullNameField,
|
||||
Object,
|
||||
OnDeleteAction,
|
||||
Relation,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
import { type Note } from '../../generated';
|
||||
|
||||
@Object({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post card',
|
||||
labelPlural: 'Post cards',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
export class PostCard {
|
||||
@Field({
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
})
|
||||
content: string;
|
||||
|
||||
@Relation({
|
||||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
}
|
||||
```
|
||||
|
||||
Hinweis: Der Dekorator-Ansatz erfordert `experimentalDecorators` in Ihrer TypeScript-Konfiguration.
|
||||
</Accordion>
|
||||
|
||||
### Anwendungskonfiguration (application.config.ts)
|
||||
|
||||
Jede App hat eine einzelne Datei `application.config.ts`, die Folgendes beschreibt:
|
||||
|
||||
* **Was die App ist**: Bezeichner, Anzeigename und Beschreibung.
|
||||
* **Wie ihre Funktionen ausgeführt werden**: welche Rolle sie für Berechtigungen verwenden.
|
||||
* **(Optional) Variablen**: Schlüssel–Wert-Paare, die Ihren Funktionen als Umgebungsvariablen zur Verfügung gestellt werden.
|
||||
|
||||
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
|
||||
Verwenden Sie `defineApp()`, um Ihre Anwendungskonfiguration zu definieren:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
// src/app/application.config.ts
|
||||
import { defineApp } from 'twenty-sdk';
|
||||
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
export default defineApp({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
@@ -310,7 +387,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +395,11 @@ Notizen:
|
||||
|
||||
* `universalIdentifier`-Felder sind deterministische IDs, die Sie besitzen; generieren Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
|
||||
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen (zum Beispiel ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
|
||||
* `defaultRoleUniversalIdentifier` muss mit der Rollendatei übereinstimmen (siehe unten).
|
||||
* `functionRoleUniversalIdentifier` muss mit der Rolle übereinstimmen, die Sie in Ihrer `*.role.ts`-Datei definieren (siehe unten).
|
||||
|
||||
#### 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.
|
||||
Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und Aktionen Ihres Workspaces kapseln. Das Feld `functionRoleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den serverlosen Funktionen Ihrer App verwendet wird.
|
||||
|
||||
* Der zur Laufzeit als `TWENTY_API_KEY` injizierte API-Schlüssel wird von dieser Standard-Funktionsrolle abgeleitet.
|
||||
* Der typisierte Client ist auf die dieser Rolle gewährten Berechtigungen beschränkt.
|
||||
@@ -333,14 +410,14 @@ Anwendungen können Rollen definieren, die Berechtigungen für die Objekte und A
|
||||
Wenn Sie eine neue App erzeugen, erstellt die CLI auch eine Standard-Rolldatei. Verwenden Sie `defineRole()`, um Rollen mit eingebauter Validierung zu definieren:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: false,
|
||||
@@ -353,7 +430,7 @@ export default defineRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectNameSingular: 'postCard',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -362,8 +439,8 @@ export default defineRole({
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectNameSingular: 'postCard',
|
||||
fieldName: 'content',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
@@ -372,10 +449,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
Der `universalIdentifier` dieser Rolle wird anschließend in `application-config.ts` als `defaultRoleUniversalIdentifier` referenziert. Anders ausgedrückt:
|
||||
Der `universalIdentifier` dieser Rolle wird anschließend in `application.config.ts` als `functionRoleUniversalIdentifier` referenziert. Anders ausgedrückt:
|
||||
|
||||
* **\*.role.ts** definiert, was die Standard-Funktionsrolle darf.
|
||||
* **application-config.ts** verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben.
|
||||
* **application.config.ts** verweist auf diese Rolle, sodass Ihre Funktionen deren Berechtigungen erben.
|
||||
|
||||
Notizen:
|
||||
|
||||
@@ -384,17 +461,22 @@ Notizen:
|
||||
* `permissionFlags` steuern den Zugriff auf Funktionen auf Plattformebene. Halten Sie sie minimal; fügen Sie nur hinzu, was Sie benötigen.
|
||||
* Ein funktionierendes Beispiel finden Sie in der 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).
|
||||
|
||||
### Konfiguration von Logikfunktionen und Einstiegspunkt
|
||||
### Konfiguration serverloser Funktionen und Einstiegspunkt
|
||||
|
||||
Jede Funktionsdatei verwendet `defineLogicFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren.
|
||||
Jede Funktionsdatei verwendet `defineFunction()`, um eine Konfiguration mit einem Handler und optionalen Triggern zu exportieren. Verwenden Sie die Dateiendung `*.function.ts` für die automatische Erkennung.
|
||||
|
||||
```typescript
|
||||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
// 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';
|
||||
import Twenty, { type Person } from '../../generated';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const handler = async (
|
||||
params:
|
||||
| RoutePayload
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||||
| CronPayload,
|
||||
) => {
|
||||
const client = new Twenty(); // generated typed client
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
@@ -410,7 +492,7 @@ const handler = async (params: RoutePayload) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
@@ -425,18 +507,18 @@ export default defineLogicFunction({
|
||||
isAuthRequired: false,
|
||||
},
|
||||
// Cron trigger (CRON pattern)
|
||||
// {
|
||||
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
// type: 'cron',
|
||||
// pattern: '0 0 1 1 *',
|
||||
// },
|
||||
{
|
||||
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'],
|
||||
// },
|
||||
{
|
||||
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.updated',
|
||||
updatedFields: ['name'],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -483,10 +565,10 @@ Notizen:
|
||||
**So migrieren Sie bestehende Funktionen:** Aktualisieren Sie Ihren Handler, sodass er nicht mehr direkt aus dem params-Objekt destrukturiert, sondern aus `event.body`, `event.queryStringParameters` oder `event.pathParameters`.
|
||||
</Warning>
|
||||
|
||||
Wenn ein Routen-Trigger Ihre Logikfunktion aufruft, erhält sie ein `RoutePayload`-Objekt, das dem AWS HTTP API v2-Format entspricht. Importieren Sie den Typ aus `twenty-sdk`:
|
||||
Wenn ein Routen-Trigger Ihre Funktion aufruft, erhält sie ein `RoutePayload`-Objekt, das dem AWS-HTTP-API-v2-Format entspricht. Importieren Sie den Typ aus `twenty-sdk`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -513,10 +595,10 @@ Der Typ `RoutePayload` hat die folgende Struktur:
|
||||
|
||||
### Weiterleiten von HTTP-Headern
|
||||
|
||||
Standardmäßig werden HTTP-Header von eingehenden Anfragen aus Sicherheitsgründen nicht an Ihre Logikfunktion weitergegeben. Um auf bestimmte Header zuzugreifen, listen Sie diese explizit im Array `forwardedRequestHeaders` auf:
|
||||
Standardmäßig werden HTTP-Header von eingehenden Anfragen aus Sicherheitsgründen nicht an Ihre serverlose Funktion weitergegeben. Um auf bestimmte Header zuzugreifen, listen Sie diese explizit im Array `forwardedRequestHeaders` auf:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,60 +633,23 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Sie können neue Funktionen auf zwei Arten erstellen:
|
||||
|
||||
* **Generiert**: Führen Sie `yarn entity: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.
|
||||
|
||||
### Frontend-Komponenten
|
||||
|
||||
Frontend-Komponenten ermöglichen es Ihnen, benutzerdefinierte React-Komponenten zu erstellen, die innerhalb der Twenty-UI gerendert werden. Verwenden Sie `defineFrontComponent()`, um Komponenten mit eingebauter Validierung zu definieren:
|
||||
|
||||
```typescript
|
||||
// src/my-widget.front-component.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const MyWidget = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>My Custom Widget</h1>
|
||||
<p>This is a custom front component for Twenty.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'my-widget',
|
||||
description: 'A custom widget component',
|
||||
component: MyWidget,
|
||||
});
|
||||
```
|
||||
|
||||
Hauptpunkte:
|
||||
|
||||
* Frontend-Komponenten sind React-Komponenten, die in isolierten Kontexten innerhalb von Twenty gerendert werden.
|
||||
* Verwenden Sie die Dateiendung `*.front-component.tsx` für die automatische Erkennung.
|
||||
* Das Feld `component` verweist auf Ihre React-Komponente.
|
||||
* Komponenten werden während `yarn app:dev` automatisch gebaut und synchronisiert.
|
||||
|
||||
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
|
||||
|
||||
* **Generiert**: Führen Sie `yarn entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
|
||||
* **Manuell**: Erstellen Sie eine neue `*.front-component.tsx`-Datei und verwenden Sie `defineFrontComponent()`.
|
||||
* **Generiert**: Führen Sie `yarn app:create-entity` aus und wählen Sie die Option zum Hinzufügen einer neuen Funktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
|
||||
* **Manuell**: Erstellen Sie eine neue `*.function.ts`-Datei und verwenden Sie `defineFunction()` nach demselben Muster.
|
||||
|
||||
### Generierter typisierter Client
|
||||
|
||||
Führen Sie yarn app:generate aus, um einen lokalen typisierten Client in generated/ basierend auf Ihrem Workspace-Schema zu erstellen. Verwenden Sie ihn in Ihren Funktionen:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import Twenty from './generated';
|
||||
|
||||
const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Der Client wird durch `yarn app:generate` erneut generiert. Führen Sie ihn nach Änderungen an Ihren Objekten oder beim Onboarding in einen neuen Workspace erneut aus.
|
||||
Der Client wird durch `yarn app:generate` erneut generiert. Führen Sie ihn nach Änderungen an Ihren Objekten und nach `yarn app:sync` bzw. beim Onboarding in einen neuen Workspace erneut aus.
|
||||
|
||||
#### Laufzeit-Anmeldedaten in Logikfunktionen
|
||||
#### Laufzeit-Anmeldedaten in serverlosen Funktionen
|
||||
|
||||
Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführung Ihres Codes Anmeldedaten als Umgebungsvariablen:
|
||||
|
||||
@@ -614,12 +659,12 @@ Wenn Ihre Funktion auf Twenty läuft, injiziert die Plattform vor der Ausführun
|
||||
Notizen:
|
||||
|
||||
* 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.
|
||||
* Die Berechtigungen des API-Schlüssels werden durch die Rolle bestimmt, auf die in Ihrer `application.config.ts` über `functionRoleUniversalIdentifier` verwiesen wird. Dies ist die Standardrolle, die von den serverlosen Funktionen 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 `functionRoleUniversalIdentifier` auf den universellen Bezeichner dieser Rolle.
|
||||
|
||||
### Hello-World-Beispiel
|
||||
|
||||
Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Komponenten und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Ein minimales End-to-End-Beispiel, das Objekte, Funktionen und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Manuelle Einrichtung (ohne Scaffolder)
|
||||
|
||||
@@ -634,29 +679,25 @@ Fügen Sie dann Skripte wie diese hinzu:
|
||||
```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"
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Jetzt können Sie dieselben Befehle über Yarn ausführen, z. B. `yarn app:dev`, `yarn app:generate` usw.
|
||||
Jetzt können Sie dieselben Befehle über Yarn ausführen, z. B. `yarn app:dev`, `yarn app:sync` usw.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
* Authentifizierungsfehler: Führen Sie `yarn auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
|
||||
* Verbindung zum Server nicht möglich: Überprüfen Sie die API-URL und dass der Twenty-Server erreichbar ist.
|
||||
* Typen oder Client fehlen/veraltet: Führen Sie `yarn app:generate` aus.
|
||||
* Typen oder Client fehlen/veraltet: Führen Sie `yarn app:generate` und anschließend `yarn app:dev` aus.
|
||||
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
|
||||
|
||||
Discord-Hilfekanal: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -292,19 +292,19 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
|
||||
</Warning>
|
||||
|
||||
## Logikfunktionen
|
||||
## Serverlose Funktionen
|
||||
|
||||
Twenty unterstützt Logikfunktionen für Workflows und benutzerdefinierte Logik. Die Ausführungsumgebung wird über die Umgebungsvariable `SERVERLESS_TYPE` konfiguriert.
|
||||
Twenty unterstützt serverlose Funktionen für Workflows und benutzerdefinierte Logik. Die Ausführungsumgebung wird über die Umgebungsvariable `SERVERLESS_TYPE` konfiguriert.
|
||||
|
||||
<Warning>
|
||||
**Sicherheitshinweis:** Der lokale Treiber (`SERVERLESS_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktivbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, empfehlen wir nachdrücklich, `SERVERLESS_TYPE=LAMBDA` oder `SERVERLESS_TYPE=DISABLED` zu verwenden.
|
||||
**Sicherheitshinweis:** Der lokale serverlose Treiber (`SERVERLESS_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktivbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, empfehlen wir nachdrücklich, `SERVERLESS_TYPE=LAMBDA` oder `SERVERLESS_TYPE=DISABLED` zu verwenden.
|
||||
</Warning>
|
||||
|
||||
### Verfügbare Treiber
|
||||
|
||||
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
|
||||
| ----------- | -------------------------- | -------------------------------------------------- | ---------------------------------- |
|
||||
| Deaktiviert | `SERVERLESS_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
|
||||
| Deaktiviert | `SERVERLESS_TYPE=DISABLED` | Serverlose Funktionen vollständig deaktivieren | N/A |
|
||||
| Lokal | `SERVERLESS_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
|
||||
|
||||
@@ -326,12 +326,12 @@ SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**Zum Deaktivieren von Logikfunktionen:**
|
||||
**Zum Deaktivieren serverloser Funktionen:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
<Note>
|
||||
Bei Verwendung von `SERVERLESS_TYPE=DISABLED` führt jeder Versuch, eine Logikfunktion auszuführen, zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne Unterstützung für Logikfunktionen betreiben möchten.
|
||||
Bei Verwendung von `SERVERLESS_TYPE=DISABLED` führt jeder Versuch, eine serverlose Funktion auszuführen, zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne Unterstützung für serverlose Funktionen betreiben möchten.
|
||||
</Note>
|
||||
|
||||
@@ -59,4 +59,4 @@ So wird sichergestellt, dass KI-Agenten Ihre Richtlinien zur Daten-Governance re
|
||||
Wir werden diesen Abschnitt aktualisieren, sobald KI-Funktionen verfügbar werden. In der Zwischenzeit:
|
||||
|
||||
* Folgen Sie unserem [GitHub](https://github.com/twentyhq/twenty) für Entwicklungsupdates
|
||||
* Treten Sie unserem [Discord](https://discord.gg/UfGNZJfAG6) bei, um Feedback und Funktionswünsche zu teilen
|
||||
* Treten Sie unserem [Discord](https://discord.gg/twenty) bei, um Feedback und Funktionswünsche zu teilen
|
||||
|
||||
@@ -39,15 +39,11 @@ Viele Datensätze in Objekt A können mit vielen Datensätzen in Objekt B verkn
|
||||
|
||||
**Beispiel:** Viele Personen können mit vielen Projekten verknüpft werden, und umgekehrt.
|
||||
|
||||
Viele-zu-viele-Beziehungen verwenden ein **Verknüpfungsobjekt**-Muster: ein Zwischenobjekt, das beide Seiten verbindet. Mit der Funktion für Verknüpfungsbeziehungen zeigt Twenty die endgültig verknüpften Datensätze direkt an und blendet das Zwischenobjekt in der Benutzeroberfläche aus.
|
||||
|
||||
<img src="/images/user-guide/fields/junction-relation-diagram.png" style={{width:'100%'}} />
|
||||
|
||||
<Warning>
|
||||
**Lab-Funktion**: Verknüpfungsbeziehungen müssen vor der Verwendung unter **Einstellungen → Updates → Lab** aktiviert werden.
|
||||
</Warning>
|
||||
**Viele-zu-Viele-Beziehungen werden noch nicht unterstützt.**
|
||||
|
||||
Siehe [Viele-zu-Viele-Beziehungen erstellen](/l/de/user-guide/data-model/how-tos/create-many-to-many-relations) für eine vollständige Schritt-für-Schritt-Anleitung.
|
||||
Dieser Beziehungstyp ist für H1 2026 geplant. Als Workaround erstellen Sie ein zwischengeschaltetes "Junction"-Objekt (z. B. "Projektzuweisungen"), das Viele-zu-Eins-Beziehungen zu beiden Objekten hat.
|
||||
</Warning>
|
||||
|
||||
## Beziehungsfeld erstellen
|
||||
|
||||
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
---
|
||||
title: Viele-zu-Viele-Beziehungen erstellen
|
||||
description: Verbinden Sie Datensätze, bei denen auf beiden Seiten viele Elemente mithilfe von Verknüpfungsobjekten miteinander verknüpft werden können.
|
||||
---
|
||||
|
||||
Viele-zu-Viele-Beziehungen ermöglichen es Ihnen, auf beiden Seiten mehrere Datensätze zu verknüpfen. Beispiel: Viele Personen können an vielen Projekten arbeiten, und jedes Projekt kann viele Personen haben.
|
||||
|
||||
<Warning>
|
||||
**Lab-Funktion**: Verknüpfungsbeziehungen befinden sich derzeit im Lab. Aktivieren Sie sie unter **Einstellungen → Updates → Lab**, bevor Sie dieser Anleitung folgen.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Für diese Funktion muss außerdem der **Erweiterte Modus** aktiviert sein (Schalter unten rechts in den Einstellungen).
|
||||
</Note>
|
||||
|
||||
## Wann Viele-zu-Viele-Beziehungen verwenden
|
||||
|
||||
Verwenden Sie Viele-zu-Viele-Beziehungen, wenn beide Seiten einer Beziehung mehrere Verknüpfungen haben können:
|
||||
|
||||
| Beziehung | Beispiel |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| Personen ↔ Projekte | Eine Person arbeitet an mehreren Projekten; ein Projekt hat mehrere Teammitglieder |
|
||||
| Unternehmen ↔ Tags | Ein Unternehmen kann mehrere Tags haben; ein Tag kann für mehrere Unternehmen gelten |
|
||||
| Produkte ↔ Bestellungen | Ein Produkt kann in mehreren Bestellungen enthalten sein; eine Bestellung enthält mehrere Produkte |
|
||||
|
||||
## Wie es funktioniert
|
||||
|
||||
Twenty verwendet für Viele-zu-Viele-Beziehungen ein Muster mit **Verknüpfungsobjekt**. Ein Verknüpfungsobjekt sitzt zwischen zwei Objekten und speichert die Verknüpfungen:
|
||||
|
||||
```
|
||||
People ←→ Project Assignments ←→ Projects
|
||||
```
|
||||
|
||||
Das Objekt **Projektzuweisungen** (Verknüpfung) hat:
|
||||
|
||||
* Eine Beziehung zu Personen (Viele-zu-Eins)
|
||||
* Eine Beziehung zu Projekten (Viele-zu-Eins)
|
||||
|
||||
Wenn Sie den Schalter für die Verknüpfungsbeziehung aktivieren, zeigt Twenty verknüpfte Datensätze direkt an, anstatt die zwischengeschalteten Verknüpfungsdatensätze anzuzeigen.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
1. **Verknüpfungsbeziehungen im Lab aktivieren**: Gehen Sie zu **Einstellungen → Updates → Lab** und aktivieren Sie **Verknüpfungsbeziehungen**
|
||||
2. **Erweiterten Modus aktivieren**: Aktivieren Sie den **Erweiterten Modus** unten rechts in der Seitenleiste der Einstellungen
|
||||
3. Planen Sie Ihr Datenmodell:
|
||||
* Welche zwei Objekte verbinden Sie?
|
||||
* Wie soll das Verknüpfungsobjekt heißen?
|
||||
|
||||
## Schritt 1: Verknüpfungsobjekt erstellen
|
||||
|
||||
Erstellen Sie zunächst das Zwischenobjekt, das die Verknüpfungen speichert.
|
||||
|
||||
1. Gehen Sie zu **Einstellungen → Datenmodell**
|
||||
2. Klicken Sie auf **+ Neues Objekt**
|
||||
3. Benennen Sie es aussagekräftig (z. B. "Projektzuweisung", "Teammitglied", "Produktbestellung")
|
||||
4. Klicken Sie auf **Speichern**
|
||||
|
||||
<Tip>
|
||||
**Namenskonvention**: Verwenden Sie einen Namen, der die Beziehung beschreibt, z. B. "Projektzuweisung" oder "Teammitgliedschaft". Das macht das Datenmodell leichter verständlich.
|
||||
</Tip>
|
||||
|
||||
## Schritt 2: Beziehungen vom Verknüpfungsobjekt erstellen
|
||||
|
||||
Fügen Sie vom Verknüpfungsobjekt aus Beziehungsfelder zu beiden Objekten hinzu, die Sie verbinden möchten.
|
||||
|
||||
### Erste Beziehung (Verknüpfung → Objekt A)
|
||||
|
||||
1. Wählen Sie Ihr Verknüpfungsobjekt unter **Einstellungen → Datenmodell** aus
|
||||
2. Klicken Sie auf **+ Feld hinzufügen**
|
||||
3. Wählen Sie **Relation** als Feldtyp
|
||||
4. Wählen Sie das erste Objekt aus (z. B. "Personen")
|
||||
5. Legen Sie den Beziehungstyp auf **Viele-zu-Eins** fest (viele Zuweisungen können mit einer Person verknüpft werden)
|
||||
6. Benennen Sie die Felder:
|
||||
* Feld auf der Verknüpfung: z. B. "Person"
|
||||
* Feld bei Personen: z. B. "Projektzuweisungen"
|
||||
7. Klicken Sie auf **Speichern**
|
||||
|
||||
### Zweite Beziehung (Verknüpfung → Objekt B)
|
||||
|
||||
1. Bleiben Sie im Verknüpfungsobjekt und klicken Sie auf **+ Feld hinzufügen**
|
||||
2. Wählen Sie **Relation** als Feldtyp
|
||||
3. Wählen Sie das zweite Objekt aus (z. B. "Projekte")
|
||||
4. Legen Sie den Beziehungstyp auf **Viele-zu-Eins** fest
|
||||
5. Benennen Sie die Felder:
|
||||
* Feld auf der Verknüpfung: z. B. "Projekt"
|
||||
* Feld bei Projekten: z. B. "Teammitglieder"
|
||||
6. Klicken Sie auf **Speichern**
|
||||
|
||||
## Schritt 3: Anzeige der Verknüpfungsbeziehung konfigurieren
|
||||
|
||||
Konfigurieren Sie nun die Quellobjekte so, dass verknüpfte Datensätze direkt angezeigt werden, wobei das zwischengeschaltete Verknüpfungsobjekt übersprungen wird.
|
||||
|
||||
1. Gehen Sie zu **Einstellungen → Datenmodell**
|
||||
2. Wählen Sie das erste Objekt aus (z. B. "Personen")
|
||||
3. Suchen Sie das Beziehungsfeld, das auf das Verknüpfungsobjekt zeigt (z. B. "Projektzuweisungen")
|
||||
4. Klicken Sie, um das Feld zu bearbeiten
|
||||
5. Aktivieren Sie **"Dies ist eine Beziehung zu einem Verknüpfungsobjekt"**
|
||||
6. Wählen Sie die **Zielbeziehung** aus (z. B. "Projekt" — das Feld an der Verknüpfung, das auf die andere Seite zeigt)
|
||||
7. Klicken Sie auf **Speichern**
|
||||
|
||||
{/* TODO: Add image
|
||||
<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/>
|
||||
*/}
|
||||
|
||||
Wiederholen Sie dies für das andere Objekt:
|
||||
|
||||
1. Wählen Sie "Projekte" im Datenmodell aus
|
||||
2. Bearbeiten Sie das Beziehungsfeld "Teammitglieder"
|
||||
3. Aktivieren Sie den Verknüpfungsschalter
|
||||
4. Wählen Sie "Person" als Zielbeziehung aus
|
||||
5. Speichern
|
||||
|
||||
## Ergebnis
|
||||
|
||||
Nach der Konfiguration:
|
||||
|
||||
* In einem **Person**-Datensatz zeigt das Feld "Projektzuweisungen" **Projekte** direkt an (keine Zuweisungsdatensätze)
|
||||
* In einem **Projekt**-Datensatz zeigt das Feld "Teammitglieder" **Personen** direkt an
|
||||
|
||||
Das Verknüpfungsobjekt existiert weiterhin und speichert die Verknüpfungen, aber die UI präsentiert eine übersichtlichere Viele-zu-Viele-Ansicht.
|
||||
|
||||
## Beispiel: Personen ↔ Projekte
|
||||
|
||||
Hier ist eine vollständige Schritt-für-Schritt-Anleitung:
|
||||
|
||||
### Verknüpfungsobjekt erstellen
|
||||
|
||||
* Name: **Projektzuweisung**
|
||||
* Beschreibung: "Verknüpft Personen mit den Projekten, an denen sie arbeiten"
|
||||
|
||||
### Beziehungen hinzufügen
|
||||
|
||||
1. **Projektzuweisung → Personen**
|
||||
* Typ: Viele-zu-Eins
|
||||
* Feld bei Zuweisung: "Person"
|
||||
* Feld bei Personen: "Projektzuweisungen"
|
||||
|
||||
2. **Projektzuweisung → Projekte**
|
||||
* Typ: Viele-zu-Eins
|
||||
* Feld bei Zuweisung: "Projekt"
|
||||
* Feld bei Projekten: "Teammitglieder"
|
||||
|
||||
### Verknüpfungsanzeige konfigurieren
|
||||
|
||||
1. Am Objekt **Personen**:
|
||||
* Feld "Projektzuweisungen" bearbeiten
|
||||
* Verknüpfungsschalter aktivieren
|
||||
* Ziel: "Projekt"
|
||||
|
||||
2. Am Objekt **Projekte**:
|
||||
* Feld "Teammitglieder" bearbeiten
|
||||
* Verknüpfungsschalter aktivieren
|
||||
* Ziel: "Person"
|
||||
|
||||
### Verwendung
|
||||
|
||||
* Öffnen Sie einen Person-Datensatz → Sehen Sie deren Projekte direkt
|
||||
* Öffnen Sie einen Projekt-Datensatz → Sehen Sie Teammitglieder direkt
|
||||
* Erstellen Sie neue Verknüpfungen von beiden Seiten
|
||||
|
||||
## Zusätzliche Daten zu Verknüpfungen hinzufügen
|
||||
|
||||
Da das Verknüpfungsobjekt ein echtes Objekt ist, können Sie benutzerdefinierte Felder hinzufügen, um Informationen über die Beziehung zu speichern:
|
||||
|
||||
* **Rolle**: "Entwickler", "Designer", "Manager"
|
||||
* **Startdatum**: Wann sie dem Projekt beigetreten sind
|
||||
* **Zugeordnete Stunden**: Wöchentliche Stunden für dieses Projekt
|
||||
|
||||
Um auf diese Daten zuzugreifen, navigieren Sie direkt zum Verknüpfungsobjekt oder greifen Sie per API-Abfrage darauf zu.
|
||||
|
||||
## Einschränkungen
|
||||
|
||||
* **CSV-Import/-Export**: Das direkte Importieren von Viele-zu-Viele-Beziehungen wird nicht unterstützt. Importieren Sie stattdessen Datensätze in das Verknüpfungsobjekt.
|
||||
* **Filter**: Das Filtern nach Viele-zu-Viele-Beziehungen bietet möglicherweise nur begrenzte Optionen.
|
||||
|
||||
## Verwandt
|
||||
|
||||
* [Beziehungsfelder](/l/de/user-guide/data-model/capabilities/relation-fields) — Beziehungstypen erklärt
|
||||
* [Benutzerdefinierte Objekte erstellen](/l/de/user-guide/data-model/how-tos/create-custom-objects) — so erstellen Sie Objekte
|
||||
* [Beziehungsfelder erstellen](/l/de/user-guide/data-model/how-tos/create-relation-fields) — grundlegende Einrichtung von Beziehungen
|
||||
@@ -9,7 +9,7 @@ API (Application Programming Interface) ermöglicht es Ihnen, Twenty mit anderen
|
||||
|
||||
## Apps
|
||||
|
||||
Apps sind benutzerdefinierte Erweiterungen, die als Code erstellt werden und Datenmodelle sowie Logikfunktionen definieren können. Damit können Entwickler wiederverwendbare Anpassungen erstellen, die in mehreren Workspaces bereitgestellt werden können.
|
||||
Apps sind benutzerdefinierte Erweiterungen, die als Code erstellt werden und Datenmodelle sowie serverlose Funktionen definieren können. Damit können Entwickler wiederverwendbare Anpassungen erstellen, die in mehreren Workspaces bereitgestellt werden können.
|
||||
|
||||
## Code-Aktionen
|
||||
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ Sie können Dateien an E-Mails anhängen, die von Workflows gesendet werden. Der
|
||||
| **Veranstaltungsbestätigungen** | Veranstaltungsdetails oder Agenda |
|
||||
|
||||
<Note>
|
||||
Anhänge sind statisch—dieselbe Datei wird an alle Empfänger gesendet. For dynamic documents (like personalized quotes), generate and attach files using a [Logic Function](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty).
|
||||
Anhänge sind statisch—dieselbe Datei wird an alle Empfänger gesendet. Für dynamische Dokumente (z. B. personalisierte Angebote) erstellen und hängen Sie Dateien mithilfe einer [Serverless-Funktion](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty) an.
|
||||
</Note>
|
||||
|
||||
## Beste Praktiken
|
||||
|
||||
@@ -256,7 +256,7 @@ Führt benutzerdefiniertes JavaScript in Ihrem Workflow aus.
|
||||
* Code direkt im Schritt testen
|
||||
|
||||
<Note>
|
||||
If you need to use external API keys in your code, you must input them directly in the function body. Sie können API-Schlüssel nicht an anderer Stelle konfigurieren und sie dann in der Logikfunktion referenzieren.
|
||||
If you need to use external API keys in your code, you must input them directly in the function body. You cannot configure API keys elsewhere and reference them in the serverless function.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
|
||||
+9
-9
@@ -7,7 +7,7 @@ Automatisch ein PDF generieren oder abrufen und an einen Datensatz in Twenty anh
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieser Workflow verwendet einen **Manuellen Auslöser**, damit Benutzer bei Bedarf für jeden ausgewählten Datensatz ein PDF generieren können. Eine **Logikfunktion** übernimmt:
|
||||
Dieser Workflow verwendet einen **Manuellen Auslöser**, damit Benutzer bei Bedarf für jeden ausgewählten Datensatz ein PDF generieren können. Eine **Serverlose Funktion** übernimmt:
|
||||
|
||||
1. Das Herunterladen des PDFs von einer URL (von einem PDF-Generierungsdienst)
|
||||
2. Das Hochladen der Datei in Twenty
|
||||
@@ -17,7 +17,7 @@ Dieser Workflow verwendet einen **Manuellen Auslöser**, damit Benutzer bei Beda
|
||||
|
||||
Bevor Sie den Workflow einrichten:
|
||||
|
||||
1. **API-Schlüssel erstellen**: Gehen Sie zu **Einstellungen → APIs** und erstellen Sie einen neuen API-Schlüssel. Sie benötigen dieses Token für die Logikfunktion.
|
||||
1. **API-Schlüssel erstellen**: Gehen Sie zu **Einstellungen → APIs** und erstellen Sie einen neuen API-Schlüssel. Sie benötigen dieses Token für die serverlose Funktion.
|
||||
2. **Richten Sie einen PDF-Generierungsdienst ein** (optional): Wenn Sie PDFs dynamisch generieren möchten (z. B. Angebote), verwenden Sie einen Dienst wie Carbone, PDFMonkey oder DocuSeal, um das PDF zu erstellen und eine Download-URL zu erhalten.
|
||||
|
||||
## Schritt-für-Schritt-Einrichtung
|
||||
@@ -32,9 +32,9 @@ Bevor Sie den Workflow einrichten:
|
||||
Mit einem manuellen Auslöser können Benutzer diesen Workflow über eine Schaltfläche ausführen, die oben rechts erscheint, sobald ein Datensatz ausgewählt ist, um ein PDF zu generieren und anzuhängen.
|
||||
</Tip>
|
||||
|
||||
### Schritt 2: Logikfunktion hinzufügen
|
||||
### Schritt 2: Serverlose Funktion hinzufügen
|
||||
|
||||
1. Fügen Sie eine **Code**-Aktion (Logikfunktion) hinzu
|
||||
1. Fügen Sie eine **Serverlose Funktion**-Aktion hinzu
|
||||
2. Erstellen Sie eine neue Funktion mit dem folgenden Code
|
||||
3. Konfigurieren Sie die Eingabeparameter
|
||||
|
||||
@@ -45,10 +45,10 @@ Bevor Sie den Workflow einrichten:
|
||||
| `companyId` | `{{trigger.object.id}}` |
|
||||
|
||||
<Note>
|
||||
Wenn Sie an ein anderes Objekt anhängen (Person, Opportunity usw.), benennen Sie den Parameter entsprechend um (z. B. `personId`, `opportunityId`) und aktualisieren Sie die Logikfunktion.
|
||||
Wenn Sie an ein anderes Objekt anhängen (Person, Opportunity usw.), benennen Sie den Parameter entsprechend um (z. B. `personId`, `opportunityId`) und aktualisieren Sie die serverlose Funktion.
|
||||
</Note>
|
||||
|
||||
#### Code der Logikfunktion
|
||||
#### Code der serverlosen Funktion
|
||||
|
||||
```typescript
|
||||
export const main = async (
|
||||
@@ -172,7 +172,7 @@ Aktualisieren Sie sowohl den Funktionsparameter als auch das Objekt `variables.d
|
||||
Wenn Sie einen PDF-Generierungsdienst verwenden, können Sie:
|
||||
|
||||
1. Führen Sie zuerst eine HTTP-Request-Aktion aus, um das PDF zu generieren
|
||||
2. Übergeben Sie die zurückgegebene PDF-URL als Parameter an die Logikfunktion
|
||||
2. Übergeben Sie die zurückgegebene PDF-URL als Parameter an die serverlose Funktion
|
||||
|
||||
```typescript
|
||||
export const main = async (
|
||||
@@ -211,7 +211,7 @@ Zum Erstellen dynamischer Angebote oder Rechnungen:
|
||||
* **DocuSeal** – Plattform für Dokumentenautomatisierung
|
||||
* **Documint** – API-first-Dokumentenerstellung
|
||||
|
||||
Jeder Dienst stellt eine API bereit, die eine PDF-URL zurückgibt, die Sie anschließend an die Logikfunktion übergeben können.
|
||||
Jeder Dienst stellt eine API bereit, die eine PDF-URL zurückgibt, die Sie anschließend an die serverlose Funktion übergeben können.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
@@ -224,5 +224,5 @@ Jeder Dienst stellt eine API bereit, die eine PDF-URL zurückgibt, die Sie ansch
|
||||
## Verwandt
|
||||
|
||||
* [Workflow-Trigger](/l/de/user-guide/workflows/capabilities/workflow-triggers)
|
||||
* [Logikfunktionen](/l/de/user-guide/workflows/capabilities/workflow-actions#code)
|
||||
* [Serverlose Funktionen](/l/de/user-guide/workflows/capabilities/workflow-actions#serverless-function)
|
||||
* [Ein Angebot oder eine Rechnung aus Twenty generieren](/l/de/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty)
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ description: Häufig gestellte Fragen zu Workflows in Twenty.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Wie lang ist die maximale Ausführungszeit für Code-Aktionen?">
|
||||
Code-Aktionen (Logikfunktionen) haben ein **Standard-Timeout von 5 Minuten** (300 Sekunden).
|
||||
Code-Aktionen (serverlose Funktionen) haben ein **Standard-Timeout von 5 Minuten** (300 Sekunden).
|
||||
|
||||
Das maximal konfigurierbare Timeout beträgt **15 Minuten** (900 Sekunden).
|
||||
|
||||
|
||||
+11
-11
@@ -1,22 +1,22 @@
|
||||
---
|
||||
title: Mejores prácticas
|
||||
title: Best Practices
|
||||
---
|
||||
|
||||
Este documento describe las mejores prácticas que debe seguir al trabajar en el backend.
|
||||
This document outlines the best practices you should follow when working on the backend.
|
||||
|
||||
## Siga un enfoque modular
|
||||
## Follow a modular approach
|
||||
|
||||
El backend sigue un enfoque modular, que es un principio fundamental al trabajar con NestJS. Asegúrese de descomponer su código en módulos reutilizables para mantener una base de código limpia y organizada.
|
||||
Cada módulo debe encapsular una característica o funcionalidad particular y tener un alcance bien definido. Este enfoque modular permite una clara separación de responsabilidades y elimina complejidades innecesarias.
|
||||
The backend follows a modular approach, which is a fundamental principle when working with NestJS. Make sure you break down your code into reusable modules to maintain a clean and organized codebase.
|
||||
Each module should encapsulate a particular feature or functionality and have a well-defined scope. This modular approach enables clear separation of concerns and removes unnecessary complexities.
|
||||
|
||||
## Exponer servicios para usar en módulos
|
||||
## Expose services to use in modules
|
||||
|
||||
Siempre cree servicios que tengan una responsabilidad clara y única, lo que mejora la legibilidad y mantenibilidad del código. Nombre los servicios de manera descriptiva y consistente.
|
||||
Always create services that have a clear and single responsibility, which enhances code readability and maintainability. Name the services descriptively and consistently.
|
||||
|
||||
También debe exponer servicios que desee usar en otros módulos. Exponer servicios a otros módulos es posible a través del poderoso sistema de inyección de dependencias de NestJS, y promueve un acoplamiento débil entre los componentes.
|
||||
You should also expose services that you want to use in other modules. Exposing services to other modules is possible through NestJS's powerful dependency injection system, and promotes loose coupling between components.
|
||||
|
||||
## Evitar usar el tipo `any`
|
||||
## Avoid using `any` type
|
||||
|
||||
Cuando declara una variable como `any`, el verificador de tipos de TypeScript no realiza ninguna comprobación de tipos, lo que hace posible asignar cualquier tipo de valores a la variable. TypeScript utiliza la inferencia de tipos para determinar el tipo de la variable a partir del valor. Al declararlo como `any`, TypeScript ya no puede inferir el tipo. Esto dificulta la captura de errores relacionados con el tipo durante el desarrollo, lo que lleva a errores en tiempo de ejecución y hace que el código sea menos mantenible, menos fiable y más difícil de entender para otros.
|
||||
When you declare a variable as `any`, TypeScript's type checker doesn't perform any type checking, making it possible to assign any type of values to the variable. TypeScript uses type inference to determine the type of variable based on the value. By declaring it as `any`, TypeScript can no longer infer the type. This makes it hard to catch type-related errors during development, leading to runtime errors and makes the code less maintainable, less reliable, and harder to understand for others.
|
||||
|
||||
Por eso todo debe tener un tipo. Entonces, si crea un nuevo objeto con un nombre y apellido, debe crear una interfaz o tipo que contenga un nombre y apellido y defina la forma del objeto que está manipulando.
|
||||
This is why everything should have a type. So if you create a new object with a first name and last name, you should create an interface or type that contains a first name and last name that defines the shape of the object you are manipulating.
|
||||
|
||||
+15
-15
@@ -1,39 +1,39 @@
|
||||
---
|
||||
title: Objetos personalizados
|
||||
title: Custom Objects
|
||||
---
|
||||
|
||||
Los objetos son estructuras que te permiten almacenar datos (registros, atributos y valores) específicos de una organización. Twenty proporciona tanto objetos estándar como personalizados.
|
||||
Objects are structures that allow you to store data (records, attributes, and values) specific to an organization. Twenty provides both standard and custom objects.
|
||||
|
||||
Los objetos estándar son objetos incorporados con un conjunto de atributos disponibles para todos los usuarios. Ejemplos de objetos estándar en Twenty incluyen Empresa y Persona. Los objetos estándar tienen campos estándar que también están disponibles para todos los usuarios de Twenty, como Company.displayName.
|
||||
Standard objects are in-built objects with a set of attributes available for all users. Examples of standard objects in Twenty include Company and Person. Standard objects have standard fields that are also available for all Twenty users, like Company.displayName.
|
||||
|
||||
Los objetos personalizados son objetos que puedes crear para almacenar información que es única para tu organización. No están incorporados; los miembros de tu espacio de trabajo pueden crear y personalizar objetos personalizados para albergar información para la cual los objetos estándar no son aptos.
|
||||
Custom objects are objects that you can create to store information that is unique to your organization. They are not built-in; members of your workspace can create and customize custom objects to hold information that standard objects aren't suitable for.
|
||||
|
||||
## Esquema de alto nivel
|
||||
## High-level schema
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="Esquema de alto nivel" />
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="High level schema" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## Cómo funciona
|
||||
## How it works
|
||||
|
||||
Los objetos personalizados provienen de tablas de metadatos que determinan la forma, el nombre y el tipo de los objetos. Toda esta información está presente en la base de datos del esquema de metadatos, que consta de tablas:
|
||||
Custom objects come from metadata tables that determine the shape, name, and type of the objects. All this information is present in the metadata schema database, consisting of tables:
|
||||
|
||||
* **DataSource**: Detalles de dónde se encuentra la información.
|
||||
* **Object**: Describe el objeto y lo vincula a un DataSource.
|
||||
* **Field**: Describe los campos de un objeto y lo conecta al objeto.
|
||||
* **DataSource**: Details where the data is present.
|
||||
* **Object**: Describes the object and links to a DataSource.
|
||||
* **Field**: Outlines an Object's fields and connects to the Object.
|
||||
|
||||
Para añadir un objeto personalizado, el workspaceMember consultará la API de /metadata. Esto actualiza los metadatos de acuerdo y calcula un esquema GraphQL basado en los metadatos, almacenándolo en un caché de GQL para su uso posterior.
|
||||
To add a custom object, the workspaceMember will query the /metadata API. This updates the metadata accordingly and computes a GraphQL schema based on the metadata, storing it in a GQL cache for later use.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Consultando la API de /metadata para añadir objetos personalizados" />
|
||||
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Query the /metadata API to add custom objects" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
Para obtener datos, el proceso implica hacer consultas a través del endpoint /graphql y pasarlos a través del Query Resolver.
|
||||
To fetch data, the process involves making queries through the /graphql endpoint and passing them through the Query Resolver.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="Consulta el endpoint /graphql para obtener datos" />
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="Query the /graphql endpoint to fetch data" />
|
||||
</div>
|
||||
|
||||
+12
-12
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: Indicadores de característica
|
||||
title: Feature Flags
|
||||
---
|
||||
|
||||
Los indicadores de característica se usan para ocultar características experimentales. Para Twenty, se configuran a nivel de espacio de trabajo y no a nivel de usuario.
|
||||
Feature flags are used to hide experimental features. For Twenty, they are set on workspace level and not on a user level.
|
||||
|
||||
## Agregar un nuevo indicador de característica
|
||||
## Adding a new feature flag
|
||||
|
||||
En `FeatureFlagKey.ts` agrega el indicador de característica:
|
||||
In `FeatureFlagKey.ts` add the feature flag:
|
||||
|
||||
```ts
|
||||
type FeatureFlagKey =
|
||||
@@ -14,7 +14,7 @@ type FeatureFlagKey =
|
||||
| ...;
|
||||
```
|
||||
|
||||
También agrégalo al enum en `feature-flag.entity.ts`:
|
||||
Also add it to the enum in `feature-flag.entity.ts`:
|
||||
|
||||
```ts
|
||||
enum FeatureFlagKeys {
|
||||
@@ -23,7 +23,7 @@ enum FeatureFlagKeys {
|
||||
}
|
||||
```
|
||||
|
||||
Para aplicar un indicador de característica en una característica de **backend** usa:
|
||||
To apply a feature flag on a **backend** feature use:
|
||||
|
||||
```ts
|
||||
@Gate({
|
||||
@@ -31,16 +31,16 @@ Para aplicar un indicador de característica en una característica de **backend
|
||||
})
|
||||
```
|
||||
|
||||
Para aplicar un indicador de característica en una característica de **frontend** usa:
|
||||
To apply a feature flag on a **frontend** feature use:
|
||||
|
||||
```ts
|
||||
const isFeatureNameEnabled = useIsFeatureEnabled('IS_FEATURENAME_ENABLED');
|
||||
```
|
||||
|
||||
## Configurar indicadores de característica para el despliegue
|
||||
## Configure feature flags for the deployment
|
||||
|
||||
Cambie el registro correspondiente en la Tabla `core.featureFlag`:
|
||||
Change the corresponding record in the Table `core.featureFlag`:
|
||||
|
||||
| iD | clave | workspaceId | valor |
|
||||
| --------- | ------------------------ | ------------------------- | ----------- |
|
||||
| Aleatorio | `IS_FEATURENAME_ENABLED` | ID del espacio de trabajo | `verdadero` |
|
||||
| id | key | workspaceId | value |
|
||||
| ------ | ------------------------ | ----------- | ------ |
|
||||
| Random | `IS_FEATURENAME_ENABLED` | WorkspaceID | `true` |
|
||||
|
||||
+62
-62
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: Arquitectura de Carpetas
|
||||
info: Una mirada detallada a la arquitectura de carpetas de nuestro servidor
|
||||
title: Folder Architecture
|
||||
info: A detailed look into our server folder architecture
|
||||
---
|
||||
|
||||
La estructura del directorio backend es la siguiente:
|
||||
The backend directory structure is as follows:
|
||||
|
||||
```
|
||||
servidor
|
||||
server
|
||||
└───ability
|
||||
└───constants
|
||||
└───core
|
||||
@@ -21,105 +21,105 @@ servidor
|
||||
└───utils
|
||||
```
|
||||
|
||||
## Habilidad
|
||||
## Ability
|
||||
|
||||
Define permisos e incluye gestores para cada entidad.
|
||||
Defines permissions and includes handlers for each entity.
|
||||
|
||||
## Decoradores
|
||||
## Decorators
|
||||
|
||||
Define decoradores personalizados en NestJS para funcionalidad adicional.
|
||||
Defines custom decorators in NestJS for added functionality.
|
||||
|
||||
Ver [decoradores personalizados](https://docs.nestjs.com/custom-decorators) para más detalles.
|
||||
See [custom decorators](https://docs.nestjs.com/custom-decorators) for more details.
|
||||
|
||||
## Filtros
|
||||
## Filters
|
||||
|
||||
Incluye filtros de excepciones para manejar excepciones que puedan ocurrir en endpoints de GraphQL.
|
||||
Includes exception filters to handle exceptions that might occur in GraphQL endpoints.
|
||||
|
||||
## Guardias
|
||||
## Guards
|
||||
|
||||
Ver [guardias](https://docs.nestjs.com/guards) para más detalles.
|
||||
See [guards](https://docs.nestjs.com/guards) for more details.
|
||||
|
||||
## Salud
|
||||
## Health
|
||||
|
||||
Incluye una API REST públicamente disponible (healthz) que devuelve un JSON para confirmar si la base de datos está funcionando como se esperaba.
|
||||
Includes a publicly available REST API (healthz) that returns a JSON to confirm whether the database is working as expected.
|
||||
|
||||
## Metadatos
|
||||
## Metadata
|
||||
|
||||
Define objetos personalizados y hace disponible una API de GraphQL (graphql/metadata).
|
||||
Defines custom objects and makes available a GraphQL API (graphql/metadata).
|
||||
|
||||
## Espacio de trabajo
|
||||
## Workspace
|
||||
|
||||
Genera y sirve un esquema GraphQL personalizado basado en los metadatos.
|
||||
Generates and serves custom GraphQL schema based on the metadata.
|
||||
|
||||
### Estructura del Directorio de Espacio de Trabajo
|
||||
### Workspace Directory Structure
|
||||
|
||||
```
|
||||
workspace
|
||||
|
||||
└───construcción-esquema-espacio-de-trabajo
|
||||
└───fábricas
|
||||
└───tipos-graphql
|
||||
└───bases-datos
|
||||
└───workspace-schema-builder
|
||||
└───factories
|
||||
└───graphql-types
|
||||
└───database
|
||||
└───interfaces
|
||||
└───definiciones-objetos
|
||||
└───servicios
|
||||
└───almacenamiento
|
||||
└───utilidades
|
||||
└───constructor-resolver-espacio-de-trabajo
|
||||
└───fábricas
|
||||
└───object-definitions
|
||||
└───services
|
||||
└───storage
|
||||
└───utils
|
||||
└───workspace-resolver-builder
|
||||
└───factories
|
||||
└───interfaces
|
||||
└───constructor-consultas-espacio-de-trabajo
|
||||
└───fábricas
|
||||
└───workspace-query-builder
|
||||
└───factories
|
||||
└───interfaces
|
||||
└───ejecutor-consultas-espacio-de-trabajo
|
||||
└───workspace-query-runner
|
||||
└───interfaces
|
||||
└───utilidades
|
||||
└───fuente-datos-espacio-de-trabajo
|
||||
└───gestor-espacio-de-trabajo
|
||||
└───ejecutor-migraciones-espacio-de-trabajo
|
||||
└───utilidades
|
||||
└───espacio.trabajo.module.ts
|
||||
└───espacio.trabajo.factory.spec.ts
|
||||
└───espacio.trabajo.factory.ts
|
||||
└───utils
|
||||
└───workspace-datasource
|
||||
└───workspace-manager
|
||||
└───workspace-migration-runner
|
||||
└───utils
|
||||
└───workspace.module.ts
|
||||
└───workspace.factory.spec.ts
|
||||
└───workspace.factory.ts
|
||||
```
|
||||
|
||||
La raíz del directorio de espacio de trabajo incluye el `espacio.trabajo.factory.ts`, un archivo que contiene la función `createGraphQLSchema`. Esta función genera un esquema específico para el espacio de trabajo utilizando los metadatos para adaptar un esquema para espacios de trabajo individuales. Al separar la construcción del esquema y del resolver, usamos la función `makeExecutableSchema`, que combina estos elementos discretos.
|
||||
The root of the workspace directory includes the `workspace.factory.ts`, a file containing the `createGraphQLSchema` function. This function generates workspace-specific schema by using the metadata to tailor a schema for individual workspaces. By separating the schema and resolver construction, we use the `makeExecutableSchema` function, which combines these discrete elements.
|
||||
|
||||
Esta estrategia no solo se trata de organización, sino que también ayuda con la optimización, como el almacenamiento en caché de definiciones de tipos generados para mejorar el rendimiento y la escalabilidad.
|
||||
This strategy is not just about organization, but also helps with optimization, such as caching generated type definitions to enhance performance and scalability.
|
||||
|
||||
### Constructor de Esquema de Espacio de Trabajo
|
||||
### Workspace Schema builder
|
||||
|
||||
Genera el esquema GraphQL, e incluye:
|
||||
Generates the GraphQL schema, and includes:
|
||||
|
||||
#### Fábricas:
|
||||
#### Factories:
|
||||
|
||||
Constructores especializados para generar constructos relacionados con GraphQL.
|
||||
Specialised constructors to generate GraphQL-related constructs.
|
||||
|
||||
* La fábrica de tipos traduce los metadatos de los campos en tipos GraphQL utilizando `TypeMapperService`.
|
||||
* La fábrica de definiciones de tipos crea objetos de entrada o salida de GraphQL derivados de `objectMetadata`.
|
||||
* The type.factory translates field metadata into GraphQL types using `TypeMapperService`.
|
||||
* The type-definition.factory creates GraphQL input or output objects derived from `objectMetadata`.
|
||||
|
||||
#### Tipos GraphQL
|
||||
#### GraphQL Types
|
||||
|
||||
Incluye enumeraciones, entradas, objetos y escalares, y sirve como bloques de construcción para la construcción del esquema.
|
||||
Includes enumerations, inputs, objects, and scalars, and serves as the building blocks for the schema construction.
|
||||
|
||||
#### Interfaces y Definiciones de Objetos
|
||||
#### Interfaces and Object Definitions
|
||||
|
||||
Contiene los planos para entidades GraphQL, e incluye tanto tipos predefinidos como personalizados como `MONEY` o `URL`.
|
||||
Contains the blueprints for GraphQL entities, and includes both predefined and custom types like `MONEY` or `URL`.
|
||||
|
||||
#### Servicios
|
||||
#### Services
|
||||
|
||||
Contiene el servicio responsable de asociar FieldMetadataType con su escalar de GraphQL apropiado o modificadores de consulta.
|
||||
Contains the service responsible for associating FieldMetadataType with its appropriate GraphQL scalar or query modifiers.
|
||||
|
||||
#### Almacenamiento
|
||||
#### Storage
|
||||
|
||||
Incluye la clase `TypeDefinitionsStorage` que contiene definiciones de tipos reutilizables, previniendo duplicación de tipos GraphQL.
|
||||
Includes the `TypeDefinitionsStorage` class that contains reusable type definitions, preventing duplication of GraphQL types.
|
||||
|
||||
### Constructor de Resolver de Espacio de Trabajo
|
||||
### Workspace Resolver Builder
|
||||
|
||||
Crea funciones de resolutor para consultar y modificar el esquema GraphQL.
|
||||
Creates resolver functions for querying and mutating the GraphQL schema.
|
||||
|
||||
Cada fábrica en este directorio es responsable de producir un tipo de resolutor distinto, como el `FindManyResolverFactory`, diseñado para aplicación adaptable a través de varias tablas.
|
||||
Each factory in this directory is responsible for producing a distinct resolver type, such as the `FindManyResolverFactory`, designed for adaptable application across various tables.
|
||||
|
||||
### Ejecutor de Consultas de Espacio de Trabajo
|
||||
### Workspace Query Runner
|
||||
|
||||
Ejecuta las consultas generadas en la base de datos y analiza el resultado.
|
||||
Runs the generated queries on the database and parses the result.
|
||||
|
||||
+10
-10
@@ -1,20 +1,20 @@
|
||||
---
|
||||
title: Cola de Mensajes
|
||||
title: Message Queue
|
||||
---
|
||||
|
||||
Las colas facilitan la realización de operaciones asíncronas. Se pueden utilizar para realizar tareas en segundo plano, como enviar un correo de bienvenida al registrarse.
|
||||
Cada caso de uso tendrá su propia clase de cola extendida de `MessageQueueServiceBase`.
|
||||
Queues facilitate async operations to be performed. They can be used for performing background tasks such as sending a welcome email on register.
|
||||
Each use case will have its own queue class extended from `MessageQueueServiceBase`.
|
||||
|
||||
Actualmente, solo soportamos `bull-mq`[bull-mq](https://bullmq.io/) como el controlador de cola.
|
||||
Currently, we only support `bull-mq`[bull-mq](https://bullmq.io/) as the queue driver.
|
||||
|
||||
## Pasos para crear y usar una nueva cola
|
||||
## Steps to create and use a new queue
|
||||
|
||||
1. Agregue un nombre de cola para su nueva cola en la enumeración `MESSAGE_QUEUES`.
|
||||
2. Proporcione la implementación de fábrica de la cola con el nombre de la cola como el token de dependencia.
|
||||
3. Inyecte la cola que creó en el módulo/servicio requerido con el nombre de la cola como el token de dependencia.
|
||||
4. Agregue una clase de trabajador con inyección basada en token, al igual que el productor.
|
||||
1. Add a queue name for your new queue under enum `MESSAGE_QUEUES`.
|
||||
2. Provide the factory implementation of the queue with the queue name as the dependency token.
|
||||
3. Inject the queue that you created in the required module/service with the queue name as the dependency token.
|
||||
4. Add worker class with token based injection just like producer.
|
||||
|
||||
### Ejemplo de uso
|
||||
### Example usage
|
||||
|
||||
```ts
|
||||
class Resolver {
|
||||
|
||||
+27
-26
@@ -1,19 +1,19 @@
|
||||
---
|
||||
title: Comandos de Backend
|
||||
title: Backend Commands
|
||||
---
|
||||
|
||||
## Comandos útiles
|
||||
## Useful commands
|
||||
|
||||
Estos comandos deben ejecutarse desde la carpeta packages/twenty-server.
|
||||
Desde cualquier otra carpeta, puedes ejecutar `npx nx {command} twenty-server` (o `npx nx run twenty-server:{command}`).
|
||||
These commands should be executed from packages/twenty-server folder.
|
||||
From any other folder you can run `npx nx {command} twenty-server` (or `npx nx run twenty-server:{command}`).
|
||||
|
||||
### Configuración inicial
|
||||
### First time setup
|
||||
|
||||
```
|
||||
npx nx database:reset twenty-server # setup the database with dev seeds
|
||||
```
|
||||
|
||||
### Iniciando el servidor
|
||||
### Starting the server
|
||||
|
||||
```
|
||||
npx nx run twenty-server:start
|
||||
@@ -25,52 +25,53 @@ npx nx run twenty-server:start
|
||||
npx nx run twenty-server:lint # pass --fix to fix lint errors
|
||||
```
|
||||
|
||||
### Prueba
|
||||
### Test
|
||||
|
||||
```
|
||||
npx nx run twenty-server:test:unit # run unit tests
|
||||
npx nx run twenty-server:test:integration # run integration tests
|
||||
```
|
||||
|
||||
Nota: puedes ejecutar `npx nx run twenty-server:test:integration:with-db-reset` en caso de que necesites restablecer la base de datos antes de ejecutar las pruebas de integración.
|
||||
Note: you can run `npx nx run twenty-server:test:integration:with-db-reset` in case you need to reset the database before running the integration tests.
|
||||
|
||||
### Restablecer la base de datos
|
||||
### Resetting the database
|
||||
|
||||
Si deseas restablecer y sembrar la base de datos, puedes ejecutar el siguiente comando:
|
||||
If you want to reset and seed the database, you can run the following command:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:database:reset
|
||||
```
|
||||
|
||||
### Migraciones
|
||||
### Migrations
|
||||
|
||||
#### Para objetos en esquemas Core/Metadata (TypeORM)
|
||||
#### For objects in Core/Metadata schemas (TypeORM)
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
|
||||
```
|
||||
|
||||
#### Para objetos de Workspace
|
||||
#### For Workspace objects
|
||||
|
||||
No hay archivos de migraciones, las migraciones se generan automáticamente para cada espacio de trabajo, se almacenan en la base de datos y se aplican con este comando
|
||||
There are no migrations files, migration are generated automatically for each workspace,
|
||||
stored in the database, and applied with this command
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Esto eliminará la base de datos y volverá a ejecutar las migraciones y semillas.
|
||||
This will drop the database and re-run the migrations and seed.
|
||||
|
||||
Asegúrate de respaldar cualquier dato que desees conservar antes de ejecutar este comando.
|
||||
Make sure to back up any data you want to keep before running this command.
|
||||
</Warning>
|
||||
|
||||
## Stack Tecnológico
|
||||
## Tech Stack
|
||||
|
||||
Twenty utiliza principalmente NestJS para el backend.
|
||||
Twenty primarily uses NestJS for the backend.
|
||||
|
||||
Prisma fue el primer ORM que usamos. Pero para permitir a los usuarios crear campos y objetos personalizados, un nivel más bajo tenía más sentido ya que necesitamos tener un control detallado. El proyecto ahora usa TypeORM.
|
||||
Prisma was the first ORM we used. But in order to allow users to create custom fields and custom objects, a lower-level made more sense as we need to have fine-grained control. The project now uses TypeORM.
|
||||
|
||||
Así es como se ve la pila tecnológica ahora.
|
||||
Here's what the tech stack now looks like.
|
||||
|
||||
**Core**
|
||||
|
||||
@@ -78,23 +79,23 @@ Así es como se ve la pila tecnológica ahora.
|
||||
* [TypeORM](https://typeorm.io/)
|
||||
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
||||
|
||||
**Base de datos**
|
||||
**Database**
|
||||
|
||||
* [Postgres](https://www.postgresql.org/)
|
||||
|
||||
**Integraciones de terceros**
|
||||
**Third-party integrations**
|
||||
|
||||
* [Sentry](https://sentry.io/welcome/) para rastrear errores
|
||||
* [Sentry](https://sentry.io/welcome/) for tracking bugs
|
||||
|
||||
**Pruebas**
|
||||
**Testing**
|
||||
|
||||
* [Jest](https://jestjs.io/)
|
||||
|
||||
**Herramientas**
|
||||
**Tooling**
|
||||
|
||||
* [Yarn](https://yarnpkg.com/)
|
||||
* [ESLint](https://eslint.org/)
|
||||
|
||||
**Desarrollo**
|
||||
**Development**
|
||||
|
||||
* [AWS EKS](https://aws.amazon.com/eks/)
|
||||
|
||||
+20
-20
@@ -1,18 +1,18 @@
|
||||
---
|
||||
title: Aplicación Zapier
|
||||
title: Zapier App
|
||||
---
|
||||
|
||||
Sincroniza sin esfuerzo Twenty con más de 3000 aplicaciones usando [Zapier](https://zapier.com/). ¡Automatiza tareas, mejora la productividad y potencia tus relaciones con los clientes!
|
||||
Effortlessly sync Twenty with 3000+ apps using [Zapier](https://zapier.com/). Automate tasks, boost productivity, and supercharge your customer relationships!
|
||||
|
||||
## Acerca de Zapier
|
||||
## About Zapier
|
||||
|
||||
Zapier es una herramienta que permite automatizar flujos de trabajo al conectar las aplicaciones que tu equipo utiliza a diario. El concepto fundamental de Zapier son los flujos de trabajo automatizados, llamados Zaps, que incluyen desencadenantes y acciones.
|
||||
Zapier is a tool that allows you to automate workflows by connecting the apps that your team uses every day. The fundamental concept of Zapier is automation workflows, called Zaps, and include triggers and actions.
|
||||
|
||||
Puedes aprender más sobre cómo funciona Zapier [aquí](https://zapier.com/how-it-works).
|
||||
You can learn more about how Zapier works [here](https://zapier.com/how-it-works).
|
||||
|
||||
## Configuración
|
||||
## Setup
|
||||
|
||||
### Paso 1: Instalar paquetes de Zapier
|
||||
### Step 1: Install Zapier packages
|
||||
|
||||
```bash
|
||||
cd packages/twenty-zapier
|
||||
@@ -20,33 +20,33 @@ cd packages/twenty-zapier
|
||||
yarn
|
||||
```
|
||||
|
||||
### Paso 2: Iniciar sesión con la CLI
|
||||
### Step 2: Login with the CLI
|
||||
|
||||
Utiliza tus credenciales de Zapier para iniciar sesión usando la CLI:
|
||||
Use your Zapier credentials to log in using the CLI:
|
||||
|
||||
```bash
|
||||
zapier login
|
||||
```
|
||||
|
||||
### Paso 3: Configurar variables de entorno
|
||||
### Step 3: Set environment variables
|
||||
|
||||
Desde la carpeta `packages/twenty-zapier`, ejecuta:
|
||||
From the `packages/twenty-zapier` folder, run:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Ejecuta la aplicación localmente, ve a [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks) y genera una clave API.
|
||||
Run the application locally, go to [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks), and generate an API key.
|
||||
|
||||
Reemplaza el valor de **YOUR_API_KEY** en el archivo `.env` con la clave API que acabas de generar.
|
||||
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
|
||||
|
||||
## Desarrollo
|
||||
## Development
|
||||
|
||||
<Warning>
|
||||
Asegúrate de ejecutar `yarn build` antes de cualquier comando `zapier`.
|
||||
Make sure to run `yarn build` before any `zapier` command.
|
||||
</Warning>
|
||||
|
||||
### Prueba
|
||||
### Test
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
@@ -58,25 +58,25 @@ yarn test
|
||||
yarn format
|
||||
```
|
||||
|
||||
### Observa y compila mientras editas el código
|
||||
### Watch and compile as you edit code
|
||||
|
||||
```bash
|
||||
yarn watch
|
||||
```
|
||||
|
||||
### Valida tu aplicación Zapier
|
||||
### Validate your Zapier app
|
||||
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
|
||||
### Despliega tu aplicación Zapier
|
||||
### Deploy your Zapier app
|
||||
|
||||
```bash
|
||||
yarn deploy
|
||||
```
|
||||
|
||||
### Lista todos los comandos de Zapier CLI
|
||||
### List all Zapier CLI commands
|
||||
|
||||
```bash
|
||||
zapier
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
---
|
||||
title: Errores, solicitudes y pull requests
|
||||
info: Informa de problemas, solicita funcionalidades y contribuye con código
|
||||
title: Bugs, Requests & Pull Requests
|
||||
info: Report issues, request features, and contribute code
|
||||
---
|
||||
|
||||
## Reportar errores
|
||||
## Reporting Bugs
|
||||
|
||||
Para reportar un error, por favor [crea un problema en GitHub](https://github.com/twentyhq/twenty/issues/new).
|
||||
To report a bug, please [create an issue on GitHub](https://github.com/twentyhq/twenty/issues/new).
|
||||
|
||||
También puedes pedir ayuda en [Discord](https://discord.gg/cx5n4Jzs57).
|
||||
You can also ask for help on [Discord](https://discord.gg/cx5n4Jzs57).
|
||||
|
||||
## Solicitudes de funciones
|
||||
## Feature Requests
|
||||
|
||||
Si no estás seguro de si es un error y sientes que está más cerca de una solicitud de función, entonces probablemente deberías [abrir una discusión en su lugar](https://github.com/twentyhq/twenty/discussions/new).
|
||||
If you're not sure if it's a bug, and you feel it's closer to a feature request, then you should probably [open a discussion instead](https://github.com/twentyhq/twenty/discussions/new).
|
||||
|
||||
## Envía un pull request
|
||||
## Submit a Pull Request
|
||||
|
||||
Contribuir código a Twenty comienza con un pull request (PR).
|
||||
Contributing code to Twenty starts with a pull request (PR).
|
||||
|
||||
### Antes de empezar
|
||||
### Before You Start
|
||||
|
||||
1. Consulta [los issues existentes](https://github.com/twentyhq/twenty/issues) para ver trabajos relacionados
|
||||
2. Para nuevas funcionalidades, abre primero un issue para discutir
|
||||
3. Revisa nuestro [Código de conducta](https://github.com/twentyhq/twenty/blob/main/CODE_OF_CONDUCT.md)
|
||||
1. Check [existing issues](https://github.com/twentyhq/twenty/issues) for related work
|
||||
2. For new features, open an issue first to discuss
|
||||
3. Review our [Code of Conduct](https://github.com/twentyhq/twenty/blob/main/CODE_OF_CONDUCT.md)
|
||||
|
||||
### Haz un fork y clona
|
||||
### Fork and Clone
|
||||
|
||||
1. Haz un fork del repositorio en GitHub
|
||||
2. Clona tu fork:
|
||||
1. Fork the repository on GitHub
|
||||
2. Clone your fork:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/twenty.git
|
||||
cd twenty
|
||||
```
|
||||
|
||||
3. Añade el remoto upstream:
|
||||
3. Add upstream remote:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/twentyhq/twenty.git
|
||||
```
|
||||
|
||||
### Crea una rama
|
||||
### Create a Branch
|
||||
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
Usa nombres de rama descriptivos:
|
||||
Use descriptive branch names:
|
||||
|
||||
* `feature/add-export-button`
|
||||
* `fix/login-redirect-issue`
|
||||
* `docs/update-api-guide`
|
||||
|
||||
### Realiza tus cambios
|
||||
### Make Your Changes
|
||||
|
||||
1. Escribe código limpio y bien documentado
|
||||
2. Sigue el estilo de código existente
|
||||
3. Añade pruebas para la nueva funcionalidad
|
||||
4. Actualiza la documentación si es necesario
|
||||
1. Write clean, well-documented code
|
||||
2. Follow existing code style
|
||||
3. Add tests for new functionality
|
||||
4. Update documentation if needed
|
||||
|
||||
### Envía tu PR
|
||||
### Submit Your PR
|
||||
|
||||
1. Haz push de tu rama:
|
||||
1. Push your branch:
|
||||
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
2. Abre un PR en GitHub
|
||||
3. Rellena la plantilla del PR
|
||||
4. Vincula los issues relacionados
|
||||
2. Open a PR on GitHub
|
||||
3. Fill in the PR template
|
||||
4. Link related issues
|
||||
|
||||
### Lista de verificación de PR
|
||||
### PR Checklist
|
||||
|
||||
* [ ] El código sigue las pautas de estilo del proyecto
|
||||
* [ ] Las pruebas pasan localmente
|
||||
* [ ] La documentación está actualizada
|
||||
* [ ] La descripción del PR explica los cambios
|
||||
* [ ] Code follows project style guidelines
|
||||
* [ ] Tests pass locally
|
||||
* [ ] Documentation is updated
|
||||
* [ ] PR description explains the changes
|
||||
|
||||
+80
-80
@@ -1,19 +1,19 @@
|
||||
---
|
||||
title: Mejores prácticas
|
||||
title: Best Practices
|
||||
---
|
||||
|
||||
Este documento describe las mejores prácticas que debes seguir al trabajar en el frontend.
|
||||
This document outlines the best practices you should follow when working on the frontend.
|
||||
|
||||
## Gestión de estado
|
||||
## State management
|
||||
|
||||
React y Recoil manejan la gestión de estado en la base de código.
|
||||
React and Recoil handle state management in the codebase.
|
||||
|
||||
### Usa `useRecoilState` para almacenar el estado
|
||||
### Use `useRecoilState` to store state
|
||||
|
||||
Es buena práctica crear tantos átomos como necesites para almacenar tu estado.
|
||||
It's good practice to create as many atoms as you need to store your state.
|
||||
|
||||
<Warning>
|
||||
Es mejor usar átomos adicionales que intentar ser demasiado concisos con la perforación de props.
|
||||
It's better to use extra atoms than trying to be too concise with props drilling.
|
||||
</Warning>
|
||||
|
||||
```tsx
|
||||
@@ -36,29 +36,29 @@ export const MyComponent = () => {
|
||||
}
|
||||
```
|
||||
|
||||
### No uses `useRef` para almacenar el estado
|
||||
### Do not use `useRef` to store state
|
||||
|
||||
Evita usar `useRef` para almacenar el estado.
|
||||
Avoid using `useRef` to store state.
|
||||
|
||||
If you want to store state, you should use `useState` or `useRecoilState`.
|
||||
|
||||
Consulta [cómo gestionar las re-renderizaciones](#managing-re-renders) si sientes que necesitas `useRef` para evitar algunas re-renderizaciones.
|
||||
See [how to manage re-renders](#managing-re-renders) if you feel like you need `useRef` to prevent some re-renders from happening.
|
||||
|
||||
## Gestión de las re-renderizaciones
|
||||
## Managing re-renders
|
||||
|
||||
Las re-renderizaciones pueden ser difíciles de gestionar en React.
|
||||
Re-renders can be hard to manage in React.
|
||||
|
||||
Aquí hay algunas reglas a seguir para evitar re-renderizaciones innecesarias.
|
||||
Here are some rules to follow to avoid unnecessary re-renders.
|
||||
|
||||
Ten en cuenta que siempre puedes evitar re-renderizaciones comprendiendo su causa.
|
||||
Keep in mind that you can **always** avoid re-renders by understanding their cause.
|
||||
|
||||
### Trabaja a nivel de raíz
|
||||
### Work at the root level
|
||||
|
||||
Ahora es fácil evitar re-renderizaciones en nuevas funciones eliminándolas a nivel de raíz.
|
||||
Avoiding re-renders in new features is now made easy by eliminating them at the root level.
|
||||
|
||||
The `PageChangeEffect` sidecar component contains just one `useEffect` that holds all the logic to execute on a page change.
|
||||
|
||||
De esa manera, sabes que solo hay un lugar que puede desencadenar una re-renderización.
|
||||
That way you know that there's just one place that can trigger a re-render.
|
||||
|
||||
### Always think twice before adding `useEffect` in your codebase
|
||||
|
||||
@@ -68,17 +68,17 @@ You should think whether you need `useEffect`, or if you can move the logic in a
|
||||
|
||||
You'll find it generally easy to move the logic in a `handleClick` or `handleChange` function.
|
||||
|
||||
También puedes encontrarlas en bibliotecas como Apollo: `onCompleted`, `onError`, etc.
|
||||
You can also find them in libraries like Apollo: `onCompleted`, `onError`, etc.
|
||||
|
||||
### Use a sibling component to extract `useEffect` or data fetching logic
|
||||
|
||||
If you feel like you need to add a `useEffect` in your root component, you should consider extracting it in a sidecar component.
|
||||
|
||||
Puedes aplicar lo mismo para la lógica de obtención de datos, con hooks de Apollo.
|
||||
You can apply the same for data fetching logic, with Apollo hooks.
|
||||
|
||||
```tsx
|
||||
// ❌ Malo, provocará re-renderizados incluso si los datos no cambian,
|
||||
// porque useEffect necesita volver a evaluarse
|
||||
// ❌ Bad, will cause re-renders even if data is not changing,
|
||||
// because useEffect needs to be re-evaluated
|
||||
export const PageComponent = () => {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
const [someDependency] = useRecoilState(someDependencyState);
|
||||
@@ -100,8 +100,8 @@ export const App = () => (
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Bueno, no provocará re-renderizados si los datos no cambian,
|
||||
// porque useEffect se vuelve a evaluar en otro componente hermano
|
||||
// ✅ Good, will not cause re-renders if data is not changing,
|
||||
// because useEffect is re-evaluated in another sibling component
|
||||
export const PageComponent = () => {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
|
||||
@@ -129,84 +129,84 @@ export const App = () => (
|
||||
);
|
||||
```
|
||||
|
||||
### Usa estados de familia de recoil y selectores de familia de recoil
|
||||
### Use recoil family states and recoil family selectors
|
||||
|
||||
Los estados y selectores de familia de recoil son una gran manera de evitar re-renderizaciones.
|
||||
Recoil family states and selectors are a great way to avoid re-renders.
|
||||
|
||||
Son útiles cuando necesitas almacenar una lista de elementos.
|
||||
They are useful when you need to store a list of items.
|
||||
|
||||
### No deberías usar `React.memo(MyComponent)`
|
||||
### You shouldn't use `React.memo(MyComponent)`
|
||||
|
||||
Evita usar `React.memo()` porque no resuelve la causa de la re-renderización, sino que rompe la cadena de re-renderización, lo que puede llevar a un comportamiento inesperado y hacer que el código sea muy difícil de refactorizar.
|
||||
Avoid using `React.memo()` because it does not solve the cause of the re-render, but instead breaks the re-render chain, which can lead to unexpected behavior and make the code very hard to refactor.
|
||||
|
||||
### Limita el uso de `useCallback` o `useMemo`
|
||||
### Limit `useCallback` or `useMemo` usage
|
||||
|
||||
A menudo no son necesarios y harán que el código sea más difícil de leer y mantener para una ganancia de rendimiento que es imperceptible.
|
||||
They are often not necessary and will make the code harder to read and maintain for a gain of performance that is unnoticeable.
|
||||
|
||||
## Console.logs
|
||||
|
||||
Las declaraciones `console.log` son valiosas durante el desarrollo, ofreciendo información en tiempo real sobre los valores de las variables y el flujo de código. Pero, dejarlas en el código de producción puede llevar a varios problemas:
|
||||
`console.log` statements are valuable during development, offering real-time insights into variable values and code flow. But, leaving them in production code can lead to several issues:
|
||||
|
||||
1. **Rendimiento**: El registro excesivo puede afectar el rendimiento en tiempo de ejecución, especialmente en aplicaciones del lado del cliente.
|
||||
1. **Performance**: Excessive logging can affect the runtime performance, especially on client-side applications.
|
||||
|
||||
2. **Seguridad**: Registrar datos sensibles puede exponer información crítica a cualquier persona que inspeccione la consola del navegador.
|
||||
2. **Security**: Logging sensitive data can expose critical information to anyone who inspects the browser's console.
|
||||
|
||||
3. **Limpieza**: Llenar la consola con registros puede oscurecer advertencias o errores importantes que los desarrolladores o herramientas necesitan ver.
|
||||
3. **Cleanliness**: Filling up the console with logs can obscure important warnings or errors that developers or tools need to see.
|
||||
|
||||
4. **Profesionalismo**: Los usuarios finales o clientes que revisen la consola y vean una miríada de declaraciones de registros podrían cuestionar la calidad y el acabado del código.
|
||||
4. **Professionalism**: End users or clients checking the console and seeing a myriad of log statements might question the code's quality and polish.
|
||||
|
||||
Asegúrate de eliminar todos los `console.logs` antes de enviar el código a producción.
|
||||
Make sure you remove all `console.logs` before pushing the code to production.
|
||||
|
||||
## Nomenclatura
|
||||
## Naming
|
||||
|
||||
### Nombres de variables
|
||||
### Variable Naming
|
||||
|
||||
Los nombres de variables deben describir con precisión el propósito o función de la variable.
|
||||
Variable names ought to precisely depict the purpose or function of the variable.
|
||||
|
||||
#### El problema con los nombres genéricos
|
||||
#### The issue with generic names
|
||||
|
||||
Los nombres genéricos en programación no son ideales porque carecen de especificidad, lo que lleva a la ambigüedad y reduce la legibilidad del código. Tales nombres no transmiten el propósito de la variable o función, lo que dificulta a los desarrolladores entender la intención del código sin una investigación más profunda. Esto puede resultar en un aumento del tiempo de depuración, una mayor susceptibilidad a errores y dificultades en el mantenimiento y colaboración. Mientras tanto, la nomenclatura descriptiva hace que el código sea autoexplicativo y más fácil de navegar, mejorando la calidad del código y la productividad del desarrollador.
|
||||
Generic names in programming are not ideal because they lack specificity, leading to ambiguity and reduced code readability. Such names fail to convey the variable or function's purpose, making it challenging for developers to understand the code's intent without deeper investigation. This can result in increased debugging time, higher susceptibility to errors, and difficulties in maintenance and collaboration. Meanwhile, descriptive naming makes the code self-explanatory and easier to navigate, enhancing code quality and developer productivity.
|
||||
|
||||
```tsx
|
||||
// ❌ Malo, usa un nombre genérico que no comunica claramente su
|
||||
// propósito o contenido
|
||||
// ❌ Bad, uses a generic name that doesn't communicate its
|
||||
// purpose or content clearly
|
||||
const [value, setValue] = useState('');
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Bueno, usa un nombre descriptivo
|
||||
// ✅ Good, uses a descriptive name
|
||||
const [email, setEmail] = useState('');
|
||||
```
|
||||
|
||||
#### Algunas palabras a evitar en los nombres de variables
|
||||
#### Some words to avoid in variable names
|
||||
|
||||
* ficticio
|
||||
* dummy
|
||||
|
||||
### Manejadores de eventos
|
||||
### Event handlers
|
||||
|
||||
Los nombres de manejadores de eventos deben comenzar con `handle`, mientras que `on` es un prefijo usado para nombrar eventos en las props de los componentes.
|
||||
Event handler names should start with `handle`, while `on` is a prefix used to name events in components props.
|
||||
|
||||
```tsx
|
||||
// ❌ Malo
|
||||
// ❌ Bad
|
||||
const onEmailChange = (val: string) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Bueno
|
||||
// ✅ Good
|
||||
const handleEmailChange = (val: string) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## Props opcionales
|
||||
## Optional Props
|
||||
|
||||
Evita pasar el valor predeterminado para una prop opcional.
|
||||
Avoid passing the default value for an optional prop.
|
||||
|
||||
**EJEMPLO**
|
||||
**EXAMPLE**
|
||||
|
||||
Toma el componente `EmailField` definido a continuación:
|
||||
Take the`EmailField` component defined below:
|
||||
|
||||
```tsx
|
||||
type EmailFieldProps = {
|
||||
@@ -219,28 +219,28 @@ const EmailField = ({ value, disabled = false }: EmailFieldProps) => (
|
||||
);
|
||||
```
|
||||
|
||||
**Uso**
|
||||
**Usage**
|
||||
|
||||
```tsx
|
||||
// ❌ Malo, pasar el mismo valor que el valor predeterminado no aporta nada
|
||||
// ❌ Bad, passing in the same value as the default value adds no value
|
||||
const Form = () => <EmailField value="[email protected]" disabled={false} />;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Bueno, asume el valor predeterminado
|
||||
// ✅ Good, assumes the default value
|
||||
const Form = () => <EmailField value="[email protected]" />;
|
||||
```
|
||||
|
||||
## Componente como props
|
||||
## Component as props
|
||||
|
||||
Intenta tanto como sea posible pasar componentes no instanciados como propiedades, para que los hijos puedan decidir por sí mismos qué propiedades necesitan pasar.
|
||||
Try as much as possible to pass uninstantiated components as props, so children can decide on their own of what props they need to pass.
|
||||
|
||||
El ejemplo más común de esto son los componentes de icono:
|
||||
The most common example for that is icon components:
|
||||
|
||||
```tsx
|
||||
const SomeParentComponent = () => <MyComponent Icon={MyIcon} />;
|
||||
|
||||
// En MyComponent
|
||||
// In MyComponent
|
||||
const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -252,25 +252,25 @@ const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
|
||||
};
|
||||
```
|
||||
|
||||
Para que React entienda que el componente es un componente, necesitas usar PascalCase, para luego instanciarlo con `<MyIcon>`
|
||||
For React to understand that the component is a component, you need to use PascalCase, to later instantiate it with `<MyIcon>`
|
||||
|
||||
## Prop Drilling: Mantenlo Minimalista
|
||||
## Prop Drilling: Keep It Minimal
|
||||
|
||||
El prop drilling, en el contexto de React, se refiere a la práctica de pasar variables de estado y sus setters a través de muchas capas de componentes, incluso si los componentes intermedios no los usan. Aunque a veces es necesario, el exceso de prop drilling puede llevar a:
|
||||
Prop drilling, in the React context, refers to the practice of passing state variables and their setters through many component layers, even if intermediary components don't use them. While sometimes necessary, excessive prop drilling can lead to:
|
||||
|
||||
1. **Readabilidad Reducida**: Rastrear de dónde proviene una propiedad o dónde se utiliza puede volverse complicado en una estructura de componentes muy anidada.
|
||||
1. **Decreased Readability**: Tracing where a prop originates or where it's utilized can become convoluted in a deeply nested component structure.
|
||||
|
||||
2. **Desafíos de Mantenimiento**: Los cambios en la estructura de propiedades de un componente podrían requerir ajustes en varios componentes, incluso si no utilizan directamente la propiedad.
|
||||
2. **Maintenance Challenges**: Changes in one component's prop structure might require adjustments in several components, even if they don't directly use the prop.
|
||||
|
||||
3. **Reducción de la Reusabilidad del Componente**: Un componente que recibe muchas propiedades solo para pasarlas se vuelve menos general y más difícil de reutilizar en diferentes contextos.
|
||||
3. **Reduced Component Reusability**: A component receiving a lot of props solely for passing them down becomes less general-purpose and harder to reuse in different contexts.
|
||||
|
||||
Si sientes que estás usando en exceso el prop drilling, consulta [mejores prácticas de gestión de estado](#state-management).
|
||||
If you feel that you are using excessive prop drilling, see [state management best practices](#state-management).
|
||||
|
||||
## Importar
|
||||
## Imports
|
||||
|
||||
Al importar, opta por los alias designados en lugar de especificar rutas completas o relativas.
|
||||
When importing, opt for the designated aliases rather than specifying complete or relative paths.
|
||||
|
||||
**Alias del identificador**
|
||||
**The Aliases**
|
||||
|
||||
```js
|
||||
{
|
||||
@@ -282,10 +282,10 @@ Al importar, opta por los alias designados en lugar de especificar rutas complet
|
||||
}
|
||||
```
|
||||
|
||||
**Uso**
|
||||
**Usage**
|
||||
|
||||
```tsx
|
||||
// ❌ Malo, especifica toda la ruta relativa
|
||||
// ❌ Bad, specifies the entire relative path
|
||||
import {
|
||||
CatalogDecorator
|
||||
} from '../../../../../testing/decorators/CatalogDecorator';
|
||||
@@ -295,14 +295,14 @@ import {
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Bueno, utiliza los alias designados
|
||||
// ✅ Good, utilises the designated aliases
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
```
|
||||
|
||||
## Validación de Esquema
|
||||
## Schema Validation
|
||||
|
||||
[Zod](https://github.com/colinhacks/zod) es el validador de esquemas para objetos no tipados:
|
||||
[Zod](https://github.com/colinhacks/zod) is the schema validator for untyped objects:
|
||||
|
||||
```js
|
||||
const validationSchema = z
|
||||
@@ -310,16 +310,16 @@ const validationSchema = z
|
||||
exist: z.boolean(),
|
||||
email: z
|
||||
.string()
|
||||
.email('El correo electrónico debe ser válido'),
|
||||
.email('Email must be a valid email'),
|
||||
password: z
|
||||
.string()
|
||||
.regex(PASSWORD_REGEX, 'La contraseña debe contener al menos 8 caracteres'),
|
||||
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
```
|
||||
|
||||
## Cambios Cruciales
|
||||
## Breaking Changes
|
||||
|
||||
Siempre ejecuta pruebas manuales exhaustivas antes de proceder para garantizar que las modificaciones no hayan causado interrupciones en otras partes, dado que las pruebas aún no se han integrado extensivamente.
|
||||
Always perform thorough manual testing before proceeding to guarantee that modifications haven’t caused disruptions elsewhere, given that tests have not yet been extensively integrated.
|
||||
|
||||
+36
-36
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Arquitectura de Carpetas
|
||||
info: Una mirada detallada a nuestra arquitectura de carpetas
|
||||
title: Folder Architecture
|
||||
info: A detailed look into our folder architecture
|
||||
---
|
||||
|
||||
En esta guía, explorarás los detalles de la estructura del directorio del proyecto y cómo contribuye a la organización y mantenibilidad de Twenty.
|
||||
In this guide, you will explore the details of the project directory structure and how it contributes to the organization and maintainability of Twenty.
|
||||
|
||||
Siguiendo esta convención de arquitectura de carpetas, es más fácil encontrar los archivos relacionados con funciones específicas y asegurar que la aplicación sea escalable y mantenible.
|
||||
By following this folder architecture convention, it's easier to find the files related to specific features and ensure that the application is scalable and maintainable.
|
||||
|
||||
```
|
||||
front
|
||||
@@ -22,14 +22,14 @@ front
|
||||
└───...
|
||||
```
|
||||
|
||||
## Páginas
|
||||
## Pages
|
||||
|
||||
Incluye los componentes de alto nivel definidos por las rutas de la aplicación. Importan más componentes de bajo nivel de la carpeta de módulos (más detalles abajo).
|
||||
Includes the top-level components defined by the application routes. They import more low-level components from the modules folder (more details below).
|
||||
|
||||
## Módulos
|
||||
## Modules
|
||||
|
||||
Cada módulo representa una función o un grupo de funciones, comprendiendo sus componentes específicos, estados y lógica operativa.
|
||||
Todos deberían seguir la estructura siguiente. Puedes anidar módulos dentro de módulos (denominados submódulos) y se aplicarán las mismas reglas.
|
||||
Each module represents a feature or a group of feature, comprising its specific components, states, and operational logic.
|
||||
They should all follow the structure below. You can nest modules within modules (referred to as submodules) and the same rules will apply.
|
||||
|
||||
```
|
||||
module1
|
||||
@@ -50,60 +50,60 @@ module1
|
||||
└───utils
|
||||
```
|
||||
|
||||
### Contextos
|
||||
### Contexts
|
||||
|
||||
Un contexto es una manera de pasar datos a través del árbol de componentes sin tener que pasar propiedades manualmente en cada nivel.
|
||||
A context is a way to pass data through the component tree without having to pass props down manually at every level.
|
||||
|
||||
Ver [React Context](https://react.dev/reference/react#context-hooks) para más detalles.
|
||||
See [React Context](https://react.dev/reference/react#context-hooks) for more details.
|
||||
|
||||
### GraphQL
|
||||
|
||||
Incluye fragmentos, consultas y mutaciones.
|
||||
Includes fragments, queries, and mutations.
|
||||
|
||||
Ver [GraphQL](https://graphql.org/learn/) para más detalles.
|
||||
See [GraphQL](https://graphql.org/learn/) for more details.
|
||||
|
||||
* Fragmentos
|
||||
* Fragments
|
||||
|
||||
Un fragmento es una parte reutilizable de una consulta, que puedes usar en diferentes lugares. Usando fragmentos, es más fácil evitar la duplicación de código.
|
||||
A fragment is a reusable piece of a query, which you can use in different places. By using fragments, it's easier to avoid duplicating code.
|
||||
|
||||
Ver [GraphQL Fragments](https://graphql.org/learn/queries/#fragments) para más detalles.
|
||||
See [GraphQL Fragments](https://graphql.org/learn/queries/#fragments) for more details.
|
||||
|
||||
* Consultas
|
||||
* Queries
|
||||
|
||||
Ver [GraphQL Queries](https://graphql.org/learn/queries/) para más detalles.
|
||||
See [GraphQL Queries](https://graphql.org/learn/queries/) for more details.
|
||||
|
||||
* Mutaciones
|
||||
* Mutations
|
||||
|
||||
Ver [GraphQL Mutations](https://graphql.org/learn/queries/#mutations) para más detalles.
|
||||
See [GraphQL Mutations](https://graphql.org/learn/queries/#mutations) for more details.
|
||||
|
||||
### Hooks
|
||||
|
||||
Ver [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) para más detalles.
|
||||
See [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) for more details.
|
||||
|
||||
### Estados
|
||||
### States
|
||||
|
||||
Contiene la lógica de gestión de estado. [RecoilJS](https://recoiljs.org) maneja esto.
|
||||
Contains the state management logic. [RecoilJS](https://recoiljs.org) handles this.
|
||||
|
||||
* Selectores: Ver [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) para más detalles.
|
||||
* Selectors: See [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) for more details.
|
||||
|
||||
La gestión de estado incorporada de React todavía maneja el estado dentro de un componente.
|
||||
React's built-in state management still handles state within a component.
|
||||
|
||||
### Utilidades
|
||||
### Utils
|
||||
|
||||
Debería contener solo funciones puras reutilizables. De lo contrario, crea hooks personalizados en la carpeta `hooks`.
|
||||
Should just contain reusable pure functions. Otherwise, create custom hooks in the `hooks` folder.
|
||||
|
||||
## Interfaz de usuario
|
||||
## UI
|
||||
|
||||
Contiene todos los componentes de interfaz de usuario reutilizables utilizados en la aplicación.
|
||||
Contains all the reusable UI components used in the application.
|
||||
|
||||
Esta carpeta puede contener subcarpetas, como `datos`, `visualización`, `retroalimentación` y `entrada` para tipos específicos de componentes. Cada componente debe ser autónomo y reutilizable, para que puedas usarlo en diferentes partes de la aplicación.
|
||||
This folder can contain sub-folders, like `data`, `display`, `feedback`, and `input` for specific types of components. Each component should be self-contained and reusable, so that you can use it in different parts of the application.
|
||||
|
||||
Al separar los componentes de la interfaz de usuario de otros componentes en la carpeta `modules`, es más fácil mantener un diseño consistente y realizar cambios en la interfaz de usuario sin afectar otras partes (lógica de negocio) del código base.
|
||||
By separating the UI components from the other components in the `modules` folder, it's easier to maintain a consistent design and to make changes to the UI without affecting other parts (business logic) of the codebase.
|
||||
|
||||
## Interfaz y dependencias
|
||||
## Interface and dependencies
|
||||
|
||||
Puedes importar otro código de módulo desde cualquier módulo excepto desde la carpeta `ui`. Esto mantendrá su código fácil de probar.
|
||||
You can import other module code from any module except for the `ui` folder. This will keep its code easy to test.
|
||||
|
||||
### Interno
|
||||
### Internal
|
||||
|
||||
Cada parte (hooks, estados, ...) de un módulo puede tener una carpeta `internal`, que contiene partes que se usan solo dentro del módulo.
|
||||
Each part (hooks, states, ...) of a module can have an `internal` folder, which contains parts that are just used within the module.
|
||||
|
||||
+27
-27
@@ -1,22 +1,22 @@
|
||||
---
|
||||
title: Comandos del Frontend
|
||||
title: Frontend Commands
|
||||
---
|
||||
|
||||
## Comandos útiles
|
||||
## Useful commands
|
||||
|
||||
### Iniciando la aplicación
|
||||
### Starting the app
|
||||
|
||||
```bash
|
||||
npx nx start twenty-front
|
||||
```
|
||||
|
||||
### Regenerar el esquema de graphql basado en el esquema API graphql
|
||||
### Regenerate graphql schema based on API graphql schema
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
|
||||
O
|
||||
OR
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate
|
||||
@@ -25,30 +25,30 @@ npx nx run twenty-front:graphql:generate
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lint # pasar --fix para corregir errores de lint
|
||||
npx nx run twenty-front:lint # pass --fix to fix lint errors
|
||||
```
|
||||
|
||||
## Traducciones
|
||||
## Translations
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lingui:extract
|
||||
npx nx run twenty-front:lingui:compile
|
||||
```
|
||||
|
||||
### Prueba
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:test # ejecutar pruebas con Jest
|
||||
npx nx run twenty-front:storybook:serve:dev # ejecutar Storybook
|
||||
npx nx run twenty-front:storybook:test # ejecutar pruebas # (requiere que yarn storybook:serve:dev esté en ejecución)
|
||||
npx nx run twenty-front:storybook:coverage # (requiere que yarn storybook:serve:dev esté en ejecución)
|
||||
npx nx run twenty-front:test # run jest tests
|
||||
npx nx run twenty-front:storybook:serve:dev # run storybook
|
||||
npx nx run twenty-front:storybook:test # run tests # (needs yarn storybook:serve:dev to be running)
|
||||
npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to be running)
|
||||
```
|
||||
|
||||
## Stack Tecnológico
|
||||
## Tech Stack
|
||||
|
||||
El proyecto tiene un stack limpio y sencillo, con un código boilerplate mínimo.
|
||||
The project has a clean and simple stack, with minimal boilerplate code.
|
||||
|
||||
**Aplicación**
|
||||
**App**
|
||||
|
||||
* [React](https://react.dev/)
|
||||
* [Apollo](https://www.apollographql.com/docs/)
|
||||
@@ -56,35 +56,35 @@ El proyecto tiene un stack limpio y sencillo, con un código boilerplate mínimo
|
||||
* [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
|
||||
* [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
**Pruebas**
|
||||
**Testing**
|
||||
|
||||
* [Jest](https://jestjs.io/)
|
||||
* [Storybook](https://storybook.js.org/)
|
||||
|
||||
**Herramientas**
|
||||
**Tooling**
|
||||
|
||||
* [Yarn](https://yarnpkg.com/)
|
||||
* [Craco](https://craco.js.org/docs/)
|
||||
* [ESLint](https://eslint.org/)
|
||||
|
||||
## Arquitectura
|
||||
## Architecture
|
||||
|
||||
### Enrutamiento
|
||||
### Routing
|
||||
|
||||
[React Router](https://reactrouter.com/) maneja el enrutamiento.
|
||||
[React Router](https://reactrouter.com/) handles the routing.
|
||||
|
||||
To avoid unnecessary [re-renders](/l/es/developers/contribute/capabilities/frontend-development/best-practices-front#managing-re-renders) all the routing logic is in a `useEffect` in `PageChangeEffect`.
|
||||
|
||||
### Gestión del Estado
|
||||
### State Management
|
||||
|
||||
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) maneja la gestión del estado.
|
||||
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) handles state management.
|
||||
|
||||
Ver [mejores prácticas](/l/es/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) para más información sobre la gestión del estado.
|
||||
See [best practices](/l/es/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) for more information on state management.
|
||||
|
||||
## Pruebas
|
||||
## Testing
|
||||
|
||||
[Jest](https://jestjs.io/) sirve como la herramienta para pruebas unitarias mientras [Storybook](https://storybook.js.org/) es para pruebas de componentes.
|
||||
[Jest](https://jestjs.io/) serves as the tool for unit testing while [Storybook](https://storybook.js.org/) is for component testing.
|
||||
|
||||
Jest es principalmente para probar funciones utilitarias, y no los componentes en sí mismos.
|
||||
Jest is mainly for testing utility functions, and not components themselves.
|
||||
|
||||
Storybook es para probar el comportamiento de componentes aislados, así como mostrar el sistema de diseño.
|
||||
Storybook is for testing the behavior of isolated components, as well as displaying the design system.
|
||||
|
||||
+34
-34
@@ -1,42 +1,42 @@
|
||||
---
|
||||
title: Atajos de teclado
|
||||
title: Hotkeys
|
||||
---
|
||||
|
||||
## Introducción
|
||||
## Introduction
|
||||
|
||||
Cuando necesitas escuchar una tecla de acceso rápido, normalmente usarías el evento `onKeyDown`.
|
||||
When you need to listen to a hotkey, you would normally use the `onKeyDown` event listener.
|
||||
|
||||
En `twenty-front`, sin embargo, podrías tener conflictos entre los mismos atajos de teclado utilizados en diferentes componentes, montados al mismo tiempo.
|
||||
In `twenty-front` however, you might have conflicts between same hotkeys that are used in different components, mounted at the same time.
|
||||
|
||||
Por ejemplo, si tienes una página que escucha la tecla Enter y un modal que escucha la tecla Enter, con un componente Select dentro de ese modal que también escucha la tecla Enter, podrías tener un conflicto cuando todos están montados al mismo tiempo.
|
||||
For example, if you have a page that listens for the Enter key, and a modal that listens for the Enter key, with a Select component inside that modal that listens for the Enter key, you might have a conflict when all are mounted at the same time.
|
||||
|
||||
## El gancho `useScopedHotkeys`
|
||||
## The `useScopedHotkeys` hook
|
||||
|
||||
Para manejar este problema, tenemos un gancho personalizado que hace posible escuchar atajos de teclado sin ningún conflicto.
|
||||
To handle this problem, we have a custom hook that makes it possible to listen to hotkeys without any conflict.
|
||||
|
||||
Lo colocas en un componente, y escuchará los atajos de teclado solo cuando el componente está montado Y cuando el **ámbito del atajo de teclado** especificado está activo.
|
||||
You place it in a component, and it will listen to the hotkeys only when the component is mounted AND when the specified **hotkey scope** is active.
|
||||
|
||||
## ¿Cómo escuchar atajos de teclado en la práctica?
|
||||
## How to listen for hotkeys in practice?
|
||||
|
||||
Hay dos pasos involucrados en configurar la escucha de atajos de teclado:
|
||||
There are two steps involved in setting up hotkey listening :
|
||||
|
||||
1. Establece el [ámbito del atajo de teclado](#qué-es-un-ambito-de-atajo-de-teclado-) que escuchará los atajos de teclado
|
||||
2. Usa el gancho `useScopedHotkeys` para escuchar atajos de teclado
|
||||
1. Set the [hotkey scope](#what-is-a-hotkey-scope-) that will listen to hotkeys
|
||||
2. Use the `useScopedHotkeys` hook to listen to hotkeys
|
||||
|
||||
Configurar los ámbitos de atajos de teclado es necesario incluso en páginas simples, porque otros elementos de la interfaz de usuario como el menú lateral o el menú de comandos también podrían escuchar atajos de teclado.
|
||||
Setting up hotkey scopes is required even in simple pages, because other UI elements like left menu or command menu might also listen to hotkeys.
|
||||
|
||||
## Casos de uso para atajos de teclado
|
||||
## Use cases for hotkeys
|
||||
|
||||
En general, tendrás dos casos de uso que requieren atajos de teclado:
|
||||
In general, you'll have two use cases that require hotkeys :
|
||||
|
||||
1. En una página o un componente montado en una página
|
||||
2. En un componente tipo modal que toma el enfoque debido a la acción del usuario
|
||||
1. In a page or a component mounted in a page
|
||||
2. In a modal-type component that takes the focus due to a user action
|
||||
|
||||
El segundo caso de uso puede ocurrir recursivamente: un desplegable en un modal, por ejemplo.
|
||||
The second use case can happen recursively : a dropdown in a modal for example.
|
||||
|
||||
### Escuchando atajos de teclado en una página
|
||||
### Listening to hotkeys in a page
|
||||
|
||||
Ejemplo:
|
||||
Example :
|
||||
|
||||
```tsx
|
||||
const PageListeningEnter = () => {
|
||||
@@ -71,11 +71,11 @@ const PageListeningEnter = () => {
|
||||
};
|
||||
```
|
||||
|
||||
### Escuchando atajos de teclado en un componente tipo modal
|
||||
### Listening to hotkeys in a modal-type component
|
||||
|
||||
Para este ejemplo utilizaremos un componente modal que escucha la tecla Escape para indicar a su padre que lo cierre.
|
||||
For this example we'll use a modal component that listens for the Escape key to tell its parent to close it.
|
||||
|
||||
Aquí la interacción del usuario está cambiando el ámbito.
|
||||
Here the user interaction is changing the scope.
|
||||
|
||||
```tsx
|
||||
const ExamplePageWithModal = () => {
|
||||
@@ -108,7 +108,7 @@ const ExamplePageWithModal = () => {
|
||||
};
|
||||
```
|
||||
|
||||
Luego, en el componente modal:
|
||||
Then in the modal component :
|
||||
|
||||
```tsx
|
||||
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
|
||||
@@ -131,15 +131,15 @@ It's important to use this pattern when you're not sure that just using a useEff
|
||||
|
||||
Those conflicts can be hard to debug, and it might happen more often than not with useEffects.
|
||||
|
||||
## ¿Qué es un ámbito de atajo de teclado?
|
||||
## What is a hotkey scope?
|
||||
|
||||
Un ámbito de atajo de teclado es una cadena que representa un contexto en el que los atajos de teclado están activos. Por lo general, se codifica como un enum.
|
||||
A hotkey scope is a string that represents a context in which the hotkeys are active. It is generally encoded as an enum.
|
||||
|
||||
Cuando cambias el ámbito del atajo de teclado, se habilitarán los atajos que están escuchando este ámbito y se deshabilitarán los atajos que escuchan otros ámbitos.
|
||||
When you change the hotkey scope, the hotkeys that are listening to this scope will be enabled and the hotkeys that are listening to other scopes will be disabled.
|
||||
|
||||
Solo puedes establecer un ámbito a la vez.
|
||||
You can set only one scope at a time.
|
||||
|
||||
Como ejemplo, los ámbitos de atajos de teclado para cada página se definen en el enum `PageHotkeyScope`:
|
||||
As an example, the hotkey scopes for each page are defined in the `PageHotkeyScope` enum:
|
||||
|
||||
```tsx
|
||||
export enum PageHotkeyScope {
|
||||
@@ -160,7 +160,7 @@ export enum PageHotkeyScope {
|
||||
}
|
||||
```
|
||||
|
||||
Internamente, el ámbito seleccionado se almacena en un estado de Recoil que se comparte en toda la aplicación:
|
||||
Internally, the currently selected scope is stored in a Recoil state that is shared across the application :
|
||||
|
||||
```tsx
|
||||
export const currentHotkeyScopeState = createState<HotkeyScope>({
|
||||
@@ -169,10 +169,10 @@ export const currentHotkeyScopeState = createState<HotkeyScope>({
|
||||
});
|
||||
```
|
||||
|
||||
¡Pero este estado de Recoil nunca debe manejarse manualmente! Veremos cómo usarlo en la siguiente sección.
|
||||
But this Recoil state should never be handled manually ! We'll see how to use it in the next section.
|
||||
|
||||
## ¿Cómo funciona internamente?
|
||||
## How is it working internally?
|
||||
|
||||
Hicimos un contenedor delgado sobre [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) que lo hace más eficiente y evita renders innecesarios.
|
||||
We made a thin wrapper on top of [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) that makes it more performant and avoids unnecessary re-renders.
|
||||
|
||||
También creamos un estado de Recoil para manejar el estado del ámbito de atajos de teclado y hacerlo disponible en toda la aplicación.
|
||||
We also create a Recoil state to handle the hotkey scope state and make it available everywhere in the application.
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Storybook
|
||||
description: Navegar por la biblioteca de componentes de interfaz de Twenty
|
||||
description: Browse Twenty's UI component library
|
||||
---
|
||||
|
||||
Ver nuestra biblioteca completa de componentes y documentación en Storybook.
|
||||
View our complete component library and documentation in Storybook.
|
||||
|
||||
[Abrir Storybook →](https://storybook.twenty.com)
|
||||
[Open Storybook →](https://storybook.twenty.com)
|
||||
|
||||
+58
-58
@@ -1,24 +1,24 @@
|
||||
---
|
||||
title: Guía de Estilo
|
||||
title: Style Guide
|
||||
---
|
||||
|
||||
Este documento incluye las reglas a seguir al escribir código.
|
||||
This document includes the rules to follow when writing code.
|
||||
|
||||
El objetivo aquí es tener una base de código coherente, que sea fácil de leer y de mantener.
|
||||
The goal here is to have a consistent codebase, which is easy to read and easy to maintain.
|
||||
|
||||
Para esto, es mejor ser un poco más detallado que ser demasiado conciso.
|
||||
For this, it's better to be a bit more verbose than to be too concise.
|
||||
|
||||
Ten siempre en cuenta que la gente lee código más a menudo de lo que lo escribe, especialmente en un proyecto de código abierto, donde cualquiera puede contribuir.
|
||||
Always keep in mind that people read code more often than they write it, specially on an open source project, where anyone can contribute.
|
||||
|
||||
Hay muchas reglas que no están definidas aquí, pero que son verificadas automáticamente por linters.
|
||||
There are a lot of rules that are not defined here, but that are automatically checked by linters.
|
||||
|
||||
## React
|
||||
|
||||
### Usar componentes funcionales
|
||||
### Use functional components
|
||||
|
||||
Siempre usa componentes funcionales TSX.
|
||||
Always use TSX functional components.
|
||||
|
||||
No uses `import` por defecto con `const`, porque es más difícil de leer y más difícil de importar con el autocompletado de código.
|
||||
Do not use default `import` with `const`, because it's harder to read and harder to import with code completion.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, harder to read, harder to import with code completion
|
||||
@@ -34,11 +34,11 @@ export function MyComponent() {
|
||||
};
|
||||
```
|
||||
|
||||
### "Props"
|
||||
### Props
|
||||
|
||||
Crea el tipo de las props y llámalo `(NombreDelComponente)Props` si no hay necesidad de exportarlo.
|
||||
Create the type of the props and call it `(ComponentName)Props` if there's no need to export it.
|
||||
|
||||
Usa la desestructuración de props.
|
||||
Use props destructuring.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, no type
|
||||
@@ -52,7 +52,7 @@ type MyComponentProps = {
|
||||
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
|
||||
```
|
||||
|
||||
#### Evita usar `React.FC` o `React.FunctionComponent` para definir tipos de props
|
||||
#### Refrain from using `React.FC` or `React.FunctionComponent` to define prop types
|
||||
|
||||
```tsx
|
||||
/* ❌ - Bad, defines the component type annotations with `FC`
|
||||
@@ -67,10 +67,10 @@ const EmailField: React.FC<{
|
||||
```
|
||||
|
||||
```tsx
|
||||
/* ✅ - Bueno, se define explícitamente un tipo (OwnProps) separado para las
|
||||
* props del componente
|
||||
* - Este método no incluye automáticamente la prop children. Si
|
||||
* quieres incluirla, debes especificarla en OwnProps.
|
||||
/* ✅ - Good, a separate type (OwnProps) is explicitly defined for the
|
||||
* component's props
|
||||
* - This method doesn't automatically include the children prop. If
|
||||
* you want to include it, you have to specify it in OwnProps.
|
||||
*/
|
||||
type EmailFieldProps = {
|
||||
value: string;
|
||||
@@ -81,9 +81,9 @@ const EmailField = ({ value }: EmailFieldProps) => (
|
||||
);
|
||||
```
|
||||
|
||||
#### Sin Propagación de una sola variable de Props en Elementos JSX
|
||||
#### No Single Variable Prop Spreading in JSX Elements
|
||||
|
||||
Evita usar la propagación de una sola variable de props en elementos JSX, como `{...props}`. Esta práctica a menudo resulta en un código que es menos legible y más difícil de mantener porque no está claro qué props está recibiendo el componente.
|
||||
Avoid using single variable prop spreading in JSX elements, like `{...props}`. This practice often results in code that is less readable and harder to maintain because it's unclear which props the component is receiving.
|
||||
|
||||
```tsx
|
||||
/* ❌ - Bad, spreads a single variable prop into the underlying component
|
||||
@@ -94,23 +94,23 @@ const MyComponent = (props: OwnProps) => {
|
||||
```
|
||||
|
||||
```tsx
|
||||
/* ✅ - Bueno, enumera explícitamente todas las props
|
||||
* - Mejora la legibilidad y la mantenibilidad
|
||||
/* ✅ - Good, Explicitly lists all props
|
||||
* - Enhances readability and maintainability
|
||||
*/
|
||||
const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
|
||||
return <OtherComponent {...{ prop1, prop2, prop3 }} />;
|
||||
};
|
||||
```
|
||||
|
||||
Razonamiento:
|
||||
Rationale:
|
||||
|
||||
* A simple vista, es más claro qué props se está pasando, lo que hace que sea más fácil de entender y mantener.
|
||||
* Ayuda a prevenir el acoplamiento estricto entre componentes mediante sus props.
|
||||
* Las herramientas de linting facilitan la identificación de props mal escritas o sin uso al listar props explícitamente.
|
||||
* At a glance, it's clearer which props the code passes down, making it easier to understand and maintain.
|
||||
* It helps to prevent tight coupling between components via their props.
|
||||
* Linting tools make it easier to identify misspelled or unused props when you list props explicitly.
|
||||
|
||||
## JavaScript
|
||||
|
||||
### Usar el operador de fusión nula `??`
|
||||
### Use nullish-coalescing operator `??`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, can return 'default' even if value is 0 or ''
|
||||
@@ -120,21 +120,21 @@ const value = process.env.MY_VALUE || 'default';
|
||||
const value = process.env.MY_VALUE ?? 'default';
|
||||
```
|
||||
|
||||
### Usar encadenamiento opcional `?.`
|
||||
### Use optional chaining `?.`
|
||||
|
||||
```tsx
|
||||
// ❌ Malo
|
||||
// ❌ Bad
|
||||
onClick && onClick();
|
||||
|
||||
// ✅ Bueno
|
||||
// ✅ Good
|
||||
onClick?.();
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
### Usar `type` en lugar de `interface`
|
||||
### Use `type` instead of `interface`
|
||||
|
||||
Siempre usa `type` en lugar de `interface`, porque casi siempre se superponen y `type` es más flexible.
|
||||
Always use `type` instead of `interface`, because they almost always overlap, and `type` is more flexible.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
@@ -148,11 +148,11 @@ type MyType = {
|
||||
};
|
||||
```
|
||||
|
||||
### Usar literales de cadena en lugar de enums
|
||||
### Use string literals instead of enums
|
||||
|
||||
[Los literales de cadena](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) son la manera preferida para manejar valores tipo enum en TypeScript. Son más fáciles de extender con Pick y Omit, y ofrecen una mejor experiencia de desarrollo, especialmente con la autocompletación de código.
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, specially with code completion.
|
||||
|
||||
Puedes ver por qué TypeScript recomienda evitar enums [aquí](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
|
||||
You can see why TypeScript recommends avoiding enums [here](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, utilizes an enum
|
||||
@@ -171,13 +171,13 @@ let color = Color.Red;
|
||||
let color: "red" | "green" | "blue" = "red";
|
||||
```
|
||||
|
||||
#### GraphQL y bibliotecas internas
|
||||
#### GraphQL and internal libraries
|
||||
|
||||
Deberías usar enums que genera el codegen de GraphQL.
|
||||
You should use enums that GraphQL codegen generates.
|
||||
|
||||
También es mejor usar un enum al usar una biblioteca interna, para que la biblioteca interna no tenga que exponer un tipo de literal de cadena que no está relacionado con la API interna.
|
||||
It's also better to use an enum when using an internal library, so the internal library doesn't have to expose a string literal type that is not related to the internal API.
|
||||
|
||||
Ejemplo:
|
||||
Example:
|
||||
|
||||
```TSX
|
||||
const {
|
||||
@@ -190,11 +190,11 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
);
|
||||
```
|
||||
|
||||
## Estilo
|
||||
## Styling
|
||||
|
||||
### Usar StyledComponents
|
||||
### Use StyledComponents
|
||||
|
||||
Estiliza los componentes con [styled-components](https://emotion.sh/docs/styled).
|
||||
Style the components with [styled-components](https://emotion.sh/docs/styled).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
@@ -208,7 +208,7 @@ const StyledTitle = styled.div`
|
||||
`;
|
||||
```
|
||||
|
||||
Prefija los componentes estilizados con "Styled" para diferenciarlos de los componentes "reales".
|
||||
Prefix styled components with "Styled" to differentiate them from "real" components.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
@@ -224,17 +224,17 @@ const StyledTitle = styled.div`
|
||||
`;
|
||||
```
|
||||
|
||||
### Tematización
|
||||
### Theming
|
||||
|
||||
Utilizar el tema para la mayor parte de la estilización de los componentes es el enfoque preferido.
|
||||
Utilizing the theme for the majority of component styling is the preferred approach.
|
||||
|
||||
#### Unidades de medida
|
||||
#### Units of measurement
|
||||
|
||||
Evita usar valores `px` o `rem` directamente dentro de los componentes estilizados. Los valores necesarios suelen estar ya definidos en el tema, por lo que se recomienda usar el tema para estos fines.
|
||||
Avoid using `px` or `rem` values directly within the styled components. The necessary values are generally already defined in the theme, so it’s recommended to make use of the theme for these purposes.
|
||||
|
||||
#### Colores
|
||||
#### Colors
|
||||
|
||||
Abstente de introducir nuevos colores; en su lugar, utiliza la paleta existente del tema. Si hay una situación en la que la paleta no se ajusta, deja un comentario para que el equipo pueda corregirlo.
|
||||
Refrain from introducing new colors; instead, use the existing palette from the theme. Should there be a situation where the palette does not align, please leave a comment so that the team can rectify it.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, directly specifies style values without utilizing the theme
|
||||
@@ -258,9 +258,9 @@ const StyledButton = styled.button`
|
||||
`;
|
||||
```
|
||||
|
||||
## Aplicando No-Type Imports
|
||||
## Enforcing No-Type Imports
|
||||
|
||||
Evita las importaciones de tipo. Para reforzar este estándar, una regla de ESLint verifica y reporta cualquier importación de tipo. Esto ayuda a mantener la consistencia y la legibilidad en el código TypeScript.
|
||||
Avoid type imports. To enforce this standard, an ESLint rule checks for and reports any type imports. This helps maintain consistency and readability in the TypeScript code.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
@@ -273,18 +273,18 @@ import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
```
|
||||
|
||||
### Por qué No-Type Imports
|
||||
### Why No-Type Imports
|
||||
|
||||
* **Consistencia**: Al evitar las importaciones de tipo y usar un solo enfoque tanto para las importaciones de tipo como de valor, la base de código se mantiene consistente en su estilo de importación de módulos.
|
||||
* **Consistency**: By avoiding type imports and using a single approach for both type and value imports, the codebase remains consistent in its module import style.
|
||||
|
||||
* **Legibilidad**: Las no-importaciones de tipo mejoran la legibilidad del código al dejar claro cuándo se están importando valores o tipos. Esto reduce la ambigüedad y hace más fácil entender el propósito de los símbolos importados.
|
||||
* **Readability**: No-type imports improve code readability by making it clear when you're importing values or types. This reduces ambiguity and makes it easier to understand the purpose of imported symbols.
|
||||
|
||||
* **Mantenibilidad**: Mejora la mantenibilidad de la base de código porque los desarrolladores pueden identificar y localizar importaciones solo de tipo al revisar o modificar el código.
|
||||
* **Maintainability**: It enhances codebase maintainability because developers can identify and locate type-only imports when reviewing or modifying code.
|
||||
|
||||
### Regla de ESLint
|
||||
### ESLint Rule
|
||||
|
||||
Una regla de ESLint, `@typescript-eslint/consistent-type-imports`, impone el estándar de no usar "type" en las importaciones. Esta regla generará errores o advertencias sobre cualquier violación de importación de tipo.
|
||||
An ESLint rule, `@typescript-eslint/consistent-type-imports`, enforces the no-type import standard. This rule will generate errors or warnings for any type import violations.
|
||||
|
||||
Por favor, ten en cuenta que esta regla específicamente aborda extraños casos límite donde ocurren importaciones de tipo no intencionadas. TypeScript en sí mismo desaconseja esta práctica, como se menciona en las [notas de lanzamiento de TypeScript 3.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). En la mayoría de situaciones, no deberías necesitar usar importaciones solo de tipo.
|
||||
Please note that this rule specifically addresses rare edge cases where unintentional type imports occur. TypeScript itself discourages this practice, as mentioned in the [TypeScript 3.8 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). In most situations, you should not need to use type-only imports.
|
||||
|
||||
Para asegurarte de que tu código cumpla con esta regla, asegúrate de ejecutar ESLint como parte de tu flujo de trabajo de desarrollo.
|
||||
To ensure your code complies with this rule, make sure to run ESLint as part of your development workflow.
|
||||
|
||||
+33
-33
@@ -1,59 +1,59 @@
|
||||
---
|
||||
title: Trabajar con Figma
|
||||
info: Aprende cómo puedes colaborar con Twenty en Figma
|
||||
title: Work with Figma
|
||||
info: Learn how you can collaborate with Twenty's Figma
|
||||
---
|
||||
|
||||
Figma es una herramienta de diseño de interfaces colaborativa que ayuda a superar la barrera de comunicación entre diseñadores y desarrolladores.
|
||||
Esta guía explica cómo puedes colaborar con Figma.
|
||||
Figma is a collaborative interface design tool that aids in bridging the communication barrier between designers and developers.
|
||||
This guide explains how you can collaborate with Figma.
|
||||
|
||||
## Acceso
|
||||
## Access
|
||||
|
||||
1. **Accede al enlace compartido:** Puedes acceder al archivo de Figma del proyecto [aquí](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty).
|
||||
2. **Inicia sesión:** Si aún no has iniciado sesión, Figma te pedirá que lo hagas.
|
||||
Las características clave solo están disponibles para usuarios registrados, como el modo desarrollador y la capacidad de seleccionar un marco dedicado.
|
||||
1. **Access the shared link:** You can access the project's Figma file [here](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty).
|
||||
2. **Sign in:** If you're not already signed in, Figma will prompt you to do so.
|
||||
Key features are only available to logged-in users, such as the developer mode and the ability to select a dedicated frame.
|
||||
|
||||
<Warning>
|
||||
No podrás colaborar efectivamente sin una cuenta.
|
||||
You will not be able to collaborate effectively without an account.
|
||||
</Warning>
|
||||
|
||||
## Estructura de Figma
|
||||
## Figma structure
|
||||
|
||||
En la barra lateral izquierda, puedes acceder a las diferentes páginas del Figma de Twenty. Así es como están organizadas:
|
||||
On the left sidebar, you can access the different pages of Twenty's Figma. This is how they're organized:
|
||||
|
||||
* **Página de componentes:** Esta es la primera página. El diseñador la utiliza para crear y organizar los elementos de diseño reutilizables que se usan en todo el archivo de diseño. Por ejemplo, botones, íconos, símbolos u otros componentes reutilizables. Sirve para mantener la consistencia a lo largo del diseño.
|
||||
* **Página principal:** La segunda página es la página principal, que muestra la interfaz de usuario completa del proyecto. Puedes presionar ***Play*** para usar el prototipo completo de la aplicación.
|
||||
* **Páginas de características:** Las otras páginas están típicamente dedicadas a características en progreso. Contienen el diseño de características específicas o módulos de la aplicación o sitio web. Normalmente, aún están en desarrollo.
|
||||
* **Components page:** This is the first page. The designer uses it to create and organize the reusable design elements used throughout the design file. For example, buttons, icons, symbols, or any other reusable components. It serves to maintain consistency across the design.
|
||||
* **Main page:** The second page is the main page, which shows the complete user interface of the project. You can press ***Play*** to use the full app prototype.
|
||||
* **Features pages:** The other pages are typically dedicated to features in progress. They contain the design of specific features or modules of the application or website. They are typically still in progress.
|
||||
|
||||
## Consejos útiles
|
||||
## Useful Tips
|
||||
|
||||
Con acceso de solo lectura, no puedes editar el diseño, pero puedes acceder a todas las características que serán útiles para convertir los diseños en código.
|
||||
With read-only access, you can't edit the design, but you can access all features that will be useful to convert the designs into code.
|
||||
|
||||
### Usa el modo Dev
|
||||
### Use the Dev mode
|
||||
|
||||
El Modo Dev de Figma mejora la productividad de los desarrolladores al proporcionar una navegación fácil por el diseño, una gestión eficaz de assets, herramientas de comunicación eficientes, integraciones de caja de herramientas, fragmentos de código rápidos e información clave de capas, superando la brecha entre diseño y desarrollo. Puedes aprender más sobre el Modo Dev [aquí](https://www.figma.com/dev-mode/).
|
||||
Figma's Dev Mode enhances developers' productivity by providing easy design navigation, effective asset management, efficient communication tools, toolbox integrations, quick code snippets, and key layer information, bridging the gap between design and development. You can learn more about Dev Mode [here](https://www.figma.com/dev-mode/).
|
||||
|
||||
Cambia al modo "Desarrollador" en la parte derecha de la barra de herramientas para ver las especificaciones de diseño, copiar CSS y acceder a los assets.
|
||||
Switch to the "Developer" mode in the right part of the toolbar to see design specs, copy CSS, and access assets.
|
||||
|
||||
### Usar el prototipo
|
||||
### Use the Prototype
|
||||
|
||||
Haz clic en cualquier elemento del lienzo y presiona el botón "Play" en el extremo superior derecho de la interfaz para acceder a la vista del prototipo. El modo de prototipo te permite interactuar con el diseño como si fuera el producto final. Demuestra el flujo entre pantallas y cómo los elementos de la interfaz, como botones, enlaces o menús, se comportan al interactuar con ellos.
|
||||
Click on any element on the canvas and press the “Play” button at the top right edge of the interface to access the prototype view. Prototype mode allows you to interact with the design as if it were the final product. It demonstrates the flow between screens and how interface elements like buttons, links, or menus behave when interacted with.
|
||||
|
||||
1. **Entendiendo las transiciones y animaciones:** En el modo Prototipo, puedes ver cualquier transición o animación añadida por un diseñador entre pantallas o elementos de UI, proporcionando instrucciones visuales claras a los desarrolladores sobre la conducta y el estilo intencionados.
|
||||
2. **Aclaración de implementación:** Un prototipo también puede ayudar a reducir las ambigüedades. Los desarrolladores pueden interactuar con él para obtener una mejor comprensión de la funcionalidad o apariencia de elementos particulares.
|
||||
1. **Understanding transitions and animations:** In the Prototype mode, you can view any transitions or animations added by a designer between screens or UI elements, providing clear visual instructions to developers on the intended behavior and style.
|
||||
2. **Implementation clarification:** A prototype can also help reduce ambiguities. Developers can interact with it to gain a better understanding of the functionality or appearance of particular elements.
|
||||
|
||||
Para más detalles y orientación sobre el aprendizaje de la plataforma Figma, puedes visitar la [Documentación Oficial de Figma](https://help.figma.com/hc/en-us).
|
||||
For more comprehensive details and guidance on learning the Figma platform, you can visit the official [Figma Documentation](https://help.figma.com/hc/en-us).
|
||||
|
||||
### Medir distancias
|
||||
### Measure distances
|
||||
|
||||
Selecciona un elemento, mantén presionada la tecla `Option` (Mac) o `Alt` (Windows), luego pasa el cursor sobre otro elemento para ver la distancia entre ellos.
|
||||
Select an element, hold `Option` key (Mac) or `Alt` key (Windows), then hover over another element to see the distance between them.
|
||||
|
||||
### Extensión Figma para VSCode (Recomendado)
|
||||
### Figma extension for VSCode (Recommended)
|
||||
|
||||
[Figma para VS Code](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension)
|
||||
te permite navegar e inspeccionar archivos de diseño, colaborar con diseñadores, rastrear cambios y acelerar la implementación, todo sin salir de tu editor de texto.
|
||||
Forma parte de nuestras extensiones recomendadas.
|
||||
[Figma for VS Code](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension)
|
||||
lets you navigate and inspect design files, collaborate with designers, track changes, and speed up implementation - all without leaving your text editor.
|
||||
It's part of our recommended extensions.
|
||||
|
||||
## Colaboración
|
||||
## Collaboration
|
||||
|
||||
1. **Uso de Comentarios:** Puedes utilizar la función de comentarios haciendo clic en el ícono de burbuja en la parte izquierda de la barra de herramientas.
|
||||
2. **Chat de Cursor:** Una característica agradable de Figma es el Chat de Cursor. Simplemente presiona `;` en Mac y `/` en Windows para enviar un mensaje si ves a alguien más usando Figma al mismo tiempo que tú.
|
||||
1. **Using Comments:** You are welcome to use the comment feature by clicking on the bubble icon in the left part of the toolbar.
|
||||
2. **Cursor chat:** A nice feature of Figma is the Cursor chat. Just press `;` on Mac and `/` on Windows to send a message if you see someone else using Figma as the same time as you.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: Configuración Local
|
||||
description: La guía para los colaboradores (o desarrolladores curiosos) que quieren ejecutar Twenty localmente.
|
||||
title: Local Setup
|
||||
description: The guide for contributors (or curious developers) who want to run Twenty locally.
|
||||
---
|
||||
|
||||
## Prerrequisitos
|
||||
## Prerequisites
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux y MacOS">
|
||||
Antes de que puedas instalar y usar Twenty, asegúrate de instalar lo siguiente en tu computadora:
|
||||
<Tab title="Linux and MacOS">
|
||||
Before you can install and use Twenty, make sure you install the following on your computer:
|
||||
|
||||
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [Node v24.5.0](https://nodejs.org/en/download)
|
||||
@@ -15,25 +15,25 @@ description: La guía para los colaboradores (o desarrolladores curiosos) que qu
|
||||
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
|
||||
|
||||
<Warning>
|
||||
`npm` no funcionará, deberías usar `yarn` en su lugar. Yarn ahora se envía con Node.js, por lo que no necesitas instalarlo por separado.
|
||||
Solo tienes que ejecutar `corepack enable` para habilitar Yarn si aún no lo has hecho.
|
||||
`npm` won't work, you should use `yarn` instead. Yarn is now shipped with Node.js, so you don't need to install it separately.
|
||||
You only have to run `corepack enable` to enable Yarn if you haven't done it yet.
|
||||
</Warning>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Windows (WSL)">
|
||||
1. Instalar WSL
|
||||
Abre PowerShell como Administrador y ejecuta:
|
||||
1. Install WSL
|
||||
Open PowerShell as Administrator and run:
|
||||
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
|
||||
Ahora deberías ver un aviso para reiniciar tu computadora. Si no, reiníciala manualmente.
|
||||
You should now see a prompt to restart your computer. If not, restart it manually.
|
||||
|
||||
Al reiniciar, se abrirá una ventana de PowerShell e instalará Ubuntu. Esto puede tomar algo de tiempo.
|
||||
Verás un aviso para crear un nombre de usuario y contraseña para tu instalación de Ubuntu.
|
||||
Upon restart, a powershell window will open and install Ubuntu. This may take up some time.
|
||||
You'll see a prompt to create a username and password for your Ubuntu installation.
|
||||
|
||||
2. Instalar y configurar git
|
||||
2. Install and configure git
|
||||
|
||||
```bash
|
||||
sudo apt-get install git
|
||||
@@ -43,10 +43,10 @@ description: La guía para los colaboradores (o desarrolladores curiosos) que qu
|
||||
git config --global user.email "[email protected]"
|
||||
```
|
||||
|
||||
3. Instalar nvm, node.js y yarn
|
||||
3. Install nvm, node.js and yarn
|
||||
|
||||
<Warning>
|
||||
Usa `nvm` para instalar la versión correcta de `node`. El `.nvmrc` asegura que todos los colaboradores usen la misma versión.
|
||||
Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contributors use the same version.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
@@ -55,7 +55,7 @@ description: La guía para los colaboradores (o desarrolladores curiosos) que qu
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
```
|
||||
|
||||
Cierra y vuelve a abrir tu terminal para usar nvm. Luego ejecuta los siguientes comandos.
|
||||
Close and reopen your terminal to use nvm. Then run the following commands.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -70,13 +70,13 @@ description: La guía para los colaboradores (o desarrolladores curiosos) que qu
|
||||
|
||||
---
|
||||
|
||||
## Paso 1: Clonar con Git
|
||||
## Step 1: Git Clone
|
||||
|
||||
En tu terminal, ejecuta el siguiente comando.
|
||||
In your terminal, run the following command.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="SSH (Recomendado)">
|
||||
Si aún no has configurado claves SSH, puedes aprender cómo hacerlo [aquí](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
|
||||
<Tab title="SSH (Recommended)">
|
||||
If you haven't already set up SSH keys, you can learn how to do so [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
|
||||
|
||||
```bash
|
||||
git clone [email protected]:twentyhq/twenty.git
|
||||
@@ -90,36 +90,36 @@ En tu terminal, ejecuta el siguiente comando.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Paso 2: Ubícate en la raíz
|
||||
## Step 2: Position yourself at the root
|
||||
|
||||
```bash
|
||||
cd twenty
|
||||
```
|
||||
|
||||
Debes ejecutar todos los comandos de los siguientes pasos desde la raíz del proyecto.
|
||||
You should run all commands in the following steps from the root of the project.
|
||||
|
||||
## Paso 3: Configurar una Base de Datos PostgreSQL
|
||||
## Step 3: Set up a PostgreSQL Database
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**Opción 1 (preferido):** Para aprovisionar tu base de datos localmente:
|
||||
Usa el siguiente enlace para instalar PostgreSQL en tu máquina Linux: [Instalación de PostgreSQL](https://www.postgresql.org/download/linux/)
|
||||
**Option 1 (preferred):** To provision your database locally:
|
||||
Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
|
||||
Nota: Puede que necesites agregar `sudo -u postgres` antes del comando `psql` para evitar errores de permisos.
|
||||
Note: You might need to add `sudo -u postgres` to the command before `psql` to avoid permission errors.
|
||||
|
||||
**Opción 2:** Si tienes Docker instalado:
|
||||
**Option 2:** If you have docker installed:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker postgres-on-docker
|
||||
make postgres-on-docker
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Mac OS">
|
||||
**Opción 1 (preferido):** Para aprovisionar tu base de datos localmente con `brew`:
|
||||
**Option 1 (preferred):** To provision your database locally with `brew`:
|
||||
|
||||
```bash
|
||||
brew install postgresql@16
|
||||
@@ -128,16 +128,16 @@ Debes ejecutar todos los comandos de los siguientes pasos desde la raíz del pro
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
|
||||
Puedes verificar si el servidor PostgreSQL está corriendo ejecutando:
|
||||
You can verify if the PostgreSQL server is running by executing:
|
||||
|
||||
```bash
|
||||
brew services list
|
||||
```
|
||||
|
||||
El instalador puede que no cree el usuario `postgres` por defecto al instalar
|
||||
vía Homebrew en macOS. En cambio, crea un rol de PostgreSQL que coincide con tu
|
||||
nombre de usuario de macOS (por ejemplo, "john").
|
||||
Para comprobar y crear el usuario `postgres` si es necesario, sigue estos pasos:
|
||||
The installer might not create the `postgres` user by default when installing
|
||||
via Homebrew on MacOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
username (e.g., "john").
|
||||
To check and create the `postgres` user if necessary, follow these steps:
|
||||
|
||||
```bash
|
||||
# Connect to PostgreSQL
|
||||
@@ -146,14 +146,14 @@ Debes ejecutar todos los comandos de los siguientes pasos desde la raíz del pro
|
||||
psql -U $(whoami) -d postgres
|
||||
```
|
||||
|
||||
Una vez en el comando psql (postgres=#), ejecuta:
|
||||
Once at the psql prompt (postgres=#), run:
|
||||
|
||||
```bash
|
||||
# List existing PostgreSQL roles
|
||||
\du
|
||||
```
|
||||
|
||||
Verás una salida similar a:
|
||||
You'll see output similar to:
|
||||
|
||||
```bash
|
||||
Role name | Attributes | Member of
|
||||
@@ -161,105 +161,98 @@ Debes ejecutar todos los comandos de los siguientes pasos desde la raíz del pro
|
||||
john | Superuser | {}
|
||||
```
|
||||
|
||||
Si no ves un rol `postgres` listado, procede al siguiente paso.
|
||||
Crea el rol `postgres` manualmente:
|
||||
If you do not see a `postgres` role listed, proceed to the next step.
|
||||
Create the `postgres` role manually:
|
||||
|
||||
```bash
|
||||
CREATE ROLE postgres WITH SUPERUSER LOGIN;
|
||||
```
|
||||
|
||||
Esto crea un rol de superusuario llamado `postgres` con acceso de inicio de sesión.
|
||||
This creates a superuser role named `postgres` with login access.
|
||||
|
||||
**Option 2:** If you have docker installed:
|
||||
|
||||
```bash
|
||||
Nombre del rol | Atributos | Miembro de
|
||||
-----------+-------------+-----------
|
||||
postgres | Superuser | {}
|
||||
john | Superuser | {}
|
||||
```
|
||||
|
||||
**Opción 2:** Si tienes Docker instalado:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker postgres-on-docker
|
||||
make postgres-on-docker
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Windows (WSL)">
|
||||
Todos los siguientes pasos deben ejecutarse en la terminal de WSL (dentro de tu máquina virtual)
|
||||
All the following steps are to be run in the WSL terminal (within your virtual machine)
|
||||
|
||||
**Opción 1:** Para aprovisionar tu PostgreSQL localmente:
|
||||
Usa el siguiente enlace para instalar PostgreSQL en tu máquina virtual Linux: [Instalación de PostgreSQL](https://www.postgresql.org/download/linux/)
|
||||
**Option 1:** To provision your Postgresql locally:
|
||||
Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
|
||||
Nota: Puede que necesites agregar `sudo -u postgres` antes del comando `psql` para evitar errores de permisos.
|
||||
Note: You might need to add `sudo -u postgres` to the command before `psql` to avoid permission errors.
|
||||
|
||||
**Opción 2:** Si tienes Docker instalado:
|
||||
Ejecutar Docker en WSL agrega una capa extra de complejidad.
|
||||
Solo usa esta opción si estás cómodo con los pasos extras involucrados, incluyendo activar [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
|
||||
**Option 2:** If you have docker installed:
|
||||
Running Docker on WSL adds an extra layer of complexity.
|
||||
Only use this option if you are comfortable with the extra steps involved, including turning on [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker postgres-on-docker
|
||||
make postgres-on-docker
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Ahora puedes acceder a la base de datos en [localhost:5432](localhost:5432), con usuario `postgres` y contraseña `postgres`.
|
||||
You can now access the database at [localhost:5432](localhost:5432), with user `postgres` and password `postgres` .
|
||||
|
||||
## Paso 4: Configurar una Base de Datos Redis (cache)
|
||||
## Step 4: Set up a Redis Database (cache)
|
||||
|
||||
Twenty requiere un caché de redis para proporcionar el mejor rendimiento
|
||||
Twenty requires a redis cache to provide the best performance
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**Opción 1:** Para aprovisionar tu Redis localmente:
|
||||
Usa el siguiente enlace para instalar Redis en tu máquina Linux: [Instalación de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
|
||||
**Option 1:** To provision your Redis locally:
|
||||
Use the following link to install Redis on your Linux machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
|
||||
|
||||
**Opción 2:** Si tienes Docker instalado:
|
||||
**Option 2:** If you have docker installed:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker redis-on-docker
|
||||
make redis-on-docker
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Mac OS">
|
||||
**Opción 1 (preferido):** Para aprovisionar tu Redis localmente con `brew`:
|
||||
**Option 1 (preferred):** To provision your Redis locally with `brew`:
|
||||
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
|
||||
Inicia tu servidor redis:
|
||||
Start your redis server:
|
||||
`brew services start redis`
|
||||
|
||||
**Opción 2:** Si tienes Docker instalado:
|
||||
**Option 2:** If you have docker installed:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker redis-on-docker
|
||||
make redis-on-docker
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Windows (WSL)">
|
||||
**Opción 1:** Para aprovisionar tu Redis localmente:
|
||||
Usa el siguiente enlace para instalar Redis en tu máquina virtual Linux: [Instalación de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
|
||||
**Option 1:** To provision your Redis locally:
|
||||
Use the following link to install Redis on your Linux virtual machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
|
||||
|
||||
**Opción 2:** Si tienes Docker instalado:
|
||||
**Option 2:** If you have docker installed:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker redis-on-docker
|
||||
make redis-on-docker
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Si necesitas una interfaz gráfica de cliente, recomendamos [Redis Insight](https://redis.io/insight/) (versión gratuita disponible)
|
||||
If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/) (free version available)
|
||||
|
||||
## Paso 5: Configurar las variables de entorno
|
||||
## Step 5: Setup environment variables
|
||||
|
||||
Usa variables de entorno o archivos `.env` para configurar tu proyecto. Más información [aquí](/l/es/developers/self-host/capabilities/setup)
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/l/es/developers/self-host/capabilities/setup)
|
||||
|
||||
Copia los archivos `.env.example` en `/front` y `/server`:
|
||||
Copy the `.env.example` files in `/front` and `/server`:
|
||||
|
||||
```bash
|
||||
cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env
|
||||
@@ -267,29 +260,29 @@ cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
|
||||
```
|
||||
|
||||
<Info>
|
||||
**Modo de múltiples espacios de trabajo:** De forma predeterminada, Twenty se ejecuta en modo de un solo espacio de trabajo, donde solo se puede crear un espacio de trabajo. Para habilitar la compatibilidad con múltiples espacios de trabajo (útil para probar funciones basadas en subdominios), establece `IS_MULTIWORKSPACE_ENABLED=true` en el archivo `.env` de tu servidor. Consulta [Modo de múltiples espacios de trabajo](/l/es/developers/self-host/capabilities/setup#multi-workspace-mode) para más detalles.
|
||||
**Multi-Workspace Mode:** By default, Twenty runs in single-workspace mode where only one workspace can be created. To enable multi-workspace support (useful for testing subdomain-based features), set `IS_MULTIWORKSPACE_ENABLED=true` in your server `.env` file. See [Multi-Workspace Mode](/l/es/developers/self-host/capabilities/setup#multi-workspace-mode) for details.
|
||||
</Info>
|
||||
|
||||
## Paso 6: Instalación de dependencias
|
||||
## Step 6: Installing dependencies
|
||||
|
||||
Para compilar el servidor de Twenty e ingresar algunos datos en tu base de datos, ejecuta el siguiente comando:
|
||||
To build Twenty server and seed some data into your database, run the following command:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
Ten en cuenta que `npm` o `pnpm` no funcionarán
|
||||
Note that `npm` or `pnpm` won't work
|
||||
|
||||
## Paso 7: Ejecutar el proyecto
|
||||
## Step 7: Running the project
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
Dependiendo de tu distribución de Linux, el servidor Redis podría iniciarse automáticamente.
|
||||
Si no, revisa la [guía de instalación de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) para tu distribución.
|
||||
Depending on your Linux distribution, Redis server might be started automatically.
|
||||
If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro.
|
||||
</Tab>
|
||||
|
||||
<Tab title="Mac OS">
|
||||
Redis ya debería estar funcionando. Si no, ejecuta:
|
||||
Redis should already be running. If not, run:
|
||||
|
||||
```bash
|
||||
brew services start redis
|
||||
@@ -297,18 +290,18 @@ Ten en cuenta que `npm` o `pnpm` no funcionarán
|
||||
</Tab>
|
||||
|
||||
<Tab title="Windows (WSL)">
|
||||
Dependiendo de tu distribución de Linux, el servidor Redis podría iniciarse automáticamente.
|
||||
Si no es así, consulte la [guía de instalación de Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) para su distribución.
|
||||
Depending on your Linux distribution, Redis server might be started automatically.
|
||||
If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Configure su base de datos con el siguiente comando:
|
||||
Set up your database with the following command:
|
||||
|
||||
```bash
|
||||
npx nx database:reset twenty-server
|
||||
```
|
||||
|
||||
Inicie el servidor, el trabajador y los servicios frontend:
|
||||
Start the server, the worker and the frontend services:
|
||||
|
||||
```bash
|
||||
npx nx start twenty-server
|
||||
@@ -316,25 +309,25 @@ npx nx worker twenty-server
|
||||
npx nx start twenty-front
|
||||
```
|
||||
|
||||
Alternativamente, puede iniciar todos los servicios a la vez:
|
||||
Alternatively, you can start all services at once:
|
||||
|
||||
```bash
|
||||
npx nx start
|
||||
```
|
||||
|
||||
## Paso 8: Use Twenty
|
||||
## Step 8: Use Twenty
|
||||
|
||||
**Frontend**
|
||||
|
||||
El frontend de Twenty estará ejecutándose en [http://localhost:3001](http://localhost:3001).
|
||||
Puede iniciar sesión usando la cuenta demo por defecto: `[email protected]` (contraseña: `[email protected]`)
|
||||
Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001).
|
||||
You can log in using the default demo account: `[email protected]` (password: `[email protected]`)
|
||||
|
||||
**Backend**
|
||||
|
||||
* El servidor de Twenty estará operativo en [http://localhost:3000](http://localhost:3000)
|
||||
* La API GraphQL puede ser accedida en [http://localhost:3000/graphql](http://localhost:3000/graphql)
|
||||
* La API REST puede ser alcanzada en [http://localhost:3000/rest](http://localhost:3000/rest)
|
||||
* Twenty's server will be up and running at [http://localhost:3000](http://localhost:3000)
|
||||
* The GraphQL API can be accessed at [http://localhost:3000/graphql](http://localhost:3000/graphql)
|
||||
* The REST API can be reached at [http://localhost:3000/rest](http://localhost:3000/rest)
|
||||
|
||||
## Solución de Problemas
|
||||
## Troubleshooting
|
||||
|
||||
Si encuentras algún problema, consulta [Solución de Problemas](/l/es/developers/self-host/capabilities/troubleshooting) para ver soluciones.
|
||||
If you encounter any problem, check [Troubleshooting](/l/es/developers/self-host/capabilities/troubleshooting) for solutions.
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
---
|
||||
title: Contribuir
|
||||
description: Contribuye al desarrollo de código abierto de Twenty.
|
||||
title: Contribute
|
||||
description: Contribute to Twenty's open-source development.
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/user-guide/github/github-header.png" alt="IA" />
|
||||
<img src="/images/user-guide/github/github-header.png" alt="AI" />
|
||||
</Frame>
|
||||
|
||||
## Resumen
|
||||
## Overview
|
||||
|
||||
Twenty es de código abierto y da la bienvenida a las contribuciones de la comunidad. Ya sea que estés solucionando errores, agregando funciones o mejorando la documentación, tus contribuciones ayudan a que Twenty sea mejor para todos.
|
||||
Twenty is open-source and welcomes contributions from the community. Whether you're fixing bugs, adding features, or improving documentation, your contributions help make Twenty better for everyone.
|
||||
|
||||
## Formas de contribuir
|
||||
## Ways to Contribute
|
||||
|
||||
* **Reporta errores**: Ayuda a identificar y documentar problemas
|
||||
* **Propón funcionalidades**: Propón e implementa nuevas funcionalidades
|
||||
* **Mejora la documentación**: Haz que nuestra documentación sea más clara y útil
|
||||
* **Desarrollo de frontend**: Trabaja en la interfaz de usuario basada en React
|
||||
* **Desarrollo de backend**: Contribuye al servidor NestJS
|
||||
* **Report bugs**: Help identify and document issues
|
||||
* **Submit features**: Propose and implement new functionality
|
||||
* **Improve documentation**: Make our docs clearer and more helpful
|
||||
* **Frontend development**: Work on the React-based UI
|
||||
* **Backend development**: Contribute to the NestJS server
|
||||
|
||||
## Primeros pasos
|
||||
## Getting Started
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Informes de errores y solicitudes" icon="bug" href="/l/es/developers/contribute/capabilities/bug-and-requests">
|
||||
Reporta problemas o solicita funcionalidades
|
||||
<Card title="Bug Reports & Requests" icon="bug" href="/l/es/developers/contribute/capabilities/bug-and-requests">
|
||||
Report issues or request features
|
||||
</Card>
|
||||
|
||||
<Card title="Desarrollo Frontend" icon="browser" href="/l/es/developers/contribute/capabilities/frontend-development">
|
||||
Contribuye a la interfaz de usuario
|
||||
<Card title="Frontend Development" icon="browser" href="/l/es/developers/contribute/capabilities/frontend-development">
|
||||
Contribute to the UI
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,147 +1,147 @@
|
||||
---
|
||||
title: APIs
|
||||
description: Consulta y modifica tus datos de CRM de forma programática usando REST o GraphQL.
|
||||
description: Query and modify your CRM data programmatically using REST or GraphQL.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
Twenty fue creado para ser amigable con los desarrolladores, ofreciendo APIs potentes que se adaptan a tu modelo de datos personalizado. Proveemos cuatro tipos de API distintos para satisfacer diferentes necesidades de integración.
|
||||
Twenty was built to be developer-friendly, offering powerful APIs that adapt to your custom data model. We provide four distinct API types to meet different integration needs.
|
||||
|
||||
## Enfoque centrado en el desarrollador
|
||||
## Developer-First Approach
|
||||
|
||||
Twenty genera APIs específicamente para tu modelo de datos:
|
||||
Twenty generates APIs specifically for your data model:
|
||||
|
||||
* **No se requieren IDs largos**: Usa los nombres de tus objetos y campos directamente en los endpoints.
|
||||
* **Objetos estándar y personalizados tratados por igual**: Tus objetos personalizados reciben el mismo tratamiento de API que los incorporados.
|
||||
* **Endpoints dedicados**: Cada objeto y campo recibe su propio endpoint de API.
|
||||
* **Documentación personalizada**: Generada específicamente para el modelo de datos de tu espacio de trabajo.
|
||||
* **No long IDs required**: Use your object and field names directly in endpoints
|
||||
* **Standard and custom objects treated equally**: Your custom objects get the same API treatment as built-in ones
|
||||
* **Dedicated endpoints**: Each object and field gets its own API endpoint
|
||||
* **Custom documentation**: Generated specifically for your workspace's data model
|
||||
|
||||
<Note>
|
||||
Tu documentación personalizada de la API está disponible en **Configuración → API & Webhooks** después de crear una clave de API. Como Twenty genera APIs que coinciden con tu modelo de datos personalizado, la documentación es única para tu espacio de trabajo.
|
||||
Your personalized API documentation is available under **Settings → API & Webhooks** after creating an API key. Since Twenty generates APIs that match your custom data model, the documentation is unique to your workspace.
|
||||
</Note>
|
||||
|
||||
## Los dos tipos de API
|
||||
## The Two API Types
|
||||
|
||||
### API Principal
|
||||
### Core API
|
||||
|
||||
Accesible en `/rest/` o `/graphql/`
|
||||
Accessed on `/rest/` or `/graphql/`
|
||||
|
||||
Trabaja con tus **registros** reales (los datos):
|
||||
Work with your actual **records** (the data):
|
||||
|
||||
* Crear, leer, actualizar y eliminar Personas, Empresas, Oportunidades, etc.
|
||||
* Consultar y filtrar datos
|
||||
* Gestionar relaciones de registros
|
||||
* Create, read, update, delete People, Companies, Opportunities, etc.
|
||||
* Query and filter data
|
||||
* Manage record relationships
|
||||
|
||||
### API de Metadatos
|
||||
### Metadata API
|
||||
|
||||
Accesible en `/rest/metadata/` o `/metadata/`
|
||||
Accessed on `/rest/metadata/` or `/metadata/`
|
||||
|
||||
Administra tu **espacio de trabajo y modelo de datos**:
|
||||
Manage your **workspace and data model**:
|
||||
|
||||
* Crear, modificar o eliminar objetos y campos
|
||||
* Configurar ajustes del espacio de trabajo
|
||||
* Define relaciones entre objetos
|
||||
* Create, modify, or delete objects and fields
|
||||
* Configure workspace settings
|
||||
* Define relationships between objects
|
||||
|
||||
## REST vs GraphQL
|
||||
|
||||
Tanto las API Core como las de Metadatos están disponibles en formatos REST y GraphQL:
|
||||
Both Core and Metadata APIs are available in REST and GraphQL formats:
|
||||
|
||||
| Formato | Operaciones disponibles |
|
||||
| ----------- | ----------------------------------------------------------------------------- |
|
||||
| **REST** | CRUD, operaciones por lotes, upserts |
|
||||
| **GraphQL** | Lo mismo + **upserts por lotes**, consultas de relaciones en una sola llamada |
|
||||
| Format | Available Operations |
|
||||
| ----------- | ---------------------------------------------------------- |
|
||||
| **REST** | CRUD, batch operations, upserts |
|
||||
| **GraphQL** | Same + **batch upserts**, relationship queries in one call |
|
||||
|
||||
Elige según tus necesidades — ambos formatos acceden a los mismos datos.
|
||||
Choose based on your needs — both formats access the same data.
|
||||
|
||||
## Puntos de Acceso de API
|
||||
## API Endpoints
|
||||
|
||||
| Entorno | URL base |
|
||||
| ------------------- | ------------------------- |
|
||||
| **Nube** | `https://api.twenty.com/` |
|
||||
| **Autoalojamiento** | `https://{your-domain}/` |
|
||||
| Environment | Base URL |
|
||||
| --------------- | ------------------------- |
|
||||
| **Cloud** | `https://api.twenty.com/` |
|
||||
| **Self-Hosted** | `https://{your-domain}/` |
|
||||
|
||||
## Autenticación
|
||||
## Authentication
|
||||
|
||||
Cada solicitud a la API requiere una clave de API en el encabezado:
|
||||
Every API request requires an API key in the header:
|
||||
|
||||
```
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
### Crear una Clave de API
|
||||
### Create an API Key
|
||||
|
||||
1. Ve a **Configuración → APIs y Webhooks**
|
||||
2. Haz clic en **+ Crear clave**
|
||||
3. Configurar:
|
||||
* **Nombre**: Nombre descriptivo para la clave
|
||||
* **Fecha de vencimiento**: Cuándo expira la clave
|
||||
4. Haga clic en **Guardar**
|
||||
5. **Copia de inmediato** — la clave solo se muestra una vez
|
||||
1. Go to **Settings → APIs & Webhooks**
|
||||
2. Click **+ Create key**
|
||||
3. Configure:
|
||||
* **Name**: Descriptive name for the key
|
||||
* **Expiration Date**: When the key expires
|
||||
4. Click **Save**
|
||||
5. **Copy immediately** — the key is only shown once
|
||||
|
||||
<VimeoEmbed videoId="928786722" title="Creación de clave de API" />
|
||||
<VimeoEmbed videoId="928786722" title="Creating API key" />
|
||||
|
||||
<Warning>
|
||||
Tu clave de API concede acceso a datos sensibles. No la compartas con servicios no confiables. Si se ve comprometida, desactívala de inmediato y genera una nueva.
|
||||
Your API key grants access to sensitive data. Don't share it with untrusted services. If compromised, disable it immediately and generate a new one.
|
||||
</Warning>
|
||||
|
||||
### Asignar un rol a una clave de API
|
||||
### Assign a Role to an API Key
|
||||
|
||||
Para mayor seguridad, asigna un rol específico para limitar el acceso:
|
||||
For better security, assign a specific role to limit access:
|
||||
|
||||
1. Ve a **Configuración → Roles**
|
||||
2. Haz clic en el rol que deseas asignar
|
||||
3. Abre la pestaña **Asignación**
|
||||
4. En **Claves de API**, haz clic en **+ Asignar a clave de API**
|
||||
5. Selecciona la clave de API
|
||||
1. Go to **Settings → Roles**
|
||||
2. Click on the role to assign
|
||||
3. Open the **Assignment** tab
|
||||
4. Under **API Keys**, click **+ Assign to API key**
|
||||
5. Select the API key
|
||||
|
||||
La clave heredará los permisos de ese rol. Consulta [Permisos](/l/es/user-guide/permissions-access/capabilities/permissions) para más detalles.
|
||||
The key will inherit that role's permissions. See [Permissions](/l/es/user-guide/permissions-access/capabilities/permissions) for details.
|
||||
|
||||
### Gestionar Claves de API
|
||||
### Manage API Keys
|
||||
|
||||
**Regenerar**: Configuración → APIs & Webhooks → Haz clic en la clave → **Regenerar**
|
||||
**Regenerate**: Settings → APIs & Webhooks → Click key → **Regenerate**
|
||||
|
||||
**Eliminar**: Configuración → APIs & Webhooks → Haz clic en la clave → **Eliminar**
|
||||
**Delete**: Settings → APIs & Webhooks → Click key → **Delete**
|
||||
|
||||
## Playground de la API
|
||||
## API Playground
|
||||
|
||||
Prueba tus API directamente en el navegador con nuestro playground integrado — disponible tanto para **REST** como para **GraphQL**.
|
||||
Test your APIs directly in the browser with our built-in playground — available for both **REST** and **GraphQL**.
|
||||
|
||||
### Accede al Playground
|
||||
### Access the Playground
|
||||
|
||||
1. Ve a **Configuración → APIs y Webhooks**
|
||||
2. Crea una clave de API (obligatorio)
|
||||
3. Haz clic en **REST API** o **GraphQL API** para abrir el playground
|
||||
1. Go to **Settings → APIs & Webhooks**
|
||||
2. Create an API key (required)
|
||||
3. Click on **REST API** or **GraphQL API** to open the playground
|
||||
|
||||
### Lo que obtienes
|
||||
### What You Get
|
||||
|
||||
* **Documentación interactiva**: Generada para tu modelo de datos específico
|
||||
* **Pruebas en vivo**: Ejecuta llamadas reales a la API en tu espacio de trabajo
|
||||
* **Explorador de esquemas**: Navega por los objetos, campos y relaciones disponibles
|
||||
* **Constructor de solicitudes**: Crea consultas con autocompletado
|
||||
* **Interactive documentation**: Generated for your specific data model
|
||||
* **Live testing**: Execute real API calls against your workspace
|
||||
* **Schema explorer**: Browse available objects, fields, and relationships
|
||||
* **Request builder**: Construct queries with autocomplete
|
||||
|
||||
El playground refleja tus objetos y campos personalizados, por lo que la documentación siempre es precisa para tu espacio de trabajo.
|
||||
The playground reflects your custom objects and fields, so documentation is always accurate for your workspace.
|
||||
|
||||
## Operaciones por Lotes
|
||||
## Batch Operations
|
||||
|
||||
Tanto REST como GraphQL admiten operaciones por lotes:
|
||||
Both REST and GraphQL support batch operations:
|
||||
|
||||
* **Tamaño del lote**: Hasta 60 registros por solicitud
|
||||
* **Operaciones**: Crear, actualizar y eliminar múltiples registros
|
||||
* **Batch size**: Up to 60 records per request
|
||||
* **Operations**: Create, update, delete multiple records
|
||||
|
||||
**Características exclusivas de GraphQL:**
|
||||
**GraphQL-only features:**
|
||||
|
||||
* **Upsert por lotes**: Crear o actualizar en una llamada
|
||||
* Usa nombres de objetos en plural (por ejemplo, `CreateCompanies` en lugar de `CreateCompany`)
|
||||
* **Batch Upsert**: Create or update in one call
|
||||
* Use plural object names (e.g., `CreateCompanies` instead of `CreateCompany`)
|
||||
|
||||
## Límites de tasa
|
||||
## Rate Limits
|
||||
|
||||
Las solicitudes a la API se limitan para garantizar la estabilidad de la plataforma:
|
||||
API requests are throttled to ensure platform stability:
|
||||
|
||||
| Límite | Valor |
|
||||
| ------------------- | -------------------------- |
|
||||
| **Solicitudes** | 100 solicitudes por minuto |
|
||||
| **Tamaño del lote** | 60 registros por llamada |
|
||||
| Limit | Value |
|
||||
| -------------- | -------------------- |
|
||||
| **Requests** | 100 calls per minute |
|
||||
| **Batch size** | 60 records per call |
|
||||
|
||||
<Tip>
|
||||
Usa operaciones por lotes para maximizar el rendimiento — procesa hasta 60 registros en una sola llamada a la API en lugar de hacer solicitudes individuales.
|
||||
Use batch operations to maximize throughput — process up to 60 records in a single API call instead of making individual requests.
|
||||
</Tip>
|
||||
|
||||
@@ -1,85 +1,81 @@
|
||||
---
|
||||
title: Aplicaciones de Twenty
|
||||
description: Crea y gestiona personalizaciones de Twenty como código.
|
||||
title: Twenty Apps
|
||||
description: Build and manage Twenty customizations as code.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Las aplicaciones están actualmente en pruebas alfa. La funcionalidad es operativa, pero sigue evolucionando.
|
||||
Apps are currently in alpha testing. The feature is functional but still evolving.
|
||||
</Warning>
|
||||
|
||||
## ¿Qué son las aplicaciones?
|
||||
## What Are Apps?
|
||||
|
||||
Las aplicaciones te permiten crear y administrar personalizaciones de Twenty **como código**. En lugar de configurar todo a través de la interfaz de usuario, defines tu modelo de datos y funciones de lógica en código, lo que hace más rápido crear, mantener y desplegar en múltiples espacios de trabajo.
|
||||
Apps let you build and manage Twenty customizations **as code**. Instead of configuring everything through the UI, you define your data model and serverless functions in code — making it faster to build, maintain, and roll out to multiple workspaces.
|
||||
|
||||
**Lo que puedes hacer hoy:**
|
||||
**What you can do today:**
|
||||
|
||||
* Define objetos y campos personalizados como código (modelo de datos gestionado)
|
||||
* Crea funciones de lógica con desencadenadores personalizados
|
||||
* Despliega la misma aplicación en múltiples espacios de trabajo
|
||||
* Define custom objects and fields as code (managed data model)
|
||||
* Build serverless functions with custom triggers
|
||||
* Deploy the same app across multiple workspaces
|
||||
|
||||
**Próximamente:**
|
||||
**Coming soon:**
|
||||
|
||||
* Diseños y componentes de la interfaz de usuario personalizados
|
||||
* Custom UI layouts and components
|
||||
|
||||
## Prerrequisitos
|
||||
## Prerequisites
|
||||
|
||||
* Node.js 24+ y Yarn 4
|
||||
* Un espacio de trabajo de Twenty y una clave de API (créala en https://app.twenty.com/settings/api-webhooks)
|
||||
* Node.js 24+ and Yarn 4
|
||||
* A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||||
|
||||
## Primeros pasos
|
||||
## Getting Started
|
||||
|
||||
Crea una aplicación nueva usando el generador oficial, luego autentícate y comienza a desarrollar:
|
||||
Create a new app using the official scaffolder, then authenticate and start developing:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Crear la estructura de una nueva aplicación
|
||||
# Scaffold a new app
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Si no usas yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth
|
||||
|
||||
# Autentícate con tu clave de API (se te pedirá)
|
||||
yarn auth:login
|
||||
|
||||
# Inicia el modo de desarrollo: sincroniza automáticamente los cambios locales con tu espacio de trabajo
|
||||
yarn app:dev
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Desde aquí usted puede:
|
||||
From here you can:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Añade una nueva entidad a tu aplicación (guiado)
|
||||
yarn entity:add
|
||||
# Add a new entity to your application (guided)
|
||||
yarn create-entity
|
||||
|
||||
# Genera un cliente tipado de Twenty y tipos de entidad del espacio de trabajo
|
||||
yarn app:generate
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn generate
|
||||
|
||||
# Supervisa los registros de funciones de tu aplicación
|
||||
yarn function:logs
|
||||
# Run a one‑time sync (instead of watch mode)
|
||||
yarn sync
|
||||
|
||||
# Ejecuta una función por nombre
|
||||
yarn function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
# Watch your application's functions logs
|
||||
yarn logs
|
||||
|
||||
# Desinstala la aplicación del espacio de trabajo actual
|
||||
yarn app:uninstall
|
||||
# Uninstall the application from the current workspace
|
||||
yarn uninstall
|
||||
|
||||
# Muestra la ayuda de los comandos
|
||||
# Display commands' help
|
||||
yarn help
|
||||
```
|
||||
|
||||
Consulta también: las páginas de referencia de la CLI para [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) y [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
|
||||
## Estructura del proyecto (generada)
|
||||
## Project structure (scaffolded)
|
||||
|
||||
Cuando ejecutas `npx create-twenty-app@latest my-twenty-app`, el generador:
|
||||
When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
|
||||
|
||||
* Copia una aplicación base mínima en `my-twenty-app/`
|
||||
* Añade una dependencia local de `twenty-sdk` y la configuración de Yarn 4
|
||||
* Crea archivos de configuración y scripts vinculados a la CLI `twenty`
|
||||
* Genera una configuración de aplicación predeterminada y un rol de función predeterminado
|
||||
* Copies a minimal base application into `my-twenty-app/`
|
||||
* Adds a local `twenty-sdk` dependency and Yarn 4 configuration
|
||||
* Creates config files and scripts wired to the `twenty` CLI
|
||||
* Generates a default application config and a default function role
|
||||
|
||||
Una aplicación recién generada se ve así:
|
||||
A freshly scaffolded app looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -89,150 +85,79 @@ my-twenty-app/
|
||||
.nvmrc
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
releases/
|
||||
yarn-4.9.2.cjs
|
||||
install-state.gz
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Carpeta de recursos públicos (imágenes, fuentes, etc.)
|
||||
src/
|
||||
application.config.ts # Requerido - configuración principal de la aplicación
|
||||
default-function.role.ts # Rol predeterminado para funciones sin servidor
|
||||
hello-world.function.ts # Ejemplo de función sin servidor
|
||||
hello-world.front-component.tsx # Ejemplo de componente frontal
|
||||
// tus entidades (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
application.config.ts
|
||||
role.config.ts
|
||||
// your entities, actions, and other app files
|
||||
```
|
||||
|
||||
### Convención sobre configuración
|
||||
At a high level:
|
||||
|
||||
Las aplicaciones usan un enfoque de **convención sobre configuración** en el que las entidades se detectan por su sufijo de archivo. Esto permite una organización flexible dentro de la carpeta `src/app/`:
|
||||
* **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall`, and `auth` that delegate to the local `twenty` CLI.
|
||||
* **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `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.
|
||||
* **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your app’s TypeScript sources.
|
||||
* **README.md**: A short README in the app root with basic instructions.
|
||||
* **src/**: The main place where you define your application-as-code:
|
||||
* `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See “Application config” below.
|
||||
* `role.config.ts`: Default function role used by your serverless functions. See “Default function role” below.
|
||||
* Future entities, actions/functions, and any supporting code you add.
|
||||
|
||||
| Sufijo de archivo | Tipo de entidad |
|
||||
| ----------------------- | --------------------------------------- |
|
||||
| `*.object.ts` | Definiciones de objetos personalizados |
|
||||
| `*.function.ts` | Definiciones de funciones sin servidor |
|
||||
| `*.front-component.tsx` | Definiciones de componentes de interfaz |
|
||||
| `*.role.ts` | Definiciones de roles |
|
||||
Later commands will add more files and folders:
|
||||
|
||||
### Organizaciones de carpetas compatibles
|
||||
* `yarn generate` will create a `generated/` folder (typed Twenty client + workspace types).
|
||||
* `yarn create-entity` will add entity definition files under `src/` for your custom objects.
|
||||
|
||||
Puedes organizar tus entidades con cualquiera de estos patrones:
|
||||
## Authentication
|
||||
|
||||
**Tradicional (por tipo):**
|
||||
The first time you run `yarn auth`, you'll be prompted for:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
* API URL (defaults to http://localhost:3000 or your current workspace profile)
|
||||
* API key
|
||||
|
||||
**Basado en funcionalidades:**
|
||||
Your credentials are stored per-user in `~/.twenty/config.json`. You can maintain multiple profiles and switch using `--workspace <name>`.
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Plano:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
A grandes rasgos:
|
||||
|
||||
* **package.json**: Declara el nombre de la aplicación, la versión, los entornos (Node 24+, Yarn 4) y agrega `twenty-sdk` además de scripts como `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` y `auth:login` que delegan en la CLI local `twenty`.
|
||||
* **.gitignore**: Ignora artefactos comunes como `node_modules`, `.yarn`, `generated/` (cliente tipado), `dist/`, `build/`, carpetas de cobertura, archivos de registro y archivos `.env*`.
|
||||
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloquean y configuran la cadena de herramientas Yarn 4 utilizada por el proyecto.
|
||||
* **.nvmrc**: Fija la versión de Node.js esperada por el proyecto.
|
||||
* **eslint.config.mjs** y **tsconfig.json**: Proporcionan linting y configuración de TypeScript para las fuentes de TypeScript de tu aplicación.
|
||||
* **README.md**: Un README breve en la raíz de la aplicación con instrucciones básicas.
|
||||
* **public/**: Una carpeta para almacenar recursos públicos (imágenes, fuentes, archivos estáticos) que se servirán con tu aplicación. Los archivos colocados aquí se cargan durante la sincronización y son accesibles en tiempo de ejecución.
|
||||
* **src/**: El lugar principal donde defines tu aplicación como código:
|
||||
* `application.config.ts`: Configuración global de tu aplicación (metadatos y vinculación en tiempo de ejecución). Consulta "Configuración de la aplicación" más abajo.
|
||||
* `*.role.ts`: Definiciones de roles usadas por tus funciones de lógica. Consulta "Rol de función predeterminado" más abajo.
|
||||
* `*.object.ts`: Definiciones de objetos personalizados.
|
||||
* `*.function.ts`: Definiciones de funciones de lógica.
|
||||
* `*.front-component.tsx`: Definiciones de componentes de interfaz.
|
||||
|
||||
Comandos posteriores añadirán más archivos y carpetas:
|
||||
|
||||
* `yarn app:generate` creará una carpeta `generated/` (cliente tipado de Twenty + tipos del espacio de trabajo).
|
||||
* `yarn entity:add` añadirá archivos de definición de entidades en `src/` para tus objetos, funciones, componentes de interfaz o roles personalizados.
|
||||
|
||||
## Autenticación
|
||||
|
||||
La primera vez que ejecutes `yarn auth:login`, se te solicitará:
|
||||
|
||||
* URL de la API (por defecto http://localhost:3000 o el perfil de tu espacio de trabajo actual)
|
||||
* Clave de API
|
||||
|
||||
Tus credenciales se almacenan por usuario en `~/.twenty/config.json`. Puedes mantener varios perfiles y cambiar entre ellos.
|
||||
|
||||
### Gestión de espacios de trabajo
|
||||
Examples:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn auth:login
|
||||
yarn auth
|
||||
|
||||
# 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
|
||||
# Use a specific workspace profile
|
||||
yarn auth --workspace my-custom-workspace
|
||||
```
|
||||
|
||||
Una vez que hayas cambiado de espacio de trabajo con `auth:switch`, todos los comandos posteriores usarán ese espacio de trabajo de forma predeterminada. Aún puedes anularlo temporalmente con `--workspace <name>`.
|
||||
## Use the SDK resources (types & config)
|
||||
|
||||
## Usa los recursos del SDK (tipos y configuración)
|
||||
The twenty-sdk provides typed building blocks you use inside your app. Below are the key pieces you'll touch most often.
|
||||
|
||||
El twenty-sdk proporciona bloques de construcción tipados y funciones auxiliares que utilizas dentro de tu aplicación. A continuación, las partes clave que usarás con más frecuencia.
|
||||
### Defining objects
|
||||
|
||||
### Funciones auxiliares
|
||||
Custom objects are regular TypeScript classes annotated with decorators from `twenty-sdk`. They live under `src/objects/` in your app and describe both schema and behavior for records in your workspace.
|
||||
|
||||
El SDK proporciona cuatro funciones auxiliares con validación incorporada para definir las entidades de tu aplicación:
|
||||
|
||||
| Función | Propósito |
|
||||
| --------------------- | ---------------------------------------------- |
|
||||
| `defineApplication()` | Configura los metadatos de la aplicación |
|
||||
| `defineObject()` | Define objetos personalizados con campos |
|
||||
| `defineFunction()` | Define funciones de lógica con controladores |
|
||||
| `defineRole()` | Configura permisos de roles y acceso a objetos |
|
||||
|
||||
Estas funciones validan tu configuración en tiempo de ejecución y proporcionan un mejor autocompletado en el IDE y seguridad de tipos.
|
||||
|
||||
### Definir objetos
|
||||
|
||||
Los objetos personalizados describen tanto el esquema como el comportamiento de los registros en tu espacio de trabajo. Usa `defineObject()` para definir objetos con validación incorporada:
|
||||
Here is an example `postCard` object from the Hello World app:
|
||||
|
||||
```typescript
|
||||
// src/app/postCard.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
import { type Note } from '../../generated';
|
||||
|
||||
import {
|
||||
type AddressField,
|
||||
Field,
|
||||
FieldType,
|
||||
type FullNameField,
|
||||
Object,
|
||||
OnDeleteAction,
|
||||
Relation,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
@@ -241,135 +166,176 @@ enum PostCardStatus {
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
export default defineObject({
|
||||
@Object({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post Card',
|
||||
labelPlural: 'Post Cards',
|
||||
description: 'A post card object',
|
||||
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,
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
export class PostCard {
|
||||
@Field({
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
})
|
||||
content: string;
|
||||
|
||||
@Field({
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
recipientName: FullNameField;
|
||||
|
||||
@Field({
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
})
|
||||
recipientAddress: AddressField;
|
||||
|
||||
@Field({
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
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' },
|
||||
],
|
||||
})
|
||||
status: PostCardStatus;
|
||||
|
||||
@Relation({
|
||||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
|
||||
@Field({
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
})
|
||||
deliveredAt?: Date;
|
||||
}
|
||||
```
|
||||
|
||||
Puntos clave:
|
||||
Key points:
|
||||
|
||||
* Usa `defineObject()` para validación incorporada y mejor soporte del IDE.
|
||||
* El `universalIdentifier` debe ser único y estable entre implementaciones.
|
||||
* Cada campo requiere `name`, `type`, `label` y su propio `universalIdentifier` estable.
|
||||
* La matriz `fields` es opcional: puedes definir objetos sin campos personalizados.
|
||||
* Puedes generar nuevos objetos usando `yarn entity:add`, que te guía por el nombrado, los campos y las relaciones.
|
||||
* The `@Object` decorator defines the object identity and labels used across the workspace; its `universalIdentifier` must be unique and stable across deployments.
|
||||
* Each `@Field` decorator defines a field on the object with a type, label, and its own stable `universalIdentifier`.
|
||||
* `@Relation` wires this object to other objects (standard or custom) and controls cascade behavior with `onDelete`.
|
||||
* You can scaffold new objects using `yarn create-entity`, which guides you through naming, fields, and relationships, then generates object files similar to the `postCard` example.
|
||||
|
||||
<Note>
|
||||
**Los campos base se crean automáticamente.** Cuando defines un objeto personalizado, Twenty añade automáticamente campos estándar como `name`, `createdAt`, `updatedAt`, `createdBy`, `position` y `deletedAt`. No necesitas definir estos en tu matriz `fields` — solo agrega tus campos personalizados.
|
||||
</Note>
|
||||
### Application config (application.config.ts)
|
||||
|
||||
### Configuración de la aplicación (application.config.ts)
|
||||
Every app has a single `application.config.ts` file that describes:
|
||||
|
||||
Cada aplicación tiene un único archivo `application.config.ts` que describe:
|
||||
* **Who the app is**: identifiers, display name, and description.
|
||||
* **How its functions run**: which role they use for permissions.
|
||||
* **(Optional) variables**: key–value pairs exposed to your functions as environment variables.
|
||||
|
||||
* **Qué es la aplicación**: identificadores, nombre para mostrar y descripción.
|
||||
* **Cómo se ejecutan sus funciones**: qué rol usan para permisos.
|
||||
* **Variables (opcionales)**: pares clave–valor expuestos a tus funciones como variables de entorno.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
When you scaffold a new app, you start with a minimal config:
|
||||
|
||||
```typescript
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '<generated-app-uuid>',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
functionRoleUniversalIdentifier: '<generated-role-uuid>',
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
You can gradually extend this file as your app grows. For example, you can add an icon and application-scoped variables:
|
||||
|
||||
```typescript
|
||||
import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '<your-app-uuid>',
|
||||
displayName: 'My App',
|
||||
description: 'What your app does',
|
||||
icon: 'IconWorld', // Choose an icon by name
|
||||
applicationVariables: {
|
||||
DEFAULT_RECIPIENT_NAME: {
|
||||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||||
description: 'Default recipient name for postcards',
|
||||
universalIdentifier: '<uuid>',
|
||||
description: 'Default recipient used by functions',
|
||||
value: 'Jane Doe',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
functionRoleUniversalIdentifier: '<your-role-uuid>',
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
Notas:
|
||||
Notes:
|
||||
|
||||
* Los campos `universalIdentifier` son ID deterministas bajo tu control; genéralos una vez y mantenlos estables entre sincronizaciones.
|
||||
* Las `applicationVariables` se convierten en variables de entorno para tus funciones (por ejemplo, `DEFAULT_RECIPIENT_NAME` está disponible como `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||||
* `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
|
||||
* `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `functionRoleUniversalIdentifier` must match the role you define in `role.config.ts` (see below).
|
||||
|
||||
#### Roles y permisos
|
||||
#### Roles and permissions
|
||||
|
||||
Las aplicaciones pueden definir roles que encapsulan permisos sobre los objetos y acciones de tu espacio de trabajo. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
|
||||
Applications can define roles that encapsulate permissions on your workspace’s objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app’s serverless functions.
|
||||
|
||||
* La clave de API en tiempo de ejecución inyectada como `TWENTY_API_KEY` se deriva de este rol de función predeterminado.
|
||||
* El cliente tipado estará restringido a los permisos otorgados a ese rol.
|
||||
* Sigue el principio de mínimo privilegio: crea un rol dedicado con solo los permisos que necesitan tus funciones y luego referencia su identificador universal.
|
||||
* 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 least‑privilege: create a dedicated role with only the permissions your functions need, then reference its universal identifier.
|
||||
|
||||
##### Rol de función predeterminado (\*.role.ts)
|
||||
##### Default function role (role.config.ts)
|
||||
|
||||
Cuando generas una nueva aplicación, la CLI también crea un archivo de rol predeterminado. Usa `defineRole()` para definir roles con validación incorporada:
|
||||
When you scaffold a new app, the CLI also creates `src/role.config.ts`. This file exports the default role your serverless functions will use at runtime:
|
||||
|
||||
```typescript
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
export const functionRole: RoleConfig = {
|
||||
universalIdentifier: '<generated-role-uuid>',
|
||||
label: 'My Twenty App default function role',
|
||||
description: 'My Twenty App default function role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
};
|
||||
```
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
The `universalIdentifier` of this role is automatically wired into `application.config.ts` as `functionRoleUniversalIdentifier`. In other words:
|
||||
|
||||
* **role.config.ts** defines what the default function role can do.
|
||||
* **application.config.ts** points to that role so your functions inherit its permissions.
|
||||
|
||||
As you move beyond the initial scaffold, you should tighten this role and make it explicit about what it can access. A more production-ready role might look closer to:
|
||||
|
||||
```typescript
|
||||
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
|
||||
|
||||
export const functionRole: RoleConfig = {
|
||||
universalIdentifier: '<your-role-uuid>',
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
canReadAllObjectRecords: false,
|
||||
@@ -382,7 +348,7 @@ export default defineRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectNameSingular: 'postCard',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -391,42 +357,47 @@ export default defineRole({
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectNameSingular: 'postCard',
|
||||
fieldName: 'content',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
});
|
||||
permissionFlags: ['APPLICATIONS'],
|
||||
};
|
||||
```
|
||||
|
||||
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `roleUniversalIdentifier`. En otras palabras:
|
||||
Notes:
|
||||
|
||||
* **\*.role.ts** define lo que puede hacer el rol de función predeterminado.
|
||||
* **application.config.ts** apunta a ese rol para que tus funciones hereden sus permisos.
|
||||
* 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).
|
||||
|
||||
Notas:
|
||||
### Serverless function config and entrypoint
|
||||
|
||||
* Parte del rol generado y luego restríngele progresivamente siguiendo el principio de mínimo privilegio.
|
||||
* Reemplaza `objectPermissions` y `fieldPermissions` con los objetos/campos que necesitan tus funciones.
|
||||
* `permissionFlags` controla el acceso a capacidades a nivel de plataforma. Mantenlos al mínimo; agrega solo lo que necesites.
|
||||
* Consulta un ejemplo funcional en la aplicación 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).
|
||||
|
||||
### Configuración y punto de entrada de funciones de lógica
|
||||
|
||||
Cada archivo de función usa `defineFunction()` para exportar una configuración con un controlador y desencadenadores opcionales. Usa el sufijo de archivo `*.function.ts` para la detección automática.
|
||||
Each function exports a main handler and a config describing its triggers. You can mix multiple trigger types.
|
||||
|
||||
```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';
|
||||
// src/actions/create-new-post-card.ts
|
||||
import type {
|
||||
FunctionConfig,
|
||||
DatabaseEventPayload,
|
||||
ObjectRecordCreateEvent,
|
||||
CronPayload,
|
||||
} from 'twenty-sdk';
|
||||
import Twenty, { type Person } from '../generated';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
// main handler can accept parameters from route, cron, or database events
|
||||
export const main = async (
|
||||
params:
|
||||
| { name?: string }
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||||
| CronPayload,
|
||||
) => {
|
||||
const client = new Twenty(); // generated typed client
|
||||
const name = 'name' in params.queryStringParameters
|
||||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
const name = 'name' in params
|
||||
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
|
||||
const result = await client.mutation({
|
||||
@@ -439,216 +410,113 @@ const handler = async (params: RoutePayload) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
export const config: FunctionConfig = {
|
||||
universalIdentifier: '<function-uuid>',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
handler,
|
||||
triggers: [
|
||||
// Public HTTP route trigger '/s/post-card/create'
|
||||
{
|
||||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||||
universalIdentifier: '<route-trigger-uuid>',
|
||||
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'],
|
||||
// },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Tipos de desencadenadores comunes:
|
||||
|
||||
* **route**: Expone tu función en una ruta y método HTTP **bajo el endpoint `/s/`**:
|
||||
|
||||
> p. ej. `path: '/post-card/create',` -> llamar en `<APP_URL>/s/post-card/create`
|
||||
|
||||
* **cron**: Ejecuta tu función en un horario usando una expresión CRON.
|
||||
* **databaseEvent**: Se ejecuta en eventos del ciclo de vida de objetos del espacio de trabajo. Cuando la operación del evento es `updated`, se pueden especificar campos específicos que se deben escuchar en el arreglo `updatedFields`. Si se deja sin definir o vacío, cualquier actualización activará la función.
|
||||
|
||||
> p. ej., `person.updated`
|
||||
|
||||
Notas:
|
||||
|
||||
* La matriz `triggers` es opcional. Las funciones sin desencadenadores pueden usarse como funciones utilitarias llamadas por otras funciones.
|
||||
* Puedes combinar múltiples tipos de desencadenadores en una sola función.
|
||||
|
||||
### Carga útil del disparador de ruta
|
||||
|
||||
<Warning>
|
||||
**Cambio no retrocompatible (v1.16, enero de 2026):** El formato de la carga útil del disparador de ruta ha cambiado. Antes de la v1.16, los parámetros de consulta, los parámetros de ruta y el cuerpo se enviaban directamente como la carga útil. A partir de la v1.16, están anidados dentro de un objeto `RoutePayload` estructurado.
|
||||
|
||||
**Antes de la v1.16:**
|
||||
|
||||
```typescript
|
||||
const handler = async (params) => {
|
||||
const { param1, param2 } = params; // Direct access
|
||||
};
|
||||
```
|
||||
|
||||
**Después de la v1.16:**
|
||||
|
||||
```typescript
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { param1, param2 } = event.body; // Access via .body
|
||||
const { queryParam } = event.queryStringParameters;
|
||||
const { id } = event.pathParameters;
|
||||
};
|
||||
```
|
||||
|
||||
**Para migrar las funciones existentes:** Actualiza tu controlador para desestructurar desde `event.body`, `event.queryStringParameters` o `event.pathParameters` en lugar de hacerlo directamente desde el objeto params.
|
||||
</Warning>
|
||||
|
||||
Cuando un disparador de ruta invoca tu función de lógica, esta recibe un objeto `RoutePayload` que sigue el formato de AWS HTTP API v2. Importa el tipo desde `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' };
|
||||
};
|
||||
```
|
||||
|
||||
El tipo `RoutePayload` tiene la siguiente estructura:
|
||||
|
||||
| Propiedad | Tipo | Descripción |
|
||||
| ---------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `headers` | `Record<string, string \| undefined>` | Encabezados HTTP (solo aquellos listados en `forwardedRequestHeaders`) |
|
||||
| `queryStringParameters` | `Record<string, string \| undefined>` | Parámetros de consulta (valores múltiples unidos con comas) |
|
||||
| `pathParameters` | `Record<string, string \| undefined>` | Parámetros de ruta extraídos del patrón de ruta (p. ej., `/users/:id` → `{ id: '123' }`) |
|
||||
| `cuerpo` | `object \| null` | Cuerpo de la solicitud analizado (JSON) |
|
||||
| `isBase64Encoded` | `booleano` | Indica si el cuerpo está codificado en base64 |
|
||||
| `requestContext.http.method` | `string` | Método HTTP (GET, POST, PUT, PATCH, DELETE) |
|
||||
| `requestContext.http.path` | `string` | Ruta de la solicitud sin procesar |
|
||||
|
||||
### Reenvío de encabezados HTTP
|
||||
|
||||
De forma predeterminada, los encabezados HTTP de las solicitudes entrantes **no** se pasan a tu función de lógica por razones de seguridad. Para acceder a encabezados específicos, enuméralos explícitamente en el arreglo `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'],
|
||||
universalIdentifier: '<cron-trigger-uuid>',
|
||||
type: 'cron',
|
||||
pattern: '0 0 1 1 *',
|
||||
},
|
||||
// Database event trigger
|
||||
{
|
||||
universalIdentifier: '<db-trigger-uuid>',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.created',
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
En tu controlador, luego puedes acceder a estos encabezados:
|
||||
|
||||
```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>
|
||||
Los nombres de los encabezados se normalizan a minúsculas. Accede a ellos usando claves en minúsculas (por ejemplo, `event.headers['content-type']`).
|
||||
</Note>
|
||||
Common trigger types:
|
||||
|
||||
Puedes crear funciones nuevas de dos maneras:
|
||||
* route: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
|
||||
|
||||
* **Generado**: Ejecuta `yarn entity:add` y elige la opción para añadir una nueva función. Esto genera un archivo inicial con un controlador y configuración.
|
||||
* **Manual**: Crea un nuevo archivo `*.function.ts` y usa `defineFunction()`, siguiendo el mismo patrón.
|
||||
> e.g. `path: '/post-card/create',` -> call on `<APP_URL>/s/post-card/create`
|
||||
|
||||
### Cliente tipado generado
|
||||
* cron: Runs your function on a schedule using a CRON expression.
|
||||
* databaseEvent: Runs on workspace object lifecycle events
|
||||
|
||||
Ejecuta yarn app:generate para crear un cliente tipado local en generated/ basado en el esquema de tu espacio de trabajo. Úsalo en tus funciones:
|
||||
> e.g. `person.created`
|
||||
|
||||
You can create new functions in two ways:
|
||||
|
||||
* **Scaffolded**: Run `yarn create-entity --path <custom-path>` and choose the option to add a new function. This generates a starter file under `<custom-path>` with a `main` handler and a `config` block similar to the example above.
|
||||
* **Manual**: Create a new file and export `main` and `config` yourself, following the same pattern.
|
||||
|
||||
### Generated typed client
|
||||
|
||||
Run yarn generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import Twenty from './generated';
|
||||
|
||||
const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
El cliente se vuelve a generar con `yarn app:generate`. Vuelve a ejecutarlo después de cambiar tus objetos o al incorporarte a un nuevo espacio de trabajo.
|
||||
The client is re-generated by `yarn generate`. Re-run after changing your objects and `yarn sync` or when onboarding to a new workspace.
|
||||
|
||||
#### Credenciales en tiempo de ejecución en funciones de lógica
|
||||
#### Runtime credentials in serverless functions
|
||||
|
||||
Cuando tu función se ejecuta en Twenty, la plataforma inyecta credenciales como variables de entorno antes de que tu código se ejecute:
|
||||
When your function runs on Twenty, the platform injects credentials as environment variables before your code executes:
|
||||
|
||||
* `TWENTY_API_URL`: URL base de la API de Twenty a la que apunta tu aplicación.
|
||||
* `TWENTY_API_KEY`: Clave de corta duración con alcance al rol de función predeterminado de tu aplicación.
|
||||
* `TWENTY_API_URL`: Base URL of the Twenty API your app targets.
|
||||
* `TWENTY_API_KEY`: Short‑lived key scoped to your application’s default function role.
|
||||
|
||||
Notas:
|
||||
Notes:
|
||||
|
||||
* No necesitas pasar la URL ni la clave de API al cliente generado. Lee `TWENTY_API_URL` y `TWENTY_API_KEY` de process.env en tiempo de ejecución.
|
||||
* The API key's permissions are determined by the role referenced in your `application.config.ts` via `roleUniversalIdentifier`. Este es el rol predeterminado que usan las funciones de lógica de tu aplicación.
|
||||
* Las aplicaciones pueden definir roles para seguir el principio de mínimo privilegio. Grant only the permissions your functions need, then point `roleUniversalIdentifier` to that role's universal identifier.
|
||||
* 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 `functionRoleUniversalIdentifier`. This is the default role used by serverless functions of your application.
|
||||
* Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that role’s universal identifier.
|
||||
|
||||
### Ejemplo Hello World
|
||||
### Hello World example
|
||||
|
||||
Explora un ejemplo mínimo de extremo a extremo que demuestra objetos, funciones y múltiples desencadenadores [aquí](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Configuración manual (sin el generador)
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
Aunque recomendamos usar `create-twenty-app` para la mejor experiencia de inicio, también puedes configurar un proyecto manualmente. No instales la CLI globalmente. En su lugar, agrega `twenty-sdk` como dependencia local y conecta scripts en tu package.json:
|
||||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire scripts in your package.json:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Luego agrega scripts como estos:
|
||||
Then add scripts like these:
|
||||
|
||||
```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"
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ahora puedes ejecutar los mismos comandos mediante Yarn, p. ej., `yarn app:dev`, `yarn app:generate`, etc.
|
||||
Now you can run the same commands via Yarn, e.g. `yarn dev`, `yarn sync`, etc.
|
||||
|
||||
## Solución de problemas
|
||||
## Troubleshooting
|
||||
|
||||
* Errores de autenticación: ejecuta `yarn auth:login` y asegúrate de que tu clave de API tenga los permisos necesarios.
|
||||
* No se puede conectar al servidor: verifica la URL de la API y que el servidor de Twenty sea accesible.
|
||||
* Tipos o cliente faltantes/obsoletos: ejecuta `yarn app:generate`.
|
||||
* El modo de desarrollo no sincroniza: asegúrate de que `yarn app:dev` esté ejecutándose y de que los cambios no sean ignorados por tu entorno.
|
||||
* Authentication errors: run `yarn auth` and ensure your API key has the required permissions.
|
||||
* Cannot connect to server: verify the API URL and that the Twenty server is reachable.
|
||||
* Types or client missing/outdated: run `yarn generate` and then `yarn dev`.
|
||||
* Dev mode not syncing: ensure `yarn dev` is running and that changes are not ignored by your environment.
|
||||
|
||||
Canal de ayuda en Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
---
|
||||
title: Webhooks
|
||||
description: Recibe notificaciones en tiempo real cuando ocurran eventos en tu CRM.
|
||||
description: Receive real-time notifications when events occur in your CRM.
|
||||
---
|
||||
|
||||
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
|
||||
Los webhooks envían datos a tus sistemas en tiempo real cuando ocurren eventos en Twenty — no se requiere sondeo. Úsalos para mantener sincronizados los sistemas externos, activar automatizaciones o enviar alertas.
|
||||
Webhooks push data to your systems in real-time when events occur in Twenty — no polling required. Use them to keep external systems in sync, trigger automations, or send alerts.
|
||||
|
||||
## Crear un Webhook
|
||||
## Create a Webhook
|
||||
|
||||
1. Ve a **Configuración → APIs y Webhooks → Webhooks**
|
||||
2. Haga clic en **+ Crear webhook**
|
||||
3. Introduce la URL de tu webhook (debe ser públicamente accesible)
|
||||
4. Haga clic en **Guardar**
|
||||
1. Go to **Settings → APIs & Webhooks → Webhooks**
|
||||
2. Click **+ Create webhook**
|
||||
3. Enter your webhook URL (must be publicly accessible)
|
||||
4. Click **Save**
|
||||
|
||||
El webhook se activa de inmediato y comienza a enviar notificaciones.
|
||||
The webhook activates immediately and starts sending notifications.
|
||||
|
||||
<VimeoEmbed videoId="928786708" title="Crear un webhook" />
|
||||
<VimeoEmbed videoId="928786708" title="Creating a webhook" />
|
||||
|
||||
### Gestionar Webhooks
|
||||
### Manage Webhooks
|
||||
|
||||
**Editar**: Haz clic en el webhook → Actualizar la URL → **Guardar**
|
||||
**Edit**: Click the webhook → Update URL → **Save**
|
||||
|
||||
**Eliminar**: Haz clic en el webhook → **Eliminar** → Confirmar
|
||||
**Delete**: Click the webhook → **Delete** → Confirm
|
||||
|
||||
## Eventos
|
||||
## Events
|
||||
|
||||
Twenty envía webhooks para estos tipos de eventos:
|
||||
Twenty sends webhooks for these event types:
|
||||
|
||||
| Evento | Ejemplo |
|
||||
| ---------------------------- | ---------------------------------------------------------- |
|
||||
| **Se crea un registro** | `person.created`, `company.created`, `note.created` |
|
||||
| **Se actualiza un registro** | `person.updated`, `company.updated`, `opportunity.updated` |
|
||||
| **Se elimina un registro** | `person.deleted`, `company.deleted` |
|
||||
| Event | Example |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Record Created** | `person.created`, `company.created`, `note.created` |
|
||||
| **Record Updated** | `person.updated`, `company.updated`, `opportunity.updated` |
|
||||
| **Record Deleted** | `person.deleted`, `company.deleted` |
|
||||
|
||||
Todos los tipos de eventos se envían a la URL de tu webhook. Es posible que se agregue el filtrado de eventos en versiones futuras.
|
||||
All event types are sent to your webhook URL. Event filtering may be added in future releases.
|
||||
|
||||
## Formato de la carga útil
|
||||
## Payload Format
|
||||
|
||||
Cada webhook envía una solicitud HTTP POST con un cuerpo JSON:
|
||||
Each webhook sends an HTTP POST with a JSON body:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -55,35 +55,35 @@ Cada webhook envía una solicitud HTTP POST con un cuerpo JSON:
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Descripción |
|
||||
| ----------------- | -------------------------------------------------- |
|
||||
| `evento` | Qué ocurrió (p. ej., `person.created`) |
|
||||
| `datos` | El registro completo que se creó/actualizó/eliminó |
|
||||
| `marca de tiempo` | Cuándo ocurrió el evento (UTC) |
|
||||
| Field | Description |
|
||||
| ----------- | ------------------------------------------------ |
|
||||
| `event` | What happened (e.g., `person.created`) |
|
||||
| `data` | The full record that was created/updated/deleted |
|
||||
| `timestamp` | When the event occurred (UTC) |
|
||||
|
||||
<Note>
|
||||
Responde con un **estado HTTP 2xx** (200-299) para confirmar la recepción. Las respuestas que no sean 2xx se registran como errores de entrega.
|
||||
Respond with a **2xx HTTP status** (200-299) to acknowledge receipt. Non-2xx responses are logged as delivery failures.
|
||||
</Note>
|
||||
|
||||
## Validación de Webhook
|
||||
## Webhook Validation
|
||||
|
||||
Twenty firma cada solicitud de webhook por seguridad. Valida las firmas para garantizar que las solicitudes sean auténticas.
|
||||
Twenty signs each webhook request for security. Validate signatures to ensure requests are authentic.
|
||||
|
||||
### Encabezados
|
||||
### Headers
|
||||
|
||||
| Encabezado | Descripción |
|
||||
| ---------------------------- | ------------------------------- |
|
||||
| `X-Twenty-Webhook-Signature` | Firma HMAC SHA256 |
|
||||
| `X-Twenty-Webhook-Timestamp` | Marca de tiempo de la solicitud |
|
||||
| Header | Description |
|
||||
| ---------------------------- | --------------------- |
|
||||
| `X-Twenty-Webhook-Signature` | HMAC SHA256 signature |
|
||||
| `X-Twenty-Webhook-Timestamp` | Request timestamp |
|
||||
|
||||
### Pasos de validación
|
||||
### Validation Steps
|
||||
|
||||
1. Obtén la marca de tiempo de `X-Twenty-Webhook-Timestamp`
|
||||
2. Crea la cadena: `{timestamp}:{JSON payload}`
|
||||
3. Calcula HMAC SHA256 usando tu secreto de webhook
|
||||
4. Compara con `X-Twenty-Webhook-Signature`
|
||||
1. Get the timestamp from `X-Twenty-Webhook-Timestamp`
|
||||
2. Create the string: `{timestamp}:{JSON payload}`
|
||||
3. Compute HMAC SHA256 using your webhook secret
|
||||
4. Compare with `X-Twenty-Webhook-Signature`
|
||||
|
||||
### Ejemplo (Node.js)
|
||||
### Example (Node.js)
|
||||
|
||||
```javascript
|
||||
const crypto = require("crypto");
|
||||
@@ -101,12 +101,12 @@ const expectedSignature = crypto
|
||||
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
|
||||
```
|
||||
|
||||
## Webhooks vs flujos de trabajo
|
||||
## Webhooks vs Workflows
|
||||
|
||||
| Método | Dirección | Caso de uso |
|
||||
| --------------------------------------------- | --------- | --------------------------------------------------------------------------------- |
|
||||
| **Webhooks** | SALIDA | Notificar automáticamente a los sistemas externos cualquier cambio en un registro |
|
||||
| **Flujo de trabajo + solicitud HTTP** | SALIDA | Enviar datos con lógica personalizada (filtros, transformaciones) |
|
||||
| **Disparador de webhook de flujo de trabajo** | ENTRADA | Recibir datos en Twenty desde sistemas externos |
|
||||
| Method | Direction | Use Case |
|
||||
| ---------------------------- | --------- | ---------------------------------------------------------- |
|
||||
| **Webhooks** | OUT | Automatically notify external systems of any record change |
|
||||
| **Workflow + HTTP Request** | OUT | Send data out with custom logic (filters, transformations) |
|
||||
| **Workflow Webhook Trigger** | IN | Receive data into Twenty from external systems |
|
||||
|
||||
Para recibir datos externos, consulta [Configurar un disparador de webhook](/l/es/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).
|
||||
For receiving external data, see [Set Up a Webhook Trigger](/l/es/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
---
|
||||
title: Ampliar
|
||||
description: Amplía la funcionalidad de Twenty con APIs, webhooks y aplicaciones personalizadas.
|
||||
title: Extend
|
||||
description: Extend Twenty's functionality with APIs, webhooks, and custom apps.
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/user-guide/integrations/plug.png" alt="IA" />
|
||||
<img src="/images/user-guide/integrations/plug.png" alt="AI" />
|
||||
</Frame>
|
||||
|
||||
## Resumen
|
||||
## Overview
|
||||
|
||||
Twenty está diseñado para ser extensible. Usa nuestras APIs, webhooks y el framework de aplicaciones para integrarte con tus herramientas existentes y crear funcionalidades personalizadas.
|
||||
Twenty is designed to be extensible. Use our APIs, webhooks, and app framework to integrate with your existing tools and build custom functionality.
|
||||
|
||||
## Lo que puedes hacer
|
||||
## What You Can Do
|
||||
|
||||
* **APIs**: Consulta y modifica tus datos de CRM de forma programática usando REST o GraphQL
|
||||
* **Webhooks**: Recibe notificaciones en tiempo real cuando ocurran eventos en Twenty
|
||||
* **Aplicaciones**: Crea aplicaciones personalizadas que amplíen las capacidades de Twenty - Próximamente.
|
||||
* **APIs**: Query and modify your CRM data programmatically using REST or GraphQL
|
||||
* **Webhooks**: Receive real-time notifications when events occur in Twenty
|
||||
* **Apps**: Build custom applications that extend Twenty's capabilities - Coming soon!
|
||||
|
||||
## Primeros pasos
|
||||
## Getting Started
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="APIs" icon="código" href="/l/es/developers/extend/capabilities/apis">
|
||||
Conéctate a Twenty de forma programática
|
||||
<Card title="APIs" icon="code" href="/l/es/developers/extend/capabilities/apis">
|
||||
Connect to Twenty programmatically
|
||||
</Card>
|
||||
|
||||
<Card title="Webhooks" icon="bell" href="/l/es/developers/extend/capabilities/webhooks">
|
||||
Recibe notificaciones de eventos en tiempo real
|
||||
Get notified of events in real-time
|
||||
</Card>
|
||||
|
||||
<Card title="Aplicaciones" icon="puzzle-piece" href="/l/es/developers/extend/capabilities/apps">
|
||||
Crea personalizaciones como código (Alpha)
|
||||
<Card title="Apps" icon="puzzle-piece" href="/l/es/developers/extend/capabilities/apps">
|
||||
Build customizations as code (Alpha)
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
title: Primeros pasos
|
||||
description: Bienvenido a la documentación para desarrolladores de Twenty, tus recursos para ampliar, autoalojar y contribuir a Twenty.
|
||||
title: Getting Started
|
||||
description: Welcome to Twenty Developer Documentation, your resources for extending, self-hosting, and contributing to Twenty.
|
||||
---
|
||||
|
||||
import { CardTitle } from "/snippets/card-title.mdx"
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card href="/l/es/developers/extend/extend" img="/images/user-guide/integrations/plug.png">
|
||||
<CardTitle>Ampliar</CardTitle>
|
||||
Crea integraciones con APIs, webhooks y aplicaciones personalizadas.
|
||||
<CardTitle>Extend</CardTitle>
|
||||
Build integrations with APIs, webhooks, and custom apps.
|
||||
</Card>
|
||||
|
||||
<Card href="/l/es/developers/self-host/self-host" img="/images/user-guide/what-is-twenty/20.png">
|
||||
<CardTitle>Autoalojar</CardTitle>
|
||||
Despliega y administra Twenty en tu propia infraestructura.
|
||||
<CardTitle>Self-Host</CardTitle>
|
||||
Deploy and manage Twenty on your own infrastructure.
|
||||
</Card>
|
||||
|
||||
<Card href="/l/es/developers/contribute/contribute" img="/images/user-guide/github/github-header.png">
|
||||
<CardTitle>Contribuir</CardTitle>
|
||||
Únete a nuestra comunidad de código abierto y contribuye a Twenty.
|
||||
<CardTitle>Contribute</CardTitle>
|
||||
Join our open-source community and contribute to Twenty.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
---
|
||||
title: Otros métodos
|
||||
title: Other methods
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Este documento es mantenido por la comunidad. Podría contener problemas.
|
||||
This document is maintained by the community. It might contain issues.
|
||||
</Warning>
|
||||
|
||||
## Kubernetes vía Terraform y Manifests
|
||||
## Kubernetes via Terraform and Manifests
|
||||
|
||||
Documentación comunitaria para la implementación de Kubernetes está disponible [aquí](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
Community-led documentation for Kubernetes deployment is available [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Despliega Twenty en servidores usando Coolify. (la imagen oficial en Coolify estará disponible pronto)
|
||||
Deploy Twenty on servers using Coolify. (official image on Coolify will be available soon)
|
||||
|
||||
[Documentación de Coolify](https://coolify.io/docs/get-started/introduction)
|
||||
[Coolify documentation](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Despliega Twenty en EasyPanel con la plantilla mantenida por la comunidad a continuación.
|
||||
Deploy Twenty on EasyPanel with the community maintained template below.
|
||||
|
||||
[Desplegar en EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
[Deploy on EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Despliega Twenty en servidores con Elest.io utilizando el enlace a continuación.
|
||||
Deploy Twenty on servers with Elest.io using link below.
|
||||
|
||||
[Desplegar en Elest.io](https://elest.io/open-source/twenty)
|
||||
[Deploy on Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty en Railway
|
||||
### Twenty on Railway
|
||||
|
||||
Despliega Twenty en Railway con la plantilla mantenida por la comunidad a continuación.
|
||||
Deploy Twenty on Railway with the community maintained template below.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
### Twenty en Sealos
|
||||
### Twenty on Sealos
|
||||
|
||||
Despliega Twenty en Sealos con la plantilla mantenida por la comunidad a continuación.
|
||||
Deploy Twenty on Sealos with the community maintained template below.
|
||||
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
[](https://sealos.io/products/app-store/twenty)
|
||||
|
||||
## Otros
|
||||
## Others
|
||||
|
||||
No dudes en abrir un PR para añadir más opciones de proveedores de la nube.
|
||||
Please feel free to Open a PR to add more Cloud Provider options.
|
||||
|
||||
@@ -1,253 +1,253 @@
|
||||
---
|
||||
title: 1-Clic con Docker Compose
|
||||
title: 1-Click w/ Docker Compose
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Los contenedores de Docker son para alojamiento en producción o autoalojamiento, para la contribución por favor revise la [Configuración Local](/l/es/developers/contribute/capabilities/local-setup).
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](/l/es/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## Resumen
|
||||
## Overview
|
||||
|
||||
Esta guía proporciona instrucciones paso a paso para instalar y configurar la aplicación Twenty utilizando Docker Compose. El objetivo es simplificar el proceso y prevenir errores comunes que podrían arruinar tu configuración.
|
||||
This guide provides step-by-step instructions to install and configure the Twenty application using Docker Compose. The aim is to make the process straightforward and prevent common pitfalls that could break your setup.
|
||||
|
||||
**Importante:** Solo modifica configuraciones explícitamente mencionadas en esta guía. Alterar otras configuraciones puede causar problemas.
|
||||
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
|
||||
|
||||
Consulta los documentos [Configurar Variables de Entorno](/l/es/developers/self-host/capabilities/setup) para configuraciones avanzadas. Todas las variables de entorno deben ser declaradas en el archivo docker-compose.yml en el nivel del servidor y/o trabajador dependiendo de la variable.
|
||||
See docs [Setup Environment Variables](/l/es/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
|
||||
## Requisitos del sistema
|
||||
## System Requirements
|
||||
|
||||
* RAM: Asegúrate de que tu entorno tenga al menos 2GB de RAM. La memoria insuficiente puede causar que los procesos se bloqueen.
|
||||
* Docker & Docker Compose: Asegúrate de que ambos estén instalados y actualizados.
|
||||
* RAM: Ensure your environment has at least 2GB of RAM. Insufficient memory can cause processes to crash.
|
||||
* Docker & Docker Compose: Make sure both are installed and up-to-date.
|
||||
|
||||
## Opción 1: Script de una línea
|
||||
## Option 1: One-line script
|
||||
|
||||
Instala la última versión estable de Twenty con un solo comando:
|
||||
Install the latest stable version of Twenty with a single command:
|
||||
|
||||
```bash
|
||||
bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
|
||||
```
|
||||
|
||||
Para instalar una versión o rama específica:
|
||||
To install a specific version or branch:
|
||||
|
||||
```bash
|
||||
VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
|
||||
```
|
||||
|
||||
* Reemplace x.y.z con el número de versión deseado.
|
||||
* Reemplace branch-name con el nombre de la rama que desea instalar.
|
||||
* Replace x.y.z with the desired version number.
|
||||
* Replace branch-name with the name of the branch you want to install.
|
||||
|
||||
## Opción 2: Pasos manuales
|
||||
## Option 2: Manual steps
|
||||
|
||||
Sigue estos pasos para una configuración manual.
|
||||
Follow these steps for a manual setup.
|
||||
|
||||
### Paso 1: Configurar el archivo de entorno
|
||||
### Step 1: Set Up the Environment File
|
||||
|
||||
1. **Crea el archivo .env**
|
||||
1. **Create the .env File**
|
||||
|
||||
Copia el archivo de entorno de ejemplo a tu directorio de trabajo a un nuevo archivo .env:
|
||||
Copy the example environment file to a new .env file in your working directory:
|
||||
|
||||
```bash
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Generar tokens secretos**
|
||||
2. **Generate Secret Tokens**
|
||||
|
||||
Ejecuta el siguiente comando para generar una cadena única aleatoria:
|
||||
Run the following command to generate a unique random string:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
**Importante:** Mantén este valor en secreto / no lo compartas.
|
||||
**Important:** Keep this value secret / do not share it.
|
||||
|
||||
3. **Actualiza el `.env`**
|
||||
3. **Update the `.env`**
|
||||
|
||||
Reemplaza el valor de marcador de posición en tu archivo .env con el token generado:
|
||||
Replace the placeholder value in your .env file with the generated token:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
```
|
||||
|
||||
4. **Establecer la contraseña de Postgres**
|
||||
4. **Set the Postgres Password**
|
||||
|
||||
Actualiza el valor de `PG_DATABASE_PASSWORD` en el archivo .env con una contraseña fuerte sin caracteres especiales.
|
||||
Update the `PG_DATABASE_PASSWORD` value in the .env file with a strong password without special characters.
|
||||
|
||||
```ini
|
||||
PG_DATABASE_PASSWORD=my_strong_password
|
||||
```
|
||||
|
||||
### Paso 2: Obtener el archivo Docker Compose
|
||||
### Step 2: Obtain the Docker Compose File
|
||||
|
||||
Descarga el archivo `docker-compose.yml` en tu directorio de trabajo:
|
||||
Download the `docker-compose.yml` file to your working directory:
|
||||
|
||||
```bash
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml
|
||||
```
|
||||
|
||||
### Paso 3: Lanza la aplicación
|
||||
### Step 3: Launch the Application
|
||||
|
||||
Inicia los contenedores Docker:
|
||||
Start the Docker containers:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Paso 4: Acceder a la aplicación
|
||||
### Step 4: Access the Application
|
||||
|
||||
Si alojas twentyCRM en tu propia computadora, abre tu navegador y navega a [http://localhost:3000](http://localhost:3000).
|
||||
If you host twentyCRM on your own computer, open your browser and navigate to [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
Si lo alojas en un servidor, verifica que el servidor esté en funcionamiento y que todo esté bien con
|
||||
If you host it on a server, check that the server is running and that everything is ok with
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000
|
||||
```
|
||||
|
||||
## Configuración
|
||||
## Configuration
|
||||
|
||||
### Exponer Twenty para acceso externo
|
||||
### Expose Twenty to External Access
|
||||
|
||||
Por defecto, Twenty se ejecuta en `localhost` en el puerto `3000`. Para acceder a él mediante un dominio externo o dirección IP, necesitas configurar `SERVER_URL` en tu archivo `.env`.
|
||||
By default, Twenty runs on `localhost` at port `3000`. To access it via an external domain or IP address, you need to configure the `SERVER_URL` in your `.env` file.
|
||||
|
||||
#### Entendiendo `SERVER_URL`
|
||||
#### Understanding `SERVER_URL`
|
||||
|
||||
* **Protocolo:** Usa `http` o `https` dependiendo de tu configuración.
|
||||
* Usa `http` si no has configurado SSL.
|
||||
* Usa `https` si tienes SSL configurado.
|
||||
* **Dominio/IP:** Este es el nombre de dominio o dirección IP donde tu aplicación es accesible.
|
||||
* **Puerto:** Incluye el número de puerto si no estás usando los puertos predeterminados (`80` para `http`, `443` para `https`).
|
||||
* **Protocol:** Use `http` or `https` depending on your setup.
|
||||
* Use `http` if you haven't set up SSL.
|
||||
* Use `https` if you have SSL configured.
|
||||
* **Domain/IP:** This is the domain name or IP address where your application is accessible.
|
||||
* **Port:** Include the port number if you're not using the default ports (`80` for `http`, `443` for `https`).
|
||||
|
||||
### Requisitos de SSL
|
||||
### SSL Requirements
|
||||
|
||||
SSL (HTTPS) es requerido para que ciertas características del navegador funcionen correctamente. Aunque estas características podrían funcionar durante el desarrollo local (ya que los navegadores tratan localhost de manera diferente), se requiere una configuración SSL adecuada al alojar Twenty en un dominio regular.
|
||||
SSL (HTTPS) is required for certain browser features to work properly. While these features might work during local development (as browsers treat localhost differently), a proper SSL setup is needed when hosting Twenty on a regular domain.
|
||||
|
||||
Por ejemplo, es posible que la API del portapapeles requiera un contexto seguro: algunas características como los botones de copia en toda la aplicación pueden no funcionar sin HTTPS habilitado.
|
||||
For example, the clipboard API might require a secure context - some features like copy buttons throughout the application might not work without HTTPS enabled.
|
||||
|
||||
Recomendamos encarecidamente configurar Twenty detrás de un proxy inverso con terminación SSL para una seguridad y funcionalidad óptimas.
|
||||
We strongly recommend setting up Twenty behind a reverse proxy with SSL termination for optimal security and functionality.
|
||||
|
||||
#### Configurando `SERVER_URL`
|
||||
#### Configuring `SERVER_URL`
|
||||
|
||||
1. **Determine su URL de acceso**
|
||||
* **Sin proxy inverso (Acceso directo):**
|
||||
1. **Determine Your Access URL**
|
||||
* **Without Reverse Proxy (Direct Access):**
|
||||
|
||||
Si estás accediendo a la aplicación directamente sin un proxy inverso:
|
||||
If you're accessing the application directly without a reverse proxy:
|
||||
|
||||
```ini
|
||||
SERVER_URL=http://your-domain-or-ip:3000
|
||||
```
|
||||
|
||||
* **Con proxy inverso (Puertos estándar):**
|
||||
* **With Reverse Proxy (Standard Ports):**
|
||||
|
||||
Si estás usando un proxy inverso como Nginx o Traefik y tienes SSL configurado:
|
||||
If you're using a reverse proxy like Nginx or Traefik and have SSL configured:
|
||||
|
||||
```ini
|
||||
SERVER_URL=https://your-domain-or-ip
|
||||
```
|
||||
|
||||
* **Con proxy inverso (Puertos personalizados):**
|
||||
* **With Reverse Proxy (Custom Ports):**
|
||||
|
||||
Si estás usando puertos no estándar:
|
||||
If you're using non-standard ports:
|
||||
|
||||
```ini
|
||||
SERVER_URL=https://your-domain-or-ip:custom-port
|
||||
```
|
||||
|
||||
2. **Actualiza el archivo `.env`**
|
||||
2. **Update the `.env` File**
|
||||
|
||||
Abre tu archivo `.env` y actualiza el `SERVER_URL`:
|
||||
Open your `.env` file and update the `SERVER_URL`:
|
||||
|
||||
```ini
|
||||
SERVER_URL=http(s)://your-domain-or-ip:your-port
|
||||
```
|
||||
|
||||
**Ejemplos:**
|
||||
**Examples:**
|
||||
|
||||
* Acceso directo sin SSL:
|
||||
* Direct access without SSL:
|
||||
```ini
|
||||
SERVER_URL=http://123.45.67.89:3000
|
||||
```
|
||||
* Acceso vía dominio con SSL:
|
||||
* Access via domain with SSL:
|
||||
```ini
|
||||
SERVER_URL=https://mytwentyapp.com
|
||||
```
|
||||
|
||||
3. **Reiniciar la aplicación**
|
||||
3. **Restart the Application**
|
||||
|
||||
Para que los cambios surtan efecto, reinicia los contenedores Docker:
|
||||
For changes to take effect, restart the Docker containers:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Consideraciones
|
||||
#### Considerations
|
||||
|
||||
* **Configuración del Proxy Inverso:**
|
||||
* **Reverse Proxy Configuration:**
|
||||
|
||||
Asegúrese de que su proxy inverso envíe las solicitudes al puerto interno correcto (`3000` por defecto). Configure la terminación SSL y cualquier cabecera necesaria.
|
||||
Ensure your reverse proxy forwards requests to the correct internal port (`3000` by default). Configure SSL termination and any necessary headers.
|
||||
|
||||
* **Configuración del Cortafuegos:**
|
||||
* **Firewall Settings:**
|
||||
|
||||
Abra los puertos necesarios en su cortafuegos para permitir el acceso externo.
|
||||
Open necessary ports in your firewall to allow external access.
|
||||
|
||||
* **Consistencia:**
|
||||
* **Consistency:**
|
||||
|
||||
La `SERVER_URL` debe coincidir con cómo los usuarios acceden a su aplicación en sus navegadores.
|
||||
The `SERVER_URL` must match how users access your application in their browsers.
|
||||
|
||||
#### Persistencia
|
||||
#### Persistence
|
||||
|
||||
* **Volúmenes de Datos:**
|
||||
* **Data Volumes:**
|
||||
|
||||
La configuración de Docker Compose utiliza volúmenes para persistir datos para la base de datos y el almacenamiento del servidor.
|
||||
The Docker Compose configuration uses volumes to persist data for the database and server storage.
|
||||
|
||||
* **Entornos Sin Estado:**
|
||||
* **Stateless Environments:**
|
||||
|
||||
Si se despliega en un entorno sin estado (por ejemplo, ciertos servicios en la nube), configure un almacenamiento externo para persistir los datos.
|
||||
If deploying to a stateless environment (e.g., certain cloud services), configure external storage to persist data.
|
||||
|
||||
## Copia de seguridad y restauración
|
||||
## Backup and Restore
|
||||
|
||||
Las copias de seguridad periódicas protegen los datos de tu CRM contra la pérdida.
|
||||
Regular backups protect your CRM data from loss.
|
||||
|
||||
### Crea una copia de seguridad de la base de datos
|
||||
### Create a Database Backup
|
||||
|
||||
```bash
|
||||
docker exec twenty-postgres pg_dump -U postgres twenty > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Automatiza las copias de seguridad diarias
|
||||
### Automate Daily Backups
|
||||
|
||||
Añade a tu crontab (`crontab -e`):
|
||||
Add to your crontab (`crontab -e`):
|
||||
|
||||
```bash
|
||||
0 2 * * * docker exec twenty-postgres pg_dump -U postgres twenty > /backups/twenty_$(date +\%Y\%m\%d).sql
|
||||
```
|
||||
|
||||
### Restaura desde una copia de seguridad
|
||||
### Restore from Backup
|
||||
|
||||
1. Detén la aplicación:
|
||||
1. Stop the application:
|
||||
|
||||
```bash
|
||||
docker compose stop twenty-server twenty-front
|
||||
```
|
||||
|
||||
2. Restaura la base de datos:
|
||||
2. Restore the database:
|
||||
|
||||
```bash
|
||||
docker exec -i twenty-postgres psql -U postgres twenty < backup_20240115.sql
|
||||
```
|
||||
|
||||
3. Reinicia los servicios:
|
||||
3. Restart services:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Mejores prácticas de copias de seguridad
|
||||
### Backup Best Practices
|
||||
|
||||
* **Prueba las restauraciones con regularidad** — verifica que las copias de seguridad realmente funcionen
|
||||
* **Almacena las copias de seguridad fuera del sitio** — usa almacenamiento en la nube (S3, GCS, etc.)
|
||||
* **Cifra los datos sensibles** — protege las copias de seguridad con cifrado
|
||||
* **Conserva múltiples copias** — mantén copias de seguridad diarias, semanales y mensuales
|
||||
* **Test restores regularly** — verify backups actually work
|
||||
* **Store backups off-site** — use cloud storage (S3, GCS, etc.)
|
||||
* **Encrypt sensitive data** — protect backups with encryption
|
||||
* **Retain multiple copies** — keep daily, weekly, and monthly backups
|
||||
|
||||
## Solución de Problemas
|
||||
## Troubleshooting
|
||||
|
||||
Si encuentras algún problema, consulta [Solución de Problemas](/l/es/developers/self-host/capabilities/troubleshooting) para ver soluciones.
|
||||
If you encounter any problem, check [Troubleshooting](/l/es/developers/self-host/capabilities/troubleshooting) for solutions.
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
---
|
||||
title: Configuración
|
||||
title: Setup
|
||||
---
|
||||
|
||||
# Gestión de Configuración
|
||||
# Configuration Management
|
||||
|
||||
<Warning>
|
||||
**¿Instalando por primera vez?** Siga la [guía de instalación de Docker Compose](/l/es/developers/self-host/capabilities/docker-compose) para ejecutar Twenty, luego regrese aquí para la configuración.
|
||||
**First time installing?** Follow the [Docker Compose installation guide](/l/es/developers/self-host/capabilities/docker-compose) to get Twenty running, then return here for configuration.
|
||||
</Warning>
|
||||
|
||||
Twenty ofrece **dos modos de configuración** para adaptarse a diferentes necesidades de implementación:
|
||||
Twenty offers **two configuration modes** to suit different deployment needs:
|
||||
|
||||
**Acceso al panel de administración:** Solo los usuarios con privilegios de administrador (`canAccessFullAdminPanel: true`) pueden acceder a la interfaz de configuración.
|
||||
**Admin panel access:** Only users with admin privileges (`canAccessFullAdminPanel: true`) can access the configuration interface.
|
||||
|
||||
## 1. Configuración del Panel de Administración (Predeterminado)
|
||||
## 1. Admin Panel Configuration (Default)
|
||||
|
||||
```bash
|
||||
IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
||||
```
|
||||
|
||||
**La mayoría de las configuraciones se realizan a través de la interfaz** después de la instalación:
|
||||
**Most configuration happens through the UI** after installation:
|
||||
|
||||
1. Acceda a su instancia de Twenty (normalmente `http://localhost:3000`)
|
||||
2. Vaya a **Configuración / Panel de Administración / Variables de Configuración**
|
||||
3. Configure integraciones, correo electrónico, almacenamiento y más
|
||||
4. Los cambios se aplican inmediatamente (dentro de 15 segundos para implementaciones multicontenedor)
|
||||
1. Access your Twenty instance (usually `http://localhost:3000`)
|
||||
2. Go to **Settings / Admin Panel / Configuration Variables**
|
||||
3. Configure integrations, email, storage, and more
|
||||
4. Changes take effect immediately (within 15 seconds for multi-container deployments)
|
||||
|
||||
<Warning>
|
||||
**Implementaciones Multicontenedor:** Al usar la configuración de base de datos (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), tanto los contenedores del servidor como los de trabajo leen de la misma base de datos. Los cambios en el panel de administración afectan a ambos automáticamente, eliminando la necesidad de duplicar las variables de entorno entre contenedores (excepto para las variables de infraestructura).
|
||||
**Multi-Container Deployments:** When using database configuration (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), both server and worker containers read from the same database. Admin panel changes affect both automatically, eliminating the need to duplicate environment variables between containers (except for infrastructure variables).
|
||||
</Warning>
|
||||
|
||||
**Qué se puede configurar a través del panel de administración:**
|
||||
**What you can configure through the admin panel:**
|
||||
|
||||
* **Autenticación** - OAuth de Google/Microsoft, configuración de contraseñas
|
||||
* **Correo Electrónico** - Configuración de SMTP, plantillas, verificación
|
||||
* **Almacenamiento** - Configuración S3, rutas de almacenamiento local
|
||||
* **Integraciones** - Gmail, Google Calendar, servicios de Microsoft
|
||||
* **Flujo de Trabajo y Limitación de Tasas** - Límites de ejecución, restricción de API
|
||||
* **Y mucho más...**
|
||||
* **Authentication** - Google/Microsoft OAuth, password settings
|
||||
* **Email** - SMTP settings, templates, verification
|
||||
* **Storage** - S3 configuration, local storage paths
|
||||
* **Integrations** - Gmail, Google Calendar, Microsoft services
|
||||
* **Workflow & Rate Limiting** - Execution limits, API throttling
|
||||
* **And much more...**
|
||||
|
||||

|
||||

|
||||
|
||||
<Warning>
|
||||
Cada variable está documentada con descripciones en su panel de administración en **Configuración → Panel de Administración → Variables de Configuración**.
|
||||
Algunas configuraciones de infraestructura como las conexiones de base de datos (`PG_DATABASE_URL`), URLs del servidor (`SERVER_URL`), y secretos de la aplicación (`APP_SECRET`) solo se pueden configurar a través del archivo `.env`.
|
||||
Each variable is documented with descriptions in your admin panel at **Settings → Admin Panel → Configuration Variables**.
|
||||
Some infrastructure settings like database connections (`PG_DATABASE_URL`), server URLs (`SERVER_URL`), and app secrets (`APP_SECRET`) can only be configured via `.env` file.
|
||||
|
||||
[Referencia técnica completa →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
[Complete technical reference →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## 2. Configuración Solo de Entorno
|
||||
## 2. Environment-Only Configuration
|
||||
|
||||
```bash
|
||||
IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
|
||||
```
|
||||
|
||||
**Toda la configuración se gestiona a través de archivos `.env`:**
|
||||
**All configuration managed through `.env` files:**
|
||||
|
||||
1. Establezca `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` en su archivo `.env`
|
||||
2. Agregue todas las variables de configuración a su archivo `.env`
|
||||
3. Reinicie los contenedores para que los cambios tengan efecto
|
||||
4. El panel de administración mostrará los valores actuales pero no podrá modificarlos
|
||||
1. Set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` in your `.env` file
|
||||
2. Add all configuration variables to your `.env` file
|
||||
3. Restart containers for changes to take effect
|
||||
4. Admin panel will show current values but cannot modify them
|
||||
|
||||
## Modo de múltiples espacios de trabajo
|
||||
## Multi-Workspace Mode
|
||||
|
||||
De forma predeterminada, Twenty se ejecuta en **modo de un solo espacio de trabajo** — ideal para la mayoría de las implementaciones autoalojadas en las que necesitas una instancia de CRM para tu organización.
|
||||
By default, Twenty runs in **single-workspace mode** — ideal for most self-hosted deployments where you need one CRM instance for your organization.
|
||||
|
||||
### Modo de un solo espacio de trabajo (predeterminado)
|
||||
### Single-Workspace Mode (Default)
|
||||
|
||||
```bash
|
||||
IS_MULTIWORKSPACE_ENABLED=false # default
|
||||
```
|
||||
|
||||
* Un espacio de trabajo por instancia de Twenty
|
||||
* El primer usuario se convierte automáticamente en administrador con privilegios completos (`canImpersonate` y `canAccessFullAdminPanel`)
|
||||
* Los nuevos registros se deshabilitan después de crear el primer espacio de trabajo
|
||||
* Estructura de URL simple: `https://your-domain.com`
|
||||
* One workspace per Twenty instance
|
||||
* First user automatically becomes admin with full privileges (`canImpersonate` and `canAccessFullAdminPanel`)
|
||||
* New signups are disabled after the first workspace is created
|
||||
* Simple URL structure: `https://your-domain.com`
|
||||
|
||||
### Habilitar el modo de múltiples espacios de trabajo
|
||||
### Enabling Multi-Workspace Mode
|
||||
|
||||
```bash
|
||||
IS_MULTIWORKSPACE_ENABLED=true
|
||||
DEFAULT_SUBDOMAIN=app # default value
|
||||
```
|
||||
|
||||
Habilita el modo de múltiples espacios de trabajo para implementaciones tipo SaaS en las que varios equipos independientes necesitan sus propios espacios de trabajo en la misma instancia de Twenty.
|
||||
Enable multi-workspace mode for SaaS-like deployments where multiple independent teams need their own workspaces on the same Twenty instance.
|
||||
|
||||
**Diferencias clave respecto al modo de un solo espacio de trabajo:**
|
||||
**Key differences from single-workspace mode:**
|
||||
|
||||
* Se pueden crear varios espacios de trabajo en la misma instancia
|
||||
* Cada espacio de trabajo obtiene su propio subdominio (p. ej., `sales.your-domain.com`, `marketing.your-domain.com`)
|
||||
* Los usuarios se registran e inician sesión en `{DEFAULT_SUBDOMAIN}.your-domain.com` (p. ej., `app.your-domain.com`)
|
||||
* Sin privilegios de administrador automáticos — el primer usuario de cada espacio de trabajo es un usuario normal
|
||||
* Configuraciones específicas del espacio de trabajo, como subdominio y dominio personalizado, están disponibles en la configuración del espacio de trabajo
|
||||
* Multiple workspaces can be created on the same instance
|
||||
* Each workspace gets its own subdomain (e.g., `sales.your-domain.com`, `marketing.your-domain.com`)
|
||||
* Users sign up and log in at `{DEFAULT_SUBDOMAIN}.your-domain.com` (e.g., `app.your-domain.com`)
|
||||
* No automatic admin privileges — first user in each workspace is a regular user
|
||||
* Workspace-specific settings like subdomain and custom domain become available in workspace settings
|
||||
|
||||
<Warning>
|
||||
**Configuración solo por entorno:** `IS_MULTIWORKSPACE_ENABLED` solo se puede configurar mediante el archivo `.env` y requiere un reinicio. No se puede cambiar a través del panel de administración.
|
||||
**Environment-only setting:** `IS_MULTIWORKSPACE_ENABLED` can only be configured via `.env` file and requires a restart. It cannot be changed through the admin panel.
|
||||
</Warning>
|
||||
|
||||
### Configuración de DNS para múltiples espacios de trabajo
|
||||
### DNS Configuration for Multi-Workspace
|
||||
|
||||
Al usar el modo de múltiples espacios de trabajo, configura tu DNS con un registro comodín para permitir la creación dinámica de subdominios:
|
||||
When using multi-workspace mode, configure your DNS with a wildcard record to allow dynamic subdomain creation:
|
||||
|
||||
```
|
||||
*.your-domain.com -> your-server-ip
|
||||
```
|
||||
|
||||
Esto habilita el enrutamiento automático de subdominios para nuevos espacios de trabajo sin configuración manual de DNS.
|
||||
This enables automatic subdomain routing for new workspaces without manual DNS configuration.
|
||||
|
||||
### Restricción de la creación de espacios de trabajo
|
||||
### Restricting Workspace Creation
|
||||
|
||||
En el modo de múltiples espacios de trabajo, es posible que quieras limitar quién puede crear nuevos espacios de trabajo:
|
||||
In multi-workspace mode, you may want to limit who can create new workspaces:
|
||||
|
||||
```bash
|
||||
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
|
||||
```
|
||||
|
||||
Cuando está habilitado, solo los usuarios con `canAccessFullAdminPanel` pueden crear espacios de trabajo adicionales. Los usuarios aún pueden crear su primer espacio de trabajo durante el registro inicial.
|
||||
When enabled, only users with `canAccessFullAdminPanel` can create additional workspaces. Users can still create their first workspace during initial signup.
|
||||
|
||||
## Integración con Gmail y Google Calendar
|
||||
## Gmail & Google Calendar Integration
|
||||
|
||||
### Crear Proyecto en Google Cloud
|
||||
### Create Google Cloud Project
|
||||
|
||||
1. Vaya a [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Cree un nuevo proyecto o seleccione uno existente
|
||||
3. Habilite estas APIs:
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select existing one
|
||||
3. Enable these APIs:
|
||||
|
||||
* [Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com)
|
||||
* [Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com)
|
||||
* [People API](https://console.cloud.google.com/apis/library/people.googleapis.com)
|
||||
|
||||
### Configurar OAuth
|
||||
### Configure OAuth
|
||||
|
||||
1. Vaya a [Credenciales](https://console.cloud.google.com/apis/credentials)
|
||||
2. Cree un ID de Cliente OAuth 2.0
|
||||
3. Agregue estas URIs de redirección:
|
||||
* `https://{your-domain}/auth/google/redirect` (para SSO)
|
||||
* `https://{your-domain}/auth/google-apis/get-access-token` (para integraciones)
|
||||
1. Go to [Credentials](https://console.cloud.google.com/apis/credentials)
|
||||
2. Create OAuth 2.0 Client ID
|
||||
3. Add these redirect URIs:
|
||||
* `https://{your-domain}/auth/google/redirect` (for SSO)
|
||||
* `https://{your-domain}/auth/google-apis/get-access-token` (for integrations)
|
||||
|
||||
### Configurar en Twenty
|
||||
### Configure in Twenty
|
||||
|
||||
1. Vaya a **Configuración → Panel de Administración → Variables de Configuración**
|
||||
2. Encuentre la sección **Google Auth**
|
||||
3. Establezca estas variables:
|
||||
1. Go to **Settings → Admin Panel → Configuration Variables**
|
||||
2. Find the **Google Auth** section
|
||||
3. Set these variables:
|
||||
* `MESSAGING_PROVIDER_GMAIL_ENABLED=true`
|
||||
* `CALENDAR_PROVIDER_GOOGLE_ENABLED=true`
|
||||
* `AUTH_GOOGLE_CLIENT_ID={client-id}`
|
||||
@@ -149,35 +149,35 @@ Cuando está habilitado, solo los usuarios con `canAccessFullAdminPanel` pueden
|
||||
* `AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token`
|
||||
|
||||
<Warning>
|
||||
**Modo solo de entorno:** Si establece `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, agregue estas variables a su archivo `.env` en su lugar.
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
|
||||
**Ámbitos requeridos** (configurados automáticamente):
|
||||
[Ver código fuente relevante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
|
||||
**Required scopes** (automatically configured):
|
||||
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
|
||||
|
||||
* `https://www.googleapis.com/auth/calendar.events`
|
||||
* `https://www.googleapis.com/auth/gmail.readonly`
|
||||
* `https://www.googleapis.com/auth/profile.emails.read`
|
||||
|
||||
### Si su aplicación está en modo de prueba
|
||||
### If your app is in test mode
|
||||
|
||||
Si su aplicación está en modo de prueba, deberá agregar usuarios de prueba a su proyecto.
|
||||
If your app is in test mode, you will need to add test users to your project.
|
||||
|
||||
En [Pantalla de consentimiento OAuth](https://console.cloud.google.com/apis/credentials/consent), agregue sus usuarios de prueba en la sección "Usuarios de prueba".
|
||||
Under [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent), add your test users to the "Test users" section.
|
||||
|
||||
## Integración con Microsoft 365
|
||||
## Microsoft 365 Integration
|
||||
|
||||
<Warning>
|
||||
Los usuarios deben tener una [licencia de Microsoft 365](https://admin.microsoft.com/Adminportal/Home) para poder usar la API de Calendar y Messaging. No podrán sincronizar su cuenta en Twenty sin una.
|
||||
Users must have a [Microsoft 365 Licence](https://admin.microsoft.com/Adminportal/Home) to be able to use the Calendar and Messaging API. They will not be able to sync their account on Twenty without one.
|
||||
</Warning>
|
||||
|
||||
### Cree un proyecto en Microsoft Azure
|
||||
### Create a project in Microsoft Azure
|
||||
|
||||
Necesitará crear un proyecto en [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) y obtener las credenciales.
|
||||
You will need to create a project in [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) and get the credentials.
|
||||
|
||||
### Habilitar APIs
|
||||
### Enable APIs
|
||||
|
||||
En la Consola de Microsoft Azure habilite las siguientes APIs en "Permisos":
|
||||
On Microsoft Azure Console enable the following APIs in "Permissions":
|
||||
|
||||
* Microsoft Graph: Mail.ReadWrite
|
||||
* Microsoft Graph: Mail.Send
|
||||
@@ -188,20 +188,20 @@ En la Consola de Microsoft Azure habilite las siguientes APIs en "Permisos":
|
||||
* Microsoft Graph: profile
|
||||
* Microsoft Graph: offline_access
|
||||
|
||||
Nota: "Mail.ReadWrite" y "Mail.Send" solo son obligatorios si desea enviar correos electrónicos usando nuestras acciones de flujo de trabajo. Puede usar "Mail.Read" en su lugar si solo desea recibir correos electrónicos.
|
||||
Note: "Mail.ReadWrite" and "Mail.Send" are only mandatory if you want to send emails using our workflow actions. You can use "Mail.Read" instead if you only want to receive emails.
|
||||
|
||||
### URIs de redirección autorizadas
|
||||
### Authorized redirect URIs
|
||||
|
||||
Necesita agregar las siguientes URIs de redirección a su proyecto:
|
||||
You need to add the following redirect URIs to your project:
|
||||
|
||||
* `https://{your-domain}/auth/microsoft/redirect` si quieres usar el inicio de sesión único de Microsoft
|
||||
* `https://{your-domain}/auth/microsoft/redirect` if you want to use Microsoft SSO
|
||||
* `https://{your-domain}/auth/microsoft-apis/get-access-token`
|
||||
|
||||
### Configurar en Twenty
|
||||
### Configure in Twenty
|
||||
|
||||
1. Vaya a **Configuración → Panel de Administración → Variables de Configuración**
|
||||
2. Encuentre la sección **Microsoft Auth**
|
||||
3. Establezca estas variables:
|
||||
1. Go to **Settings → Admin Panel → Configuration Variables**
|
||||
2. Find the **Microsoft Auth** section
|
||||
3. Set these variables:
|
||||
* `MESSAGING_PROVIDER_MICROSOFT_ENABLED=true`
|
||||
* `CALENDAR_PROVIDER_MICROSOFT_ENABLED=true`
|
||||
* `AUTH_MICROSOFT_ENABLED=true`
|
||||
@@ -211,32 +211,32 @@ Necesita agregar las siguientes URIs de redirección a su proyecto:
|
||||
* `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://{your-domain}/auth/microsoft-apis/get-access-token`
|
||||
|
||||
<Warning>
|
||||
**Modo solo de entorno:** Si establece `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, agregue estas variables a su archivo `.env` en su lugar.
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
|
||||
### Configurar ámbitos
|
||||
### Configure scopes
|
||||
|
||||
[Ver código fuente relevante](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts#L2-L9)
|
||||
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts#L2-L9)
|
||||
|
||||
* 'openid'
|
||||
* 'correo Electrónico'
|
||||
* 'perfil'
|
||||
* 'email'
|
||||
* 'profile'
|
||||
* 'offline_access'
|
||||
* 'Mail.ReadWrite'
|
||||
* 'Mail.Send'
|
||||
* 'Calendars.Read'
|
||||
|
||||
### Si su aplicación está en modo de prueba
|
||||
### If your app is in test mode
|
||||
|
||||
Si su aplicación está en modo de prueba, deberá agregar usuarios de prueba a su proyecto.
|
||||
If your app is in test mode, you will need to add test users to your project.
|
||||
|
||||
Agregue sus usuarios de prueba a la sección "Usuarios y grupos".
|
||||
Add your test users to the "Users and groups" section.
|
||||
|
||||
## Trabajos en segundo plano para Calendarios y Mensajes
|
||||
## Background Jobs for Calendar & Messaging
|
||||
|
||||
Después de configurar las integraciones de Gmail, Google Calendar, o Microsoft 365, necesita iniciar los trabajos en segundo plano que sincronizan los datos.
|
||||
After configuring Gmail, Google Calendar, or Microsoft 365 integrations, you need to start the background jobs that sync data.
|
||||
|
||||
Registre los siguientes trabajos recurrentes en su contenedor de trabajo:
|
||||
Register the following recurring jobs in your worker container:
|
||||
|
||||
```bash
|
||||
# from your worker container
|
||||
@@ -249,15 +249,15 @@ yarn command:prod cron:calendar:ongoing-stale
|
||||
yarn command:prod cron:workflow:automated-cron-trigger
|
||||
```
|
||||
|
||||
## Configuración de Correo Electrónico
|
||||
## Email Configuration
|
||||
|
||||
1. Vaya a **Configuración → Panel de Administración → Variables de Configuración**
|
||||
2. Encuentre la sección **Correo Electrónico**
|
||||
3. Configure su configuración SMTP:
|
||||
1. Go to **Settings → Admin Panel → Configuration Variables**
|
||||
2. Find the **Email** section
|
||||
3. Configure your SMTP settings:
|
||||
|
||||
<ArticleTabs label1="Gmail" label2="Office365" label3="Smtp4dev">
|
||||
<ArticleTab>
|
||||
Necesitará proporcionar una [Contraseña de Aplicación](https://support.google.com/accounts/answer/185833).
|
||||
You will need to provision an [App Password](https://support.google.com/accounts/answer/185833).
|
||||
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
@@ -267,7 +267,7 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
Tenga en cuenta que si tiene la autenticación de dos factores habilitada, necesitará proporcionar una [Contraseña de Aplicación](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
|
||||
Keep in mind that if you have 2FA enabled, you will need to provision an [App Password](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
|
||||
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=smtp.office365.com
|
||||
@@ -277,11 +277,11 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
**smtp4dev** es un servidor de correo SMTP falso para desarrollo y pruebas.
|
||||
**smtp4dev** is a fake SMTP email server for development and testing.
|
||||
|
||||
* Ejecute la imagen de smtp4dev: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
|
||||
* Acceda a la interfaz de smtp4dev aquí: [http://localhost:8090](http://localhost:8090)
|
||||
* Establezca las siguientes variables:
|
||||
* Run the smtp4dev image: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
|
||||
* Access the smtp4dev ui here: [http://localhost:8090](http://localhost:8090)
|
||||
* Set the following variables:
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=localhost
|
||||
* EMAIL_SMTP_PORT=2525
|
||||
@@ -289,49 +289,5 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
</ArticleTabs>
|
||||
|
||||
<Warning>
|
||||
**Modo solo de entorno:** Si establece `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, agregue estas variables a su archivo `.env` en su lugar.
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
|
||||
## Funciones de lógica
|
||||
|
||||
Twenty admite funciones de lógica para flujos de trabajo y lógica personalizada. El entorno de ejecución se configura mediante la variable de entorno `SERVERLESS_TYPE`.
|
||||
|
||||
<Warning>
|
||||
**Aviso de seguridad:** El controlador local (`SERVERLESS_TYPE=LOCAL`) ejecuta código directamente en el host en un proceso de Node.js sin aislamiento. Solo debe utilizarse para código de confianza en desarrollo. Para implementaciones de producción que manejen código no confiable, recomendamos encarecidamente usar `SERVERLESS_TYPE=LAMBDA` o `SERVERLESS_TYPE=DISABLED`.
|
||||
</Warning>
|
||||
|
||||
### Controladores disponibles
|
||||
|
||||
| Controlador | Variable de entorno | Caso de uso | Nivel de seguridad |
|
||||
| ----------- | -------------------------- | ------------------------------------------------ | -------------------------------------- |
|
||||
| Desactivado | `SERVERLESS_TYPE=DISABLED` | Desactivar completamente las funciones de lógica | N/A |
|
||||
| Local | `SERVERLESS_TYPE=LOCAL` | Entornos de desarrollo y de confianza | Bajo (sin aislamiento) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Producción con código no confiable | Alto (aislamiento a nivel de hardware) |
|
||||
|
||||
### Configuración recomendada
|
||||
|
||||
**Para desarrollo:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=LOCAL # default
|
||||
```
|
||||
|
||||
**Para producción (AWS):**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=LAMBDA
|
||||
SERVERLESS_LAMBDA_REGION=us-east-1
|
||||
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**Para desactivar las funciones de lógica:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
<Note>
|
||||
Al usar `SERVERLESS_TYPE=DISABLED`, cualquier intento de ejecutar una función de lógica devolverá un error. Esto es útil si desea ejecutar Twenty sin capacidades de funciones de lógica.
|
||||
</Note>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
---
|
||||
title: Solución de problemas
|
||||
title: Troubleshooting
|
||||
---
|
||||
|
||||
## Solución de Problemas
|
||||
## Troubleshooting
|
||||
|
||||
Si encuentra algún problema al configurar el entorno para el desarrollo, al actualizar su instancia o al autoalojar, aquí hay algunas soluciones para problemas comunes.
|
||||
If you encounter any problem while setting up environment for development, upgrading your instance or self-hosting,
|
||||
here are some solutions for common problems.
|
||||
|
||||
### Autoalojamiento
|
||||
### Self-hosting
|
||||
|
||||
#### La primera instalación resulta en `fallo de autenticación de contraseña para el usuario "postgres"`
|
||||
#### First install results in `password authentication failed for user "postgres"`
|
||||
|
||||
🚨 **IMPORTANTE: Esta solución es SOLO para instalaciones nuevas** 🚨
|
||||
Si tiene una instancia de Twenty existente con datos de producción, **NO** siga estos pasos ya que borrarán permanentemente su base de datos.
|
||||
🚨 **IMPORTANT: This solution is ONLY for fresh installations** 🚨
|
||||
If you have an existing Twenty instance with production data, **DO NOT** follow these steps as they will permanently delete your database!
|
||||
|
||||
Al instalar Twenty por primera vez, es posible que desee cambiar la contraseña predeterminada de la base de datos.
|
||||
La contraseña que establezca durante la primera instalación se almacena permanentemente en el volumen de base de datos. Si más tarde intenta cambiar esta contraseña en su configuración sin eliminar el volumen anterior, obtendrá errores de autenticación porque la base de datos todavía está usando la contraseña original.
|
||||
While installing Twenty for the first time, you might want to change the default database password.
|
||||
The password you set during the first installation becomes permanently stored in the database volume. If you later try to change this password in your configuration without removing the old volume, you'll get authentication errors because the database is still using the original password.
|
||||
|
||||
⚠️ ADVERTENCIA: ¡Seguir los pasos borrará PERMANENTEMENTE todos los datos de la base de datos! ⚠️
|
||||
Prosiga solo si se trata de una instalación nueva sin datos importantes.
|
||||
⚠️ WARNING: Following steps will PERMANENTLY DELETE all database data! ⚠️
|
||||
Only proceed if this is a fresh installation with no important data.
|
||||
|
||||
Para actualizar el `PG_DATABASE_PASSWORD` necesita:
|
||||
In order to update the `PG_DATABASE_PASSWORD` you need to:
|
||||
|
||||
```sh
|
||||
# Update the PG_DATABASE_PASSWORD in .env
|
||||
@@ -27,33 +28,33 @@ docker compose down --volumes
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Rupturas de línea de CR encontradas [Windows]
|
||||
#### CR line breaks found [Windows]
|
||||
|
||||
Esto se debe a los caracteres de salto de línea de Windows y a la configuración de Git. Intente ejecutar:
|
||||
This is due to the line break characters of Windows and the git configuration. Try running:
|
||||
|
||||
```
|
||||
git config --global core.autocrlf false
|
||||
```
|
||||
|
||||
Luego elimine el repositorio y clónelo de nuevo.
|
||||
Then delete the repository and clone it again.
|
||||
|
||||
#### Esquema de metadatos faltante
|
||||
#### Missing metadata schema
|
||||
|
||||
Durante la instalación de Twenty, debe aprovisionar su base de datos postgres con los esquemas, extensiones y usuarios correctos.
|
||||
Si ha ejecutado con éxito este aprovisionamiento, debería tener esquemas `default` y `metadata` en su base de datos.
|
||||
Si no los tiene, asegúrese de no tener más de una instancia de postgres ejecutándose en su computadora.
|
||||
During Twenty installation, you need to provision your postgres database with the right schemas, extensions, and users.
|
||||
If you're successful in running this provisioning, you should have `default` and `metadata` schemas in your database.
|
||||
If you don't, make sure you don't have more than one postgres instance running on your computer.
|
||||
|
||||
#### No se puede encontrar el módulo 'twenty-emails' o sus declaraciones de tipo correspondientes.
|
||||
#### Cannot find module 'twenty-emails' or its corresponding type declarations.
|
||||
|
||||
Tienes que construir el paquete `twenty-emails` antes de ejecutar la inicialización de la base de datos con `npx nx run twenty-emails:build`
|
||||
You have to build the package `twenty-emails` before running the initialization of the database with `npx nx run twenty-emails:build`
|
||||
|
||||
#### Falta el paquete twenty-x
|
||||
#### Missing twenty-x package
|
||||
|
||||
Asegúrese de ejecutar yarn en el directorio raíz y luego ejecute `npx nx server:dev twenty-server`. Si esto aún no funciona, intente construir el paquete faltante manualmente.
|
||||
Make sure to run yarn in the root directory and then run `npx nx server:dev twenty-server`. If this still doesn't work try building the missing package manually.
|
||||
|
||||
#### Lint on Save no funciona
|
||||
#### Lint on Save not working
|
||||
|
||||
Esto debería funcionar sin configuración adicional con la extensión de ESLint instalada. Si esto no funciona, intente agregar esto a su configuración de vscode (en el ámbito del contenedor de desarrollo):
|
||||
This should work out of the box with the eslint extension installed. If this doesn't work try adding this to your vscode setting (on the dev container scope):
|
||||
|
||||
```
|
||||
"editor.codeActionsOnSave": {
|
||||
@@ -63,85 +64,85 @@ Esto debería funcionar sin configuración adicional con la extensión de ESLint
|
||||
}
|
||||
```
|
||||
|
||||
#### Al ejecutar `npx nx start` o `npx nx start twenty-front`, se produce un error de falta de memoria
|
||||
#### While running `npx nx start` or `npx nx start twenty-front`, Out of memory error is thrown
|
||||
|
||||
En `packages/twenty-front/.env` descomente `VITE_DISABLE_TYPESCRIPT_CHECKER=true` y `VITE_DISABLE_ESLINT_CHECKER=true` para deshabilitar las comprobaciones en segundo plano y así reducir la cantidad de RAM necesaria.
|
||||
In `packages/twenty-front/.env` uncomment `VITE_DISABLE_TYPESCRIPT_CHECKER=true` to disable background checks thus reducing amount of needed RAM.
|
||||
|
||||
**Si no funciona:**
|
||||
Ejecute solo los servicios que necesite, en lugar de `npx nx start`. Por ejemplo, si trabaja en el servidor, ejecute solo `npx nx worker twenty-server`
|
||||
**If it does not work:**
|
||||
Run only the services you need, instead of `npx nx start`. For instance, if you work on the server, run only `npx nx worker twenty-server`
|
||||
|
||||
**Si no funciona:**
|
||||
Si intentó ejecutar solo `npx nx run twenty-server:start` en WSL y falla con el siguiente error de memoria:
|
||||
**If it does not work:**
|
||||
If you tried to run only `npx nx run twenty-server:start` on WSL and it's failing with the below memory error:
|
||||
|
||||
`ERROR FATAL: Las marcas compactas ineficaces cercanas al límite del montón Falló la asignación - JavaScript heap out of memory`
|
||||
`FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`
|
||||
|
||||
La solución es ejecutar el siguiente comando en el terminal o agregarlo en el perfil .bashrc para configurarlo automáticamente:
|
||||
Workaround is to execute below command in terminal or add it in .bashrc profile to get setup automatically:
|
||||
|
||||
`export NODE_OPTIONS="--max-old-space-size=8192"`
|
||||
|
||||
La bandera --max-old-space-size=8192 establece un límite superior de 8GB para el montón de Node.js; su uso escala con la demanda de la aplicación.
|
||||
Referencia: https://stackoverflow.com/questions/56982005/where-do-i-set-node-options-max-old-space-size-2048
|
||||
The --max-old-space-size=8192 flag sets an upper limit of 8GB for the Node.js heap; usage scales with application demand.
|
||||
Reference: https://stackoverflow.com/questions/56982005/where-do-i-set-node-options-max-old-space-size-2048
|
||||
|
||||
**Si no funciona:**
|
||||
Investigue qué procesos están ocupando la mayor parte de la RAM de su máquina. En Twenty, notamos que algunas extensiones de VScode estaban ocupando mucha RAM, por lo que las desactivamos temporalmente.
|
||||
**If it does not work:**
|
||||
Investigate which processes are taking you most of your machine RAM. At Twenty, we noticed that some VScode extensions were taking a lot of RAM so we temporarily disable them.
|
||||
|
||||
**Si no funciona:**
|
||||
Reiniciar su máquina ayuda a limpiar procesos fantasma.
|
||||
**If it does not work:**
|
||||
Restart your machine helps to clean up ghost processes.
|
||||
|
||||
#### Mientras ejecuta `npx nx start` hay [0] y [1] extraños en los registros
|
||||
#### While running `npx nx start` there are weird [0] and [1] in logs
|
||||
|
||||
Es esperado, ya que el comando `npx nx start` está ejecutando más comandos detrás de escena.
|
||||
That's expected as command `npx nx start` is running more commands under the hood
|
||||
|
||||
#### No se envían correos electrónicos
|
||||
#### No emails are sent
|
||||
|
||||
La mayoría de las veces, se debe a que el `worker` no se está ejecutando en segundo plano. Intente ejecutar
|
||||
Most of the time, it's because the `worker` is not running in the background. Try to run
|
||||
|
||||
```
|
||||
npx nx worker twenty-server
|
||||
```
|
||||
|
||||
#### No se puede conectar mi cuenta de Microsoft 365
|
||||
#### Cannot connect my Microsoft 365 account
|
||||
|
||||
La mayoría de las veces, se debe a que su administrador no ha habilitado la licencia de Microsoft 365 para su cuenta. Verifique [https://admin.microsoft.com/](https://admin.microsoft.com/Adminportal/Home).
|
||||
Most of the time, it's because your admin has not enabled the Microsoft 365 Licence for your account. Check [https://admin.microsoft.com/](https://admin.microsoft.com/Adminportal/Home).
|
||||
|
||||
Si tiene un código de error `AADSTS50020`, probablemente significa que está usando una cuenta de Microsoft personal. Esto aún no es compatible. Más información [aquí](https://learn.microsoft.com/fr-fr/troubleshoot/entra/entra-id/app-integration/error-code-aadsts50020-user-account-identity-provider-does-not-exist)
|
||||
If you have an error code `AADSTS50020`, it probably means that you are using a personal Microsoft account. This is not supported yet. More info [here](https://learn.microsoft.com/fr-fr/troubleshoot/entra/entra-id/app-integration/error-code-aadsts50020-user-account-identity-provider-does-not-exist)
|
||||
|
||||
#### Mientras ejecuta `yarn` aparecen advertencias en la consola
|
||||
#### While running `yarn` warnings appear in console
|
||||
|
||||
Las advertencias informan sobre la carga de dependencias adicionales que no están explicitadas en `package.json`, así que mientras no aparezca un error crítico, todo debería funcionar como se espera.
|
||||
Warnings are informing about pulling additional dependencies which aren't explicitly stated in `package.json`, so as long as no breaking error appears, everything should work as expected.
|
||||
|
||||
#### Cuando el usuario accede a la página de inicio de sesión, aparece un error sobre un usuario no autorizado que intenta acceder al espacio de trabajo en los registros
|
||||
#### When user accesses login page, error about unauthorized user trying to access workspace appears in logs
|
||||
|
||||
Es esperado ya que el usuario no está autorizado cuando cierra sesión porque su identidad no está verificada.
|
||||
That's expected as user is unauthorized when logged out since its identity is not verified.
|
||||
|
||||
#### ¿Cómo comprobar si su worker está funcionando?
|
||||
#### How to check if your worker is running?
|
||||
|
||||
* Vaya a [webhook-test.com](https://webhook-test.com/) y copie **Su URL de Webhook Única**.
|
||||
* Go to [webhook-test.com](https://webhook-test.com/) and copy **Your Unique Webhook URL**.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/self-hosting/webhook-test.jpg" alt="Prueba de webhook" />
|
||||
<img src="/images/docs/developers/self-hosting/webhook-test.jpg" alt="Webhook test" />
|
||||
</div>
|
||||
|
||||
* Abra la aplicación Twenty, navegue a `/settings` y active el interruptor **Avanzado** en la parte inferior izquierda de la pantalla.
|
||||
* Cree un nuevo webhook.
|
||||
* Pegue **Su URL de Webhook Única** en el campo **Url de EndPoint** en Twenty. Establezca los **Filtros** en `Companies` y `Created`.
|
||||
* Open your Twenty app, navigate to `/settings`, and enable the **Advanced** toggle at the bottom left of the screen.
|
||||
* Create a new webhook.
|
||||
* Paste **Your Unique Webhook URL** in the **Endpoint Url** field in Twenty. Set the **Filters** to `Companies` and `Created`.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/self-hosting/webhook-settings.jpg" alt="Configuraciones del webhook" />
|
||||
<img src="/images/docs/developers/self-hosting/webhook-settings.jpg" alt="Webhook settings" />
|
||||
</div>
|
||||
|
||||
* Vaya a `/objects/companies` y cree un nuevo registro de empresa.
|
||||
* Regrese a [webhook-test.com](https://webhook-test.com/) y verifique si se ha recibido una nueva **solicitud POST**.
|
||||
* Go to `/objects/companies` and create a new company record.
|
||||
* Return to [webhook-test.com](https://webhook-test.com/) and check if a new **POST request** has been received.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/self-hosting/webhook-test-result.jpg" alt="Resultado de la prueba del webhook" />
|
||||
<img src="/images/docs/developers/self-hosting/webhook-test-result.jpg" alt="Webhook test result" />
|
||||
</div>
|
||||
|
||||
* Si se recibe una **solicitud POST**, su worker está funcionando con éxito. De lo contrario, debe solucionar problemas de su worker.
|
||||
* If a **POST request** is received, your worker is running successfully. Otherwise, you need to troubleshoot your worker.
|
||||
|
||||
#### El front-end no comienza y devuelve el error TS5042: La opción 'project' no se puede mezclar con archivos fuente en una línea de comando
|
||||
#### Front-end fails to start and returns error TS5042: Option 'project' cannot be mixed with source files on a command line
|
||||
|
||||
Comente el plugin checker en `packages/twenty-ui/vite-config.ts` como en el ejemplo a continuación
|
||||
Comment out checker plugin in `packages/twenty-ui/vite-config.ts` like in example below
|
||||
|
||||
```
|
||||
plugins: [
|
||||
@@ -165,62 +166,62 @@ plugins: [
|
||||
],
|
||||
```
|
||||
|
||||
#### Panel de administración no accesible
|
||||
#### Admin panel not accessible
|
||||
|
||||
Ejecute `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email protected]';` en el contenedor de la base de datos para obtener acceso al panel de administración.
|
||||
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email protected]';` in database container to get access to admin panel.
|
||||
|
||||
### 1-clic con Docker Compose
|
||||
### 1-click Docker compose
|
||||
|
||||
#### No se puede iniciar sesión
|
||||
#### Unable to Log In
|
||||
|
||||
Si no puedes iniciar sesión después de la configuración:
|
||||
If you can't log in after setup:
|
||||
|
||||
1. Ejecución de los siguientes comandos:
|
||||
1. Run the following commands:
|
||||
```bash
|
||||
docker exec -it twenty-server-1 yarn
|
||||
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
|
||||
```
|
||||
2. Reinicie los contenedores de Docker:
|
||||
2. Restart the Docker containers:
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Tenga en cuenta que el comando database:reset borrará toda su base de datos y la recreará desde cero.
|
||||
Note the database:reset command will completely erase your database and recreate it from scratch.
|
||||
|
||||
#### Problemas de conexión detrás de un Proxy Reverso
|
||||
#### Connection Issues Behind a Reverse Proxy
|
||||
|
||||
Si está ejecutando Twenty detrás de un proxy inverso y experimenta problemas de conexión:
|
||||
If you're running Twenty behind a reverse proxy and experiencing connection issues:
|
||||
|
||||
1. **Verifique SERVER_URL:**
|
||||
1. **Verify SERVER_URL:**
|
||||
|
||||
Asegúrese de que `SERVER_URL` en su archivo `.env` coincida con su URL de acceso externa, incluyendo `https` si SSL está habilitado.
|
||||
Ensure `SERVER_URL` in your `.env` file matches your external access URL, including `https` if SSL is enabled.
|
||||
|
||||
2. **Verifique la configuración del Proxy Reverso:**
|
||||
2. **Check Reverse Proxy Settings:**
|
||||
|
||||
* Confirme que su proxy reverso está reenviando correctamente las solicitudes al servidor de Twenty.
|
||||
* Asegúrese de que los encabezados como `X-Forwarded-For` y `X-Forwarded-Proto` estén configurados correctamente.
|
||||
* Confirm that your reverse proxy is correctly forwarding requests to the Twenty server.
|
||||
* Ensure headers like `X-Forwarded-For` and `X-Forwarded-Proto` are properly set.
|
||||
|
||||
3. **Reinicie los Servicios:**
|
||||
3. **Restart Services:**
|
||||
|
||||
Después de hacer cambios, reinicie tanto el proxy inverso como los contenedores de Twenty.
|
||||
After making changes, restart both the reverse proxy and Twenty containers.
|
||||
|
||||
#### Error al cargar una imagen - permiso denegado
|
||||
#### Error when uploading an image - permission denied
|
||||
|
||||
Cambiar la propiedad de la carpeta de datos en el host de raíz a otro usuario y grupo resuelve este problema.
|
||||
Switching the data folder ownership on the host from root to another user and group resolves this problem.
|
||||
|
||||
## Obtención de Ayuda
|
||||
## Getting Help
|
||||
|
||||
Si enfrenta problemas no cubiertos en esta guía:
|
||||
If you encounter issues not covered in this guide:
|
||||
|
||||
* Verifique los Registros:
|
||||
* Check Logs:
|
||||
|
||||
Vea los registros del contenedor por mensajes de error:
|
||||
View container logs for error messages:
|
||||
|
||||
```bash
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
* Soporte Comunitario:
|
||||
* Community Support:
|
||||
|
||||
Póngase en contacto con la [comunidad de Twenty](https://github.com/twentyhq/twenty/issues) o [los canales de soporte](https://discord.gg/cx5n4Jzs57) para obtener asistencia.
|
||||
Reach out to the [Twenty community](https://github.com/twentyhq/twenty/issues) or [support channels](https://discord.gg/cx5n4Jzs57) for assistance.
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
---
|
||||
title: Guía de actualización
|
||||
title: Upgrade guide
|
||||
---
|
||||
|
||||
## Guías generales
|
||||
## General guidelines
|
||||
|
||||
**Asegúrese siempre de respaldar su base de datos antes de iniciar el proceso de actualización** ejecutando `docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql`.
|
||||
**Always make sure to back up your database before starting the upgrade process** by running `docker exec -it {db_container_name_or_id} pg_dumpall -U {postgres_user} > databases_backup.sql`.
|
||||
|
||||
Para restaurar el respaldo, ejecute `cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}`.
|
||||
To restore backup, run `cat databases_backup.sql | docker exec -i {db_container_name_or_id} psql -U {postgres_user}`.
|
||||
|
||||
Si usó Docker Compose, siga estos pasos:
|
||||
If you used Docker Compose, follow these steps:
|
||||
|
||||
1. En una terminal, en el host donde Twenty está funcionando, apague Twenty: `docker compose down`
|
||||
1. In a terminal, on the host where Twenty is running, turn off Twenty: `docker compose down`
|
||||
|
||||
2. Actualice la versión cambiando el valor de `TAG` en el archivo .env cerca de su docker-compose. ( Recomendamos consumir la versión `major.minor` como `v0.53` )
|
||||
2. Upgrade the version by changing the `TAG` value in the .env file near your docker-compose. ( We recommend consuming `major.minor` version such as `v0.53` )
|
||||
|
||||
3. Vuelva a conectar Twenty con `docker compose up -d`
|
||||
3. Bring Twenty back online with `docker compose up -d`
|
||||
|
||||
Si desea actualizar su instancia por algunas versiones, por ejemplo de v0.33.0 a v0.35.0, debe actualizar su instancia secuencialmente, en este ejemplo de v0.33.0 a v0.34.0, luego de v0.34.0 a v0.35.0.
|
||||
If you want to upgrade your instance by few versions, e.g. from v0.33.0 to v0.35.0, you have to upgrade your instance sequentially, in this example from v0.33.0 to v0.34.0, then from v0.34.0 to v0.35.0.
|
||||
|
||||
**Asegúrese de que después de cada versión actualizada tenga una copia de respaldo no corrupta.**
|
||||
**Make sure that after each upgraded version you have non-corrupted backup.**
|
||||
|
||||
## Pasos de actualización específicos por versión
|
||||
## Version-specific upgrade steps
|
||||
|
||||
## v1.0
|
||||
|
||||
¡Hola Twenty v1.0! 🎉
|
||||
Hello Twenty v1.0! 🎉
|
||||
|
||||
## v0.60
|
||||
|
||||
### Mejoras de rendimiento
|
||||
### Performance Enhancements
|
||||
|
||||
Todas las interacciones con la API de metadatos han sido optimizadas para un mejor rendimiento, particularmente para la manipulación de metadatos de objetos y operaciones de creación de espacios de trabajo.
|
||||
All interactions with the metadata API have been optimized for better performance, particularly for object metadata manipulation and workspace creation operations.
|
||||
|
||||
Hemos reestructurado nuestra estrategia de almacenamiento en caché para priorizar los aciertos de caché sobre las consultas de base de datos cuando sea posible, mejorando significativamente el rendimiento de las operaciones de la API de metadatos.
|
||||
We've refactored our caching strategy to prioritize cache hits over database queries when possible, significantly improving the performance of metadata API operations.
|
||||
|
||||
Si encuentra problemas de ejecución después de actualizar, es posible que deba vaciar su caché para asegurar que esté sincronizado con los cambios más recientes. Ejecute este comando en su contenedor del servidor de twenty:
|
||||
If you encounter any runtime issues after upgrading, you may need to flush your cache to ensure it's synchronized with the latest changes. Run this command in your twenty-server container:
|
||||
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
@@ -42,113 +42,113 @@ yarn command:prod cache:flush
|
||||
|
||||
### v0.55
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.55
|
||||
Upgrade your Twenty instance to use v0.55 image
|
||||
|
||||
Ya no necesita ejecutar ningún comando, la nueva imagen se encargará automáticamente de ejecutar todas las migraciones necesarias.
|
||||
You don't need to run any command anymore, the new image will automatically care about running all required migrations.
|
||||
|
||||
### Error: `El usuario no tiene permiso`
|
||||
### `User does not have permission` error
|
||||
|
||||
Si encuentra errores de autorización en la mayoría de solicitudes después de actualizar, es posible que deba vaciar su caché para recalcular los permisos más recientes.
|
||||
If you encounter authorization errors on most requests after upgrading, you may need to flush your cache to recompute the latest permissions.
|
||||
|
||||
En su contenedor `twenty-server`, ejecute:
|
||||
In your `twenty-server` container, run:
|
||||
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
```
|
||||
|
||||
Este problema es específico de esta versión de Twenty y no debería ser necesario para futuras actualizaciones.
|
||||
This issue is specific to this Twenty version and should not be required for future upgrades.
|
||||
|
||||
### v0.54
|
||||
|
||||
Desde la versión `0.53`, no se necesitan acciones manuales.
|
||||
Since version `0.53`, no manual actions needed.
|
||||
|
||||
#### Desaparición del esquema de metadatos
|
||||
#### Metadata schema deprecation
|
||||
|
||||
Hemos fusionado el esquema `metadata` en el esquema `core` para simplificar la recuperación de datos desde `TypeORM`.
|
||||
Hemos fusionado el paso del comando `migrate` dentro del comando `upgrade`. No recomendamos ejecutar `migrate` manualmente dentro de ninguno de sus contenedores de servidor/trabajador.
|
||||
We've merged the `metadata` schema into the `core` one to simplify data retrieval from `TypeORM`.
|
||||
We have merged the `migrate` command step within the `upgrade` command. We do not recommend running `migrate` manually within any of your server/worker containers.
|
||||
|
||||
### Desde v0.53
|
||||
### Since v0.53
|
||||
|
||||
A partir de `0.53`, la actualización se realiza de forma programática dentro del `DockerFile`, esto significa que de ahora en adelante, no debería necesitar ejecutar ningún comando manualmente.
|
||||
Starting from `0.53`, upgrade is programmatically done within the `DockerFile`, this means from now on, you shouldn't have to run any command manually anymore.
|
||||
|
||||
Asegúrese de seguir actualizando su instancia secuencialmente, sin omitir ninguna versión principal (por ejemplo, de `0.43.3` a `0.44.0` está permitido, pero de `0.43.1` a `0.45.0` no lo está), de lo contrario, podría provocar un desincronización de la versión del espacio de trabajo que podría resultar en errores de ejecución y funciones faltantes.
|
||||
Make sure to keep upgrading your instance sequentially, without skipping any major version (e.g. `0.43.3` to `0.44.0` is allowed, but `0.43.1` to `0.45.0` isn't), else could lead to workspace version desynchronization that could result in runtime error and missing functionality.
|
||||
|
||||
Para verificar si un espacio de trabajo se ha migrado correctamente, puede revisar su versión en la base de datos en la tabla `core.workspace`.
|
||||
To check if a workspace has been correctly migrated you can review its version in database in `core.workspace` table.
|
||||
|
||||
Siempre debería estar dentro del rango de la versión `major.minor` actual de su instancia de Twenty; puede ver la versión de su instancia en el panel de administración (en `/settings/admin-panel`, accesible si su usuario tiene la propiedad `canAccessFullAdminPanel` establecida en verdadero en la base de datos) o ejecutando `echo $APP_VERSION` en su contenedor `twenty-server`.
|
||||
It should always be in the range of your current Twenty's instance `major.minor` version, you can view your instance version in the admin panel (at `/settings/admin-panel`, accessible if your user has `canAccessFullAdminPanel` property set to true in the database) or by running `echo $APP_VERSION` in your `twenty-server` container.
|
||||
|
||||
Para corregir una versión de espacio de trabajo desincronizada, tendrá que actualizar desde la correspondiente versión de twenty siguiendo la guía de actualización relacionada secuencialmente, y así sucesivamente hasta alcanzar la versión deseada.
|
||||
To fix a desynchronized workspace version, you will have to upgrade from the corresponding twenty's version following related upgrade guide sequentially and so on until it reaches desired version.
|
||||
|
||||
#### Eliminación de `auditLog`
|
||||
#### `auditLog` removal
|
||||
|
||||
Hemos eliminado el objeto estándar auditLog, lo que significa que el tamaño de su copia de seguridad podría reducirse significativamente después de esta migración.
|
||||
We've removed the auditLog standard object, which means your backup size might be significantly reduced after this migration.
|
||||
|
||||
### v0.51 a v0.52
|
||||
### v0.51 to v0.52
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.52
|
||||
Upgrade your Twenty instance to use v0.52 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
#### Tengo un espacio de trabajo bloqueado en la versión entre `0.52.0` y `0.52.6`
|
||||
#### I have a workspace blocked in version between `0.52.0` and `0.52.6`
|
||||
|
||||
Desafortunadamente, `0.52.0` y `0.52.6` se han eliminado completamente de dockerHub.
|
||||
Tendrá que actualizar manualmente la versión de su espacio de trabajo a `0.51.0` en la base de datos y actualizar usando la versión twenty `0.52.11` siguiendo su guía de actualización justo arriba.
|
||||
Unfortunately `0.52.0` and `0.52.6` have been completely removed from dockerHub.
|
||||
You will have to manually update your workspace version to `0.51.0` in database and upgrade using twenty version `0.52.11` following its just above upgrade guide.
|
||||
|
||||
### v0.50 a v0.51
|
||||
### v0.50 to v0.51
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.51
|
||||
Upgrade your Twenty instance to use v0.51 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### v0.44.0 a v0.50.0
|
||||
### v0.44.0 to v0.50.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.50.0
|
||||
Upgrade your Twenty instance to use v0.50.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
#### Mutación del docker-compose.yml
|
||||
#### Docker-compose.yml mutation
|
||||
|
||||
Esta versión incluye una mutación del `docker-compose.yml` para dar acceso al servicio `worker` al volumen `server-local-data`.
|
||||
Actualice su `docker-compose.yml` local con el [docker-compose.yml de v0.50.0](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)
|
||||
This version includes a `docker-compose.yml` mutation to give `worker` service access to the `server-local-data` volume.
|
||||
Please update your local `docker-compose.yml` with [v0.50.0 docker-compose.yml](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)
|
||||
|
||||
### v0.43.0 a v0.44.0
|
||||
### v0.43.0 to v0.44.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.44.0
|
||||
Upgrade your Twenty instance to use v0.44.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### v0.42.0 a v0.43.0
|
||||
### v0.42.0 to v0.43.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.43.0
|
||||
Upgrade your Twenty instance to use v0.43.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
En esta versión, también hemos cambiado a la imagen de postgres:16 en docker-compose.yml.
|
||||
In this version, we have also switched to postgres:16 image in docker-compose.yml.
|
||||
|
||||
#### (Opción 1) Migración de base de datos
|
||||
#### (Option 1) Database migration
|
||||
|
||||
Mantener la imagen postgres-spilo existente está bien, pero tendrá que congelar la versión en su docker-compose.yml a 0.43.0.
|
||||
Keeping the existing postgres-spilo image is fine, but you will have to freeze the version in your docker-compose.yml to be 0.43.0.
|
||||
|
||||
#### (Opción 2) Migración de base de datos
|
||||
#### (Option 2) Database migration
|
||||
|
||||
Si desea migrar su base de datos a la nueva imagen de postgres:16, siga estos pasos:
|
||||
If you want to migrate your database to the new postgres:16 image, please follow these steps:
|
||||
|
||||
1. Descargue su base de datos del contenedor antiguo de postgres-spilo
|
||||
1. Dump your database from the old postgres-spilo container
|
||||
|
||||
```
|
||||
docker exec -it twenty-db-1 sh
|
||||
@@ -157,11 +157,11 @@ exit
|
||||
docker cp twenty-db-1:/home/postgres/databases_backup.sql .
|
||||
```
|
||||
|
||||
Asegúrese de que su archivo de respaldo no esté vacío.
|
||||
Make sure your dump file is not empty.
|
||||
|
||||
2. Actualice su docker-compose.yml para usar la imagen de postgres:16 como en el archivo [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml).
|
||||
2. Upgrade your docker-compose.yml to use postgres:16 image as in the [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) file.
|
||||
|
||||
3. Restaure la base de datos al nuevo contenedor postgres:16
|
||||
3. Restore the database to the new postgres:16 container
|
||||
|
||||
```
|
||||
docker cp databases_backup.sql twenty-db-1:/databases_backup.sql
|
||||
@@ -170,86 +170,86 @@ psql -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} -f databases_backup.sql
|
||||
exit
|
||||
```
|
||||
|
||||
### v0.41.0 a v0.42.0
|
||||
### v0.41.0 to v0.42.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.42.0
|
||||
Upgrade your Twenty instance to use v0.42.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.42
|
||||
```
|
||||
|
||||
**Variables del entorno**
|
||||
**Environment Variables**
|
||||
|
||||
* Removidos: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
|
||||
* Agregados: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
|
||||
* Removed: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
|
||||
* Added: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
|
||||
|
||||
### v0.40.0 a v0.41.0
|
||||
### v0.40.0 to v0.41.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.41.0
|
||||
Upgrade your Twenty instance to use v0.41.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.41
|
||||
```
|
||||
|
||||
**Variables del entorno**
|
||||
**Environment Variables**
|
||||
|
||||
* Removido: `AUTH_MICROSOFT_TENANT_ID`
|
||||
* Removed: `AUTH_MICROSOFT_TENANT_ID`
|
||||
|
||||
### v0.35.0 a v0.40.0
|
||||
### v0.35.0 to v0.40.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.40.0
|
||||
Upgrade your Twenty instance to use v0.40.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.40
|
||||
```
|
||||
|
||||
**Variables del entorno**
|
||||
**Environment Variables**
|
||||
|
||||
* Agregados: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
|
||||
* Added: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
|
||||
|
||||
### v0.34.0 a v0.35.0
|
||||
### v0.34.0 to v0.35.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.35.0
|
||||
Upgrade your Twenty instance to use v0.35.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.35
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.35` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.35` takes care of the data migration of all workspaces.
|
||||
|
||||
**Variables del entorno**
|
||||
**Environment Variables**
|
||||
|
||||
* Reemplazamos `ENABLE_DB_MIGRATIONS` por `DISABLE_DB_MIGRATIONS` (valor predeterminado ahora es `false`, probablemente no tenga que establecer nada)
|
||||
* We replaced `ENABLE_DB_MIGRATIONS` with `DISABLE_DB_MIGRATIONS` (default value is now `false`, you probably don't have to set anything)
|
||||
|
||||
### v0.33.0 a v0.34.0
|
||||
### v0.33.0 to v0.34.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.34.0
|
||||
Upgrade your Twenty instance to use v0.34.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.34
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.34` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.34` takes care of the data migration of all workspaces.
|
||||
|
||||
**Variables del entorno**
|
||||
**Environment Variables**
|
||||
|
||||
* Removido: `FRONT_BASE_URL`
|
||||
* Agregados: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
|
||||
* Removed: `FRONT_BASE_URL`
|
||||
* Added: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
|
||||
|
||||
Hemos actualizado la forma en que manejamos la URL del frontend.
|
||||
Ahora puede configurar la URL del frontend usando las variables `FRONT_DOMAIN`, `FRONT_PROTOCOL` y `FRONT_PORT`.
|
||||
Si FRONT_DOMAIN no está configurado, la URL del frontend volverá a `SERVER_URL`.
|
||||
We have updated the way we handle the frontend URL.
|
||||
You can now set the frontend URL using the `FRONT_DOMAIN`, `FRONT_PROTOCOL` and `FRONT_PORT` variables.
|
||||
If FRONT_DOMAIN is not set, the frontend URL will fall back to `SERVER_URL`.
|
||||
|
||||
### v0.32.0 a v0.33.0
|
||||
### v0.32.0 to v0.33.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.33.0
|
||||
Upgrade your Twenty instance to use v0.33.0 image
|
||||
|
||||
```
|
||||
yarn command:prod cache:flush
|
||||
@@ -257,68 +257,68 @@ yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.33
|
||||
```
|
||||
|
||||
El comando `yarn command:prod cache:flush` eliminará la caché de Redis.
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.33` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn command:prod cache:flush` command will flush the Redis cache.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.33` takes care of the data migration of all workspaces.
|
||||
|
||||
A partir de esta versión, la imagen twenty-postgres para DB quedó obsoleta y ahora se usa twenty-postgres-spilo.
|
||||
Si desea seguir usando la imagen twenty-postgres, simplemente reemplace `twentycrm/twenty-postgres:${TAG}` con `twentycrm/twenty-postgres` en docker-compose.yml.
|
||||
Starting from this version, twenty-postgres image for DB became deprecated and twenty-postgres-spilo is used instead.
|
||||
If you want to keep using twenty-postgres image, simply replace `twentycrm/twenty-postgres:${TAG}` with `twentycrm/twenty-postgres` in docker-compose.yml.
|
||||
|
||||
### v0.31.0 a v0.32.0
|
||||
### v0.31.0 to v0.32.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.32.0
|
||||
Upgrade your Twenty instance to use v0.32.0 image
|
||||
|
||||
**Migración de esquemas y datos**
|
||||
**Schema and data migration**
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.32
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.32` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.32` takes care of the data migration of all workspaces.
|
||||
|
||||
**Variables del entorno**
|
||||
**Environment Variables**
|
||||
|
||||
Hemos actualizado la forma en que manejamos la conexión Redis.
|
||||
We have updated the way we handle the Redis connection.
|
||||
|
||||
* Removidos: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
|
||||
* Agregado: `REDIS_URL`
|
||||
* Removed: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
|
||||
* Added: `REDIS_URL`
|
||||
|
||||
Actualice su archivo `.env` para usar la nueva variable `REDIS_URL` en lugar de los parámetros de conexión individuales de Redis.
|
||||
Update your `.env` file to use the new `REDIS_URL` variable instead of the individual Redis connection parameters.
|
||||
|
||||
También hemos simplificado la forma en que manejamos los tokens JWT.
|
||||
We have also simplified the way we handle the JWT tokens.
|
||||
|
||||
* Removidos: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
|
||||
* Agregado: `APP_SECRET`
|
||||
* Removed: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
|
||||
* Added: `APP_SECRET`
|
||||
|
||||
Actualice su archivo `.env` para usar la nueva variable `APP_SECRET` en lugar de los secretos de tokens individuales (puede usar el mismo secreto que antes o generar una nueva cadena aleatoria)
|
||||
Update your `.env` file to use the new `APP_SECRET` variable instead of the individual tokens secrets (you can use the same secret as before or generate a new random string)
|
||||
|
||||
**Cuenta conectada**
|
||||
**Connected Account**
|
||||
|
||||
Si está utilizando cuentas conectadas para sincronizar sus correos electrónicos y calendarios de Google, deberá activar la [API de People](https://developers.google.com/people) en su consola de administración de Google.
|
||||
If you are using connected account to synchronize your Google emails and calendars, you will need to activate the [People API](https://developers.google.com/people) on your Google Admin console.
|
||||
|
||||
### v0.30.0 a v0.31.0
|
||||
### v0.30.0 to v0.31.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.31.0
|
||||
Upgrade your Twenty instance to use v0.31.0 image
|
||||
|
||||
**Migración de esquemas y datos:**
|
||||
**Schema and data migration**:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.31
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.31` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.31` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.24.0 a v0.30.0
|
||||
### v0.24.0 to v0.30.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.30.0
|
||||
Upgrade your Twenty instance to use v0.30.0 image
|
||||
|
||||
**Cambio importante**:
|
||||
Para mejorar el rendimiento, Twenty ahora requiere que la caché redis esté configurada. Hemos actualizado nuestro [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) para reflejar esto.
|
||||
Asegúrese de actualizar su configuración y sus variables de entorno en consecuencia:
|
||||
**Breaking change**:
|
||||
To enhance performances, Twenty now requires redis cache to be configured. We have updated our [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) to reflect this.
|
||||
Make sure to update your configuration and to update your environment variables accordingly:
|
||||
|
||||
```
|
||||
REDIS_HOST={your-redis-host}
|
||||
@@ -326,49 +326,49 @@ REDIS_PORT={your-redis-port}
|
||||
CACHE_STORAGE_TYPE=redis
|
||||
```
|
||||
|
||||
**Migración de esquemas y datos:**
|
||||
**Schema and data migration**:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.30
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.30` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.30` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.23.0 a v0.24.0
|
||||
### v0.23.0 to v0.24.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.24.0
|
||||
Upgrade your Twenty instance to use v0.24.0 image
|
||||
|
||||
Ejecución de los siguientes comandos:
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.24
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la estructura de la base de datos (esquemas core y metadata)
|
||||
El `yarn command:prod upgrade-0.24` se encarga de la migración de datos de todos los espacios de trabajo.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.24` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.22.0 a v0.23.0
|
||||
### v0.22.0 to v0.23.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.23.0
|
||||
Upgrade your Twenty instance to use v0.23.0 image
|
||||
|
||||
Ejecución de los siguientes comandos:
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.23
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la base de datos.
|
||||
El `yarn command:prod upgrade-0.23` se encarga de la migración de datos, incluyendo la transferencia de actividades a tareas/notas.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the Database.
|
||||
The `yarn command:prod upgrade-0.23` takes care of the data migration, including transferring activities to tasks/notes.
|
||||
|
||||
### v0.21.0 a v0.22.0
|
||||
### v0.21.0 to v0.22.0
|
||||
|
||||
Actualice su instancia de Twenty para usar la imagen v0.22.0
|
||||
Upgrade your Twenty instance to use v0.22.0 image
|
||||
|
||||
Ejecución de los siguientes comandos:
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
@@ -376,6 +376,6 @@ yarn command:prod workspace:sync-metadata -f
|
||||
yarn command:prod upgrade-0.22
|
||||
```
|
||||
|
||||
El comando `yarn database:migrate:prod` aplicará las migraciones a la base de datos.
|
||||
El comando `yarn command:prod workspace:sync-metadata -f` sincronizará la definición de objetos estándar a las tablas de metadatos y aplicará las migraciones requeridas a los espacios de trabajo existentes.
|
||||
El comando `yarn command:prod upgrade-0.22` aplicará transformaciones de datos específicas para adaptarse a las nuevas opciones predeterminadas de instrumentación de solicitud de objetos.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the Database.
|
||||
The `yarn command:prod workspace:sync-metadata -f` command will sync the definition of standard objects to the metadata tables and apply to required migrations to existing workspaces.
|
||||
The `yarn command:prod upgrade-0.22` command will apply specific data transformations to adapt to the new object defaultRequestInstrumentationOptions.
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
---
|
||||
title: Autoalojamiento
|
||||
description: Despliega y administra Twenty en tu propia infraestructura.
|
||||
title: Self-Host
|
||||
description: Deploy and manage Twenty on your own infrastructure.
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/user-guide/what-is-twenty/20.png" alt="IA" />
|
||||
<img src="/images/user-guide/what-is-twenty/20.png" alt="AI" />
|
||||
</Frame>
|
||||
|
||||
## Resumen
|
||||
## Overview
|
||||
|
||||
Puedes autoalojar Twenty en tu propia infraestructura, lo que te brinda control total sobre tus datos y el despliegue.
|
||||
Twenty can be self-hosted on your own infrastructure, giving you full control over your data and deployment.
|
||||
|
||||
## ¿Por qué autoalojar?
|
||||
## Why Self-Host?
|
||||
|
||||
* **Propiedad de los datos**: Mantén todos los datos de CRM en tus propios servidores
|
||||
* **Cumplimiento**: Cumple los requisitos normativos de residencia de datos
|
||||
* **Personalización**: Acceso completo para modificar y ampliar la plataforma
|
||||
* **Data ownership**: Keep all CRM data on your own servers
|
||||
* **Compliance**: Meet regulatory requirements for data residency
|
||||
* **Customization**: Full access to modify and extend the platform
|
||||
|
||||
## Primeros pasos
|
||||
## Getting Started
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Docker Compose" icon="docker" href="/l/es/developers/self-host/capabilities/docker-compose">
|
||||
Configuración rápida con Docker
|
||||
Quick setup with Docker
|
||||
</Card>
|
||||
|
||||
<Card title="Proveedores de la nube" icon="cloud" href="/l/es/developers/self-host/capabilities/cloud-providers">
|
||||
Despliega en AWS, GCP o Azure
|
||||
<Card title="Cloud Providers" icon="cloud" href="/l/es/developers/self-host/capabilities/cloud-providers">
|
||||
Deploy on AWS, GCP, or Azure
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,197 +1,197 @@
|
||||
{
|
||||
"tabs": {
|
||||
"userGuide": {
|
||||
"label": "Guía de usuario",
|
||||
"label": "User Guide",
|
||||
"groups": {
|
||||
"discoverTwenty": {
|
||||
"label": "Descubre Twenty",
|
||||
"label": "Discover Twenty",
|
||||
"groups": {
|
||||
"gettingStartedCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"gettingStartedHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dataModel": {
|
||||
"label": "Modelo de datos",
|
||||
"label": "Data Model",
|
||||
"groups": {
|
||||
"dataModelCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"dataModelHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dataMigration": {
|
||||
"label": "Migración de datos",
|
||||
"label": "Data Migration",
|
||||
"groups": {
|
||||
"dataMigrationCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"dataMigrationHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calendarEmails": {
|
||||
"label": "Calendario y correos electrónicos",
|
||||
"label": "Calendar & Emails",
|
||||
"groups": {
|
||||
"calendarEmailsCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"calendarEmailsHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workflows": {
|
||||
"label": "Flujos de trabajo",
|
||||
"label": "Workflows",
|
||||
"groups": {
|
||||
"workflowsCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"workflowsHowTos": {
|
||||
"label": "Guías prácticas",
|
||||
"label": "How-Tos",
|
||||
"groups": {
|
||||
"crmAutomations": {
|
||||
"label": "Automatizaciones de CRM"
|
||||
"label": "CRM Automations"
|
||||
},
|
||||
"connectToOtherTools": {
|
||||
"label": "Conectar con otras herramientas"
|
||||
"label": "Connect to Other Tools"
|
||||
},
|
||||
"advancedConfigurations": {
|
||||
"label": "Configuraciones avanzadas"
|
||||
"label": "Advanced Configurations"
|
||||
},
|
||||
"needMoreHelp": {
|
||||
"label": "¿Necesitas más ayuda?"
|
||||
"label": "Need More Help"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ai": {
|
||||
"label": "IA",
|
||||
"label": "AI",
|
||||
"groups": {
|
||||
"aiCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"aiHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewsPipelines": {
|
||||
"label": "Vistas y canalizaciones",
|
||||
"label": "Views & Pipelines",
|
||||
"groups": {
|
||||
"viewsPipelinesCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"viewsPipelinesHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboards": {
|
||||
"label": "Tableros",
|
||||
"label": "Dashboards",
|
||||
"groups": {
|
||||
"dashboardsCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"dashboardsHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissionsAccess": {
|
||||
"label": "Permisos y acceso",
|
||||
"label": "Permissions & Access",
|
||||
"groups": {
|
||||
"permissionsAccessCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"permissionsAccessHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"billing": {
|
||||
"label": "Facturación",
|
||||
"label": "Billing",
|
||||
"groups": {
|
||||
"billingCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"billingHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"label": "Configuración",
|
||||
"label": "Settings",
|
||||
"groups": {
|
||||
"settingsCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
},
|
||||
"settingsHowTos": {
|
||||
"label": "Guías prácticas"
|
||||
"label": "How-Tos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"developers": {
|
||||
"label": "Desarrolladores",
|
||||
"label": "Developers",
|
||||
"groups": {
|
||||
"developersGroup": {
|
||||
"label": "Desarrolladores"
|
||||
"label": "Developers"
|
||||
},
|
||||
"extend": {
|
||||
"label": "Ampliar",
|
||||
"label": "Extend",
|
||||
"groups": {
|
||||
"extendCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selfHost": {
|
||||
"label": "Autoalojamiento",
|
||||
"label": "Self-Host",
|
||||
"groups": {
|
||||
"selfHostCapabilities": {
|
||||
"label": "Capacidades"
|
||||
"label": "Capabilities"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contribute": {
|
||||
"label": "Contribuir",
|
||||
"label": "Contribute",
|
||||
"groups": {
|
||||
"contributeCapabilities": {
|
||||
"label": "Capacidades",
|
||||
"label": "Capabilities",
|
||||
"groups": {
|
||||
"frontendDevelopment": {
|
||||
"label": "Desarrollo Frontend",
|
||||
"label": "Frontend Development",
|
||||
"groups": {
|
||||
"twentyUi": {
|
||||
"label": "Twenty UI",
|
||||
"groups": {
|
||||
"display": {
|
||||
"label": "Mostrar"
|
||||
"label": "Display"
|
||||
},
|
||||
"feedback": {
|
||||
"label": "Retroalimentación"
|
||||
"label": "Feedback"
|
||||
},
|
||||
"input": {
|
||||
"label": "Entrada"
|
||||
"label": "Input"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "Navegación"
|
||||
"label": "Navigation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"backendDevelopment": {
|
||||
"label": "Desarrollo Backend"
|
||||
"label": "Backend Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user