Compare commits
220
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
568304c132 | ||
|
|
55b1624210 | ||
|
|
c268a50e4e | ||
|
|
4c57352450 | ||
|
|
b37ef3e7da | ||
|
|
4d877d072d | ||
|
|
baf2fc4cc9 | ||
|
|
3b55026452 | ||
|
|
53065f241f | ||
|
|
300be990b0 | ||
|
|
0a76db94bc | ||
|
|
c26c0b9d71 | ||
|
|
f52d66b960 | ||
|
|
65b2baca7a | ||
|
|
66d9f92e60 | ||
|
|
8dd172ba00 | ||
|
|
83653a463c | ||
|
|
8c4a6cd663 | ||
|
|
f99c05f0e8 | ||
|
|
c99db7d9c6 | ||
|
|
5a22cc88c5 | ||
|
|
9a403b84f3 | ||
|
|
a82d078906 | ||
|
|
61720470c5 | ||
|
|
31baf52528 | ||
|
|
217957f2a1 | ||
|
|
4e6cf185fa | ||
|
|
001b7285e6 | ||
|
|
ec7d19a3af | ||
|
|
ed07afa774 | ||
|
|
b284c8323c | ||
|
|
34a903b4fa | ||
|
|
aed81a54a2 | ||
|
|
847e7124d7 | ||
|
|
186bc02533 | ||
|
|
e03ac126ab | ||
|
|
63c407e2f7 | ||
|
|
4b3a46d953 | ||
|
|
f13e7e01fe | ||
|
|
43ce396152 | ||
|
|
2ea62317d8 | ||
|
|
f8c35a95b5 | ||
|
|
67e7f05a68 | ||
|
|
cbefd42c4c | ||
|
|
f6423f5925 | ||
|
|
4fd721a3c9 | ||
|
|
20f28d6593 | ||
|
|
0fa7beba44 | ||
|
|
fb950cf312 | ||
|
|
2bb939b4b5 | ||
|
|
d2f51cc939 | ||
|
|
7a317b9182 | ||
|
|
df1d0d877d | ||
|
|
e3077691d1 | ||
|
|
8a10071253 | ||
|
|
7ef80dd238 | ||
|
|
8fde5d9da3 | ||
|
|
afdd914b83 | ||
|
|
77498d2f73 | ||
|
|
9bea5f73f1 | ||
|
|
c54747379a | ||
|
|
00208dbf1f | ||
|
|
e411f076f1 | ||
|
|
07745947ee | ||
|
|
082822b790 | ||
|
|
0c7712926d | ||
|
|
086256eb8d | ||
|
|
93eeeb2397 | ||
|
|
36fbfca069 | ||
|
|
5116002ca2 | ||
|
|
caac791421 | ||
|
|
d3a1f45027 | ||
|
|
a5174c0519 | ||
|
|
50c5a1a8de | ||
|
|
74e26ae635 | ||
|
|
5e3cf7cd2b | ||
|
|
16e145b036 | ||
|
|
1e908e5f0f | ||
|
|
d495a9f412 | ||
|
|
1df9942416 | ||
|
|
34972f6167 | ||
|
|
37acd15033 | ||
|
|
c76824e69f | ||
|
|
7bd84a6029 | ||
|
|
1577a1933f | ||
|
|
086ea644bb | ||
|
|
19f11b1216 | ||
|
|
9b8cb610c3 | ||
|
|
64c7f52f06 | ||
|
|
dc6e76b6ab | ||
|
|
a78a843419 | ||
|
|
5eaabe95e7 | ||
|
|
5edc034f8b | ||
|
|
9080180156 | ||
|
|
f8dff23a3e | ||
|
|
1b8f26323a | ||
|
|
80337d5c37 | ||
|
|
91b40b391d | ||
|
|
c27e3cd1a0 | ||
|
|
ffb6029c2d | ||
|
|
6073bb6706 | ||
|
|
5874fb5f39 | ||
|
|
37dbd94596 | ||
|
|
238018dc7b | ||
|
|
2267c56f15 | ||
|
|
7aed9291cd | ||
|
|
5c867303e0 | ||
|
|
fda3eabb5f | ||
|
|
7d6111b0a5 | ||
|
|
4df3539ba1 | ||
|
|
8905d860c7 | ||
|
|
bc7b5aee58 | ||
|
|
31b6dbc583 | ||
|
|
3306d66f5b | ||
|
|
6cd3f2db2b | ||
|
|
0e0fb246e6 | ||
|
|
83917f0dca | ||
|
|
ac4819758d | ||
|
|
d07c27a907 | ||
|
|
265d859c6e | ||
|
|
1ae88f4e4f | ||
|
|
85be463487 | ||
|
|
8a84e32cf6 | ||
|
|
8aa208fc93 | ||
|
|
9a58f3d459 | ||
|
|
9d96e529c8 | ||
|
|
e9310555fb | ||
|
|
8090fa4364 | ||
|
|
90de1d4a34 | ||
|
|
8026451220 | ||
|
|
b3d46b0fa3 | ||
|
|
a8085db5fd | ||
|
|
b540ae9735 | ||
|
|
20adb86917 | ||
|
|
c91d642f29 | ||
|
|
15eb3e7edc | ||
|
|
af3423dc6f | ||
|
|
a0484f686a | ||
|
|
5c8b6e395b | ||
|
|
9bdef449e6 | ||
|
|
e2bd3e8ef4 | ||
|
|
c0eacedfec | ||
|
|
24a5273a79 | ||
|
|
607e708670 | ||
|
|
61abfd103d | ||
|
|
54be3b7e87 | ||
|
|
137e068ced | ||
|
|
91f8f7329a | ||
|
|
d341d0d624 | ||
|
|
5e9792009f | ||
|
|
38802bd0b8 | ||
|
|
e692e97428 | ||
|
|
1f3965e5f8 | ||
|
|
e048d03872 | ||
|
|
653180d10f | ||
|
|
8702300b07 | ||
|
|
6e23ca35e6 | ||
|
|
96a242eb7d | ||
|
|
d97a1cfc27 | ||
|
|
c844109ffc | ||
|
|
dd2a09576b | ||
|
|
fe07de63b0 | ||
|
|
1c9fc94c1f | ||
|
|
d10ef156c1 | ||
|
|
d3ec64072b | ||
|
|
1b14e7e1f1 | ||
|
|
6ad9566043 | ||
|
|
f39fccc3c4 | ||
|
|
ac1ec91f25 | ||
|
|
d324bbfc25 | ||
|
|
7c2a9abed4 | ||
|
|
c0c0cbb896 | ||
|
|
a2188cb0eb | ||
|
|
23874848a4 | ||
|
|
b23d2b4e73 | ||
|
|
601dc02ed7 | ||
|
|
2d552fc9fd | ||
|
|
f33ad53e72 | ||
|
|
7053e1bbc5 | ||
|
|
955aa9191f | ||
|
|
3f87d27d5d | ||
|
|
68cd2f6d61 | ||
|
|
8c9228cb2b | ||
|
|
35b76539cc | ||
|
|
ea4ef99565 | ||
|
|
c7d1cd11e0 | ||
|
|
83d30f8b76 | ||
|
|
aec43da1e2 | ||
|
|
646edec104 | ||
|
|
ea572975d8 | ||
|
|
8acfacc69c | ||
|
|
4aa1d71b12 | ||
|
|
7bf309ba73 | ||
|
|
8d61bb9ae6 | ||
|
|
0d44d7c6d7 | ||
|
|
d3f0162cf5 | ||
|
|
3a1e112b86 | ||
|
|
e062343802 | ||
|
|
3747af4b5a | ||
|
|
bad488494f | ||
|
|
f25e040712 | ||
|
|
265b2582ee | ||
|
|
ed912ec548 | ||
|
|
563e27831b | ||
|
|
9cc794ddb6 | ||
|
|
80c5cfc2ec | ||
|
|
804e0539d9 | ||
|
|
4db7ae20c4 | ||
|
|
8da69e0f77 | ||
|
|
282ee9ac42 | ||
|
|
0b44c9e4e5 | ||
|
|
e1a58df140 | ||
|
|
93c0c98495 | ||
|
|
f8c3960cf2 | ||
|
|
d562a384c2 | ||
|
|
90597e47ca | ||
|
|
b55765a991 | ||
|
|
0b8421a45a | ||
|
|
4ba2f3b184 | ||
|
|
8543576dae |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
|
||||
"env": {}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start development environment (frontend + backend + worker)
|
||||
yarn start
|
||||
|
||||
# Individual package development
|
||||
npx nx start twenty-front # Start frontend dev server
|
||||
npx nx start twenty-server # Start backend server
|
||||
npx nx run twenty-server:worker # Start background worker
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Preferred: run a single test file (fast)
|
||||
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
|
||||
|
||||
# Run all tests for a package
|
||||
npx nx test twenty-front # Frontend unit tests
|
||||
npx nx test twenty-server # Backend unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
|
||||
# To run an indivual test or a pattern of tests, use the following command:
|
||||
cd packages/{workspace} && npx jest "pattern or filename"
|
||||
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front
|
||||
npx nx storybook:test twenty-front
|
||||
|
||||
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Linting (diff with main - fastest, always prefer this)
|
||||
npx nx lint:diff-with-main twenty-front
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
|
||||
|
||||
# Linting (full project - slower, use only when needed)
|
||||
npx nx lint twenty-front
|
||||
npx nx lint twenty-server
|
||||
|
||||
# Type checking
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
|
||||
# Format code
|
||||
npx nx fmt twenty-front
|
||||
npx nx fmt twenty-server
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build packages (twenty-shared must be built first)
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
```bash
|
||||
# Database management
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
|
||||
|
||||
# Generate an instance command (fast or slow)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
|
||||
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Inspect workspace data, metadata, and object definitions while developing
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
### Package Structure
|
||||
```
|
||||
packages/
|
||||
├── twenty-front/ # React frontend application
|
||||
├── twenty-server/ # NestJS backend API
|
||||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-website/ # Next.js documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
```
|
||||
|
||||
### Key Development Principles
|
||||
- **Functional components only** (no class components)
|
||||
- **Named exports only** (no default exports)
|
||||
- **Types over interfaces** (except when extending third-party interfaces)
|
||||
- **String literals over enums** (except for GraphQL enums)
|
||||
- **No 'any' type allowed** — strict TypeScript enforced
|
||||
- **Event handlers preferred over useEffect** for state updates
|
||||
- **Props down, events up** — unidirectional data flow
|
||||
- **Composition over inheritance**
|
||||
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
|
||||
|
||||
### Naming Conventions
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
|
||||
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
|
||||
- **TypeScript generics**: descriptive names (`TData` not `T`)
|
||||
|
||||
### File Structure
|
||||
- Components under 300 lines, services under 500 lines
|
||||
- Components in their own directories with tests and stories
|
||||
- Use `index.ts` barrel exports for clean imports
|
||||
- Import order: external libraries first, then internal (`@/`), then relative
|
||||
|
||||
### Comments
|
||||
- Use short-form comments (`//`), not JSDoc blocks
|
||||
- Explain WHY (business logic), not WHAT
|
||||
- Do not comment obvious code
|
||||
- Multi-line comments use multiple `//` lines, not `/** */`
|
||||
|
||||
### State Management
|
||||
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
|
||||
- GraphQL cache managed by Apollo Client
|
||||
- Use functional state updates: `setState(prev => prev + 1)`
|
||||
|
||||
### Backend Architecture
|
||||
- **NestJS modules** for feature organization
|
||||
- **TypeORM** for database ORM with PostgreSQL
|
||||
- **GraphQL** API with code-first approach
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Upgrade Commands
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
|
||||
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
|
||||
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
|
||||
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
|
||||
- Include both `up` and `down` logic in instance commands
|
||||
- Never delete or rewrite committed instance command `up`/`down` logic
|
||||
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
|
||||
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
|
||||
### Testing Strategy
|
||||
- **Test behavior, not implementation** — focus on user perspective
|
||||
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
|
||||
- Query by user-visible elements (text, roles, labels) over test IDs
|
||||
- Use `@testing-library/user-event` for realistic interactions
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
|
||||
```bash
|
||||
bash packages/twenty-utils/setup-dev-env.sh
|
||||
```
|
||||
|
||||
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
|
||||
|
||||
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
|
||||
- `--down` — stop services
|
||||
- `--reset` — wipe data and restart fresh
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
|
||||
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Detailed development guidelines and best practices
|
||||
@@ -12,7 +12,7 @@ This directory contains Twenty's development guidelines and best practices in th
|
||||
### Core Guidelines
|
||||
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
|
||||
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
|
||||
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
|
||||
- **server-migrations.mdc** - Upgrade command guidelines (instance commands and workspace commands) for `twenty-server` (Auto-attached to server entities and upgrade command files)
|
||||
- **creating-syncable-entity.mdc** - Comprehensive guide for creating new syncable entities (with universalIdentifier and applicationId) in the workspace migration system (Agent-requested for metadata-modules and workspace-migration files)
|
||||
|
||||
### Code Quality
|
||||
@@ -81,11 +81,8 @@ npx nx run twenty-server:typecheck # Type checking
|
||||
npx nx run twenty-server:test # Run unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
|
||||
|
||||
# Migrations
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
|
||||
# Workspace
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata
|
||||
# Upgrade commands (instance + workspace)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
@@ -55,7 +55,8 @@ If feature descriptions are not provided or need enhancement, research the codeb
|
||||
- Services: Look for `*.service.ts` files
|
||||
|
||||
**For Database/ORM Changes:**
|
||||
- Migrations: `packages/twenty-server/src/database/typeorm/`
|
||||
- Instance commands (fast/slow): `packages/twenty-server/src/database/commands/upgrade-version-command/`
|
||||
- Legacy TypeORM migrations: `packages/twenty-server/src/database/typeorm/`
|
||||
- Entities: `packages/twenty-server/src/entities/`
|
||||
|
||||
### Research Commands
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
---
|
||||
description: Guidelines for generating and managing TypeORM migrations in twenty-server
|
||||
description: Guidelines for generating and managing upgrade commands (instance commands and workspace commands) in twenty-server
|
||||
globs: [
|
||||
"packages/twenty-server/src/**/*.entity.ts",
|
||||
"packages/twenty-server/src/database/typeorm/**/*.ts"
|
||||
"packages/twenty-server/src/database/commands/upgrade-version-command/**/*.ts"
|
||||
]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Server Migrations (twenty-server)
|
||||
## Upgrade Commands (twenty-server)
|
||||
|
||||
- **When changing an entity, always generate a migration**
|
||||
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
|
||||
- Use the Nx + TypeORM command from the project root:
|
||||
The upgrade system uses two types of commands instead of raw TypeORM migrations:
|
||||
- **Instance commands** — schema and data migrations that run once at the instance level.
|
||||
- **Workspace commands** — commands that iterate over all active/suspended workspaces.
|
||||
|
||||
See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation.
|
||||
|
||||
### Instance Commands
|
||||
|
||||
- **When changing a `*.entity.ts` file**, generate an instance command:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
|
||||
- **Fast commands** (`--type fast`, default) are for schema-only changes that must run immediately. They implement `FastInstanceCommand` with `up`/`down` methods and use the `@RegisteredInstanceCommand` decorator.
|
||||
|
||||
- **Prefer generated migrations over manual edits**
|
||||
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
|
||||
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
|
||||
- **Slow commands** (`--type slow`) add a `runDataMigration` method for potentially long-running data backfills that execute before `up`. They only run when `--include-slow` is passed. Use the decorator with `{ type: 'slow' }`.
|
||||
|
||||
- **Keep migrations consistent and reversible**
|
||||
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
|
||||
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
|
||||
- The generator auto-registers the command in `instance-commands.constant.ts` — do not edit that file manually.
|
||||
|
||||
- **Keep commands consistent and reversible**: include both `up` and `down` logic. Do not delete or rewrite existing, committed commands unless on a pre-release branch.
|
||||
|
||||
### Workspace Commands
|
||||
|
||||
- Use the `@RegisteredWorkspaceCommand` decorator alongside nest-commander's `@Command` decorator.
|
||||
- Extend `ActiveOrSuspendedWorkspaceCommandRunner` and implement `runOnWorkspace`.
|
||||
- The base class provides `--dry-run`, `--verbose`, and workspace filter options automatically.
|
||||
|
||||
### Execution Order
|
||||
|
||||
Within a given version, commands run in this order (timestamp-sorted within each group):
|
||||
1. Instance fast commands
|
||||
2. Instance slow commands (only with `--include-slow`)
|
||||
3. Workspace commands
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Deploy Twenty App
|
||||
description: Build and deploy a Twenty app to a remote instance
|
||||
|
||||
inputs:
|
||||
api-url:
|
||||
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
|
||||
required: true
|
||||
api-key:
|
||||
description: API key or access token for the target instance
|
||||
required: true
|
||||
app-path:
|
||||
description: Path to the app directory (relative to repo root). Defaults to repo root.
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Enable Corepack
|
||||
shell: bash
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '${{ inputs.app-path }}/.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Configure remote
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.twenty
|
||||
node -e "
|
||||
const fs = require('fs'), path = require('path'), os = require('os');
|
||||
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
|
||||
version: 1,
|
||||
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
|
||||
}, null, 2));
|
||||
"
|
||||
env:
|
||||
API_URL: ${{ inputs.api-url }}
|
||||
API_KEY: ${{ inputs.api-key }}
|
||||
|
||||
- name: Deploy
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn twenty deploy --remote target
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Install Twenty App
|
||||
description: Install (or upgrade) a Twenty app on a specific workspace
|
||||
|
||||
inputs:
|
||||
api-url:
|
||||
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
|
||||
required: true
|
||||
api-key:
|
||||
description: API key or access token for the target workspace
|
||||
required: true
|
||||
app-path:
|
||||
description: Path to the app directory (relative to repo root). Defaults to repo root.
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Enable Corepack
|
||||
shell: bash
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '${{ inputs.app-path }}/.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Configure remote
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.twenty
|
||||
node -e "
|
||||
const fs = require('fs'), path = require('path'), os = require('os');
|
||||
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
|
||||
version: 1,
|
||||
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY } }
|
||||
}, null, 2));
|
||||
"
|
||||
env:
|
||||
API_URL: ${{ inputs.api-url }}
|
||||
API_KEY: ${{ inputs.api-key }}
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn twenty install --remote target
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Spawn Twenty App Dev Test
|
||||
description: >
|
||||
Starts a Twenty all-in-one test instance (server, worker, database, redis)
|
||||
using the twentycrm/twenty-app-dev Docker image on port 2021.
|
||||
The server is available at http://localhost:2021 with seeded demo data.
|
||||
|
||||
inputs:
|
||||
twenty-version:
|
||||
description: 'Twenty Docker Hub image tag for twenty-app-dev (e.g., "latest" or "v1.20.0").'
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
outputs:
|
||||
server-url:
|
||||
description: 'URL where the Twenty test server can be reached'
|
||||
value: http://localhost:2021
|
||||
api-key:
|
||||
description: 'API key (type: API_KEY) for the seeded Twenty dev workspace'
|
||||
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Start twenty-app-dev-test container
|
||||
shell: bash
|
||||
run: |
|
||||
docker run -d \
|
||||
--name twenty-app-dev-test \
|
||||
-p 2021:2021 \
|
||||
-e NODE_PORT=2021 \
|
||||
-e SERVER_URL=http://localhost:2021 \
|
||||
twentycrm/twenty-app-dev:${{ inputs.twenty-version }}
|
||||
|
||||
echo "Waiting for Twenty test instance to become healthy…"
|
||||
TIMEOUT=180
|
||||
ELAPSED=0
|
||||
until curl -sf http://localhost:2021/healthz > /dev/null 2>&1; do
|
||||
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
|
||||
echo "::error::Twenty did not become healthy within ${TIMEOUT}s"
|
||||
docker logs twenty-app-dev-test 2>&1 | tail -80
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
ELAPSED=$((ELAPSED + 3))
|
||||
echo " … waited ${ELAPSED}s"
|
||||
done
|
||||
echo "Twenty test instance is ready at http://localhost:2021 (took ~${ELAPSED}s)"
|
||||
@@ -251,6 +251,14 @@ jobs:
|
||||
rm -f /tmp/current-server.pid
|
||||
fi
|
||||
|
||||
- name: Flush Redis between server runs
|
||||
run: |
|
||||
# Clear all Redis caches to prevent stale data from the current branch
|
||||
# server contaminating the main branch server. Both servers share the
|
||||
# same Redis instance, and CoreEntityCacheService/WorkspaceCacheService
|
||||
# persist cached entities across process restarts.
|
||||
redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis"
|
||||
|
||||
- name: Checkout main branch
|
||||
run: |
|
||||
git stash
|
||||
|
||||
@@ -25,12 +25,10 @@ jobs:
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
create-app-e2e-hello-world:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
@@ -55,6 +53,8 @@ jobs:
|
||||
- 6379:6379
|
||||
env:
|
||||
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -142,27 +142,24 @@ jobs:
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Build server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Create and setup database
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Authenticate with twenty-server
|
||||
env:
|
||||
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
|
||||
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
@@ -189,8 +186,6 @@ jobs:
|
||||
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
@@ -23,12 +23,10 @@ jobs:
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
create-app-e2e-minimal:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
@@ -53,6 +51,8 @@ jobs:
|
||||
- 6379:6379
|
||||
env:
|
||||
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -136,41 +136,26 @@ jobs:
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Build server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Create and setup database
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Authenticate with twenty-server
|
||||
env:
|
||||
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
|
||||
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty deploy
|
||||
|
||||
- name: Install scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty install
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
@@ -25,12 +25,10 @@ jobs:
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
create-app-e2e-postcard:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
@@ -55,6 +53,8 @@ jobs:
|
||||
- 6379:6379
|
||||
env:
|
||||
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -140,27 +140,24 @@ jobs:
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Build server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Create and setup database
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Authenticate with twenty-server
|
||||
env:
|
||||
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
|
||||
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
@@ -187,8 +184,6 @@ jobs:
|
||||
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
name: CI Example App Hello World
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-apps/examples/hello-world/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
|
||||
example-app-hello-world:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build SDK packages
|
||||
run: npx nx build twenty-sdk
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Run integration tests
|
||||
working-directory: packages/twenty-apps/examples/hello-world
|
||||
run: npx vitest run
|
||||
|
||||
ci-example-app-hello-world-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, example-app-hello-world]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -0,0 +1,94 @@
|
||||
name: CI Example App Postcard
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-apps/examples/postcard/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
|
||||
example-app-postcard:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build SDK packages
|
||||
run: npx nx build twenty-sdk
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
- name: Run integration tests
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: npx vitest run
|
||||
|
||||
ci-example-app-postcard-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, example-app-postcard]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
TASK_CACHE_KEY: front-task-${{ matrix.task }}
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
files: |
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-server/**
|
||||
.github/workflows/ci-sdk.yaml
|
||||
!packages/twenty-sdk/package.json
|
||||
sdk-test:
|
||||
needs: changed-files-check
|
||||
@@ -69,7 +70,8 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
NODE_ENV: test
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -77,16 +79,26 @@ jobs:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
- name: Build SDK
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Server / Create Test DB
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server > /tmp/twenty-server.log 2>&1 &
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
- name: SDK / Run e2e Tests
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: test:e2e
|
||||
run: NODE_ENV=test npx vitest run --config ./vitest.e2e.config.ts
|
||||
working-directory: packages/twenty-sdk
|
||||
- name: Server / Dump logs on failure
|
||||
if: failure()
|
||||
run: tail -100 /tmp/twenty-server.log || echo "No server log file found"
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -144,15 +144,18 @@ jobs:
|
||||
exit 1
|
||||
- name: Server / Check for Pending Migrations
|
||||
run: |
|
||||
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
|
||||
npx nx database:migrate:generate twenty-server -- --name pending-migration-check || true
|
||||
|
||||
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
|
||||
if ! git diff --quiet; then
|
||||
echo "::error::Unexpected migration files were generated. Please run 'npx nx database:migrate:generate twenty-server -- --name <migration-name>' and commit the result."
|
||||
echo ""
|
||||
echo "The following migration changes were detected:"
|
||||
echo "==================================================="
|
||||
git diff
|
||||
echo "==================================================="
|
||||
echo ""
|
||||
|
||||
if [ -n "$CORE_MIGRATION_FILE" ]; then
|
||||
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f packages/twenty-server/*core-migration-check.ts
|
||||
git checkout -- .
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -53,3 +53,4 @@ mcp.json
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
.playwright-cli/
|
||||
output/playwright/
|
||||
|
||||
@@ -71,13 +71,10 @@ npx nx build twenty-server
|
||||
# Database management
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
|
||||
|
||||
# Generate migration (replace [name] with kebab-case descriptive name)
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
|
||||
# Sync metadata
|
||||
npx nx run twenty-server:command workspace:sync-metadata
|
||||
# Generate an instance command (fast or slow)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
@@ -87,7 +84,7 @@ A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation or `workspace:sync-metadata` issues
|
||||
- Inspect metadata tables to debug GraphQL schema generation issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
@@ -161,14 +158,17 @@ packages/
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Migrations
|
||||
### Database & Upgrade Commands
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- Always generate migrations when changing entity files
|
||||
- Migration names must be kebab-case (e.g. `add-agent-turn-evaluation`)
|
||||
- Include both `up` and `down` logic in migrations
|
||||
- Never delete or rewrite committed migrations
|
||||
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
|
||||
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
|
||||
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
|
||||
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
|
||||
- Include both `up` and `down` logic in instance commands
|
||||
- Never delete or rewrite committed instance command `up`/`down` logic
|
||||
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
@@ -181,7 +181,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure database migrations are generated for entity changes
|
||||
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"cache": false,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npm pkg set version={args.releaseVersion}"
|
||||
"command": "node -e \"const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='{args.releaseVersion}';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n');\""
|
||||
}
|
||||
},
|
||||
"storybook:build": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.8.0-canary.9",
|
||||
"version": "1.22.0-canary.3",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-and-install:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Deploy
|
||||
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
|
||||
- name: Install
|
||||
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
+11
-5
@@ -6,9 +6,16 @@ on:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -16,12 +23,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
- name: Spawn Twenty test instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
@@ -39,4 +45,4 @@ jobs:
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
|
||||
@@ -3,43 +3,51 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
|
||||
|
||||
beforeAll(async () => {
|
||||
const apiUrl = process.env.TWENTY_API_URL!;
|
||||
const token = process.env.TWENTY_API_KEY!;
|
||||
|
||||
if (!apiUrl || !token) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify(configFile, null, 2),
|
||||
CONFIG_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey: token },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
|
||||
});
|
||||
|
||||
@@ -14,8 +14,10 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export const copyBaseApplicationProject = async ({
|
||||
console.log(chalk.gray('Generating application project...'));
|
||||
await fs.copy(join(__dirname, './constants/template'), appDirectory);
|
||||
|
||||
await renameGitignore({ appDirectory });
|
||||
await renameDotfiles({ appDirectory });
|
||||
|
||||
await generateUniversalIdentifiers({
|
||||
appDisplayName,
|
||||
@@ -32,11 +32,20 @@ export const copyBaseApplicationProject = async ({
|
||||
await updatePackageJson({ appName, appDirectory });
|
||||
};
|
||||
|
||||
const renameGitignore = async ({ appDirectory }: { appDirectory: string }) => {
|
||||
const gitignorePath = join(appDirectory, 'gitignore');
|
||||
// npm strips dotfiles/dotdirs (.gitignore, .github/) from published packages,
|
||||
// so we store them without the leading dot and rename after copying.
|
||||
const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
|
||||
const renames = [
|
||||
{ from: 'gitignore', to: '.gitignore' },
|
||||
{ from: 'github', to: '.github' },
|
||||
];
|
||||
|
||||
if (await fs.pathExists(gitignorePath)) {
|
||||
await fs.rename(gitignorePath, join(appDirectory, '.gitignore'));
|
||||
for (const { from, to } of renames) {
|
||||
const sourcePath = join(appDirectory, from);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await fs.rename(sourcePath, join(appDirectory, to));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
|
||||
Updates Last interaction and Interaction status fields based on last email date
|
||||
|
||||
## Requirements
|
||||
- an `apiKey` - go to Settings > API & Webhooks to generate one
|
||||
|
||||
## Setup
|
||||
1. Add and synchronize app
|
||||
Add and synchronize app
|
||||
```bash
|
||||
cd packages/twenty-apps/community/last-email-interaction
|
||||
yarn auth
|
||||
yarn sync
|
||||
yarn twenty remote add
|
||||
yarn twenty install
|
||||
```
|
||||
2. Go to Settings > Integrations > Last email interaction > Settings and add required variables
|
||||
|
||||
## Flow
|
||||
- Checks if fields are created, if not, creates them on fly
|
||||
- Extracts the datetime of message and calculates the last interaction status
|
||||
- Fetches all users and companies connected to them and updates their Last interaction and Interaction status fields
|
||||
- Extracts the datetime of fetched message and calculates the last interaction status
|
||||
- Fetches all users and companies connected to the message and updates their Last interaction and Interaction status fields
|
||||
|
||||
## Todo:
|
||||
- update app with generated Twenty object once extending objects is possible
|
||||
## Notes
|
||||
- Upon install, creates fields to Person and Company objects
|
||||
- Every day at midnight app goes through all companies and people records and updates their Interaction status based on Last interaction date
|
||||
@@ -1,23 +0,0 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
|
||||
displayName: 'Last email interaction',
|
||||
description:
|
||||
'Updates Last interaction and Interaction status fields based on last received email',
|
||||
icon: "IconMailFast",
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e',
|
||||
isSecret: true,
|
||||
description: 'Required to send requests to Twenty',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d',
|
||||
isSecret: false,
|
||||
description: 'Optional, defaults to cloud API URL',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -9,19 +9,23 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"twenty-sdk": "0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"axios": "1.14.0",
|
||||
"twenty-sdk": "0.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"auth": "twenty auth login",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
|
||||
displayName: 'Last email interaction',
|
||||
description:
|
||||
'Updates Last interaction and Interaction status fields based on last received email',
|
||||
icon: "IconMailFast",
|
||||
defaultRoleUniversalIdentifier: '7a66af97-5056-45b2-96a9-c89f0fd181d1',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '9378751e-c23b-4e84-887d-2905cb8359b4',
|
||||
name: 'interactionStatus',
|
||||
label: 'Interaction status',
|
||||
type: FieldType.SELECT,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
description: 'Indicates the health of relation',
|
||||
options: [
|
||||
{
|
||||
id: '39d54a6b-5a0e-4209-9a59-2a2d7b8c462b',
|
||||
color: 'green',
|
||||
label: 'Recent',
|
||||
value: 'RECENT',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '7377d6c5-a75c-453e-a1a1-63fb9cba4e26',
|
||||
color: 'yellow',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: 'a8b99246-237f-4715-b21f-94a3ae14994e',
|
||||
color: 'sky',
|
||||
label: 'Cooling',
|
||||
value: 'COOLING',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '1f05d528-eaab-4639-aba1-328050a87220',
|
||||
color: 'gray',
|
||||
label: 'Dormant',
|
||||
value: 'DORMANT',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '2f195c4c-1db1-4bbe-80b6-25c2f63168b0',
|
||||
name: 'lastInteraction',
|
||||
label: 'Last interaction',
|
||||
type: FieldType.DATE_TIME,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
description: 'Date when the last interaction happened',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'fa342e26-9742-4db8-85b4-4d78ba18482f',
|
||||
name: 'interactionStatus',
|
||||
label: 'Interaction status',
|
||||
type: FieldType.SELECT,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
description: 'Indicates the health of relation',
|
||||
options: [
|
||||
{
|
||||
id: '1dfbfa99-35fb-43af-8c14-74e682a8121b',
|
||||
color: 'green',
|
||||
label: 'Recent',
|
||||
value: 'RECENT',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '955788e8-6d64-45ba-80ea-a1a5446a0ae7',
|
||||
color: 'yellow',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '7b84ca72-fac5-4c6d-ab08-b148e4b3efdf',
|
||||
color: 'sky',
|
||||
label: 'Cooling',
|
||||
value: 'COOLING',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '04dea3e5-ec26-41cf-b23f-37abab67827a',
|
||||
color: 'gray',
|
||||
label: 'Dormant',
|
||||
value: 'DORMANT',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'bec14de7-6683-4784-91ba-62d83b5f30f7',
|
||||
name: 'lastInteraction',
|
||||
label: 'Last interaction',
|
||||
type: FieldType.DATE_TIME,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
description: 'Date when the last interaction happened',
|
||||
});
|
||||
@@ -1,270 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { type FunctionConfig } from 'twenty-sdk';
|
||||
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '';
|
||||
const TWENTY_URL =
|
||||
process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined
|
||||
? `${process.env.TWENTY_API_URL}/rest`
|
||||
: 'https://api.twenty.com/rest';
|
||||
|
||||
const create_last_interaction = (id: string) => {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: `${TWENTY_URL}/metadata/fields`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
data: {
|
||||
type: 'DATE_TIME',
|
||||
objectMetadataId: `${id}`,
|
||||
name: 'lastInteraction',
|
||||
label: 'Last interaction',
|
||||
description: 'Date when the last interaction happened',
|
||||
icon: 'IconCalendarClock',
|
||||
defaultValue: null,
|
||||
isNullable: true,
|
||||
settings: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const create_interaction_status = (id: string) => {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: `${TWENTY_URL}/metadata/fields`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
data: {
|
||||
type: 'SELECT',
|
||||
objectMetadataId: `${id}`,
|
||||
name: 'interactionStatus',
|
||||
label: 'Interaction status',
|
||||
description: 'Indicates the health of relation',
|
||||
icon: 'IconProgress',
|
||||
defaultValue: null,
|
||||
isNullable: true,
|
||||
settings: {},
|
||||
options: [
|
||||
{
|
||||
color: 'green',
|
||||
label: 'Recent',
|
||||
value: 'RECENT',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
color: 'yellow',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
color: 'sky',
|
||||
label: 'Cooling',
|
||||
value: 'COOLING',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
color: 'gray',
|
||||
label: 'Dormant',
|
||||
value: 'DORMANT',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const calculateStatus = (date: string) => {
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
const now = Date.now();
|
||||
const messageDate = Date.parse(date);
|
||||
const deltaTime = now - messageDate;
|
||||
return deltaTime < 7 * day
|
||||
? 'RECENT'
|
||||
: deltaTime < 30 * day
|
||||
? 'ACTIVE'
|
||||
: deltaTime < 90 * day
|
||||
? 'COOLING'
|
||||
: 'DORMANT';
|
||||
};
|
||||
|
||||
const updateInteractionStatus = async (objectName: string, id: string, messageDate: string, status: string) => {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
url: `${TWENTY_URL}/${objectName}/${id}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
data: {
|
||||
lastInteraction: messageDate,
|
||||
interactionStatus: status
|
||||
}
|
||||
};
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
if (response.status === 200) {
|
||||
console.log('Successfully updated company last interaction field');
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRelatedCompanyId = async (id: string) => {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${TWENTY_URL}/people/${id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
};
|
||||
try {
|
||||
const req = await axios.request(options);
|
||||
if (req.status === 200 && req.data.person.companyId !== null && req.data.person.companyId !== undefined) {
|
||||
return req.data.person.companyId;
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const main = async (params: {
|
||||
properties: Record<string, any>;
|
||||
recordId: string;
|
||||
userId: string;
|
||||
}): Promise<object | undefined> => {
|
||||
if (TWENTY_API_KEY === '') {
|
||||
console.log("Function exited as API key or URL hasn't been set properly");
|
||||
return {};
|
||||
}
|
||||
const { properties, recordId } = params;
|
||||
// Check if fields are created
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${TWENTY_URL}/metadata/objects`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
};
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
const objects = response.data.data.objects;
|
||||
const company_object = objects.find(
|
||||
(object: any) => object.nameSingular === 'company',
|
||||
);
|
||||
const company_last_interaction = company_object.fields.find(
|
||||
(field: any) => field.name === 'lastInteraction',
|
||||
);
|
||||
const company_interaction_status = company_object.fields.find(
|
||||
(field: any) => field.name === 'interactionStatus',
|
||||
);
|
||||
const person_object = objects.find(
|
||||
(object: any) => object.nameSingular === 'person',
|
||||
);
|
||||
const person_last_interaction = person_object.fields.find(
|
||||
(field: any) => field.name === 'lastInteraction',
|
||||
);
|
||||
const person_interaction_status = person_object.fields.find(
|
||||
(field: any) => field.name === 'interactionStatus',
|
||||
);
|
||||
// If not, create them
|
||||
if (company_last_interaction === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_last_interaction(company_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created company last interaction field');
|
||||
}
|
||||
}
|
||||
if (company_interaction_status === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_interaction_status(company_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created company interaction status field');
|
||||
}
|
||||
}
|
||||
if (person_last_interaction === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_last_interaction(person_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created person last interaction field');
|
||||
}
|
||||
}
|
||||
if (person_interaction_status === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_interaction_status(person_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created person interaction status field');
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the timestamp of message
|
||||
const messageDate = properties.after.receivedAt;
|
||||
const interactionStatus = calculateStatus(messageDate);
|
||||
|
||||
// Get the details of person and related company
|
||||
const messageOptions = {
|
||||
method: 'GET',
|
||||
url: `${TWENTY_URL}/messages/${recordId}?depth=1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
};
|
||||
const messageDetails = await axios.request(messageOptions);
|
||||
const peopleIds: string[] = [];
|
||||
for (const participant of messageDetails.data.messages
|
||||
.messageParticipants) {
|
||||
peopleIds.push(participant.personId);
|
||||
}
|
||||
|
||||
const companiesIds = [];
|
||||
for (const id of peopleIds) {
|
||||
companiesIds.push(await fetchRelatedCompanyId(id));
|
||||
}
|
||||
// Update the field value depending on the timestamp
|
||||
for (const id of peopleIds) {
|
||||
await updateInteractionStatus("people", id, messageDate, interactionStatus);
|
||||
}
|
||||
|
||||
for (const id of companiesIds) {
|
||||
await updateInteractionStatus("companies", id, messageDate, interactionStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.message);
|
||||
return {};
|
||||
}
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const config: FunctionConfig = {
|
||||
universalIdentifier: '683966a0-b60a-424e-86b1-7448c9191bde',
|
||||
name: 'test',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'f4f1e127-87f0-4dcf-99fe-8061adf5cbe6',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'message.created',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '4c17878f-b6b3-4d0a-8de6-967b1cb55002',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'message.updated',
|
||||
},
|
||||
],
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { calculateStatus } from '../shared/calculate-status';
|
||||
|
||||
const fetchAllPeople = async () => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
people: {
|
||||
__args: {
|
||||
filter: {
|
||||
/*
|
||||
lastInteraction: {
|
||||
is: 'NOT_NULL',
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
lastInteraction: true,
|
||||
interactionStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result.people) {
|
||||
throw new Error('Could not find any people');
|
||||
}
|
||||
return result.people.edges;
|
||||
}
|
||||
|
||||
const fetchAllCompanies = async () => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
companies: {
|
||||
__args: {
|
||||
filter: {
|
||||
/* how to fetch fields added in fields folder?
|
||||
lastInteraction: {
|
||||
is: 'NOT_NULL',
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
lastInteraction: true,
|
||||
interactionStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result.companies) {
|
||||
throw new Error('Could not find any companies');
|
||||
}
|
||||
return result.companies.edges;
|
||||
}
|
||||
|
||||
const updateCompany = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePerson = async (
|
||||
personId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updatePerson: {
|
||||
__args: {
|
||||
id: personId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updatePerson) {
|
||||
throw new Error(`Failed to update person ${personId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handler = async () => {
|
||||
const people = await fetchAllPeople();
|
||||
for (const person of people) {
|
||||
const interactionStatus = calculateStatus(person.node.lastInteraction as string);
|
||||
if (interactionStatus !== person.node.interactionStatus) {
|
||||
await updatePerson(person.node.id, {interactionStatus: interactionStatus});
|
||||
}
|
||||
}
|
||||
const companies = await fetchAllCompanies();
|
||||
for (const company of companies) {
|
||||
const interactionStatus = calculateStatus(company.node.lastInteraction as string);
|
||||
if (interactionStatus !== company.node.interactionStatus) {
|
||||
await updateCompany(company.node.id, {interactionStatus: interactionStatus});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'c79f1f30-f369-4264-9e5b-c183577bc709',
|
||||
name: 'on-cron-job',
|
||||
description:
|
||||
'Runs daily at midnight and updates all companies and people with correct interaction status',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
cronTriggerSettings: {
|
||||
pattern: '0 0 * * *', // runs daily at midnight
|
||||
},
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
DatabaseEventPayload,
|
||||
defineLogicFunction,
|
||||
ObjectRecordCreateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { calculateStatus } from '../shared/calculate-status';
|
||||
|
||||
|
||||
const fetchMessageParticipants = async (messageId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
messageParticipants: {
|
||||
__args: {
|
||||
filter: {
|
||||
messageId: {
|
||||
eq: messageId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
personId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let people: string[] = [];
|
||||
if (result.messageParticipants === undefined) {
|
||||
return people;
|
||||
}
|
||||
for (const person of result.messageParticipants.edges) {
|
||||
if (person.node.personId !== undefined) {
|
||||
people.push(person.node.personId);
|
||||
}
|
||||
}
|
||||
return people;
|
||||
};
|
||||
|
||||
const fetchRelatedCompany = async (personId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
people: {
|
||||
__args: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: personId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
company: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (
|
||||
result.people === undefined ||
|
||||
result.people.edges[0].node.company === undefined
|
||||
) {
|
||||
throw new Error(`Failed to fetch related company of person ${personId}`);
|
||||
}
|
||||
return result.people.edges[0].node.company.id;
|
||||
};
|
||||
|
||||
const updateCompany = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePerson = async (
|
||||
personId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updatePerson: {
|
||||
__args: {
|
||||
id: personId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updatePerson) {
|
||||
throw new Error(`Failed to update person ${personId}`);
|
||||
}
|
||||
};
|
||||
|
||||
type Message = {
|
||||
receivedAt: string;
|
||||
};
|
||||
|
||||
type MessageCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Message>
|
||||
>;
|
||||
|
||||
const handler = async (
|
||||
event: MessageCreatedEvent,
|
||||
): Promise<object | undefined> => {
|
||||
const { properties, recordId } = event;
|
||||
const interactionStatus = calculateStatus(properties.after.receivedAt);
|
||||
const peopleIds: string[] = [];
|
||||
peopleIds.push(...(await fetchMessageParticipants(recordId)));
|
||||
const updateData = {
|
||||
lastInteraction: properties.after.receivedAt,
|
||||
interactionStatus: interactionStatus,
|
||||
};
|
||||
for (const person of peopleIds) {
|
||||
const companyId = await fetchRelatedCompany(person);
|
||||
await updatePerson(person, updateData);
|
||||
await updateCompany(companyId, updateData);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '543432c2-6509-4ee9-90ec-15ffb4d7abfc',
|
||||
name: 'on-message-created',
|
||||
description: 'Triggered when new message is created',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'message.created',
|
||||
},
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
DatabaseEventPayload,
|
||||
defineLogicFunction,
|
||||
ObjectRecordCreateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { calculateStatus } from '../shared/calculate-status';
|
||||
|
||||
|
||||
const fetchMessageParticipants = async (messageId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
messageParticipants: {
|
||||
__args: {
|
||||
filter: {
|
||||
messageId: {
|
||||
eq: messageId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
personId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let people: string[] = [];
|
||||
if (result.messageParticipants === undefined) {
|
||||
return people;
|
||||
}
|
||||
for (const person of result.messageParticipants.edges) {
|
||||
if (person.node.personId !== undefined) {
|
||||
people.push(person.node.personId);
|
||||
}
|
||||
}
|
||||
return people;
|
||||
};
|
||||
|
||||
const fetchRelatedCompany = async (personId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
people: {
|
||||
__args: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: personId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
company: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (
|
||||
result.people === undefined ||
|
||||
result.people.edges[0].node.company === undefined
|
||||
) {
|
||||
throw new Error(`Failed to fetch related company of person ${personId}`);
|
||||
}
|
||||
return result.people.edges[0].node.company.id;
|
||||
};
|
||||
|
||||
const updateCompany = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePerson = async (
|
||||
personId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updatePerson: {
|
||||
__args: {
|
||||
id: personId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updatePerson) {
|
||||
throw new Error(`Failed to update person ${personId}`);
|
||||
}
|
||||
};
|
||||
|
||||
type Message = {
|
||||
receivedAt: string;
|
||||
};
|
||||
|
||||
type MessageCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Message>
|
||||
>;
|
||||
|
||||
const handler = async (
|
||||
event: MessageCreatedEvent,
|
||||
): Promise<object | undefined> => {
|
||||
const { properties, recordId } = event;
|
||||
const interactionStatus = calculateStatus(properties.after.receivedAt);
|
||||
const peopleIds: string[] = [];
|
||||
peopleIds.push(...(await fetchMessageParticipants(recordId)));
|
||||
const updateData = {
|
||||
lastInteraction: properties.after.receivedAt,
|
||||
interactionStatus: interactionStatus,
|
||||
};
|
||||
for (const person of peopleIds) {
|
||||
const companyId = await fetchRelatedCompany(person);
|
||||
await updatePerson(person, updateData);
|
||||
await updateCompany(companyId, updateData);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '9bfcb3e4-9119-4b65-b6e9-d395d0764ce5',
|
||||
name: 'on-message-updated',
|
||||
description: 'Triggered when message is updated',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'message.updated',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export const calculateStatus = (date: string) => {
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
const now = Date.now();
|
||||
const messageDate = Date.parse(date);
|
||||
const deltaTime = now - messageDate;
|
||||
return deltaTime < 7 * day
|
||||
? 'RECENT'
|
||||
: deltaTime < 30 * day
|
||||
? 'ACTIVE'
|
||||
: deltaTime < 90 * day
|
||||
? 'COOLING'
|
||||
: 'DORMANT';
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-and-install:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Deploy
|
||||
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
|
||||
- name: Install
|
||||
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
@@ -6,9 +6,16 @@ on:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -16,12 +23,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
- name: Spawn Twenty test instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
@@ -39,4 +45,4 @@ jobs:
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "0.8.0-canary.5",
|
||||
"twenty-sdk": "0.8.0-canary.5"
|
||||
"twenty-client-sdk": "0.9.0",
|
||||
"twenty-sdk": "0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
@@ -3,43 +3,51 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
|
||||
|
||||
beforeAll(async () => {
|
||||
const apiUrl = process.env.TWENTY_API_URL!;
|
||||
const token = process.env.TWENTY_API_KEY!;
|
||||
|
||||
if (!apiUrl || !token) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify(configFile, null, 2),
|
||||
CONFIG_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey: token },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
|
||||
});
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
import {
|
||||
definePostInstallLogicFunction,
|
||||
type InstallLogicFunctionPayload,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
console.log(
|
||||
'Post install logic function executed successfully!',
|
||||
payload.previousVersion,
|
||||
);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '8c726dcc-1709-4eac-aa8b-f99960a9ec1b',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
});
|
||||
|
||||
@@ -14,9 +14,10 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-and-install:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Deploy
|
||||
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
|
||||
- name: Install
|
||||
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
@@ -6,9 +6,16 @@ on:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -16,12 +23,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
- name: Spawn Twenty test instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
@@ -39,4 +45,4 @@ jobs:
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "0.8.0-canary.8",
|
||||
"twenty-sdk": "0.8.0-canary.8"
|
||||
"twenty-client-sdk": "0.9.0",
|
||||
"twenty-sdk": "0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
+3
-1
@@ -44,7 +44,9 @@ describe('App installation', () => {
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
`App uninstall failed: ${
|
||||
uninstallResult.error?.message ?? 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,43 +3,51 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
|
||||
|
||||
beforeAll(async () => {
|
||||
const apiUrl = process.env.TWENTY_API_URL!;
|
||||
const token = process.env.TWENTY_API_KEY!;
|
||||
|
||||
if (!apiUrl || !token) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify(configFile, null, 2),
|
||||
CONFIG_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey: token },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { definePostInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const SEED_POST_CARDS = [
|
||||
{
|
||||
name: 'Greetings from Paris',
|
||||
content: 'Wish you were here! The Eiffel Tower is breathtaking.',
|
||||
},
|
||||
{
|
||||
name: 'Hello from Tokyo',
|
||||
content: 'Cherry blossoms are in full bloom. Sending love!',
|
||||
},
|
||||
];
|
||||
|
||||
const handler = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createPostCards: {
|
||||
__args: { data: SEED_POST_CARDS as any },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
|
||||
return {};
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { definePreInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (params: any) => {
|
||||
console.log(
|
||||
`Pre-install logic function executed successfully with params`,
|
||||
params,
|
||||
);
|
||||
return {};
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'bf27f558-4ec6-481f-b76e-1dbcd05aef1f',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before migrations to set up the application.',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
@@ -51,28 +51,28 @@ export default defineObject({
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{
|
||||
id: 'a1b2c3d4-0001-4000-8000-000000000001',
|
||||
id: '8ab3abad-02e7-4670-9283-983d7fac7fe4',
|
||||
value: PostCardStatus.DRAFT,
|
||||
label: 'Draft',
|
||||
position: 0,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0002-4000-8000-000000000002',
|
||||
id: '2bcdd195-6c99-4d69-84b2-e2838ee54467',
|
||||
value: PostCardStatus.SENT,
|
||||
label: 'Sent',
|
||||
position: 1,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0003-4000-8000-000000000003',
|
||||
id: '918ff60c-c26e-4fae-8eba-3fbce04dc48b',
|
||||
value: PostCardStatus.DELIVERED,
|
||||
label: 'Delivered',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0004-4000-8000-000000000004',
|
||||
id: '3c91a653-5d31-4023-be6c-a69c68d21233',
|
||||
value: PostCardStatus.RETURNED,
|
||||
label: 'Returned',
|
||||
position: 3,
|
||||
|
||||
@@ -33,7 +33,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
fieldUniversalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
canUpdateFieldValue: true,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
|
||||
@@ -14,8 +14,10 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3317,8 +3317,8 @@ __metadata:
|
||||
oxlint: "npm:^0.16.0"
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
twenty-client-sdk: "npm:0.8.0-canary.8"
|
||||
twenty-sdk: "npm:0.8.0-canary.8"
|
||||
twenty-client-sdk: "npm:0.9.0"
|
||||
twenty-sdk: "npm:0.9.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
vite-tsconfig-paths: "npm:^4.2.1"
|
||||
vitest: "npm:^3.1.1"
|
||||
@@ -4059,21 +4059,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-client-sdk@npm:0.8.0-canary.8":
|
||||
version: 0.8.0-canary.8
|
||||
resolution: "twenty-client-sdk@npm:0.8.0-canary.8"
|
||||
"twenty-client-sdk@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "twenty-client-sdk@npm:0.9.0"
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
esbuild: "npm:^0.25.0"
|
||||
graphql: "npm:^16.8.1"
|
||||
checksum: 10c0/acb5bf952a9729811235a3474ccb4db82630c53a2caed03176bfb4f442576ee2ef7e625886f63714e2534f7ec83b03d83e4c0b1170a0eb816c531acfc2c98010
|
||||
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@npm:0.8.0-canary.8":
|
||||
version: 0.8.0-canary.8
|
||||
resolution: "twenty-sdk@npm:0.8.0-canary.8"
|
||||
"twenty-sdk@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "twenty-sdk@npm:0.9.0"
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
@@ -4093,7 +4093,7 @@ __metadata:
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
twenty-client-sdk: "npm:0.8.0-canary.8"
|
||||
twenty-client-sdk: "npm:0.9.0"
|
||||
typescript: "npm:^5.9.2"
|
||||
uuid: "npm:^13.0.0"
|
||||
vite: "npm:^7.0.0"
|
||||
@@ -4101,7 +4101,7 @@ __metadata:
|
||||
zod: "npm:^4.1.11"
|
||||
bin:
|
||||
twenty: dist/cli.cjs
|
||||
checksum: 10c0/a22cf071f41c19d769e68a995912a0bda0f087bd9a295534bf9e545544f6dfc84f284a477f9011fc135f2a44de9372b977a66de06454e760822119e76921ea69
|
||||
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
"twenty-sdk": "latest",
|
||||
"twenty-client-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
"twenty-sdk": "latest",
|
||||
"twenty-client-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
"twenty-sdk": "latest",
|
||||
"twenty-client-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -10,25 +10,31 @@ export default defineField({
|
||||
description: 'Post card category',
|
||||
options: [
|
||||
{
|
||||
id: 'c1d2e3f4-0001-4000-8000-000000000001',
|
||||
id: 'cd751c81-787d-4581-bc51-efe43f0050a7',
|
||||
value: 'PERSONAL',
|
||||
label: 'Personal',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: 'c1d2e3f4-0002-4000-8000-000000000002',
|
||||
id: 'eec437ca-5beb-41a9-a826-c9a5eca2eef4',
|
||||
value: 'BUSINESS',
|
||||
label: 'Business',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'c1d2e3f4-0003-4000-8000-000000000003',
|
||||
id: 'a5baa37d-1047-4972-b6b8-7faae0e3eac1',
|
||||
value: 'PROMOTIONAL',
|
||||
label: 'Promotional',
|
||||
color: 'orange',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
value: 'OTHER',
|
||||
label: 'Other',
|
||||
color: 'gray',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ enum PostCardStatus {
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
LOST = 'LOST',
|
||||
}
|
||||
|
||||
export const POST_CARD_UNIVERSAL_IDENTIFIER =
|
||||
@@ -53,33 +54,40 @@ export default defineObject({
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{
|
||||
id: 'a1b2c3d4-0001-4000-8000-000000000001',
|
||||
id: '1b008e19-1e59-4a07-b187-65a20e547c4e',
|
||||
value: PostCardStatus.DRAFT,
|
||||
label: 'Draft',
|
||||
position: 0,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0002-4000-8000-000000000002',
|
||||
id: '452b9d40-889c-4342-9697-98319394db04',
|
||||
value: PostCardStatus.SENT,
|
||||
label: 'Sent',
|
||||
position: 1,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0003-4000-8000-000000000003',
|
||||
id: 'c2ed0b8c-a3ed-4383-aef9-e0441267bcfe',
|
||||
value: PostCardStatus.DELIVERED,
|
||||
label: 'Delivered',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0004-4000-8000-000000000004',
|
||||
id: 'c57a5e08-7ef7-49b8-87e6-32d720d22802',
|
||||
value: PostCardStatus.RETURNED,
|
||||
label: 'Returned',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
// No id — exercises addMissingFieldOptionIds in the object branch
|
||||
value: PostCardStatus.LOST,
|
||||
label: 'Lost',
|
||||
position: 4,
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
name: 'status',
|
||||
},
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
@@ -1 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,11 +0,0 @@
|
||||
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
|
||||
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "my-twenty-app",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty-app"
|
||||
],
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "0.8.0-canary.8",
|
||||
"twenty-sdk": "0.8.0-canary.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
`Build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(
|
||||
`Deploy failed: ${deployResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(
|
||||
`Install failed: ${installResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(application: { universalIdentifier: string }) =>
|
||||
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${TWENTY_API_URL} returned ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
JSON.stringify(configFile, null, 2),
|
||||
);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
APP_DESCRIPTION,
|
||||
APP_DISPLAY_NAME,
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: APP_DISPLAY_NAME,
|
||||
description: APP_DESCRIPTION,
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
export const APP_DISPLAY_NAME = 'My twenty app';
|
||||
export const APP_DESCRIPTION = '';
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER = '83a4b244-c80d-4923-b0d5-ae2406e83072';
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'a1a28134-04d1-43ac-b8cc-b547e8cf97ba';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: `${APP_DISPLAY_NAME} default function role`,
|
||||
description: `${APP_DISPLAY_NAME} default function role`,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.integration-test.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_KEY:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik',
|
||||
},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "0.8.0-canary.9",
|
||||
"version": "1.22.0-canary.3",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql';
|
||||
import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql';
|
||||
|
||||
import { generateMetadataClient } from '../src/generate/generate-metadata-client';
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ type ViewField {
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
isOverridden: Boolean!
|
||||
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
|
||||
}
|
||||
|
||||
enum AggregateOperations {
|
||||
@@ -693,7 +693,7 @@ type ViewFieldGroup {
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
viewFields: [ViewField!]!
|
||||
isOverridden: Boolean!
|
||||
isOverridden: Boolean! @deprecated(reason: "isOverridden is deprecated")
|
||||
}
|
||||
|
||||
type View {
|
||||
@@ -896,10 +896,11 @@ type PageLayoutWidget {
|
||||
position: PageLayoutWidgetPosition
|
||||
configuration: WidgetConfiguration!
|
||||
conditionalDisplay: JSON
|
||||
conditionalAvailabilityExpression: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
isOverridden: Boolean!
|
||||
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
|
||||
}
|
||||
|
||||
enum WidgetType {
|
||||
@@ -921,6 +922,7 @@ enum WidgetType {
|
||||
WORKFLOW_RUN
|
||||
FRONT_COMPONENT
|
||||
RECORD_TABLE
|
||||
EMAIL_THREAD
|
||||
}
|
||||
|
||||
union PageLayoutWidgetPosition = PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition | PageLayoutWidgetCanvasPosition
|
||||
@@ -948,7 +950,7 @@ type PageLayoutWidgetCanvasPosition {
|
||||
layoutMode: PageLayoutTabLayoutMode!
|
||||
}
|
||||
|
||||
union WidgetConfiguration = AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration
|
||||
union WidgetConfiguration = AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration
|
||||
|
||||
type AggregateChartConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
@@ -989,6 +991,7 @@ enum WidgetConfigurationType {
|
||||
WORKFLOW_RUN
|
||||
FRONT_COMPONENT
|
||||
RECORD_TABLE
|
||||
EMAIL_THREAD
|
||||
}
|
||||
|
||||
type StandaloneRichTextConfiguration {
|
||||
@@ -1154,6 +1157,10 @@ type EmailsConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
}
|
||||
|
||||
type EmailThreadConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
}
|
||||
|
||||
type FieldConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
fieldMetadataId: String!
|
||||
@@ -1163,6 +1170,7 @@ type FieldConfiguration {
|
||||
"""Display mode for field configuration widgets"""
|
||||
enum FieldDisplayMode {
|
||||
CARD
|
||||
EDITOR
|
||||
FIELD
|
||||
VIEW
|
||||
}
|
||||
@@ -1227,7 +1235,7 @@ type PageLayoutTab {
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
isOverridden: Boolean!
|
||||
isOverridden: Boolean @deprecated(reason: "isOverridden is deprecated")
|
||||
}
|
||||
|
||||
type PageLayout {
|
||||
@@ -1628,12 +1636,15 @@ type ClientAIModelConfig {
|
||||
modelFamily: ModelFamily
|
||||
modelFamilyLabel: String
|
||||
sdkPackage: String
|
||||
inputCostPerMillionTokensInCredits: Float!
|
||||
outputCostPerMillionTokensInCredits: Float!
|
||||
inputCostPerMillionTokens: Float
|
||||
outputCostPerMillionTokens: Float
|
||||
nativeCapabilities: NativeModelCapabilities
|
||||
isDeprecated: Boolean
|
||||
isRecommended: Boolean
|
||||
providerName: String
|
||||
providerLabel: String
|
||||
contextWindowTokens: Float
|
||||
maxOutputTokens: Float
|
||||
dataResidency: String
|
||||
}
|
||||
|
||||
@@ -1723,17 +1734,15 @@ enum FeatureFlagKey {
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_AI_ENABLED
|
||||
IS_COMMAND_MENU_ITEM_ENABLED
|
||||
IS_MARKETPLACE_ENABLED
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
IS_PUBLIC_DOMAIN_ENABLED
|
||||
IS_EMAILING_DOMAIN_ENABLED
|
||||
IS_JUNCTION_RELATIONS_ENABLED
|
||||
IS_DRAFT_EMAIL_ENABLED
|
||||
IS_USAGE_ANALYTICS_ENABLED
|
||||
IS_RICH_TEXT_V1_MIGRATED
|
||||
IS_DIRECT_GRAPHQL_EXECUTION_ENABLED
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED
|
||||
IS_RICH_TEXT_V1_MIGRATED
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
|
||||
IS_RECORD_TABLE_WIDGET_ENABLED
|
||||
IS_DATASOURCE_MIGRATED
|
||||
}
|
||||
@@ -2542,7 +2551,7 @@ type ConnectedImapSmtpCaldavAccount {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
accountOwnerId: UUID!
|
||||
userWorkspaceId: UUID!
|
||||
connectionParameters: ImapSmtpCaldavConnectionParameters
|
||||
}
|
||||
|
||||
@@ -2570,6 +2579,7 @@ type CommandMenuItem {
|
||||
position: Float!
|
||||
isPinned: Boolean!
|
||||
availabilityType: CommandMenuItemAvailabilityType!
|
||||
payload: CommandMenuItemPayload
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
@@ -2596,19 +2606,11 @@ enum EngineComponentKey {
|
||||
SEE_DELETED_RECORDS
|
||||
CREATE_NEW_VIEW
|
||||
HIDE_DELETED_RECORDS
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
EDIT_RECORD_PAGE_LAYOUT
|
||||
EDIT_DASHBOARD_LAYOUT
|
||||
SAVE_DASHBOARD_LAYOUT
|
||||
CANCEL_DASHBOARD_LAYOUT
|
||||
DUPLICATE_DASHBOARD
|
||||
GO_TO_WORKFLOWS
|
||||
ACTIVATE_WORKFLOW
|
||||
DEACTIVATE_WORKFLOW
|
||||
DISCARD_DRAFT_WORKFLOW
|
||||
@@ -2619,7 +2621,6 @@ enum EngineComponentKey {
|
||||
ADD_NODE_WORKFLOW
|
||||
TIDY_UP_WORKFLOW
|
||||
DUPLICATE_WORKFLOW
|
||||
GO_TO_RUNS
|
||||
SEE_VERSION_WORKFLOW_RUN
|
||||
SEE_WORKFLOW_WORKFLOW_RUN
|
||||
STOP_WORKFLOW_RUN
|
||||
@@ -2631,8 +2632,20 @@ enum EngineComponentKey {
|
||||
SEARCH_RECORDS_FALLBACK
|
||||
ASK_AI
|
||||
VIEW_PREVIOUS_AI_CHATS
|
||||
NAVIGATION
|
||||
TRIGGER_WORKFLOW_VERSION
|
||||
FRONT_COMPONENT_RENDERER
|
||||
REPLY_TO_EMAIL_THREAD
|
||||
COMPOSE_EMAIL
|
||||
GO_TO_PEOPLE
|
||||
GO_TO_COMPANIES
|
||||
GO_TO_DASHBOARDS
|
||||
GO_TO_OPPORTUNITIES
|
||||
GO_TO_SETTINGS
|
||||
GO_TO_TASKS
|
||||
GO_TO_NOTES
|
||||
GO_TO_WORKFLOWS
|
||||
GO_TO_RUNS
|
||||
DELETE_SINGLE_RECORD
|
||||
DELETE_MULTIPLE_RECORDS
|
||||
RESTORE_SINGLE_RECORD
|
||||
@@ -2650,6 +2663,16 @@ enum CommandMenuItemAvailabilityType {
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
union CommandMenuItemPayload = PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload
|
||||
|
||||
type PathCommandMenuItemPayload {
|
||||
path: String!
|
||||
}
|
||||
|
||||
type ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: UUID!
|
||||
}
|
||||
|
||||
type ToolIndexEntry {
|
||||
name: String!
|
||||
description: String!
|
||||
@@ -2756,6 +2779,54 @@ type DuplicatedDashboard {
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type ConnectedAccountDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
connectionParameters: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type PublicConnectionParametersOutput {
|
||||
host: String!
|
||||
port: Float!
|
||||
username: String
|
||||
secure: Boolean
|
||||
}
|
||||
|
||||
type PublicImapSmtpCaldavConnectionParameters {
|
||||
IMAP: PublicConnectionParametersOutput
|
||||
SMTP: PublicConnectionParametersOutput
|
||||
CALDAV: PublicConnectionParametersOutput
|
||||
}
|
||||
|
||||
type ConnectedAccountPublicDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
connectionParameters: PublicImapSmtpCaldavConnectionParameters
|
||||
}
|
||||
|
||||
type SendEmailOutput {
|
||||
success: Boolean!
|
||||
error: String
|
||||
}
|
||||
|
||||
type EventLogRecord {
|
||||
event: String!
|
||||
timestamp: DateTime!
|
||||
@@ -2924,20 +2995,6 @@ enum CalendarChannelContactAutoCreationPolicy {
|
||||
NONE
|
||||
}
|
||||
|
||||
type ConnectedAccountDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
lastSignedInAt: DateTime
|
||||
userWorkspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type MessageChannel {
|
||||
id: UUID!
|
||||
visibility: MessageChannelVisibility!
|
||||
@@ -3013,7 +3070,7 @@ type MessageFolder {
|
||||
name: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
parentFolderId: UUID
|
||||
parentFolderId: String
|
||||
externalId: String
|
||||
pendingSyncAction: MessageFolderPendingSyncAction!
|
||||
messageChannelId: UUID!
|
||||
@@ -3183,6 +3240,7 @@ type Query {
|
||||
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myConnectedAccounts: [ConnectedAccountDTO!]!
|
||||
connectedAccountById(id: UUID!): ConnectedAccountPublicDTO
|
||||
connectedAccounts: [ConnectedAccountDTO!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
webhooks: [Webhook!]!
|
||||
@@ -3220,7 +3278,6 @@ type Query {
|
||||
findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats!
|
||||
findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariable!]!
|
||||
applicationRegistrationTarballUrl(id: String!): String
|
||||
getApplicationShareLink(id: String!): String!
|
||||
currentUser: User!
|
||||
currentWorkspace: Workspace!
|
||||
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
|
||||
@@ -3328,6 +3385,7 @@ enum EventLogTable {
|
||||
PAGEVIEW
|
||||
OBJECT_EVENT
|
||||
USAGE_EVENT
|
||||
APPLICATION_LOG
|
||||
}
|
||||
|
||||
input EventLogFiltersInput {
|
||||
@@ -3370,6 +3428,7 @@ enum UsageOperationType {
|
||||
AI_WORKFLOW_TOKEN
|
||||
WORKFLOW_EXECUTION
|
||||
CODE_EXECUTION
|
||||
WEB_SEARCH
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -3381,6 +3440,7 @@ type Mutation {
|
||||
updateNavigationMenuItem(input: UpdateOneNavigationMenuItemInput!): NavigationMenuItem!
|
||||
deleteManyNavigationMenuItems(ids: [UUID!]!): [NavigationMenuItem!]!
|
||||
deleteNavigationMenuItem(id: UUID!): NavigationMenuItem!
|
||||
uploadEmailAttachmentFile(file: Upload!): FileWithSignedUrl!
|
||||
uploadAIChatFile(file: Upload!): FileWithSignedUrl!
|
||||
uploadWorkflowFile(file: Upload!): FileWithSignedUrl!
|
||||
uploadWorkspaceLogo(file: Upload!): FileWithSignedUrl!
|
||||
@@ -3445,6 +3505,8 @@ type Mutation {
|
||||
updatePageLayout(id: String!, input: UpdatePageLayoutInput!): PageLayout!
|
||||
destroyPageLayout(id: String!): Boolean!
|
||||
updatePageLayoutWithTabsAndWidgets(id: String!, input: UpdatePageLayoutWithTabsInput!): PageLayout!
|
||||
resetPageLayoutWidgetToDefault(id: String!): PageLayoutWidget!
|
||||
resetPageLayoutTabToDefault(id: String!): PageLayoutTab!
|
||||
createPageLayoutWidget(input: CreatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
updatePageLayoutWidget(id: String!, input: UpdatePageLayoutWidgetInput!): PageLayoutWidget!
|
||||
destroyPageLayoutWidget(id: String!): Boolean!
|
||||
@@ -3492,7 +3554,7 @@ type Mutation {
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String): SendChatMessageResult!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
|
||||
stopAgentChatStream(threadId: UUID!): Boolean!
|
||||
deleteQueuedChatMessage(messageId: UUID!): Boolean!
|
||||
createSkill(input: CreateSkillInput!): Skill!
|
||||
@@ -3545,13 +3607,16 @@ type Mutation {
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
|
||||
impersonate(userId: UUID!, workspaceId: UUID!): Impersonate!
|
||||
sendEmail(input: SendEmailInput!): SendEmailOutput!
|
||||
startChannelSync(connectedAccountId: UUID!): ChannelSyncSuccess!
|
||||
saveImapSmtpCaldavAccount(accountOwnerId: UUID!, handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess!
|
||||
updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag!
|
||||
userLookupAdminPanel(userIdentifier: String!): UserLookup!
|
||||
updateWorkspaceFeatureFlag(workspaceId: UUID!, featureFlag: String!, value: Boolean!): Boolean!
|
||||
setAdminAiModelEnabled(modelId: String!, enabled: Boolean!): Boolean!
|
||||
setAdminAiModelsEnabled(modelIds: [String!]!, enabled: Boolean!): Boolean!
|
||||
setAdminAiModelRecommended(modelId: String!, recommended: Boolean!): Boolean!
|
||||
setAdminAiModelsRecommended(modelIds: [String!]!, recommended: Boolean!): Boolean!
|
||||
setAdminDefaultAiModel(role: AiModelRole!, modelId: String!): Boolean!
|
||||
createDatabaseConfigVariable(key: String!, value: JSON!): Boolean!
|
||||
updateDatabaseConfigVariable(key: String!, value: JSON!): Boolean!
|
||||
@@ -3948,6 +4013,7 @@ input UpdatePageLayoutWidgetWithIdInput {
|
||||
position: JSON
|
||||
configuration: JSON
|
||||
conditionalDisplay: JSON
|
||||
conditionalAvailabilityExpression: String
|
||||
}
|
||||
|
||||
input GridPositionInput {
|
||||
@@ -3968,6 +4034,7 @@ input CreatePageLayoutWidgetInput {
|
||||
}
|
||||
|
||||
input UpdatePageLayoutWidgetInput {
|
||||
pageLayoutTabId: UUID
|
||||
title: String
|
||||
type: WidgetType
|
||||
objectMetadataId: UUID
|
||||
@@ -3975,6 +4042,7 @@ input UpdatePageLayoutWidgetInput {
|
||||
position: JSON
|
||||
configuration: JSON
|
||||
conditionalDisplay: JSON
|
||||
conditionalAvailabilityExpression: String
|
||||
}
|
||||
|
||||
input CreateLogicFunctionFromSourceInput {
|
||||
@@ -4034,6 +4102,7 @@ input CreateCommandMenuItemInput {
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
payload: JSON
|
||||
}
|
||||
|
||||
input UpdateCommandMenuItemInput {
|
||||
@@ -4515,6 +4584,22 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
input SendEmailInput {
|
||||
connectedAccountId: String!
|
||||
to: String!
|
||||
cc: String
|
||||
bcc: String
|
||||
subject: String!
|
||||
body: String!
|
||||
inReplyTo: String
|
||||
files: [SendEmailAttachmentInput!]
|
||||
}
|
||||
|
||||
input SendEmailAttachmentInput {
|
||||
id: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
input EmailAccountConnectionParameters {
|
||||
IMAP: ConnectionParameters
|
||||
SMTP: ConnectionParameters
|
||||
@@ -4579,6 +4664,7 @@ enum FileFolder {
|
||||
FilesField
|
||||
Dependencies
|
||||
Workflow
|
||||
EmailAttachment
|
||||
AppTarball
|
||||
GeneratedSdkClient
|
||||
}
|
||||
|
||||
@@ -416,7 +416,8 @@ export interface ViewField {
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
isOverridden: Scalars['Boolean']
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: Scalars['Boolean']
|
||||
__typename: 'ViewField'
|
||||
}
|
||||
|
||||
@@ -493,6 +494,7 @@ export interface ViewFieldGroup {
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
viewFields: ViewField[]
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden: Scalars['Boolean']
|
||||
__typename: 'ViewFieldGroup'
|
||||
}
|
||||
@@ -669,14 +671,16 @@ export interface PageLayoutWidget {
|
||||
position?: PageLayoutWidgetPosition
|
||||
configuration: WidgetConfiguration
|
||||
conditionalDisplay?: Scalars['JSON']
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
isOverridden: Scalars['Boolean']
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: Scalars['Boolean']
|
||||
__typename: 'PageLayoutWidget'
|
||||
}
|
||||
|
||||
export type WidgetType = 'VIEW' | 'IFRAME' | 'FIELD' | 'FIELDS' | 'GRAPH' | 'STANDALONE_RICH_TEXT' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE'
|
||||
export type WidgetType = 'VIEW' | 'IFRAME' | 'FIELD' | 'FIELDS' | 'GRAPH' | 'STANDALONE_RICH_TEXT' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
|
||||
|
||||
export type PageLayoutWidgetPosition = (PageLayoutWidgetGridPosition | PageLayoutWidgetVerticalListPosition | PageLayoutWidgetCanvasPosition) & { __isUnion?: true }
|
||||
|
||||
@@ -702,7 +706,7 @@ export interface PageLayoutWidgetCanvasPosition {
|
||||
__typename: 'PageLayoutWidgetCanvasPosition'
|
||||
}
|
||||
|
||||
export type WidgetConfiguration = (AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration) & { __isUnion?: true }
|
||||
export type WidgetConfiguration = (AggregateChartConfiguration | StandaloneRichTextConfiguration | PieChartConfiguration | LineChartConfiguration | IframeConfiguration | GaugeChartConfiguration | BarChartConfiguration | CalendarConfiguration | FrontComponentConfiguration | EmailsConfiguration | EmailThreadConfiguration | FieldConfiguration | FieldRichTextConfiguration | FieldsConfiguration | FilesConfiguration | NotesConfiguration | TasksConfiguration | TimelineConfiguration | ViewConfiguration | RecordTableConfiguration | WorkflowConfiguration | WorkflowRunConfiguration | WorkflowVersionConfiguration) & { __isUnion?: true }
|
||||
|
||||
export interface AggregateChartConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
@@ -721,7 +725,7 @@ export interface AggregateChartConfiguration {
|
||||
__typename: 'AggregateChartConfiguration'
|
||||
}
|
||||
|
||||
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'GAUGE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE'
|
||||
export type WidgetConfigurationType = 'AGGREGATE_CHART' | 'GAUGE_CHART' | 'PIE_CHART' | 'BAR_CHART' | 'LINE_CHART' | 'IFRAME' | 'STANDALONE_RICH_TEXT' | 'VIEW' | 'FIELD' | 'FIELDS' | 'TIMELINE' | 'TASKS' | 'NOTES' | 'FILES' | 'EMAILS' | 'CALENDAR' | 'FIELD_RICH_TEXT' | 'WORKFLOW' | 'WORKFLOW_VERSION' | 'WORKFLOW_RUN' | 'FRONT_COMPONENT' | 'RECORD_TABLE' | 'EMAIL_THREAD'
|
||||
|
||||
export interface StandaloneRichTextConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
@@ -869,6 +873,11 @@ export interface EmailsConfiguration {
|
||||
__typename: 'EmailsConfiguration'
|
||||
}
|
||||
|
||||
export interface EmailThreadConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
__typename: 'EmailThreadConfiguration'
|
||||
}
|
||||
|
||||
export interface FieldConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
fieldMetadataId: Scalars['String']
|
||||
@@ -878,7 +887,7 @@ export interface FieldConfiguration {
|
||||
|
||||
|
||||
/** Display mode for field configuration widgets */
|
||||
export type FieldDisplayMode = 'CARD' | 'FIELD' | 'VIEW'
|
||||
export type FieldDisplayMode = 'CARD' | 'EDITOR' | 'FIELD' | 'VIEW'
|
||||
|
||||
export interface FieldRichTextConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
@@ -951,7 +960,8 @@ export interface PageLayoutTab {
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
deletedAt?: Scalars['DateTime']
|
||||
isOverridden: Scalars['Boolean']
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: Scalars['Boolean']
|
||||
__typename: 'PageLayoutTab'
|
||||
}
|
||||
|
||||
@@ -1336,12 +1346,15 @@ export interface ClientAIModelConfig {
|
||||
modelFamily?: ModelFamily
|
||||
modelFamilyLabel?: Scalars['String']
|
||||
sdkPackage?: Scalars['String']
|
||||
inputCostPerMillionTokensInCredits: Scalars['Float']
|
||||
outputCostPerMillionTokensInCredits: Scalars['Float']
|
||||
inputCostPerMillionTokens?: Scalars['Float']
|
||||
outputCostPerMillionTokens?: Scalars['Float']
|
||||
nativeCapabilities?: NativeModelCapabilities
|
||||
isDeprecated?: Scalars['Boolean']
|
||||
isRecommended?: Scalars['Boolean']
|
||||
providerName?: Scalars['String']
|
||||
providerLabel?: Scalars['String']
|
||||
contextWindowTokens?: Scalars['Float']
|
||||
maxOutputTokens?: Scalars['Float']
|
||||
dataResidency?: Scalars['String']
|
||||
__typename: 'ClientAIModelConfig'
|
||||
}
|
||||
@@ -1424,7 +1437,7 @@ export interface PublicFeatureFlag {
|
||||
__typename: 'PublicFeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_USAGE_ANALYTICS_ENABLED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_AI_ENABLED' | 'IS_COMMAND_MENU_ITEM_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_DRAFT_EMAIL_ENABLED' | 'IS_CONNECTED_ACCOUNT_MIGRATED' | 'IS_RICH_TEXT_V1_MIGRATED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_RECORD_TABLE_WIDGET_ENABLED' | 'IS_DATASOURCE_MIGRATED'
|
||||
|
||||
export interface ClientConfigMaintenanceMode {
|
||||
startAt: Scalars['DateTime']
|
||||
@@ -2250,7 +2263,7 @@ export interface ConnectedImapSmtpCaldavAccount {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
accountOwnerId: Scalars['UUID']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParameters
|
||||
__typename: 'ConnectedImapSmtpCaldavAccount'
|
||||
}
|
||||
@@ -2281,6 +2294,7 @@ export interface CommandMenuItem {
|
||||
position: Scalars['Float']
|
||||
isPinned: Scalars['Boolean']
|
||||
availabilityType: CommandMenuItemAvailabilityType
|
||||
payload?: CommandMenuItemPayload
|
||||
hotKeys?: Scalars['String'][]
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
availabilityObjectMetadataId?: Scalars['UUID']
|
||||
@@ -2290,10 +2304,22 @@ export interface CommandMenuItem {
|
||||
__typename: 'CommandMenuItem'
|
||||
}
|
||||
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'GO_TO_WORKFLOWS' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'GO_TO_RUNS' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
|
||||
|
||||
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'RECORD_SELECTION' | 'FALLBACK'
|
||||
|
||||
export type CommandMenuItemPayload = (PathCommandMenuItemPayload | ObjectMetadataCommandMenuItemPayload) & { __isUnion?: true }
|
||||
|
||||
export interface PathCommandMenuItemPayload {
|
||||
path: Scalars['String']
|
||||
__typename: 'PathCommandMenuItemPayload'
|
||||
}
|
||||
|
||||
export interface ObjectMetadataCommandMenuItemPayload {
|
||||
objectMetadataItemId: Scalars['UUID']
|
||||
__typename: 'ObjectMetadataCommandMenuItemPayload'
|
||||
}
|
||||
|
||||
export interface ToolIndexEntry {
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
@@ -2411,6 +2437,59 @@ export interface DuplicatedDashboard {
|
||||
__typename: 'DuplicatedDashboard'
|
||||
}
|
||||
|
||||
export interface ConnectedAccountDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParameters
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ConnectedAccountDTO'
|
||||
}
|
||||
|
||||
export interface PublicConnectionParametersOutput {
|
||||
host: Scalars['String']
|
||||
port: Scalars['Float']
|
||||
username?: Scalars['String']
|
||||
secure?: Scalars['Boolean']
|
||||
__typename: 'PublicConnectionParametersOutput'
|
||||
}
|
||||
|
||||
export interface PublicImapSmtpCaldavConnectionParameters {
|
||||
IMAP?: PublicConnectionParametersOutput
|
||||
SMTP?: PublicConnectionParametersOutput
|
||||
CALDAV?: PublicConnectionParametersOutput
|
||||
__typename: 'PublicImapSmtpCaldavConnectionParameters'
|
||||
}
|
||||
|
||||
export interface ConnectedAccountPublicDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParameters
|
||||
__typename: 'ConnectedAccountPublicDTO'
|
||||
}
|
||||
|
||||
export interface SendEmailOutput {
|
||||
success: Scalars['Boolean']
|
||||
error?: Scalars['String']
|
||||
__typename: 'SendEmailOutput'
|
||||
}
|
||||
|
||||
export interface EventLogRecord {
|
||||
event: Scalars['String']
|
||||
timestamp: Scalars['DateTime']
|
||||
@@ -2570,21 +2649,6 @@ export type CalendarChannelVisibility = 'METADATA' | 'SHARE_EVERYTHING'
|
||||
|
||||
export type CalendarChannelContactAutoCreationPolicy = 'AS_PARTICIPANT_AND_ORGANIZER' | 'AS_PARTICIPANT' | 'AS_ORGANIZER' | 'NONE'
|
||||
|
||||
export interface ConnectedAccountDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ConnectedAccountDTO'
|
||||
}
|
||||
|
||||
export interface MessageChannel {
|
||||
id: Scalars['UUID']
|
||||
visibility: MessageChannelVisibility
|
||||
@@ -2628,7 +2692,7 @@ export interface MessageFolder {
|
||||
name?: Scalars['String']
|
||||
isSentFolder: Scalars['Boolean']
|
||||
isSynced: Scalars['Boolean']
|
||||
parentFolderId?: Scalars['UUID']
|
||||
parentFolderId?: Scalars['String']
|
||||
externalId?: Scalars['String']
|
||||
pendingSyncAction: MessageFolderPendingSyncAction
|
||||
messageChannelId: Scalars['UUID']
|
||||
@@ -2746,6 +2810,7 @@ export interface Query {
|
||||
myMessageFolders: MessageFolder[]
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountDTO[]
|
||||
connectedAccountById?: ConnectedAccountPublicDTO
|
||||
connectedAccounts: ConnectedAccountDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
webhooks: Webhook[]
|
||||
@@ -2774,7 +2839,6 @@ export interface Query {
|
||||
findApplicationRegistrationStats: ApplicationRegistrationStats
|
||||
findApplicationRegistrationVariables: ApplicationRegistrationVariable[]
|
||||
applicationRegistrationTarballUrl?: Scalars['String']
|
||||
getApplicationShareLink: Scalars['String']
|
||||
currentUser: User
|
||||
currentWorkspace: Workspace
|
||||
getPublicWorkspaceDataByDomain: PublicWorkspaceData
|
||||
@@ -2817,9 +2881,9 @@ export type SortDirection = 'ASC' | 'DESC'
|
||||
/** Sort Nulls Options */
|
||||
export type SortNulls = 'NULLS_FIRST' | 'NULLS_LAST'
|
||||
|
||||
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT'
|
||||
export type EventLogTable = 'WORKSPACE_EVENT' | 'PAGEVIEW' | 'OBJECT_EVENT' | 'USAGE_EVENT' | 'APPLICATION_LOG'
|
||||
|
||||
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION'
|
||||
export type UsageOperationType = 'AI_CHAT_TOKEN' | 'AI_WORKFLOW_TOKEN' | 'WORKFLOW_EXECUTION' | 'CODE_EXECUTION' | 'WEB_SEARCH'
|
||||
|
||||
export interface Mutation {
|
||||
addQueryToEventStream: Scalars['Boolean']
|
||||
@@ -2830,6 +2894,7 @@ export interface Mutation {
|
||||
updateNavigationMenuItem: NavigationMenuItem
|
||||
deleteManyNavigationMenuItems: NavigationMenuItem[]
|
||||
deleteNavigationMenuItem: NavigationMenuItem
|
||||
uploadEmailAttachmentFile: FileWithSignedUrl
|
||||
uploadAIChatFile: FileWithSignedUrl
|
||||
uploadWorkflowFile: FileWithSignedUrl
|
||||
uploadWorkspaceLogo: FileWithSignedUrl
|
||||
@@ -2894,6 +2959,8 @@ export interface Mutation {
|
||||
updatePageLayout: PageLayout
|
||||
destroyPageLayout: Scalars['Boolean']
|
||||
updatePageLayoutWithTabsAndWidgets: PageLayout
|
||||
resetPageLayoutWidgetToDefault: PageLayoutWidget
|
||||
resetPageLayoutTabToDefault: PageLayoutTab
|
||||
createPageLayoutWidget: PageLayoutWidget
|
||||
updatePageLayoutWidget: PageLayoutWidget
|
||||
destroyPageLayoutWidget: Scalars['Boolean']
|
||||
@@ -2994,13 +3061,16 @@ export interface Mutation {
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
editSSOIdentityProvider: EditSso
|
||||
impersonate: Impersonate
|
||||
sendEmail: SendEmailOutput
|
||||
startChannelSync: ChannelSyncSuccess
|
||||
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess
|
||||
updateLabPublicFeatureFlag: FeatureFlag
|
||||
userLookupAdminPanel: UserLookup
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean']
|
||||
setAdminAiModelEnabled: Scalars['Boolean']
|
||||
setAdminAiModelsEnabled: Scalars['Boolean']
|
||||
setAdminAiModelRecommended: Scalars['Boolean']
|
||||
setAdminAiModelsRecommended: Scalars['Boolean']
|
||||
setAdminDefaultAiModel: Scalars['Boolean']
|
||||
createDatabaseConfigVariable: Scalars['Boolean']
|
||||
updateDatabaseConfigVariable: Scalars['Boolean']
|
||||
@@ -3043,7 +3113,7 @@ export type AiModelRole = 'FAST' | 'SMART'
|
||||
|
||||
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
|
||||
|
||||
export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'AppTarball' | 'GeneratedSdkClient'
|
||||
export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient'
|
||||
|
||||
export interface Subscription {
|
||||
onEventSubscription?: EventSubscription
|
||||
@@ -3499,6 +3569,7 @@ export interface ViewFieldGenqlSelection{
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -3573,6 +3644,7 @@ export interface ViewFieldGroupGenqlSelection{
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
viewFields?: ViewFieldGenqlSelection
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -3741,9 +3813,11 @@ export interface PageLayoutWidgetGenqlSelection{
|
||||
position?: PageLayoutWidgetPositionGenqlSelection
|
||||
configuration?: WidgetConfigurationGenqlSelection
|
||||
conditionalDisplay?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -3790,6 +3864,7 @@ export interface WidgetConfigurationGenqlSelection{
|
||||
on_CalendarConfiguration?:CalendarConfigurationGenqlSelection,
|
||||
on_FrontComponentConfiguration?:FrontComponentConfigurationGenqlSelection,
|
||||
on_EmailsConfiguration?:EmailsConfigurationGenqlSelection,
|
||||
on_EmailThreadConfiguration?:EmailThreadConfigurationGenqlSelection,
|
||||
on_FieldConfiguration?:FieldConfigurationGenqlSelection,
|
||||
on_FieldRichTextConfiguration?:FieldRichTextConfigurationGenqlSelection,
|
||||
on_FieldsConfiguration?:FieldsConfigurationGenqlSelection,
|
||||
@@ -3958,6 +4033,12 @@ export interface EmailsConfigurationGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EmailThreadConfigurationGenqlSelection{
|
||||
configurationType?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FieldConfigurationGenqlSelection{
|
||||
configurationType?: boolean | number
|
||||
fieldMetadataId?: boolean | number
|
||||
@@ -4048,6 +4129,7 @@ export interface PageLayoutTabGenqlSelection{
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
deletedAt?: boolean | number
|
||||
/** @deprecated isOverridden is deprecated */
|
||||
isOverridden?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -4440,12 +4522,15 @@ export interface ClientAIModelConfigGenqlSelection{
|
||||
modelFamily?: boolean | number
|
||||
modelFamilyLabel?: boolean | number
|
||||
sdkPackage?: boolean | number
|
||||
inputCostPerMillionTokensInCredits?: boolean | number
|
||||
outputCostPerMillionTokensInCredits?: boolean | number
|
||||
inputCostPerMillionTokens?: boolean | number
|
||||
outputCostPerMillionTokens?: boolean | number
|
||||
nativeCapabilities?: NativeModelCapabilitiesGenqlSelection
|
||||
isDeprecated?: boolean | number
|
||||
isRecommended?: boolean | number
|
||||
providerName?: boolean | number
|
||||
providerLabel?: boolean | number
|
||||
contextWindowTokens?: boolean | number
|
||||
maxOutputTokens?: boolean | number
|
||||
dataResidency?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -5431,7 +5516,7 @@ export interface ConnectedImapSmtpCaldavAccountGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
accountOwnerId?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -5465,6 +5550,7 @@ export interface CommandMenuItemGenqlSelection{
|
||||
position?: boolean | number
|
||||
isPinned?: boolean | number
|
||||
availabilityType?: boolean | number
|
||||
payload?: CommandMenuItemPayloadGenqlSelection
|
||||
hotKeys?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
availabilityObjectMetadataId?: boolean | number
|
||||
@@ -5475,6 +5561,24 @@ export interface CommandMenuItemGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface CommandMenuItemPayloadGenqlSelection{
|
||||
on_PathCommandMenuItemPayload?:PathCommandMenuItemPayloadGenqlSelection,
|
||||
on_ObjectMetadataCommandMenuItemPayload?:ObjectMetadataCommandMenuItemPayloadGenqlSelection,
|
||||
__typename?: boolean | number
|
||||
}
|
||||
|
||||
export interface PathCommandMenuItemPayloadGenqlSelection{
|
||||
path?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ObjectMetadataCommandMenuItemPayloadGenqlSelection{
|
||||
objectMetadataItemId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ToolIndexEntryGenqlSelection{
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
@@ -5603,6 +5707,64 @@ export interface DuplicatedDashboardGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
connectionParameters?: ImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PublicConnectionParametersOutputGenqlSelection{
|
||||
host?: boolean | number
|
||||
port?: boolean | number
|
||||
username?: boolean | number
|
||||
secure?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PublicImapSmtpCaldavConnectionParametersGenqlSelection{
|
||||
IMAP?: PublicConnectionParametersOutputGenqlSelection
|
||||
SMTP?: PublicConnectionParametersOutputGenqlSelection
|
||||
CALDAV?: PublicConnectionParametersOutputGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountPublicDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface SendEmailOutputGenqlSelection{
|
||||
success?: boolean | number
|
||||
error?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EventLogRecordGenqlSelection{
|
||||
event?: boolean | number
|
||||
timestamp?: boolean | number
|
||||
@@ -5770,22 +5932,6 @@ export interface CalendarChannelGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ConnectedAccountDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface MessageChannelGenqlSelection{
|
||||
id?: boolean | number
|
||||
visibility?: boolean | number
|
||||
@@ -5954,6 +6100,7 @@ export interface QueryGenqlSelection{
|
||||
myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myConnectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
connectedAccountById?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
connectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
webhooks?: WebhookGenqlSelection
|
||||
@@ -5988,7 +6135,6 @@ export interface QueryGenqlSelection{
|
||||
findApplicationRegistrationStats?: (ApplicationRegistrationStatsGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
findApplicationRegistrationVariables?: (ApplicationRegistrationVariableGenqlSelection & { __args: {applicationRegistrationId: Scalars['String']} })
|
||||
applicationRegistrationTarballUrl?: { __args: {id: Scalars['String']} }
|
||||
getApplicationShareLink?: { __args: {id: Scalars['String']} }
|
||||
currentUser?: UserGenqlSelection
|
||||
currentWorkspace?: WorkspaceGenqlSelection
|
||||
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
|
||||
@@ -6063,6 +6209,7 @@ export interface MutationGenqlSelection{
|
||||
updateNavigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {input: UpdateOneNavigationMenuItemInput} })
|
||||
deleteManyNavigationMenuItems?: (NavigationMenuItemGenqlSelection & { __args: {ids: Scalars['UUID'][]} })
|
||||
deleteNavigationMenuItem?: (NavigationMenuItemGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
uploadEmailAttachmentFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
uploadAIChatFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
uploadWorkflowFile?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
uploadWorkspaceLogo?: (FileWithSignedUrlGenqlSelection & { __args: {file: Scalars['Upload']} })
|
||||
@@ -6127,6 +6274,8 @@ export interface MutationGenqlSelection{
|
||||
updatePageLayout?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutInput} })
|
||||
destroyPageLayout?: { __args: {id: Scalars['String']} }
|
||||
updatePageLayoutWithTabsAndWidgets?: (PageLayoutGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWithTabsInput} })
|
||||
resetPageLayoutWidgetToDefault?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
resetPageLayoutTabToDefault?: (PageLayoutTabGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
createPageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {input: CreatePageLayoutWidgetInput} })
|
||||
updatePageLayoutWidget?: (PageLayoutWidgetGenqlSelection & { __args: {id: Scalars['String'], input: UpdatePageLayoutWidgetInput} })
|
||||
destroyPageLayoutWidget?: { __args: {id: Scalars['String']} }
|
||||
@@ -6174,7 +6323,7 @@ export interface MutationGenqlSelection{
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null)} })
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
|
||||
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
|
||||
deleteQueuedChatMessage?: { __args: {messageId: Scalars['UUID']} }
|
||||
createSkill?: (SkillGenqlSelection & { __args: {input: CreateSkillInput} })
|
||||
@@ -6227,13 +6376,16 @@ export interface MutationGenqlSelection{
|
||||
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
|
||||
editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} })
|
||||
impersonate?: (ImpersonateGenqlSelection & { __args: {userId: Scalars['UUID'], workspaceId: Scalars['UUID']} })
|
||||
sendEmail?: (SendEmailOutputGenqlSelection & { __args: {input: SendEmailInput} })
|
||||
startChannelSync?: (ChannelSyncSuccessGenqlSelection & { __args: {connectedAccountId: Scalars['UUID']} })
|
||||
saveImapSmtpCaldavAccount?: (ImapSmtpCaldavConnectionSuccessGenqlSelection & { __args: {accountOwnerId: Scalars['UUID'], handle: Scalars['String'], connectionParameters: EmailAccountConnectionParameters, id?: (Scalars['UUID'] | null)} })
|
||||
updateLabPublicFeatureFlag?: (FeatureFlagGenqlSelection & { __args: {input: UpdateLabPublicFeatureFlagInput} })
|
||||
userLookupAdminPanel?: (UserLookupGenqlSelection & { __args: {userIdentifier: Scalars['String']} })
|
||||
updateWorkspaceFeatureFlag?: { __args: {workspaceId: Scalars['UUID'], featureFlag: Scalars['String'], value: Scalars['Boolean']} }
|
||||
setAdminAiModelEnabled?: { __args: {modelId: Scalars['String'], enabled: Scalars['Boolean']} }
|
||||
setAdminAiModelsEnabled?: { __args: {modelIds: Scalars['String'][], enabled: Scalars['Boolean']} }
|
||||
setAdminAiModelRecommended?: { __args: {modelId: Scalars['String'], recommended: Scalars['Boolean']} }
|
||||
setAdminAiModelsRecommended?: { __args: {modelIds: Scalars['String'][], recommended: Scalars['Boolean']} }
|
||||
setAdminDefaultAiModel?: { __args: {role: AiModelRole, modelId: Scalars['String']} }
|
||||
createDatabaseConfigVariable?: { __args: {key: Scalars['String'], value: Scalars['JSON']} }
|
||||
updateDatabaseConfigVariable?: { __args: {key: Scalars['String'], value: Scalars['JSON']} }
|
||||
@@ -6405,13 +6557,13 @@ export interface UpdatePageLayoutWithTabsInput {name: Scalars['String'],type: Pa
|
||||
|
||||
export interface UpdatePageLayoutTabWithWidgetsInput {id: Scalars['UUID'],title: Scalars['String'],position: Scalars['Float'],icon?: (Scalars['String'] | null),layoutMode?: (PageLayoutTabLayoutMode | null),widgets: UpdatePageLayoutWidgetWithIdInput[]}
|
||||
|
||||
export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
|
||||
export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
|
||||
|
||||
export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float'],rowSpan: Scalars['Float'],columnSpan: Scalars['Float']}
|
||||
|
||||
export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration: Scalars['JSON']}
|
||||
|
||||
export interface UpdatePageLayoutWidgetInput {title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
|
||||
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
|
||||
|
||||
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
@@ -6429,7 +6581,7 @@ update: UpdateLogicFunctionFromSourceInputUpdates}
|
||||
|
||||
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null)}
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null)}
|
||||
|
||||
@@ -6577,6 +6729,10 @@ export interface DeleteSsoInput {identityProviderId: Scalars['UUID']}
|
||||
|
||||
export interface EditSsoInput {id: Scalars['UUID'],status: SSOIdentityProviderStatus}
|
||||
|
||||
export interface SendEmailInput {connectedAccountId: Scalars['String'],to: Scalars['String'],cc?: (Scalars['String'] | null),bcc?: (Scalars['String'] | null),subject: Scalars['String'],body: Scalars['String'],inReplyTo?: (Scalars['String'] | null),files?: (SendEmailAttachmentInput[] | null)}
|
||||
|
||||
export interface SendEmailAttachmentInput {id: Scalars['String'],name: Scalars['String']}
|
||||
|
||||
export interface EmailAccountConnectionParameters {IMAP?: (ConnectionParameters | null),SMTP?: (ConnectionParameters | null),CALDAV?: (ConnectionParameters | null)}
|
||||
|
||||
export interface ConnectionParameters {host: Scalars['String'],port: Scalars['Float'],username?: (Scalars['String'] | null),password: Scalars['String'],secure?: (Scalars['Boolean'] | null)}
|
||||
@@ -6956,7 +7112,7 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const WidgetConfiguration_possibleTypes: string[] = ['AggregateChartConfiguration','StandaloneRichTextConfiguration','PieChartConfiguration','LineChartConfiguration','IframeConfiguration','GaugeChartConfiguration','BarChartConfiguration','CalendarConfiguration','FrontComponentConfiguration','EmailsConfiguration','FieldConfiguration','FieldRichTextConfiguration','FieldsConfiguration','FilesConfiguration','NotesConfiguration','TasksConfiguration','TimelineConfiguration','ViewConfiguration','RecordTableConfiguration','WorkflowConfiguration','WorkflowRunConfiguration','WorkflowVersionConfiguration']
|
||||
const WidgetConfiguration_possibleTypes: string[] = ['AggregateChartConfiguration','StandaloneRichTextConfiguration','PieChartConfiguration','LineChartConfiguration','IframeConfiguration','GaugeChartConfiguration','BarChartConfiguration','CalendarConfiguration','FrontComponentConfiguration','EmailsConfiguration','EmailThreadConfiguration','FieldConfiguration','FieldRichTextConfiguration','FieldsConfiguration','FilesConfiguration','NotesConfiguration','TasksConfiguration','TimelineConfiguration','ViewConfiguration','RecordTableConfiguration','WorkflowConfiguration','WorkflowRunConfiguration','WorkflowVersionConfiguration']
|
||||
export const isWidgetConfiguration = (obj?: { __typename?: any } | null): obj is WidgetConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isWidgetConfiguration"')
|
||||
return WidgetConfiguration_possibleTypes.includes(obj.__typename)
|
||||
@@ -7044,6 +7200,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const EmailThreadConfiguration_possibleTypes: string[] = ['EmailThreadConfiguration']
|
||||
export const isEmailThreadConfiguration = (obj?: { __typename?: any } | null): obj is EmailThreadConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEmailThreadConfiguration"')
|
||||
return EmailThreadConfiguration_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FieldConfiguration_possibleTypes: string[] = ['FieldConfiguration']
|
||||
export const isFieldConfiguration = (obj?: { __typename?: any } | null): obj is FieldConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFieldConfiguration"')
|
||||
@@ -8388,6 +8552,30 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const CommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload','ObjectMetadataCommandMenuItemPayload']
|
||||
export const isCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is CommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isCommandMenuItemPayload"')
|
||||
return CommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PathCommandMenuItemPayload_possibleTypes: string[] = ['PathCommandMenuItemPayload']
|
||||
export const isPathCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is PathCommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPathCommandMenuItemPayload"')
|
||||
return PathCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ObjectMetadataCommandMenuItemPayload_possibleTypes: string[] = ['ObjectMetadataCommandMenuItemPayload']
|
||||
export const isObjectMetadataCommandMenuItemPayload = (obj?: { __typename?: any } | null): obj is ObjectMetadataCommandMenuItemPayload => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isObjectMetadataCommandMenuItemPayload"')
|
||||
return ObjectMetadataCommandMenuItemPayload_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry']
|
||||
export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"')
|
||||
@@ -8476,6 +8664,46 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO']
|
||||
export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"')
|
||||
return ConnectedAccountDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PublicConnectionParametersOutput_possibleTypes: string[] = ['PublicConnectionParametersOutput']
|
||||
export const isPublicConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is PublicConnectionParametersOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicConnectionParametersOutput"')
|
||||
return PublicConnectionParametersOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PublicImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['PublicImapSmtpCaldavConnectionParameters']
|
||||
export const isPublicImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is PublicImapSmtpCaldavConnectionParameters => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicImapSmtpCaldavConnectionParameters"')
|
||||
return PublicImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountPublicDTO_possibleTypes: string[] = ['ConnectedAccountPublicDTO']
|
||||
export const isConnectedAccountPublicDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountPublicDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountPublicDTO"')
|
||||
return ConnectedAccountPublicDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const SendEmailOutput_possibleTypes: string[] = ['SendEmailOutput']
|
||||
export const isSendEmailOutput = (obj?: { __typename?: any } | null): obj is SendEmailOutput => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isSendEmailOutput"')
|
||||
return SendEmailOutput_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EventLogRecord_possibleTypes: string[] = ['EventLogRecord']
|
||||
export const isEventLogRecord = (obj?: { __typename?: any } | null): obj is EventLogRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEventLogRecord"')
|
||||
@@ -8604,14 +8832,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ConnectedAccountDTO_possibleTypes: string[] = ['ConnectedAccountDTO']
|
||||
export const isConnectedAccountDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountDTO"')
|
||||
return ConnectedAccountDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const MessageChannel_possibleTypes: string[] = ['MessageChannel']
|
||||
export const isMessageChannel = (obj?: { __typename?: any } | null): obj is MessageChannel => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isMessageChannel"')
|
||||
@@ -8912,7 +9132,8 @@ export const enumWidgetType = {
|
||||
WORKFLOW_VERSION: 'WORKFLOW_VERSION' as const,
|
||||
WORKFLOW_RUN: 'WORKFLOW_RUN' as const,
|
||||
FRONT_COMPONENT: 'FRONT_COMPONENT' as const,
|
||||
RECORD_TABLE: 'RECORD_TABLE' as const
|
||||
RECORD_TABLE: 'RECORD_TABLE' as const,
|
||||
EMAIL_THREAD: 'EMAIL_THREAD' as const
|
||||
}
|
||||
|
||||
export const enumPageLayoutTabLayoutMode = {
|
||||
@@ -8943,7 +9164,8 @@ export const enumWidgetConfigurationType = {
|
||||
WORKFLOW_VERSION: 'WORKFLOW_VERSION' as const,
|
||||
WORKFLOW_RUN: 'WORKFLOW_RUN' as const,
|
||||
FRONT_COMPONENT: 'FRONT_COMPONENT' as const,
|
||||
RECORD_TABLE: 'RECORD_TABLE' as const
|
||||
RECORD_TABLE: 'RECORD_TABLE' as const,
|
||||
EMAIL_THREAD: 'EMAIL_THREAD' as const
|
||||
}
|
||||
|
||||
export const enumObjectRecordGroupByDateGranularity = {
|
||||
@@ -8987,6 +9209,7 @@ export const enumBarChartLayout = {
|
||||
|
||||
export const enumFieldDisplayMode = {
|
||||
CARD: 'CARD' as const,
|
||||
EDITOR: 'EDITOR' as const,
|
||||
FIELD: 'FIELD' as const,
|
||||
VIEW: 'VIEW' as const
|
||||
}
|
||||
@@ -9091,17 +9314,15 @@ export const enumFeatureFlagKey = {
|
||||
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
|
||||
IS_AI_ENABLED: 'IS_AI_ENABLED' as const,
|
||||
IS_COMMAND_MENU_ITEM_ENABLED: 'IS_COMMAND_MENU_ITEM_ENABLED' as const,
|
||||
IS_MARKETPLACE_ENABLED: 'IS_MARKETPLACE_ENABLED' as const,
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as const,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
|
||||
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
|
||||
IS_DRAFT_EMAIL_ENABLED: 'IS_DRAFT_EMAIL_ENABLED' as const,
|
||||
IS_USAGE_ANALYTICS_ENABLED: 'IS_USAGE_ANALYTICS_ENABLED' as const,
|
||||
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
|
||||
IS_DIRECT_GRAPHQL_EXECUTION_ENABLED: 'IS_DIRECT_GRAPHQL_EXECUTION_ENABLED' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
|
||||
IS_CONNECTED_ACCOUNT_MIGRATED: 'IS_CONNECTED_ACCOUNT_MIGRATED' as const,
|
||||
IS_RICH_TEXT_V1_MIGRATED: 'IS_RICH_TEXT_V1_MIGRATED' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
|
||||
IS_RECORD_TABLE_WIDGET_ENABLED: 'IS_RECORD_TABLE_WIDGET_ENABLED' as const,
|
||||
IS_DATASOURCE_MIGRATED: 'IS_DATASOURCE_MIGRATED' as const
|
||||
}
|
||||
@@ -9215,19 +9436,11 @@ export const enumEngineComponentKey = {
|
||||
SEE_DELETED_RECORDS: 'SEE_DELETED_RECORDS' as const,
|
||||
CREATE_NEW_VIEW: 'CREATE_NEW_VIEW' as const,
|
||||
HIDE_DELETED_RECORDS: 'HIDE_DELETED_RECORDS' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
|
||||
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
|
||||
GO_TO_TASKS: 'GO_TO_TASKS' as const,
|
||||
GO_TO_NOTES: 'GO_TO_NOTES' as const,
|
||||
EDIT_RECORD_PAGE_LAYOUT: 'EDIT_RECORD_PAGE_LAYOUT' as const,
|
||||
EDIT_DASHBOARD_LAYOUT: 'EDIT_DASHBOARD_LAYOUT' as const,
|
||||
SAVE_DASHBOARD_LAYOUT: 'SAVE_DASHBOARD_LAYOUT' as const,
|
||||
CANCEL_DASHBOARD_LAYOUT: 'CANCEL_DASHBOARD_LAYOUT' as const,
|
||||
DUPLICATE_DASHBOARD: 'DUPLICATE_DASHBOARD' as const,
|
||||
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
|
||||
ACTIVATE_WORKFLOW: 'ACTIVATE_WORKFLOW' as const,
|
||||
DEACTIVATE_WORKFLOW: 'DEACTIVATE_WORKFLOW' as const,
|
||||
DISCARD_DRAFT_WORKFLOW: 'DISCARD_DRAFT_WORKFLOW' as const,
|
||||
@@ -9238,7 +9451,6 @@ export const enumEngineComponentKey = {
|
||||
ADD_NODE_WORKFLOW: 'ADD_NODE_WORKFLOW' as const,
|
||||
TIDY_UP_WORKFLOW: 'TIDY_UP_WORKFLOW' as const,
|
||||
DUPLICATE_WORKFLOW: 'DUPLICATE_WORKFLOW' as const,
|
||||
GO_TO_RUNS: 'GO_TO_RUNS' as const,
|
||||
SEE_VERSION_WORKFLOW_RUN: 'SEE_VERSION_WORKFLOW_RUN' as const,
|
||||
SEE_WORKFLOW_WORKFLOW_RUN: 'SEE_WORKFLOW_WORKFLOW_RUN' as const,
|
||||
STOP_WORKFLOW_RUN: 'STOP_WORKFLOW_RUN' as const,
|
||||
@@ -9250,8 +9462,20 @@ export const enumEngineComponentKey = {
|
||||
SEARCH_RECORDS_FALLBACK: 'SEARCH_RECORDS_FALLBACK' as const,
|
||||
ASK_AI: 'ASK_AI' as const,
|
||||
VIEW_PREVIOUS_AI_CHATS: 'VIEW_PREVIOUS_AI_CHATS' as const,
|
||||
NAVIGATION: 'NAVIGATION' as const,
|
||||
TRIGGER_WORKFLOW_VERSION: 'TRIGGER_WORKFLOW_VERSION' as const,
|
||||
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
|
||||
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
|
||||
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
|
||||
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
|
||||
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
|
||||
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
|
||||
GO_TO_OPPORTUNITIES: 'GO_TO_OPPORTUNITIES' as const,
|
||||
GO_TO_SETTINGS: 'GO_TO_SETTINGS' as const,
|
||||
GO_TO_TASKS: 'GO_TO_TASKS' as const,
|
||||
GO_TO_NOTES: 'GO_TO_NOTES' as const,
|
||||
GO_TO_WORKFLOWS: 'GO_TO_WORKFLOWS' as const,
|
||||
GO_TO_RUNS: 'GO_TO_RUNS' as const,
|
||||
DELETE_SINGLE_RECORD: 'DELETE_SINGLE_RECORD' as const,
|
||||
DELETE_MULTIPLE_RECORDS: 'DELETE_MULTIPLE_RECORDS' as const,
|
||||
RESTORE_SINGLE_RECORD: 'RESTORE_SINGLE_RECORD' as const,
|
||||
@@ -9401,14 +9625,16 @@ export const enumEventLogTable = {
|
||||
WORKSPACE_EVENT: 'WORKSPACE_EVENT' as const,
|
||||
PAGEVIEW: 'PAGEVIEW' as const,
|
||||
OBJECT_EVENT: 'OBJECT_EVENT' as const,
|
||||
USAGE_EVENT: 'USAGE_EVENT' as const
|
||||
USAGE_EVENT: 'USAGE_EVENT' as const,
|
||||
APPLICATION_LOG: 'APPLICATION_LOG' as const
|
||||
}
|
||||
|
||||
export const enumUsageOperationType = {
|
||||
AI_CHAT_TOKEN: 'AI_CHAT_TOKEN' as const,
|
||||
AI_WORKFLOW_TOKEN: 'AI_WORKFLOW_TOKEN' as const,
|
||||
WORKFLOW_EXECUTION: 'WORKFLOW_EXECUTION' as const,
|
||||
CODE_EXECUTION: 'CODE_EXECUTION' as const
|
||||
CODE_EXECUTION: 'CODE_EXECUTION' as const,
|
||||
WEB_SEARCH: 'WEB_SEARCH' as const
|
||||
}
|
||||
|
||||
export const enumAnalyticsType = {
|
||||
@@ -9442,6 +9668,7 @@ export const enumFileFolder = {
|
||||
FilesField: 'FilesField' as const,
|
||||
Dependencies: 'Dependencies' as const,
|
||||
Workflow: 'Workflow' as const,
|
||||
EmailAttachment: 'EmailAttachment' as const,
|
||||
AppTarball: 'AppTarball' as const,
|
||||
GeneratedSdkClient: 'GeneratedSdkClient' as const
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-18
@@ -47,26 +47,9 @@ npx nx run twenty-server:database:reset
|
||||
#### For objects in Core/Metadata schemas (TypeORM)
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
|
||||
npx nx run twenty-server:database:migrate:generate
|
||||
```
|
||||
|
||||
#### For Workspace objects
|
||||
|
||||
There are no migrations files, migration are generated automatically for each workspace,
|
||||
stored in the database, and applied with this command
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
||||
This will drop the database and re-run the migrations and seed.
|
||||
|
||||
Make sure to back up any data you want to keep before running this command.
|
||||
|
||||
</Warning>
|
||||
|
||||
## Tech Stack
|
||||
|
||||
Twenty primarily uses NestJS for the backend.
|
||||
|
||||
@@ -644,49 +644,15 @@ export default defineLogicFunction({
|
||||
**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>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
|
||||
|
||||
A pre-install function is a logic function that runs automatically before your app is installed on a workspace. This is useful for validation tasks, prerequisite checks, or preparing workspace state before the main installation proceeds.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the pre-install function at any time using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Pre-install functions use `definePreInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
|
||||
- Only one pre-install function is allowed per application. The manifest build will error if more than one is detected.
|
||||
- The function's `universalIdentifier` is automatically set as `preInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
|
||||
- The default timeout is set to 300 seconds (5 minutes) to allow for longer preparation tasks.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Define a post-install logic function (one per app)">
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
A post-install function is a logic function that runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
@@ -695,6 +661,8 @@ export default definePostInstallLogicFunction({
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
@@ -707,10 +675,168 @@ yarn twenty exec --postInstall
|
||||
|
||||
Key points:
|
||||
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
- The handler receives an `InstallLogicFunctionPayload` with `{ previousVersion: string }` — the version of the app that was previously installed (or an empty string for fresh installs).
|
||||
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
|
||||
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
|
||||
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
|
||||
- `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
|
||||
- `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error.
|
||||
- Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`.
|
||||
- The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app.
|
||||
- Only one post-install function is allowed per application. The manifest build will error if more than one is detected.
|
||||
- The function's `universalIdentifier` is automatically set as `postInstallLogicFunctionUniversalIdentifier` on the application manifest during the build — you do not need to reference it in `defineApplication()`.
|
||||
- The function's `universalIdentifier`, `shouldRunOnVersionUpgrade`, and `shouldRunSynchronously` are automatically attached to the application manifest under the `postInstallLogicFunction` field during the build — you do not need to reference them in `defineApplication()`.
|
||||
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
- **Not executed in dev mode**: when an app is registered locally (via `yarn twenty dev`), the server skips the install flow entirely and syncs files directly through the CLI watcher — so post-install never runs in dev mode, regardless of `shouldRunSynchronously`. Use `yarn twenty exec --postInstall` to trigger it manually against a running workspace.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Define a pre-install logic function (one per app)">
|
||||
|
||||
A pre-install function is a logic function that runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the pre-install function at any time using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot.
|
||||
- Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks.
|
||||
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
|
||||
- **Execution model**: pre-install is executed **synchronously** and **blocks the install**. If the handler throws, the install is aborted before any schema changes are applied — the workspace stays on the previous version in a consistent state. This is intentional: pre-install is your last chance to refuse a risky upgrade.
|
||||
- As with post-install, only one pre-install function is allowed per application. It is attached to the application manifest under `preInstallLogicFunction` automatically during the build.
|
||||
- **Not executed in dev mode**: same as post-install — the install flow is skipped entirely for locally-registered apps, so pre-install never runs under `yarn twenty dev`. Use `yarn twenty exec --preInstall` to trigger it manually.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-install vs post-install: when to use which" description="Choosing the right install hook">
|
||||
|
||||
Both hooks are part of the same install flow and receive the same `InstallPayload`. The difference is **when** they run relative to the workspace metadata migration, and that changes what data they can safely touch.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Pre-install is always **synchronous** (it blocks the install and can abort it). Post-install is **asynchronous by default** — enqueued on a worker with automatic retries — but can opt into synchronous execution with `shouldRunSynchronously: true`. See the `definePostInstallLogicFunction` accordion above for when to use each mode.
|
||||
|
||||
**Use `post-install` for anything that needs the new schema to exist.** This is the common case:
|
||||
|
||||
- Seeding default data (creating initial records, default views, demo content) against newly-added objects and fields.
|
||||
- Registering webhooks with third-party services now that the app has its credentials.
|
||||
- Calling your own API to finish setup that depends on the synchronized metadata.
|
||||
- Idempotent "ensure this exists" logic that should reconcile state on every upgrade — combine with `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Example — seed a default `PostCard` record after install:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky:
|
||||
|
||||
- **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs.
|
||||
- **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first.
|
||||
- **Validating compatibility and refusing the upgrade if the current data cannot be migrated cleanly** — throw from the handler and the install aborts with no changes applied. This is safer than discovering the incompatibility mid-migration.
|
||||
- **Renaming or rekeying data** ahead of a schema change that would lose the association.
|
||||
|
||||
Example — archive records before a destructive migration:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Rule of thumb:**
|
||||
|
||||
| You want to… | Use |
|
||||
|---|---|
|
||||
| Seed default data, configure the workspace, register external resources | `post-install` |
|
||||
| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) |
|
||||
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
|
||||
| Read or back up data that the upcoming migration would lose | `pre-install` |
|
||||
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
|
||||
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
|
||||
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
|
||||
|
||||
<Note>
|
||||
If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineFrontComponent" description="Define front components for custom UI" >
|
||||
@@ -756,7 +882,7 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
After syncing with `yarn twenty dev`, the quick action appears in the top-right corner of the page:
|
||||
After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Quick action button in the top-right corner" />
|
||||
@@ -1402,7 +1528,7 @@ export default defineFrontComponent({
|
||||
|
||||
### How bundling works
|
||||
|
||||
The build step (`yarn twenty dev` or `yarn twenty build`) uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
|
||||
The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
|
||||
|
||||
**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed.
|
||||
|
||||
@@ -1818,8 +1944,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute pre-install or post-install functions
|
||||
yarn twenty exec --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
|
||||
@@ -98,6 +98,21 @@ Dev mode is only available on Twenty instances running in development (`NODE_ENV
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
|
||||
</div>
|
||||
|
||||
#### One-shot sync with `yarn twenty dev --once`
|
||||
|
||||
If you do not want a watcher running in the background (for example in a CI pipeline, a git hook, or a scripted workflow), pass the `--once` flag. It runs the same pipeline as `yarn twenty dev` — build manifest, bundle files, upload, sync, regenerate the typed API client — but **exits as soon as the sync completes**:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Command | Behavior | When to use |
|
||||
|---------|----------|-------------|
|
||||
| `yarn twenty dev` | Watches your source files and re-syncs on every change. Keeps running until you stop it. | Interactive local development — you want the live status panel and instant feedback loop. |
|
||||
| `yarn twenty dev --once` | Performs a single build + sync, then exits with code `0` on success or `1` on failure. | Scripts, CI, pre-commit hooks, AI agents, and any non-interactive workflow. |
|
||||
|
||||
Both modes require a Twenty server running in development mode and an authenticated remote — the same prerequisites apply.
|
||||
|
||||
### See your app in Twenty
|
||||
|
||||
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
|
||||
|
||||
@@ -52,7 +52,11 @@ yarn twenty deploy
|
||||
|
||||
### Sharing a deployed app
|
||||
|
||||
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. To share a deployed app:
|
||||
<Warning>
|
||||
Sharing private (tarball) apps across workspaces is an **Enterprise** feature. The **Distribution** tab will show an upgrade prompt instead of the share controls until your workspace has a valid Enterprise key. See [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to activate it.
|
||||
</Warning>
|
||||
|
||||
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. Once your workspace is on the Enterprise plan, you can share a deployed app like this:
|
||||
|
||||
1. Go to **Settings > Applications > Registrations** and open your app
|
||||
2. In the **Distribution** tab, click **Copy share link**
|
||||
@@ -60,18 +64,20 @@ Tarball apps are not listed in the public marketplace, so other workspaces on th
|
||||
|
||||
The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server.
|
||||
|
||||
<Warning>
|
||||
Sharing private apps is an Enterprise feature. Go to [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to enable it.
|
||||
</Warning>
|
||||
|
||||
### Version management
|
||||
|
||||
When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI.
|
||||
|
||||
To release an update:
|
||||
|
||||
1. Bump the `version` field in your `package.json`
|
||||
1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`)
|
||||
2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`)
|
||||
3. Workspaces that have the app installed will see the upgrade available in their settings
|
||||
|
||||
<Note>
|
||||
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
## Publishing to npm
|
||||
@@ -189,3 +195,12 @@ You can also install apps from the command line:
|
||||
```bash filename="Terminal"
|
||||
yarn twenty install
|
||||
```
|
||||
|
||||
<Note>
|
||||
The server enforces semver versioning on install, mirroring the rules on deploy:
|
||||
|
||||
- Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error.
|
||||
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.
|
||||
|
||||
To install a newer version, deploy or publish it first, then re-run `yarn twenty install`.
|
||||
</Note>
|
||||
|
||||
+1
-18
@@ -47,26 +47,9 @@ npx nx run twenty-server:database:reset
|
||||
#### للكائنات داخل مخططات Core/Metadata (TypeORM)
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
|
||||
npx nx run twenty-server:database:migrate:generate
|
||||
```
|
||||
|
||||
#### لكائنات مساحة العمل
|
||||
|
||||
لا توجد ملفات هجيرات، يتم إنشاء الهجيرات تلقائيًا لكل مساحة عمل،
|
||||
مخزنة في قاعدة البيانات، ويتم تطبيقها مع هذا الأمر
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
||||
سيؤدي هذا إلى إسقاط قاعدة البيانات وإعادة تشغيل الهجرات والبذور.
|
||||
|
||||
تأكد من عمل نسخة احتياطية لأي بيانات تريد الاحتفاظ بها قبل تشغيل هذا الأمر.
|
||||
|
||||
</Warning>
|
||||
|
||||
## "التقنية المستخدمة"
|
||||
|
||||
تستخدم Twenty بشكل أساسي NestJS للواجهة الخلفية.
|
||||
|
||||
@@ -81,7 +81,7 @@ import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
@@ -643,49 +643,15 @@ export default defineLogicFunction({
|
||||
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق)">
|
||||
|
||||
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا قبل تثبيت تطبيقك على مساحة عمل. يفيد ذلك في مهام التحقق، وفحص المتطلبات المسبقة، أو تجهيز حالة مساحة العمل قبل متابعة التثبيت الرئيسي.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
|
||||
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
|
||||
* يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
|
||||
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `preInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
|
||||
* تم ضبط المهلة الافتراضية على 300 ثانية (5 دقائق) للسماح بمهام التحضير الأطول.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="تعريف دالة منطقية لما بعد التثبيت (واحدة لكل تطبيق)">
|
||||
|
||||
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. هذا مفيد لمهام الإعداد لمرة واحدة مثل تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، أو تكوين إعدادات مساحة العمل.
|
||||
دالة ما بعد التثبيت هي دالة منطقية تعمل تلقائيًا بعد تثبيت تطبيقك على مساحة عمل. ينفّذه الخادم **بعد** مزامنة البيانات الوصفية للتطبيق وإنشاء عميل SDK، بحيث تكون مساحة العمل جاهزة تمامًا للاستخدام ويكون المخطط الجديد مطبَّقًا. تشمل حالات الاستخدام النموذجية تهيئة البيانات الافتراضية، وإنشاء السجلات الأولية، وتكوين إعدادات مساحة العمل، أو توفير الموارد على خدمات جهات خارجية.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
@@ -694,6 +660,8 @@ export default definePostInstallLogicFunction({
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
@@ -706,16 +674,181 @@ yarn twenty exec --postInstall
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما بعد التثبيت `definePostInstallLogicFunction()` — وهو إصدار متخصص يستبعد إعدادات المُشغِّل (`cronTriggerSettings` و`databaseEventTriggerSettings` و`httpRouteTriggerSettings` و`isTool`).
|
||||
* يتلقى المُعالج `InstallLogicFunctionPayload` يحوي `{ previousVersion: string }` — إصدار التطبيق الذي كان مُثبّتًا سابقًا (أو سلسلة فارغة للتثبيتات الجديدة).
|
||||
* يتلقى المعالج `InstallPayload` يحتوي على `{ previousVersion?: string; newVersion: string }` — حيث إن `newVersion` هو الإصدار الجاري تثبيته، و`previousVersion` هو الإصدار الذي كان مُثبّتًا سابقًا (أو `undefined` عند التثبيت الأولي). استخدم هذه القيم للتمييز بين عمليات التثبيت الجديدة والترقيات ولتشغيل منطق الترحيل الخاص بالإصدار.
|
||||
* **موعد تشغيل الخطاف**: في عمليات التثبيت الجديدة فقط، افتراضيًا. مرّر `shouldRunOnVersionUpgrade: true` إذا كنت تريد تشغيله أيضًا عند ترقية التطبيق من إصدار سابق. عند إغفاله، تكون القيمة الافتراضية للعلم `false`، وتتجاوز الترقيات هذا الخطاف.
|
||||
* **نموذج التنفيذ — غير متزامن افتراضيًا، والتزامني اختياري**: يتحكّم العلم `shouldRunSynchronously` في كيفية تنفيذ ما بعد التثبيت.
|
||||
* `shouldRunSynchronously: false` *(الإعداد الافتراضي)* — يتم **إدراج الخطاف في قائمة الرسائل** مع `retryLimit: 3` ويعمل بشكل غير متزامن داخل عامل عمل. يعود ردّ التثبيت بمجرد وضع المهمة في الطابور، لذا فإن معالجًا بطيئًا أو متعطلًا لا يحجب المستدعي. سيُجرِّب العامل إعادة المحاولة حتى ثلاث مرات. **استخدم هذا للمهام طويلة التشغيل** — بَذر مجموعات بيانات كبيرة، استدعاء واجهات برمجة تطبيقات خارجية بطيئة، تهيئة موارد خارجية، أو أي شيء قد يتجاوز نافذة استجابة HTTP المعقولة.
|
||||
* `shouldRunSynchronously: true` — يُنفّذ الخطاف **ضمن تدفّق التثبيت مباشرةً** (نفس المنفِّذ كما قبل التثبيت). يَحجُب طلب التثبيت حتى ينتهي المعالج، وإذا رمى استثناءً، سيتلقى مستدعي التثبيت `POST_INSTALL_ERROR`. لا توجد محاولات إعادة تلقائية. **استخدم هذا للمهام السريعة التي يجب إكمالها قبل الاستجابة** — مثل إظهار خطأ تحقق للمستخدم، أو إعداد سريع سيعتمد عليه العميل مباشرةً بعد عودة نداء التثبيت. ضع في اعتبارك أن ترحيل البيانات الوصفية يكون قد طُبِّق بالفعل عند تشغيل ما بعد التثبيت، لذلك فإن فشل الوضع المتزامن **لا** يعيد التغييرات على المخطط إلى الوراء — بل يكتفي بإبراز الخطأ.
|
||||
* تأكّد من أن معالجك قابل للتنفيذ المتكرر دون آثار جانبية. في الوضع غير المتزامن قد تُعيد قائمة الانتظار المحاولة حتى ثلاث مرات؛ وفي أي من الوضعين قد يعمل الخطاف مجددًا أثناء الترقيات عند ضبط `shouldRunOnVersionUpgrade: true`.
|
||||
* متغيرات البيئة `APPLICATION_ID` و`APP_ACCESS_TOKEN` و`API_URL` متاحة داخل المعالج (كما في أي دالة منطق أخرى)، لذا يمكنك استدعاء واجهة Twenty API باستخدام رمز وصول للتطبيق مقيّد بنطاق تطبيقك.
|
||||
* يُسمح بدالة ما بعد التثبيت واحدة فقط لكل تطبيق. سيُنتج إنشاء ملف البيان خطأً إذا تم اكتشاف أكثر من واحدة.
|
||||
* يتم تعيين `universalIdentifier` للدالة تلقائيًا كـ `postInstallLogicFunctionUniversalIdentifier` في بيان التطبيق أثناء الإنشاء — لست بحاجة إلى الإشارة إليه في `defineApplication()`.
|
||||
* تُرفَق خصائص الدالة `universalIdentifier` و`shouldRunOnVersionUpgrade` و`shouldRunSynchronously` تلقائيًا ببيان التطبيق ضمن الحقل `postInstallLogicFunction` أثناء عملية البناء — ولا تحتاج إلى الإشارة إليها في `defineApplication()`.
|
||||
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
|
||||
* **لا يُنفَّذ في وضع التطوير**: عند تسجيل تطبيق محليًا (عبر `yarn twenty dev`)، يتجاوز الخادم تدفّق التثبيت بالكامل ويُزامن الملفات مباشرةً عبر مراقِب CLI — لذا لن يعمل ما بعد التثبيت في وضع التطوير مطلقًا، بغضّ النظر عن `shouldRunSynchronously`. استخدم `yarn twenty exec --postInstall` لتشغيله يدويًا على مساحة عمل قيد التشغيل.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="تعريف دالة منطقية لما قبل التثبيت (واحدة لكل تطبيق)">
|
||||
|
||||
دالة ما قبل التثبيت هي دالة منطقية تعمل تلقائيًا أثناء التثبيت، **قبل تطبيق ترحيل البيانات الوصفية لمساحة العمل**. تتشارك نفس بنية الحمولة مع ما بعد التثبيت (`InstallPayload`)، لكنها موضوعة أبكر في تدفّق التثبيت كي تجهّز حالة يعتمد عليها الترحيل القادم — ومن الاستخدامات الشائعة: نسخ البيانات احتياطيًا، التحقق من التوافق مع المخطط الجديد، أو أرشفة السجلات التي ستُعاد هيكلتها أو ستُحذف.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما قبل التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
* تستخدم دوال ما قبل التثبيت `definePreInstallLogicFunction()` — نفس الإعدادات المتخصصة كما في ما بعد التثبيت، لكنها مرتبطة بموضع مختلف ضمن دورة الحياة.
|
||||
* يتلقّى كلٌّ من معالجي ما قبل التثبيت وما بعد التثبيت النوع نفسه `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. استورده مرة واحدة وأعد استخدامه لكلا الخطافين.
|
||||
* **موعد تشغيل الخطاف**: موضوع مباشرةً قبل ترحيل البيانات الوصفية لمساحة العمل (`synchronizeFromManifest`). قبل التنفيذ، يُشغِّل الخادم مزامنة "pared-down sync" ذات طابع إضافي فقط تقوم بتسجيل دالة ما قبل التثبيت للإصدار **الجديد** في البيانات الوصفية لمساحة العمل — دون لمس أي شيء آخر — ثم يُنفّذها. لأن هذه المزامنة «إضافية فقط»، تبقى كائنات وحقول وبيانات الإصدار السابق سليمة عند تشغيل معالجك: يمكنك قراءة حالة ما قبل الترحيل ونسخها احتياطيًا بأمان.
|
||||
* **نموذج التنفيذ**: يُنفَّذ ما قبل التثبيت **بشكل متزامن** و**يحجب عملية التثبيت**. إذا رمى المعالج استثناءً، تُلغى عملية التثبيت قبل تطبيق أي تغييرات على المخطط — وتبقى مساحة العمل على الإصدار السابق بحالة متّسقة. هذا مقصود: ما قبل التثبيت هو فرصتك الأخيرة لرفض ترقية تنطوي على مخاطر.
|
||||
* كما هو الحال مع ما بعد التثبيت، يُسمح بدالة ما قبل التثبيت واحدة فقط لكل تطبيق. تُربَط تلقائيًا ببيان التطبيق تحت `preInstallLogicFunction` أثناء عملية البناء.
|
||||
* **لا يُنفَّذ في وضع التطوير**: كما في ما بعد التثبيت — يتم تجاوز تدفّق التثبيت بالكامل للتطبيقات المسجّلة محليًا، لذا لن يعمل ما قبل التثبيت مطلقًا عند `yarn twenty dev`. استخدم `yarn twenty exec --preInstall` لتشغيله يدويًا.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="ما قبل التثبيت مقابل ما بعد التثبيت: متى تستخدم أيّهما" description="اختيار خطاف التثبيت المناسب">
|
||||
|
||||
كلا الخطافين جزء من تدفّق التثبيت نفسه ويتلقّيان نفس `InstallPayload`. الاختلاف يكمن في **موعد** تشغيلهما نسبةً إلى ترحيل البيانات الوصفية لمساحة العمل، وهذا يغيّر البيانات التي يمكنهما التعامل معها بأمان.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
ما قبل التثبيت دائمًا **متزامن** (يحجب التثبيت ويمكنه إحباطه). ما بعد التثبيت **غير متزامن افتراضيًا** — يُدرج على عامل مع محاولات إعادة تلقائية — لكن يمكن التبديل إلى تنفيذ متزامن عبر `shouldRunSynchronously: true`. راجع الأكورديون `definePostInstallLogicFunction` أعلاه لمعرفة متى تستخدم كل وضع.
|
||||
|
||||
**استخدم `post-install` لأي شيء يتطلّب وجود المخطط الجديد.** وهذا هو السيناريو الشائع:
|
||||
|
||||
* بَذر بيانات افتراضية (إنشاء سجلات أولية وعروض افتراضية ومحتوى تجريبي) للكائنات والحقول المضافة حديثًا.
|
||||
* تسجيل خطافات الويب مع خدمات أطراف ثالثة بعد أن حصل التطبيق على بيانات الاعتماد الخاصة به.
|
||||
* استدعاء واجهة برمجة التطبيقات الخاصة بك لإكمال إعداد يعتمد على البيانات الوصفية المتزامنة.
|
||||
* منطق idempotent لتحقيق "تأكّد من وجود هذا" والذي ينبغي مواءمة الحالة في كل ترقية — بالاقتران مع `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
مثال — بَذر سجل `PostCard` افتراضي بعد التثبيت:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**استخدم `pre-install` عندما قد يُتلف الترحيل أو يدمّر البيانات الحالية.** لأن ما قبل التثبيت يعمل مقابل المخطط *السابق* وفشله يُرجِع الترقية إلى الوراء، فهو المكان المناسب لأي شيء محفوف بالمخاطر:
|
||||
|
||||
* **نسخ البيانات احتياطيًا قبل حذفها أو إعادة هيكلتها** — مثل إزالة حقل في v2 وتحتاج إلى نسخ قيمه إلى حقل آخر أو تصديرها إلى التخزين قبل تشغيل الترحيل.
|
||||
* **أرشفة السجلات التي سيبطلها قيد جديد** — مثل أن يصبح حقل ما `NOT NULL` وتحتاج أولًا إلى حذف الصفوف ذات القيم الفارغة أو إصلاحها.
|
||||
* **التحقق من التوافق ورفض الترقية إذا تعذّر ترحيل البيانات الحالية بسلاسة** — ارمِ من داخل المعالج وسيُلغى التثبيت دون تطبيق أي تغييرات. هذا أكثر أمانًا من اكتشاف عدم التوافق في منتصف الترحيل.
|
||||
* **إعادة تسمية البيانات أو إعادة تعيين مفاتيحها** قبل تغيير في المخطط قد يؤدي إلى فقدان الارتباط.
|
||||
|
||||
مثال — أرشف السجلات قبل ترحيل هدّام:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**قاعدة عامة:**
|
||||
|
||||
| ترغب في… | استخدام |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| بذر بيانات افتراضية، تهيئة مساحة العمل، تسجيل موارد خارجية | `post-install` |
|
||||
| تشغيل بذر طويل الأمد أو استدعاءات أطراف ثالثة لا ينبغي أن تحجب استجابة التثبيت | `post-install` (الإعداد الافتراضي — `shouldRunSynchronously: false`، مع محاولات إعادة من العامل) |
|
||||
| تشغيل إعداد سريع سيعتمد عليه المستدعي مباشرةً بعد عودة نداء التثبيت | `post-install` مع `shouldRunSynchronously: true` |
|
||||
| قراءة البيانات أو نسخها احتياطيًا والتي قد يفقدها الترحيل القادم | `pre-install` |
|
||||
| رفض ترقية قد تُفسد البيانات الحالية | `pre-install` (ارمِ من المعالج) |
|
||||
| تنفيذ مواءمة في كل ترقية | `post-install` مع `shouldRunOnVersionUpgrade: true` |
|
||||
| تنفيذ إعداد لمرة واحدة في التثبيت الأول فقط | `post-install` مع `shouldRunOnVersionUpgrade: false` (الإعداد الافتراضي) |
|
||||
|
||||
<Note>
|
||||
إذا ساورك الشك، فاجعل الافتراضي هو **post-install**. الجأ إلى ما قبل التثبيت فقط عندما يكون الترحيل نفسه هدّامًا وتحتاج إلى التقاط الحالة السابقة قبل أن تزول.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineFrontComponent" description="عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة">
|
||||
|
||||
المكوّنات الأمامية هي مكوّنات React تُعرَض مباشرة داخل واجهة مستخدم Twenty. تعمل ضمن **Web Worker** معزول باستخدام Remote DOM — تكون شيفرتك في صندوق عزل لكنها تُعرَض أصيلًا داخل الصفحة، وليس ضمن iframe.
|
||||
|
||||
#### أين يمكن استخدام مكوّنات الواجهة الأمامية
|
||||
|
||||
يمكن عرض مكوّنات الواجهة الأمامية في موقعين داخل Twenty:
|
||||
|
||||
* **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
|
||||
* **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
|
||||
|
||||
#### مثال أساسي
|
||||
|
||||
أسرع طريقة لرؤية مكوّن أمامي قيد العمل هي تسجيله كأمر. إضافة حقل `command` مع `isPinned: true` يجعلُه يظهر كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة — دون الحاجة إلى تخطيط صفحة:
|
||||
@@ -748,7 +881,7 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
بعد المزامنة باستخدام `yarn twenty dev`، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
|
||||
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty dev --once`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
|
||||
@@ -773,9 +906,13 @@ export default defineFrontComponent({
|
||||
|
||||
إضافةً إلى الأوامر، يمكنك تضمين مكوّن أمامي مباشرةً في صفحة سجل عبر إضافته كودجت في **تخطيط صفحة**. راجع قسم [definePageLayout](#definepagelayout) للتفاصيل.
|
||||
|
||||
#### مكوّنات عديمة الواجهة (`isHeadless: true`)
|
||||
#### عديم الرأس مقابل غير عديم الرأس
|
||||
|
||||
المكوّنات عديمة الواجهة لا تُظهر واجهة مرئية لكنها ما تزال تُشغِّل منطق React. يكون هذا مفيدًا لـ **مكوّنات التأثير** — وهي مكوّنات تُجري تأثيرات جانبية عند التثبيت، مثل مزامنة البيانات، بدء مؤقّت، الاستماع إلى الأحداث، أو إطلاق إشعار.
|
||||
تأتي مكوّنات الواجهة الأمامية بوضعَي عرض يتحكّم بهما الخيار `isHeadless`:
|
||||
|
||||
**غير عديم الرأس (افتراضي)** — يعرض المكوّن واجهة مستخدم مرئية. عند تشغيله من قائمة الأوامر يفتح في اللوحة الجانبية. هذا هو السلوك الافتراضي عندما تكون `isHeadless` تساوي `false` أو يتم تجاهلها.
|
||||
|
||||
**عديم الرأس (`isHeadless: true`)** — يتم تركيب المكوّن بشكل غير مرئي في الخلفية. لا يفتح اللوحة الجانبية. تم تصميم المكوّنات عديمة الرأس لإجراءات تنفّذ منطقًا ثم تُزيل تركيبها ذاتيًا — على سبيل المثال، تشغيل مهمة غير متزامنة، أو الانتقال إلى صفحة، أو إظهار نافذة تأكيد منبثقة. تتوافق بشكل طبيعي مع مكوّنات Command في SDK الموصوفة أدناه.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
|
||||
@@ -802,6 +939,89 @@ export default defineFrontComponent({
|
||||
|
||||
نظرًا لأن المكوّن يُرجع `null`، فإن Twenty يتخطّى عرض حاوية له — ولن تظهر مساحة فارغة في التخطيط. لا يزال لدى المكوّن إمكانية الوصول إلى جميع الخطافات وواجهة برمجة الاتصال مع المضيف.
|
||||
|
||||
#### مكوّنات Command في SDK
|
||||
|
||||
توفر حزمة `twenty-sdk` أربعة مكوّنات مساعدة من نوع Command مصممة للمكوّنات عديمة الرأس في الواجهة الأمامية. كل مكوّن ينفّذ إجراءً عند التركيب، ويتعامل مع الأخطاء بعرض إشعار Snackbar، ويزيل تركيب مكوّن الواجهة الأمامية تلقائيًا عند الانتهاء.
|
||||
|
||||
استوردها من `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — يشغّل رد نداء غير متزامن عبر الخاصية `execute`.
|
||||
* **`CommandLink`** — ينتقل إلى مسار في التطبيق. الخصائص: `to`، `params`، `queryParams`، `options`.
|
||||
* **`CommandModal`** — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء `execute`. الخصائص: `title`، `subtitle`، `execute`، `confirmButtonText`، `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — يفتح صفحة محدّدة في اللوحة الجانبية. الخصائص: `page`، `pageTitle`، `pageIcon`.
|
||||
|
||||
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم `Command` لتشغيل إجراء من قائمة الأوامر:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
ومثال يستخدم `CommandModal` لطلب التأكيد قبل التنفيذ:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### الوصول إلى سياق وقت التشغيل
|
||||
|
||||
داخل مكوّنك، استخدم خطافات SDK للوصول إلى المستخدم الحالي، والسجل، ومثيل المكوّن:
|
||||
@@ -858,6 +1078,50 @@ export default defineFrontComponent({
|
||||
| `unmountFrontComponent()` | إلغاء تركيب المكوّن |
|
||||
| `updateProgress(progress)` | تحديث مؤشّر التقدّم |
|
||||
|
||||
فيما يلي مثال يستخدم واجهة برمجة تطبيقات المضيف لعرض Snackbar وإغلاق اللوحة الجانبية بعد اكتمال الإجراء:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
#### خيارات الأوامر
|
||||
|
||||
إضافة حقل `command` إلى `defineFrontComponent` تُسجِّل المكوّن في قائمة الأوامر (Cmd+K). إذا كانت قيمة `isPinned` هي `true`، فسيظهر أيضًا كزر إجراء سريع في الزاوية العلوية اليمنى من الصفحة.
|
||||
@@ -1263,7 +1527,7 @@ export default defineFrontComponent({
|
||||
|
||||
### كيف يعمل التجميع
|
||||
|
||||
تستخدم خطوة البناء (`yarn twenty dev` أو `yarn twenty build`) أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية وكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
|
||||
تستخدم خطوة البناء أداة esbuild لإنتاج ملف واحد مستقل لكل دالة منطقية ولكل مكوّن أمامي. تُضمَّن جميع الحزم المستوردة داخل الحزمة.
|
||||
|
||||
**الدوال المنطقية** تعمل في بيئة Node.js. الوحدات المدمجة في Node (`fs` و`path` و`crypto` و`http` وغيرها) متاحة ولا تحتاج إلى تثبيت.
|
||||
|
||||
@@ -1678,8 +1942,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute pre-install or post-install functions
|
||||
yarn twenty exec --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
@@ -1712,3 +1975,86 @@ yarn twenty uninstall
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty uninstall --yes
|
||||
```
|
||||
|
||||
## إدارة الريموتات
|
||||
|
||||
**الريموت** هو خادم Twenty يتصل به تطبيقك. أثناء الإعداد، تُنشئ أداة إنشاء الهيكل واحدًا لك تلقائيًا. يمكنك إضافة ريموتات أخرى أو التبديل بينها في أي وقت.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
تُخزَّن بيانات اعتمادك في `~/.twenty/config.json`.
|
||||
|
||||
## التكامل المستمر (CI) باستخدام GitHub Actions
|
||||
|
||||
تولّد أداة إنشاء الهيكل سير عمل GitHub Actions جاهزًا للاستخدام في `.github/workflows/ci.yml`. يشغّل اختبارات التكامل لديك تلقائيًا عند كل دفع إلى `main` وعلى طلبات السحب.
|
||||
|
||||
سير العمل:
|
||||
|
||||
1. يجلب الشيفرة الخاصة بك
|
||||
2. يشغّل خادم Twenty مؤقتًا باستخدام الإجراء `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. يثبّت التبعيات باستخدام `yarn install --immutable`
|
||||
4. يشغّل `yarn test` مع حقن `TWENTY_API_URL` و`TWENTY_API_KEY` من مخرجات الإجراء
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
لا تحتاج إلى تهيئة أي أسرار — إذ يبدأ إجراء `spawn-twenty-docker-image` خادم Twenty عابرًا مباشرة في المشغّل ويُخرِج تفاصيل الاتصال. يتم توفير السر `GITHUB_TOKEN` تلقائيًا من قِبل GitHub.
|
||||
|
||||
لتثبيت إصدار محدّد من Twenty بدلًا من `latest`، غيّر متغير البيئة `TWENTY_VERSION` في أعلى سير العمل.
|
||||
|
||||
@@ -7,7 +7,9 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
التطبيقات حاليًا في مرحلة الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
</Warning>
|
||||
|
||||
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول ووظائف منطقية ومهارات ذكاء اصطناعي ومكونات واجهة مستخدم مخصصة — جميعها تُدار ككود.
|
||||
## ما هي التطبيقات؟
|
||||
|
||||
تتيح لك التطبيقات توسيع Twenty باستخدام كائنات وحقول مخصّصة ووظائف منطقية ومكوّنات الواجهة الأمامية ومهارات الذكاء الاصطناعي وغير ذلك — جميعها تُدار ككود. بدلًا من تكوين كل شيء عبر واجهة المستخدم، تعرّف نموذج بياناتك ومنطقك في TypeScript وتقوم بنشره إلى مساحة عمل واحدة أو أكثر.
|
||||
|
||||
## المتطلبات الأساسية
|
||||
|
||||
@@ -17,7 +19,9 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
|
||||
* **Yarn 4** — يأتي مع Node.js عبر Corepack. قم بتمكينه عبر تشغيل `corepack enable`
|
||||
* **Docker** — [نزّل من هنا](https://www.docker.com/products/docker-desktop/). مطلوب لتشغيل مثيل محلي من Twenty. غير مطلوب إذا كان لديك خادم Twenty قيد التشغيل بالفعل.
|
||||
|
||||
## الخطوة 1: إنشاء هيكل تطبيقك
|
||||
## قم بإنشاء تطبيقك الأول
|
||||
|
||||
### أنشئ هيكل تطبيقك
|
||||
|
||||
افتح الطرفية وشغّل:
|
||||
|
||||
@@ -29,18 +33,7 @@ npx create-twenty-app@latest my-twenty-app
|
||||
|
||||
سيؤدي ذلك إلى إنشاء مجلد جديد باسم `my-twenty-app` يحتوي على كل ما تحتاجه.
|
||||
|
||||
<Note>
|
||||
أداة إنشاء الهيكل تدعم الأعلام التالية:
|
||||
|
||||
* `--minimal` — إنشاء الهيكل للملفات الأساسية فقط، بدون أمثلة (افتراضي)
|
||||
* `--exhaustive` — إنشاء الهيكل لجميع كيانات الأمثلة
|
||||
* `--name <name>` — تعيين اسم التطبيق (يتخطى المطالبة)
|
||||
* `--display-name <displayName>` — تعيين اسم العرض (يتخطى المطالبة)
|
||||
* `--description <description>` — تعيين الوصف (يتخطى المطالبة)
|
||||
* `--skip-local-instance` — تخطي مطالبة إعداد الخادم المحلي
|
||||
</Note>
|
||||
|
||||
## الخطوة 2: إعداد مثيل محلي من Twenty
|
||||
### إعداد مثيل محلي من Twenty
|
||||
|
||||
ستسأل أداة إنشاء الهيكل:
|
||||
|
||||
@@ -53,7 +46,7 @@ npx create-twenty-app@latest my-twenty-app
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="هل يجب بدء المثيل المحلي؟" />
|
||||
</div>
|
||||
|
||||
## الخطوة 3: سجّل الدخول إلى مساحة العمل الخاصة بك
|
||||
### سجّل الدخول إلى مساحة العمل الخاصة بك
|
||||
|
||||
بعد ذلك، ستُفتح نافذة متصفح تعرض صفحة تسجيل الدخول الخاصة بـ Twenty. سجّل الدخول باستخدام حساب العرض التوضيحي المُجهَّز مسبقًا:
|
||||
|
||||
@@ -64,7 +57,7 @@ npx create-twenty-app@latest my-twenty-app
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="شاشة تسجيل الدخول إلى Twenty" />
|
||||
</div>
|
||||
|
||||
## الخطوة 4: تفويض التطبيق
|
||||
### قم بتفويض التطبيق
|
||||
|
||||
بعد تسجيل الدخول، ستظهر لك شاشة تفويض. يتيح هذا لتطبيقك التفاعل مع مساحة العمل الخاصة بك.
|
||||
|
||||
@@ -80,7 +73,7 @@ npx create-twenty-app@latest my-twenty-app
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="تم إنشاء هيكل التطبيق بنجاح" />
|
||||
</div>
|
||||
|
||||
## الخطوة 5: ابدأ التطوير
|
||||
### ابدأ التطوير
|
||||
|
||||
انتقل إلى مجلد تطبيقك الجديد وابدأ خادم التطوير:
|
||||
|
||||
@@ -105,7 +98,22 @@ yarn twenty dev --verbose
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="مخرجات الطرفية في وضع التطوير" />
|
||||
</div>
|
||||
|
||||
## الخطوة 6: اعرض تطبيقك في Twenty
|
||||
#### مزامنة لمرة واحدة باستخدام `yarn twenty dev --once`
|
||||
|
||||
إذا كنت لا تريد تشغيل مراقب في الخلفية (مثلًا في خط أنابيب CI، أو خطاف Git، أو سير عمل مُؤتمت عبر سكربت)، فمرِّر الخيار `--once`. يُشغِّل خط الأنابيب نفسه مثل `yarn twenty dev` — إنشاء بيان البناء، تجميع الملفات، الرفع، المزامنة، إعادة توليد عميل API مضبوط الأنواع — ولكنه **ينهي التنفيذ فور اكتمال المزامنة**:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| أمر | السلوك | متى يُستخدم |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | يراقب ملفات المصدر ويعيد المزامنة عند كل تغيير. يستمر بالتشغيل حتى توقفه. | تطوير محلي تفاعلي — تريد لوحة حالة مباشرة وحلقة تغذية راجعة فورية. |
|
||||
| `yarn twenty dev --once` | يجري عملية بناء واحدة + مزامنة واحدة، ثم ينهي التنفيذ برمز `0` عند النجاح أو `1` عند الفشل. | البرامج النصية، وCI، وخطافات ما قبل الالتزام، ووكلاء الذكاء الاصطناعي، وأي سير عمل غير تفاعلي. |
|
||||
|
||||
كلا الوضعين يتطلبان خادم Twenty يعمل في وضع التطوير وجهة بعيدة موثَّقة — تنطبق المتطلبات المسبقة نفسها.
|
||||
|
||||
### اعرض تطبيقك في Twenty
|
||||
|
||||
افتح [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) في متصفحك. انتقل إلى **Settings > Apps** واختر علامة التبويب **Developer**. يُفترض أن ترى تطبيقك مُدرجًا تحت **Your Apps**:
|
||||
|
||||
@@ -133,13 +141,28 @@ yarn twenty dev --verbose
|
||||
|
||||
أنت جاهز تمامًا! حرّر أي ملف في `src/` وسيتم التقاط التغييرات تلقائيًا.
|
||||
|
||||
انتقل إلى [بناء التطبيقات](/l/ar/developers/extend/apps/building) للحصول على دليل مفصّل حول إنشاء الكائنات، ودوال المنطق، ومكونات الواجهة الأمامية، والمهارات، والمزيد.
|
||||
---
|
||||
|
||||
## ما الذي يمكنك بناؤه
|
||||
|
||||
تتكون التطبيقات من **كيانات** — يُعرَّف كل منها كملف TypeScript يحتوي على `export default` واحد:
|
||||
|
||||
| كيان | ماذا يفعل |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **الكائنات والحقول** | عرّف نماذج بيانات مخصّصة (مثل Post Card، Invoice) مع حقول محددة النوع |
|
||||
| **الوظائف المنطقية** | دوال TypeScript على جانب الخادم يتم تشغيلها عبر مسارات HTTP، وجداول cron، أو أحداث قاعدة البيانات |
|
||||
| **المكوّنات الأمامية** | مكوّنات React تُعرَض داخل واجهة مستخدم Twenty (اللوحة الجانبية، الودجات، قائمة الأوامر) |
|
||||
| **المهارات والوكلاء** | قدرات الذكاء الاصطناعي — تعليمات قابلة لإعادة الاستخدام ومساعدون مستقلون ذاتيًا |
|
||||
| **طرق العرض والتنقّل** | طرق عرض القوائم مُعدّة مسبقًا وعناصر قائمة الشريط الجانبي لكائناتك |
|
||||
| **تخطيطات الصفحات** | صفحات تفاصيل سجلات مخصصة تتضمن علامات تبويب وعناصر واجهة |
|
||||
|
||||
انتقل إلى [بناء التطبيقات](/l/ar/developers/extend/apps/building) للاطلاع على دليل مفصّل لكل نوع من الكيانات.
|
||||
|
||||
---
|
||||
|
||||
## هيكل المشروع
|
||||
|
||||
تولّد أداة إنشاء الهيكل بنية الملفات التالية (مُبيّنة بوضع `--exhaustive` الذي يتضمن أمثلة لكل نوع من الكيانات):
|
||||
تولّد أداة إنشاء الهيكل بنية الملفات التالية:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -148,160 +171,50 @@ my-twenty-app/
|
||||
.gitignore
|
||||
.nvmrc
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
install-state.gz
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
LLMS.md
|
||||
README.md
|
||||
.github/
|
||||
└── workflows/
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── __tests__/
|
||||
│ ├── setup-test.ts # Test setup (server health check, config)
|
||||
│ └── app-install.integration-test.ts # Example integration test
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── create-hello-world-company.ts # Example logic function using CoreApiClient
|
||||
│ ├── pre-install.ts # Runs before installation
|
||||
│ └── post-install.ts # Runs after installation
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── page-layouts/
|
||||
│ └── example-record-page-layout.ts # Example page layout with front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Example AI agent skill definition
|
||||
└── agents/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── default-role.ts # Default role for logic functions
|
||||
├── constants/
|
||||
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
|
||||
└── __tests__/
|
||||
├── setup-test.ts # Test setup (server health check, config)
|
||||
└── app-install.integration-test.ts # Integration test
|
||||
```
|
||||
|
||||
افتراضيًا (`--minimal`)، تُنشأ الملفات الأساسية فقط: `application-config.ts`، `roles/default-role.ts`، `logic-functions/pre-install.ts`، و`logic-functions/post-install.ts`. استخدم `--exhaustive` لتضمين جميع ملفات الأمثلة الموضّحة أعلاه.
|
||||
### البدء من مثال
|
||||
|
||||
للبدء من مثال أكثر اكتمالًا يضم كائنات وحقولًا مخصّصة، ودوال المنطق، ومكوّنات الواجهة الأمامية، وغير ذلك، استخدم الخيار `--example`:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
تُستمد الأمثلة من الدليل [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) على GitHub. يمكنك أيضًا إنشاء هيكل لكيانات فردية داخل مشروع قائم باستخدام `yarn twenty add` (انظر [بناء التطبيقات](/l/ar/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
|
||||
|
||||
### الملفات الرئيسية
|
||||
|
||||
| ملف / مجلد | الغرض |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `package.json` | يصرّح باسم تطبيقك وإصداره واعتماداته. يتضمن نصًا برمجيًا باسم `twenty` بحيث يمكنك تشغيل `yarn twenty help` للاطلاع على جميع الأوامر. |
|
||||
| `src/application-config.ts` | **مطلوب.** ملف الإعداد الرئيسي لتطبيقك. |
|
||||
| `src/roles/` | يعرِّف الأدوار التي تتحكم بما يمكن لدوال المنطق الوصول إليه. |
|
||||
| `src/logic-functions/` | دوال على جانب الخادم يتم تشغيلها عبر المسارات، وجداول cron، أو أحداث قاعدة البيانات. |
|
||||
| `src/front-components/` | مكونات React تُعرَض داخل واجهة مستخدم Twenty. |
|
||||
| `src/objects/` | تعريفات كائنات مخصّصة لتوسيع نموذج البيانات لديك. |
|
||||
| `src/fields/` | حقول مخصّصة تُضاف إلى الكائنات الموجودة. |
|
||||
| `src/views/` | تكوينات العروض المحفوظة. |
|
||||
| `src/navigation-menu-items/` | روابط مخصّصة في شريط التنقل الجانبي. |
|
||||
| `src/skills/` | مهارات توسّع قدرات وكلاء الذكاء الاصطناعي في Twenty. |
|
||||
| `src/agents/` | وكلاء ذكاء اصطناعي مع موجهات مخصّصة. |
|
||||
| `src/page-layouts/` | تخطيطات صفحات مخصّصة لعرض السجلات. |
|
||||
| `src/__tests__/` | اختبارات تكامل (إعداد + اختبار مثال). |
|
||||
| `public/` | أصول ثابتة (صور، خطوط) تُقدَّم مع تطبيقك. |
|
||||
| ملف / مجلد | الغرض |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `package.json` | يصرّح باسم تطبيقك وإصداره واعتماداته. يتضمن نصًا برمجيًا باسم `twenty` بحيث يمكنك تشغيل `yarn twenty help` للاطلاع على جميع الأوامر. |
|
||||
| `src/application-config.ts` | **مطلوب.** ملف الإعداد الرئيسي لتطبيقك. |
|
||||
| `src/default-role.ts` | الدور الافتراضي الذي يتحكم بما يمكن لدوال المنطق الوصول إليه. |
|
||||
| `src/constants/universal-identifiers.ts` | معرّفات UUID وبيانات التعريف للتطبيق، والمولَّدة تلقائيًا (اسم العرض، الوصف). |
|
||||
| `src/__tests__/` | اختبارات تكامل (إعداد + اختبار مثال). |
|
||||
| `public/` | أصول ثابتة (صور، خطوط) تُقدَّم مع تطبيقك. |
|
||||
|
||||
## إدارة الريموتات
|
||||
## خادم التطوير المحلي
|
||||
|
||||
**الريموت** هو خادم Twenty يتصل به تطبيقك. أثناء الإعداد، تُنشئ أداة إنشاء الهيكل واحدًا لك تلقائيًا. يمكنك إضافة ريموتات أخرى أو التبديل بينها في أي وقت.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
تُخزَّن بيانات اعتمادك في `~/.twenty/config.json`.
|
||||
|
||||
## خادم التطوير المحلي (`yarn twenty server`)
|
||||
|
||||
يمكن لأداة سطر الأوامر (CLI) إدارة خادم Twenty محلي يعمل داخل Docker. هذا هو الخادم نفسه الذي يبدأ تلقائيًا عند إنشاء هيكل تطبيق باستخدام `create-twenty-app`، لكن يمكنك أيضًا إدارته يدويًا.
|
||||
|
||||
### بدء الخادم
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server start
|
||||
```
|
||||
|
||||
سيؤدي ذلك إلى سحب صورة Docker `twentycrm/twenty-app-dev:latest` (إن لم تكن موجودة بالفعل)، وإنشاء حاوية باسم `twenty-app-dev`، وبدء تشغيلها على المنفذ **2020**. تنتظر أداة CLI حتى يجتاز الخادم فحص السلامة قبل الإنهاء.
|
||||
|
||||
يتم إنشاء حجمين في Docker للاحتفاظ بالبيانات بين عمليات إعادة التشغيل:
|
||||
|
||||
* `twenty-app-dev-data` — قاعدة بيانات PostgreSQL
|
||||
* `twenty-app-dev-storage` — تخزين ملفات
|
||||
|
||||
إذا كان المنفذ 2020 مستخدمًا بالفعل، يمكنك البدء على منفذ مختلف:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server start --port 3030
|
||||
```
|
||||
|
||||
تقوم أداة CLI تلقائيًا بتهيئة قيم `NODE_PORT` و`SERVER_URL` الداخلية في الحاوية لتطابق المنفذ المختار، بحيث تعمل دوال المنطق وOAuth وكل الشبكات الداخلية الأخرى بشكل صحيح.
|
||||
|
||||
بمجرد البدء، يُسجَّل الخادم تلقائيًا كـ `local` remote في إعدادات CLI لديك.
|
||||
|
||||
### التحقق من حالة الخادم
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server status
|
||||
```
|
||||
|
||||
يعرض ما إذا كان الخادم قيد التشغيل، وعنوان URL الخاص به، وبيانات اعتماد تسجيل الدخول الافتراضية (`tim@apple.dev` / `tim@apple.dev`).
|
||||
|
||||
### عرض سجلات الخادم
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server logs
|
||||
```
|
||||
|
||||
يبث سجلات الحاوية. استخدم `--lines` للتحكّم بعدد الأسطر الحديثة المراد عرضها:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server logs --lines 100
|
||||
```
|
||||
|
||||
### إيقاف الخادم
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server stop
|
||||
```
|
||||
|
||||
يوقف الحاوية. تُحفَظ بياناتك في أحجام Docker — وستُستأنف الحالة مع عملية `start` التالية من حيث توقفت.
|
||||
|
||||
### إعادة تعيين الخادم
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
يزيل الحاوية **و** يحذف كلا حجمي Docker، ممّا يمحو جميع البيانات. ستنشئ عملية `start` التالية مثيلًا جديدًا من البداية.
|
||||
|
||||
<Note>
|
||||
يتطلّب الخادم أن يكون **Docker** قيد التشغيل. إذا ظهرت لك رسالة خطأ "Docker not running"، فتأكّد من تشغيل Docker Desktop (أو خادوم Docker).
|
||||
</Note>
|
||||
|
||||
### مرجع الأوامر
|
||||
لقد قامت أداة إنشاء الهيكل بالفعل بتشغيل خادم Twenty محليًا لك. لإدارته لاحقًا، استخدم `yarn twenty server`:
|
||||
|
||||
| أمر | الوصف |
|
||||
| -------------------------------------- | --------------------------------------------- |
|
||||
@@ -313,65 +226,11 @@ yarn twenty server reset
|
||||
| `yarn twenty server logs --lines 100` | عرض آخر 100 سطر من السجلات |
|
||||
| `yarn twenty server reset` | حذف جميع البيانات والبدء من جديد |
|
||||
|
||||
## التكامل المستمر (CI) باستخدام GitHub Actions
|
||||
يتم الاحتفاظ بالبيانات عبر عمليات إعادة التشغيل في وحدتي تخزين Docker (`twenty-app-dev-data` لـ PostgreSQL، و`twenty-app-dev-storage` للملفات). استخدم `reset` لمسح كل شيء والبدء من جديد.
|
||||
|
||||
تولّد أداة إنشاء الهيكل سير عمل GitHub Actions جاهزًا للاستخدام في `.github/workflows/ci.yml`. يشغّل اختبارات التكامل لديك تلقائيًا عند كل دفع إلى `main` وعلى طلبات السحب.
|
||||
|
||||
سير العمل:
|
||||
|
||||
1. يجلب الشيفرة الخاصة بك
|
||||
2. يشغّل خادم Twenty مؤقتًا باستخدام الإجراء `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. يثبّت الاعتمادات باستخدام `yarn install --immutable`
|
||||
4. يشغّل `yarn test` مع حقن `TWENTY_API_URL` و`TWENTY_API_KEY` من مخرجات الإجراء
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
لا تحتاج إلى تهيئة أي أسرار — إذ يبدأ إجراء `spawn-twenty-docker-image` خادم Twenty عابرًا مباشرة في المشغّل ويُخرِج تفاصيل الاتصال. يتم توفير السر `GITHUB_TOKEN` تلقائيًا من قِبل GitHub.
|
||||
|
||||
لتثبيت إصدار محدّد من Twenty بدلًا من `latest`، غيّر متغير البيئة `TWENTY_VERSION` في أعلى سير العمل.
|
||||
<Note>
|
||||
يتطلّب الخادم أن يكون **Docker** قيد التشغيل. إذا ظهرت لك رسالة خطأ "Docker not running"، فتأكّد من تشغيل Docker Desktop (أو خادوم Docker).
|
||||
</Note>
|
||||
|
||||
## إعداد يدوي (بدون المهيئ)
|
||||
|
||||
|
||||
@@ -52,7 +52,11 @@ yarn twenty deploy
|
||||
|
||||
### مشاركة تطبيق منشور
|
||||
|
||||
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. لمشاركة تطبيق منشور:
|
||||
<Warning>
|
||||
تُعد مشاركة التطبيقات الخاصة (tarball) عبر مساحات العمل ميزة ضمن **Enterprise**. ستعرض علامة التبويب **Distribution** مطالبة بالترقية بدلًا من عناصر التحكم في المشاركة حتى تحتوي مساحة العمل لديك على مفتاح Enterprise صالح. اطلع على [الإعدادات > لوحة الإدارة > Enterprise](/settings/admin-panel#enterprise) لتنشيطه.
|
||||
</Warning>
|
||||
|
||||
تطبيقات tarball لا تُدرَج في السوق العامة، لذا لن تكتشفها مساحات العمل الأخرى على الخادم نفسه عبر الاستعراض. بمجرد أن تصبح مساحة العمل لديك ضمن خطة Enterprise، يمكنك مشاركة تطبيق تم نشره كما يلي:
|
||||
|
||||
1. اذهب إلى **الإعدادات > التطبيقات > التسجيلات** وافتح تطبيقك
|
||||
2. في علامة التبويب **التوزيع**، انقر **نسخ رابط المشاركة**
|
||||
@@ -60,18 +64,20 @@ yarn twenty deploy
|
||||
|
||||
يستخدم رابط المشاركة عنوان URL الأساسي للخادم (من دون أي نطاق فرعي لمساحة عمل)، لذا يعمل مع أي مساحة عمل على الخادم.
|
||||
|
||||
<Warning>
|
||||
مشاركة التطبيقات الخاصة هي ميزة ضمن باقة Enterprise. اذهب إلى [الإعدادات > لوحة الإدارة > Enterprise](/settings/admin-panel#enterprise) لتمكينها.
|
||||
</Warning>
|
||||
|
||||
### إدارة الإصدارات
|
||||
|
||||
عند تحديث تطبيق tarball منشور مسبقًا، يشترط الخادم أن تكون قيمة `version` في `package.json` **أعلى قطعًا** (وفق ترتيب [الإصدار الدلالي](https://semver.org)) من الإصدار المنشور حاليًا. إعادة نشر الإصدار نفسه، أو دفع إصدار أدنى، يُرفَض قبل تخزين ملف tarball — سترى خطأ `VERSION_ALREADY_EXISTS` من CLI.
|
||||
|
||||
لطرح تحديث:
|
||||
|
||||
1. ارفع قيمة الحقل `version` في ملف `package.json`
|
||||
1. قم بزيادة الحقل `version` في ملف `package.json` (مثلًا: `1.2.3` → `1.2.4`، `1.3.0`، أو `2.0.0`)
|
||||
2. شغّل `yarn twenty deploy` (أو `yarn twenty deploy --remote production`)
|
||||
3. سترى مساحات العمل التي ثبّتت التطبيق الترقية متاحة في إعداداتها
|
||||
|
||||
<Note>
|
||||
علامات ما قبل الإصدار تعمل كما هو متوقع: زيادة `1.0.0-rc.1` → `1.0.0-rc.2` مسموح بها، ويُعترَف بالإصدار النهائي مثل `1.0.0` على أنه أعلى من `1.0.0-rc.5`. يجب أن يكون الإصدار في `package.json` بنفسه سلسلة semver صالحة.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
## النشر على npm
|
||||
@@ -189,3 +195,12 @@ jobs:
|
||||
```bash filename="Terminal"
|
||||
yarn twenty install
|
||||
```
|
||||
|
||||
<Note>
|
||||
يفرض الخادم اعتماد إصدارات semver عند التثبيت، بما يعكس القواعد المطبّقة عند النشر:
|
||||
|
||||
* تثبيت الإصدار نفسه المثبّت بالفعل في مساحة عملك يُرفَض بخطأ `APP_ALREADY_INSTALLED`.
|
||||
* تثبيت إصدار أدنى من الإصدار المثبّت حاليًا يُرفَض بخطأ `CANNOT_DOWNGRADE_APPLICATION`.
|
||||
|
||||
لتثبيت إصدار أحدث، انشره (deploy) أو انشره إلى السجل (publish) أولًا، ثم أعد تشغيل `yarn twenty install`.
|
||||
</Note>
|
||||
|
||||
@@ -140,12 +140,12 @@ description: دليل شامل لاستكشاف أخطاء استيراد CSV و
|
||||
#### تاريخ
|
||||
|
||||
**المشكلة:** تنسيق تاريخ غير معروف
|
||||
**الحل:** استخدم تنسيقًا متّسقًا في جميع أنحاء الملف
|
||||
**الحل:** استخدم تنسيق `YYYY-MM-DD` بشكل متّسق في جميع أنحاء الملف
|
||||
|
||||
```
|
||||
✓ 2024-03-15 (YYYY-MM-DD - recommended)
|
||||
✓ 03/15/2024 (MM/DD/YYYY)
|
||||
✓ 15/03/2024 (DD/MM/YYYY)
|
||||
✓ 2024-03-15 (YYYY-MM-DD)
|
||||
❌ 03/15/2024 (MM/DD/YYYY)
|
||||
❌ 15/03/2024 (DD/MM/YYYY)
|
||||
```
|
||||
|
||||
#### هاتف
|
||||
|
||||
@@ -103,8 +103,6 @@ description: دليل كامل خطوة بخطوة لتنسيق بياناتك
|
||||
استخدم تنسيقًا موحدًا في كامل ملفك:
|
||||
|
||||
* `YYYY-MM-DD` (مُوصى به): `2024-03-15`
|
||||
* `MM/DD/YYYY`: `03/15/2024`
|
||||
* `DD/MM/YYYY`: `15/03/2024`
|
||||
* ISO 8601: `2024-03-15T10:30:00Z`
|
||||
|
||||
### حقول الأرقام
|
||||
|
||||
+1
-17
@@ -47,25 +47,9 @@ npx nx run twenty-server:database:reset
|
||||
#### Pro objekty ve schématech Core/Metadata (TypeORM)
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
|
||||
npx nx run twenty-server:database:migrate:generate
|
||||
```
|
||||
|
||||
#### Pro objekty Pracovní plochy
|
||||
|
||||
Nejsou žádné soubory migrací, migrace jsou generovány automaticky pro každou pracovní plochu, uloženy v databázi a aplikovány tímto příkazem
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
||||
Tímto se databáze smaže a znovu se spustí migrace a seedování.
|
||||
|
||||
Před spuštěním tohoto příkazu si nezapomeňte zálohovat všechna data, která chcete zachovat.
|
||||
|
||||
</Warning>
|
||||
|
||||
## Technologický stack
|
||||
|
||||
Twenty primárně používá NestJS pro backend.
|
||||
|
||||
@@ -81,7 +81,7 @@ import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
|
||||
displayName: 'My Twenty App',
|
||||
description: 'My first Twenty app',
|
||||
icon: 'IconWorld',
|
||||
@@ -644,49 +644,15 @@ export default defineLogicFunction({
|
||||
**Napište kvalitní `description`.** Agenti AI se spoléhají na pole funkce `description` při rozhodování, kdy nástroj použít. Buďte konkrétní ohledně toho, co nástroj dělá a kdy se má volat.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Definujte předinstalační logickou funkci (jedna na aplikaci)">
|
||||
|
||||
Předinstalační funkce je logická funkce, která se automaticky spouští před instalací vaší aplikace v pracovním prostoru. To je užitečné pro validační úlohy, kontrolu předpokladů nebo přípravu stavu pracovního prostoru před zahájením hlavní instalace.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'e0604b9e-e946-456b-886d-3f27d9a6b324',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* Předinstalační funkce používají `definePreInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
|
||||
* Na jednu aplikaci je povolena pouze jedna předinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
|
||||
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `preInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
|
||||
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší přípravné úlohy.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePostInstallLogicFunction" description="Definujte postinstalační logickou funkci (jedna na aplikaci)">
|
||||
|
||||
Postinstalační funkce je logická funkce, která se automaticky spouští po instalaci vaší aplikace do pracovního prostoru. To je užitečné pro jednorázové úlohy nastavení, jako je naplnění výchozími daty, vytvoření počátečních záznamů nebo konfigurace nastavení pracovního prostoru.
|
||||
Postinstalační funkce je logická funkce, která se spustí automaticky, jakmile je instalace vaší aplikace v pracovním prostoru dokončena. Server ji provede **poté**, co byla synchronizována metadata aplikace a vygenerován klient SDK, takže je pracovní prostor plně připraven k použití a nové schéma je zavedeno. Mezi typické případy použití patří naplnění výchozími daty, vytvoření počátečních záznamů, konfigurace nastavení pracovního prostoru nebo zřizování prostředků ve službách třetích stran.
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
@@ -695,6 +661,8 @@ export default definePostInstallLogicFunction({
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
@@ -707,16 +675,181 @@ yarn twenty exec --postInstall
|
||||
|
||||
Hlavní body:
|
||||
* Postinstalační funkce používají `definePostInstallLogicFunction()` — specializovanou variantu, která vynechává nastavení spouštěčů (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
* Obslužná funkce (handler) obdrží `InstallLogicFunctionPayload` s `{ previousVersion: string }` — verzi aplikace, která byla dříve nainstalována (nebo prázdný řetězec při čisté instalaci).
|
||||
* Obslužná funkce obdrží `InstallPayload` s `{ previousVersion?: string; newVersion: string }` — `newVersion` je verze, která se instaluje, a `previousVersion` je verze, která byla nainstalována dříve (nebo `undefined` při čisté instalaci). Tyto hodnoty použijte k rozlišení čistých instalací od aktualizací a ke spuštění migrační logiky specifické pro verzi.
|
||||
* **Kdy se hook spouští**: ve výchozím nastavení pouze při čistých instalacích. Předejte `shouldRunOnVersionUpgrade: true`, pokud chcete, aby se spouštěl i při aktualizaci aplikace z předchozí verze. Pokud je vynechán, příznak má výchozí hodnotu `false` a při aktualizacích se hook přeskočí.
|
||||
* **Model provádění — ve výchozím nastavení asynchronní, synchronní volitelně**: příznak `shouldRunSynchronously` určuje *jak* se spouští post-install.
|
||||
* `shouldRunSynchronously: false` *(výchozí)* — hook je **zařazen do fronty zpráv** s `retryLimit: 3` a běží asynchronně ve workeru. Odezva instalace se vrátí hned po zařazení úlohy do fronty, takže pomalá nebo chybující obslužná funkce neblokuje volajícího. Worker se pokusí o opakování až třikrát. **Použijte pro dlouho běžící úlohy** — plnění velkých datových sad, volání pomalých externích API, zřizování externích prostředků, cokoli, co by mohlo přesáhnout rozumné časové okno HTTP odezvy.
|
||||
* `shouldRunSynchronously: true` — hook se provádí **inline během instalačního procesu** (stejný vykonavatel jako pre-install). Instalační požadavek blokuje, dokud obslužná funkce nedokončí, a pokud vyvolá výjimku, volající instalace obdrží `POST_INSTALL_ERROR`. Žádné automatické opakování. **Použijte pro rychlé úlohy, které se musí dokončit před odpovědí** — například vrácení validační chyby uživateli nebo rychlé nastavení, na kterém bude klient záviset ihned po návratu volání instalace. Mějte na paměti, že v době, kdy se spustí post-install, už byla migrace metadat aplikována, takže selhání v synchronním režimu změny schématu **ne**vrací zpět — pouze odhalí chybu.
|
||||
* Ujistěte se, že vaše obslužná funkce je idempotentní. V asynchronním režimu se může fronta pokusit až třikrát; v obou režimech se může hook znovu spustit při aktualizacích, pokud je `shouldRunOnVersionUpgrade: true`.
|
||||
* Proměnné prostředí `APPLICATION_ID`, `APP_ACCESS_TOKEN` a `API_URL` jsou dostupné uvnitř obslužné funkce (stejně jako u jakékoli jiné logické funkce), takže můžete volat Twenty API s aplikačním přístupovým tokenem omezeným na vaši aplikaci.
|
||||
* Na jednu aplikaci je povolena pouze jedna postinstalační funkce. Sestavení manifestu skončí chybou, pokud je zjištěna více než jedna.
|
||||
* Identifikátor `universalIdentifier` funkce se během sestavení automaticky nastaví v manifestu aplikace jako `postInstallLogicFunctionUniversalIdentifier` — není potřeba jej uvádět v `defineApplication()`.
|
||||
* Atributy funkce `universalIdentifier`, `shouldRunOnVersionUpgrade` a `shouldRunSynchronously` jsou během buildu automaticky připojeny k manifestu aplikace do pole `postInstallLogicFunction` — není potřeba je uvádět v `defineApplication()`.
|
||||
* Výchozí časový limit je nastaven na 300 sekund (5 minut), aby umožnil delší úlohy nastavení, jako je naplnění daty.
|
||||
* **Nespouští se v režimu dev**: když je aplikace registrována lokálně (pomocí `yarn twenty dev`), server zcela přeskočí instalační tok a synchronizuje soubory přímo prostřednictvím sledovače CLI — takže se post-install v režimu dev nikdy nespustí bez ohledu na `shouldRunSynchronously`. Použijte `yarn twenty exec --postInstall` k ručnímu spuštění nad běžícím pracovním prostorem.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="Definujte předinstalační logickou funkci (jedna na aplikaci)">
|
||||
|
||||
Funkce pre-install je logická funkce, která se během instalace spouští automaticky, **před aplikováním migrace metadat pracovního prostoru**. Má stejný tvar payloadu jako post-install (`InstallPayload`), ale je zařazena dříve v instalačním toku, aby mohla připravit stav, na němž nadcházející migrace závisí — typické použití zahrnuje zálohování dat, ověření kompatibility s novým schématem nebo archivaci záznamů, které se chystají přeuspořádat nebo odstranit.
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
Předinstalační funkci můžete také kdykoli spustit ručně pomocí CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec --preInstall
|
||||
```
|
||||
|
||||
Hlavní body:
|
||||
* Funkce pre-install používají `definePreInstallLogicFunction()` — stejné specializované nastavení jako u post-install, pouze připojené k jiné fázi životního cyklu.
|
||||
* Obě obslužné funkce pre- i post-install přijímají stejný typ `InstallPayload`: `{ previousVersion?: string; newVersion: string }`. Importujte jej jednou a znovu použijte pro oba hooky.
|
||||
* **Kdy se hook spouští**: umístěn těsně před migrací metadat pracovního prostoru (`synchronizeFromManifest`). Před spuštěním server provede čistě aditivní "zjednodušenou synchronizaci", která v metadatech pracovního prostoru zaregistruje pre-install funkci **nové** verze — ničeho dalšího se nedotkne — a poté ji spustí. Protože tato synchronizace je pouze aditivní, objekty, pole a data předchozí verze zůstávají při spuštění vaší obslužné funkce zachována: můžete bezpečně číst a zálohovat stav před migrací.
|
||||
* **Model provádění**: pre-install se provádí **synchronně** a **blokuje instalaci**. Pokud obslužná funkce vyvolá výjimku, instalace se přeruší ještě před aplikováním jakýchkoli změn schématu — pracovní prostor zůstane na předchozí verzi v konzistentním stavu. Je to záměrné: pre-install je vaše poslední šance odmítnout rizikovou aktualizaci.
|
||||
* Stejně jako u post-install je na jednu aplikaci povolena pouze jedna funkce pre-install. Během buildu je automaticky připojena k manifestu aplikace pod `preInstallLogicFunction`.
|
||||
* **Nespouští se v režimu dev**: stejně jako u post-install — u lokálně registrovaných aplikací je instalační tok zcela přeskočen, takže se pre-install pod `yarn twenty dev` nikdy nespustí. Použijte `yarn twenty exec --preInstall` k ručnímu spuštění.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Pre-install vs post-install: kdy použít který" description="Výběr správného instalačního hooku">
|
||||
|
||||
Oba hooky jsou součástí téhož instalačního toku a přijímají stejný `InstallPayload`. Rozdíl je v tom, **kdy** se spouštějí vzhledem k migraci metadat pracovního prostoru, a to určuje, jakých dat se mohou bezpečně dotýkat.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ install flow │
|
||||
│ │
|
||||
│ upload package → [pre-install] → metadata migration → │
|
||||
│ generate SDK → [post-install] │
|
||||
│ │
|
||||
│ old schema visible new schema visible │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Pre-install je vždy **synchronní** (blokuje instalaci a může ji přerušit). Post-install je **ve výchozím nastavení asynchronní** — zařazen do workeru s automatickými pokusy o opakování — ale může přejít na synchronní provádění pomocí `shouldRunSynchronously: true`. Viz accordion `definePostInstallLogicFunction` výše, kdy použít jednotlivé režimy.
|
||||
|
||||
**Použijte `post-install` pro cokoli, co vyžaduje existenci nového schématu.** To je běžný případ:
|
||||
|
||||
* Plnění výchozími daty (vytváření počátečních záznamů, výchozích pohledů, demo obsahu) vůči nově přidaným objektům a polím.
|
||||
* Registrace webhooků u služeb třetích stran poté, co má aplikace své přihlašovací údaje.
|
||||
* Volání vlastního API k dokončení nastavení, které závisí na synchronizovaných metadatech.
|
||||
* Idempotentní logika "zajisti, že to existuje", která má při každé aktualizaci uvést stav do souladu — kombinujte s `shouldRunOnVersionUpgrade: true`.
|
||||
|
||||
Příklad — po instalaci naplňte výchozí záznam `PostCard`:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
});
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Použijte `pre-install`, pokud by migrace jinak zničila nebo poškodila existující data.** Protože pre-install běží proti *předchozímu* schématu a jeho selhání vrací aktualizaci zpět, je to správné místo pro cokoli rizikového:
|
||||
|
||||
* **Zálohování dat, která se chystají odstranit nebo přeuspořádat** — např. odstraňujete pole ve verzi v2 a potřebujete jeho hodnoty zkopírovat do jiného pole nebo je před spuštěním migrace exportovat do úložiště.
|
||||
* **Archivace záznamů, které by nové omezení zneplatnilo** — např. pole se stává `NOT NULL` a je třeba nejprve smazat nebo opravit řádky s hodnotami null.
|
||||
* **Ověření kompatibility a odmítnutí aktualizace, pokud nelze aktuální data čistě migrovat** — vyhoďte výjimku z obslužné funkce a instalace se ukončí bez provedených změn. Je to bezpečnější, než zjistit nekompatibilitu uprostřed migrace.
|
||||
* **Přejmenování nebo změna klíčů dat** před změnou schématu, která by ztratila vazby.
|
||||
|
||||
Příklad — archivujte záznamy před destruktivní migrací:
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk';
|
||||
import { createClient } from './generated/client';
|
||||
|
||||
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
|
||||
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
|
||||
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Backs up legacy notes into description before the v2 migration.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**Zlaté pravidlo:**
|
||||
|
||||
| Chcete… | Použít |
|
||||
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Naplňte výchozí data, nakonfigurujte pracovní prostor, zaregistrujte externí prostředky | `post-install` |
|
||||
| Spusťte dlouho běžící plnění nebo volání třetích stran, která by neměla blokovat odezvu instalace | `post-install` (výchozí — `shouldRunSynchronously: false`, s opakovanými pokusy workeru) |
|
||||
| Spusťte rychlé nastavení, na které bude volající spoléhat ihned po návratu volání instalace | `post-install` s `shouldRunSynchronously: true` |
|
||||
| Čtěte nebo zálohujte data, která by nadcházející migrace ztratila | `pre-install` |
|
||||
| Odmítněte aktualizaci, která by poškodila existující data | `pre-install` (vyhoďte výjimku z obslužné funkce) |
|
||||
| Spouštějte srovnání stavu při každé aktualizaci | `post-install` s `shouldRunOnVersionUpgrade: true` |
|
||||
| Proveďte jednorázové nastavení pouze při první instalaci | `post-install` s `shouldRunOnVersionUpgrade: false` (výchozí) |
|
||||
|
||||
<Note>
|
||||
Pokud si nejste jisti, výchozí volbou je **post-install**. Po pre-install sáhněte pouze tehdy, když je samotná migrace destruktivní a potřebujete zachytit předchozí stav, než zmizí.
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineFrontComponent" description="Definujte frontendové komponenty pro vlastní uživatelské rozhraní">
|
||||
|
||||
Frontendové komponenty jsou React komponenty, které se vykreslují přímo v uživatelském rozhraní Twenty. Běží v **izolovaném Web Workeru** s využitím Remote DOM — váš kód je sandboxovaný, ale vykresluje se nativně na stránce, nikoli v iframu.
|
||||
|
||||
#### Kde lze použít front komponenty
|
||||
|
||||
Front komponenty se mohou vykreslovat na dvou místech v rámci Twenty:
|
||||
|
||||
* **Postranní panel** — Ne-headless front komponenty se otevírají v pravém postranním panelu. Toto je výchozí chování, když je front komponenta vyvolána z menu příkazů.
|
||||
* **Widgety (nástěnky a stránky záznamů)** — Front komponenty lze vkládat jako widgety do rozložení stránek. Při konfiguraci nástěnky nebo rozložení stránky záznamu mohou uživatelé přidat widget front komponenty.
|
||||
|
||||
#### Základní příklad
|
||||
|
||||
Nejrychlejší způsob, jak vidět frontendovou komponentu v akci, je zaregistrovat ji jako **příkaz**. Přidáním pole `command` s `isPinned: true` se zobrazí jako tlačítko rychlé akce v pravém horním rohu stránky — není potřeba žádné rozvržení stránky:
|
||||
@@ -749,7 +882,7 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
Po synchronizaci pomocí `yarn twenty dev` se rychlá akce zobrazí v pravém horním rohu stránky:
|
||||
Po synchronizaci pomocí `yarn twenty dev` (nebo po jednorázovém spuštění `yarn twenty dev --once`) se rychlá akce zobrazí v pravém horním rohu stránky:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Tlačítko rychlé akce v pravém horním rohu" />
|
||||
@@ -774,9 +907,13 @@ Kliknutím na něj vykreslíte komponentu přímo ve stránce.
|
||||
|
||||
Mimo příkazy můžete frontendovou komponentu vložit přímo na stránku záznamu přidáním jako widget v **rozvržení stránky**. Podrobnosti viz sekce [definePageLayout](#definepagelayout).
|
||||
|
||||
#### Headless komponenty (`isHeadless: true`)
|
||||
#### Headless vs. ne-headless
|
||||
|
||||
Headless komponenty nevykreslují viditelné UI, ale stále spouštějí logiku Reactu. To je užitečné pro **efektové komponenty** — komponenty, které po připojení provádějí vedlejší efekty, jako je synchronizace dat, spuštění časovače, naslouchání událostem nebo vyvolání oznámení.
|
||||
Front komponenty existují ve dvou režimech vykreslování řízených volbou `isHeadless`:
|
||||
|
||||
**Ne-headless (výchozí)** — Komponenta vykreslí viditelné uživatelské rozhraní. Po vyvolání z menu příkazů se otevře v postranním panelu. Toto je výchozí chování, když je `isHeadless` `false` nebo když tato volba není uvedena.
|
||||
|
||||
**Headless (`isHeadless: true`)** — Komponenta se neviditelně inicializuje na pozadí. Neotevírá postranní panel. Headless komponenty jsou určené pro akce, které provedou logiku a poté se odpojí — například spuštění asynchronního úkolu, navigaci na stránku nebo zobrazení potvrzovacího modálního okna. Přirozeně se hodí ke komponentám SDK Command popsaným níže.
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent, useRecordId, enqueueSnackbar } from 'twenty-sdk';
|
||||
@@ -803,6 +940,89 @@ export default defineFrontComponent({
|
||||
|
||||
Protože komponenta vrací `null`, Twenty přeskočí vykreslení kontejneru — v rozvržení se neobjeví žádné prázdné místo. Komponenta má však stále přístup ke všem hookům a API komunikace s hostitelem.
|
||||
|
||||
#### Komponenty SDK Command
|
||||
|
||||
Balíček `twenty-sdk` poskytuje čtyři pomocné komponenty Command navržené pro headless front komponenty. Každá komponenta při připojení provede akci, chyby zpracuje zobrazením oznámení ve snackbaru a po dokončení automaticky odpojí front komponentu.
|
||||
|
||||
Importujte je z `twenty-sdk/command`:
|
||||
|
||||
* **`Command`** — Spustí asynchronní callback přes prop `execute`.
|
||||
* **`CommandLink`** — Naviguje na cestu v aplikaci. Props: `to`, `params`, `queryParams`, `options`.
|
||||
* **`CommandModal`** — Otevře potvrzovací modální okno. Pokud uživatel potvrdí, provede callback `execute`. Props: `title`, `subtitle`, `execute`, `confirmButtonText`, `confirmButtonAccent`.
|
||||
* **`CommandOpenSidePanelPage`** — Otevře konkrétní stránku postranního panelu. Props: `page`, `pageTitle`, `pageIcon`.
|
||||
|
||||
Zde je kompletní příklad headless front komponenty, která pomocí `Command` spouští akci z menu příkazů:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createTask: {
|
||||
__args: { data: { title: 'Created by my app' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return <Command execute={execute} />;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
name: 'run-action',
|
||||
description: 'Creates a task from the command menu',
|
||||
component: RunAction,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
A příklad s použitím `CommandModal` k vyžádání potvrzení před provedením:
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
// perform the deletion
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandModal
|
||||
title="Delete draft?"
|
||||
subtitle="This action cannot be undone."
|
||||
execute={execute}
|
||||
confirmButtonText="Delete"
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
|
||||
name: 'delete-draft',
|
||||
description: 'Deletes a draft with confirmation',
|
||||
component: DeleteDraft,
|
||||
isHeadless: true,
|
||||
command: {
|
||||
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
|
||||
label: 'Delete draft',
|
||||
icon: 'IconTrash',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Přístup k běhovému kontextu
|
||||
|
||||
Uvnitř komponenty použijte hooky SDK pro přístup k aktuálnímu uživateli, záznamu a instanci komponenty:
|
||||
@@ -859,6 +1079,50 @@ Frontendové komponenty mohou pomocí funkcí z `twenty-sdk` vyvolávat navigaci
|
||||
| `unmountFrontComponent()` | Odmontovat komponentu |
|
||||
| `updateProgress(progress)` | Aktualizovat indikátor průběhu |
|
||||
|
||||
Zde je příklad, který používá hostitelské API k zobrazení snackbaru a zavření postranního panelu po dokončení akce:
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
updateTask: {
|
||||
__args: { id: recordId, data: { status: 'ARCHIVED' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Record archived',
|
||||
variant: 'success',
|
||||
});
|
||||
|
||||
await closeSidePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<p>Archive this record?</p>
|
||||
<button onClick={handleArchive}>Archive</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
|
||||
name: 'archive-record',
|
||||
description: 'Archives the current record',
|
||||
component: ArchiveRecord,
|
||||
});
|
||||
```
|
||||
|
||||
#### Možnosti příkazu
|
||||
|
||||
Přidání pole `command` do `defineFrontComponent` zaregistruje komponentu v příkazovém menu (Cmd+K). Pokud je `isPinned` nastaveno na `true`, zobrazí se také jako tlačítko rychlé akce v pravém horním rohu stránky.
|
||||
@@ -1264,7 +1528,7 @@ export default defineFrontComponent({
|
||||
|
||||
### Jak funguje bundlování
|
||||
|
||||
Krok sestavení (`yarn twenty dev` nebo `yarn twenty build`) používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
|
||||
Krok sestavení používá esbuild k vytvoření jediného samostatného souboru pro každou logickou funkci a každou frontendovou komponentu. Všechny importované balíčky jsou vloženy přímo do bundlu.
|
||||
|
||||
**Logické funkce** běží v prostředí Node.js. Vestavěné moduly Node (`fs`, `path`, `crypto`, `http` atd.) jsou k dispozici a není je třeba instalovat.
|
||||
|
||||
@@ -1679,8 +1943,7 @@ yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
# Pass a JSON payload
|
||||
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute pre-install or post-install functions
|
||||
yarn twenty exec --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
@@ -1713,3 +1976,86 @@ yarn twenty uninstall
|
||||
# Skip the confirmation prompt
|
||||
yarn twenty uninstall --yes
|
||||
```
|
||||
|
||||
## Správa vzdálených serverů
|
||||
|
||||
**Remote** je server Twenty, ke kterému se vaše aplikace připojuje. Během nastavení jej generátor kostry automaticky vytvoří. Můžete kdykoli přidat další vzdálené servery nebo mezi nimi přepínat.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Vaše přihlašovací údaje jsou uloženy v `~/.twenty/config.json`.
|
||||
|
||||
## CI s GitHub Actions
|
||||
|
||||
Generátor kostry vytvoří připravený k použití workflow GitHub Actions v `.github/workflows/ci.yml`. Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů.
|
||||
|
||||
Workflow:
|
||||
|
||||
1. Načte váš kód (checkout).
|
||||
2. Spustí dočasný server Twenty pomocí akce `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Nainstaluje závislosti pomocí `yarn install --immutable`
|
||||
4. Spustí `yarn test` s proměnnými `TWENTY_API_URL` a `TWENTY_API_KEY` vloženými z výstupů akce
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
Není potřeba konfigurovat žádné secrets — akce `spawn-twenty-docker-image` spustí efemérní server Twenty přímo v runneru a vypíše podrobnosti připojení. Secret `GITHUB_TOKEN` je poskytován GitHubem automaticky.
|
||||
|
||||
Chcete-li připnout konkrétní verzi Twenty místo `latest`, změňte proměnnou prostředí `TWENTY_VERSION` na začátku workflow.
|
||||
|
||||
@@ -7,7 +7,9 @@ description: Vytvořte svou první aplikaci Twenty během několika minut.
|
||||
Aplikace jsou aktuálně v alfa fázi. Funkce funguje, ale stále se vyvíjí.
|
||||
</Warning>
|
||||
|
||||
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, AI schopnosti a komponenty uživatelského rozhraní — vše je spravováno jako kód.
|
||||
## Co jsou aplikace?
|
||||
|
||||
Aplikace vám umožňují rozšířit Twenty o vlastní objekty, pole, logické funkce, frontendové komponenty, AI schopnosti a další — vše je spravováno jako kód. Místo konfigurace všeho přes uživatelské rozhraní definujete v TypeScriptu svůj datový model a logiku a nasadíte je do jednoho nebo více pracovních prostorů.
|
||||
|
||||
## Předpoklady
|
||||
|
||||
@@ -17,7 +19,9 @@ Než začnete, ujistěte se, že máte ve svém počítači nainstalováno násl
|
||||
* **Yarn 4** — Dodává se s Node.js prostřednictvím Corepacku. Povolte jej spuštěním `corepack enable`
|
||||
* **Docker** — [Stáhnout zde](https://www.docker.com/products/docker-desktop/). Nutné pro spuštění lokální instance Twenty. Není potřeba, pokud už máte spuštěný server Twenty.
|
||||
|
||||
## Krok 1: Vytvořte kostru své aplikace
|
||||
## Vytvořte svou první aplikaci
|
||||
|
||||
### Vytvořte kostru své aplikace
|
||||
|
||||
Otevřete terminál a spusťte:
|
||||
|
||||
@@ -29,18 +33,7 @@ Budete vyzváni k zadání názvu a popisu své aplikace. Stisknutím **Enter**
|
||||
|
||||
Tím se vytvoří nová složka s názvem `my-twenty-app` se vším potřebným.
|
||||
|
||||
<Note>
|
||||
Generátor kostry podporuje tyto přepínače:
|
||||
|
||||
* `--minimal` — vygeneruje pouze nezbytné soubory, bez příkladů (výchozí)
|
||||
* `--exhaustive` — vygeneruje všechny ukázkové entity
|
||||
* `--name <name>` — nastaví název aplikace (přeskočí výzvu)
|
||||
* `--display-name <displayName>` — nastaví zobrazovaný název (přeskočí výzvu)
|
||||
* `--description <description>` — nastaví popis (přeskočí výzvu)
|
||||
* `--skip-local-instance` — přeskočí výzvu k nastavení lokálního serveru
|
||||
</Note>
|
||||
|
||||
## Krok 2: Nastavte lokální instanci Twenty
|
||||
### Nastavte lokální instanci Twenty
|
||||
|
||||
Generátor kostry se zeptá:
|
||||
|
||||
@@ -53,7 +46,7 @@ Generátor kostry se zeptá:
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Spustit lokální instanci?" />
|
||||
</div>
|
||||
|
||||
## Krok 3: Přihlaste se do svého pracovního prostoru
|
||||
### Přihlaste se do svého pracovního prostoru
|
||||
|
||||
Poté se otevře okno prohlížeče se stránkou přihlášení do Twenty. Přihlaste se předpřipraveným demo účtem:
|
||||
|
||||
@@ -64,7 +57,7 @@ Poté se otevře okno prohlížeče se stránkou přihlášení do Twenty. Přih
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Přihlašovací obrazovka Twenty" />
|
||||
</div>
|
||||
|
||||
## Krok 4: Autorizujte aplikaci
|
||||
### Autorizujte aplikaci
|
||||
|
||||
Po přihlášení uvidíte autorizační obrazovku. Tím umožníte vaší aplikaci pracovat s vaším pracovním prostorem.
|
||||
|
||||
@@ -80,7 +73,7 @@ Po autorizaci váš terminál potvrdí, že je vše nastaveno.
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="Aplikace byla úspěšně vygenerována" />
|
||||
</div>
|
||||
|
||||
## Krok 5: Začněte vyvíjet
|
||||
### Začněte vyvíjet
|
||||
|
||||
Přejděte do nové složky aplikace a spusťte vývojový server:
|
||||
|
||||
@@ -105,7 +98,22 @@ Vývojový režim je k dispozici pouze na instancích Twenty běžících v rež
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Výstup terminálu ve vývojovém režimu" />
|
||||
</div>
|
||||
|
||||
## Krok 6: Zobrazte svou aplikaci v Twenty
|
||||
#### Jednorázová synchronizace pomocí `yarn twenty dev --once`
|
||||
|
||||
Pokud nechcete, aby na pozadí běžel watcher (například v CI pipeline, git hooku nebo skriptovaném workflow), použijte příznak `--once`. Spouští stejnou pipeline jako `yarn twenty dev` — sestaví manifest, zabalí soubory, nahraje, synchronizuje, znovu vygeneruje typovaného klienta API — ale **ukončí se, jakmile se synchronizace dokončí**:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
```
|
||||
|
||||
| Příkaz | Chování | Kdy použít |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `yarn twenty dev` | Sleduje vaše zdrojové soubory a při každé změně znovu spustí synchronizaci. Zůstává spuštěný, dokud jej nezastavíte. | Interaktivní lokální vývoj — chcete živý panel stavu a okamžitou zpětnovazebnou smyčku. |
|
||||
| `yarn twenty dev --once` | Provede jedno sestavení + synchronizaci, poté ukončí běh s kódem `0` při úspěchu nebo `1` při neúspěchu. | Skripty, CI, pre-commit hooky, AI agenti a jakýkoli neinteraktivní pracovní postup. |
|
||||
|
||||
Oba režimy vyžadují server Twenty běžící v režimu vývoje a autentizovaný remote — platí stejné požadavky.
|
||||
|
||||
### Zobrazte svou aplikaci v Twenty
|
||||
|
||||
Otevřete ve svém prohlížeči [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). Přejděte do **Settings > Apps** a vyberte kartu **Developer**. Vaše aplikace by měla být uvedena v části **Your Apps**:
|
||||
|
||||
@@ -133,13 +141,28 @@ Přepněte na kartu **Content**, abyste viděli vše, co vaše aplikace poskytuj
|
||||
|
||||
Vše je připraveno! Upravte libovolný soubor v `src/` a změny se automaticky projeví.
|
||||
|
||||
Přejděte na [Tvorba aplikací](/l/cs/developers/extend/apps/building) pro podrobný průvodce vytvářením objektů, logických funkcí, frontendových komponent, dovedností a dalšího.
|
||||
---
|
||||
|
||||
## Co můžete vytvořit
|
||||
|
||||
Aplikace se skládají z **entit** — každá je definována jako soubor TypeScriptu s jediným `export default`:
|
||||
|
||||
| Entita | K čemu slouží |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Objekty a pole** | Definujte vlastní datové modely (např. Post Card, Invoice) s typovanými poli |
|
||||
| **Logické funkce** | Serverové funkce v TypeScriptu spouštěné HTTP trasami, plánovačem cron nebo událostmi databáze |
|
||||
| **Frontendové komponenty** | Komponenty Reactu, které se vykreslují v uživatelském rozhraní Twenty (postranní panel, widgety, příkazová nabídka) |
|
||||
| **Dovednosti a agenti** | Schopnosti AI — opakovaně použitelné pokyny a autonomní asistenti |
|
||||
| **Pohledy a navigace** | Předkonfigurované seznamové pohledy a položky postranní nabídky pro vaše objekty |
|
||||
| **Rozvržení stránek** | Vlastní stránky detailu záznamu s kartami a widgety |
|
||||
|
||||
Přejděte na [Tvorba aplikací](/l/cs/developers/extend/apps/building) pro podrobný průvodce každým typem entity.
|
||||
|
||||
---
|
||||
|
||||
## Struktura projektu
|
||||
|
||||
Generátor kostry vytvoří následující strukturu souborů (zobrazeno v režimu `--exhaustive`, který zahrnuje příklady pro každý typ entity):
|
||||
Nástroj pro vytvoření kostry vygeneruje následující strukturu souborů:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -148,160 +171,50 @@ my-twenty-app/
|
||||
.gitignore
|
||||
.nvmrc
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
install-state.gz
|
||||
.oxlintrc.json
|
||||
tsconfig.json
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
tsconfig.spec.json # TypeScript config for tests
|
||||
vitest.config.ts # Vitest test runner configuration
|
||||
LLMS.md
|
||||
README.md
|
||||
.github/
|
||||
└── workflows/
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
└── ci.yml # GitHub Actions CI workflow
|
||||
public/ # Public assets (images, fonts, etc.)
|
||||
src/
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── __tests__/
|
||||
│ ├── setup-test.ts # Test setup (server health check, config)
|
||||
│ └── app-install.integration-test.ts # Example integration test
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ ├── create-hello-world-company.ts # Example logic function using CoreApiClient
|
||||
│ ├── pre-install.ts # Runs before installation
|
||||
│ └── post-install.ts # Runs after installation
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── page-layouts/
|
||||
│ └── example-record-page-layout.ts # Example page layout with front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
├── skills/
|
||||
│ └── example-skill.ts # Example AI agent skill definition
|
||||
└── agents/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
├── application-config.ts # Required — main application configuration
|
||||
├── default-role.ts # Default role for logic functions
|
||||
├── constants/
|
||||
│ └── universal-identifiers.ts # Auto-generated UUIDs and app metadata
|
||||
└── __tests__/
|
||||
├── setup-test.ts # Test setup (server health check, config)
|
||||
└── app-install.integration-test.ts # Integration test
|
||||
```
|
||||
|
||||
Ve výchozím nastavení (`--minimal`) se vytvoří pouze základní soubory: `application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts` a `logic-functions/post-install.ts`. Pro zahrnutí všech ukázkových souborů výše použijte `--exhaustive`.
|
||||
### Začínáme s příkladem
|
||||
|
||||
Chcete-li začít s úplnějším příkladem s vlastními objekty, poli, logickými funkcemi, frontendovými komponentami a dalšími, použijte přepínač `--example`:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app --example postcard
|
||||
```
|
||||
|
||||
Příklady pocházejí z adresáře [twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples) na GitHubu. Můžete také vytvořit kostru jednotlivých entit v existujícím projektu pomocí `yarn twenty add` (viz [Tvorba aplikací](/l/cs/developers/extend/apps/building#scaffolding-entities-with-yarn-twenty-add)).
|
||||
|
||||
### Klíčové soubory
|
||||
|
||||
| Soubor / Složka | Účel |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `package.json` | Definuje název, verzi a závislosti vaší aplikace. Obsahuje skript `twenty`, takže můžete spustit `yarn twenty help` a zobrazit všechny příkazy. |
|
||||
| `src/application-config.ts` | **Povinné.** Hlavní konfigurační soubor vaší aplikace. |
|
||||
| `src/roles/` | Definuje role, které určují, k čemu mají vaše logické funkce přístup. |
|
||||
| `src/logic-functions/` | Serverové funkce spouštěné trasami, plánovačem cron nebo událostmi databáze. |
|
||||
| `src/front-components/` | Komponenty Reactu, které se vykreslují uvnitř uživatelského rozhraní Twenty. |
|
||||
| `src/objects/` | Vlastní definice objektů pro rozšíření vašeho datového modelu. |
|
||||
| `src/fields/` | Vlastní pole přidaná k existujícím objektům. |
|
||||
| `src/views/` | Konfigurace uložených zobrazení. |
|
||||
| `src/navigation-menu-items/` | Vlastní odkazy v postranní navigaci. |
|
||||
| `src/skills/` | Dovednosti, které rozšiřují možnosti AI agentů Twenty. |
|
||||
| `src/agents/` | AI agenti s vlastními prompty. |
|
||||
| `src/page-layouts/` | Vlastní rozvržení stránek pro zobrazení záznamů. |
|
||||
| `src/__tests__/` | Integrační testy (nastavení + ukázkový test). |
|
||||
| `public/` | Statická aktiva (obrázky, písma) poskytovaná s vaší aplikací. |
|
||||
| Soubor / Složka | Účel |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `package.json` | Definuje název, verzi a závislosti vaší aplikace. Obsahuje skript `twenty`, takže můžete spustit `yarn twenty help` a zobrazit všechny příkazy. |
|
||||
| `src/application-config.ts` | **Povinné.** Hlavní konfigurační soubor vaší aplikace. |
|
||||
| `src/default-role.ts` | Výchozí role, která určuje, k čemu mají vaše logické funkce přístup. |
|
||||
| `src/constants/universal-identifiers.ts` | Automaticky generovaná UUID a metadata aplikace (zobrazovaný název, popis). |
|
||||
| `src/__tests__/` | Integrační testy (nastavení + ukázkový test). |
|
||||
| `public/` | Statická aktiva (obrázky, písma) poskytovaná s vaší aplikací. |
|
||||
|
||||
## Správa vzdálených serverů
|
||||
## Lokální vývojový server
|
||||
|
||||
**Remote** je server Twenty, ke kterému se vaše aplikace připojuje. Během nastavení jej generátor kostry automaticky vytvoří. Můžete kdykoli přidat další vzdálené servery nebo mezi nimi přepínat.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
yarn twenty remote add
|
||||
|
||||
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a remote non-interactively (useful for CI)
|
||||
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
|
||||
|
||||
# List all configured remotes
|
||||
yarn twenty remote list
|
||||
|
||||
# Switch the active remote
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Vaše přihlašovací údaje jsou uloženy v `~/.twenty/config.json`.
|
||||
|
||||
## Lokální vývojový server (`yarn twenty server`)
|
||||
|
||||
CLI může spravovat lokální server Twenty běžící v Dockeru. Jde o stejný server, který se spustí automaticky při vytvoření kostry aplikace pomocí `create-twenty-app`, ale můžete jej spravovat i ručně.
|
||||
|
||||
### Spuštění serveru
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server start
|
||||
```
|
||||
|
||||
Stáhne image Dockeru `twentycrm/twenty-app-dev:latest` (pokud již není k dispozici), vytvoří kontejner s názvem `twenty-app-dev` a spustí jej na portu **2020**. CLI čeká, dokud server neprojde kontrolou stavu, než vrátí řízení.
|
||||
|
||||
Vytvoří se dva svazky Dockeru pro zachování dat mezi restartováními:
|
||||
|
||||
* `twenty-app-dev-data` — databáze PostgreSQL
|
||||
* `twenty-app-dev-storage` — úložiště souborů
|
||||
|
||||
Pokud je port 2020 již používán, můžete spustit na jiném portu:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server start --port 3030
|
||||
```
|
||||
|
||||
CLI automaticky nakonfiguruje interní `NODE_PORT` a `SERVER_URL` kontejneru tak, aby odpovídaly zvolenému portu, takže logické funkce, OAuth a veškerá ostatní vnitřní síťová komunikace fungují správně.
|
||||
|
||||
Po spuštění je server automaticky zaregistrován jako `local` remote ve vaší konfiguraci CLI.
|
||||
|
||||
### Kontrola stavu serveru
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server status
|
||||
```
|
||||
|
||||
Zobrazí, zda server běží, jeho URL a výchozí přihlašovací údaje (`tim@apple.dev` / `tim@apple.dev`).
|
||||
|
||||
### Zobrazení protokolů serveru
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server logs
|
||||
```
|
||||
|
||||
Streamuje protokoly kontejneru. Pomocí `--lines` ovládnete, kolik posledních řádků se má zobrazit:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server logs --lines 100
|
||||
```
|
||||
|
||||
### Zastavení serveru
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server stop
|
||||
```
|
||||
|
||||
Zastaví kontejner. Vaše data jsou zachována ve svazcích Dockeru — další `start` naváže tam, kde jste skončili.
|
||||
|
||||
### Resetování serveru
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
Odstraní kontejner **a** smaže oba svazky Dockeru, čímž vymaže všechna data. Další `start` vytvoří čistou instanci.
|
||||
|
||||
<Note>
|
||||
Server vyžaduje, aby **Docker** běžel. Pokud vidíte chybu "Docker not running", ujistěte se, že je spuštěný Docker Desktop (nebo démon Dockeru).
|
||||
</Note>
|
||||
|
||||
### Přehled příkazů
|
||||
Nástroj pro vytvoření kostry vám již spustil lokální server Twenty. Pro jeho správu později použijte `yarn twenty server`:
|
||||
|
||||
| Příkaz | Popis |
|
||||
| -------------------------------------- | ------------------------------------------------------ |
|
||||
@@ -313,65 +226,11 @@ Odstraní kontejner **a** smaže oba svazky Dockeru, čímž vymaže všechna da
|
||||
| `yarn twenty server logs --lines 100` | Zobrazí posledních 100 řádků logu |
|
||||
| `yarn twenty server reset` | Smaže všechna data a začne znovu |
|
||||
|
||||
## CI s GitHub Actions
|
||||
Data přetrvávají při restartech ve dvou svazcích Dockeru (`twenty-app-dev-data` pro PostgreSQL, `twenty-app-dev-storage` pro soubory). Pomocí `reset` vymažte vše a začněte znovu.
|
||||
|
||||
Generátor kostry vytvoří připravený k použití workflow GitHub Actions v `.github/workflows/ci.yml`. Automaticky spouští integrační testy při každém pushi do `main` a u pull requestů.
|
||||
|
||||
Workflow:
|
||||
|
||||
1. Načte váš kód (checkout).
|
||||
2. Spustí dočasný server Twenty pomocí akce `twentyhq/twenty/.github/actions/spawn-twenty-docker-image`
|
||||
3. Nainstaluje závislosti pomocí `yarn install --immutable`
|
||||
4. Spustí `yarn test` s proměnnými `TWENTY_API_URL` a `TWENTY_API_KEY` vloženými z výstupů akce
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
Není potřeba konfigurovat žádné secrets — akce `spawn-twenty-docker-image` spustí efemérní server Twenty přímo v runneru a vypíše podrobnosti připojení. Secret `GITHUB_TOKEN` je poskytován GitHubem automaticky.
|
||||
|
||||
Chcete-li připnout konkrétní verzi Twenty místo `latest`, změňte proměnnou prostředí `TWENTY_VERSION` na začátku workflow.
|
||||
<Note>
|
||||
Server vyžaduje, aby **Docker** běžel. Pokud vidíte chybu "Docker not running", ujistěte se, že je spuštěný Docker Desktop (nebo démon Dockeru).
|
||||
</Note>
|
||||
|
||||
## Ruční nastavení (bez scaffolderu)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user