Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41fcd692a6 | ||
|
|
b9bc1264b3 | ||
|
|
be358e398e |
@@ -1,34 +0,0 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
make \
|
||||
build-essential \
|
||||
postgresql-client \
|
||||
docker.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install nvm (project recommends nvm + .nvmrc for consistent Node versions)
|
||||
ENV NVM_DIR=/usr/local/nvm
|
||||
RUN mkdir -p $NVM_DIR \
|
||||
&& curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Copy .nvmrc so nvm install picks up the right version
|
||||
COPY .nvmrc /tmp/.nvmrc
|
||||
|
||||
# Install Node.js from .nvmrc, enable Corepack, and symlink binaries
|
||||
# so they're available on PATH without hardcoding a version
|
||||
RUN . $NVM_DIR/nvm.sh \
|
||||
&& nvm install $(cat /tmp/.nvmrc) \
|
||||
&& nvm alias default $(cat /tmp/.nvmrc) \
|
||||
&& corepack enable \
|
||||
&& BIN_DIR=$(dirname $(nvm which default)) \
|
||||
&& ln -sf $BIN_DIR/node /usr/local/bin/node \
|
||||
&& ln -sf $BIN_DIR/npm /usr/local/bin/npm \
|
||||
&& ln -sf $BIN_DIR/npx /usr/local/bin/npx \
|
||||
&& ln -sf $BIN_DIR/corepack /usr/local/bin/corepack
|
||||
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"install": "yarn install",
|
||||
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Installing dependencies complete'",
|
||||
"start": "sudo service docker start && echo 'Docker service started' && sleep 3 && echo 'Starting PostgreSQL and Redis containers...' && make postgres-on-docker && make redis-on-docker && echo 'Waiting for containers to initialize...' && sleep 20 && echo 'Checking container status...' && docker ps --filter name=twenty_ && echo 'Waiting for PostgreSQL to be ready...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'PostgreSQL not ready yet, waiting...'; sleep 3; done && echo 'PostgreSQL is ready!' && echo 'Setting up database...' && cd packages/twenty-server && npx nx database:reset twenty-server || echo 'Database already initialized' && echo 'Environment setup complete!'",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Server",
|
||||
"command": "yarn start"
|
||||
"command": "echo 'Waiting for database to be fully ready...' && sleep 30 && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'Starting Twenty development server...' && export SERVER_URL=http://localhost:3000 && export PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres && yarn start"
|
||||
},
|
||||
{
|
||||
"name": "Database Management",
|
||||
"command": "sleep 25 && echo 'Database management terminal ready' && echo 'Waiting for PostgreSQL to be available...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'PostgreSQL is ready for database operations!' && echo 'You can now run database commands like:' && echo ' npx nx database:reset twenty-server' && echo ' npx nx database:migrate twenty-server' && bash"
|
||||
},
|
||||
{
|
||||
"name": "Container Logs & Status",
|
||||
"command": "sleep 10 && echo '=== Container Status Monitor ===' && while true; do echo '\\n=== Container Status at $(date) ===' && docker ps --filter name=twenty_ --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' && echo '\\n=== PostgreSQL Status ===' && (docker exec twenty_pg pg_isready -U postgres -h localhost && echo 'PostgreSQL: ✅ Ready') || echo 'PostgreSQL: ❌ Not Ready' && echo '\\n=== Redis Status ===' && (docker exec twenty_redis redis-cli ping && echo 'Redis: ✅ Ready') || echo 'Redis: ❌ Not Ready' && sleep 30; done"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,393 +0,0 @@
|
||||
---
|
||||
name: syncable-entity-builder-and-validation
|
||||
description: Create validation logic and migration action builders for syncable entities in Twenty. Use when implementing business rule validation, uniqueness checks, foreign key validation, or building workspace migration actions for syncable entities. Validators never throw and never mutate.
|
||||
---
|
||||
|
||||
# Syncable Entity: Builder & Validation (Step 3/6)
|
||||
|
||||
**Purpose**: Implement business rule validation and create migration action builders.
|
||||
|
||||
**When to use**: After completing Steps 1-2 (Types, Cache, Transform). Required before implementing action handlers.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Validator service (business logic validation)
|
||||
2. Builder service (action creation)
|
||||
3. Orchestrator wiring (**CRITICAL** - often forgotten!)
|
||||
|
||||
**Key principles**:
|
||||
- Validators **never throw** - return error arrays
|
||||
- Validators **never mutate** - pass optimistic entity maps
|
||||
- Use indexed lookups (O(1)) not `Object.values().find()` (O(n))
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Validator Service
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { t, msg } from '@lingui/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
|
||||
import { WorkspaceMigrationValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/types/workspace-migration-validation-error.type';
|
||||
import { MyEntityExceptionCode } from 'src/engine/metadata-modules/my-entity/exceptions/my-entity-exception-code.enum';
|
||||
|
||||
@Injectable()
|
||||
export class FlatMyEntityValidatorService {
|
||||
validateMyEntityForCreate(
|
||||
flatMyEntity: FlatMyEntity,
|
||||
optimisticFlatMyEntityMaps: FlatMyEntityMaps,
|
||||
): WorkspaceMigrationValidationError[] {
|
||||
const errors: WorkspaceMigrationValidationError[] = [];
|
||||
|
||||
// Pattern 1: Required field validation
|
||||
if (!isDefined(flatMyEntity.name) || flatMyEntity.name.trim() === '') {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.NAME_REQUIRED,
|
||||
message: t`Name is required`,
|
||||
userFriendlyMessage: msg`Please provide a name for this entity`,
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern 2: Uniqueness check - use indexed map (O(1))
|
||||
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[flatMyEntity.name];
|
||||
|
||||
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.MY_ENTITY_ALREADY_EXISTS,
|
||||
message: t`Entity with name ${flatMyEntity.name} already exists`,
|
||||
userFriendlyMessage: msg`An entity with this name already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern 3: Foreign key validation
|
||||
if (isDefined(flatMyEntity.parentEntityId)) {
|
||||
const parentEntity = optimisticFlatParentEntityMaps.byId[flatMyEntity.parentEntityId];
|
||||
|
||||
if (!isDefined(parentEntity)) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.PARENT_ENTITY_NOT_FOUND,
|
||||
message: t`Parent entity with ID ${flatMyEntity.parentEntityId} not found`,
|
||||
userFriendlyMessage: msg`The specified parent entity does not exist`,
|
||||
});
|
||||
} else if (isDefined(parentEntity.deletedAt)) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.PARENT_ENTITY_DELETED,
|
||||
message: t`Parent entity is deleted`,
|
||||
userFriendlyMessage: msg`Cannot reference a deleted parent entity`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 4: Standard entity protection
|
||||
if (flatMyEntity.isCustom === false) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_CREATED,
|
||||
message: t`Cannot create standard entity`,
|
||||
userFriendlyMessage: msg`Standard entities can only be created by the system`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
validateMyEntityForUpdate(
|
||||
flatMyEntity: FlatMyEntity,
|
||||
updates: Partial<FlatMyEntity>,
|
||||
optimisticFlatMyEntityMaps: FlatMyEntityMaps,
|
||||
): WorkspaceMigrationValidationError[] {
|
||||
const errors: WorkspaceMigrationValidationError[] = [];
|
||||
|
||||
// Standard entity protection
|
||||
if (flatMyEntity.isCustom === false) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_UPDATED,
|
||||
message: t`Cannot update standard entity`,
|
||||
userFriendlyMessage: msg`Standard entities cannot be modified`,
|
||||
});
|
||||
return errors; // Early return if standard
|
||||
}
|
||||
|
||||
// Uniqueness check for name changes
|
||||
if (isDefined(updates.name) && updates.name !== flatMyEntity.name) {
|
||||
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[updates.name];
|
||||
|
||||
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.MY_ENTITY_ALREADY_EXISTS,
|
||||
message: t`Entity with name ${updates.name} already exists`,
|
||||
userFriendlyMessage: msg`An entity with this name already exists`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
validateMyEntityForDelete(
|
||||
flatMyEntity: FlatMyEntity,
|
||||
): WorkspaceMigrationValidationError[] {
|
||||
const errors: WorkspaceMigrationValidationError[] = [];
|
||||
|
||||
// Standard entity protection
|
||||
if (flatMyEntity.isCustom === false) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_DELETED,
|
||||
message: t`Cannot delete standard entity`,
|
||||
userFriendlyMessage: msg`Standard entities cannot be deleted`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance warning**: Avoid `Object.values().find()` - use indexed maps instead!
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: O(n) - slow for large datasets
|
||||
const duplicate = Object.values(optimisticFlatMyEntityMaps.byId).find(
|
||||
(entity) => entity.name === flatMyEntity.name && entity.id !== flatMyEntity.id
|
||||
);
|
||||
|
||||
// ✅ GOOD: O(1) - use indexed map
|
||||
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[flatMyEntity.name];
|
||||
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
|
||||
// Handle duplicate
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Builder Service
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-entity-migration-builder.service';
|
||||
import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
import {
|
||||
type UniversalCreateMyEntityAction,
|
||||
type UniversalUpdateMyEntityAction,
|
||||
type UniversalDeleteMyEntityAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationMyEntityActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
|
||||
'myEntity',
|
||||
UniversalFlatMyEntity,
|
||||
UniversalCreateMyEntityAction,
|
||||
UniversalUpdateMyEntityAction,
|
||||
UniversalDeleteMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatMyEntityValidatorService: FlatMyEntityValidatorService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected buildCreateAction(
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): BuildWorkspaceMigrationActionReturnType<UniversalCreateMyEntityAction> {
|
||||
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForCreate(
|
||||
universalFlatMyEntity,
|
||||
flatEntityMaps.flatMyEntityMaps,
|
||||
);
|
||||
|
||||
if (validationResult.length > 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
errors: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create',
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected buildUpdateAction(
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
universalUpdates: Partial<UniversalFlatMyEntity>,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): BuildWorkspaceMigrationActionReturnType<UniversalUpdateMyEntityAction> {
|
||||
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForUpdate(
|
||||
universalFlatMyEntity,
|
||||
universalUpdates,
|
||||
flatEntityMaps.flatMyEntityMaps,
|
||||
);
|
||||
|
||||
if (validationResult.length > 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
errors: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'update',
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
universalUpdates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected buildDeleteAction(
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
): BuildWorkspaceMigrationActionReturnType<UniversalDeleteMyEntityAction> {
|
||||
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForDelete(
|
||||
universalFlatMyEntity,
|
||||
);
|
||||
|
||||
if (validationResult.length > 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
errors: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete',
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Wire into Orchestrator (**CRITICAL**)
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-build-orchestrator.service.ts`
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationBuildOrchestratorService {
|
||||
constructor(
|
||||
// ... existing builders
|
||||
private readonly workspaceMigrationMyEntityActionsBuilderService: WorkspaceMigrationMyEntityActionsBuilderService,
|
||||
) {}
|
||||
|
||||
async buildWorkspaceMigration({
|
||||
allFlatEntityOperationByMetadataName,
|
||||
flatEntityMaps,
|
||||
isSystemBuild,
|
||||
}: BuildWorkspaceMigrationInput): Promise<BuildWorkspaceMigrationOutput> {
|
||||
// ... existing code
|
||||
|
||||
// Add your entity builder
|
||||
const myEntityResult = await this.workspaceMigrationMyEntityActionsBuilderService.build({
|
||||
flatEntitiesToCreate: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToCreate ?? [],
|
||||
flatEntitiesToUpdate: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToUpdate ?? [],
|
||||
flatEntitiesToDelete: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToDelete ?? [],
|
||||
flatEntityMaps,
|
||||
isSystemBuild,
|
||||
});
|
||||
|
||||
// ... aggregate errors
|
||||
|
||||
return {
|
||||
status: aggregatedErrors.length > 0 ? 'failed' : 'success',
|
||||
errors: aggregatedErrors,
|
||||
actions: [
|
||||
...existingActions,
|
||||
...myEntityResult.actions,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ This step is the most commonly forgotten!** Your entity won't sync without orchestrator wiring.
|
||||
|
||||
---
|
||||
|
||||
## Validation Patterns
|
||||
|
||||
### Pattern 1: Required Field
|
||||
```typescript
|
||||
if (!isDefined(field) || field.trim() === '') {
|
||||
errors.push({ code: ..., message: ..., userFriendlyMessage: ... });
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Uniqueness (O(1) lookup)
|
||||
```typescript
|
||||
const existing = optimisticMaps.byName[entity.name];
|
||||
if (isDefined(existing) && existing.id !== entity.id) {
|
||||
errors.push({ ... });
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Foreign Key Validation
|
||||
```typescript
|
||||
if (isDefined(entity.parentId)) {
|
||||
const parent = parentMaps.byId[entity.parentId];
|
||||
if (!isDefined(parent)) {
|
||||
errors.push({ code: NOT_FOUND, ... });
|
||||
} else if (isDefined(parent.deletedAt)) {
|
||||
errors.push({ code: DELETED, ... });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Standard Entity Protection
|
||||
```typescript
|
||||
if (entity.isCustom === false) {
|
||||
errors.push({ code: STANDARD_ENTITY_PROTECTED, ... });
|
||||
return errors; // Early return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 4:
|
||||
|
||||
- [ ] Validator service created
|
||||
- [ ] Validator **never throws** (returns error arrays)
|
||||
- [ ] Validator **never mutates** (uses optimistic maps)
|
||||
- [ ] All uniqueness checks use indexed maps (O(1))
|
||||
- [ ] Required field validation implemented
|
||||
- [ ] Foreign key validation implemented
|
||||
- [ ] Standard entity protection implemented
|
||||
- [ ] Builder service extends `WorkspaceEntityMigrationBuilderService`
|
||||
- [ ] Builder creates actions with universal entities
|
||||
- [ ] **Builder wired into orchestrator** (**CRITICAL**)
|
||||
- [ ] **Builder injected in orchestrator constructor**
|
||||
- [ ] **Builder called in `buildWorkspaceMigration`**
|
||||
- [ ] **Actions added to orchestrator return statement**
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once builder and validation are complete, proceed to:
|
||||
**[Syncable Entity: Runner & Actions (Step 4/6)](../syncable-entity-runner-and-actions/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -1,303 +0,0 @@
|
||||
---
|
||||
name: syncable-entity-cache-and-transform
|
||||
description: Create cache services and transformation utilities for syncable entities in Twenty. Use when implementing entity-to-flat conversions, input DTO transpilation to universal flat entities, or cache recomputation for syncable entities.
|
||||
---
|
||||
|
||||
# Syncable Entity: Cache & Transform (Step 2/6)
|
||||
|
||||
**Purpose**: Create cache layer and transformation utilities to convert between different entity representations.
|
||||
|
||||
**When to use**: After completing Step 1 (Types & Constants). Required before building validators and action handlers.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Cache service for flat entity maps
|
||||
2. Entity-to-flat conversion utility
|
||||
3. Input transform utils (DTO → Universal Flat Entity)
|
||||
|
||||
**Key principle**: Input transform utils must output **universal flat entities** (with `universalIdentifier` and foreign keys mapped to universal identifiers).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Cache Service
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WorkspaceCache } from 'src/engine/twenty-orm/decorators/workspace-cache.decorator';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
|
||||
import { fromMyEntityEntityToFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util';
|
||||
|
||||
@Injectable()
|
||||
export class FlatMyEntityCacheService {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {}
|
||||
|
||||
@WorkspaceCache({ flatMapsKey: 'flatMyEntityMaps' })
|
||||
async getFlatMyEntityMaps(): Promise<FlatMyEntityMaps> {
|
||||
const myEntities = await this.myEntityRepository.find({
|
||||
withDeleted: true, // CRITICAL: Include soft-deleted entities
|
||||
});
|
||||
|
||||
const flatMyEntities = myEntities.map((entity) =>
|
||||
fromMyEntityEntityToFlatMyEntity(entity),
|
||||
);
|
||||
|
||||
return {
|
||||
byId: Object.fromEntries(flatMyEntities.map((e) => [e.id, e])),
|
||||
byName: Object.fromEntries(flatMyEntities.map((e) => [e.name, e])),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Critical rules**:
|
||||
- Use `@WorkspaceCache` decorator with unique `flatMapsKey`
|
||||
- **Always** use `withDeleted: true` to include soft-deleted entities
|
||||
- Cache key pattern: `flat{EntityName}Maps` (camelCase)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Entity-to-Flat Conversion
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import { v4 } from 'uuid';
|
||||
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
|
||||
export const fromMyEntityEntityToFlatMyEntity = (
|
||||
entity: MyEntityEntity,
|
||||
): FlatMyEntity => {
|
||||
return {
|
||||
id: entity.id,
|
||||
// Critical: generate a new UUID for universalIdentifier
|
||||
universalIdentifier: v4(),
|
||||
workspaceId: entity.workspaceId,
|
||||
applicationId: entity.applicationId,
|
||||
name: entity.name,
|
||||
label: entity.label,
|
||||
description: entity.description,
|
||||
isCustom: entity.isCustom,
|
||||
parentEntityId: entity.parentEntityId,
|
||||
settings: entity.settings,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
deletedAt: entity.deletedAt?.toISOString() ?? null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Critical**: `universalIdentifier` must be a new UUID generated with `v4()` (not `entity.id`)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Input Transform Utils (DTO → Universal Flat Entity)
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import { v4 } from 'uuid';
|
||||
import { sanitizeString } from 'twenty-shared/string';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
|
||||
|
||||
export const fromCreateMyEntityInputToUniversalFlatMyEntity = ({
|
||||
input,
|
||||
workspaceId,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
input: CreateMyEntityInput;
|
||||
workspaceId: string;
|
||||
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
|
||||
}): UniversalFlatMyEntity => {
|
||||
const id = v4();
|
||||
const universalIdentifier = v4();
|
||||
|
||||
// 1. Extract foreign key IDs BEFORE sanitization
|
||||
const parentEntityId = input.parentEntityId ?? null;
|
||||
|
||||
// 2. Sanitize string properties
|
||||
const name = sanitizeString(input.name);
|
||||
const label = sanitizeString(input.label);
|
||||
const description = input.description ? sanitizeString(input.description) : null;
|
||||
|
||||
// 3. Build base flat entity
|
||||
const baseFlatEntity = {
|
||||
id,
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
applicationId: null,
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
isCustom: true,
|
||||
parentEntityId,
|
||||
settings: input.settings ?? null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
// 4. Resolve foreign keys to universal identifiers (if flatEntityMaps provided)
|
||||
if (flatEntityMaps) {
|
||||
return resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: baseFlatEntity,
|
||||
flatEntityMaps,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Return with null universal foreign keys if no maps
|
||||
return {
|
||||
...baseFlatEntity,
|
||||
parentEntityUniversalIdentifier: null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Key steps**:
|
||||
1. Generate IDs (`id` and `universalIdentifier` with `v4()`)
|
||||
2. Extract foreign keys **before** sanitization
|
||||
3. Sanitize all string properties
|
||||
4. Build base flat entity
|
||||
5. Resolve foreign keys → universal identifiers
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create Flat Entity Module
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/flat-my-entity.module.ts`
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { FlatMyEntityCacheService } from 'src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([MyEntityEntity], 'metadata')],
|
||||
providers: [FlatMyEntityCacheService],
|
||||
exports: [FlatMyEntityCacheService],
|
||||
})
|
||||
export class FlatMyEntityModule {}
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- Import entity with `'metadata'` datasource
|
||||
- Export cache service for use in other modules
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Foreign Key Resolution
|
||||
|
||||
```typescript
|
||||
// Extract foreign keys BEFORE sanitization
|
||||
const parentEntityId = input.parentEntityId ?? null;
|
||||
|
||||
// After building base entity, resolve to universal identifiers
|
||||
const universalFlatEntity = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: baseFlatEntity,
|
||||
flatEntityMaps,
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: JSONB with SerializedRelation
|
||||
|
||||
```typescript
|
||||
// For JSONB properties containing foreign keys
|
||||
const settings = input.settings
|
||||
? {
|
||||
...input.settings,
|
||||
fieldMetadataId: input.settings.fieldMetadataId,
|
||||
}
|
||||
: null;
|
||||
|
||||
// After resolution, JSONB foreign keys become universal identifiers
|
||||
return resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: { ...baseFlatEntity, settings },
|
||||
flatEntityMaps,
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Update Transform
|
||||
|
||||
```typescript
|
||||
// from-update-my-entity-input-to-universal-flat-my-entity-updates.util.ts
|
||||
export const fromUpdateMyEntityInputToUniversalFlatMyEntityUpdates = ({
|
||||
input,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
input: UpdateMyEntityInput;
|
||||
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
|
||||
}): Partial<UniversalFlatMyEntity> => {
|
||||
const updates: Partial<UniversalFlatMyEntity> = {};
|
||||
|
||||
if (input.name !== undefined) {
|
||||
updates.name = sanitizeString(input.name);
|
||||
}
|
||||
|
||||
if (input.parentEntityId !== undefined) {
|
||||
updates.parentEntityId = input.parentEntityId;
|
||||
}
|
||||
|
||||
updates.updatedAt = new Date().toISOString();
|
||||
|
||||
// Resolve foreign keys if maps provided
|
||||
if (flatEntityMaps) {
|
||||
return resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: updates as any,
|
||||
flatEntityMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return updates;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 3:
|
||||
|
||||
- [ ] Cache service created with `@WorkspaceCache` decorator
|
||||
- [ ] Cache uses `withDeleted: true`
|
||||
- [ ] Cache key follows `flat{EntityName}Maps` pattern
|
||||
- [ ] Entity-to-flat conversion implemented
|
||||
- [ ] `universalIdentifier` set correctly (generated with `v4()`)
|
||||
- [ ] Create input transform implemented
|
||||
- [ ] Update input transform implemented (if needed)
|
||||
- [ ] Foreign keys extracted before sanitization
|
||||
- [ ] String properties sanitized
|
||||
- [ ] Foreign keys resolved to universal identifiers
|
||||
- [ ] Flat entity module created and exports cache service
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once cache and transform utilities are complete, proceed to:
|
||||
**[Syncable Entity: Builder & Validation (Step 3/6)](../syncable-entity-builder-and-validation/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -1,326 +0,0 @@
|
||||
---
|
||||
name: syncable-entity-integration
|
||||
description: Wire syncable entity services into NestJS modules, create service layer and resolvers for Twenty entities. Use when registering builders, validators, and action handlers in modules, creating business services, or exposing entities via GraphQL API with proper exception handling.
|
||||
---
|
||||
|
||||
# Syncable Entity: Integration (Step 5/6)
|
||||
|
||||
**Purpose**: Wire everything together, register in modules, create services and resolvers.
|
||||
|
||||
**When to use**: After completing Steps 1-4 (all previous steps). Required before testing.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step:
|
||||
1. Registers services in 3 NestJS modules
|
||||
2. Creates service layer (returns flat entities)
|
||||
3. Creates resolver layer (converts flat → DTO)
|
||||
4. Uses exception interceptor for GraphQL
|
||||
|
||||
**Key principle**: Services return flat entities, resolvers transpile flat → DTO.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Register in Builder Module
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts`
|
||||
|
||||
```typescript
|
||||
import { WorkspaceMigrationMyEntityActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
],
|
||||
providers: [
|
||||
// ... existing providers
|
||||
WorkspaceMigrationMyEntityActionsBuilderService,
|
||||
],
|
||||
exports: [
|
||||
// ... existing exports
|
||||
WorkspaceMigrationMyEntityActionsBuilderService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderModule {}
|
||||
```
|
||||
|
||||
**Important**: Add to both `providers` AND `exports` (builder needs to be exported for orchestrator).
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Register in Validators Module
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts`
|
||||
|
||||
```typescript
|
||||
import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
],
|
||||
providers: [
|
||||
// ... existing providers
|
||||
FlatMyEntityValidatorService,
|
||||
],
|
||||
exports: [
|
||||
// ... existing exports
|
||||
FlatMyEntityValidatorService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderValidatorsModule {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Register Action Handlers
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts`
|
||||
|
||||
```typescript
|
||||
import { CreateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service';
|
||||
import { UpdateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service';
|
||||
import { DeleteMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
],
|
||||
providers: [
|
||||
// ... existing providers
|
||||
CreateMyEntityActionHandlerService,
|
||||
UpdateMyEntityActionHandlerService,
|
||||
DeleteMyEntityActionHandlerService,
|
||||
],
|
||||
exports: [
|
||||
// ... existing exports (action handlers typically not exported)
|
||||
],
|
||||
})
|
||||
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
|
||||
```
|
||||
|
||||
**Note**: Action handlers are typically only in `providers`, not `exports`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create Service Layer
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/my-entity.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateMyEntityInputToUniversalFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class MyEntityService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
async create(input: CreateMyEntityInput, workspaceId: string): Promise<FlatMyEntity> {
|
||||
// 1. Transform input to universal flat entity
|
||||
const universalFlatMyEntityToCreate = fromCreateMyEntityInputToUniversalFlatMyEntity({
|
||||
input,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// 2. Validate, build, and run
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
myEntity: {
|
||||
flatEntityToCreate: [universalFlatMyEntityToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Throw if validation failed
|
||||
if (isDefined(result)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
result,
|
||||
'Validation errors occurred while creating entity',
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Return freshly cached flat entity
|
||||
const { flatMyEntityMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatMyEntityMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: universalFlatMyEntityToCreate.id,
|
||||
flatEntityMaps: flatMyEntityMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Service pattern**:
|
||||
1. Transform input → universal flat entity
|
||||
2. Call `validateBuildAndRunWorkspaceMigration`
|
||||
3. Throw if validation errors
|
||||
4. **Return flat entity** (not DTO)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Create Resolver Layer
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/my-entity.resolver.ts`
|
||||
|
||||
```typescript
|
||||
import { UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { MyEntityService } from 'src/engine/metadata-modules/my-entity/my-entity.service';
|
||||
import { fromFlatMyEntityToMyEntityDto } from 'src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util';
|
||||
|
||||
@Resolver(() => MyEntityDto)
|
||||
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
|
||||
export class MyEntityResolver {
|
||||
constructor(private readonly myEntityService: MyEntityService) {}
|
||||
|
||||
@Mutation(() => MyEntityDto)
|
||||
async createMyEntity(
|
||||
@Args('input') input: CreateMyEntityInput,
|
||||
@Workspace() { id: workspaceId }: Workspace,
|
||||
): Promise<MyEntityDto> {
|
||||
// Service returns flat entity
|
||||
const flatMyEntity = await this.myEntityService.create(input, workspaceId);
|
||||
|
||||
// Resolver converts flat entity to DTO
|
||||
return fromFlatMyEntityToMyEntityDto(flatMyEntity);
|
||||
}
|
||||
|
||||
@Mutation(() => MyEntityDto)
|
||||
async updateMyEntity(
|
||||
@Args('id') id: string,
|
||||
@Args('input') input: UpdateMyEntityInput,
|
||||
@Workspace() { id: workspaceId }: Workspace,
|
||||
): Promise<MyEntityDto> {
|
||||
const flatMyEntity = await this.myEntityService.update(id, input, workspaceId);
|
||||
return fromFlatMyEntityToMyEntityDto(flatMyEntity);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteMyEntity(
|
||||
@Args('id') id: string,
|
||||
@Workspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
await this.myEntityService.delete(id, workspaceId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Resolver responsibilities**:
|
||||
- Receives flat entities from service
|
||||
- **Converts flat → DTO** using conversion utility
|
||||
- Returns DTOs to GraphQL API
|
||||
- Uses exception interceptor for error formatting
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Flat-to-DTO Conversion
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util.ts`
|
||||
|
||||
```typescript
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
|
||||
|
||||
export const fromFlatMyEntityToMyEntityDto = (
|
||||
flatMyEntity: FlatMyEntity,
|
||||
): MyEntityDto => {
|
||||
return {
|
||||
id: flatMyEntity.id,
|
||||
name: flatMyEntity.name,
|
||||
label: flatMyEntity.label,
|
||||
description: flatMyEntity.description,
|
||||
isCustom: flatMyEntity.isCustom,
|
||||
createdAt: flatMyEntity.createdAt,
|
||||
updatedAt: flatMyEntity.updatedAt,
|
||||
// Convert foreign key IDs to relation objects if needed
|
||||
// parentEntity: flatMyEntity.parentEntityId ? { id: flatMyEntity.parentEntityId } : null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
| Layer | Input | Output | Responsibility |
|
||||
|-------|-------|--------|----------------|
|
||||
| **Service** | Input DTO | Flat Entity | Business logic, validation orchestration |
|
||||
| **Resolver** | Service result | DTO | Flat → DTO conversion, GraphQL exposure |
|
||||
|
||||
**Service Layer**:
|
||||
- Works with flat entities internally
|
||||
- Returns `FlatMyEntity` type
|
||||
- No knowledge of DTOs or GraphQL types
|
||||
|
||||
**Resolver Layer**:
|
||||
- Receives flat entities from service
|
||||
- Converts flat entities to DTOs
|
||||
- Returns DTOs to GraphQL API
|
||||
|
||||
---
|
||||
|
||||
## Exception Interceptor
|
||||
|
||||
The `WorkspaceMigrationGraphqlApiExceptionInterceptor` automatically handles:
|
||||
|
||||
1. `FlatEntityMapsException` → Converts to GraphQL errors (NotFoundError, etc.)
|
||||
2. `WorkspaceMigrationBuilderException` → Formats validation errors with i18n
|
||||
3. `WorkspaceMigrationRunnerException` → Formats runner errors
|
||||
|
||||
**What it does**:
|
||||
- Catches exceptions and formats for API responses
|
||||
- Translates error messages based on user locale
|
||||
- Ensures consistent error structure for frontend
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 6 (Testing):
|
||||
|
||||
- [ ] Builder registered in builder module (providers + exports)
|
||||
- [ ] Validator registered in validators module (providers + exports)
|
||||
- [ ] All 3 action handlers registered in action handlers module (providers)
|
||||
- [ ] Service layer created
|
||||
- [ ] Service returns flat entities (not DTOs)
|
||||
- [ ] Resolver layer created
|
||||
- [ ] Resolver uses exception interceptor
|
||||
- [ ] Resolver converts flat → DTO
|
||||
- [ ] Flat-to-DTO conversion utility created
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once integration is complete, proceed to (**MANDATORY**):
|
||||
**[Syncable Entity: Integration Testing (Step 6/6)](../syncable-entity-testing/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -1,355 +0,0 @@
|
||||
---
|
||||
name: syncable-entity-runner-and-actions
|
||||
description: Implement action handlers for executing workspace migrations in Twenty. Use when creating database operations for syncable entities, implementing universal-to-flat entity transpilation, or handling create/update/delete actions in the runner layer.
|
||||
---
|
||||
|
||||
# Syncable Entity: Runner & Actions (Step 4/6)
|
||||
|
||||
**Purpose**: Execute migration actions against the database with proper transpilation from universal to flat entities.
|
||||
|
||||
**When to use**: After completing Steps 1-3 (Types, Cache, Builder). Required before integration.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Create action handler
|
||||
2. Update action handler
|
||||
3. Delete action handler
|
||||
4. Universal-to-flat conversion utilities
|
||||
|
||||
**Key pattern**: Each handler has two phases:
|
||||
1. **Transpilation**: Universal action → Flat action
|
||||
2. **Execution**: Flat action → Database operation
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Action Handler
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCreateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-create-action-handler.service';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
|
||||
import {
|
||||
type UniversalCreateMyEntityAction,
|
||||
type FlatCreateMyEntityAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateMyEntityActionHandlerService extends WorkspaceCreateActionHandlerService<
|
||||
'myEntity',
|
||||
UniversalCreateMyEntityAction,
|
||||
FlatCreateMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
// Phase 1: Transpile universal action to flat action
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction: UniversalCreateMyEntityAction,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatCreateMyEntityAction {
|
||||
return {
|
||||
type: 'create',
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 2: Execute flat action against database
|
||||
protected async executeForMetadata(
|
||||
flatActions: FlatCreateMyEntityAction[],
|
||||
): Promise<void> {
|
||||
const flatEntities = flatActions.map((action) => action.flatEntity);
|
||||
|
||||
await this.insertFlatEntitiesInRepository({
|
||||
repository: this.myEntityRepository,
|
||||
flatEntities,
|
||||
});
|
||||
}
|
||||
|
||||
protected async executeForWorkspaceSchema(): Promise<void> {
|
||||
// No workspace schema changes needed for metadata-only entity
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key helper methods**:
|
||||
- `transpileUniversalActionToFlatAction`: Converts universal → flat
|
||||
- `insertFlatEntitiesInRepository`: Base class helper for inserts
|
||||
- `executeForMetadata`: Metadata database operations
|
||||
- `executeForWorkspaceSchema`: Workspace schema changes (if needed)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Update Action Handler
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceUpdateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-update-action-handler.service';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
|
||||
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateMyEntityActionHandlerService extends WorkspaceUpdateActionHandlerService<
|
||||
'myEntity',
|
||||
UniversalUpdateMyEntityAction,
|
||||
FlatUpdateMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction: UniversalUpdateMyEntityAction,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatUpdateMyEntityAction {
|
||||
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
);
|
||||
|
||||
// Resolve universal foreign keys in updates to regular IDs
|
||||
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'myEntity',
|
||||
universalUpdates: universalAction.universalUpdates,
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'update',
|
||||
metadataName: 'myEntity',
|
||||
flatEntity,
|
||||
updates: flatUpdates,
|
||||
};
|
||||
}
|
||||
|
||||
protected async executeForMetadata(
|
||||
flatActions: FlatUpdateMyEntityAction[],
|
||||
): Promise<void> {
|
||||
for (const action of flatActions) {
|
||||
await this.myEntityRepository.update(
|
||||
{ id: action.flatEntity.id },
|
||||
action.updates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async executeForWorkspaceSchema(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Update-specific helper**:
|
||||
- `resolveUniversalUpdateRelationIdentifiersToIds`: Maps universal identifiers back to regular IDs in the updates object
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Delete Action Handler
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceDeleteActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-delete-action-handler.service';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteMyEntityActionHandlerService extends WorkspaceDeleteActionHandlerService<
|
||||
'myEntity',
|
||||
UniversalDeleteMyEntityAction,
|
||||
FlatDeleteMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction: UniversalDeleteMyEntityAction,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatDeleteMyEntityAction {
|
||||
// Use base class helper for delete transpilation
|
||||
return this.transpileUniversalDeleteActionToFlatDeleteAction({
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
|
||||
});
|
||||
}
|
||||
|
||||
protected async executeForMetadata(
|
||||
flatActions: FlatDeleteMyEntityAction[],
|
||||
): Promise<void> {
|
||||
const ids = flatActions.map((action) => action.flatEntity.id);
|
||||
|
||||
await this.myEntityRepository.delete(ids);
|
||||
}
|
||||
|
||||
protected async executeForWorkspaceSchema(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Delete-specific helper**:
|
||||
- `transpileUniversalDeleteActionToFlatDeleteAction`: Base class helper that handles standard delete transpilation
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Universal-to-Flat Conversion
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
|
||||
|
||||
export const fromUniversalFlatMyEntityToFlatMyEntity = (
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatMyEntity => {
|
||||
// Resolve universal foreign keys back to regular IDs
|
||||
return resolveUniversalRelationIdentifiersToIds({
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
flatEntityMaps,
|
||||
}) as FlatMyEntity;
|
||||
};
|
||||
```
|
||||
|
||||
**Key utility**:
|
||||
- `resolveUniversalRelationIdentifiersToIds`: Maps universal identifiers → regular IDs (reverse of `resolveEntityRelationUniversalIdentifiers`)
|
||||
|
||||
---
|
||||
|
||||
## Action Handler Patterns
|
||||
|
||||
### Pattern: Create Handler
|
||||
```typescript
|
||||
// 1. Transpile: Universal → Flat
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
) {
|
||||
return {
|
||||
type: 'create',
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Execute: Flat → Database
|
||||
protected async executeForMetadata(flatActions) {
|
||||
await this.insertFlatEntitiesInRepository({
|
||||
repository: this.myEntityRepository,
|
||||
flatEntities: flatActions.map(a => a.flatEntity),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Update Handler
|
||||
```typescript
|
||||
// Transpile with update-specific resolution
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
) {
|
||||
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
);
|
||||
|
||||
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'myEntity',
|
||||
universalUpdates: universalAction.universalUpdates,
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
return { type: 'update', metadataName: 'myEntity', flatEntity, updates: flatUpdates };
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Delete Handler
|
||||
```typescript
|
||||
// Use base class helper
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
) {
|
||||
return this.transpileUniversalDeleteActionToFlatDeleteAction({
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete
|
||||
protected async executeForMetadata(flatActions) {
|
||||
const ids = flatActions.map(a => a.flatEntity.id);
|
||||
await this.myEntityRepository.delete(ids);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 5:
|
||||
|
||||
- [ ] Create action handler implemented
|
||||
- [ ] Update action handler implemented
|
||||
- [ ] Delete action handler implemented
|
||||
- [ ] All handlers extend appropriate base class
|
||||
- [ ] `transpileUniversalActionToFlatAction` implemented in all handlers
|
||||
- [ ] `executeForMetadata` implemented in all handlers
|
||||
- [ ] `executeForWorkspaceSchema` implemented (or returns empty)
|
||||
- [ ] Universal-to-flat conversion utility created
|
||||
- [ ] Create handler uses `insertFlatEntitiesInRepository`
|
||||
- [ ] Update handler uses `resolveUniversalUpdateRelationIdentifiersToIds`
|
||||
- [ ] Delete handler uses `transpileUniversalDeleteActionToFlatDeleteAction`
|
||||
- [ ] Delete handler uses hard delete (`delete()`)
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once action handlers are complete, proceed to:
|
||||
**[Syncable Entity: Integration (Step 5/6)](../syncable-entity-integration/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -1,494 +0,0 @@
|
||||
---
|
||||
name: syncable-entity-testing
|
||||
description: Create comprehensive integration tests for syncable entities in Twenty. Use when writing integration tests for metadata entities, covering validator exceptions, input transpilation errors, and CRUD operations. Tests are MANDATORY for all syncable entities.
|
||||
---
|
||||
|
||||
# Syncable Entity: Integration Testing (Step 6/6 - MANDATORY)
|
||||
|
||||
**Purpose**: Create comprehensive test suite covering all validation scenarios, input transpilation exceptions, and successful use cases.
|
||||
|
||||
**When to use**: After completing Steps 1-5. Integration tests are **REQUIRED** for all syncable entities.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
Tests must cover:
|
||||
1. **Failing scenarios** - All validator exceptions and input transpilation errors
|
||||
2. **Successful scenarios** - All CRUD operations and edge cases
|
||||
3. **Test utilities** - Reusable query factories and helper functions
|
||||
|
||||
**Test pattern**: Two-file pattern (query factory + wrapper) for each operation.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Test Utilities
|
||||
|
||||
### Pattern: Query Factory
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util.ts`
|
||||
|
||||
```typescript
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
|
||||
export type CreateMyEntityFactoryInput = CreateMyEntityInput;
|
||||
|
||||
const DEFAULT_MY_ENTITY_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
label
|
||||
description
|
||||
isCustom
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
|
||||
export const createMyEntityQueryFactory = ({
|
||||
input,
|
||||
gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
|
||||
query: gql`
|
||||
mutation CreateMyEntity($input: CreateMyEntityInput!) {
|
||||
createMyEntity(input: $input) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Wrapper Utility
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import {
|
||||
type CreateMyEntityFactoryInput,
|
||||
createMyEntityQueryFactory,
|
||||
} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
|
||||
|
||||
export const createMyEntity = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
|
||||
createMyEntity: MyEntityDto;
|
||||
}> => {
|
||||
const graphqlOperation = createMyEntityQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'My entity creation should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'My entity creation has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
```
|
||||
|
||||
**Required utilities** (follow same pattern):
|
||||
- `update-my-entity-query-factory.util.ts` + `update-my-entity.util.ts`
|
||||
- `delete-my-entity-query-factory.util.ts` + `delete-my-entity.util.ts`
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Failing Creation Tests
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts`
|
||||
|
||||
```typescript
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
|
||||
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
|
||||
type TestContext = {
|
||||
input: CreateMyEntityInput;
|
||||
};
|
||||
|
||||
type GlobalTestContext = {
|
||||
existingEntityLabel: string;
|
||||
existingEntityName: string;
|
||||
};
|
||||
|
||||
const globalTestContext: GlobalTestContext = {
|
||||
existingEntityLabel: 'Existing Test Entity',
|
||||
existingEntityName: 'existingTestEntity',
|
||||
};
|
||||
|
||||
type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];
|
||||
|
||||
describe('My entity creation should fail', () => {
|
||||
let existingEntityId: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Setup: Create entity for uniqueness tests
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: globalTestContext.existingEntityName,
|
||||
label: globalTestContext.existingEntityLabel,
|
||||
},
|
||||
});
|
||||
|
||||
existingEntityId = data.createMyEntity.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
if (isDefined(existingEntityId)) {
|
||||
await deleteMyEntity({
|
||||
expectToFail: false,
|
||||
input: { id: existingEntityId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
|
||||
// Input transpilation validation
|
||||
{
|
||||
title: 'when name is missing',
|
||||
context: {
|
||||
input: {
|
||||
label: 'Entity Missing Name',
|
||||
} as CreateMyEntityInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when label is missing',
|
||||
context: {
|
||||
input: {
|
||||
name: 'entityMissingLabel',
|
||||
} as CreateMyEntityInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when name is empty string',
|
||||
context: {
|
||||
input: {
|
||||
name: '',
|
||||
label: 'Empty Name Entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Validator business logic
|
||||
{
|
||||
title: 'when name already exists (uniqueness)',
|
||||
context: {
|
||||
input: {
|
||||
name: globalTestContext.existingEntityName,
|
||||
label: 'Duplicate Name Entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when trying to create standard entity',
|
||||
context: {
|
||||
input: {
|
||||
name: 'myEntity',
|
||||
label: 'Standard Entity',
|
||||
isCustom: false,
|
||||
} as CreateMyEntityInput,
|
||||
},
|
||||
},
|
||||
|
||||
// Foreign key validation
|
||||
{
|
||||
title: 'when parentEntityId does not exist',
|
||||
context: {
|
||||
input: {
|
||||
name: 'invalidParentEntity',
|
||||
label: 'Invalid Parent Entity',
|
||||
parentEntityId: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const { errors } = await createMyEntity({
|
||||
expectToFail: true,
|
||||
input: context.input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
**Test coverage requirements**:
|
||||
- ✅ Missing required fields
|
||||
- ✅ Empty strings
|
||||
- ✅ Invalid format
|
||||
- ✅ Uniqueness violations
|
||||
- ✅ Standard entity protection
|
||||
- ✅ Foreign key validation
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Successful Creation Tests
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/successful-my-entity-creation.integration-spec.ts`
|
||||
|
||||
```typescript
|
||||
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
|
||||
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
|
||||
describe('My entity creation should succeed', () => {
|
||||
let createdEntityId: string;
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdEntityId) {
|
||||
await deleteMyEntity({
|
||||
expectToFail: false,
|
||||
input: { id: createdEntityId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should create entity with minimal required input', async () => {
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'minimalEntity',
|
||||
label: 'Minimal Entity',
|
||||
},
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'minimalEntity',
|
||||
label: 'Minimal Entity',
|
||||
description: null,
|
||||
isCustom: true,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create entity with all optional fields', async () => {
|
||||
const input = {
|
||||
name: 'fullEntity',
|
||||
label: 'Full Entity',
|
||||
description: 'Entity with all fields specified',
|
||||
} as const satisfies CreateMyEntityInput;
|
||||
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input,
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'fullEntity',
|
||||
label: 'Full Entity',
|
||||
description: 'Entity with all fields specified',
|
||||
isCustom: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize input by trimming whitespace', async () => {
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: ' entityWithSpaces ',
|
||||
label: ' Entity With Spaces ',
|
||||
description: ' Description with spaces ',
|
||||
},
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'entityWithSpaces',
|
||||
label: 'Entity With Spaces',
|
||||
description: 'Description with spaces',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle long text content', async () => {
|
||||
const longDescription = 'A'.repeat(1000);
|
||||
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'longDescEntity',
|
||||
label: 'Long Description Entity',
|
||||
description: longDescription,
|
||||
},
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
description: longDescription,
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Test coverage requirements**:
|
||||
- ✅ Minimal required input
|
||||
- ✅ All optional fields
|
||||
- ✅ Input sanitization
|
||||
- ✅ Long text content
|
||||
- ✅ Special characters
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Update and Delete Tests
|
||||
|
||||
Create similar test files for update and delete operations:
|
||||
|
||||
**Required files**:
|
||||
- `failing-my-entity-update.integration-spec.ts`
|
||||
- `successful-my-entity-update.integration-spec.ts`
|
||||
- `failing-my-entity-deletion.integration-spec.ts`
|
||||
- `successful-my-entity-deletion.integration-spec.ts`
|
||||
|
||||
---
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### Pattern: Cleanup
|
||||
```typescript
|
||||
afterEach(async () => {
|
||||
if (createdEntityId) {
|
||||
await deleteMyEntity({
|
||||
expectToFail: false,
|
||||
input: { id: createdEntityId },
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Type-Safe Inputs
|
||||
```typescript
|
||||
const input = {
|
||||
name: 'myEntity',
|
||||
label: 'My Entity',
|
||||
} as const satisfies CreateMyEntityInput;
|
||||
```
|
||||
|
||||
### Pattern: Snapshot Testing
|
||||
```typescript
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all entity tests
|
||||
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs
|
||||
|
||||
# Run specific test file
|
||||
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs
|
||||
|
||||
# Update snapshots
|
||||
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Test Checklist
|
||||
|
||||
### Test Utilities
|
||||
- [ ] `create-my-entity-query-factory.util.ts` created
|
||||
- [ ] `create-my-entity.util.ts` created
|
||||
- [ ] `update-my-entity-query-factory.util.ts` created
|
||||
- [ ] `update-my-entity.util.ts` created
|
||||
- [ ] `delete-my-entity-query-factory.util.ts` created
|
||||
- [ ] `delete-my-entity.util.ts` created
|
||||
|
||||
### Failing Tests Coverage
|
||||
- [ ] Missing required fields
|
||||
- [ ] Empty string validation
|
||||
- [ ] Uniqueness violations
|
||||
- [ ] Standard entity protection
|
||||
- [ ] Foreign key validation
|
||||
- [ ] JSONB property validation (if applicable)
|
||||
|
||||
### Successful Tests Coverage
|
||||
- [ ] Create with minimal input
|
||||
- [ ] Create with all optional fields
|
||||
- [ ] Input sanitization (whitespace)
|
||||
- [ ] Long text content
|
||||
- [ ] Update single field
|
||||
- [ ] Update multiple fields
|
||||
- [ ] Successful deletion
|
||||
|
||||
### Snapshot Tests
|
||||
- [ ] All failing tests use `expectOneNotInternalServerErrorSnapshot`
|
||||
- [ ] Snapshots committed to `__snapshots__/` directory
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Your integration tests are complete when:
|
||||
|
||||
✅ All test utilities created (minimum 6 files)
|
||||
✅ Failing creation tests cover all validators
|
||||
✅ Failing update tests cover business rules
|
||||
✅ Failing deletion tests cover protection rules
|
||||
✅ Successful tests cover all use cases
|
||||
✅ All snapshots generated and committed
|
||||
✅ All tests pass consistently
|
||||
✅ Test coverage meets requirements (>80%)
|
||||
|
||||
---
|
||||
|
||||
## Final Step
|
||||
|
||||
✅ **Step 6 Complete!** → Your syncable entity is fully tested and production-ready!
|
||||
|
||||
**Congratulations!** You've successfully created a new syncable entity in Twenty's workspace migration system.
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -1,309 +0,0 @@
|
||||
---
|
||||
name: syncable-entity-types-and-constants
|
||||
description: Define types, entities, and central constant registrations for syncable entities in Twenty's workspace migration system. Use when creating new syncable entities, defining TypeORM entities, flat entity types, or registering in central constants (ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME, ALL_METADATA_RELATIONS, ALL_UNIVERSAL_METADATA_RELATIONS).
|
||||
---
|
||||
|
||||
# Syncable Entity: Types & Constants (Step 1/6)
|
||||
|
||||
**Purpose**: Define all types, entities, and register in central constants. This is the foundation - everything else depends on these types being correct.
|
||||
|
||||
**When to use**: First step when creating any new syncable entity. Must be completed before other steps.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Metadata name constant (twenty-shared)
|
||||
2. TypeORM entity (extends `SyncableEntity`)
|
||||
3. Flat entity types
|
||||
4. Action types (universal + flat)
|
||||
5. Central constant registrations (4 constants)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Add Metadata Name
|
||||
|
||||
**File**: `packages/twenty-shared/src/metadata/all-metadata-name.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const ALL_METADATA_NAME = {
|
||||
// ... existing entries
|
||||
myEntity: 'myEntity',
|
||||
} as const;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create TypeORM Entity
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/entities/my-entity.entity.ts`
|
||||
|
||||
```typescript
|
||||
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity({ name: 'myEntity' })
|
||||
export class MyEntityEntity extends SyncableEntity {
|
||||
@Column({ type: 'varchar' })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
label: string;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isCustom: boolean;
|
||||
|
||||
// Foreign key example (optional)
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
parentEntityId: string | null;
|
||||
|
||||
@ManyToOne(() => ParentEntityEntity, { nullable: true })
|
||||
@JoinColumn({ name: 'parentEntityId' })
|
||||
parentEntity: ParentEntityEntity | null;
|
||||
|
||||
// JSONB column example (optional)
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
settings: Record<string, any> | null;
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules**:
|
||||
- Must extend `SyncableEntity` (provides `id`, `universalIdentifier`, `applicationId`, etc.)
|
||||
- Must have `isCustom` boolean column
|
||||
- Use `@Column({ type: 'jsonb' })` for JSON data
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Define Flat Entity Types
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts`
|
||||
|
||||
```typescript
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
|
||||
export type FlatMyEntity = FlatEntityFrom<MyEntityEntity>;
|
||||
```
|
||||
|
||||
**Maps file** (if entity has indexed lookups):
|
||||
|
||||
```typescript
|
||||
// flat-my-entity-maps.type.ts
|
||||
export type FlatMyEntityMaps = {
|
||||
byId: Record<string, FlatMyEntity>;
|
||||
byName: Record<string, FlatMyEntity>;
|
||||
// Add other indexes as needed
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Define Editable Properties
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/constants/editable-flat-my-entity-properties.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const EDITABLE_FLAT_MY_ENTITY_PROPERTIES = [
|
||||
'name',
|
||||
'label',
|
||||
'description',
|
||||
'parentEntityId',
|
||||
'settings',
|
||||
] as const satisfies ReadonlyArray<keyof FlatMyEntity>;
|
||||
```
|
||||
|
||||
**Rule**: Only include properties that can be updated (exclude `id`, `createdAt`, `universalIdentifier`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Define Action Types
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type.ts`
|
||||
|
||||
```typescript
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
|
||||
// Universal actions (used by builder/runner)
|
||||
export type UniversalCreateMyEntityAction = {
|
||||
type: 'create';
|
||||
metadataName: 'myEntity';
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
};
|
||||
|
||||
export type UniversalUpdateMyEntityAction = {
|
||||
type: 'update';
|
||||
metadataName: 'myEntity';
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
universalUpdates: Partial<UniversalFlatMyEntity>;
|
||||
};
|
||||
|
||||
export type UniversalDeleteMyEntityAction = {
|
||||
type: 'delete';
|
||||
metadataName: 'myEntity';
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
};
|
||||
|
||||
// Flat actions (internal to runner)
|
||||
export type FlatCreateMyEntityAction = {
|
||||
type: 'create';
|
||||
metadataName: 'myEntity';
|
||||
flatEntity: FlatMyEntity;
|
||||
};
|
||||
|
||||
export type FlatUpdateMyEntityAction = {
|
||||
type: 'update';
|
||||
metadataName: 'myEntity';
|
||||
flatEntity: FlatMyEntity;
|
||||
updates: Partial<FlatMyEntity>;
|
||||
};
|
||||
|
||||
export type FlatDeleteMyEntityAction = {
|
||||
type: 'delete';
|
||||
metadataName: 'myEntity';
|
||||
flatEntity: FlatMyEntity;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Register in Central Constants
|
||||
|
||||
### 6a. AllFlatEntityTypesByMetadataName
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts`
|
||||
|
||||
```typescript
|
||||
export type AllFlatEntityTypesByMetadataName = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
flatEntityMaps: FlatMyEntityMaps;
|
||||
universalActions: {
|
||||
create: UniversalCreateMyEntityAction;
|
||||
update: UniversalUpdateMyEntityAction;
|
||||
delete: UniversalDeleteMyEntityAction;
|
||||
};
|
||||
flatActions: {
|
||||
create: FlatCreateMyEntityAction;
|
||||
update: FlatUpdateMyEntityAction;
|
||||
delete: FlatDeleteMyEntityAction;
|
||||
};
|
||||
flatEntity: FlatMyEntity;
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
entity: MyEntityEntity;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 6b. ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
name: { toCompare: true },
|
||||
label: { toCompare: true },
|
||||
description: { toCompare: true },
|
||||
parentEntityId: {
|
||||
toCompare: true,
|
||||
universalProperty: 'parentEntityUniversalIdentifier',
|
||||
},
|
||||
settings: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: 'universalSettings',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- `toCompare: true` → Editable property (checked for changes)
|
||||
- `toStringify: true` → JSONB/object property (needs JSON serialization)
|
||||
- `universalProperty` → Maps to universal version (for foreign keys & JSONB with `SerializedRelation`)
|
||||
|
||||
### 6c. ALL_METADATA_RELATIONS
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const ALL_METADATA_RELATIONS = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
manyToOne: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
parentEntity: {
|
||||
metadataName: 'parentEntity',
|
||||
flatEntityForeignKeyAggregator: 'myEntityIds',
|
||||
foreignKey: 'parentEntityId',
|
||||
isNullable: false,
|
||||
},
|
||||
},
|
||||
oneToMany: {
|
||||
childEntities: { metadataName: 'childEntity' },
|
||||
},
|
||||
// Only if JSONB contains SerializedRelation fields
|
||||
serializedRelations: {
|
||||
fieldMetadata: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 6d. ALL_UNIVERSAL_METADATA_RELATIONS
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const ALL_UNIVERSAL_METADATA_RELATIONS = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
manyToOne: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
parentEntity: {
|
||||
metadataName: 'parentEntity',
|
||||
foreignKey: 'parentEntityId',
|
||||
universalForeignKey: 'parentEntityUniversalIdentifier',
|
||||
universalFlatEntityForeignKeyAggregator: 'myEntityUniversalIdentifiers',
|
||||
isNullable: false,
|
||||
},
|
||||
},
|
||||
oneToMany: {
|
||||
childEntities: { metadataName: 'childEntity' },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 2:
|
||||
|
||||
- [ ] Metadata name added to `ALL_METADATA_NAME`
|
||||
- [ ] TypeORM entity created (extends `SyncableEntity`)
|
||||
- [ ] `isCustom` column added
|
||||
- [ ] Flat entity type defined
|
||||
- [ ] Flat entity maps type defined (if needed)
|
||||
- [ ] Editable properties constant defined
|
||||
- [ ] Universal and flat action types defined
|
||||
- [ ] Registered in `AllFlatEntityTypesByMetadataName`
|
||||
- [ ] Registered in `ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME`
|
||||
- [ ] Registered in `ALL_METADATA_RELATIONS`
|
||||
- [ ] Registered in `ALL_UNIVERSAL_METADATA_RELATIONS`
|
||||
- [ ] TypeScript compiles without errors
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once all types and constants are defined, proceed to:
|
||||
**[Syncable Entity: Cache & Transform (Step 2/6)](../syncable-entity-cache-and-transform/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
packages/create-twenty-app/**
|
||||
!packages/create-twenty-app/package.json
|
||||
create-app-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
|
||||
@@ -28,14 +28,11 @@ jobs:
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-sdk/**
|
||||
!packages/twenty-sdk/package.json
|
||||
changed-files-check-e2e:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
playwright.config.ts
|
||||
.github/workflows/ci-front.yaml
|
||||
front-sb-build:
|
||||
@@ -197,19 +194,10 @@ jobs:
|
||||
tag: scope:frontend
|
||||
tasks: reset:env
|
||||
- name: Run ${{ matrix.task }} task
|
||||
id: run-task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
- name: Check for coverage threshold failure
|
||||
if: always() && steps.run-task.outcome == 'failure' && matrix.task == 'test'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::error::The test task failed. If no individual test is failing, this is likely a coverage threshold not being met."
|
||||
echo ""
|
||||
echo "To debug locally, run: npx nx run twenty-front:test:ci"
|
||||
exit 1
|
||||
- name: Save ${{ matrix.task }} cache
|
||||
uses: ./.github/actions/save-cache
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,6 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-sdk/**
|
||||
!packages/twenty-sdk/package.json
|
||||
sdk-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
@@ -26,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]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
@@ -40,62 +39,72 @@ 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 uncomment when syncApplication resolver is fixed
|
||||
# sdk-e2e-integration-test:
|
||||
# timeout-minutes: 30
|
||||
# runs-on: ubuntu-latest-8-cores
|
||||
# needs: [changed-files-check, sdk-test]
|
||||
# strategy:
|
||||
# matrix:
|
||||
# task: [test:integration, test:e2e]
|
||||
# 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 ${{ matrix.task }} Tests
|
||||
# uses: ./.github/actions/nx-affected
|
||||
# with:
|
||||
# tag: scope:sdk
|
||||
# tasks: ${{ matrix.task }}
|
||||
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]
|
||||
# TODO uncomment when syncApplication resolver is fixed
|
||||
# needs: [changed-files-check, sdk-test, sdk-e2e-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
.nx/installation
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.nx/nxw.js
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
@@ -50,4 +49,3 @@ dump.rdb
|
||||
mcp.json
|
||||
/.junie/
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
// This file should be committed to your repository! It wraps Nx and ensures
|
||||
// that your local installation matches nx.json.
|
||||
// See: https://nx.dev/recipes/installation/install-non-javascript for more info.
|
||||
|
||||
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
const installationPath = path.join(__dirname, 'installation', 'package.json');
|
||||
function matchesCurrentNxInstall(currentInstallation, nxJsonInstallation) {
|
||||
if (!currentInstallation.devDependencies ||
|
||||
!Object.keys(currentInstallation.devDependencies).length) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (currentInstallation.devDependencies['nx'] !==
|
||||
nxJsonInstallation.version ||
|
||||
require(path.join(path.dirname(installationPath), 'node_modules', 'nx', 'package.json')).version !== nxJsonInstallation.version) {
|
||||
return false;
|
||||
}
|
||||
for (const [plugin, desiredVersion] of Object.entries(nxJsonInstallation.plugins || {})) {
|
||||
if (currentInstallation.devDependencies[plugin] !== desiredVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function ensureDir(p) {
|
||||
if (!fs.existsSync(p)) {
|
||||
fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
}
|
||||
function getCurrentInstallation() {
|
||||
try {
|
||||
return require(installationPath);
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
name: 'nx-installation',
|
||||
version: '0.0.0',
|
||||
devDependencies: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
function performInstallation(currentInstallation, nxJson) {
|
||||
fs.writeFileSync(installationPath, JSON.stringify({
|
||||
name: 'nx-installation',
|
||||
devDependencies: {
|
||||
nx: nxJson.installation.version,
|
||||
...nxJson.installation.plugins,
|
||||
},
|
||||
}));
|
||||
try {
|
||||
cp.execSync('npm i', {
|
||||
cwd: path.dirname(installationPath),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
// revert possible changes to the current installation
|
||||
fs.writeFileSync(installationPath, JSON.stringify(currentInstallation));
|
||||
// rethrow
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
function ensureUpToDateInstallation() {
|
||||
const nxJsonPath = path.join(__dirname, '..', 'nx.json');
|
||||
let nxJson;
|
||||
try {
|
||||
nxJson = require(nxJsonPath);
|
||||
if (!nxJson.installation) {
|
||||
console.error('[NX]: The "installation" entry in the "nx.json" file is required when running the nx wrapper. See https://nx.dev/recipes/installation/install-non-javascript');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.error('[NX]: The "nx.json" file is required when running the nx wrapper. See https://nx.dev/recipes/installation/install-non-javascript');
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
ensureDir(path.join(__dirname, 'installation'));
|
||||
const currentInstallation = getCurrentInstallation();
|
||||
if (!matchesCurrentNxInstall(currentInstallation, nxJson.installation)) {
|
||||
performInstallation(currentInstallation, nxJson);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const messageLines = [
|
||||
'[NX]: Nx wrapper failed to synchronize installation.',
|
||||
];
|
||||
if (e instanceof Error) {
|
||||
messageLines.push('');
|
||||
messageLines.push(e.message);
|
||||
messageLines.push(e.stack);
|
||||
}
|
||||
else {
|
||||
messageLines.push(e.toString());
|
||||
}
|
||||
console.error(messageLines.join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!process.env.NX_WRAPPER_SKIP_INSTALL) {
|
||||
ensureUpToDateInstallation();
|
||||
}
|
||||
|
||||
require('./installation/node_modules/nx/bin/nx');
|
||||
+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,
|
||||
|
||||
@@ -272,6 +272,9 @@
|
||||
"inputs": ["default", "^default"]
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"version": "22.3.3"
|
||||
},
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
"application": {
|
||||
|
||||
+4
-6
@@ -22,7 +22,6 @@
|
||||
"googleapis": "105",
|
||||
"hex-rgb": "^5.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"jotai": "^2.17.1",
|
||||
"libphonenumber-js": "^1.10.26",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
@@ -138,9 +137,9 @@
|
||||
"@typescript-eslint/utils": "^8.39.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"@vitejs/plugin-react-swc": "3.11.0",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/coverage-istanbul": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/browser-playwright": "^4.0.17",
|
||||
"@vitest/coverage-istanbul": "^4.0.17",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@yarnpkg/types": "^4.0.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.2.2",
|
||||
@@ -183,11 +182,10 @@
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.18"
|
||||
"vitest": "^4.0.17"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
|
||||
@@ -35,46 +35,41 @@ cd my-twenty-app
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Get help and list all available commands
|
||||
yarn twenty help
|
||||
# Get help
|
||||
yarn run help
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn twenty function:execute -n my-function -p '{"key": "value"}'
|
||||
yarn function:execute -n my-function -p '{"key": "value"}'
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
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
|
||||
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
|
||||
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
|
||||
- Example placeholders to help you add entities, actions, and sync logic
|
||||
|
||||
## Next steps
|
||||
- Run `yarn twenty help` to see all available commands.
|
||||
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles).
|
||||
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- Keep your types up‑to‑date using `yarn twenty app:generate`.
|
||||
- Explore the generated project and add your first entity with `yarn entity:add` (functions, front components, objects, roles).
|
||||
- Keep your types up‑to‑date using `yarn app:generate`.
|
||||
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
|
||||
|
||||
## Publish your application
|
||||
@@ -102,8 +97,8 @@ git push
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty app:generate` runs without errors, then re‑start `yarn twenty app:dev`.
|
||||
- Auth prompts not appearing: run `yarn auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn app:generate` runs without errors, then re‑start `yarn app:dev`.
|
||||
|
||||
## Contributing
|
||||
- See our [GitHub](https://github.com/twentyhq/twenty)
|
||||
|
||||
@@ -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,13 +1,11 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.6.0-alpha",
|
||||
"version": "0.4.0",
|
||||
"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"
|
||||
@@ -45,9 +43,6 @@
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"twenty-sdk": "workspace:*",
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
|
||||
@@ -5,36 +5,34 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
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 twenty app:dev # Start dev mode (watch, build, and sync)
|
||||
yarn twenty entity:add # Add a new entity (function, front-component, object, role)
|
||||
yarn twenty app:generate # Generate typed Twenty client
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
yarn app:dev # Start dev mode (watch, build, and sync)
|
||||
yarn entity:add # Add a new entity (function, front-component, object, role)
|
||||
yarn app:generate # Generate typed Twenty client
|
||||
yarn function:logs # Stream function logs
|
||||
yarn function:execute # Execute a function with JSON payload
|
||||
yarn app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.role.ts exists in src/app/
|
||||
const roleConfigPath = join(srcAppPath, 'default.role.ts');
|
||||
expect(await fs.pathExists(roleConfigPath)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -70,8 +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('latest');
|
||||
expect(packageJson.scripts['twenty']).toBe('twenty');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.0');
|
||||
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
|
||||
});
|
||||
|
||||
it('should create .gitignore file', async () => {
|
||||
@@ -105,7 +102,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 defineApplication and correct values', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
@@ -113,7 +110,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');
|
||||
|
||||
// Verify it uses defineApplication
|
||||
@@ -124,7 +125,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
// Verify it imports the role identifier
|
||||
expect(appConfigContent).toContain(
|
||||
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'",
|
||||
"import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role'",
|
||||
);
|
||||
|
||||
// Verify display name and description
|
||||
@@ -142,7 +143,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should create default-role.ts with defineRole and correct values', async () => {
|
||||
it('should create default.role.ts with defineRole and correct values', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
@@ -150,12 +151,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const roleConfigPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'roles',
|
||||
DEFAULT_ROLE_FILE_NAME,
|
||||
);
|
||||
const roleConfigPath = join(testAppDirectory, 'src', 'default.role.ts');
|
||||
const roleConfigContent = await fs.readFile(roleConfigPath, 'utf8');
|
||||
|
||||
// Verify it uses defineRole
|
||||
@@ -210,7 +206,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 +239,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',
|
||||
);
|
||||
|
||||
@@ -280,12 +280,12 @@ describe('copyBaseApplicationProject', () => {
|
||||
});
|
||||
|
||||
const firstRoleConfig = await fs.readFile(
|
||||
join(firstAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
|
||||
join(firstAppDir, 'src', 'default.role.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const secondRoleConfig = await fs.readFile(
|
||||
join(secondAppDir, 'src', 'roles', DEFAULT_ROLE_FILE_NAME),
|
||||
join(secondAppDir, 'src', 'default.role.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
|
||||
@@ -33,27 +33,20 @@ export const copyBaseApplicationProject = async ({
|
||||
await createDefaultRoleConfig({
|
||||
displayName: appDisplayName,
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'roles',
|
||||
fileName: 'default-role.ts',
|
||||
});
|
||||
|
||||
await createDefaultFrontComponent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'front-components',
|
||||
fileName: 'hello-world.tsx',
|
||||
});
|
||||
|
||||
await createDefaultFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
fileName: 'hello-world.ts',
|
||||
});
|
||||
|
||||
await createApplicationConfig({
|
||||
displayName: appDisplayName,
|
||||
description: appDescription,
|
||||
appDirectory: sourceFolderPath,
|
||||
fileName: 'application-config.ts',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -115,13 +108,9 @@ yarn-error.log*
|
||||
const createDefaultRoleConfig = async ({
|
||||
displayName,
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
displayName: string;
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
@@ -141,18 +130,13 @@ export default defineRole({
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
await fs.writeFile(join(appDirectory, 'default.role.ts'), content);
|
||||
};
|
||||
|
||||
const createDefaultFrontComponent = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
@@ -175,20 +159,19 @@ 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';
|
||||
|
||||
@@ -203,33 +186,35 @@ export default defineLogicFunction({
|
||||
description: 'A simple logic function',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/hello-world-logic-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '${triggerUniversalIdentifier}',
|
||||
type: 'route',
|
||||
path: '/hello-world-logic-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.logic-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';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/default.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '${v4()}',
|
||||
@@ -239,8 +224,7 @@ export default defineApplication({
|
||||
});
|
||||
`;
|
||||
|
||||
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 ({
|
||||
@@ -261,18 +245,29 @@ const createPackageJson = async ({
|
||||
},
|
||||
packageManager: '[email protected]',
|
||||
scripts: {
|
||||
twenty: 'twenty',
|
||||
'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',
|
||||
'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': 'latest',
|
||||
'twenty-sdk': '0.4.0',
|
||||
},
|
||||
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",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import packageJson from './package.json';
|
||||
import type { PackageJson } from 'type-fest';
|
||||
|
||||
const moduleEntries = Object.keys((packageJson as any).exports || {})
|
||||
.filter(
|
||||
@@ -71,23 +70,13 @@ export default defineConfig(() => {
|
||||
outDir: 'dist',
|
||||
lib: { entry: entries, name: 'create-twenty-app' },
|
||||
rollupOptions: {
|
||||
external: (id: string) => {
|
||||
if (/^node:/.test(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const builtins = ['path', 'fs', 'child_process', 'util'];
|
||||
|
||||
if (builtins.includes(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deps = Object.keys(
|
||||
(packageJson as PackageJson).dependencies || {},
|
||||
);
|
||||
|
||||
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
|
||||
},
|
||||
external: [
|
||||
...Object.keys((packageJson as any).dependencies || {}),
|
||||
'path',
|
||||
'fs',
|
||||
'child_process',
|
||||
'util',
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'es',
|
||||
|
||||
@@ -13,7 +13,7 @@ const config: ApplicationConfig = {
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
- Build logic 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
|
||||
@@ -35,32 +38,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
From here you can:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn twenty help
|
||||
yarn 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).
|
||||
@@ -90,62 +93,87 @@ my-twenty-app/
|
||||
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
|
||||
application.config.ts # Required - main application configuration
|
||||
default-function.role.ts # Default role for serverless functions
|
||||
hello-world.function.ts # Example serverless function
|
||||
hello-world.front-component.tsx # Example front component
|
||||
// your entities (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### 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 |
|
||||
| `*.front-component.tsx` | Front component definitions |
|
||||
| `*.role.ts` | Role definitions |
|
||||
|
||||
### Supported folder organizations
|
||||
|
||||
You can organize your entities in any of these patterns:
|
||||
|
||||
**Traditional (by type):**
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Feature-based:**
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Flat:**
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
At a high level:
|
||||
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus a `twenty` script that delegates to the local `twenty` CLI. Run `yarn twenty help` to list all available commands.
|
||||
- **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 `auth:login` 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/**: 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 logic functions. See "Default function role" below.
|
||||
- `*.object.ts`: Custom object definitions.
|
||||
- `*.function.ts`: Logic function definitions.
|
||||
- `*.front-component.tsx`: Front component definitions.
|
||||
|
||||
Later commands will add more files and folders:
|
||||
|
||||
- `yarn twenty app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
|
||||
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
|
||||
- `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.
|
||||
|
||||
## Authentication
|
||||
|
||||
The first time you run `yarn twenty auth:login`, you'll be prompted for:
|
||||
The first time you run `yarn auth:login`, you'll be prompted for:
|
||||
|
||||
- API URL (defaults to http://localhost:3000 or your current workspace profile)
|
||||
- API key
|
||||
@@ -156,25 +184,25 @@ Your credentials are stored per-user in `~/.twenty/config.json`. You can maintai
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
Once you've switched workspaces with `yarn twenty auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
|
||||
## Use the SDK resources (types & config)
|
||||
|
||||
@@ -182,18 +210,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) |
|
||||
| `defineApplication()` | Configure application metadata |
|
||||
| `defineObject()` | Define custom objects with fields |
|
||||
| `defineLogicFunction()` | Define logic functions with handlers |
|
||||
| `defineFrontComponent()` | Define front components for custom UI |
|
||||
| `defineFunction()` | Define logic 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,16 +300,16 @@ Key points:
|
||||
- The `universalIdentifier` must be unique and stable across deployments.
|
||||
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
|
||||
- The `fields` array is optional — you can define objects without custom fields.
|
||||
- You can scaffold new objects using `yarn twenty entity:add`, which guides you through naming, fields, and relationships.
|
||||
- You can scaffold new objects using `yarn entity:add`, which guides you through naming, fields, and relationships.
|
||||
|
||||
<Note>
|
||||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields 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>
|
||||
|
||||
|
||||
### Application config (application-config.ts)
|
||||
### Application config (application.config.ts)
|
||||
|
||||
Every app has a single `application-config.ts` file that describes:
|
||||
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.
|
||||
@@ -292,9 +318,9 @@ Every app has a single `application-config.ts` file that describes:
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -309,18 +335,18 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_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).
|
||||
- `roleUniversalIdentifier` 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 `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
|
||||
|
||||
- The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role.
|
||||
- The typed client will be restricted to the permissions granted to that role.
|
||||
@@ -331,7 +357,7 @@ 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 =
|
||||
@@ -370,10 +396,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 `roleUniversalIdentifier`. 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.
|
||||
@@ -383,11 +409,11 @@ Notes:
|
||||
|
||||
### Logic 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';
|
||||
|
||||
@@ -407,7 +433,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,
|
||||
@@ -476,7 +502,7 @@ const handler = async (event: RoutePayload) => {
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -506,7 +532,7 @@ The `RoutePayload` type has the following structure:
|
||||
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:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -541,113 +567,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
You can create new functions in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
|
||||
- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern.
|
||||
|
||||
### Marking a logic function as a tool
|
||||
|
||||
Logic functions can be exposed as **tools** for AI agents and workflows. When a function is marked as a tool, it becomes discoverable by Twenty's AI features and can be selected as a step in workflow automations.
|
||||
|
||||
To mark a logic function as a tool, set `isTool: true` and provide a `toolInputSchema` describing the expected input parameters using [JSON Schema](https://json-schema.org/):
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import Twenty from '~/generated';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new Twenty();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- **`isTool`** (`boolean`, default: `false`): When set to `true`, the function is registered as a tool and becomes available to AI agents and workflow automations.
|
||||
- **`toolInputSchema`** (`object`, optional): A JSON Schema object that describes the parameters your function accepts. AI agents use this schema to understand what inputs the tool expects and to validate calls. If omitted, the schema defaults to `{ type: 'object', properties: {} }` (no parameters).
|
||||
- Functions with `isTool: false` (or unset) are **not** exposed as tools. They can still be executed directly or called by other functions, but will not appear in tool discovery.
|
||||
- **Tool naming**: When exposed as a tool, the function name is automatically normalized to `logic_function_<name>` (lowercased, non-alphanumeric characters replaced with underscores). For example, `enrich-company` becomes `logic_function_enrich_company`.
|
||||
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events (cron, database events, routes) at the same time.
|
||||
|
||||
<Note>
|
||||
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
|
||||
</Note>
|
||||
|
||||
### 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 twenty app:dev`.
|
||||
|
||||
You can create new front components in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
|
||||
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
|
||||
- **Scaffolded**: Run `yarn entity:add` 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 twenty app:generate` to create a local typed client in `generated/` based on your workspace schema. Use it in your functions:
|
||||
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';
|
||||
@@ -656,7 +581,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
The client is re-generated by `yarn twenty 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 or when onboarding to a new workspace.
|
||||
|
||||
#### Runtime credentials in logic functions
|
||||
|
||||
@@ -667,39 +592,50 @@ 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 `roleUniversalIdentifier`. 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 `roleUniversalIdentifier` 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)
|
||||
|
||||
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 a single script in your 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
|
||||
```
|
||||
|
||||
Then add a `twenty` script:
|
||||
Then add scripts like these:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc.
|
||||
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
|
||||
- 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 twenty app:generate`.
|
||||
- Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment.
|
||||
- Types or client missing/outdated: run `yarn app:generate`.
|
||||
- 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
|
||||
|
||||
@@ -17,6 +17,10 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
* أنشئ وظائف منطقية مع مشغلات مخصصة
|
||||
* انشر التطبيق نفسه عبر مساحات عمل متعددة
|
||||
|
||||
**قريبًا:**
|
||||
|
||||
* تخطيطات ومكونات واجهة مستخدم مخصصة
|
||||
|
||||
## المتطلبات الأساسية
|
||||
|
||||
* Node.js 24+ وYarn 4
|
||||
@@ -36,32 +40,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# قم بالمصادقة باستخدام مفتاح واجهة برمجة التطبيقات الخاص بك (سيُطلب منك ذلك)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
من هنا يمكنك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# أضف كيانًا جديدًا إلى تطبيقك (موجّه)
|
||||
yarn twenty entity:add
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
|
||||
# ولِّد عميل Twenty مضبوط الأنواع وأنواع كيانات مساحة العمل
|
||||
yarn twenty app:generate
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# راقب سجلات وظائف تطبيقك
|
||||
yarn twenty function:logs
|
||||
# Watch your application's function logs
|
||||
yarn function:logs
|
||||
|
||||
# نفّذ وظيفة بالاسم
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
# Execute a function by name
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# أزل تثبيت التطبيق من مساحة العمل الحالية
|
||||
yarn twenty app:uninstall
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
|
||||
# اعرض مساعدة الأوامر
|
||||
yarn twenty help
|
||||
# Display commands' help
|
||||
yarn help
|
||||
```
|
||||
|
||||
راجع أيضًا: صفحات مرجع CLI لـ [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) و[twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
|
||||
src/
|
||||
├── application-config.ts # مطلوب - التكوين الرئيسي للتطبيق
|
||||
├── roles/
|
||||
│ └── default-role.ts # الدور الافتراضي لوظائف المنطق
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # مثال لوظيفة منطقية
|
||||
└── front-components/
|
||||
└── hello-world.tsx # مثال لمكوّن الواجهة الأمامية
|
||||
application.config.ts # مطلوب - التكوين الرئيسي للتطبيق
|
||||
default-function.role.ts # الدور الافتراضي للوظائف بدون خادم
|
||||
hello-world.function.ts # مثال لوظيفة بدون خادم
|
||||
hello-world.front-component.tsx # مثال لمكوّن الواجهة الأمامية
|
||||
// كياناتك (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### الاتفاقية فوق التهيئة
|
||||
|
||||
تستخدم التطبيقات نهج **الاتفاقية فوق التهيئة** حيث تُكتشف الكيانات عبر لاحقة اسم الملف. يتيح ذلك تنظيمًا مرنًا داخل مجلد `src/app/`:
|
||||
|
||||
| لاحقة الملف | نوع الكيان |
|
||||
| ----------------------- | --------------------------- |
|
||||
| `*.object.ts` | تعريفات كائنات مخصصة |
|
||||
| `*.function.ts` | تعريفات وظائف بلا خادم |
|
||||
| `*.front-component.tsx` | Front component definitions |
|
||||
| `*.role.ts` | تعريفات الأدوار |
|
||||
|
||||
### طرق تنظيم المجلدات المدعومة
|
||||
|
||||
يمكنك تنظيم الكيانات بأي من الأنماط التالية:
|
||||
|
||||
**تقليدي (حسب النوع):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**حسب الميزة:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**مسطح:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
بشكل عام:
|
||||
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` بالإضافة إلى نص برمجي `twenty` يفوِّض إلى `twenty` CLI المحلي. شغِّل `yarn twenty help` لعرض جميع الأوامر المتاحة.
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` فضلًا عن نصوص مثل `app:dev` و`app:generate` و`entity:add` و`function:logs` و`function:execute` و`app:uninstall` و`auth:login` التي تفوِّض إلى `twenty` CLI المحلي.
|
||||
* **.gitignore**: يتجاهل العناصر الشائعة مثل `node_modules` و`.yarn` و`generated/` (عميل مضبوط الأنواع) و`dist/` و`build/` ومجلدات التغطية وملفات السجلات وملفات `.env*`.
|
||||
* **yarn.lock**، **.yarnrc.yml**، **.yarn/**: تقوم بقفل وتكوين حزمة أدوات Yarn 4 المستخدمة في المشروع.
|
||||
* **.nvmrc**: يثبّت إصدار Node.js المتوقع للمشروع.
|
||||
* **eslint.config.mjs** و**tsconfig.json**: يقدّمان إعدادات الفحص والتهيئة لـ TypeScript لمصادر TypeScript في تطبيقك.
|
||||
* **README.md**: ملف README قصير في جذر التطبيق يتضمن تعليمات أساسية.
|
||||
* **public/**: مجلد لتخزين الأصول العامة (صور، خطوط، ملفات ثابتة) التي سيتم تقديمها مع تطبيقك. الملفات الموضوعة هنا تُرفع أثناء المزامنة وتكون متاحة أثناء وقت التشغيل.
|
||||
* **src/**: المكان الرئيسي حيث تعرّف تطبيقك ككود
|
||||
|
||||
### اكتشاف الكيانات
|
||||
|
||||
يكتشف 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/**: المكان الرئيسي حيث تعرّف تطبيقك ككود:
|
||||
* `application.config.ts`: التكوين العام لتطبيقك (بيانات وصفية وربط وقت التشغيل). انظر "تكوين التطبيق" أدناه.
|
||||
* `*.role.ts`: تعريفات الأدوار المستخدمة بواسطة وظائفك المنطقية. انظر "الدور الافتراضي للوظيفة" أدناه.
|
||||
* `*.object.ts`: تعريفات كائنات مخصصة.
|
||||
* `*.function.ts`: تعريفات الوظائف المنطقية.
|
||||
* `*.front-component.tsx`: تعريفات المكونات الواجهية.
|
||||
|
||||
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
|
||||
|
||||
* `yarn twenty app:generate` سيُنشئ مجلدًا `generated/` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
|
||||
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار.
|
||||
* `yarn app:generate` سيُنشئ مجلدًا `generated/` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
|
||||
* `yarn entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار.
|
||||
|
||||
## المصادقة
|
||||
|
||||
في المرة الأولى التي تشغّل فيها `yarn twenty auth:login`، سيُطلب منك إدخال:
|
||||
في المرة الأولى التي تشغّل فيها `yarn auth:login`، سيُطلب منك إدخال:
|
||||
|
||||
* عنوان URL لواجهة برمجة التطبيقات (الافتراضي http://localhost:3000 أو ملف تعريف مساحة العمل الحالية لديك)
|
||||
* مفتاح واجهة برمجة التطبيقات
|
||||
@@ -157,26 +188,26 @@ export default defineObject({
|
||||
### Managing workspaces
|
||||
|
||||
```bash filename="Terminal"
|
||||
# تسجيل الدخول تفاعليًا (مُوصى به)
|
||||
yarn twenty auth:login
|
||||
# Login interactively (recommended)
|
||||
yarn auth:login
|
||||
|
||||
# تسجيل الدخول إلى ملف تعريف لمساحة عمل محددة
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
# Login to a specific workspace profile
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# عرض جميع مساحات العمل المُكوَّنة
|
||||
yarn twenty auth:list
|
||||
# List all configured workspaces
|
||||
yarn auth:list
|
||||
|
||||
# تبديل مساحة العمل الافتراضية (تفاعليًا)
|
||||
yarn twenty auth:switch
|
||||
# Switch the default workspace (interactive)
|
||||
yarn auth:switch
|
||||
|
||||
# التبديل إلى مساحة عمل محددة
|
||||
yarn twenty auth:switch production
|
||||
# Switch to a specific workspace
|
||||
yarn auth:switch production
|
||||
|
||||
# التحقق من حالة المصادقة الحالية
|
||||
yarn twenty auth:status
|
||||
# Check current authentication status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
بمجرد أن تقوم بالتبديل بين مساحات العمل باستخدام `yarn twenty auth:switch`، ستستخدم جميع الأوامر اللاحقة تلك المساحة افتراضيًا. You can still override it temporarily with `--workspace <name>`.
|
||||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
|
||||
## استخدم موارد SDK (الأنواع والتكوين)
|
||||
|
||||
@@ -184,18 +215,16 @@ yarn twenty auth:status
|
||||
|
||||
### دوال مساعدة
|
||||
|
||||
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
|
||||
يوفّر SDK أربع دوال مساعدة مع تحقق مدمج لتعريف كيانات تطبيقك:
|
||||
|
||||
| دالة | الغرض |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
|
||||
| دالة | الغرض |
|
||||
| --------------------- | ---------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التطبيق الوصفية |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
|
||||
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
تتحقق هذه الدوال من تكوينك في وقت التشغيل وتوفر إكمالًا تلقائيًا أفضل في بيئة التطوير وأمان أنواع أعلى.
|
||||
|
||||
### تعريف الكائنات
|
||||
|
||||
@@ -276,15 +305,15 @@ export default defineObject({
|
||||
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
|
||||
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
|
||||
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
|
||||
<Note>
|
||||
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية مثل `name` و`createdAt` و`updatedAt` و`createdBy` و`position` و`deletedAt`. لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
|
||||
</Note>
|
||||
|
||||
### تكوين التطبيق (application-config.ts)
|
||||
### تكوين التطبيق (application.config.ts)
|
||||
|
||||
كل تطبيق لديه ملف واحد `application-config.ts` يصف:
|
||||
كل تطبيق لديه ملف واحد `application.config.ts` يصف:
|
||||
|
||||
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
|
||||
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
|
||||
@@ -293,9 +322,9 @@ export default defineObject({
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ export default defineApplication({
|
||||
|
||||
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
|
||||
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
|
||||
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||||
|
||||
#### الأدوار والصلاحيات
|
||||
|
||||
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. يعين الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق في تطبيقك.
|
||||
يمكن للتطبيقات تعريف أدوار تُغلّف الصلاحيات على كائنات وإجراءات مساحة العمل لديك. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
|
||||
|
||||
* مفتاح واجهة البرمجة في وقت التشغيل المحقون باسم `TWENTY_API_KEY` مستمد من دور الوظيفة الافتراضي هذا.
|
||||
* سيُقيَّد العميل مضبوط الأنواع بالأذونات الممنوحة لذلك الدور.
|
||||
@@ -333,7 +362,7 @@ 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 =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application-config.ts` باسم `defaultRoleUniversalIdentifier`. بعبارة أخرى:
|
||||
يُشار بعد ذلك إلى `universalIdentifier` لهذا الدور في `application.config.ts` باسم `roleUniversalIdentifier`. بعبارة أخرى:
|
||||
|
||||
* **\\*.role.ts** يحدد ما يمكن أن يفعله الدور الافتراضي للوظيفة.
|
||||
* **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
|
||||
* **application.config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك صلاحياته.
|
||||
|
||||
الملاحظات:
|
||||
|
||||
@@ -386,11 +415,11 @@ export default defineRole({
|
||||
|
||||
### تكوين الوظيفة المنطقية ونقطة الدخول
|
||||
|
||||
كل ملف وظيفة يستخدم `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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ export default defineLogicFunction({
|
||||
عندما يستدعي مشغّل المسار وظيفتك المنطقية، يتلقى كائنًا من النوع `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
|
||||
@@ -516,7 +545,7 @@ const handler = async (event: RoutePayload) => {
|
||||
افتراضيًا، **لا** تُمرَّر رؤوس HTTP من الطلبات الواردة إلى وظيفتك المنطقية لأسباب أمنية. للوصول إلى رؤوس محددة، قم بإدراجها صراحةً في مصفوفة `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
يمكنك إنشاء وظائف جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغّل `yarn twenty 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 twenty app:dev`.
|
||||
|
||||
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.front-component.tsx` واستخدم `defineFrontComponent()`.
|
||||
* **مُنشأ بالقالب**: شغّل `yarn entity:add` واختر خيار إضافة وظيفة جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.function.ts` واستخدم `defineFunction()` مع اتباع النمط نفسه.
|
||||
|
||||
### عميل مُولَّد مضبوط الأنواع
|
||||
|
||||
شغّل `yarn twenty app:generate` لإنشاء عميل محلي مضبوط الأنواع في `generated/` استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
|
||||
شغّل yarn app:generate لإنشاء عميل محلي مضبوط الأنواع في generated/ استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
يُعاد توليد العميل بواسطة `yarn twenty app:generate`. أعِد التشغيل بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة.
|
||||
يُعاد توليد العميل بواسطة `yarn app:generate`. أعِد التشغيل بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة.
|
||||
|
||||
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
|
||||
|
||||
@@ -614,38 +606,49 @@ 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` عبر `roleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
|
||||
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `roleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
|
||||
|
||||
### مثال 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):
|
||||
|
||||
## إعداد يدوي (بدون المهيئ)
|
||||
|
||||
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي واربط سكربتًا واحدًا في ملف package.json لديك:
|
||||
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي ووصل السكربتات في ملف package.json لديك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
ثم أضف سكربتًا باسم `twenty`:
|
||||
ثم أضف نصوصًا مثل هذه:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:generate": "twenty app:generate",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"help": "twenty help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty <command>`، مثلًا: `yarn twenty app:dev`، `yarn twenty app:generate`، `yarn twenty help`، إلخ.
|
||||
يمكنك الآن تشغيل الأوامر نفسها عبر Yarn، مثل `yarn app:dev` و`yarn app:generate`، إلخ.
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
* أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
|
||||
* أخطاء المصادقة: شغّل `yarn auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
|
||||
* يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة البرمجة وأن خادم Twenty قابل للوصول.
|
||||
* الأنواع أو العميل مفقود/قديم: شغّل `yarn twenty app:generate`.
|
||||
* وضع التطوير لا يزامن: تأكد من أن `yarn twenty app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
|
||||
* الأنواع أو العميل مفقود/قديم: شغّل `yarn app:generate`.
|
||||
* وضع التطوير لا يزامن: تأكد من أن `yarn app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
|
||||
|
||||
قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -17,6 +17,10 @@ Aplikace vám umožňují vytvářet a spravovat přizpůsobení Twenty **jako k
|
||||
* Vytvářejte logické 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
|
||||
@@ -36,32 +40,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# Přihlaste se pomocí svého API klíče (budete vyzváni)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Spusťte vývojový režim: automaticky synchronizuje místní změny s vaším pracovním prostorem
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Odtud můžete:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Přidejte do vaší aplikace novou entitu (s průvodcem)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Vygenerujte typovaného klienta Twenty a typy entit pracovního prostoru
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Sledujte logy funkcí vaší aplikace
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Spusťte funkci podle názvu
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Odinstalujte aplikaci z aktuálního pracovního prostoru
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Zobrazte nápovědu k příkazům
|
||||
yarn twenty help
|
||||
yarn 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).
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
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
|
||||
application.config.ts # Povinné – hlavní konfigurace aplikace
|
||||
default-function.role.ts # Výchozí role pro bezserverové funkce
|
||||
hello-world.function.ts # Ukázková bezserverová funkce
|
||||
hello-world.front-component.tsx # Ukázková front-endová komponenta
|
||||
// vaše entity (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### 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í |
|
||||
| `*.front-component.tsx` | Definice frontendových komponent |
|
||||
| `*.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/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Podle funkcí:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Plochá:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
V kostce:
|
||||
|
||||
* **package.json**: Deklaruje název aplikace, verzi, engines (Node 24+, Yarn 4) a přidává `twenty-sdk` plus skript `twenty`, který deleguje na lokální `twenty` CLI. Spusťte `yarn twenty help` pro výpis všech dostupných příkazů.
|
||||
* **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 `auth:login`, 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/**: 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 logickými funkcemi. Viz „Výchozí role funkce“ níže.
|
||||
* `*.object.ts`: Definice vlastních objektů.
|
||||
* `*.function.ts`: Definice logických funkcí.
|
||||
* `*.front-component.tsx`: Definice frontových komponent.
|
||||
|
||||
Pozdější příkazy přidají další soubory a složky:
|
||||
|
||||
* `yarn twenty app:generate` vytvoří složku `generated/` (typovaný klient Twenty + typy pracovního prostoru).
|
||||
* `yarn twenty entity:add` přidá soubory s definicemi entit do `src/` pro vaše vlastní objekty, funkce, frontové komponenty nebo role.
|
||||
* `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.
|
||||
|
||||
## Ověření
|
||||
|
||||
Při prvním spuštění `yarn twenty auth:login` budete vyzváni k zadání:
|
||||
Při prvním spuštění `yarn auth:login` budete vyzváni k zadání:
|
||||
|
||||
* URL API (výchozí je http://localhost:3000 nebo váš aktuální profil pracovního prostoru)
|
||||
* Klíč API
|
||||
@@ -158,25 +189,25 @@ Vaše přihlašovací údaje se ukládají pro jednotlivé uživatele do `~/.twe
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
Jakmile přepnete pracovní prostor pomocí `yarn twenty auth:switch`, všechny následující příkazy budou tento pracovní prostor používat jako výchozí. Můžete jej stále dočasně přepsat pomocí `--workspace <name>`.
|
||||
Jakmile přepnete pracovní prostor pomocí `auth:switch`, všechny následující příkazy budou tento pracovní prostor používat jako výchozí. Můžete jej stále dočasně přepsat pomocí `--workspace <name>`.
|
||||
|
||||
## Používejte zdroje SDK (typy a konfiguraci)
|
||||
|
||||
@@ -184,18 +215,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 |
|
||||
| --------------------- | ------------------------------------------------ |
|
||||
| `defineApplication()` | Konfigurace metadat aplikace |
|
||||
| `defineObject()` | Definice vlastních objektů s poli |
|
||||
| `defineFunction()` | Definice logických 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,15 +305,15 @@ Hlavní body:
|
||||
* Hodnota `universalIdentifier` musí být jedinečná a stabilní napříč nasazeními.
|
||||
* Každé pole vyžaduje `name`, `type`, `label` a svůj vlastní stabilní `universalIdentifier`.
|
||||
* Pole `fields` je volitelné — objekty můžete definovat i bez vlastních polí.
|
||||
* Nové objekty můžete vygenerovat pomocí `yarn twenty entity:add`, který vás provede pojmenováním, poli a vztahy.
|
||||
* Nové objekty můžete vygenerovat pomocí `yarn entity:add`, který vás provede pojmenováním, poli a vztahy.
|
||||
|
||||
<Note>
|
||||
**Základní pole jsou vytvořena automaticky.** Když definujete vlastní objekt, Twenty automaticky přidá standardní pole 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)
|
||||
### Konfigurace aplikace (application.config.ts)
|
||||
|
||||
Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
|
||||
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í.
|
||||
@@ -293,9 +322,9 @@ Každá aplikace má jeden soubor `application-config.ts`, který popisuje:
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,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).
|
||||
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||||
|
||||
#### 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. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
|
||||
|
||||
* 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,7 +362,7 @@ 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 =
|
||||
@@ -372,10 +401,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 `roleUniversalIdentifier`. 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:
|
||||
|
||||
@@ -386,11 +415,11 @@ Poznámky:
|
||||
|
||||
### Konfigurace logických 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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ Poznámky:
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -516,7 +545,7 @@ Typ `RoutePayload` má následující strukturu:
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Nové funkce můžete vytvářet dvěma způsoby:
|
||||
|
||||
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou logickou funkci. Tím se vygeneruje startovací soubor s obslužnou funkcí a konfigurací.
|
||||
* **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 twenty app:dev` automaticky sestaví a synchronizují.
|
||||
|
||||
Nové frontendové komponenty můžete vytvořit dvěma způsoby:
|
||||
|
||||
* **Vygenerované**: Spusťte `yarn twenty entity:add` a zvolte možnost přidat novou frontendovou komponentu.
|
||||
* **Ruční**: Vytvořte nový soubor `*.front-component.tsx` a použijte `defineFrontComponent()`.
|
||||
* **Vygenerované**: Spusťte `yarn entity:add` 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 twenty 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:
|
||||
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';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Klient je znovu generován příkazem `yarn twenty 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 znovu po změně vašich objektů nebo při připojování k novému pracovnímu prostoru.
|
||||
|
||||
#### Běhové přihlašovací údaje v logických funkcích
|
||||
|
||||
@@ -614,38 +606,49 @@ 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 `roleUniversalIdentifier`. 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 `roleUniversalIdentifier` 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)
|
||||
|
||||
Ačkoli pro nejlepší začátky doporučujeme použít `create-twenty-app`, projekt můžete nastavit i ručně. Neinstalujte CLI globálně. Místo toho přidejte `twenty-sdk` jako lokální závislost a přidejte jeden skript do souboru package.json:
|
||||
Ačkoli pro nejlepší začátky doporučujeme použít `create-twenty-app`, projekt můžete nastavit i ručně. Neinstalujte CLI globálně. Místo toho přidejte `twenty-sdk` jako lokální závislost a propojte skripty v souboru package.json:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Poté přidejte skript `twenty`:
|
||||
Poté přidejte skripty jako tyto:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nyní můžete spouštět všechny příkazy přes `yarn twenty <command>`, např. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` atd.
|
||||
Nyní můžete spouštět stejné příkazy přes Yarn, např. `yarn app:dev`, `yarn app:generate` atd.
|
||||
|
||||
## Řešení potíží
|
||||
|
||||
* Chyby ověření: spusťte `yarn twenty auth:login` a ujistěte se, že váš klíč API má požadovaná oprávnění.
|
||||
* 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 twenty app:generate`.
|
||||
* Režim vývoje se nesynchronizuje: ujistěte se, že běží `yarn twenty app:dev` a že vaše prostředí změny neignoruje.
|
||||
* Typy nebo klient chybí nebo jsou zastaralé: spusťte `yarn app:generate`.
|
||||
* 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
|
||||
|
||||
@@ -17,6 +17,10 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
|
||||
* Logikfunktionen 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
|
||||
@@ -27,41 +31,41 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
|
||||
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Eine neue App erstellen
|
||||
# Scaffold a new app
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Falls du yarn@4 nicht verwendest
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Mit deinem API-Schlüssel authentifizieren (du wirst dazu aufgefordert)
|
||||
yarn twenty auth:login
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth:login
|
||||
|
||||
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
|
||||
yarn twenty app:dev
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Von hier aus können Sie:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Eine neue Entität zu deiner Anwendung hinzufügen (geführt)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Einen typisierten Twenty-Client und Entitätstypen für den Arbeitsbereich generieren
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Die Funktionsprotokolle deiner Anwendung überwachen
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Eine Funktion anhand ihres Namens ausführen
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Hilfe zu Befehlen anzeigen
|
||||
yarn twenty help
|
||||
yarn 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,65 +93,92 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # Ordner für öffentliche Assets (Bilder, Schriftarten usw.)
|
||||
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
|
||||
application.config.ts # Erforderlich – Hauptkonfiguration der Anwendung
|
||||
default-function.role.ts # Standardrolle für serverlose Funktionen
|
||||
hello-world.function.ts # Beispiel für eine serverlose Funktion
|
||||
hello-world.front-component.tsx # Beispiel für eine Frontend-Komponente
|
||||
// Ihre Entitäten (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### 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 |
|
||||
| `*.front-component.tsx` | Definitionen von Frontend-Komponenten |
|
||||
| `*.role.ts` | Rollendefinitionen |
|
||||
|
||||
### Unterstützte Ordnerorganisationen
|
||||
|
||||
Sie können Ihre Entitäten nach einem der folgenden Muster organisieren:
|
||||
|
||||
**Traditionell (nach Typ):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Feature-basiert:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Flach:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
Auf hoher Ebene:
|
||||
|
||||
* **package.json**: Deklariert den App-Namen, die Version und die Engines (Node 24+, Yarn 4) und fügt `twenty-sdk` sowie ein `twenty`-Skript hinzu, das an die lokale `twenty`-CLI delegiert. Führe `yarn twenty help` aus, um alle verfügbaren Befehle aufzulisten.
|
||||
* **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` und `auth:login` 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/**: 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 Logikfunktionen verwendet werden. Siehe unten „Standard-Funktionsrolle“.
|
||||
* `*.object.ts`: Benutzerdefinierte Objektdefinitionen.
|
||||
* `*.function.ts`: Definitionen von Logikfunktionen.
|
||||
* `*.front-component.tsx`: Front-Komponenten-Definitionen.
|
||||
|
||||
Spätere Befehle fügen weitere Dateien und Ordner hinzu:
|
||||
|
||||
* `yarn twenty app:generate` erstellt einen `generated/`-Ordner (typisierter Twenty-Client + Workspace-Typen).
|
||||
* `yarn twenty entity:add` fügt unter `src/` Entitätsdefinitionsdateien für benutzerdefinierte Objekte, Funktionen, Frontend-Komponenten oder Rollen 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.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
Wenn Sie `yarn twenty auth:login` zum ersten Mal ausführen, werden Sie nach Folgendem gefragt:
|
||||
Wenn Sie `yarn auth:login` zum ersten Mal ausführen, werden Sie nach Folgendem gefragt:
|
||||
|
||||
* API-URL (standardmäßig http://localhost:3000 oder Ihr aktuelles Workspace-Profil)
|
||||
* API-Schlüssel
|
||||
@@ -158,25 +189,25 @@ Ihre Anmeldedaten werden pro Benutzer in `~/.twenty/config.json` gespeichert. Si
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
Sobald Sie mit `yarn twenty auth:switch` den Arbeitsbereich gewechselt haben, verwenden alle nachfolgenden Befehle standardmäßig diesen Arbeitsbereich. Sie können es weiterhin vorübergehend mit `--workspace <name>` überschreiben.
|
||||
Sobald Sie mit `auth:switch` den Arbeitsbereich gewechselt haben, verwenden alle nachfolgenden Befehle standardmäßig diesen Arbeitsbereich. Sie können es weiterhin vorübergehend mit `--workspace <name>` überschreiben.
|
||||
|
||||
## SDK-Ressourcen verwenden (Typen & Konfiguration)
|
||||
|
||||
@@ -184,18 +215,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 |
|
||||
| --------------------- | ---------------------------------------------------- |
|
||||
| `defineApplication()` | Anwendungsmetadaten konfigurieren |
|
||||
| `defineObject()` | Benutzerdefinierte Objekte mit Feldern definieren |
|
||||
| `defineFunction()` | Logikfunktionen 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,15 +305,15 @@ Hauptpunkte:
|
||||
* Der `universalIdentifier` muss eindeutig und über Deployments hinweg stabil sein.
|
||||
* Jedes Feld benötigt `name`, `type`, `label` und einen eigenen stabilen `universalIdentifier`.
|
||||
* Das Array `fields` ist optional — Sie können Objekte ohne benutzerdefinierte Felder definieren.
|
||||
* Sie können mit `yarn twenty entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
|
||||
* Sie können mit `yarn entity:add` neue Objekte erzeugen; der Assistent führt Sie durch Benennung, Felder und Beziehungen.
|
||||
|
||||
<Note>
|
||||
**Basisfelder werden automatisch erstellt.** Wenn Sie ein benutzerdefiniertes Objekt definieren, fügt Twenty automatisch Standardfelder 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)
|
||||
### Anwendungskonfiguration (application.config.ts)
|
||||
|
||||
Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschreibt:
|
||||
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.
|
||||
@@ -293,9 +322,9 @@ Jede App hat eine einzelne Datei `application-config.ts`, die Folgendes beschrei
|
||||
Verwenden Sie `defineApplication()`, um Ihre Anwendungskonfiguration zu definieren:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,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).
|
||||
* `roleUniversalIdentifier` 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 `roleUniversalIdentifier` in `application.config.ts` legt die Standardrolle fest, die von den Logikfunktionen 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,7 +362,7 @@ 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 =
|
||||
@@ -372,10 +401,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 `roleUniversalIdentifier` 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:
|
||||
|
||||
@@ -386,11 +415,11 @@ Notizen:
|
||||
|
||||
### Konfiguration von Logikfunktionen 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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ Notizen:
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -516,7 +545,7 @@ Der Typ `RoutePayload` hat die folgende Struktur:
|
||||
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:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Sie können neue Funktionen auf zwei Arten erstellen:
|
||||
|
||||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Logikfunktion. Dadurch wird eine Starterdatei mit Handler und Konfiguration erzeugt.
|
||||
* **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 twenty app:dev` automatisch gebaut und synchronisiert.
|
||||
|
||||
Sie können neue Frontend-Komponenten auf zwei Arten erstellen:
|
||||
|
||||
* **Generiert**: Führen Sie `yarn twenty entity:add` aus und wählen Sie die Option zum Hinzufügen einer neuen Frontend-Komponente.
|
||||
* **Manuell**: Erstellen Sie eine neue `*.front-component.tsx`-Datei und verwenden Sie `defineFrontComponent()`.
|
||||
* **Generiert**: Führen Sie `yarn entity:add` 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 twenty app:generate` aus, um einen lokalen typisierten Client in `generated/` basierend auf Ihrem Arbeitsbereichs-Schema zu erstellen. Verwenden Sie ihn in Ihren Funktionen:
|
||||
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';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Der Client wird durch `yarn twenty 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 oder beim Onboarding in einen neuen Workspace erneut aus.
|
||||
|
||||
#### Laufzeit-Anmeldedaten in Logikfunktionen
|
||||
|
||||
@@ -614,38 +606,49 @@ 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 `roleUniversalIdentifier` 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 `roleUniversalIdentifier` 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)
|
||||
|
||||
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie ein einzelnes Skript in Ihrer package.json ein:
|
||||
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie Skripte in Ihrer package.json ein:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Fügen Sie dann ein `twenty`-Skript hinzu:
|
||||
Fügen Sie dann Skripte wie diese hinzu:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Jetzt können Sie alle Befehle über `yarn twenty <command>` ausführen, z. B. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` usw.
|
||||
Jetzt können Sie dieselben Befehle über Yarn ausführen, z. B. `yarn app:dev`, `yarn app:generate` usw.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
* Authentifizierungsfehler: Führen Sie `yarn twenty auth:login` aus und stellen Sie sicher, dass Ihr API-Schlüssel die erforderlichen Berechtigungen hat.
|
||||
* 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 twenty app:generate` aus.
|
||||
* Dev-Modus synchronisiert nicht: Stellen Sie sicher, dass `yarn twenty app:dev` läuft und dass Änderungen von Ihrer Umgebung nicht ignoriert werden.
|
||||
* Typen oder Client fehlen/veraltet: Führen Sie `yarn app:generate` 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
|
||||
|
||||
@@ -17,6 +17,10 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
|
||||
* Crea funzioni logiche con trigger personalizzati
|
||||
* Distribuisci la stessa app su più spazi di lavoro
|
||||
|
||||
**In arrivo:**
|
||||
|
||||
* Layout e componenti UI personalizzati
|
||||
|
||||
## Prerequisiti
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
@@ -36,32 +40,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# Autenticati usando la tua API key (ti verrà richiesto)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Da qui puoi:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Aggiungi una nuova entità alla tua applicazione (guidata)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Genera un client Twenty tipizzato e i tipi di entità dell'area di lavoro
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Monitora i log delle funzioni della tua applicazione
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Esegui una funzione per nome
|
||||
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Disinstalla l'applicazione dallo spazio di lavoro corrente
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Mostra l'aiuto dei comandi
|
||||
yarn twenty help
|
||||
yarn help
|
||||
```
|
||||
|
||||
Vedi anche: le pagine di riferimento della CLI per [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) e [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # Cartella delle risorse pubbliche (immagini, font, ecc.)
|
||||
src/
|
||||
├── application-config.ts # Obbligatorio - configurazione principale dell'applicazione
|
||||
├── roles/
|
||||
│ └── default-role.ts # Ruolo predefinito per le funzioni logiche
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Funzione logica di esempio
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Componente front-end di esempio
|
||||
application.config.ts # Obbligatorio - configurazione principale dell'applicazione
|
||||
default-function.role.ts # Ruolo predefinito per le funzioni serverless
|
||||
hello-world.function.ts # Funzione serverless di esempio
|
||||
hello-world.front-component.tsx # Componente front-end di esempio
|
||||
// le tue entità (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### Convenzioni invece della configurazione
|
||||
|
||||
Le applicazioni usano un approccio basato sulle **convenzioni invece della configurazione** in cui le entità vengono rilevate in base al suffisso del file. Questo consente un'organizzazione flessibile all'interno della cartella `src/app/`:
|
||||
|
||||
| Suffisso del file | Tipo di entità |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| `*.object.ts` | Definizioni di oggetti personalizzati |
|
||||
| `*.function.ts` | Definizioni di funzioni serverless |
|
||||
| `*.front-component.tsx` | Definizioni dei componenti front-end |
|
||||
| `*.role.ts` | Definizioni di ruoli |
|
||||
|
||||
### Organizzazioni di cartelle supportate
|
||||
|
||||
Puoi organizzare le tue entità in uno qualsiasi di questi modelli:
|
||||
|
||||
**Tradizionale (per tipo):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Per funzionalità:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Struttura piatta:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
A livello generale:
|
||||
|
||||
* **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk` più uno script `twenty` che delega alla CLI locale `twenty`. Esegui `yarn twenty help` per elencare tutti i comandi disponibili.
|
||||
* **package.json**: Dichiara il nome dell'app, la versione, i motori (Node 24+, Yarn 4) e aggiunge `twenty-sdk`, oltre a script come `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` e `auth:login` che delegano alla CLI locale `twenty`.
|
||||
* **.gitignore**: Ignora i file generati comuni come `node_modules`, `.yarn`, `generated/` (client tipizzato), `dist/`, `build/`, cartelle di coverage, file di log e file `.env*`.
|
||||
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloccano e configurano la toolchain Yarn 4 utilizzata dal progetto.
|
||||
* **.nvmrc**: Fissa la versione di Node.js prevista dal progetto.
|
||||
* **eslint.config.mjs** e **tsconfig.json**: Forniscono linting e configurazione TypeScript per i sorgenti TypeScript della tua app.
|
||||
* **README.md**: Un breve README nella radice dell'app con istruzioni di base.
|
||||
* **public/**: Una cartella per archiviare risorse pubbliche (immagini, font, file statici) che saranno servite con la tua applicazione. I file collocati qui vengono caricati durante la sincronizzazione e sono accessibili in fase di esecuzione.
|
||||
* **src/**: Il luogo principale in cui definisci la tua applicazione come codice
|
||||
|
||||
### Rilevamento delle entità
|
||||
|
||||
L'SDK rileva le entità analizzando i tuoi file TypeScript alla ricerca di chiamate **`export default define<Entity>({...})`**. Ogni tipo di entità ha una corrispondente funzione helper esportata da `twenty-sdk`:
|
||||
|
||||
| Funzione helper | Tipo di entità |
|
||||
| ------------------------ | ----------------------------------------- |
|
||||
| `defineObject()` | Definizioni di oggetti personalizzati |
|
||||
| `defineLogicFunction()` | Definizioni di funzioni logiche |
|
||||
| `defineFrontComponent()` | Definizioni dei componenti front-end |
|
||||
| `defineRole()` | Definizioni di ruoli |
|
||||
| `defineField()` | Estensioni di campo per oggetti esistenti |
|
||||
|
||||
<Note>
|
||||
**La denominazione dei file è flessibile.** Il rilevamento delle entità è basato sull'AST — l'SDK esegue la scansione dei file sorgente alla ricerca del pattern `export default define<Entity>({...})`. Puoi organizzare file e cartelle come preferisci. Raggruppare per tipo di entità (ad es., `logic-functions/`, `roles/`) è solo una convenzione per l'organizzazione del codice, non un requisito.
|
||||
</Note>
|
||||
|
||||
Esempio di entità rilevata:
|
||||
|
||||
```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/**: Il luogo principale in cui definisci la tua applicazione come codice:
|
||||
* `application.config.ts`: Configurazione globale della tua app (metadati e collegamenti di runtime). Vedi "Configurazione dell'applicazione" qui sotto.
|
||||
* `*.role.ts`: Definizioni di ruoli usate dalle tue funzioni logiche. Vedi "Ruolo funzione predefinito" qui sotto.
|
||||
* `*.object.ts`: Definizioni di oggetti personalizzati.
|
||||
* `*.function.ts`: Definizioni di funzioni logiche.
|
||||
* `*.front-component.tsx`: Definizioni di componenti front-end.
|
||||
|
||||
Comandi successivi aggiungeranno altri file e cartelle:
|
||||
|
||||
* `yarn twenty app:generate` creerà una cartella `generated/` (client Twenty tipizzato + tipi dello spazio di lavoro).
|
||||
* `yarn twenty entity:add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti, funzioni, componenti front-end o ruoli personalizzati.
|
||||
* `yarn app:generate` creerà una cartella `generated/` (client Twenty tipizzato + tipi dello spazio di lavoro).
|
||||
* `yarn entity:add` aggiungerà file di definizione delle entità sotto `src/` per i tuoi oggetti, funzioni, componenti front-end o ruoli personalizzati.
|
||||
|
||||
## Autenticazione
|
||||
|
||||
La prima volta che esegui `yarn twenty auth:login`, ti verranno richiesti:
|
||||
La prima volta che esegui `yarn auth:login`, ti verranno richiesti:
|
||||
|
||||
* URL dell'API (predefinito a http://localhost:3000 o al profilo dello spazio di lavoro corrente)
|
||||
* Chiave API
|
||||
@@ -158,25 +189,25 @@ Le tue credenziali sono archiviate per utente in `~/.twenty/config.json`. Puoi m
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
Una volta che hai cambiato area di lavoro con `yarn twenty auth:switch`, tutti i comandi successivi utilizzeranno quell'area di lavoro per impostazione predefinita. Puoi comunque sovrascriverla temporaneamente con `--workspace <name>`.
|
||||
Una volta che hai cambiato area di lavoro con `auth:switch`, tutti i comandi successivi utilizzeranno quell'area di lavoro per impostazione predefinita. Puoi comunque sovrascriverla temporaneamente con `--workspace <name>`.
|
||||
|
||||
## Usa le risorse dell'SDK (tipi e configurazione)
|
||||
|
||||
@@ -184,18 +215,16 @@ Il pacchetto twenty-sdk fornisce blocchi tipizzati e funzioni helper da usare ne
|
||||
|
||||
### Funzioni helper
|
||||
|
||||
L'SDK fornisce funzioni helper per definire le entità della tua app. Come descritto in [Rilevamento delle entità](#entity-detection), devi usare `export default define<Entity>({...})` affinché le tue entità vengano rilevate:
|
||||
L'SDK fornisce quattro funzioni helper con convalida integrata per definire le entità della tua app:
|
||||
|
||||
| Funzione | Scopo |
|
||||
| ------------------------ | ----------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Configura i metadati dell'applicazione (obbligatorio, uno per app) |
|
||||
| `defineObject()` | Definisci oggetti personalizzati con campi |
|
||||
| `defineLogicFunction()` | Definisci funzioni logiche con handler |
|
||||
| `defineFrontComponent()` | Definisci componenti front-end per un'interfaccia utente personalizzata |
|
||||
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
|
||||
| `defineField()` | Estendi gli oggetti esistenti con campi aggiuntivi |
|
||||
| Funzione | Scopo |
|
||||
| --------------------- | ------------------------------------------------------- |
|
||||
| `defineApplication()` | Configura i metadati dell'applicazione |
|
||||
| `defineObject()` | Definisci oggetti personalizzati con campi |
|
||||
| `defineFunction()` | Definisci funzioni logiche con handler |
|
||||
| `defineRole()` | Configura i permessi dei ruoli e l'accesso agli oggetti |
|
||||
|
||||
Queste funzioni convalidano la configurazione in fase di build e offrono il completamento automatico nell'IDE e la sicurezza dei tipi.
|
||||
Queste funzioni convalidano la configurazione a runtime e offrono un migliore completamento automatico nell'IDE e una maggiore sicurezza dei tipi.
|
||||
|
||||
### Definizione degli oggetti
|
||||
|
||||
@@ -276,15 +305,15 @@ Punti chiave:
|
||||
* Il `universalIdentifier` deve essere univoco e stabile tra i deployment.
|
||||
* Ogni campo richiede un `name`, `type`, `label` e il proprio `universalIdentifier` stabile.
|
||||
* L'array `fields` è facoltativo: puoi definire oggetti senza campi personalizzati.
|
||||
* Puoi generare nuovi oggetti con `yarn twenty entity:add`, che ti guida nella denominazione, nei campi e nelle relazioni.
|
||||
* Puoi generare nuovi oggetti con `yarn entity:add`, che ti guida nella denominazione, nei campi e nelle relazioni.
|
||||
|
||||
<Note>
|
||||
**I campi base vengono creati automaticamente.** Quando definisci un oggetto personalizzato, Twenty aggiunge automaticamente i campi standard come `name`, `createdAt`, `updatedAt`, `createdBy`, `position` e `deletedAt`. Non è necessario definirli nel tuo array `fields` — aggiungi solo i tuoi campi personalizzati.
|
||||
</Note>
|
||||
|
||||
### Configurazione dell'applicazione (application-config.ts)
|
||||
### Configurazione dell'applicazione (application.config.ts)
|
||||
|
||||
Ogni app ha un singolo file `application-config.ts` che descrive:
|
||||
Ogni app ha un singolo file `application.config.ts` che descrive:
|
||||
|
||||
* **Identità dell'app**: identificatori, nome visualizzato e descrizione.
|
||||
* **Come vengono eseguite le sue funzioni**: quale ruolo usano per i permessi.
|
||||
@@ -293,9 +322,9 @@ Ogni app ha un singolo file `application-config.ts` che descrive:
|
||||
Usa `defineApplication()` per definire la configurazione della tua applicazione:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ Note:
|
||||
|
||||
* I campi `universalIdentifier` sono ID deterministici sotto il tuo controllo; generali una volta e mantienili stabili tra le sincronizzazioni.
|
||||
* `applicationVariables` diventano variabili d'ambiente per le tue funzioni (ad esempio, `DEFAULT_RECIPIENT_NAME` è disponibile come `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` deve corrispondere al file del ruolo (vedi sotto).
|
||||
* `roleUniversalIdentifier` deve corrispondere al ruolo che definisci nel tuo file `*.role.ts` (vedi sotto).
|
||||
|
||||
#### Ruoli e permessi
|
||||
|
||||
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `defaultRoleUniversalIdentifier` in `application-config.ts` indica il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
|
||||
Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti e sulle azioni del tuo spazio di lavoro. Il campo `roleUniversalIdentifier` in `application.config.ts` designa il ruolo predefinito utilizzato dalle funzioni logiche della tua app.
|
||||
|
||||
* La chiave API di runtime iniettata come `TWENTY_API_KEY` è derivata da questo ruolo funzione predefinito.
|
||||
* Il client tipizzato sarà limitato ai permessi concessi a quel ruolo.
|
||||
@@ -333,7 +362,7 @@ Le applicazioni possono definire ruoli che incapsulano i permessi sugli oggetti
|
||||
Quando generi una nuova app con lo scaffolder, la CLI crea anche un file di ruolo predefinito. Usa `defineRole()` per definire ruoli con convalida integrata:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application-config.ts` come `defaultRoleUniversalIdentifier`. In altre parole:
|
||||
L'`universalIdentifier` di questo ruolo viene quindi referenziato in `application.config.ts` come `roleUniversalIdentifier`. In altre parole:
|
||||
|
||||
* **\*.role.ts** definisce ciò che il ruolo funzione predefinito può fare.
|
||||
* **application-config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
|
||||
* **application.config.ts** punta a quel ruolo in modo che le tue funzioni ne ereditino i permessi.
|
||||
|
||||
Note:
|
||||
|
||||
@@ -386,11 +415,11 @@ Note:
|
||||
|
||||
### Configurazione e punto di ingresso della funzione logica
|
||||
|
||||
Ogni file di funzione usa `defineLogicFunction()` per esportare una configurazione con un handler e trigger opzionali.
|
||||
Ogni file di funzione usa `defineFunction()` per esportare una configurazione con un handler e trigger opzionali. Usa il suffisso di file `*.function.ts` per il rilevamento automatico.
|
||||
|
||||
```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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ Note:
|
||||
Quando un trigger di route invoca la tua funzione logica, questa riceve un oggetto `RoutePayload` che segue il formato AWS HTTP API v2. Importa il tipo da `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
|
||||
@@ -516,7 +545,7 @@ Il tipo `RoutePayload` ha la seguente struttura:
|
||||
Per impostazione predefinita, le intestazioni HTTP delle richieste in ingresso **non** vengono passate alla tua funzione logica per motivi di sicurezza. Per accedere a intestazioni specifiche, elencale esplicitamente nell'array `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Puoi creare nuove funzioni in due modi:
|
||||
|
||||
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere una nuova funzione logica. Questo genera un file iniziale con un handler e una configurazione.
|
||||
* **Manuale**: Crea un nuovo file `*.logic-function.ts` e usa `defineLogicFunction()`, seguendo lo stesso schema.
|
||||
|
||||
### Componenti front-end
|
||||
|
||||
I componenti front-end ti consentono di creare componenti React personalizzati che vengono renderizzati all'interno dell'interfaccia di Twenty. Usa `defineFrontComponent()` per definire componenti con convalida integrata:
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Punti chiave:
|
||||
|
||||
* I componenti front-end sono componenti React che eseguono il rendering in contesti isolati all'interno di Twenty.
|
||||
* Usa il suffisso di file `*.front-component.tsx` per il rilevamento automatico.
|
||||
* Il campo `component` fa riferimento al tuo componente React.
|
||||
* I componenti vengono compilati e sincronizzati automaticamente durante `yarn twenty app:dev`.
|
||||
|
||||
Puoi creare nuovi componenti front-end in due modi:
|
||||
|
||||
* **Generata dallo scaffolder**: Esegui `yarn twenty entity:add` e scegli l'opzione per aggiungere un nuovo componente front-end.
|
||||
* **Manuale**: Crea un nuovo file `*.front-component.tsx` e usa `defineFrontComponent()`.
|
||||
* **Generata dallo scaffolder**: Esegui `yarn entity:add` e scegli l'opzione per aggiungere una nuova funzione. Questo genera un file iniziale con un handler e una configurazione.
|
||||
* **Manuale**: Crea un nuovo file `*.function.ts` e usa `defineFunction()`, seguendo lo stesso schema.
|
||||
|
||||
### Client tipizzato generato
|
||||
|
||||
Esegui `yarn twenty app:generate` per creare un client tipizzato locale in `generated/` basato sullo schema del tuo spazio di lavoro. Usalo nelle tue funzioni:
|
||||
Esegui yarn app:generate per creare un client tipizzato locale in generated/ basato sullo schema del tuo spazio di lavoro. Usalo nelle tue funzioni:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Il client viene rigenerato da `yarn twenty app:generate`. Eseguilo nuovamente dopo aver modificato i tuoi oggetti oppure quando effettui l'onboarding su un nuovo spazio di lavoro.
|
||||
Il client viene rigenerato da `yarn app:generate`. Eseguilo nuovamente dopo aver modificato i tuoi oggetti oppure quando effettui l'onboarding su un nuovo spazio di lavoro.
|
||||
|
||||
#### Credenziali di runtime nelle funzioni logiche
|
||||
|
||||
@@ -614,38 +606,49 @@ Quando la tua funzione viene eseguita su Twenty, la piattaforma inietta le crede
|
||||
Note:
|
||||
|
||||
* Non è necessario passare URL o chiave API al client generato. Legge `TWENTY_API_URL` e `TWENTY_API_KEY` da process.env in fase di esecuzione.
|
||||
* I permessi della chiave API sono determinati dal ruolo referenziato nel tuo `application-config.ts` tramite `defaultRoleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
|
||||
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `defaultRoleUniversalIdentifier` all'identificatore universale di quel ruolo.
|
||||
* I permessi della chiave API sono determinati dal ruolo referenziato in `application.config.ts` tramite `roleUniversalIdentifier`. Questo è il ruolo predefinito utilizzato dalle funzioni logiche della tua applicazione.
|
||||
* Le applicazioni possono definire ruoli per seguire il principio del privilegio minimo. Concedi solo i permessi necessari alle tue funzioni, quindi punta `roleUniversalIdentifier` all'identificatore universale di quel ruolo.
|
||||
|
||||
### Esempio Hello World
|
||||
|
||||
Esplora un esempio minimale end-to-end che dimostra oggetti, funzioni logiche, componenti front-end e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Esplora un esempio minimale end‑to‑end che dimostra oggetti, funzioni e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Configurazione manuale (senza lo scaffolder)
|
||||
|
||||
Sebbene consigliamo di utilizzare `create-twenty-app` per la migliore esperienza iniziale, puoi anche configurare un progetto manualmente. Non installare la CLI globalmente. Invece, aggiungi `twenty-sdk` come dipendenza locale e collega un unico script nel tuo package.json:
|
||||
Sebbene consigliamo di utilizzare `create-twenty-app` per la migliore esperienza iniziale, puoi anche configurare un progetto manualmente. Non installare la CLI globalmente. Invece, aggiungi `twenty-sdk` come dipendenza locale e collega gli script nel tuo package.json:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Quindi aggiungi uno script `twenty`:
|
||||
Quindi aggiungi script come questi:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ora puoi eseguire tutti i comandi tramite `yarn twenty <command>`, ad es. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, ecc.
|
||||
Ora puoi eseguire gli stessi comandi tramite Yarn, ad esempio `yarn app:dev`, `yarn app:generate`, ecc.
|
||||
|
||||
## Risoluzione dei problemi
|
||||
|
||||
* Errori di autenticazione: esegui `yarn twenty auth:login` e assicurati che la tua chiave API abbia i permessi richiesti.
|
||||
* Errori di autenticazione: esegui `yarn auth:login` e assicurati che la tua chiave API abbia i permessi richiesti.
|
||||
* Impossibile connettersi al server: verifica l'URL dell'API e che il server Twenty sia raggiungibile.
|
||||
* Tipi o client mancanti/obsoleti: esegui `yarn twenty app:generate`.
|
||||
* Modalità di sviluppo non sincronizzata: assicurati che `yarn twenty app:dev` sia in esecuzione e che le modifiche non vengano ignorate dal tuo ambiente.
|
||||
* Tipi o client mancanti/obsoleti: esegui `yarn app:generate`.
|
||||
* Modalità di sviluppo non in sincronizzazione: assicurati che `yarn app:dev` sia in esecuzione e che le modifiche non vengano ignorate dal tuo ambiente.
|
||||
|
||||
Canale di supporto su Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -17,6 +17,10 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
|
||||
* Crie funções de lógica com gatilhos personalizados
|
||||
* Implemente o mesmo aplicativo em vários espaços de trabalho
|
||||
|
||||
**Em breve:**
|
||||
|
||||
* Layouts e componentes de UI personalizados
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
* Node.js 24+ e Yarn 4
|
||||
@@ -27,41 +31,41 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
|
||||
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app
|
||||
# Criar a estrutura de um novo app
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# If you don't use yarn@4
|
||||
# Se você não usa yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
# Autentique-se usando sua chave de API (você será solicitado)
|
||||
yarn auth:login
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
A partir daqui você pode:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Adicionar uma nova entidade à sua aplicação (assistido)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Gerar um cliente Twenty tipado e tipos de entidades do espaço de trabalho
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Acompanhar os logs das funções da sua aplicação
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Executar uma função pelo nome
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
|
||||
# Desinstalar a aplicação do espaço de trabalho atual
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Exibir a ajuda dos comandos
|
||||
yarn twenty help
|
||||
yarn help
|
||||
```
|
||||
|
||||
Veja também: as páginas de referência da CLI para [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) e [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # Pasta de recursos públicos (imagens, fontes, etc.)
|
||||
src/
|
||||
├── application-config.ts # Obrigatório - configuração principal da aplicação
|
||||
├── roles/
|
||||
│ └── default-role.ts # Papel padrão para funções de lógica
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Exemplo de função de lógica
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Exemplo de componente de front-end
|
||||
application.config.ts # Obrigatório - configuração principal da aplicação
|
||||
default-function.role.ts # Papel padrão para funções serverless
|
||||
hello-world.function.ts # Exemplo de função serverless
|
||||
hello-world.front-component.tsx # Exemplo de componente de front-end
|
||||
// suas entidades (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### Convenção sobre configuração
|
||||
|
||||
Os aplicativos usam uma abordagem de **convenção sobre configuração** em que as entidades são detectadas pelo sufixo do arquivo. Isso permite organização flexível dentro da pasta `src/app/`:
|
||||
|
||||
| Sufixo de arquivo | Tipo de entidade |
|
||||
| ----------------------- | -------------------------------------- |
|
||||
| `*.object.ts` | Definições de objetos personalizados |
|
||||
| `*.function.ts` | Definições de funções serverless |
|
||||
| `*.front-component.tsx` | Definições de componentes de front-end |
|
||||
| `*.role.ts` | Definições de papéis |
|
||||
|
||||
### Organizações de pastas suportadas
|
||||
|
||||
Você pode organizar suas entidades em qualquer um destes padrões:
|
||||
|
||||
**Tradicional (por tipo):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Baseada em funcionalidades:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Plana:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
Em alto nível:
|
||||
|
||||
* **package.json**: Declara o nome do app, versão, engines (Node 24+, Yarn 4), e adiciona `twenty-sdk` além de um script `twenty` que delega para a CLI `twenty` local. Execute `yarn twenty help` para listar todos os comandos disponíveis.
|
||||
* **package.json**: Declara o nome do app, versão, engines (Node 24+, Yarn 4) e adiciona `twenty-sdk`, além de scripts como `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` e `auth:login` que delegam para a CLI local `twenty`.
|
||||
* **.gitignore**: Ignora artefatos comuns como `node_modules`, `.yarn`, `generated/` (cliente tipado), `dist/`, `build/`, pastas de cobertura, arquivos de log e arquivos `.env*`.
|
||||
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Bloqueiam e configuram a ferramenta Yarn 4 usada pelo projeto.
|
||||
* **.nvmrc**: Fixa a versão do Node.js esperada pelo projeto.
|
||||
* **eslint.config.mjs** e **tsconfig.json**: Fornecem lint e configuração do TypeScript para os fontes TypeScript do seu aplicativo.
|
||||
* **README.md**: Um README curto na raiz do aplicativo com instruções básicas.
|
||||
* **public/**: Uma pasta para armazenar recursos públicos (imagens, fontes, arquivos estáticos) que serão servidos com sua aplicação. Os arquivos colocados aqui são enviados durante a sincronização e ficam acessíveis em tempo de execução.
|
||||
* **src/**: O local principal onde você define seu aplicativo como código
|
||||
|
||||
### Detecção de entidades
|
||||
|
||||
O SDK detecta entidades analisando seus arquivos TypeScript em busca de chamadas **`export default define<Entity>({...})`**. Cada tipo de entidade tem uma função utilitária correspondente exportada de `twenty-sdk`:
|
||||
|
||||
| Função utilitária | Tipo de entidade |
|
||||
| ------------------------ | ------------------------------------------- |
|
||||
| `defineObject()` | Definições de objetos personalizados |
|
||||
| `defineLogicFunction()` | Definições de funções de lógica |
|
||||
| `defineFrontComponent()` | Definições de componentes de front-end |
|
||||
| `defineRole()` | Definições de papéis |
|
||||
| `defineField()` | Extensões de campos para objetos existentes |
|
||||
|
||||
<Note>
|
||||
**A nomeação de arquivos é flexível.** A detecção de entidades é baseada em AST — o SDK varre seus arquivos fonte em busca do padrão `export default define<Entity>({...})`. Você pode organizar seus arquivos e pastas como quiser. Agrupar por tipo de entidade (por exemplo, `logic-functions/`, `roles/`) é apenas uma convenção para organização do código, não um requisito.
|
||||
</Note>
|
||||
|
||||
Exemplo de uma entidade detectada:
|
||||
|
||||
```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/**: O local principal onde você define seu aplicativo como código:
|
||||
* `application.config.ts`: Configuração global do seu aplicativo (metadados e conexões de execução). Veja "Configuração do aplicativo" abaixo.
|
||||
* `*.role.ts`: Definições de papéis usadas pelas suas funções de lógica. Veja "Papel de função padrão" abaixo.
|
||||
* `*.object.ts`: Definições de objetos personalizados.
|
||||
* `*.function.ts`: Definições de funções de lógica.
|
||||
* `*.front-component.tsx`: Definições de componentes de front-end.
|
||||
|
||||
Comandos posteriores adicionarão mais arquivos e pastas:
|
||||
|
||||
* `yarn twenty app:generate` criará uma pasta `generated/` (cliente tipado do Twenty + tipos do espaço de trabalho).
|
||||
* `yarn twenty entity:add` adicionará arquivos de definição de entidade em `src/` para seus objetos, funções, componentes de front-end ou papéis personalizados.
|
||||
* `yarn app:generate` criará uma pasta `generated/` (cliente tipado do Twenty + tipos do workspace).
|
||||
* `yarn entity:add` adicionará arquivos de definição de entidade em `src/` para seus objetos, funções, componentes de front-end ou papéis personalizados.
|
||||
|
||||
## Autenticação
|
||||
|
||||
Na primeira vez que você executar `yarn twenty auth:login`, será solicitado o seguinte:
|
||||
Na primeira vez que você executar `yarn auth:login`, será solicitado o seguinte:
|
||||
|
||||
* URL da API (padrão: http://localhost:3000 ou o perfil do seu espaço de trabalho atual)
|
||||
* Chave de API
|
||||
@@ -158,25 +189,25 @@ Suas credenciais são armazenadas por usuário em `~/.twenty/config.json`. Você
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Fazer login interativamente (recomendado)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Fazer login em um perfil de espaço de trabalho específico
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# Listar todos os espaços de trabalho configurados
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Alterar o espaço de trabalho padrão (interativo)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Alternar para um espaço de trabalho específico
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Verificar o status atual da autenticação
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
Depois que você alternar os espaços de trabalho com `yarn twenty auth:switch`, todos os comandos subsequentes usarão esse espaço de trabalho por padrão. Você ainda pode substituí-lo temporariamente com `--workspace <name>`.
|
||||
Depois que você alternar os espaços de trabalho com `auth:switch`, todos os comandos subsequentes usarão esse espaço de trabalho por padrão. Você ainda pode substituí-lo temporariamente com `--workspace <name>`.
|
||||
|
||||
## Use os recursos do SDK (tipos e configuração)
|
||||
|
||||
@@ -184,18 +215,16 @@ O twenty-sdk fornece blocos de construção tipados e funções utilitárias que
|
||||
|
||||
### Funções utilitárias
|
||||
|
||||
O SDK fornece funções utilitárias para definir as entidades do seu app. Conforme descrito em [Detecção de entidades](#entity-detection), você deve usar `export default define<Entity>({...})` para que suas entidades sejam detectadas:
|
||||
O SDK fornece quatro funções utilitárias com validação integrada para definir as entidades do seu aplicativo:
|
||||
|
||||
| Função | Finalidade |
|
||||
| ------------------------ | ------------------------------------------------------------ |
|
||||
| `defineApplication()` | Configurar metadados do aplicativo (obrigatório, um por app) |
|
||||
| `defineObject()` | Define objetos personalizados com campos |
|
||||
| `defineLogicFunction()` | Defina funções de lógica com handlers |
|
||||
| `defineFrontComponent()` | Definir componentes de front-end para UI personalizada |
|
||||
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
|
||||
| `defineField()` | Estender objetos existentes com campos adicionais |
|
||||
| Função | Finalidade |
|
||||
| --------------------- | ------------------------------------------------- |
|
||||
| `defineApplication()` | Configura os metadados do aplicativo |
|
||||
| `defineObject()` | Define objetos personalizados com campos |
|
||||
| `defineFunction()` | Defina funções de lógica com handlers |
|
||||
| `defineRole()` | Configura permissões de papéis e acesso a objetos |
|
||||
|
||||
Essas funções validam sua configuração em tempo de compilação e oferecem autocompletar na IDE e segurança de tipos.
|
||||
Essas funções validam sua configuração em tempo de execução e oferecem melhor autocompletar na IDE e segurança de tipos.
|
||||
|
||||
### Definindo objetos
|
||||
|
||||
@@ -276,15 +305,15 @@ Pontos-chave:
|
||||
* O `universalIdentifier` deve ser exclusivo e estável entre implantações.
|
||||
* Cada campo requer `name`, `type`, `label` e seu próprio `universalIdentifier` estável.
|
||||
* O array `fields` é opcional — você pode definir objetos sem campos personalizados.
|
||||
* Você pode criar novos objetos usando `yarn twenty entity:add`, que orienta você sobre nomeação, campos e relacionamentos.
|
||||
* Você pode criar novos objetos usando `yarn entity:add`, que orienta você sobre nomeação, campos e relacionamentos.
|
||||
|
||||
<Note>
|
||||
**Os campos base são criados automaticamente.** Quando você define um objeto personalizado, o Twenty adiciona automaticamente campos padrão como `name`, `createdAt`, `updatedAt`, `createdBy`, `position` e `deletedAt`. Você não precisa definir esses no seu array `fields` — adicione apenas seus campos personalizados.
|
||||
</Note>
|
||||
|
||||
### Configuração do aplicativo (application-config.ts)
|
||||
### Configuração do aplicativo (application.config.ts)
|
||||
|
||||
Todo aplicativo tem um único arquivo `application-config.ts` que descreve:
|
||||
Todo aplicativo tem um único arquivo `application.config.ts` que descreve:
|
||||
|
||||
* **O que é o aplicativo**: identificadores, nome de exibição e descrição.
|
||||
* **Como suas funções são executadas**: qual papel usam para permissões.
|
||||
@@ -293,9 +322,9 @@ Todo aplicativo tem um único arquivo `application-config.ts` que descreve:
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ Notas:
|
||||
|
||||
* `universalIdentifier` são IDs determinísticos que você controla; gere-os uma vez e mantenha-os estáveis entre sincronizações.
|
||||
* `applicationVariables` tornam-se variáveis de ambiente para suas funções (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` deve corresponder ao arquivo do papel (veja abaixo).
|
||||
* `roleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||||
|
||||
#### Papéis e permissões
|
||||
|
||||
Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. O campo `defaultRoleUniversalIdentifier` em `application-config.ts` designa o papel padrão usado pelas funções de lógica do seu app.
|
||||
Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos e ações do seu espaço de trabalho. The field `roleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's logic functions.
|
||||
|
||||
* A chave de API em tempo de execução, injetada como `TWENTY_API_KEY`, é derivada desse papel padrão de função.
|
||||
* O cliente tipado ficará restrito às permissões concedidas a esse papel.
|
||||
@@ -333,7 +362,7 @@ Os aplicativos podem definir papéis que encapsulam permissões sobre os objetos
|
||||
Ao criar um novo aplicativo com o scaffold, a CLI também cria um arquivo de papel padrão. Use `defineRole()` para definir papéis com validação integrada:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
O `universalIdentifier` desse papel é então referenciado em `application-config.ts` como `defaultRoleUniversalIdentifier`. Em outras palavras:
|
||||
O `universalIdentifier` desse papel é então referenciado em `application.config.ts` como `roleUniversalIdentifier`. Em outras palavras:
|
||||
|
||||
* **\*.role.ts** define o que o papel de função padrão pode fazer.
|
||||
* **application-config.ts** aponta para esse papel para que suas funções herdem suas permissões.
|
||||
* **application.config.ts** aponta para esse papel para que suas funções herdem suas permissões.
|
||||
|
||||
Notas:
|
||||
|
||||
@@ -386,11 +415,11 @@ Notas:
|
||||
|
||||
### Configuração de função de lógica e ponto de entrada
|
||||
|
||||
Cada arquivo de função usa `defineLogicFunction()` para exportar uma configuração com um handler e gatilhos opcionais.
|
||||
Cada arquivo de função usa `defineFunction()` para exportar uma configuração com um handler e gatilhos opcionais. Use o sufixo de arquivo `*.function.ts` para detecção automática.
|
||||
|
||||
```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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ Notas:
|
||||
Quando um gatilho de rota invoca sua função de lógica, ela recebe um objeto `RoutePayload` que segue o formato do AWS HTTP API v2. Importe o tipo de `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
|
||||
@@ -516,7 +545,7 @@ O tipo `RoutePayload` tem a seguinte estrutura:
|
||||
Por padrão, os cabeçalhos HTTP das requisições recebidas **não** são repassados para sua função de lógica por motivos de segurança. Para acessar cabeçalhos específicos, liste-os explicitamente no array `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Você pode criar novas funções de duas formas:
|
||||
|
||||
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar uma nova função de lógica. Isso gera um arquivo inicial com um handler e configuração.
|
||||
* **Manual**: Crie um novo arquivo `*.logic-function.ts` e use `defineLogicFunction()`, seguindo o mesmo padrão.
|
||||
|
||||
### Componentes de front-end
|
||||
|
||||
Componentes de front-end permitem criar componentes React personalizados que são renderizados na UI do Twenty. Use `defineFrontComponent()` para definir componentes com validação integrada:
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Pontos-chave:
|
||||
|
||||
* Componentes de front-end são componentes React que renderizam em contextos isolados dentro do Twenty.
|
||||
* Use o sufixo de arquivo `*.front-component.tsx` para detecção automática.
|
||||
* O campo `component` faz referência ao seu componente React.
|
||||
* Os componentes são compilados e sincronizados automaticamente durante `yarn twenty app:dev`.
|
||||
|
||||
Você pode criar novos componentes de front-end de duas formas:
|
||||
|
||||
* **Gerado automaticamente**: Execute `yarn twenty entity:add` e escolha a opção para adicionar um novo componente de front-end.
|
||||
* **Manual**: Crie um novo arquivo `*.front-component.tsx` e use `defineFrontComponent()`.
|
||||
* **Gerado automaticamente**: Execute `yarn entity:add` e escolha a opção para adicionar uma nova função. Isso gera um arquivo inicial com um handler e configuração.
|
||||
* **Manual**: Crie um novo arquivo `*.function.ts` e use `defineFunction()`, seguindo o mesmo padrão.
|
||||
|
||||
### Cliente tipado gerado
|
||||
|
||||
Execute `yarn twenty app:generate` para criar um cliente tipado local em `generated/` com base no esquema do seu espaço de trabalho. Use-o em suas funções:
|
||||
Execute yarn app:generate para criar um cliente tipado local em generated/ com base no esquema do seu workspace. Use-o em suas funções:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
O cliente é regenerado pelo `yarn twenty app:generate`. Execute novamente após alterar seus objetos ou ao ingressar em um novo workspace.
|
||||
O cliente é regenerado pelo `yarn app:generate`. Execute novamente após alterar seus objetos ou ao ingressar em um novo workspace.
|
||||
|
||||
#### Credenciais em tempo de execução em funções de lógica
|
||||
|
||||
@@ -614,38 +606,49 @@ Quando sua função é executada no Twenty, a plataforma injeta credenciais como
|
||||
Notas:
|
||||
|
||||
* Você não precisa passar a URL ou a chave de API para o cliente gerado. Ele lê `TWENTY_API_URL` e `TWENTY_API_KEY` de process.env em tempo de execução.
|
||||
* As permissões da chave de API são determinadas pelo papel referenciado no seu `application-config.ts` via `defaultRoleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app.
|
||||
* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `defaultRoleUniversalIdentifier` para o identificador universal desse papel.
|
||||
* As permissões da chave de API são determinadas pelo papel referenciado no seu `application.config.ts` via `roleUniversalIdentifier`. Este é o papel padrão usado pelas funções de lógica do seu app.
|
||||
* Os aplicativos podem definir papéis para seguir o princípio do menor privilégio. Conceda apenas as permissões de que suas funções precisam e, em seguida, aponte `roleUniversalIdentifier` para o identificador universal desse papel.
|
||||
|
||||
### Exemplo Hello World
|
||||
|
||||
Explore um exemplo mínimo de ponta a ponta que demonstra objetos, funções de lógica, componentes de front-end e vários gatilhos [aqui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Explore um exemplo mínimo de ponta a ponta que demonstra objetos, funções e vários gatilhos [aqui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Configuração manual (sem o gerador)
|
||||
|
||||
Embora recomendemos usar `create-twenty-app` para a melhor experiência inicial, você também pode configurar um projeto manualmente. Não instale a CLI globalmente. Em vez disso, adicione `twenty-sdk` como uma dependência local e configure um único script no seu package.json:
|
||||
Embora recomendemos usar `create-twenty-app` para a melhor experiência inicial, você também pode configurar um projeto manualmente. Não instale a CLI globalmente. Em vez disso, adicione `twenty-sdk` como uma dependência local e conecte scripts no seu package.json:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Em seguida, adicione um script `twenty`:
|
||||
Em seguida, adicione scripts como estes:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agora você pode executar todos os comandos via `yarn twenty <command>`, por exemplo, `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc.
|
||||
Agora você pode executar os mesmos comandos via Yarn, por exemplo, `yarn app:dev`, `yarn app:generate`, etc.
|
||||
|
||||
## Resolução de Problemas
|
||||
|
||||
* Erros de autenticação: execute `yarn twenty auth:login` e certifique-se de que sua chave de API tenha as permissões necessárias.
|
||||
* Erros de autenticação: execute `yarn auth:login` e certifique-se de que sua chave de API tenha as permissões necessárias.
|
||||
* Não é possível conectar ao servidor: verifique a URL da API e se o servidor do Twenty está acessível.
|
||||
* Tipos ou cliente ausentes/desatualizados: execute `yarn twenty app:generate`.
|
||||
* Modo de desenvolvimento não sincronizando: certifique-se de que `yarn twenty app:dev` esteja em execução e de que as alterações não estejam sendo ignoradas pelo seu ambiente.
|
||||
* Tipos ou cliente ausentes/desatualizados: execute `yarn app:generate`.
|
||||
* Modo de desenvolvimento não sincronizando: certifique-se de que `yarn app:dev` esteja em execução e de que as alterações não estejam sendo ignoradas pelo seu ambiente.
|
||||
|
||||
Canal de ajuda no Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -17,6 +17,10 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
|
||||
* Creați funcții de logică cu declanșatoare personalizate
|
||||
* Implementați aceeași aplicație în mai multe spații de lucru
|
||||
|
||||
**În curând:**
|
||||
|
||||
* Dispuneri și componente UI personalizate
|
||||
|
||||
## Cerințe
|
||||
|
||||
* Node.js 24+ și Yarn 4
|
||||
@@ -36,32 +40,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# Autentifică-te folosind cheia ta API (ți se va solicita)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Pornește modul de dezvoltare: sincronizează automat modificările locale cu spațiul tău de lucru
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
De aici puteți:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Adaugă o entitate nouă în aplicația ta (ghidat)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Generează un client Twenty tipizat și tipurile de entități ale spațiului de lucru
|
||||
yarn twenty app:generate
|
||||
# Generează un client Twenty tipat și tipurile de entități ale spațiului de lucru
|
||||
yarn app:generate
|
||||
|
||||
# Urmărește jurnalele funcțiilor aplicației tale
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Execută o funcție după nume
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Dezinstalează aplicația din spațiul de lucru curent
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Afișează ajutorul pentru comenzi
|
||||
yarn twenty help
|
||||
yarn help},{
|
||||
```
|
||||
|
||||
Consultați și: paginile de referință CLI pentru [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) și [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -89,65 +93,92 @@ my-twenty-app/
|
||||
eslint.config.mjs
|
||||
tsconfig.json
|
||||
README.md
|
||||
public/ # Public assets folder (images, fonts, etc.)
|
||||
public/ # Director pentru resurse publice (imagini, fonturi 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
|
||||
application.config.ts # Obligatoriu - configurația principală a aplicației
|
||||
default-function.role.ts # Rolul implicit pentru funcțiile serverless
|
||||
hello-world.function.ts # Exemplu de funcție serverless
|
||||
hello-world.front-component.tsx # Exemplu de componentă de interfață
|
||||
// entitățile tale (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### Convenție în locul configurării
|
||||
|
||||
Aplicațiile folosesc o abordare bazată pe convenție în locul configurării, în care entitățile sunt detectate după sufixul fișierului. Aceasta permite o organizare flexibilă în folderul `src/app/`:
|
||||
|
||||
| Sufixul fișierului | Tipul entității |
|
||||
| ----------------------- | ---------------------------------------- |
|
||||
| `*.object.ts` | Definiții de obiecte personalizate |
|
||||
| `*.function.ts` | Definiții de funcții serverless |
|
||||
| `*.front-component.tsx` | Definiții ale componentelor de interfață |
|
||||
| `*.role.ts` | Definiții de rol |
|
||||
|
||||
### Structuri de foldere acceptate
|
||||
|
||||
Vă puteți organiza entitățile în oricare dintre aceste modele:
|
||||
|
||||
**Tradițional (după tip):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Bazat pe funcționalități:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Plat:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
Pe scurt:
|
||||
|
||||
* **package.json**: Declară numele aplicației, versiunea, motoarele (Node 24+, Yarn 4) și adaugă `twenty-sdk` plus un script `twenty` care deleagă către CLI-ul local `twenty`. Rulează `yarn twenty help` pentru a lista toate comenzile disponibile.
|
||||
* **package.json**: Declară numele aplicației, versiunea, motoarele (Node 24+, Yarn 4) și adaugă `twenty-sdk` plus scripturi precum `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` și `auth:login` care deleagă către CLI-ul local `twenty`.
|
||||
* **.gitignore**: Ignoră artefacte comune precum `node_modules`, `.yarn`, `generated/` (client tipizat), `dist/`, `build/`, foldere de coverage, fișiere jurnal și fișiere `.env*`.
|
||||
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Blochează și configurează lanțul de instrumente Yarn 4 folosit de proiect.
|
||||
* **.nvmrc**: Fixează versiunea Node.js așteptată de proiect.
|
||||
* **eslint.config.mjs** și **tsconfig.json**: Oferă linting și configurație TypeScript pentru fișierele TypeScript ale aplicației.
|
||||
* **README.md**: Un README scurt în rădăcina aplicației, cu instrucțiuni de bază.
|
||||
* **public/**: Un folder pentru stocarea resurselor publice (imagini, fonturi, fișiere statice) care vor fi servite împreună cu aplicația ta. Fișierele plasate aici sunt încărcate în timpul sincronizării și sunt accesibile la rulare.
|
||||
* **src/**: Locul principal unde vă definiți aplicația sub formă de cod
|
||||
|
||||
### Detectarea entităților
|
||||
|
||||
SDK-ul detectează entitățile analizând fișierele TypeScript pentru apeluri **`export default define<Entity>({...})`**. Fiecare tip de entitate are o funcție ajutătoare corespunzătoare, exportată din `twenty-sdk`:
|
||||
|
||||
| Funcție ajutătoare | Tipul entității |
|
||||
| ------------------------ | ------------------------------------------- |
|
||||
| `defineObject()` | Definiții de obiecte personalizate |
|
||||
| `defineLogicFunction()` | Definiții de funcții de logică |
|
||||
| `defineFrontComponent()` | Definiții ale componentelor de interfață |
|
||||
| `defineRole()` | Definiții de rol |
|
||||
| `defineField()` | Extensii de câmp pentru obiectele existente |
|
||||
|
||||
<Note>
|
||||
**Denumirea fișierelor este flexibilă.** Detectarea entităților se bazează pe AST — SDK-ul scanează fișierele sursă pentru tiparul `export default define<Entity>({...})`. Puteți organiza fișierele și folderele cum doriți. Gruparea după tipul de entitate (de exemplu, `logic-functions/`, `roles/`) este doar o convenție pentru organizarea codului, nu o cerință.
|
||||
</Note>
|
||||
|
||||
Exemplu de entitate detectată:
|
||||
|
||||
```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/**: Locul principal unde vă definiți aplicația sub formă de cod:
|
||||
* `application.config.ts`: Configurație globală pentru aplicație (metadate și conectare la runtime). Vezi "Configurația aplicației" mai jos.
|
||||
* `*.role.ts`: Definiții de rol folosite de funcțiile dvs. de logică. Vezi "Rol implicit pentru funcții" mai jos.
|
||||
* `*.object.ts`: Definiții de obiecte personalizate.
|
||||
* `*.function.ts`: Definiții de funcții de logică.
|
||||
* `*.front-component.tsx`: Definiții de componente front-end.
|
||||
|
||||
Comenzile ulterioare vor adăuga mai multe fișiere și foldere:
|
||||
|
||||
* `yarn twenty app:generate` va crea un folder `generated/` (client Twenty tipizat + tipuri pentru spațiul de lucru).
|
||||
* `yarn twenty entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end sau rolurile personalizate.
|
||||
* `yarn app:generate` va crea un folder `generated/` (client Twenty tipizat + tipuri pentru spațiul de lucru).
|
||||
* `yarn entity:add` va adăuga fișiere de definire a entităților în `src/` pentru obiectele, funcțiile, componentele front-end sau rolurile personalizate.
|
||||
|
||||
## Autentificare
|
||||
|
||||
Prima dată când rulați `yarn twenty auth:login`, vi se vor solicita:
|
||||
Prima dată când rulați `yarn auth:login`, vi se vor solicita:
|
||||
|
||||
* URL-ul API (implicit http://localhost:3000 sau profilul spațiului de lucru curent)
|
||||
* Cheie API
|
||||
@@ -158,25 +189,25 @@ Acreditările dvs. sunt stocate per utilizator în `~/.twenty/config.json`. Pute
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
După ce ați schimbat spațiul de lucru cu `yarn twenty auth:switch`, toate comenzile ulterioare vor folosi implicit acel spațiu de lucru. Îl puteți totuși suprascrie temporar cu `--workspace <name>`.
|
||||
După ce ați schimbat spațiul de lucru cu `auth:switch`, toate comenzile ulterioare vor folosi implicit acel spațiu de lucru. Îl puteți totuși suprascrie temporar cu `--workspace <name>`.
|
||||
|
||||
## Utilizați resursele SDK (tipuri și configurare)
|
||||
|
||||
@@ -184,18 +215,16 @@ Biblioteca twenty-sdk oferă blocuri de bază tipizate și funcții ajutătoare
|
||||
|
||||
### Funcții ajutătoare
|
||||
|
||||
SDK-ul oferă funcții ajutătoare pentru definirea entităților aplicației. După cum este descris în [Detectarea entităților](#entity-detection), trebuie să folosiți `export default define<Entity>({...})` pentru ca entitățile să fie detectate:
|
||||
SDK-ul oferă patru funcții ajutătoare cu validare încorporată pentru definirea entităților aplicației:
|
||||
|
||||
| Funcție | Scop |
|
||||
| ------------------------ | ---------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Configurați metadatele aplicației (obligatoriu, una per aplicație) |
|
||||
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
|
||||
| `defineLogicFunction()` | Definiți funcții de logică cu handleri |
|
||||
| `defineFrontComponent()` | Definiți componente Front pentru interfața de utilizator personalizată |
|
||||
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
|
||||
| `defineField()` | Extindeți obiectele existente cu câmpuri suplimentare |
|
||||
| Funcție | Scop |
|
||||
| --------------------- | -------------------------------------------------------- |
|
||||
| `defineApplication()` | Configurați metadatele aplicației |
|
||||
| `defineObject()` | Definiți obiecte personalizate cu câmpuri |
|
||||
| `defineFunction()` | Definiți funcții de logică cu handleri |
|
||||
| `defineRole()` | Configurați permisiunile rolurilor și accesul la obiecte |
|
||||
|
||||
Aceste funcții validează configurația în timpul build-ului și oferă completare automată în IDE și siguranța tipurilor.
|
||||
Aceste funcții validează configurația în timpul execuției și oferă o completare automată mai bună în IDE și siguranța tipurilor.
|
||||
|
||||
### Definirea obiectelor
|
||||
|
||||
@@ -276,15 +305,15 @@ Puncte cheie:
|
||||
* `universalIdentifier` trebuie să fie unic și stabil între implementări.
|
||||
* Fiecare câmp necesită un `name`, un `type`, un `label` și propriul `universalIdentifier` stabil.
|
||||
* Matricea `fields` este opțională — puteți defini obiecte fără câmpuri personalizate.
|
||||
* Puteți genera obiecte noi folosind `yarn twenty entity:add`, care vă ghidează prin denumire, câmpuri și relații.
|
||||
* Puteți genera obiecte noi folosind `yarn entity:add`, care vă ghidează prin denumire, câmpuri și relații.
|
||||
|
||||
<Note>
|
||||
**Câmpurile de bază sunt create automat.** Când definiți un obiect personalizat, Twenty adaugă automat câmpuri standard precum `name`, `createdAt`, `updatedAt`, `createdBy`, `position` și `deletedAt`. Nu trebuie să le definiți în tabloul `fields` — adăugați doar câmpurile personalizate proprii.
|
||||
</Note>
|
||||
|
||||
### Configurația aplicației (application-config.ts)
|
||||
### Configurația aplicației (application.config.ts)
|
||||
|
||||
Fiecare aplicație are un singur fișier `application-config.ts` care descrie:
|
||||
Fiecare aplicație are un singur fișier `application.config.ts` care descrie:
|
||||
|
||||
* **Cine este aplicația**: identificatori, nume de afișare și descriere.
|
||||
* **Cum rulează funcțiile**: ce rol folosesc pentru permisiuni.
|
||||
@@ -293,9 +322,9 @@ Fiecare aplicație are un singur fișier `application-config.ts` care descrie:
|
||||
Folosiți `defineApplication()` pentru a defini configurația aplicației:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ Notițe:
|
||||
|
||||
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți; generați-le o singură dată și păstrați-le stabile între sincronizări.
|
||||
* `applicationVariables` devin variabile de mediu pentru funcțiile dvs. (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` trebuie să corespundă fișierului de rol (vedeți mai jos).
|
||||
* `roleUniversalIdentifier` trebuie să corespundă rolului pe care îl definiți în fișierul `*.role.ts` (vedeți mai jos).
|
||||
|
||||
#### Roluri și permisiuni
|
||||
|
||||
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `defaultRoleUniversalIdentifier` din `application-config.ts` desemnează rolul implicit utilizat de funcțiile de logică ale aplicației.
|
||||
Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor și acțiunilor din spațiul dvs. de lucru. Câmpul `roleUniversalIdentifier` din `application.config.ts` desemnează rolul implicit folosit de funcțiile de logică ale aplicației.
|
||||
|
||||
* Cheia API de runtime injectată ca `TWENTY_API_KEY` este derivată din acest rol implicit pentru funcții.
|
||||
* Clientul tipizat va fi restricționat la permisiunile acordate acelui rol.
|
||||
@@ -333,7 +362,7 @@ Aplicațiile pot defini roluri care încapsulează permisiuni asupra obiectelor
|
||||
Când generați o aplicație nouă, CLI creează și un fișier de rol implicit. Folosiți `defineRole()` pentru a defini roluri cu validare încorporată:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
`universalIdentifier` al acestui rol este apoi referențiat în `application-config.ts` ca `defaultRoleUniversalIdentifier`. Cu alte cuvinte:
|
||||
`universalIdentifier` al acestui rol este apoi referențiat în `application.config.ts` ca `roleUniversalIdentifier`. Cu alte cuvinte:
|
||||
|
||||
* **\*.role.ts** definește ce poate face rolul implicit pentru funcții.
|
||||
* **application-config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
|
||||
* **application.config.ts** indică acel rol, astfel încât funcțiile moștenesc permisiunile lui.
|
||||
|
||||
Notițe:
|
||||
|
||||
@@ -386,11 +415,11 @@ Notițe:
|
||||
|
||||
### Configurația funcției de logică și punctul de intrare
|
||||
|
||||
Fiecare fișier de funcție folosește `defineLogicFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale.
|
||||
Fiecare fișier de funcție folosește `defineFunction()` pentru a exporta o configurație cu un handler și declanșatoare opționale. Folosiți sufixul de fișier `*.function.ts` pentru detectare automată.
|
||||
|
||||
```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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ Notițe:
|
||||
Când un declanșator de rută apelează funcția dvs. de logică, aceasta primește un obiect `RoutePayload` care urmează formatul AWS HTTP API v2. Importă tipul din `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
|
||||
@@ -516,7 +545,7 @@ Tipul `RoutePayload` are următoarea structură:
|
||||
În mod implicit, anteturile HTTP din cererile de intrare **nu** sunt transmise funcției dvs. de logică din motive de securitate. Pentru a accesa anumite anteturi, listează-le explicit în array-ul `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Puteți crea funcții noi în două moduri:
|
||||
|
||||
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o funcție logică nouă. Aceasta generează un fișier inițial cu un handler și o configurație.
|
||||
* **Manual**: Creați un fișier nou `*.logic-function.ts` și folosiți `defineLogicFunction()`, urmând același model.
|
||||
|
||||
### Componente Front
|
||||
|
||||
Componentele Front vă permit să construiți componente React personalizate care sunt randate în interfața Twenty. Utilizați `defineFrontComponent()` pentru a defini componente cu validare încorporată:
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Puncte cheie:
|
||||
|
||||
* Componentele Front sunt componente React care sunt randate în contexte izolate în cadrul Twenty.
|
||||
* Folosiți sufixul de fișier `*.front-component.tsx` pentru detectare automată.
|
||||
* Câmpul `component` face referire la componenta React.
|
||||
* Componentele sunt construite și sincronizate automat în timpul `yarn twenty app:dev`.
|
||||
|
||||
Puteți crea componente Front noi în două moduri:
|
||||
|
||||
* **Generat**: Rulați `yarn twenty entity:add` și alegeți opțiunea de a adăuga o componentă frontend nouă.
|
||||
* **Manual**: Creați un fișier nou `*.front-component.tsx` și folosiți `defineFrontComponent()`.
|
||||
* **Generat**: Rulați `yarn entity:add` și alegeți opțiunea de a adăuga o funcție nouă. Aceasta generează un fișier inițial cu un handler și o configurație.
|
||||
* **Manual**: Creați un fișier nou `*.function.ts` și folosiți `defineFunction()`, urmând același model.
|
||||
|
||||
### Client tipizat generat
|
||||
|
||||
Rulați `yarn twenty app:generate` pentru a crea un client tipizat local în `generated/`, pe baza schemei spațiului de lucru. Folosiți-l în funcțiile dvs.:
|
||||
Rulați yarn app:generate pentru a crea un client tipizat local în generated/, pe baza schemei spațiului de lucru. Folosiți-l în funcțiile dvs.:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Clientul este regenerat de `yarn twenty app:generate`. Rulați din nou după ce vă modificați obiectele sau când vă integrați într-un spațiu de lucru nou.
|
||||
Clientul este regenerat de `yarn app:generate`. Rulați din nou după ce vă modificați obiectele sau când vă integrați într-un spațiu de lucru nou.
|
||||
|
||||
#### Acreditări la runtime în funcțiile de logică
|
||||
|
||||
@@ -614,38 +606,49 @@ Când funcția rulează pe Twenty, platforma injectează acreditări ca variabil
|
||||
Notițe:
|
||||
|
||||
* Nu trebuie să transmiteți URL-ul sau cheia API către clientul generat. Acesta citește `TWENTY_API_URL` și `TWENTY_API_KEY` din process.env la runtime.
|
||||
* Permisiunile cheii API sunt determinate de rolul referențiat în `application-config.ts` prin `defaultRoleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
|
||||
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `defaultRoleUniversalIdentifier` la identificatorul universal al acelui rol.
|
||||
* Permisiunile cheii API sunt determinate de rolul referențiat în `application.config.ts` prin `roleUniversalIdentifier`. Acesta este rolul implicit folosit de funcțiile de logică ale aplicației.
|
||||
* Aplicațiile pot defini roluri pentru a urma principiul celui mai mic privilegiu. Acordați doar permisiunile de care au nevoie funcțiile, apoi setați `roleUniversalIdentifier` la identificatorul universal al acelui rol.
|
||||
|
||||
### Exemplu Hello World
|
||||
|
||||
Explorați un exemplu minim, cap la cap, care demonstrează obiecte, funcții de logică, componente Front și declanșatoare multiple [aici](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
Explorați un exemplu minim, cap‑la‑cap, care demonstrează obiecte, funcții și declanșatoare multiple [aici](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Configurare manuală (fără generator)
|
||||
|
||||
Deși recomandăm utilizarea `create-twenty-app` pentru cea mai bună experiență de început, puteți configura și un proiect manual. Nu instalați CLI-ul global. În schimb, adăugați `twenty-sdk` ca dependență locală și conectați un singur script în package.json-ul dvs.:
|
||||
Deși recomandăm utilizarea `create-twenty-app` pentru cea mai bună experiență de început, puteți configura și un proiect manual. Nu instalați CLI-ul global. În schimb, adăugați `twenty-sdk` ca dependență locală și conectați scripturile în package.json-ul dvs.:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Apoi adăugați un script `twenty`:
|
||||
Apoi adăugați scripturi ca acestea:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Acum puteți rula toate comenzile prin `yarn twenty <command>`, de ex. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help`, etc.
|
||||
Acum puteți rula aceleași comenzi prin Yarn, de ex. `yarn app:dev`, `yarn app:generate`, etc.
|
||||
|
||||
## Depanare
|
||||
|
||||
* Erori de autentificare: rulați `yarn twenty auth:login` și asigurați-vă că cheia API are permisiunile necesare.
|
||||
* Erori de autentificare: rulați `yarn auth:login` și asigurați-vă că cheia API are permisiunile necesare.
|
||||
* Nu se poate conecta la server: verificați URL-ul API și că serverul Twenty este accesibil.
|
||||
* Tipuri sau client lipsă/învechite: rulați `yarn twenty app:generate`.
|
||||
* Modul dev nu sincronizează: asigurați-vă că `yarn twenty app:dev` rulează și că modificările nu sunt ignorate de mediul dvs.
|
||||
* Tipuri sau client lipsă/învechite: rulați `yarn app:generate`.
|
||||
* Modul dev nu sincronizează: asigurați-vă că `yarn app:dev` rulează și că modificările nu sunt ignorate de mediul dvs.
|
||||
|
||||
Canal de ajutor pe Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -17,6 +17,10 @@ description: Создавайте и управляйте настройками
|
||||
* Создавайте логические функции с пользовательскими триггерами
|
||||
* Развёртывайте одно и то же приложение в нескольких рабочих пространствах
|
||||
|
||||
**Скоро:**
|
||||
|
||||
* Пользовательские макеты и компоненты интерфейса
|
||||
|
||||
## Требования
|
||||
|
||||
* Node.js 24+ и Yarn 4
|
||||
@@ -27,41 +31,41 @@ description: Создавайте и управляйте настройками
|
||||
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Создать каркас нового приложения
|
||||
# Scaffold a new app
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# Если вы не используете yarn@4
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Аутентифицироваться с помощью вашего API-ключа (вам будет предложено)
|
||||
yarn twenty auth:login
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth:login
|
||||
|
||||
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
|
||||
yarn twenty app:dev
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Отсюда вы можете:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Добавить новую сущность в ваше приложение (с мастером)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Сгенерировать типизированный клиент Twenty и типы сущностей рабочего пространства
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Просматривать логи функций вашего приложения
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Выполнить функцию по имени
|
||||
yarn twenty function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Удалить приложение из текущего рабочего пространства
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Показать справку по командам
|
||||
yarn twenty help
|
||||
yarn help
|
||||
```
|
||||
|
||||
Смотрите также: страницы справки CLI для [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) и [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # Папка общедоступных ресурсов (изображения, шрифты и т. п.)
|
||||
src/
|
||||
├── application-config.ts # Обязательный — основная конфигурация приложения
|
||||
├── roles/
|
||||
│ └── default-role.ts # Роль по умолчанию для логических функций
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Пример логической функции
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Пример фронтенд-компонента
|
||||
application.config.ts # Обязательный — основная конфигурация приложения
|
||||
default-function.role.ts # Роль по умолчанию для бессерверных функций
|
||||
hello-world.function.ts # Пример бессерверной функции
|
||||
hello-world.front-component.tsx # Пример фронтенд-компонента
|
||||
// ваши сущности (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### Соглашения важнее конфигурации
|
||||
|
||||
Приложения используют подход **соглашения важнее конфигурации**, при котором сущности определяются по суффиксу файла. Это позволяет гибко организовать структуру в папке `src/app/`:
|
||||
|
||||
| Суффикс файла | Тип сущности |
|
||||
| ----------------------- | ------------------------------------- |
|
||||
| `*.object.ts` | Определения пользовательских объектов |
|
||||
| `*.function.ts` | Определения бессерверных функций |
|
||||
| `*.front-component.tsx` | Определения компонентов фронтенда |
|
||||
| `*.role.ts` | Определения ролей |
|
||||
|
||||
### Поддерживаемые способы организации папок
|
||||
|
||||
Вы можете организовать свои сущности по любому из этих шаблонов:
|
||||
|
||||
**Традиционный (по типам):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**По функциональности:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Плоский:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
В общих чертах:
|
||||
|
||||
* **package.json**: Объявляет имя приложения, версию, движки (Node 24+, Yarn 4) и добавляет `twenty-sdk`, а также скрипт `twenty`, который делегирует выполнение локальному CLI `twenty`. Выполните `yarn twenty help`, чтобы вывести список всех доступных команд.
|
||||
* **package.json**: Объявляет имя приложения, версию, движки (Node 24+, Yarn 4) и добавляет `twenty-sdk`, а также скрипты вроде `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` и `auth:login`, которые делегируют выполнение локальному CLI `twenty`.
|
||||
* **.gitignore**: Игнорирует распространённые артефакты, такие как `node_modules`, `.yarn`, `generated/` (типизированный клиент), `dist/`, `build/`, каталоги coverage, файлы журналов и файлы `.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()` | Определения компонентов фронтенда |
|
||||
| `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/**: Основное место, где вы определяете приложение как код:
|
||||
* `application.config.ts`: Глобальная конфигурация вашего приложения (метаданные и параметры выполнения). См. раздел «Конфигурация приложения» ниже.
|
||||
* `*.role.ts`: Определения ролей, используемых вашими логическими функциями. См. раздел «Роль функции по умолчанию» ниже.
|
||||
* `*.object.ts`: Определения пользовательских объектов.
|
||||
* `*.function.ts`: Определения логических функций.
|
||||
* `*.front-component.tsx`: Определения фронтенд-компонентов.
|
||||
|
||||
Позднее команды добавят больше файлов и папок:
|
||||
|
||||
* `yarn twenty app:generate` создаст папку `generated/` (типизированный клиент Twenty + типы рабочего пространства).
|
||||
* `yarn twenty entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов или ролей.
|
||||
* `yarn app:generate` создаст папку `generated/` (типизированный клиент Twenty + типы рабочего пространства).
|
||||
* `yarn entity:add` добавит файлы определений сущностей в `src/` для ваших пользовательских объектов, функций, фронтенд-компонентов или ролей.
|
||||
|
||||
## Аутентификация
|
||||
|
||||
При первом запуске `yarn twenty auth:login` вам будет предложено указать:
|
||||
При первом запуске `yarn auth:login` вам будет предложено указать:
|
||||
|
||||
* URL API (по умолчанию http://localhost:3000 или текущий профиль рабочего пространства)
|
||||
* Ключ API
|
||||
@@ -158,25 +189,25 @@ export default defineObject({
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Войти в интерактивном режиме (рекомендуется)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Войти в профиль конкретного рабочего пространства
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# Показать список всех настроенных рабочих пространств
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Переключить рабочее пространство по умолчанию (в интерактивном режиме)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Переключиться на определённое рабочее пространство
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Проверить текущий статус аутентификации
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
После переключения рабочего пространства с помощью `yarn twenty auth:switch` все последующие команды по умолчанию будут использовать это рабочее пространство. Вы по-прежнему можете временно переопределить это с помощью `--workspace <name>`.
|
||||
После переключения рабочего пространства с помощью `auth:switch` все последующие команды по умолчанию будут использовать это рабочее пространство. Вы по-прежнему можете временно переопределить это с помощью `--workspace <name>`.
|
||||
|
||||
## Используйте ресурсы SDK (типы и конфигурация)
|
||||
|
||||
@@ -184,18 +215,16 @@ yarn twenty auth:status
|
||||
|
||||
### Вспомогательные функции
|
||||
|
||||
SDK предоставляет вспомогательные функции для определения сущностей вашего приложения. Как описано в [Обнаружение сущностей](#entity-detection), вы должны использовать `export default define<Entity>({...})`, чтобы ваши сущности были обнаружены:
|
||||
SDK предоставляет четыре вспомогательных функции с встроенной валидацией для определения сущностей вашего приложения:
|
||||
|
||||
| Функция | Назначение |
|
||||
| ------------------------ | ---------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Настройка метаданных приложения (обязательно, по одному на приложение) |
|
||||
| `defineObject()` | Определяет пользовательские объекты с полями |
|
||||
| `defineLogicFunction()` | Определение логических функций с обработчиками |
|
||||
| `defineFrontComponent()` | Определение фронт-компонентов для настраиваемого интерфейса |
|
||||
| `defineRole()` | Настраивает права роли и доступ к объектам |
|
||||
| `defineField()` | Расширение существующих объектов дополнительными полями |
|
||||
| Функция | Назначение |
|
||||
| --------------------- | ---------------------------------------------- |
|
||||
| `defineApplication()` | Настраивает метаданные приложения |
|
||||
| `defineObject()` | Определяет пользовательские объекты с полями |
|
||||
| `defineFunction()` | Определение логических функций с обработчиками |
|
||||
| `defineRole()` | Настраивает права роли и доступ к объектам |
|
||||
|
||||
Эти функции проверяют вашу конфигурацию на этапе сборки и обеспечивают автодополнение в IDE и безопасность типов.
|
||||
Эти функции проверяют вашу конфигурацию во время выполнения и обеспечивают лучшую автодополняемость в IDE и безопасность типов.
|
||||
|
||||
### Определение объектов
|
||||
|
||||
@@ -276,15 +305,15 @@ export default defineObject({
|
||||
* `universalIdentifier` должен быть уникальным и стабильным между развёртываниями.
|
||||
* Каждому полю требуются `name`, `type`, `label` и собственный стабильный `universalIdentifier`.
|
||||
* Массив `fields` необязателен — вы можете определять объекты без пользовательских полей.
|
||||
* Вы можете сгенерировать новые объекты с помощью `yarn twenty entity:add`, который проведёт вас через настройку имени, полей и связей.
|
||||
* Вы можете сгенерировать новые объекты с помощью `yarn entity:add`, который проведёт вас через выбор именования, полей и связей.
|
||||
|
||||
<Note>
|
||||
**Базовые поля создаются автоматически.** Когда вы определяете пользовательский объект, Twenty автоматически добавляет стандартные поля, такие как `name`, `createdAt`, `updatedAt`, `createdBy`, `position` и `deletedAt`. Вам не нужно определять их в массиве `fields` — добавляйте только свои пользовательские поля.
|
||||
</Note>
|
||||
|
||||
### Конфигурация приложения (application-config.ts)
|
||||
### Конфигурация приложения (application.config.ts)
|
||||
|
||||
У каждого приложения есть единственный файл `application-config.ts`, который описывает:
|
||||
У каждого приложения есть единственный файл `application.config.ts`, который описывает:
|
||||
|
||||
* **Что это за приложение**: идентификаторы, отображаемое имя и описание.
|
||||
* **Как запускаются его функции**: какую роль они используют для прав доступа.
|
||||
@@ -293,9 +322,9 @@ export default defineObject({
|
||||
Используйте `defineApplication()` для определения конфигурации вашего приложения:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ export default defineApplication({
|
||||
|
||||
* `universalIdentifier` — это детерминированные идентификаторы, которыми вы управляете; сгенерируйте их один раз и сохраняйте стабильными между синхронизациями.
|
||||
* `applicationVariables` становятся переменными окружения для ваших функций (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` должен соответствовать файлу роли (см. ниже).
|
||||
* `roleUniversalIdentifier` должен соответствовать роли, которую вы определяете в файле `*.role.ts` (см. ниже).
|
||||
|
||||
#### Роли и разрешения
|
||||
|
||||
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `defaultRoleUniversalIdentifier` в `application-config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
|
||||
Приложения могут определять роли, инкапсулирующие права на объекты и действия в вашем рабочем пространстве. Поле `roleUniversalIdentifier` в `application.config.ts` обозначает роль по умолчанию, используемую логическими функциями вашего приложения.
|
||||
|
||||
* Ключ API во время выполнения, подставляемый как `TWENTY_API_KEY`, получается из этой роли функции по умолчанию.
|
||||
* Типизированный клиент будет ограничен правами, предоставленными этой ролью.
|
||||
@@ -333,7 +362,7 @@ 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 =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
Значение `universalIdentifier` этой роли затем указывается в `application-config.ts` как `defaultRoleUniversalIdentifier`. Иными словами:
|
||||
`universalIdentifier` этой роли затем указывается в `application.config.ts` как `roleUniversalIdentifier`. Иными словами:
|
||||
|
||||
* **\*.role.ts** определяет, что может делать роль функции по умолчанию.
|
||||
* **application-config.ts** указывает на эту роль, чтобы ваши функции наследовали её права.
|
||||
* **application.config.ts** указывает на эту роль, чтобы ваши функции наследовали её права.
|
||||
|
||||
Заметки:
|
||||
|
||||
@@ -386,11 +415,11 @@ export default defineRole({
|
||||
|
||||
### Конфигурация логической функции и точка входа
|
||||
|
||||
Каждый файл функции использует `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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ export default defineLogicFunction({
|
||||
Когда триггер маршрута вызывает вашу логическую функцию, она получает объект `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
|
||||
@@ -516,7 +545,7 @@ const handler = async (event: RoutePayload) => {
|
||||
По умолчанию HTTP-заголовки из входящих запросов **не** передаются в вашу логическую функцию по соображениям безопасности. Чтобы получить доступ к определённым заголовкам, явно перечислите их в массиве `forwardedRequestHeaders`:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Вы можете создать новые функции двумя способами:
|
||||
|
||||
* **Сгенерировано**: Запустите `yarn twenty 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 twenty app:dev`.
|
||||
|
||||
Вы можете создать новые фронт-компоненты двумя способами:
|
||||
|
||||
* **Сгенерировано**: Запустите `yarn twenty entity:add` и выберите опцию добавления нового фронтенд-компонента.
|
||||
* **Вручную**: Создайте новый файл `*.front-component.tsx` и используйте `defineFrontComponent()`.
|
||||
* **Сгенерировано**: Запустите `yarn entity:add` и выберите опцию добавления новой функции. Это создаёт стартовый файл с обработчиком и конфигурацией.
|
||||
* **Вручную**: Создайте новый файл `*.function.ts` и используйте `defineFunction()`, следуя тому же шаблону.
|
||||
|
||||
### Сгенерированный типизированный клиент
|
||||
|
||||
Запустите `yarn twenty app:generate`, чтобы создать локальный типизированный клиент в `generated/` на основе схемы вашего рабочего пространства. Используйте его в своих функциях:
|
||||
Запустите yarn app:generate, чтобы создать локальный типизированный клиент в generated/ на основе схемы вашего рабочего пространства. Используйте его в своих функциях:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
Клиент повторно генерируется командой `yarn twenty app:generate`. Запускайте повторно после изменения ваших объектов или при подключении к новому рабочему пространству.
|
||||
Клиент повторно генерируется командой `yarn app:generate`. Запускайте повторно после изменения ваших объектов или при подключении к новому рабочему пространству.
|
||||
|
||||
#### Учётные данные времени выполнения в логических функциях
|
||||
|
||||
@@ -614,38 +606,49 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
Заметки:
|
||||
|
||||
* Вам не нужно передавать URL или ключ API сгенерированному клиенту. Он читает `TWENTY_API_URL` и `TWENTY_API_KEY` из process.env во время выполнения.
|
||||
* Права ключа API определяются ролью, на которую ссылается ваш `application-config.ts` через `defaultRoleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
|
||||
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Предоставляйте только те права, которые нужны вашим функциям, затем укажите в `defaultRoleUniversalIdentifier` универсальный идентификатор этой роли.
|
||||
* Права ключа API определяются ролью, на которую ссылается ваш `application.config.ts` через `roleUniversalIdentifier`. Это роль по умолчанию, используемая логическими функциями вашего приложения.
|
||||
* Приложения могут определять роли, чтобы следовать принципу наименьших привилегий. Выдавайте только те права, которые нужны вашим функциям, затем укажите в `roleUniversalIdentifier` универсальный идентификатор этой роли.
|
||||
|
||||
### Пример 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):
|
||||
|
||||
## Ручная настройка (без генератора)
|
||||
|
||||
Хотя мы рекомендуем использовать `create-twenty-app` для наилучшего старта, вы также можете настроить проект вручную. Не устанавливайте CLI глобально. Вместо этого добавьте `twenty-sdk` как локальную зависимость и настройте один скрипт в вашем package.json:
|
||||
Хотя мы рекомендуем использовать `create-twenty-app` для наилучшего старта, вы также можете настроить проект вручную. Не устанавливайте CLI глобально. Вместо этого добавьте `twenty-sdk` как локальную зависимость и настройте скрипты в вашем package.json:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Затем добавьте скрипт `twenty`:
|
||||
Затем добавьте скрипты, подобные этим:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:generate": "twenty app:generate",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"help": "twenty help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Теперь вы можете запускать все команды через `yarn twenty <command>`, например, `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` и т. д.
|
||||
Теперь вы можете запускать те же команды через Yarn, например, `yarn app:dev`, `yarn app:generate` и т. д.
|
||||
|
||||
## Устранение неполадок
|
||||
|
||||
* Ошибки аутентификации: выполните `yarn twenty auth:login` и убедитесь, что у вашего ключа API есть необходимые права.
|
||||
* Ошибки аутентификации: выполните `yarn auth:login` и убедитесь, что у вашего ключа API есть необходимые права.
|
||||
* Не удаётся подключиться к серверу: проверьте URL API и доступность сервера Twenty.
|
||||
* Типы или клиент отсутствуют/устарели: выполните `yarn twenty app:generate`.
|
||||
* Режим разработки не синхронизируется: убедитесь, что запущен `yarn twenty app:dev`, и что ваша среда не игнорирует изменения.
|
||||
* Типы или клиент отсутствуют/устарели: выполните `yarn app:generate`.
|
||||
* Режим разработки не синхронизируется: убедитесь, что запущен `yarn app:dev`, и что ваша среда не игнорирует изменения.
|
||||
|
||||
Канал помощи в Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -17,6 +17,10 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
|
||||
* Özel tetikleyicilerle mantık fonksiyonları oluşturun
|
||||
* Aynı uygulamayı birden çok çalışma alanına dağıtın
|
||||
|
||||
**Yakında:**
|
||||
|
||||
* Özel UI düzenleri ve bileşenleri
|
||||
|
||||
## Ön Gereksinimler
|
||||
|
||||
* Node.js 24+ ve Yarn 4
|
||||
@@ -36,32 +40,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
Buradan şunları yapabilirsiniz:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
yarn entity:add
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn twenty app:generate
|
||||
yarn app:generate
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty function:logs
|
||||
yarn function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
yarn app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn twenty help
|
||||
yarn help
|
||||
```
|
||||
|
||||
Ayrıca bkz.: [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) ve [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk) için CLI başvuru sayfaları.
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # Genel varlıklar klasörü (görseller, yazı tipleri vb.)
|
||||
src/
|
||||
├── application-config.ts # Gerekli - ana uygulama yapılandırması
|
||||
├── roles/
|
||||
│ └── default-role.ts # Mantık işlevleri için varsayılan rol
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Örnek mantık işlevi
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Örnek ön uç bileşeni
|
||||
application.config.ts # Gerekli - ana uygulama yapılandırması
|
||||
default-function.role.ts # Sunucusuz işlevler için varsayılan rol
|
||||
hello-world.function.ts # Örnek sunucusuz işlev
|
||||
hello-world.front-component.tsx # Örnek ön uç bileşeni
|
||||
// varlıklarınız (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### Sözleşme-öncelikli yapılandırma
|
||||
|
||||
Uygulamalar, varlıkların dosya sonekiyle algılandığı **sözleşme-öncelikli yapılandırma** yaklaşımını kullanır. Bu, `src/app/` klasörü içinde esnek bir düzenlemeye olanak tanır:
|
||||
|
||||
| Dosya soneki | Varlık türü |
|
||||
| ----------------------- | ----------------------------- |
|
||||
| `*.object.ts` | Özel nesne tanımları |
|
||||
| `*.function.ts` | Sunucusuz fonksiyon tanımları |
|
||||
| `*.front-component.tsx` | Front component definitions |
|
||||
| `*.role.ts` | Rol tanımları |
|
||||
|
||||
### Desteklenen klasör düzenleri
|
||||
|
||||
Varlıklarınızı şu desenlerden herhangi birine göre düzenleyebilirsiniz:
|
||||
|
||||
**Geleneksel (türe göre):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**Özelliğe dayalı:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**Düz:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
Genel hatlarıyla:
|
||||
|
||||
* **package.json**: Uygulama adını, sürümünü, motorları (Node 24+, Yarn 4) bildirir ve `twenty-sdk` ile yerel `twenty` CLI'sine yetki devreden bir `twenty` betiği ekler. Tüm mevcut komutları listelemek için `yarn twenty help` komutunu çalıştırın.
|
||||
* **package.json**: Uygulama adını, sürümünü, motorları (Node 24+, Yarn 4) bildirir ve yerel `twenty` CLI’sine yetki devreden `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall` ve `auth:login` gibi betiklerin yanı sıra `twenty-sdk` ekler.
|
||||
* **.gitignore**: `node_modules`, `.yarn`, `generated/` (türlendirilmiş istemci), `dist/`, `build/`, kapsam klasörleri, günlük dosyaları ve `.env*` dosyaları gibi yaygın artifaktları yok sayar.
|
||||
* **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Proje tarafından kullanılan Yarn 4 araç zincirini kilitler ve yapılandırır.
|
||||
* **.nvmrc**: Projenin beklediği Node.js sürümünü sabitler.
|
||||
* **eslint.config.mjs** ve **tsconfig.json**: Uygulamanızın TypeScript kaynakları için linting ve TypeScript yapılandırması sağlar.
|
||||
* **README.md**: Uygulama kökünde temel talimatların yer aldığı kısa bir README.
|
||||
* **public/**: Uygulamanızla birlikte sunulacak genel varlıkları (görseller, yazı tipleri, statik dosyalar) depolamak için bir klasör. Buraya yerleştirilen dosyalar senkronizasyon sırasında yüklenir ve çalışma zamanında erişilebilir olur.
|
||||
* **src/**: Uygulamanızı kod olarak tanımladığınız ana yer
|
||||
|
||||
### Varlık algılama
|
||||
|
||||
SDK, TypeScript dosyalarınızı **`export default define<Entity>({...})`** çağrılarını arayarak ayrıştırıp varlıkları algılar. Her varlık türünün, `twenty-sdk` tarafından dışa aktarılan karşılık gelen bir yardımcı fonksiyonu vardır:
|
||||
|
||||
| Yardımcı fonksiyon | Varlık türü |
|
||||
| ------------------------ | ---------------------------------------- |
|
||||
| `defineObject()` | Özel nesne tanımları |
|
||||
| `defineLogicFunction()` | Mantık fonksiyon tanımları |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | Rol tanımları |
|
||||
| `defineField()` | Mevcut nesneler için alan genişletmeleri |
|
||||
|
||||
<Note>
|
||||
**Dosya adlandırma esnektir.** Varlık algılama AST tabanlıdır — SDK, kaynak dosyalarınızı `export default define<Entity>({...})` desenini bulmak için tarar. Dosyalarınızı ve klasörlerinizi dilediğiniz gibi düzenleyebilirsiniz. Varlık türüne göre gruplama (örn. `logic-functions/`, `roles/`) bir gereklilik değil, yalnızca kod organizasyonu için bir gelenektir.
|
||||
</Note>
|
||||
|
||||
Algılanan bir varlığa örnek:
|
||||
|
||||
```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/**: Uygulamanızı kod olarak tanımladığınız ana yer:
|
||||
* `application.config.ts`: Uygulamanız için genel yapılandırma (meta veriler ve çalışma zamanı bağlantıları). Aşağıda "Uygulama yapılandırması"na bakın.
|
||||
* `*.role.ts`: Mantık fonksiyonlarınız tarafından kullanılan rol tanımları. Aşağıda "Varsayılan fonksiyon rolü"ne bakın.
|
||||
* `*.object.ts`: Özel nesne tanımları.
|
||||
* `*.function.ts`: Mantık fonksiyon tanımları.
|
||||
* `*.front-component.tsx`: Ön bileşen tanımları.
|
||||
|
||||
İlerideki komutlar daha fazla dosya ve klasör ekleyecektir:
|
||||
|
||||
* `yarn twenty app:generate`, `generated/` klasörünü oluşturur (türlendirilmiş Twenty istemcisi + çalışma alanı türleri).
|
||||
* `yarn twenty entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz veya rolleriniz için `src/` altında varlık tanım dosyaları ekler.
|
||||
* `yarn app:generate`, `generated/` klasörünü oluşturur (türlendirilmiş Twenty istemcisi + çalışma alanı türleri).
|
||||
* `yarn entity:add`, özel nesneleriniz, fonksiyonlarınız, ön bileşenleriniz veya rolleriniz için `src/` altında varlık tanım dosyaları ekler.
|
||||
|
||||
## Kimlik Doğrulama
|
||||
|
||||
`yarn twenty auth:login` komutunu ilk kez çalıştırdığınızda, sizden şunlar istenir:
|
||||
`yarn auth:login` komutunu ilk kez çalıştırdığınızda, sizden şunlar istenir:
|
||||
|
||||
* API URL’si (varsayılan: http://localhost:3000 veya mevcut çalışma alanı profiliniz)
|
||||
* API anahtarı
|
||||
@@ -157,26 +188,26 @@ Kimlik bilgileriniz kullanıcı başına `~/.twenty/config.json` içinde saklan
|
||||
### Managing workspaces
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Etkileşimli giriş yapın (önerilir)
|
||||
yarn twenty auth:login
|
||||
# Login interactively (recommended)
|
||||
yarn auth:login
|
||||
|
||||
# Belirli bir çalışma alanı profiline giriş yapın
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
# Login to a specific workspace profile
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# Yapılandırılmış tüm çalışma alanlarını listeleyin
|
||||
yarn twenty auth:list
|
||||
# List all configured workspaces
|
||||
yarn auth:list
|
||||
|
||||
# Varsayılan çalışma alanını değiştirin (etkileşimli)
|
||||
yarn twenty auth:switch
|
||||
# Switch the default workspace (interactive)
|
||||
yarn auth:switch
|
||||
|
||||
# Belirli bir çalışma alanına geçin
|
||||
yarn twenty auth:switch production
|
||||
# Switch to a specific workspace
|
||||
yarn auth:switch production
|
||||
|
||||
# Mevcut kimlik doğrulama durumunu kontrol edin
|
||||
yarn twenty auth:status
|
||||
# Check current authentication status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
`yarn twenty auth:switch` ile çalışma alanlarını değiştirdikten sonra, sonraki tüm komutlar varsayılan olarak o çalışma alanını kullanacaktır. You can still override it temporarily with `--workspace <name>`.
|
||||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
|
||||
## SDK kaynaklarını kullanın (türler ve yapılandırma)
|
||||
|
||||
@@ -184,18 +215,16 @@ twenty-sdk, uygulamanız içinde kullandığınız türlendirilmiş yapı taşla
|
||||
|
||||
### Yardımcı fonksiyonlar
|
||||
|
||||
SDK, uygulama varlıklarınızı tanımlamak için yardımcı fonksiyonlar sağlar. [Varlık algılama](#entity-detection) bölümünde açıklandığı gibi, varlıklarınızın algılanması için `export default define<Entity>({...})` kullanmalısınız:
|
||||
SDK, uygulama varlıklarınızı tanımlamak için yerleşik doğrulamaya sahip dört yardımcı fonksiyon sunar:
|
||||
|
||||
| Fonksiyon | Amaç |
|
||||
| ------------------------ | ------------------------------------------------------------------------- |
|
||||
| `defineApplication()` | Uygulama meta verilerini yapılandırın (zorunlu, uygulama başına bir adet) |
|
||||
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
|
||||
| `defineLogicFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
|
||||
| `defineFrontComponent()` | Özel kullanıcı arayüzü için ön uç bileşenlerini tanımlayın |
|
||||
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
|
||||
| `defineField()` | Mevcut nesneleri ek alanlarla genişletin |
|
||||
| Fonksiyon | Amaç |
|
||||
| --------------------- | ---------------------------------------------- |
|
||||
| `defineApplication()` | Uygulama meta verilerini yapılandırın |
|
||||
| `defineObject()` | Alanlara sahip özel nesneler tanımlayın |
|
||||
| `defineFunction()` | İşleyicilerle mantık fonksiyonları tanımlayın |
|
||||
| `defineRole()` | Rol izinlerini ve nesne erişimini yapılandırın |
|
||||
|
||||
Bu fonksiyonlar, derleme zamanında yapılandırmanızı doğrular ve IDE otomatik tamamlama ile tür güvenliği sağlar.
|
||||
Bu fonksiyonlar, yapılandırmanızı çalışma anında doğrular ve daha iyi IDE otomatik tamamlama ve tür güvenliği sağlar.
|
||||
|
||||
### Nesnelerin tanımlanması
|
||||
|
||||
@@ -276,15 +305,15 @@ export default defineObject({
|
||||
* `universalIdentifier` dağıtımlar arasında benzersiz ve kararlı olmalıdır.
|
||||
* Her alan bir `name`, `type`, `label` ve kendi kararlı `universalIdentifier` değerini gerektirir.
|
||||
* `fields` dizisi isteğe bağlıdır — özel alanlar olmadan da nesneler tanımlayabilirsiniz.
|
||||
* `yarn twenty entity:add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
|
||||
* `yarn entity:add` kullanarak, adlandırma, alanlar ve ilişkiler konusunda sizi yönlendirerek yeni nesneler oluşturabilirsiniz.
|
||||
|
||||
<Note>
|
||||
**Temel alanlar otomatik olarak oluşturulur.** Özel bir nesne tanımladığınızda Twenty, `name`, `createdAt`, `updatedAt`, `createdBy`, `position` ve `deletedAt` gibi standart alanları otomatik olarak ekler. Bunları `fields` dizinizde tanımlamanız gerekmez — yalnızca özel alanlarınızı ekleyin.
|
||||
</Note>
|
||||
|
||||
### Uygulama yapılandırması (application-config.ts)
|
||||
### Uygulama yapılandırması (application.config.ts)
|
||||
|
||||
Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` dosyası vardır:
|
||||
Her uygulamanın aşağıdakileri açıklayan tek bir `application.config.ts` dosyası vardır:
|
||||
|
||||
* **Uygulamanın kim olduğu**: tanımlayıcılar, görünen ad ve açıklama.
|
||||
* **Fonksiyonlarının nasıl çalıştığı**: izinler için hangi rolü kullandıkları.
|
||||
@@ -293,9 +322,9 @@ Her uygulamanın aşağıdakileri açıklayan tek bir `application-config.ts` do
|
||||
Uygulama yapılandırmanızı tanımlamak için `defineApplication()` kullanın:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ Notlar:
|
||||
|
||||
* `universalIdentifier` alanları size ait belirleyici kimliklerdir; bunları bir kez oluşturun ve eşitlemeler boyunca kararlı tutun.
|
||||
* `applicationVariables`, fonksiyonlarınız için ortam değişkenlerine dönüşür (örneğin, `DEFAULT_RECIPIENT_NAME` değeri `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
|
||||
* `defaultRoleUniversalIdentifier`, rol dosyasıyla eşleşmelidir (aşağıya bakın).
|
||||
* `roleUniversalIdentifier`, `*.role.ts` dosyanızda tanımladığınız rolle eşleşmelidir (aşağıya bakın).
|
||||
|
||||
#### Roller ve izinler
|
||||
|
||||
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application-config.ts` içindeki `defaultRoleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonlarının kullandığı varsayılan rolü belirtir.
|
||||
Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri kapsülleyen roller tanımlayabilir. `application.config.ts` içindeki `roleUniversalIdentifier` alanı, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan rolü belirtir.
|
||||
|
||||
* `TWENTY_API_KEY` olarak enjekte edilen çalışma zamanı API anahtarı bu varsayılan fonksiyon rolünden türetilir.
|
||||
* Türlendirilmiş istemci, o role tanınan izinlerle sınırlandırılır.
|
||||
@@ -333,7 +362,7 @@ Uygulamalar, çalışma alanınızdaki nesneler ve eylemler üzerindeki izinleri
|
||||
Yeni bir uygulama oluşturduğunuzda CLI ayrıca varsayılan bir rol dosyası da oluşturur. Yerleşik doğrulamayla roller tanımlamak için `defineRole()` kullanın:
|
||||
|
||||
```typescript
|
||||
// src/roles/default-role.ts
|
||||
// src/app/default-function.role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
Bu rolün `universalIdentifier` değeri daha sonra `application-config.ts` içinde `defaultRoleUniversalIdentifier` olarak referans verilir. Başka bir deyişle:
|
||||
Bu rolün `universalIdentifier` değeri, `application.config.ts` içinde `roleUniversalIdentifier` olarak referans verilir. Başka bir deyişle:
|
||||
|
||||
* **\*.role.ts**, varsayılan fonksiyon rolünün neler yapabileceğini tanımlar.
|
||||
* **application-config.ts**, fonksiyonlarınızın izinlerini devralması için bu role işaret eder.
|
||||
* **application.config.ts** bu role işaret eder, böylece fonksiyonlarınız onun izinlerini devralır.
|
||||
|
||||
Notlar:
|
||||
|
||||
@@ -386,11 +415,11 @@ Notlar:
|
||||
|
||||
### Mantık fonksiyon yapılandırması ve giriş noktası
|
||||
|
||||
Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren bir yapılandırmayı dışa aktarmak için `defineLogicFunction()` kullanır.
|
||||
Her fonksiyon dosyası, bir işleyici ve isteğe bağlı tetikleyiciler içeren bir yapılandırmayı dışa aktarmak için `defineFunction()` kullanır. Otomatik algılama için `*.function.ts` dosya soneğini kullanın.
|
||||
|
||||
```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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ Notlar:
|
||||
Bir rota tetikleyicisi mantık fonksiyonunuzu çağırdığında, AWS HTTP API v2 formatını izleyen bir `RoutePayload` nesnesi alır. Türü `twenty-sdk` içinden içe aktarın:
|
||||
|
||||
```typescript
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
// Access request data
|
||||
@@ -516,7 +545,7 @@ const handler = async (event: RoutePayload) => {
|
||||
Varsayılan olarak, güvenlik nedenleriyle gelen isteklerden HTTP başlıkları mantık fonksiyonunuza **aktarılmaz**. Belirli başlıklara erişmek için bunları açıkça `forwardedRequestHeaders` dizisinde listeleyin:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
Yeni fonksiyonları iki şekilde oluşturabilirsiniz:
|
||||
|
||||
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir mantık fonksiyonu ekleme seçeneğini seçin. Bu, bir işleyici ve yapılandırma içeren bir başlangıç dosyası oluşturur.
|
||||
* **Manuel**: Yeni bir `*.logic-function.ts` dosyası oluşturun ve aynı deseni izleyerek `defineLogicFunction()` kullanın.
|
||||
|
||||
### Ön uç bileşenleri
|
||||
|
||||
Ön uç bileşenleri, Twenty'nin kullanıcı arayüzünde görüntülenen özel React bileşenleri oluşturmanıza olanak tanır. Yerleşik doğrulamayla bileşenleri tanımlamak için `defineFrontComponent()` kullanın:
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Önemli noktalar:
|
||||
|
||||
* Ön uç bileşenleri, Twenty içinde yalıtılmış bağlamlarda görüntülenen React bileşenleridir.
|
||||
* Otomatik algılama için `*.front-component.tsx` dosya soneğini kullanın.
|
||||
* `component` alanı, React bileşeninize referans verir.
|
||||
* Bileşenler, `yarn twenty app:dev` sırasında otomatik olarak oluşturulur ve senkronize edilir.
|
||||
|
||||
Yeni ön uç bileşenlerini iki şekilde oluşturabilirsiniz:
|
||||
|
||||
* **Şablondan**: `yarn twenty entity:add` çalıştırın ve yeni bir ön uç bileşeni ekleme seçeneğini seçin.
|
||||
* **Manuel**: Yeni bir `*.front-component.tsx` dosyası oluşturun ve `defineFrontComponent()` kullanın.
|
||||
* **Şablondan**: `yarn entity:add` çalıştırın ve yeni bir fonksiyon ekleme seçeneğini seçin. Bu, bir işleyici ve yapılandırma içeren bir başlangıç dosyası oluşturur.
|
||||
* **Manuel**: Yeni bir `*.function.ts` dosyası oluşturun ve aynı deseni izleyerek `defineFunction()` kullanın.
|
||||
|
||||
### Oluşturulmuş türlendirilmiş istemci
|
||||
|
||||
Çalışma alanı şemanıza göre `generated/` içinde yerel bir türlendirilmiş istemci oluşturmak için `yarn twenty app:generate` çalıştırın. Fonksiyonlarınızda kullanın:
|
||||
Çalışma alanı şemanıza göre generated/ içinde yerel bir türlendirilmiş istemci oluşturmak için yarn app:generate çalıştırın. Fonksiyonlarınızda kullanın:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
İstemci `yarn twenty app:generate` tarafından yeniden oluşturulur. Nesnelerinizi değiştirdikten sonra veya yeni bir çalışma alanına katılırken yeniden çalıştırın.
|
||||
İstemci `yarn app:generate` tarafından yeniden oluşturulur. Nesnelerinizi değiştirdikten sonra veya yeni bir çalışma alanına katılırken yeniden çalıştırın.
|
||||
|
||||
#### Mantık fonksiyonlarında çalışma zamanı kimlik bilgileri
|
||||
|
||||
@@ -614,38 +606,49 @@ Fonksiyonunuz Twenty üzerinde çalıştığında, platform kodunuz yürütülme
|
||||
Notlar:
|
||||
|
||||
* Oluşturulan istemciye URL veya API anahtarı geçirmeniz gerekmez. Çalışma zamanında `TWENTY_API_URL` ve `TWENTY_API_KEY` değerlerini process.env üzerinden okur.
|
||||
* API anahtarının izinleri, `application-config.ts` içinde `defaultRoleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
|
||||
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `defaultRoleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
|
||||
* API anahtarının izinleri, `application.config.ts` içinde `roleUniversalIdentifier` aracılığıyla referans verilen role göre belirlenir. Bu, uygulamanızın mantık fonksiyonları tarafından kullanılan varsayılan roldür.
|
||||
* Uygulamalar, en az ayrıcalık ilkesini izlemek için roller tanımlayabilir. Yalnızca fonksiyonlarınızın ihtiyaç duyduğu izinleri verin ve ardından `roleUniversalIdentifier` değerini o rolün evrensel tanımlayıcısına yönlendirin.
|
||||
|
||||
### Hello World örneği
|
||||
|
||||
Nesneleri, mantık fonksiyonlarını, ön uç bileşenlerini ve birden çok tetikleyiciyi gösteren minimal, uçtan uca bir örneği [buradan](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world) inceleyin:
|
||||
Nesneleri, fonksiyonları ve birden çok tetikleyiciyi gösteren minimal, uçtan uca bir örneği [buradan](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world) inceleyin:
|
||||
|
||||
## Manuel kurulum (scaffolder olmadan)
|
||||
|
||||
En iyi başlangıç deneyimi için `create-twenty-app` kullanmanızı önersek de, bir projeyi manuel olarak da kurabilirsiniz. CLI'yi global olarak kurmayın. Bunun yerine `twenty-sdk`'yi yerel bir bağımlılık olarak ekleyin ve package.json içinde tek bir betik tanımlayın:
|
||||
En iyi başlangıç deneyimi için `create-twenty-app` kullanmanızı önersek de, bir projeyi manuel olarak da kurabilirsiniz. CLI'yi global olarak kurmayın. Bunun yerine `twenty-sdk`'yi yerel bir bağımlılık olarak ekleyin ve package.json içinde betikleri bağlayın:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Ardından bir `twenty` betiği ekleyin:
|
||||
Ardından şu gibi betikler ekleyin:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Artık tüm komutları `yarn twenty <command>` üzerinden çalıştırabilirsiniz; örn. `yarn twenty app:dev`, `yarn twenty app:generate`, `yarn twenty help` vb.
|
||||
Artık aynı komutları Yarn üzerinden çalıştırabilirsiniz; örn. `yarn app:dev`, `yarn app:generate` vb.
|
||||
|
||||
## Sorun Giderme
|
||||
|
||||
* Kimlik doğrulama hataları: `yarn twenty auth:login` çalıştırın ve API anahtarınızın gerekli izinlere sahip olduğundan emin olun.
|
||||
* Kimlik doğrulama hataları: `yarn auth:login` çalıştırın ve API anahtarınızın gerekli izinlere sahip olduğundan emin olun.
|
||||
* Sunucuya bağlanılamıyor: API URL’sini ve Twenty sunucusunun erişilebilir olduğunu doğrulayın.
|
||||
* Türler veya istemci eksik/eski: `yarn twenty app:generate` çalıştırın.
|
||||
* Geliştirme modu eşitlenmiyor: `yarn twenty app:dev`'in çalıştığından ve değişikliklerin ortamınız tarafından yok sayılmadığından emin olun.
|
||||
* Türler veya istemci eksik/eski: `yarn app:generate` çalıştırın.
|
||||
* Geliştirme modu eşitlenmiyor: `yarn app:dev`'in çalıştığından ve değişikliklerin ortamınız tarafından yok sayılmadığından emin olun.
|
||||
|
||||
Discord Yardım Kanalı: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -17,6 +17,10 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
|
||||
* 构建带有自定义触发器的逻辑函数
|
||||
* 将同一个应用部署到多个工作空间
|
||||
|
||||
**即将推出:**
|
||||
|
||||
* 自定义 UI 布局和组件
|
||||
|
||||
## 先决条件
|
||||
|
||||
* Node.js 24+ 和 Yarn 4
|
||||
@@ -36,32 +40,32 @@ corepack enable
|
||||
yarn install
|
||||
|
||||
# 使用你的 API 密钥进行身份验证(系统会提示你)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# 启动开发模式:会将本地更改自动同步到你的工作区
|
||||
yarn twenty app:dev
|
||||
yarn app:dev
|
||||
```
|
||||
|
||||
从这里您可以:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# 向你的应用添加一个新实体(引导式)
|
||||
yarn twenty entity:add
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
|
||||
# 生成类型化的 Twenty 客户端和工作区实体类型
|
||||
yarn twenty app:generate
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
|
||||
# 监听你的应用函数日志
|
||||
yarn twenty function:logs
|
||||
# Watch your application's function logs
|
||||
yarn function:logs
|
||||
|
||||
# 按名称执行一个函数
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
# Execute a function by name
|
||||
yarn function:execute -n my-function -p '{\"name\": \"test\"}'
|
||||
|
||||
# 从当前工作区卸载该应用
|
||||
yarn twenty app:uninstall
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
|
||||
# 显示命令帮助
|
||||
yarn twenty help
|
||||
# Display commands' help
|
||||
yarn help
|
||||
```
|
||||
|
||||
另请参阅:[create-twenty-app](https://www.npmjs.com/package/create-twenty-app) 和 [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk) 的 CLI 参考页面。
|
||||
@@ -91,63 +95,90 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # 公共资源文件夹(图像、字体等)
|
||||
src/
|
||||
├── application-config.ts # 必需 - 主应用程序配置
|
||||
├── roles/
|
||||
│ └── default-role.ts # 用于逻辑函数的默认角色
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # 示例逻辑函数
|
||||
└── front-components/
|
||||
└── hello-world.tsx # 示例前端组件
|
||||
application.config.ts # 必需 - 主应用程序配置
|
||||
default-function.role.ts # 用于无服务器函数的默认角色
|
||||
hello-world.function.ts # 示例无服务器函数
|
||||
hello-world.front-component.tsx # 示例前端组件
|
||||
// 你的实体 (*.object.ts, *.function.ts, *.front-component.tsx, *.role.ts)
|
||||
```
|
||||
|
||||
### 约定优于配置
|
||||
|
||||
应用采用**约定优于配置**的方式,根据文件后缀检测实体。 这使得可以在 `src/app/` 文件夹内灵活组织:
|
||||
|
||||
| 文件后缀 | 实体类型 |
|
||||
| ----------------------- | -------- |
|
||||
| `*.object.ts` | 自定义对象定义 |
|
||||
| `*.function.ts` | 无服务器函数定义 |
|
||||
| `*.front-component.tsx` | 前端组件定义 |
|
||||
| `*.role.ts` | 角色定义 |
|
||||
|
||||
### 支持的文件夹组织方式
|
||||
|
||||
你可以按以下任一模式组织实体:
|
||||
|
||||
**传统(按类型):**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── objects/
|
||||
│ └── postCard.object.ts
|
||||
├── functions/
|
||||
│ └── createPostCard.function.ts
|
||||
├── components/
|
||||
│ └── card.front-component.tsx
|
||||
└── roles/
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
**基于特性:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
└── post-card/
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── postCardAdmin.role.ts
|
||||
```
|
||||
|
||||
**扁平:**
|
||||
|
||||
```text
|
||||
src/
|
||||
├── application.config.ts
|
||||
├── postCard.object.ts
|
||||
├── createPostCard.function.ts
|
||||
├── card.front-component.tsx
|
||||
└── admin.role.ts
|
||||
```
|
||||
|
||||
总体来说:
|
||||
|
||||
* **package.json**:声明应用名称、版本、引擎(Node 24+、Yarn 4),并添加 `twenty-sdk` 以及一个 `twenty` 脚本,该脚本会委托给本地的 `twenty` CLI。 运行 `yarn twenty help` 以列出所有可用命令。
|
||||
* **package.json**:声明应用名称、版本、运行时(Node 24+、Yarn 4),并添加 `twenty-sdk`,以及诸如 `app:dev`、`app:generate`、`entity:add`、`function:logs`、`function:execute`、`app:uninstall` 和 `auth:login` 等脚本,这些脚本会委托给本地的 `twenty` CLI。
|
||||
* **.gitignore**:忽略常见产物,如 `node_modules`、`.yarn`、`generated/`(类型化客户端)、`dist/`、`build/`、覆盖率文件夹、日志文件以及 `.env*` 文件。
|
||||
* **yarn.lock**、**.yarnrc.yml**、**.yarn/**:锁定并配置项目使用的 Yarn 4 工具链。
|
||||
* **.nvmrc**:固定项目期望的 Node.js 版本。
|
||||
* **eslint.config.mjs** 和 **tsconfig.json**:为应用的 TypeScript 源码提供 Lint 与 TypeScript 配置。
|
||||
* **README.md**:应用根目录中的简短 README,包含基本说明。
|
||||
* **public/**: 一个用于存储公共资源(图像、字体、静态文件)的文件夹,这些资源将随你的应用程序一起提供。 放置在此处的文件会在同步期间上传,并可在运行时访问。
|
||||
* **src/**:你以代码形式定义应用的主要位置
|
||||
|
||||
### 实体检测
|
||||
|
||||
该 SDK 通过在你的 TypeScript 文件中解析 **`export default define<Entity>({...})`** 调用来检测实体。 每种实体类型都有一个从 `twenty-sdk` 导出的对应辅助函数:
|
||||
|
||||
| 辅助函数 | 实体类型 |
|
||||
| ------------------------ | --------- |
|
||||
| `defineObject()` | 自定义对象定义 |
|
||||
| `defineLogicFunction()` | 逻辑函数定义 |
|
||||
| `defineFrontComponent()` | 前端组件定义 |
|
||||
| `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/**:你以代码形式定义应用的主要位置:
|
||||
* `application.config.ts`:应用的全局配置(元数据和运行时关联)。 参见下方“应用配置”。
|
||||
* `*.role.ts`:你的逻辑函数所使用的角色定义。 参见下方“默认函数角色”。
|
||||
* `*.object.ts`:自定义对象定义。
|
||||
* `*.function.ts`:逻辑函数定义。
|
||||
* `*.front-component.tsx`:前端组件定义。
|
||||
|
||||
后续命令将添加更多文件和文件夹:
|
||||
|
||||
* `yarn twenty app:generate` 将创建一个 `generated/` 文件夹(类型化 Twenty 客户端 + 工作空间类型)。
|
||||
* `yarn twenty entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件或角色添加实体定义文件。
|
||||
* `yarn app:generate` 将创建一个 `generated/` 文件夹(类型化 Twenty 客户端 + 工作空间类型)。
|
||||
* `yarn entity:add` 会在 `src/` 下为你的自定义对象、函数、前端组件或角色添加实体定义文件。
|
||||
|
||||
## 身份验证
|
||||
|
||||
首次运行 `yarn twenty auth:login` 时,你将被提示输入:
|
||||
首次运行 `yarn auth:login` 时,你将被提示输入:
|
||||
|
||||
* API URL(默认为 http://localhost:3000 或你当前的工作空间配置)
|
||||
* API 密钥
|
||||
@@ -158,25 +189,25 @@ export default defineObject({
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn twenty auth:login
|
||||
yarn auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn twenty auth:list
|
||||
yarn auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn twenty auth:switch
|
||||
yarn auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn twenty auth:switch production
|
||||
yarn auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn twenty auth:status
|
||||
yarn auth:status
|
||||
```
|
||||
|
||||
使用 `yarn twenty auth:switch` 切换工作空间后,后续所有命令将默认使用该工作空间。 你仍可通过 `--workspace <name>` 临时覆盖。
|
||||
使用 `auth:switch` 切换工作空间后,后续所有命令将默认使用该工作空间。 你仍可通过 `--workspace <name>` 临时覆盖。
|
||||
|
||||
## 使用 SDK 资源(类型与配置)
|
||||
|
||||
@@ -184,18 +215,16 @@ twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以
|
||||
|
||||
### 辅助函数
|
||||
|
||||
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
|
||||
该 SDK 提供四个带内置校验的辅助函数,用于定义你的应用实体:
|
||||
|
||||
| 函数 | 目的 |
|
||||
| ------------------------ | ------------------ |
|
||||
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
|
||||
| `defineObject()` | 定义带字段的自定义对象 |
|
||||
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
|
||||
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
|
||||
| `defineRole()` | 配置角色权限和对象访问 |
|
||||
| `defineField()` | 为现有对象扩展额外字段 |
|
||||
| 函数 | 目的 |
|
||||
| --------------------- | ------------ |
|
||||
| `defineApplication()` | 配置应用元数据 |
|
||||
| `defineObject()` | 定义带字段的自定义对象 |
|
||||
| `defineFunction()` | 定义带处理程序的逻辑函数 |
|
||||
| `defineRole()` | 配置角色权限和对象访问 |
|
||||
|
||||
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
|
||||
这些函数会在运行时校验你的配置,并提供更好的 IDE 自动补全和类型安全。
|
||||
|
||||
### 定义对象
|
||||
|
||||
@@ -276,15 +305,15 @@ export default defineObject({
|
||||
* `universalIdentifier` 必须在各次部署间保持唯一且稳定。
|
||||
* 每个字段都需要 `name`、`type`、`label` 以及其自身稳定的 `universalIdentifier`。
|
||||
* `fields` 数组是可选的——你可以定义没有自定义字段的对象。
|
||||
* 你可以使用 `yarn twenty entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
|
||||
* 你可以使用 `yarn entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
|
||||
|
||||
<Note>
|
||||
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加 `name`、`createdAt`、`updatedAt`、`createdBy`、`position`、`deletedAt` 等标准字段。 你无需在 `fields` 数组中定义这些字段——只需添加你的自定义字段。
|
||||
</Note>
|
||||
|
||||
### 应用配置(application-config.ts)
|
||||
### 应用配置(application.config.ts)
|
||||
|
||||
每个应用都有一个 `application-config.ts` 文件,用于描述:
|
||||
每个应用都有一个 `application.config.ts` 文件,用于描述:
|
||||
|
||||
* **应用的身份**:标识符、显示名称和描述。
|
||||
* **函数如何运行**:它们用于权限的角色。
|
||||
@@ -293,9 +322,9 @@ export default defineObject({
|
||||
使用 `defineApplication()` 定义你的应用配置:
|
||||
|
||||
```typescript
|
||||
// src/application-config.ts
|
||||
// src/app/application.config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,7 +339,7 @@ export default defineApplication({
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
roleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -318,11 +347,11 @@ export default defineApplication({
|
||||
|
||||
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
|
||||
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
|
||||
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
|
||||
* `roleUniversalIdentifier` 必须与在 `*.role.ts` 文件中定义的角色一致(见下文)。
|
||||
|
||||
#### 角色和权限
|
||||
|
||||
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application-config.ts` 中的 `defaultRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
|
||||
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application.config.ts` 中的 `roleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
|
||||
|
||||
* 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。
|
||||
* 类型化客户端将受限于该角色授予的权限。
|
||||
@@ -333,7 +362,7 @@ 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 =
|
||||
@@ -372,10 +401,10 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
随后,该角色的 `universalIdentifier` 会在 `application-config.ts` 中被引用为 `defaultRoleUniversalIdentifier`。 换句话说:
|
||||
随后,该角色的 `universalIdentifier` 会在 `application.config.ts` 中被引用为 `roleUniversalIdentifier`。 换句话说:
|
||||
|
||||
* **\*.role.ts** 定义默认函数角色可以执行的操作。
|
||||
* **application-config.ts** 指向该角色,使你的函数继承其权限。
|
||||
* **application.config.ts** 指向该角色,使你的函数继承其权限。
|
||||
|
||||
备注:
|
||||
|
||||
@@ -386,11 +415,11 @@ export default defineRole({
|
||||
|
||||
### 逻辑函数的配置与入口点
|
||||
|
||||
每个函数文件都使用 `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';
|
||||
|
||||
@@ -410,7 +439,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,
|
||||
@@ -486,7 +515,7 @@ export default defineLogicFunction({
|
||||
当路由触发器调用你的逻辑函数时,它会接收一个遵循 AWS HTTP API v2 格式的 `RoutePayload` 对象。 从 `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
|
||||
@@ -516,7 +545,7 @@ const handler = async (event: RoutePayload) => {
|
||||
出于安全原因,默认**不会**将传入请求的 HTTP 请求头传递给你的逻辑函数。 如需访问特定请求头,请在 `forwardedRequestHeaders` 数组中显式列出:
|
||||
|
||||
```typescript
|
||||
export default defineLogicFunction({
|
||||
export default defineFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'webhook-handler',
|
||||
handler,
|
||||
@@ -551,49 +580,12 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
你可以通过两种方式创建新函数:
|
||||
|
||||
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新逻辑函数的选项。 这将生成一个包含处理程序和配置的入门文件。
|
||||
* **手动**:创建一个新的 `*.logic-function.ts` 文件,并使用 `defineLogicFunction()`,遵循相同的模式。
|
||||
|
||||
### 前端组件
|
||||
|
||||
前端组件使你可以构建在 Twenty 的 UI 中渲染的自定义 React 组件。 使用 `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,
|
||||
});
|
||||
```
|
||||
|
||||
关键点:
|
||||
|
||||
* 前端组件是在 Twenty 中的隔离上下文中渲染的 React 组件。
|
||||
* 使用 `*.front-component.tsx` 文件后缀以便自动检测。
|
||||
* `component` 字段引用你的 React 组件。
|
||||
* 组件会在 `yarn twenty app:dev` 期间自动构建并同步。
|
||||
|
||||
你可以通过两种方式创建新的前端组件:
|
||||
|
||||
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新前端组件的选项。
|
||||
* **手动**:创建一个新的 `*.front-component.tsx` 文件,并使用 `defineFrontComponent()`。
|
||||
* **脚手架生成**:运行 `yarn entity:add` 并选择添加新函数的选项。 这将生成一个包含处理程序和配置的入门文件。
|
||||
* **手动**:创建一个新的 `*.function.ts` 文件,并使用 `defineFunction()`,遵循相同的模式。
|
||||
|
||||
### 生成的类型化客户端
|
||||
|
||||
运行 `yarn twenty app:generate`,根据你的工作空间模式在 `generated/` 中创建本地类型化客户端。 在你的函数中使用它:
|
||||
运行 yarn app:generate,根据你的工作空间模式在 generated/ 中创建本地类型化客户端。 在你的函数中使用它:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +594,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
客户端会通过 `yarn twenty app:generate` 重新生成。 在更改对象之后或接入新工作空间时,请重新运行。
|
||||
客户端会通过 `yarn app:generate` 重新生成。 在更改对象之后或接入新工作空间时,请重新运行。
|
||||
|
||||
#### 逻辑函数中的运行时凭据
|
||||
|
||||
@@ -614,38 +606,49 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
备注:
|
||||
|
||||
* 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。
|
||||
* API 密钥的权限由 `application-config.ts` 中通过 `defaultRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
|
||||
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `defaultRoleUniversalIdentifier` 指向该角色的通用标识符。
|
||||
* API 密钥的权限由 `application.config.ts` 中通过 `roleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
|
||||
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `roleUniversalIdentifier` 指向该角色的通用标识符。
|
||||
|
||||
### 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)查看一个最小的端到端示例,展示对象、函数和多种触发器:
|
||||
|
||||
## 手动设置(不使用脚手架)
|
||||
|
||||
虽然我们建议使用 `create-twenty-app` 以获得最佳的上手体验,但你也可以手动设置项目。 不要全局安装 CLI。 相反,请将 `twenty-sdk` 添加为本地依赖,并在你的 package.json 中配置一个脚本:
|
||||
虽然我们建议使用 `create-twenty-app` 以获得最佳的上手体验,但你也可以手动设置项目。 不要全局安装 CLI。 相反,请将 `twenty-sdk` 添加为本地依赖,并在你的 package.json 中连接相关脚本:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
然后添加一个 `twenty` 脚本:
|
||||
然后添加如下脚本:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"twenty": "twenty"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:generate": "twenty app:generate",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"help": "twenty help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
现在你可以通过 `yarn twenty <command>` 运行所有命令,例如 `yarn twenty app:dev`、`yarn twenty app:generate`、`yarn twenty help` 等。
|
||||
现在你可以通过 Yarn 运行相同的命令,例如 `yarn app:dev`、`yarn app:generate` 等。
|
||||
|
||||
## 故障排除
|
||||
|
||||
* 身份验证错误:运行 `yarn twenty auth:login`,并确保你的 API 密钥具有所需权限。
|
||||
* 身份验证错误:运行 `yarn auth:login`,并确保你的 API 密钥具有所需权限。
|
||||
* 无法连接到服务器:请验证 API URL,并确保 Twenty 服务器可达。
|
||||
* 类型或客户端缺失/过期:运行 `yarn twenty app:generate`。
|
||||
* 开发模式未同步:确保 `yarn twenty app:dev` 正在运行,并且你的环境不会忽略变更。
|
||||
* 类型或客户端缺失/过期:运行 `yarn app:generate`。
|
||||
* 开发模式未同步:确保 `yarn app:dev` 正在运行,并且你的环境不会忽略变更。
|
||||
|
||||
Discord 帮助频道:https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -7,9 +7,6 @@ test('Create workflow', async ({ page }) => {
|
||||
|
||||
await page.goto(process.env.LINK);
|
||||
|
||||
const workflowsFolder = page.getByRole('button', { name: 'Workflows' });
|
||||
await workflowsFolder.click();
|
||||
|
||||
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
|
||||
await workflowsLink.click();
|
||||
|
||||
|
||||
@@ -5,17 +5,9 @@
|
||||
"tags": ["scope:backend"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["production", "^production"],
|
||||
"outputs": ["{projectRoot}/dist"],
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
"npx vite build",
|
||||
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --outDir dist --rootDir src --composite false && npx tsc-alias -p tsconfig.lib.json --outDir dist"
|
||||
],
|
||||
"parallel": false
|
||||
"outputPath": "{projectRoot}/dist"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react-swc';
|
||||
import * as path from 'path';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -24,13 +25,19 @@ export default defineConfig({
|
||||
configPath: path.resolve(__dirname, './lingui.config.ts'),
|
||||
}),
|
||||
tsconfigPaths({
|
||||
root: __dirname,
|
||||
root: __dirname
|
||||
}),
|
||||
dts({
|
||||
entryRoot: 'src',
|
||||
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
|
||||
}),
|
||||
],
|
||||
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: './dist',
|
||||
reportCompressedSize: false,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
|
||||
@@ -52,11 +52,11 @@ export const rule = createRule<[], 'restApiMethodsShouldBeGuarded'>({
|
||||
meta: {
|
||||
docs: {
|
||||
description:
|
||||
'REST API endpoints should have authentication guards (UserAuthGuard, WorkspaceAuthGuard, FilePathGuard, FileByIdGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
|
||||
'REST API endpoints should have authentication guards (UserAuthGuard, WorkspaceAuthGuard, FilePathGuard, or FilesFieldGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
|
||||
},
|
||||
messages: {
|
||||
restApiMethodsShouldBeGuarded:
|
||||
'All REST API controller endpoints must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard/FilePathGuard/FileByIdGuard/PublicEndpointGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases).',
|
||||
'All REST API controller endpoints must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard/FilePathGuard/FileIdGuard/FilesFieldGuard/PublicEndpointGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases).',
|
||||
},
|
||||
schema: [],
|
||||
hasSuggestions: false,
|
||||
|
||||
@@ -42,7 +42,7 @@ export const typedTokenHelpers = {
|
||||
TSESTree.AST_NODE_TYPES.Identifier &&
|
||||
decorator.expression.callee.name === 'UseGuards'
|
||||
) {
|
||||
// Check the arguments for UserAuthGuard, WorkspaceAuthGuard, PublicEndpoint, FilePathGuard or FileByIdGuard
|
||||
// Check the arguments for UserAuthGuard, WorkspaceAuthGuard, PublicEndpoint, FilePathGuard, or FilesFieldGuard
|
||||
return decorator.expression.arguments.some((arg) => {
|
||||
if (arg.type === TSESTree.AST_NODE_TYPES.Identifier) {
|
||||
return (
|
||||
@@ -50,7 +50,7 @@ export const typedTokenHelpers = {
|
||||
arg.name === 'WorkspaceAuthGuard' ||
|
||||
arg.name === 'PublicEndpointGuard' ||
|
||||
arg.name === 'FilePathGuard' ||
|
||||
arg.name === 'FileByIdGuard'
|
||||
arg.name === 'FilesFieldGuard'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -18,9 +18,9 @@ module.exports = {
|
||||
|
||||
'./src/modules/billing/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/databases/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/analytics/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/object-metadata/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/navigation-menu-item/graphql/**/*.{ts,tsx}',
|
||||
@@ -28,13 +28,9 @@ module.exports = {
|
||||
'./src/modules/attachments/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/file/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/onboarding/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/front-components/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/page-layout/widgets/**/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/dashboards/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
|
||||
'!./src/**/*.test.{ts,tsx}',
|
||||
'!./src/**/*.stories.{ts,tsx}',
|
||||
'!./src/**/__mocks__/*.ts',
|
||||
|
||||
@@ -5,11 +5,22 @@ module.exports = {
|
||||
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
|
||||
'/graphql',
|
||||
documents: [
|
||||
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/activities/emails/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/activities/calendar/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/activities/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/companies/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/people/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/opportunities/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/search/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/views/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/favorites/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/spreadsheet-import/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/command-menu/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/prefetch/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/subscription/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'!./src/**/*.test.{ts,tsx}',
|
||||
'!./src/**/*.stories.{ts,tsx}',
|
||||
|
||||
@@ -62,9 +62,9 @@ const jestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 49.5,
|
||||
lines: 48,
|
||||
functions: 40,
|
||||
statements: 50,
|
||||
lines: 48.9,
|
||||
functions: 40.9,
|
||||
},
|
||||
},
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
"graphql": "16.8.1",
|
||||
"graphql-sse": "^2.5.4",
|
||||
"input-otp": "^1.4.2",
|
||||
"jotai": "^2.17.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"json-2-csv": "^5.4.0",
|
||||
"json-logic-js": "^2.0.5",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -7,7 +7,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
|
||||
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type AppPath, type NavigateOptions } from 'twenty-shared/types';
|
||||
import { type AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
|
||||
export const useNavigateApp = () => {
|
||||
@@ -9,7 +9,10 @@ export const useNavigateApp = () => {
|
||||
to: T,
|
||||
params?: Parameters<typeof getAppPath<T>>[1],
|
||||
queryParams?: Record<string, any>,
|
||||
options?: NavigateOptions,
|
||||
options?: {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
},
|
||||
) => {
|
||||
const path = getAppPath(to, params, queryParams);
|
||||
return navigate(path, options);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const usePageChangeEffectNavigateLocation = () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user