Compare commits
146
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
133c01923d | ||
|
|
91d6a69bf8 | ||
|
|
c6f11d8adb | ||
|
|
fe4512f80c | ||
|
|
f2fa958f20 | ||
|
|
eb51f8e6da | ||
|
|
b6c62b3812 | ||
|
|
c676e46a1d | ||
|
|
c7e89a18f0 | ||
|
|
58336fb70f | ||
|
|
87efaf2ff8 | ||
|
|
dedcf4e9b9 | ||
|
|
928194cee4 | ||
|
|
13a357ab9f | ||
|
|
b1a7c5c4d6 | ||
|
|
a8c445a1c2 | ||
|
|
a74edbf715 | ||
|
|
0ae62898a3 | ||
|
|
fb5b68f1d8 | ||
|
|
2d4f6f8f6f | ||
|
|
a82739cdb1 | ||
|
|
058414fae5 | ||
|
|
994215e0dc | ||
|
|
e62dfae741 | ||
|
|
9f7c29bce8 | ||
|
|
5a51764b8e | ||
|
|
abdab2fb7e | ||
|
|
bee474afdf | ||
|
|
ab881350b2 | ||
|
|
f38d72a4c2 | ||
|
|
3204175065 | ||
|
|
05a81c82a9 | ||
|
|
731e297147 | ||
|
|
111debc1ce | ||
|
|
770a685556 | ||
|
|
f1dfcfd163 | ||
|
|
1be87eb97b | ||
|
|
1735c7527c | ||
|
|
1bb642d7cd | ||
|
|
48285be164 | ||
|
|
a121d00ddd | ||
|
|
087ee19807 | ||
|
|
c4e55d08ff | ||
|
|
13ff7af297 | ||
|
|
b6b96be603 | ||
|
|
93bd4960e3 | ||
|
|
cb4efb1d0e | ||
|
|
5dfdc1d81d | ||
|
|
32d7fa09a3 | ||
|
|
e552704201 | ||
|
|
ddedecbb36 | ||
|
|
5011e1d77b | ||
|
|
a07337fea0 | ||
|
|
6e36ad9fa2 | ||
|
|
5c745059ad | ||
|
|
6340b9e2f7 | ||
|
|
2c2f66b584 | ||
|
|
95a35f8a1d | ||
|
|
87c519b72f | ||
|
|
1b20bdaf6d | ||
|
|
67866ff59c | ||
|
|
e6f1bdd1c8 | ||
|
|
ba9aa41bba | ||
|
|
06efee1eef | ||
|
|
7a3540788a | ||
|
|
6711b40922 | ||
|
|
c753b2bee1 | ||
|
|
70a060b4ee | ||
|
|
40ff109179 | ||
|
|
48172d60fd | ||
|
|
0b0ffcb8fa | ||
|
|
6552ec83ec | ||
|
|
602db4ffea | ||
|
|
3a9247d9d1 | ||
|
|
6b48f197d4 | ||
|
|
2a8912b17a | ||
|
|
55d675bba7 | ||
|
|
d9eb317bb5 | ||
|
|
3054679411 | ||
|
|
1b1d79b08f | ||
|
|
46e515436e | ||
|
|
49bdcd6bd5 | ||
|
|
3f01249967 | ||
|
|
4b6c8d52e5 | ||
|
|
b470cb21a1 | ||
|
|
172bbd01bc | ||
|
|
58f534939c | ||
|
|
0379aea0b1 | ||
|
|
f3e0c12ce6 | ||
|
|
0641e07ca6 | ||
|
|
dfd28f5b4a | ||
|
|
349bfc8462 | ||
|
|
262f9f5fe1 | ||
|
|
5f558e5539 | ||
|
|
1cb4c98cb3 | ||
|
|
a3c392ce8b | ||
|
|
ab13020e2b | ||
|
|
3f420c84d7 | ||
|
|
0ef4741473 | ||
|
|
b5db955ac8 | ||
|
|
2a6fcfcfb3 | ||
|
|
5bfa4c5c39 | ||
|
|
1685d066be | ||
|
|
6a3281a18d | ||
|
|
741e9a8f81 | ||
|
|
0897575fd0 | ||
|
|
501fcc737f | ||
|
|
c9deab4373 | ||
|
|
c1da7be6d7 | ||
|
|
c59f420d21 | ||
|
|
06d4d62e90 | ||
|
|
eb4665bc98 | ||
|
|
f19fcd0010 | ||
|
|
cb3e32df86 | ||
|
|
db5b4d9c6c | ||
|
|
660536d6bb | ||
|
|
e8f8189167 | ||
|
|
78473a606a | ||
|
|
b21fb4aa6f | ||
|
|
38664249cf | ||
|
|
69542898a1 | ||
|
|
09beddb63d | ||
|
|
2eac82c207 | ||
|
|
f262437da6 | ||
|
|
15d0970f72 | ||
|
|
1adc325887 | ||
|
|
5726bd6e17 | ||
|
|
102b49f919 | ||
|
|
2af3121c51 | ||
|
|
a024a04e01 | ||
|
|
f0c83434a7 | ||
|
|
b699619756 | ||
|
|
b2f053490d | ||
|
|
21de221420 | ||
|
|
d9b3507866 | ||
|
|
b346f4fb59 | ||
|
|
2c5af2654d | ||
|
|
6cbc7725b7 | ||
|
|
744ef3aa9d | ||
|
|
ef92d2d321 | ||
|
|
00c3cd1051 | ||
|
|
99f885306e | ||
|
|
413d1124bb | ||
|
|
ab5fb1f658 | ||
|
|
e4e7137660 | ||
|
|
7fb8cc1c39 |
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Setting up Docker Compose environment...' && cd packages/twenty-docker && cp -n docker-compose.yml docker-compose.dev.yml || true && echo 'Dependencies installed and docker-compose prepared'",
|
||||
"start": "sudo service docker start && echo 'Docker service started' && cd packages/twenty-docker && echo 'Installing yq for YAML processing...' && sudo apt-get update -qq && sudo apt-get install -y wget && wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq && echo 'Patching docker-compose for local development...' && yq eval 'del(.services.server.image)' -i docker-compose.dev.yml && yq eval '.services.server.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.server.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && yq eval 'del(.services.worker.image)' -i docker-compose.dev.yml && yq eval '.services.worker.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.worker.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && echo 'Setting up .env file with database configuration...' && echo 'SERVER_URL=http://localhost:3000' > .env && echo 'APP_SECRET='$(openssl rand -base64 32) >> .env && echo 'PG_DATABASE_PASSWORD='$(openssl rand -hex 16) >> .env && echo 'PG_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres' >> .env && echo 'SIGN_IN_PREFILLED=true' >> .env && echo 'Building and starting services...' && docker-compose -f docker-compose.dev.yml up -d --build && echo 'Waiting for services to initialize...' && sleep 30 && echo 'Checking service health...' && docker-compose -f docker-compose.dev.yml ps && echo 'Environment setup complete!'",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Database Setup & Seed",
|
||||
"command": "sleep 40 && cd packages/twenty-docker && echo 'Waiting for PostgreSQL to be ready...' && until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 5; done && echo 'PostgreSQL is ready!' && echo 'Waiting for Twenty server to be healthy...' && until docker-compose -f docker-compose.dev.yml exec -T server curl --fail http://localhost:3000/healthz 2>/dev/null; do echo 'Waiting for server...'; sleep 5; done && echo 'Server is healthy!' && echo 'Running database setup and seeding...' && docker-compose -f docker-compose.dev.yml exec -T server npx nx database:reset twenty-server && echo 'Database seeded successfully!' && bash"
|
||||
},
|
||||
{
|
||||
"name": "Application Logs",
|
||||
"command": "sleep 35 && cd packages/twenty-docker && echo 'Following application logs...' && docker-compose -f docker-compose.dev.yml logs -f server worker"
|
||||
},
|
||||
{
|
||||
"name": "Service Monitor",
|
||||
"command": "sleep 15 && cd packages/twenty-docker && echo '=== Service Status Monitor ===' && while true; do clear; echo '=== Service Status at $(date) ===' && docker-compose -f docker-compose.dev.yml ps && echo '\\n=== Health Status ===' && (docker-compose -f docker-compose.dev.yml exec -T server curl -s http://localhost:3000/healthz 2>/dev/null && echo '✅ Twenty Server: Healthy') || echo '❌ Twenty Server: Not Ready' && (docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres 2>/dev/null && echo '✅ PostgreSQL: Ready') || echo '❌ PostgreSQL: Not Ready' && echo '\\n=== Database Connection Test ===' && docker-compose -f docker-compose.dev.yml exec -T server node -e \"const { Client } = require('pg'); const client = new Client({connectionString: process.env.PG_DATABASE_URL}); client.connect().then(() => {console.log('✅ Database Connection: OK'); client.end();}).catch(e => console.log('❌ Database Connection: Failed -', e.message));\" || echo 'Connection test failed' && sleep 45; done"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"install": "yarn install",
|
||||
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"start": "(sudo service docker start || service docker start || true) && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Server",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
storage: /tmp/verdaccio-storage
|
||||
auth:
|
||||
htpasswd:
|
||||
file: /tmp/verdaccio-htpasswd
|
||||
max_users: 100
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
packages:
|
||||
'twenty-sdk':
|
||||
access: $all
|
||||
publish: $all
|
||||
'create-twenty-app':
|
||||
access: $all
|
||||
publish: $all
|
||||
'**':
|
||||
access: $all
|
||||
proxy: npmjs
|
||||
log: { type: stdout, format: pretty, level: warn }
|
||||
@@ -0,0 +1,184 @@
|
||||
name: CI Create App E2E
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
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/create-twenty-app/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
create-app-e2e:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-4-cores
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Set CI version and prepare packages for publish
|
||||
run: |
|
||||
CI_VERSION="0.0.0-ci.$(date +%s)"
|
||||
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
|
||||
npx nx run-many -t set-local-version -p twenty-sdk create-twenty-app --releaseVersion=$CI_VERSION
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
npx nx build twenty-sdk
|
||||
npx nx build create-twenty-app
|
||||
|
||||
- name: Install and start Verdaccio
|
||||
run: |
|
||||
npx verdaccio --config .github/verdaccio-config.yaml &
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s http://localhost:4873 > /dev/null 2>&1; then
|
||||
echo "Verdaccio is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Verdaccio... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Publish packages to local registry
|
||||
run: |
|
||||
npm set //localhost:4873/:_authToken "ci-auth-token"
|
||||
|
||||
for pkg in twenty-sdk create-twenty-app; do
|
||||
cd packages/$pkg
|
||||
npm publish --registry http://localhost:4873 --tag ci
|
||||
cd ../..
|
||||
done
|
||||
|
||||
- name: Scaffold app using published create-twenty-app
|
||||
run: |
|
||||
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --exhaustive --display-name "Test App" --description "E2E test app" --skip-local-instance
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
|
||||
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
|
||||
|
||||
- name: Verify installed app versions
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
echo "--- Checking package.json references correct SDK version ---"
|
||||
node -e "
|
||||
const pkg = require('./package.json');
|
||||
const sdkVersion = pkg.devDependencies['twenty-sdk'];
|
||||
if (!sdkVersion.startsWith('0.0.0-ci.')) {
|
||||
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('SDK version in scaffolded app:', sdkVersion);
|
||||
"
|
||||
|
||||
- name: Verify SDK CLI is available
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty --version
|
||||
|
||||
- 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
|
||||
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: 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'
|
||||
|
||||
- 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 --token $SEED_API_KEY --url http://localhost:3000
|
||||
|
||||
- name: Build scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty build
|
||||
test -d .twenty/output
|
||||
|
||||
- name: Execute hello-world logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
ci-create-app-e2e-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-e2e]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -40,7 +40,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
@@ -111,7 +112,7 @@ jobs:
|
||||
run: yarn docs:generate-paths
|
||||
|
||||
- name: Commit artifacts to pull request branch
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
run: |
|
||||
git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json packages/twenty-shared/src/constants/DocumentationPaths.ts
|
||||
if git diff --staged --quiet --exit-code; then
|
||||
@@ -149,4 +150,3 @@ jobs:
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -188,13 +188,22 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## CI Environment (GitHub Actions)
|
||||
## Dev Environment Setup
|
||||
|
||||
When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up.
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
|
||||
- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh`
|
||||
```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.
|
||||
- The script is idempotent and safe to run multiple times.
|
||||
|
||||
**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
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-host/capabilities/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/contribute/capabilities/local-setup)
|
||||
|
||||
# Why Twenty
|
||||
|
||||
@@ -36,7 +36,7 @@ We built Twenty for three reasons:
|
||||
|
||||
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
|
||||
|
||||
**We believe in Open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
**We believe in open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -136,6 +136,14 @@
|
||||
"cache": true,
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"set-local-version": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npm pkg set version={args.releaseVersion}"
|
||||
}
|
||||
},
|
||||
"storybook:build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@apollo/client": "^4.0.0",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
@@ -164,6 +164,7 @@
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"verdaccio": "^6.3.1",
|
||||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
@@ -206,7 +207,6 @@
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-standard-application",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
|
||||
@@ -36,30 +36,36 @@ cd my-twenty-app
|
||||
# Get help and list all available commands
|
||||
yarn twenty help
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn twenty auth:login
|
||||
# Authenticate with your Twenty server
|
||||
yarn twenty remote add --local
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn twenty entity:add
|
||||
yarn twenty add
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
|
||||
yarn twenty app:dev
|
||||
yarn twenty dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn twenty function:logs
|
||||
yarn twenty logs
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn twenty function:execute -n my-function -p '{"key": "value"}'
|
||||
yarn twenty exec -n my-function -p '{"key": "value"}'
|
||||
|
||||
# Execute the pre-install function
|
||||
yarn twenty function:execute --preInstall
|
||||
yarn twenty exec --preInstall
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
yarn twenty exec --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty build
|
||||
|
||||
# Publish the app to npm or directly to a Twenty server
|
||||
yarn twenty publish
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
yarn twenty uninstall
|
||||
```
|
||||
|
||||
## Scaffolding modes
|
||||
@@ -104,40 +110,51 @@ npx create-twenty-app@latest my-app -m
|
||||
## Next steps
|
||||
|
||||
- Run `yarn twenty help` to see all available commands.
|
||||
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty app:dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
|
||||
## Publish your application
|
||||
## Build and publish your application
|
||||
|
||||
Applications are currently stored in `twenty/packages/twenty-apps`.
|
||||
Once your app is ready, build and publish it using the CLI:
|
||||
|
||||
You can share your application with all Twenty users:
|
||||
```bash
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty build --tarball
|
||||
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty publish --tag beta
|
||||
|
||||
# Deploy directly to a Twenty server (builds, uploads, and installs in one step)
|
||||
yarn twenty deploy
|
||||
```
|
||||
|
||||
### Publish to the Twenty marketplace
|
||||
|
||||
You can also contribute your application to the curated marketplace:
|
||||
|
||||
```bash
|
||||
# pull the Twenty project
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
cd twenty
|
||||
|
||||
# create a new branch
|
||||
git checkout -b feature/my-awesome-app
|
||||
```
|
||||
|
||||
- Copy your app folder into `twenty/packages/twenty-apps`.
|
||||
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
|
||||
|
||||
```bash
|
||||
git commit -m "Add new application"
|
||||
git push
|
||||
```
|
||||
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client.
|
||||
- Auth prompts not appearing: run `yarn twenty remote add --local` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty dev` is running — it auto‑generates the typed client.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.7.0-canary.0",
|
||||
"version": "0.8.0-canary.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"command": "node dist/cli.cjs"
|
||||
}
|
||||
},
|
||||
"set-local-version": {},
|
||||
"typecheck": {},
|
||||
"lint": {},
|
||||
"test": {
|
||||
|
||||
@@ -18,6 +18,19 @@ const program = new Command(packageJson.name)
|
||||
'-m, --minimal',
|
||||
'Create only core entities (application-config and default-role)',
|
||||
)
|
||||
.option('-n, --name <name>', 'Application name (skips prompt)')
|
||||
.option(
|
||||
'-d, --display-name <displayName>',
|
||||
'Application display name (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--description <description>',
|
||||
'Application description (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--skip-local-instance',
|
||||
'Skip the local Twenty instance setup prompt',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -25,6 +38,10 @@ const program = new Command(packageJson.name)
|
||||
options?: {
|
||||
exhaustive?: boolean;
|
||||
minimal?: boolean;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [options?.exhaustive, options?.minimal].filter(Boolean);
|
||||
@@ -47,9 +64,21 @@ const program = new Command(packageJson.name)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options?.name !== undefined && options.name.trim().length === 0) {
|
||||
console.error(chalk.red('Error: --name cannot be empty.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
|
||||
|
||||
await new CreateAppCommand().execute(directory, mode);
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
mode,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -2,16 +2,10 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, authenticate to your workspace:
|
||||
Start development mode — it auto-connects to your local Twenty server at localhost:3000:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +15,23 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
# Remotes & authentication
|
||||
yarn twenty remote add --local # Connect to local Twenty server
|
||||
yarn twenty remote add <url> # Connect to a remote server (OAuth)
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
# Development
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty build # Build the application
|
||||
yarn twenty deploy # Deploy to a Twenty server
|
||||
yarn twenty publish # Publish to npm
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty uninstall # Uninstall app from server
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import { convertToLabel } from '@/utils/convert-to-label';
|
||||
import { install } from '@/utils/install';
|
||||
import {
|
||||
type LocalInstanceResult,
|
||||
setupLocalInstance,
|
||||
} from '@/utils/setup-local-instance';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as path from 'path';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type ExampleOptions,
|
||||
@@ -15,16 +21,24 @@ import {
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
type CreateAppOptions = {
|
||||
directory?: string;
|
||||
mode?: ScaffoldingMode;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
skipLocalInstance?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
async execute(
|
||||
directory?: string,
|
||||
mode: ScaffoldingMode = 'exhaustive',
|
||||
): Promise<void> {
|
||||
async execute(options: CreateAppOptions = {}): Promise<void> {
|
||||
try {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(directory);
|
||||
await this.getAppInfos(options);
|
||||
|
||||
const exampleOptions = this.resolveExampleOptions(mode);
|
||||
const exampleOptions = this.resolveExampleOptions(
|
||||
options.mode ?? 'exhaustive',
|
||||
);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
@@ -44,7 +58,29 @@ export class CreateAppCommand {
|
||||
|
||||
await tryGitInit(appDirectory);
|
||||
|
||||
this.logSuccess(appDirectory);
|
||||
let localResult: LocalInstanceResult = { running: false };
|
||||
|
||||
if (!options.skipLocalInstance) {
|
||||
const { needsLocalInstance } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'needsLocalInstance',
|
||||
message:
|
||||
'Do you need a local instance of Twenty? Recommended if you not have one already.',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (needsLocalInstance) {
|
||||
localResult = await setupLocalInstance();
|
||||
}
|
||||
|
||||
if (isDefined(localResult.apiKey)) {
|
||||
this.runAuthLogin(appDirectory, localResult.apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, localResult);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Initialization failed:'),
|
||||
@@ -54,19 +90,25 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async getAppInfos(directory?: string): Promise<{
|
||||
private async getAppInfos(options: CreateAppOptions): Promise<{
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}> {
|
||||
const { directory } = options;
|
||||
|
||||
const hasName = isDefined(options.name) || isDefined(directory);
|
||||
const hasDisplayName = isDefined(options.displayName);
|
||||
const hasDescription = isDefined(options.description);
|
||||
|
||||
const { name, displayName, description } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !directory,
|
||||
default: 'my-awesome-app',
|
||||
when: () => !hasName,
|
||||
default: 'my-twenty-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
@@ -76,25 +118,33 @@ export class CreateAppCommand {
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Application display name:',
|
||||
default: (answers: any) => {
|
||||
return convertToLabel(answers?.name ?? directory);
|
||||
when: () => !hasDisplayName,
|
||||
default: (answers: { name?: string }) => {
|
||||
return convertToLabel(
|
||||
answers?.name ?? options.name ?? directory ?? '',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Application description (optional):',
|
||||
when: () => !hasDescription,
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const computedName = name ?? directory;
|
||||
const appName = (
|
||||
options.name ??
|
||||
name ??
|
||||
directory ??
|
||||
'my-twenty-app'
|
||||
).trim();
|
||||
|
||||
const appName = computedName.trim();
|
||||
const appDisplayName =
|
||||
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
|
||||
|
||||
const appDisplayName = displayName.trim();
|
||||
|
||||
const appDescription = description.trim();
|
||||
const appDescription = (options.description ?? description ?? '').trim();
|
||||
|
||||
const appDirectory = directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
|
||||
@@ -157,16 +207,49 @@ export class CreateAppCommand {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
private logSuccess(appDirectory: string): void {
|
||||
private runAuthLogin(appDirectory: string, apiKey: string): void {
|
||||
try {
|
||||
execSync(
|
||||
`yarn twenty auth:login --api-key "${apiKey}" --api-url http://localhost:3000`,
|
||||
{ cwd: appDirectory, stdio: 'inherit' },
|
||||
);
|
||||
console.log(chalk.green('✅ Authenticated with local Twenty instance.'));
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Auto auth:login failed. Run `yarn twenty auth:login` manually.',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
localResult: LocalInstanceResult,
|
||||
): void {
|
||||
const dirName = appDirectory.split('/').reverse()[0] ?? '';
|
||||
|
||||
console.log(chalk.green('✅ Application created!'));
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(` cd ${dirName}`));
|
||||
console.log(
|
||||
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
|
||||
if (localResult.apiKey) {
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
} else if (localResult.running) {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
} else {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' yarn twenty remote add --local # Authenticate with Twenty',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ describe('scaffoldIntegrationTest', () => {
|
||||
|
||||
expect(content).toContain('TWENTY_API_KEY');
|
||||
expect(content).not.toContain('TWENTY_TEST_API_KEY');
|
||||
expect(content).toContain('TWENTY_API_URL');
|
||||
expect(content).toContain('setup-test.ts');
|
||||
expect(content).toContain('tsconfig.spec.json');
|
||||
expect(content).toContain('integration-test.ts');
|
||||
|
||||
@@ -30,12 +30,12 @@ export const copyBaseApplicationProject = async ({
|
||||
includeExampleIntegrationTest: exampleOptions.includeExampleIntegrationTest,
|
||||
});
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
await createPublicAssetDirectory(appDirectory);
|
||||
|
||||
await createYarnLock(appDirectory);
|
||||
|
||||
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
|
||||
|
||||
await fs.ensureDir(sourceFolderPath);
|
||||
@@ -142,13 +142,6 @@ const createPublicAssetDirectory = async (appDirectory: string) => {
|
||||
await fs.ensureDir(join(appDirectory, ASSETS_DIR));
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
const createGitignore = async (appDirectory: string) => {
|
||||
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
@@ -481,7 +474,7 @@ const createExampleNavigationMenuItem = async ({
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
@@ -489,6 +482,7 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: 'VIEW',
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
@@ -590,6 +584,14 @@ export default defineApplication({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createYarnLock = async (appDirectory: string) => {
|
||||
const yarnLockContent = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(appDirectory, 'yarn.lock'), yarnLockContent);
|
||||
};
|
||||
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
@@ -608,8 +610,9 @@ const createPackageJson = async ({
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
'@types/react': '^19.0.0',
|
||||
react: '^19.0.0',
|
||||
'react-dom': '^19.0.0',
|
||||
oxlint: '^0.16.0',
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const INSTALL_SCRIPT_URL =
|
||||
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh';
|
||||
|
||||
const SERVER_CONTAINER = 'twenty-server-1';
|
||||
const DB_CONTAINER = 'twenty-db-1';
|
||||
|
||||
const isDockerAvailable = (): boolean => {
|
||||
try {
|
||||
execSync('docker compose version', { stdio: 'ignore' });
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isDockerRunning = (): boolean => {
|
||||
try {
|
||||
execSync('docker info', { stdio: 'ignore' });
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isTwentyServerRunning = async (): Promise<boolean> => {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch('http://localhost:3000/healthz', {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
return body.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getActiveWorkspaceId = (): string | null => {
|
||||
try {
|
||||
const result = execSync(
|
||||
`docker exec ${DB_CONTAINER} psql -U postgres -d default -t -c "SELECT id FROM core.workspace WHERE \\"activationStatus\\" = 'ACTIVE' LIMIT 1"`,
|
||||
{ encoding: 'utf-8' },
|
||||
).trim();
|
||||
|
||||
return result || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const generateApiKeyToken = (workspaceId: string): string | null => {
|
||||
try {
|
||||
const output = execSync(
|
||||
`docker exec -e NODE_ENV=development ${SERVER_CONTAINER} yarn command:prod workspace:generate-api-key -w ${workspaceId}`,
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
|
||||
const TOKEN_PREFIX = 'TOKEN:';
|
||||
const tokenLine = output
|
||||
.trim()
|
||||
.split('\n')
|
||||
.find((line) => line.includes(TOKEN_PREFIX));
|
||||
|
||||
if (!tokenLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenStartIndex =
|
||||
tokenLine.indexOf(TOKEN_PREFIX) + TOKEN_PREFIX.length;
|
||||
|
||||
return tokenLine.slice(tokenStartIndex).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export type LocalInstanceResult = {
|
||||
running: boolean;
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
export const setupLocalInstance = async (): Promise<LocalInstanceResult> => {
|
||||
console.log('');
|
||||
console.log(chalk.blue('🐳 Setting up local Twenty instance...'));
|
||||
|
||||
if (await isTwentyServerRunning()) {
|
||||
console.log(
|
||||
chalk.green('✅ Twenty server is already running on localhost:3000.'),
|
||||
);
|
||||
} else {
|
||||
if (!isDockerAvailable()) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Docker Compose is not installed. Please install Docker first.',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' See https://docs.docker.com/get-docker/'));
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
if (!isDockerRunning()) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Docker is not running. Please start Docker and try again.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`bash <(curl -sL ${INSTALL_SCRIPT_URL})`, {
|
||||
stdio: 'inherit',
|
||||
shell: '/bin/bash',
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow('⚠️ Local instance setup did not complete successfully.'),
|
||||
);
|
||||
|
||||
return { running: false };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
chalk.blue(
|
||||
'👉 Please create your workspace in the browser before continuing.',
|
||||
),
|
||||
);
|
||||
|
||||
const { workspaceCreated } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'workspaceCreated',
|
||||
message: 'Have you finished creating your workspace?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!workspaceCreated) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Skipping API key generation. Run `yarn twenty remote add --local` manually after creating your workspace.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: true };
|
||||
}
|
||||
|
||||
console.log(chalk.blue('🔑 Generating API key for your workspace...'));
|
||||
|
||||
const workspaceId = getActiveWorkspaceId();
|
||||
|
||||
if (!isDefined(workspaceId)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ No active workspace found. Make sure you completed the signup flow, then run `yarn twenty auth:login` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: true };
|
||||
}
|
||||
|
||||
const apiKey = generateApiKeyToken(workspaceId);
|
||||
|
||||
if (!isDefined(apiKey)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'⚠️ Could not generate API key. Run `yarn twenty auth:login` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return { running: true };
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ API key generated for your workspace.'));
|
||||
|
||||
return { running: true, apiKey };
|
||||
};
|
||||
@@ -45,7 +45,6 @@ export default defineConfig({
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
TWENTY_API_KEY:
|
||||
'${SEED_API_KEY}',
|
||||
},
|
||||
@@ -120,12 +119,13 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
profiles: {
|
||||
default: {
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ type AnalysisResult = {
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextV2Data = {
|
||||
type RichTextData = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
@@ -123,7 +123,7 @@ const createNoteInTwenty = async (
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextV2Data,
|
||||
} satisfies RichTextData,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -159,7 +159,7 @@ const createTaskInTwenty = async (
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextV2Data;
|
||||
bodyV2: RichTextData;
|
||||
dueAt?: string;
|
||||
} = {
|
||||
title: actionItem.title,
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
yarn twenty remote add --local
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ type AnalysisResult = {
|
||||
commitments: Commitment[];
|
||||
};
|
||||
|
||||
type RichTextV2Data = {
|
||||
type RichTextData = {
|
||||
markdown: string;
|
||||
blocknote: null;
|
||||
};
|
||||
@@ -362,7 +362,7 @@ const createNoteInTwenty = async (
|
||||
bodyV2: {
|
||||
markdown: noteBodyMarkdown,
|
||||
blocknote: null,
|
||||
} satisfies RichTextV2Data,
|
||||
} satisfies RichTextData,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -451,7 +451,7 @@ const createTaskInTwenty = async (
|
||||
|
||||
const taskData: {
|
||||
title: string;
|
||||
bodyV2: RichTextV2Data;
|
||||
bodyV2: RichTextData;
|
||||
dueAt?: string;
|
||||
assigneeId?: string;
|
||||
} = {
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"app:dev": "twenty app:dev",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"dev": "twenty dev",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"remote:add": "twenty remote add --local",
|
||||
"remote:status": "twenty remote status",
|
||||
"remote:switch": "twenty remote switch",
|
||||
"remote:list": "twenty remote list",
|
||||
"remote:remove": "twenty remote remove",
|
||||
"dev": "twenty dev",
|
||||
"add": "twenty add",
|
||||
"logs": "twenty logs",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 2,
|
||||
viewUniversalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 1,
|
||||
viewUniversalIdentifier: ALL_RECIPIENTS_VIEW_ID,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
yarn twenty remote add --local
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
@@ -29,12 +29,13 @@ beforeAll(async () => {
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
const configFile = {
|
||||
profiles: {
|
||||
default: {
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
@@ -7,5 +8,6 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -5,13 +5,13 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
yarn twenty remote add --local
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
@@ -21,19 +21,19 @@ Open your Twenty instance and go to `/settings/applications` section to see the
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
# Remotes & Authentication
|
||||
yarn twenty remote add --local # Authenticate with Twenty
|
||||
yarn twenty remote status # Check auth status
|
||||
yarn twenty remote switch # Switch default remote
|
||||
yarn twenty remote list # List all configured remotes
|
||||
yarn twenty remote remove <name> # Remove a remote
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty logs # Stream function logs
|
||||
yarn twenty exec # Execute a function with JSON payload
|
||||
yarn twenty uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
@@ -236,7 +236,7 @@ const handler = async (event: any) => {
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: remove `as any` after running `yarn twenty app:dev` to regenerate the typed client
|
||||
// TODO: remove `as any` after running `yarn twenty dev` to regenerate the typed client
|
||||
const updateSummary = async (markdown: string) => {
|
||||
await client.mutation({
|
||||
updateCallRecording: {
|
||||
|
||||
+2
@@ -1,10 +1,12 @@
|
||||
import { CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/call-recording-view';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '5248a62d-7d2e-43a7-ba45-6e8f61876a71',
|
||||
name: 'Call recordings',
|
||||
icon: 'IconPhone',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ export default defineObject({
|
||||
},
|
||||
{
|
||||
universalIdentifier: TRANSCRIPT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT_V2,
|
||||
type: FieldType.RICH_TEXT,
|
||||
name: 'transcript',
|
||||
label: 'Transcript',
|
||||
description: 'Human-readable transcript of the call',
|
||||
@@ -114,7 +114,7 @@ export default defineObject({
|
||||
},
|
||||
{
|
||||
universalIdentifier: SUMMARY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT_V2,
|
||||
type: FieldType.RICH_TEXT,
|
||||
name: 'summary',
|
||||
label: 'Summary',
|
||||
description: 'AI-generated summary of the call',
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Use this skill when a user asks you to summarize, analyze, or extract insights f
|
||||
|
||||
## How to Access the Data
|
||||
1. Use \`find_one_callRecording\` to fetch the call recording by its ID.
|
||||
2. Read the \`transcript\` field (RICH_TEXT_V2, markdown format) which contains the full conversation.
|
||||
2. Read the \`transcript\` field (RICH_TEXT, markdown format) which contains the full conversation.
|
||||
3. The transcript uses the format: **Speaker Name:** spoken text
|
||||
|
||||
## What to Produce
|
||||
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
@@ -6,6 +7,7 @@ export default defineNavigationMenuItem({
|
||||
name: 'Self host user',
|
||||
icon: 'IconList',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty/*
|
||||
!.twenty/output/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1 @@
|
||||
24.5.0
|
||||
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -0,0 +1,9 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,51 @@
|
||||
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
- [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) - learn about `twenty-sdk` tool.
|
||||
- [Twenty doc](https://docs.twenty.com/) - Twenty's documentation.
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
|
||||
You can check out [the Twenty GitHub repository](https://github.com/twentyhq/twenty) - your feedback and contributions are welcome!
|
||||
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default [
|
||||
// Base JS recommended rules
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript recommended rules
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Common TypeScript-friendly tweaks
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-unused-vars': 'off', // handled by TS rule
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "twentyfortwenty",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"eslint": "^9.32.0",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '0fac8de4-9d11-492b-9e6a-577e11e1c442',
|
||||
displayName: 'Twenty for Twenty',
|
||||
description: '',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
applicationVariables: {
|
||||
CLICKHOUSE_URL: {
|
||||
universalIdentifier: 'b204304f-d3d3-4ae2-aafa-24b21c159181',
|
||||
description:
|
||||
'The URL of the ClickHouse server, including the protocol and port (e.g., http://localhost:8123)',
|
||||
isSecret: true,
|
||||
},
|
||||
CLICKHOUSE_USERNAME: {
|
||||
universalIdentifier: '711e8ea8-8b19-4cf0-82ab-ab44417312bd',
|
||||
description: 'The username for authenticating with the ClickHouse server',
|
||||
isSecret: true,
|
||||
},
|
||||
CLICKHOUSE_PASSWORD: {
|
||||
universalIdentifier: 'faba7ed7-9f94-4202-944b-c25c683e9504',
|
||||
description: 'The password for authenticating with the ClickHouse server',
|
||||
isSecret: true,
|
||||
},
|
||||
CLICKHOUSE_DATABASE: {
|
||||
universalIdentifier: '3e6698fa-0c00-49e5-9c4d-34d5b177bff3',
|
||||
description: 'The name of the ClickHouse database to connect to',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// Field metadata identifiers for cloudUser2 object
|
||||
// These constants are used by both the object definition and views
|
||||
|
||||
export const CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID =
|
||||
'56e3d6ce-d57e-4ded-830f-958eb18d4d36';
|
||||
export const CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID =
|
||||
'1f7062c1-0e96-4599-a59c-6d7427794bb1';
|
||||
export const CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID =
|
||||
'e134ce6b-5ab0-4e9c-ba32-36d0851aca21';
|
||||
export const CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID =
|
||||
'f305d341-7b12-4939-9299-e5e1b6d1591e';
|
||||
export const CLOUD_USER_2_EMAIL_FIELD_ID =
|
||||
'ee110a77-34d7-4c8b-bc10-8560b9e2333a';
|
||||
export const CLOUD_USER_2_FULL_NAME_FIELD_ID =
|
||||
'3c9540be-6e42-40f2-8598-973117bbe105';
|
||||
export const CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID =
|
||||
'efaa6e54-8019-454c-a556-2e340f0b156d';
|
||||
export const CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID =
|
||||
'86a07ca4-420e-4c82-82f8-8f8dfbfc2dc4';
|
||||
export const CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID =
|
||||
'87bf8c09-1863-4c65-811f-6fb54a87238e';
|
||||
export const CLOUD_USER_2_IS_TWENTY_FIELD_ID =
|
||||
'f6dcd9f3-3f7e-4aea-84a5-41282a68910d';
|
||||
export const CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID =
|
||||
'643ab4a2-b6f2-4d11-9173-6859884b8781';
|
||||
export const CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID =
|
||||
'294e95cb-13b3-4163-95f8-e4f31837cb47';
|
||||
export const CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID =
|
||||
'8dacb4f1-b4ef-42d6-a917-17c78d6273a7';
|
||||
export const CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID =
|
||||
'c7741332-d9b6-42ed-84e2-8afa184b58f3';
|
||||
export const CLOUD_USER_2_UPDATED_BY_FIELD_ID =
|
||||
'639c9a30-2926-44c3-ab44-100dbda91c64';
|
||||
export const CLOUD_USER_2_USER_TENURE_FIELD_ID =
|
||||
'0cc9ca63-c06f-4422-9325-5e99f98d05ed';
|
||||
export const CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID =
|
||||
'5af6ae62-b741-471b-90bb-7fb6678fc8c9';
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
OnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
|
||||
CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
|
||||
} from 'src/fields/cloud-user-workspaces-on-cloud-user.field';
|
||||
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
|
||||
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
|
||||
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'cloudUser2',
|
||||
label: 'Cloud User 2',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'cloudUser2Id',
|
||||
},
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// Field metadata identifiers for cloudUserWorkspace2 object
|
||||
// These constants are used by both the object definition and views
|
||||
|
||||
export const CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID =
|
||||
'ad149693-0df5-4346-90ff-39e6d945b90f';
|
||||
export const CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID =
|
||||
'c5d02b59-ebaf-4604-809c-ce3f6e50dedc';
|
||||
export const CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID =
|
||||
'6a0f8018-be3c-49b0-bb91-8d3e6b44adec';
|
||||
export const CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID =
|
||||
'29d47f11-1e45-4c67-b495-fcfa45bb67e2';
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
|
||||
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
|
||||
|
||||
export const CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID =
|
||||
'2290d722-d4b1-47ab-a895-4e2c3163a541';
|
||||
export const CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID =
|
||||
'49b08cf9-15e1-4583-826f-943fe3c6b0e8';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
|
||||
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'cloudUserWorkspaces2',
|
||||
label: 'Cloud User Workspaces 2',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
|
||||
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
|
||||
|
||||
export const CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID =
|
||||
'90d9dfc7-7058-4d1c-aac2-1505bd7cb827';
|
||||
export const CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID =
|
||||
'7bcd2dda-d1f8-4aa7-a83f-6cf240be80b2';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
|
||||
objectUniversalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'cloudUserWorkspaces2',
|
||||
label: 'Cloud User Workspaces 2',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
|
||||
|
||||
export const CLOUD_USERS_2_ON_PERSON_ID =
|
||||
'f238e91c-11d8-4a27-a39b-c9fa3515d4a0';
|
||||
export const PERSON_ON_CLOUD_USER_2_ID = '08d3aff0-7548-4a99-a097-20a0ad2c9ee7';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: CLOUD_USERS_2_ON_PERSON_ID,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'cloudUsers2',
|
||||
label: 'Cloud Users 2',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PERSON_ON_CLOUD_USER_2_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Field metadata identifiers for cloudWorkspace2 object
|
||||
// These constants are used by both the object definition and views
|
||||
|
||||
export const CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID =
|
||||
'c30799d5-adc0-49be-82a1-6d41b5aa9e82';
|
||||
export const CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID =
|
||||
'bfa2f91d-1c6c-48de-bb11-ac2a1e2545a9';
|
||||
export const CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID =
|
||||
'f9f2ef3a-8235-4d21-9a45-d29cc2124456';
|
||||
export const CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID =
|
||||
'fbeaab89-77e0-4e13-97ed-caeb757a38c7';
|
||||
export const CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID =
|
||||
'7d3d4ba3-5af2-4248-afc7-7f4ce7c82805';
|
||||
export const CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID =
|
||||
'56e99339-dcc4-4950-ae64-1b6ae326af2c';
|
||||
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID =
|
||||
'e9c703e6-9ffc-497d-9755-018724ac8d66';
|
||||
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID =
|
||||
'81d8f622-2366-481f-b0e8-b14fb7efa4d5';
|
||||
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID =
|
||||
'3e53d606-1afa-4d49-be62-52f4bb1cb49e';
|
||||
export const CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID =
|
||||
'3fdd272c-73dd-432e-99c7-782f237a7cd8';
|
||||
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID =
|
||||
'1931f3cd-a984-41e3-b8f5-510f67b508e6';
|
||||
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID =
|
||||
'1f6a0ef6-3faf-4157-88a7-f624b7655c6b';
|
||||
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID =
|
||||
'ab0f46aa-9fef-40c6-a229-6ed4e05867d4';
|
||||
export const CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID =
|
||||
'38095859-2efb-4f40-86a1-aa883fd88e3d';
|
||||
export const CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID =
|
||||
'4ebd8329-a18c-4606-9ee0-f32fb1c4aa6f';
|
||||
export const CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID =
|
||||
'754f137f-b759-47da-8b57-7de71ad34d37';
|
||||
export const CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID =
|
||||
'1627e6b8-e87a-4048-b954-8d2fe09b67c8';
|
||||
export const CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID =
|
||||
'1d8bb210-b50d-4b38-a611-d78ba865cbae';
|
||||
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID =
|
||||
'42879662-d4fc-4806-bcfb-fdb586827491';
|
||||
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID =
|
||||
'44b2c12f-aedd-459a-848a-ade9fc4620b7';
|
||||
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID =
|
||||
'cac2d3e0-c2ee-4e22-bd96-4fa051a36f73';
|
||||
export const CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID =
|
||||
'45cbddfa-54cc-4d94-88d2-db7a7a550414';
|
||||
export const CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID =
|
||||
'a6686308-dfcf-4abb-be0f-e8f48cc579d9';
|
||||
export const CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID =
|
||||
'0432af7e-7575-486e-a022-f4a625a8ac14';
|
||||
export const CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID =
|
||||
'40854f41-4175-4b14-9567-c6bfb3f5d441';
|
||||
export const CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID =
|
||||
'44aba339-be2e-4364-a894-cf730081c8a6';
|
||||
export const CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID =
|
||||
'f11ddfc2-3356-453a-a6e1-494b6c6b12f6';
|
||||
export const CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID =
|
||||
'8fb18f52-44e9-4288-87c1-9650222efb05';
|
||||
export const CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID =
|
||||
'8f5dfc54-213b-43b2-a502-631f0f3240d1';
|
||||
export const CLOUD_WORKSPACE_2_MRR_FIELD_ID =
|
||||
'67125b6c-ff41-459f-8153-a338ed11c4f0';
|
||||
export const CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID =
|
||||
'0c38b297-0e9d-47cc-8f8f-a0e5402413ce';
|
||||
export const CLOUD_WORKSPACE_2_ARR_FIELD_ID =
|
||||
'f932f94b-c48b-4ef3-9eea-0c8dad370d89';
|
||||
export const CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID =
|
||||
'ae5da0c9-e9ac-401b-ad48-c37a31c6ecd1';
|
||||
export const CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID =
|
||||
'ef44daa9-d272-4680-a19c-caf2bac32b70';
|
||||
export const CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID =
|
||||
'a88e8cab-333f-44a3-980e-f2cc22871aee';
|
||||
export const CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID =
|
||||
'fccc73da-2b6e-434b-921b-e3bcbab1c56c';
|
||||
export const CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID =
|
||||
'488b38b4-971e-4c33-b60d-7e7934911579';
|
||||
export const CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID =
|
||||
'3815b992-a919-4c52-8629-b4b5f89a6060';
|
||||
export const CLOUD_WORKSPACE_2_TAGS_FIELD_ID =
|
||||
'0f41485f-4868-4bd6-8d76-709a6b7a54e3';
|
||||
export const CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID =
|
||||
'252cdff1-facb-4f66-8608-6138a585d099';
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
|
||||
CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
|
||||
} from 'src/fields/cloud-user-workspaces-on-cloud-workspace.field';
|
||||
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
|
||||
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
|
||||
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'cloudWorkspace2',
|
||||
label: 'Cloud Workspace 2',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
joinColumnName: 'cloudWorkspace2Id',
|
||||
},
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
OnDeleteAction,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
CLOUD_USERS_2_ON_PERSON_ID,
|
||||
PERSON_ON_CLOUD_USER_2_ID,
|
||||
} from 'src/fields/cloud-users-2-on-person.field';
|
||||
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_ON_CLOUD_USER_2_ID,
|
||||
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: CLOUD_USERS_2_ON_PERSON_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
import { syncCloudUserWorkspaces } from 'src/logic-functions/sync-product-data/sync-cloud-user-workspaces';
|
||||
import { syncCloudUsers } from 'src/logic-functions/sync-product-data/sync-cloud-users';
|
||||
import { syncCloudWorkspaces } from 'src/logic-functions/sync-product-data/sync-cloud-workspaces';
|
||||
|
||||
const handler = async (): Promise<{ message: string }> => {
|
||||
try {
|
||||
console.log('Starting product data sync');
|
||||
|
||||
const cloudUsersResult = await syncCloudUsers();
|
||||
|
||||
console.log(
|
||||
`Cloud users sync complete: ${cloudUsersResult.syncedCount} users`,
|
||||
);
|
||||
|
||||
const cloudWorkspacesResult = await syncCloudWorkspaces();
|
||||
|
||||
console.log(
|
||||
`Cloud workspaces sync complete: ${cloudWorkspacesResult.syncedCount} workspaces`,
|
||||
);
|
||||
|
||||
const cloudUserWorkspacesResult = await syncCloudUserWorkspaces();
|
||||
|
||||
console.log(
|
||||
`Cloud user workspaces sync complete: ${cloudUserWorkspacesResult.syncedCount} user workspaces`,
|
||||
);
|
||||
|
||||
return {
|
||||
message: `Product data sync complete — ${cloudUsersResult.syncedCount} users, ${cloudWorkspacesResult.syncedCount} workspaces, ${cloudUserWorkspacesResult.syncedCount} user workspaces`,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '3897e059-715e-4a4b-b165-c44f17d2e30a',
|
||||
name: 'sync-product-data',
|
||||
description:
|
||||
'Syncs cloud users, cloud workspaces, and cloud user workspaces from ClickHouse',
|
||||
timeoutSeconds: 120,
|
||||
handler,
|
||||
cronTriggerSettings: {
|
||||
pattern: '*/10 * * * *',
|
||||
},
|
||||
});
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getApplicationConfig } from 'src/shared/application-config';
|
||||
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
|
||||
import { twentyClient } from 'src/shared/twenty-client';
|
||||
|
||||
const clickHouseUserWorkspaceSchema = z.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string(),
|
||||
userId: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
deletedAt: z.string(),
|
||||
is_active_membership: z.coerce.boolean(),
|
||||
});
|
||||
|
||||
type ClickHouseUserWorkspace = z.infer<typeof clickHouseUserWorkspaceSchema>;
|
||||
|
||||
const fetchUserWorkspacesFromClickHouse = async (): Promise<
|
||||
ClickHouseUserWorkspace[]
|
||||
> => {
|
||||
const { clickHouseDatabase } = getApplicationConfig();
|
||||
|
||||
// const nowDate = 'now()'
|
||||
const nowDate = "'2026-02-04 14:28:52.000'";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
*
|
||||
FROM (
|
||||
SELECT
|
||||
*,
|
||||
row_number() OVER (PARTITION BY id ORDER BY updatedAt DESC) AS rn
|
||||
FROM
|
||||
${clickHouseDatabase}.user_workspace
|
||||
WHERE
|
||||
updatedAt >= ${nowDate} - INTERVAL 500 MINUTE
|
||||
AND
|
||||
updatedAt <= ${nowDate}
|
||||
)
|
||||
WHERE
|
||||
rn = 1
|
||||
FORMAT
|
||||
JSONEachRow;
|
||||
`;
|
||||
|
||||
return fetchFromClickHouse(query, clickHouseUserWorkspaceSchema);
|
||||
};
|
||||
|
||||
const buildCloudUserWorkspaceInput = (
|
||||
userWorkspace: ClickHouseUserWorkspace,
|
||||
) => ({
|
||||
id: userWorkspace.id,
|
||||
twentyUserIdentifier: userWorkspace.userId,
|
||||
twentyWorkspaceIdentifier: userWorkspace.workspaceId,
|
||||
idOfTheUserWorkspace: userWorkspace.id,
|
||||
cloudUser2Id: userWorkspace.userId,
|
||||
cloudWorkspace2Id: userWorkspace.workspaceId,
|
||||
});
|
||||
|
||||
export const syncCloudUserWorkspaces = async (): Promise<{
|
||||
syncedCount: number;
|
||||
}> => {
|
||||
const userWorkspaces = await fetchUserWorkspacesFromClickHouse();
|
||||
|
||||
console.log(
|
||||
`Fetched ${userWorkspaces.length} user workspaces from ClickHouse`,
|
||||
);
|
||||
|
||||
if (userWorkspaces.length === 0) {
|
||||
return { syncedCount: 0 };
|
||||
}
|
||||
|
||||
const cloudUserWorkspaceInputs = userWorkspaces.map(
|
||||
buildCloudUserWorkspaceInput,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Batch-upserting ${cloudUserWorkspaceInputs.length} cloud user workspaces`,
|
||||
);
|
||||
|
||||
await twentyClient.mutation({
|
||||
createCloudUserWorkspaces2: {
|
||||
__args: {
|
||||
data: cloudUserWorkspaceInputs,
|
||||
upsert: true,
|
||||
},
|
||||
__scalar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { syncedCount: userWorkspaces.length };
|
||||
};
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
import { enumCloudUser2ActivityStatusEnum } from 'twenty-sdk/generated';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getApplicationConfig } from 'src/shared/application-config';
|
||||
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
|
||||
import { clickHouseDateToIso } from 'src/shared/clickhouse-date-to-iso';
|
||||
import { twentyClient } from 'src/shared/twenty-client';
|
||||
|
||||
const clickHouseUserSchema = z.object({
|
||||
userId: z.uuid(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
email: z.email(),
|
||||
fullName: z.string(),
|
||||
isEmailVerified: z.boolean(),
|
||||
disabled: z.boolean(),
|
||||
canImpersonate: z.boolean(),
|
||||
canAccessFullAdminPanel: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
deletedAt: z.string(),
|
||||
locale: z.string(),
|
||||
createdDate: z.string(),
|
||||
workspaceCount: z.coerce.number(),
|
||||
workspaceIds: z.string(),
|
||||
workspaceDomains: z.string(),
|
||||
firstActivityDate: z.string(),
|
||||
lastActivityDate: z.string(),
|
||||
lastWorkspaceId: z.string(),
|
||||
totalPageviews: z.coerce.number(),
|
||||
pageviewsLast30d: z.coerce.number(),
|
||||
pageviewsLast7d: z.coerce.number(),
|
||||
pageviewsLast24h: z.coerce.number(),
|
||||
userAgeDays: z.coerce.number(),
|
||||
daysSinceLastActivity: z.coerce.number(),
|
||||
isActiveLast30d: z.boolean(),
|
||||
isActiveLast7d: z.boolean(),
|
||||
isActiveLast24h: z.boolean(),
|
||||
activityStatus: z
|
||||
.string()
|
||||
.transform((val) => val.toUpperCase())
|
||||
.pipe(z.enum(enumCloudUser2ActivityStatusEnum)),
|
||||
avgDailyPageviewsLast30d: z.coerce.number(),
|
||||
isTwenty: z.coerce.boolean(),
|
||||
maxWorkspaceMembers: z.coerce.number(),
|
||||
inTrial: z.boolean(),
|
||||
});
|
||||
|
||||
type ClickHouseUser = z.infer<typeof clickHouseUserSchema>;
|
||||
|
||||
const fetchUsersFromClickHouse = async (): Promise<ClickHouseUser[]> => {
|
||||
const { clickHouseDatabase } = getApplicationConfig();
|
||||
|
||||
// const nowDate = 'now()'
|
||||
const nowDate = "'2026-02-04 14:28:52.000'";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
${clickHouseDatabase}.user
|
||||
WHERE
|
||||
lastActivityDate >= ${nowDate} - INTERVAL 500 MINUTE
|
||||
AND
|
||||
lastActivityDate <= ${nowDate}
|
||||
FORMAT
|
||||
JSONEachRow;
|
||||
`;
|
||||
|
||||
return fetchFromClickHouse(query, clickHouseUserSchema);
|
||||
};
|
||||
|
||||
const fetchAllPeopleFromTwentyByEmail = async (emails: string[]) => {
|
||||
if (emails.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allPeople = await twentyClient.query({
|
||||
people: {
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
emails: {
|
||||
primaryEmail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
__args: {
|
||||
filter: {
|
||||
emails: {
|
||||
primaryEmail: {
|
||||
in: emails,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return allPeople.people?.edges.map((edge) => edge.node) ?? [];
|
||||
};
|
||||
|
||||
const buildCloudUserInput = ({
|
||||
user,
|
||||
personId,
|
||||
}: {
|
||||
user: ClickHouseUser;
|
||||
personId: string;
|
||||
}) => ({
|
||||
id: user.userId,
|
||||
name: user.fullName,
|
||||
email: {
|
||||
primaryEmail: user.email,
|
||||
},
|
||||
personId,
|
||||
fullName: {
|
||||
lastName: user.lastName,
|
||||
firstName: user.firstName,
|
||||
},
|
||||
isTwenty: user.isTwenty,
|
||||
userTenure: user.userAgeDays,
|
||||
isActiveL7d: user.isActiveLast7d,
|
||||
isActiveL24h: user.isActiveLast24h,
|
||||
isActiveL30d: user.isActiveLast30d,
|
||||
pageViewsL7d: user.pageviewsLast7d,
|
||||
pageViewsL24h: user.pageviewsLast24h,
|
||||
pageViewsL30d: user.pageviewsLast30d,
|
||||
activityStatus: user.activityStatus,
|
||||
workspaceCount: user.workspaceCount,
|
||||
lastActivityDate: clickHouseDateToIso(user.lastActivityDate),
|
||||
dataLastUpdatedAt: new Date().toISOString(),
|
||||
avgDailyPageviewsLast30d: user.avgDailyPageviewsLast30d,
|
||||
daysSinceLastActivity: user.daysSinceLastActivity,
|
||||
});
|
||||
|
||||
export const syncCloudUsers = async (): Promise<{
|
||||
syncedCount: number;
|
||||
}> => {
|
||||
const users = await fetchUsersFromClickHouse();
|
||||
|
||||
console.log('Fetched users from ClickHouse', users);
|
||||
|
||||
const emails = users.map((user) => user.email);
|
||||
|
||||
console.log('Fetching people from Twenty with emails', emails);
|
||||
|
||||
const people = await fetchAllPeopleFromTwentyByEmail(emails);
|
||||
|
||||
console.log('Fetched people from Twenty', people);
|
||||
|
||||
// Build a map of email -> personId from existing people
|
||||
const emailToPersonId = new Map<string, string>();
|
||||
|
||||
for (const person of people) {
|
||||
if (person.emails?.primaryEmail !== undefined) {
|
||||
emailToPersonId.set(person.emails.primaryEmail.toLowerCase(), person.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Partition users into those with/without an existing person
|
||||
const usersWithoutPerson = users.filter(
|
||||
(user) => !emailToPersonId.has(user.email.toLowerCase()),
|
||||
);
|
||||
|
||||
// Step 1: Batch-create missing people
|
||||
const newlyCreatedPersonIds: string[] = [];
|
||||
|
||||
if (usersWithoutPerson.length > 0) {
|
||||
console.log(`Batch-creating ${usersWithoutPerson.length} people records`);
|
||||
|
||||
const createPeopleResult = await twentyClient.mutation({
|
||||
createPeople: {
|
||||
__args: {
|
||||
data: usersWithoutPerson.map((user) => ({
|
||||
name: {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
},
|
||||
emails: {
|
||||
primaryEmail: user.email,
|
||||
},
|
||||
})),
|
||||
},
|
||||
id: true,
|
||||
emails: {
|
||||
primaryEmail: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdPeople = createPeopleResult.createPeople ?? [];
|
||||
|
||||
for (const person of createdPeople) {
|
||||
if (
|
||||
person.emails?.primaryEmail === undefined ||
|
||||
person.id === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newlyCreatedPersonIds.push(person.id);
|
||||
emailToPersonId.set(person.emails.primaryEmail.toLowerCase(), person.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Batch-upsert all cloud users, with rollback on failure
|
||||
const cloudUserInputs = users.map((user) => {
|
||||
const personId = emailToPersonId.get(user.email.toLowerCase());
|
||||
|
||||
if (personId === undefined) {
|
||||
throw new Error(
|
||||
`No personId found for user ${user.email} — this should not happen`,
|
||||
);
|
||||
}
|
||||
|
||||
return buildCloudUserInput({ user, personId });
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`Batch-upserting ${cloudUserInputs.length} cloud users`);
|
||||
|
||||
await twentyClient.mutation({
|
||||
createCloudUsers2: {
|
||||
__args: {
|
||||
data: cloudUserInputs,
|
||||
upsert: true,
|
||||
},
|
||||
__scalar: true,
|
||||
},
|
||||
});
|
||||
} catch (cloudUserError) {
|
||||
console.log(
|
||||
'Cloud user upsert failed, rolling back newly created people',
|
||||
cloudUserError,
|
||||
);
|
||||
|
||||
// Rollback: hard-delete people that were created in step 1
|
||||
if (newlyCreatedPersonIds.length > 0) {
|
||||
try {
|
||||
await twentyClient.mutation({
|
||||
destroyPeople: {
|
||||
__args: {
|
||||
filter: {
|
||||
id: {
|
||||
in: newlyCreatedPersonIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Rolled back ${newlyCreatedPersonIds.length} newly created people`,
|
||||
);
|
||||
} catch (rollbackError) {
|
||||
console.log(
|
||||
'Rollback of newly created people also failed',
|
||||
rollbackError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw cloudUserError;
|
||||
}
|
||||
|
||||
return { syncedCount: users.length };
|
||||
};
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
enumCloudWorkspace2ActivationStatusEnum,
|
||||
enumCloudWorkspace2PaymentFrequencyEnum,
|
||||
enumCloudWorkspace2SubscriptionStatusEnum,
|
||||
} from 'twenty-sdk/generated';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getApplicationConfig } from 'src/shared/application-config';
|
||||
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
|
||||
import { clickHouseDateToIso } from 'src/shared/clickhouse-date-to-iso';
|
||||
import { twentyClient } from 'src/shared/twenty-client';
|
||||
|
||||
const clickHouseWorkspaceSchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
workspaceName: z.string(),
|
||||
subdomain: z.string(),
|
||||
customDomain: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
deletedAt: z.string(),
|
||||
activationStatus: z
|
||||
.string()
|
||||
.transform((val) => {
|
||||
const upper = val.toUpperCase();
|
||||
if (upper === '') return 'EMPTY';
|
||||
const valid: string[] = Object.values(
|
||||
enumCloudWorkspace2ActivationStatusEnum,
|
||||
);
|
||||
return valid.includes(upper) ? upper : 'EMPTY';
|
||||
})
|
||||
.pipe(z.enum(enumCloudWorkspace2ActivationStatusEnum)),
|
||||
createdDate: z.string(),
|
||||
lastPageviewDate: z.string(),
|
||||
pageviewsLast30d: z.coerce.number(),
|
||||
pageviewsLast7d: z.coerce.number(),
|
||||
pageviewsLast24h: z.coerce.number(),
|
||||
totalEverActiveUsers: z.coerce.number(),
|
||||
totalWorkspaceUsers: z.coerce.number(),
|
||||
activeUsersLast30d: z.coerce.number(),
|
||||
activeUsersLast7d: z.coerce.number(),
|
||||
activeUsersLast24h: z.coerce.number(),
|
||||
isActiveLast30d: z.coerce.boolean(),
|
||||
isActiveLast7d: z.coerce.boolean(),
|
||||
isActiveLast24h: z.coerce.boolean(),
|
||||
activeUserRatioLast30d: z.coerce.number(),
|
||||
activeUserRatioLast7d: z.coerce.number(),
|
||||
workspaceAgeDays: z.coerce.number(),
|
||||
totalEvents: z.coerce.number(),
|
||||
eventsLast30d: z.coerce.number(),
|
||||
eventsPerUser: z.coerce.number(),
|
||||
subscription_status: z
|
||||
.string()
|
||||
.transform((val) => {
|
||||
const upper = val.toUpperCase();
|
||||
if (upper === '') return 'EMPTY';
|
||||
const valid: string[] = Object.values(
|
||||
enumCloudWorkspace2SubscriptionStatusEnum,
|
||||
);
|
||||
return valid.includes(upper) ? upper : 'OTHER';
|
||||
})
|
||||
.pipe(z.enum(enumCloudWorkspace2SubscriptionStatusEnum)),
|
||||
payment_frequency: z
|
||||
.string()
|
||||
.transform((val) => {
|
||||
const upper = val.toUpperCase();
|
||||
if (upper === '') return 'EMPTY';
|
||||
const valid: string[] = Object.values(
|
||||
enumCloudWorkspace2PaymentFrequencyEnum,
|
||||
);
|
||||
return valid.includes(upper) ? upper : 'OTHER';
|
||||
})
|
||||
.pipe(z.enum(enumCloudWorkspace2PaymentFrequencyEnum)),
|
||||
trial_status: z.string(),
|
||||
mrr: z.coerce.number(),
|
||||
potential_arr: z.coerce.number(),
|
||||
arr: z.coerce.number().nullable(),
|
||||
next_renewal_date: z.string(),
|
||||
workspace_domain: z.string(),
|
||||
domain_source: z.string(),
|
||||
creatorUserId: z.string(),
|
||||
creatorEmail: z.string(),
|
||||
creator_domain_type: z.string(),
|
||||
primary_business_domain: z.string(),
|
||||
business_domain_user_count: z.coerce.number(),
|
||||
isTwenty: z.coerce.boolean(),
|
||||
});
|
||||
|
||||
type ClickHouseWorkspace = z.infer<typeof clickHouseWorkspaceSchema>;
|
||||
|
||||
const fetchWorkspacesFromClickHouse =
|
||||
async (): Promise<ClickHouseWorkspace[]> => {
|
||||
const { clickHouseDatabase } = getApplicationConfig();
|
||||
|
||||
// const nowDate = 'now()'
|
||||
const nowDate = "'2026-02-04 14:28:52.000'";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
*
|
||||
FROM (
|
||||
SELECT
|
||||
*,
|
||||
row_number() OVER (PARTITION BY workspaceId ORDER BY updatedAt DESC) AS rn
|
||||
FROM
|
||||
${clickHouseDatabase}.workspace
|
||||
WHERE
|
||||
updatedAt >= ${nowDate} - INTERVAL 500 MINUTE
|
||||
AND
|
||||
updatedAt <= ${nowDate}
|
||||
)
|
||||
WHERE
|
||||
rn = 1
|
||||
FORMAT
|
||||
JSONEachRow;
|
||||
`;
|
||||
|
||||
return fetchFromClickHouse(query, clickHouseWorkspaceSchema);
|
||||
};
|
||||
|
||||
// Convert a dollar amount to micros (1 USD = 1_000_000 micros).
|
||||
const dollarsToAmountMicros = (dollars: number) =>
|
||||
Math.round(dollars * 1_000_000);
|
||||
|
||||
const buildCloudWorkspaceInput = (workspace: ClickHouseWorkspace) => ({
|
||||
id: workspace.workspaceId,
|
||||
name: workspace.workspaceName,
|
||||
subDomain: workspace.subdomain,
|
||||
customDomain: {
|
||||
primaryLinkUrl: workspace.customDomain,
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
activationStatus: workspace.activationStatus,
|
||||
subscriptionStatus: workspace.subscription_status,
|
||||
paymentFrequency: workspace.payment_frequency,
|
||||
lastPageViewDate: workspace.lastPageviewDate,
|
||||
pageViewsL30D: workspace.pageviewsLast30d,
|
||||
pageViewsL7D: workspace.pageviewsLast7d,
|
||||
pageViewsL24H: workspace.pageviewsLast24h,
|
||||
totalEverActiveWorkspaceUsers: workspace.totalEverActiveUsers,
|
||||
totalWorkspaceUsers: workspace.totalWorkspaceUsers,
|
||||
activeUsersL30D: workspace.activeUsersLast30d,
|
||||
activeUsersL7D: workspace.activeUsersLast7d,
|
||||
activeUsersL24H: workspace.activeUsersLast24h,
|
||||
isActiveL30D: workspace.isActiveLast30d,
|
||||
isActiveL7D: workspace.isActiveLast7d,
|
||||
isActiveL24H: workspace.isActiveLast24h,
|
||||
workspaceTenure: workspace.workspaceAgeDays,
|
||||
numberOfEventsTotal: workspace.totalEvents,
|
||||
numberOfEventsL30D: workspace.eventsLast30d,
|
||||
mrr: {
|
||||
amountMicros: dollarsToAmountMicros(workspace.mrr),
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
potentialArr: {
|
||||
amountMicros: dollarsToAmountMicros(workspace.potential_arr),
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
arr: {
|
||||
amountMicros: dollarsToAmountMicros(workspace.arr ?? 0),
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
nextRenewalDate: clickHouseDateToIso(workspace.next_renewal_date),
|
||||
creatorEmail: {
|
||||
primaryEmail: workspace.creatorEmail,
|
||||
},
|
||||
workspaceBusinessDomain: {
|
||||
primaryLinkUrl: workspace.primary_business_domain,
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
dataLastUpdatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
export const syncCloudWorkspaces = async (): Promise<{
|
||||
syncedCount: number;
|
||||
}> => {
|
||||
const workspaces = await fetchWorkspacesFromClickHouse();
|
||||
|
||||
console.log(`Fetched ${workspaces.length} workspaces from ClickHouse`);
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
return { syncedCount: 0 };
|
||||
}
|
||||
|
||||
const cloudWorkspaceInputs = workspaces.map(buildCloudWorkspaceInput);
|
||||
|
||||
console.log(
|
||||
`Batch-upserting ${cloudWorkspaceInputs.length} cloud workspaces`,
|
||||
);
|
||||
|
||||
await twentyClient.mutation({
|
||||
createCloudWorkspaces2: {
|
||||
__args: {
|
||||
data: cloudWorkspaceInputs,
|
||||
upsert: true,
|
||||
},
|
||||
__scalar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { syncedCount: workspaces.length };
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { ALL_CLOUD_USER_2_VIEW_ID } from 'src/views/all-cloud-user-2';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'ac6df084-0f0f-404a-b3d1-b084cb624d76',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
name: 'cloud-user-2',
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: ALL_CLOUD_USER_2_VIEW_ID,
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID } from 'src/views/all-cloud-user-workspace-2';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'e29251f0-78ad-493f-8f99-ec54ab0b5378',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
name: 'cloud-user-workspace-2',
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID,
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { ALL_CLOUD_WORKSPACE_2_VIEW_ID } from 'src/views/all-cloud-workspace-2';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '0ba6315e-0d79-47a5-8c05-29dec621a647',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
name: 'cloud-workspace-2',
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: ALL_CLOUD_WORKSPACE_2_VIEW_ID,
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
|
||||
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
|
||||
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
|
||||
CLOUD_USER_2_EMAIL_FIELD_ID,
|
||||
CLOUD_USER_2_FULL_NAME_FIELD_ID,
|
||||
CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
CLOUD_USER_2_IS_TWENTY_FIELD_ID,
|
||||
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
|
||||
CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
CLOUD_USER_2_UPDATED_BY_FIELD_ID,
|
||||
CLOUD_USER_2_USER_TENURE_FIELD_ID,
|
||||
CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
|
||||
} from 'src/fields/cloud-user-2-field-ids';
|
||||
|
||||
export const CLOUD_USER_2_UNIVERSAL_IDENTIFIER =
|
||||
'da264c72-df22-49b3-98e3-21cf6013e671';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'cloudUser2',
|
||||
namePlural: 'cloudUsers2',
|
||||
labelSingular: 'Cloud user 2',
|
||||
labelPlural: 'Cloud users 2',
|
||||
icon: 'IconBox',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
|
||||
type: FieldType.SELECT,
|
||||
name: 'activityStatus',
|
||||
label: 'Activity Status',
|
||||
options: [
|
||||
{
|
||||
id: '141d21f3-71b0-4b37-a3d2-5d91d43d0493',
|
||||
value: 'ACTIVE',
|
||||
label: 'Active',
|
||||
position: 0,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'adb1d290-750f-468e-99ee-ed892bcb8974',
|
||||
value: 'RECENT',
|
||||
label: 'Recent',
|
||||
position: 1,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: '96f24b75-a671-4727-a78f-5a72419370ea',
|
||||
value: 'DORMANT',
|
||||
label: 'Dormant',
|
||||
position: 2,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
id: '88ddbef9-16a8-4643-9726-5b271cd477fa',
|
||||
value: 'INACTIVE',
|
||||
label: 'Inactive',
|
||||
position: 3,
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'avgDailyPageviewsLast30d',
|
||||
label: 'Avg Daily Page Views',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'dataLastUpdatedAt',
|
||||
label: 'Data last updated at',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'daysSinceLastActivity',
|
||||
label: 'Days Since Last Activity',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_EMAIL_FIELD_ID,
|
||||
type: FieldType.EMAILS,
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_FULL_NAME_FIELD_ID,
|
||||
type: FieldType.FULL_NAME,
|
||||
name: 'fullName',
|
||||
label: 'Full Name',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isActiveL24h',
|
||||
label: 'Is Active L24H',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isActiveL30d',
|
||||
label: 'Is Active L30D',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isActiveL7d',
|
||||
label: 'Is Active L7D',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_IS_TWENTY_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isTwenty',
|
||||
label: 'Is Twenty',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'lastActivityDate',
|
||||
label: 'Last Activity Date',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'pageViewsL24h',
|
||||
label: 'Page Views L24H',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'pageViewsL30d',
|
||||
label: 'Page Views L30D',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'pageViewsL7d',
|
||||
label: 'Page Views L7D',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_UPDATED_BY_FIELD_ID,
|
||||
type: FieldType.ACTOR,
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_USER_TENURE_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'userTenure',
|
||||
label: 'User tenure',
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'workspaceCount',
|
||||
label: 'Workspace count',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
|
||||
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
} from 'src/fields/cloud-user-workspace-2-field-ids';
|
||||
|
||||
export const CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER =
|
||||
'14d6e1f4-c513-4766-9210-bc5dc8294e51';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'cloudUserWorkspace2',
|
||||
namePlural: 'cloudUserWorkspaces2',
|
||||
labelSingular: 'Cloud user workspace 2',
|
||||
labelPlural: 'Cloud user workspaces 2',
|
||||
icon: 'IconBox',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'twentyUserIdentifier',
|
||||
label: 'Twenty User Identifier',
|
||||
icon: 'IconMan',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
displayedMaxRows: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'twentyWorkspaceIdentifier',
|
||||
label: 'Twenty Workspace Identifier',
|
||||
icon: 'IconScreenShare',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
displayedMaxRows: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'idOfTheUserWorkspace',
|
||||
label: 'Id of the user workspace',
|
||||
icon: 'IconTypography',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
displayedMaxRows: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
type: FieldType.ACTOR,
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The user who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,582 @@
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ARR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_MRR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
|
||||
} from 'src/fields/cloud-workspace-2-field-ids';
|
||||
|
||||
export const CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER =
|
||||
'77376ed3-19c1-4859-bde5-90f19ad02113';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'cloudWorkspace2',
|
||||
namePlural: 'cloudWorkspaces2',
|
||||
labelSingular: 'Cloud workspace 2',
|
||||
labelPlural: 'Cloud workspaces 2',
|
||||
icon: 'IconBox',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
|
||||
type: FieldType.LINKS,
|
||||
name: 'customDomain',
|
||||
label: 'Custom domain',
|
||||
description: 'Custom domain set up by the customer to use their own.',
|
||||
icon: 'IconLink',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
|
||||
type: FieldType.LINKS,
|
||||
name: 'workspaceBusinessDomain',
|
||||
label: 'Workspace Business Domain',
|
||||
icon: 'IconLink',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
|
||||
type: FieldType.LINKS,
|
||||
name: 'companyLinkedin',
|
||||
label: 'Company LinkedIn',
|
||||
icon: 'IconBrandLinkedin',
|
||||
isNullable: true,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
type: FieldType.DATE,
|
||||
name: 'dataLastUpdatedAt',
|
||||
label: 'Data last updated at',
|
||||
description: 'by workflow',
|
||||
icon: 'IconCalendarEvent',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
|
||||
type: FieldType.DATE,
|
||||
name: 'lastPageViewDate',
|
||||
label: 'Last Page View Date',
|
||||
icon: 'IconCalendarEvent',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier:
|
||||
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'totalEverActiveWorkspaceUsers',
|
||||
label: 'Total ever active workspace users',
|
||||
icon: 'IconUsers',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'activeUsersL30D',
|
||||
label: 'Active Users L30D',
|
||||
icon: 'IconUsersGroup',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'activeUsersL7D',
|
||||
label: 'Active Users L7D',
|
||||
icon: 'IconUsersGroup',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'activeUsersL24H',
|
||||
label: 'Active Users L24H',
|
||||
icon: 'IconUsersGroup',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'workspaceTenure',
|
||||
label: 'Workspace tenure',
|
||||
description: 'in days',
|
||||
icon: 'IconCalendarHeart',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'pageViewsL30D',
|
||||
label: 'Page Views L30D',
|
||||
icon: 'IconClick',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'pageViewsL7D',
|
||||
label: 'Page Views L7D',
|
||||
icon: 'IconClick',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'pageViewsL24H',
|
||||
label: 'Page Views L24H',
|
||||
icon: 'IconClick',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'totalWorkspaceUsers',
|
||||
label: 'Total workspace users',
|
||||
icon: 'IconUsers',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'numberOfEventsL30D',
|
||||
label: 'Number of events L30D',
|
||||
icon: 'IconHandClick',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'numberOfEventsTotal',
|
||||
label: 'Number of events total',
|
||||
icon: 'IconHandClick',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'alexaRank',
|
||||
label: 'Alexa Rank',
|
||||
icon: 'IconNumber9',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'employees',
|
||||
label: 'Employees',
|
||||
icon: 'IconUsers',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
universalSettings: {
|
||||
type: 'number',
|
||||
decimals: 0,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isActiveL7D',
|
||||
label: 'Is Active L7D',
|
||||
icon: 'IconActivity',
|
||||
isNullable: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isActiveL24H',
|
||||
label: 'Is Active L24H',
|
||||
icon: 'IconActivity',
|
||||
isNullable: true,
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isActiveL30D',
|
||||
label: 'Is Active L30D',
|
||||
icon: 'IconActivity',
|
||||
isNullable: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isEnriched',
|
||||
label: 'Is Enriched',
|
||||
icon: 'IconToggleLeft',
|
||||
isNullable: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'industry',
|
||||
label: 'Industry',
|
||||
icon: 'IconWorldLongitude',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyFoundedYear',
|
||||
label: 'Company Founded Year',
|
||||
icon: 'IconCalendarPlus',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
icon: 'IconBaselineDensitySmall',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'latestFundingStage',
|
||||
label: 'Latest Funding Stage',
|
||||
icon: 'IconBellDollar',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyName',
|
||||
label: 'Company Name',
|
||||
icon: 'IconTypography',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
|
||||
type: FieldType.TEXT,
|
||||
name: 'subDomain',
|
||||
label: 'Sub domain',
|
||||
description:
|
||||
'This is the subdomain chosen by customers to access their workspace',
|
||||
icon: 'IconLink',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'annualRevenue',
|
||||
label: 'Annual Revenue',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_MRR_FIELD_ID,
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'mrr',
|
||||
label: 'MRR',
|
||||
icon: 'IconMoneybag',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'potentialArr',
|
||||
label: 'Potential ARR',
|
||||
icon: 'IconMoneybag',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ARR_FIELD_ID,
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'arr',
|
||||
label: 'ARR',
|
||||
icon: 'IconMoneybag',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'totalFunding',
|
||||
label: 'Total Funding',
|
||||
icon: 'IconMoneybag',
|
||||
isNullable: true,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
|
||||
type: FieldType.SELECT,
|
||||
name: 'activationStatus',
|
||||
label: 'Activation Status',
|
||||
icon: 'IconTag',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
options: [
|
||||
{
|
||||
id: 'c41ee49c-e629-431e-8601-183726f93315',
|
||||
color: 'yellow',
|
||||
label: 'Pending creation',
|
||||
value: 'PENDING_CREATION',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: 'cb82ad14-5fcb-4ed0-af58-c92f84b45d2b',
|
||||
color: 'gray',
|
||||
label: 'Suspended',
|
||||
value: 'SUSPENDED',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'b1084bc8-0efd-44ce-974c-7fb44c310be5',
|
||||
color: 'green',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: 'b5b33c8e-eb3f-4758-99c9-d2f39c011b96',
|
||||
color: 'blue',
|
||||
label: 'Ongoing creation',
|
||||
value: 'ONGOING_CREATION',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '2ae41a69-c521-4545-9e0b-f1f941ceee83',
|
||||
color: 'purple',
|
||||
label: 'Empty',
|
||||
value: 'EMPTY',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
|
||||
type: FieldType.SELECT,
|
||||
name: 'subscriptionStatus',
|
||||
label: 'Subscription Status',
|
||||
icon: 'IconTag',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
options: [
|
||||
{
|
||||
id: 'b5ee9a91-0a15-4cbe-a36e-198f5bae431d',
|
||||
color: 'green',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: '78837243-38d9-44d4-8b9c-a6c0ca082ba8',
|
||||
color: 'blue',
|
||||
label: 'Trialing',
|
||||
value: 'TRIALING',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '6b27ee33-3c30-4694-a607-19264765b11c',
|
||||
color: 'gray',
|
||||
label: 'Canceled',
|
||||
value: 'CANCELED',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '05118231-18b6-4589-be23-8f1368c0182f',
|
||||
color: 'yellow',
|
||||
label: 'Other',
|
||||
value: 'OTHER',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: 'ea83a7ff-4fe0-4c56-88f8-cf0bdf76cb08',
|
||||
color: 'gray',
|
||||
label: 'Empty',
|
||||
value: 'EMPTY',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
|
||||
type: FieldType.SELECT,
|
||||
name: 'paymentFrequency',
|
||||
label: 'Payment Frequency',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
options: [
|
||||
{
|
||||
id: 'fa97d4fb-12bf-4cf4-b252-14b1c657d3b6',
|
||||
color: 'sky',
|
||||
label: 'Month',
|
||||
value: 'MONTH',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
id: 'c09c3f1d-31e7-4b29-969a-a4304f04abe0',
|
||||
color: 'purple',
|
||||
label: 'Year',
|
||||
value: 'YEAR',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'a91d77ed-90ad-41fc-83ae-e688c981abb2',
|
||||
color: 'gray',
|
||||
label: 'Other',
|
||||
value: 'OTHER',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '5c595203-1b33-430b-abd8-8bc80aeedbd7',
|
||||
color: 'red',
|
||||
label: 'Empty',
|
||||
value: 'EMPTY',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'nextRenewalDate',
|
||||
label: 'Next Renewal Date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
|
||||
type: FieldType.EMAILS,
|
||||
name: 'creatorEmail',
|
||||
label: 'Creator Email',
|
||||
icon: 'IconMail',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
|
||||
type: FieldType.ARRAY,
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
icon: 'IconBracketsContain',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
{
|
||||
universalIdentifier: CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
type: FieldType.ACTOR,
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The user who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'4bd740cc-f6a1-40c5-b46b-e2a4dbf5af04';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Twenty for Twenty default function role',
|
||||
description: 'Twenty for Twenty default function role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const applicationConfigSchema = z
|
||||
.object({
|
||||
CLICKHOUSE_DATABASE: z.string().nonempty(),
|
||||
CLICKHOUSE_URL: z.url(),
|
||||
CLICKHOUSE_USERNAME: z.string().nonempty(),
|
||||
CLICKHOUSE_PASSWORD: z.string().nonempty(),
|
||||
});
|
||||
|
||||
export const getApplicationConfig = () => {
|
||||
const env = applicationConfigSchema.parse({
|
||||
CLICKHOUSE_DATABASE: process.env.CLICKHOUSE_DATABASE,
|
||||
CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,
|
||||
CLICKHOUSE_USERNAME: process.env.CLICKHOUSE_USERNAME,
|
||||
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
|
||||
});
|
||||
|
||||
return {
|
||||
clickHouseDatabase: env.CLICKHOUSE_DATABASE,
|
||||
clickHouseUrl: env.CLICKHOUSE_URL,
|
||||
clickHouseUsername: env.CLICKHOUSE_USERNAME,
|
||||
clickHousePassword: env.CLICKHOUSE_PASSWORD,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getApplicationConfig } from 'src/shared/application-config';
|
||||
|
||||
// Fetches rows from ClickHouse using a raw SQL query,
|
||||
// parses the JSONEachRow response, and validates each row against the provided Zod schema.
|
||||
export const fetchFromClickHouse = async <T>(
|
||||
query: string,
|
||||
schema: z.ZodType<T>,
|
||||
): Promise<T[]> => {
|
||||
const { clickHouseUrl, clickHouseUsername, clickHousePassword } =
|
||||
getApplicationConfig();
|
||||
|
||||
const response = await fetch(clickHouseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization:
|
||||
'Basic ' +
|
||||
Buffer.from(`${clickHouseUsername}:${clickHousePassword}`).toString(
|
||||
'base64',
|
||||
),
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: query,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
throw new Error(`ClickHouse error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
// Format is a list of JSON objects, one per line (JSONEachRow format).
|
||||
const text = await response.text();
|
||||
|
||||
const rows = text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return z.array(schema).parse(rows);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
// ClickHouse returns DateTime64 values in the format 'YYYY-MM-DD HH:mm:ss.SSSSSS'
|
||||
// (space-separated, up to 6 fractional digits). Twenty's DATE_TIME fields expect
|
||||
// ISO 8601 format ('YYYY-MM-DDTHH:mm:ss.SSSZ', at most 3 fractional digits).
|
||||
//
|
||||
// Additionally, ClickHouse uses '1970-01-01 00:00:00.000000' as a sentinel for
|
||||
// null/empty dates. We map that to null.
|
||||
|
||||
const CLICKHOUSE_EPOCH_SENTINEL = '1970-01-01 00:00:00.000000';
|
||||
|
||||
export const clickHouseDateToIso = (value: string): string | null => {
|
||||
if (!value || value === CLICKHOUSE_EPOCH_SENTINEL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(value.replace(' ', 'T') + 'Z').toISOString();
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import Twenty from 'twenty-sdk/generated';
|
||||
|
||||
export const twentyClient = new Twenty();
|
||||
@@ -0,0 +1,159 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
|
||||
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
|
||||
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
|
||||
CLOUD_USER_2_EMAIL_FIELD_ID,
|
||||
CLOUD_USER_2_FULL_NAME_FIELD_ID,
|
||||
CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
CLOUD_USER_2_IS_TWENTY_FIELD_ID,
|
||||
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
|
||||
CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
CLOUD_USER_2_UPDATED_BY_FIELD_ID,
|
||||
CLOUD_USER_2_USER_TENURE_FIELD_ID,
|
||||
CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
|
||||
} from 'src/fields/cloud-user-2-field-ids';
|
||||
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
|
||||
|
||||
export const ALL_CLOUD_USER_2_VIEW_ID = 'd6137e11-dbcd-4824-85d3-42fe1ca48cb6';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_CLOUD_USER_2_VIEW_ID,
|
||||
name: 'all-cloud-user-2',
|
||||
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '9e82305e-9fd9-4bf7-8cc5-098eb35a748e',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_FULL_NAME_FIELD_ID,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ea737839-bcb7-4da1-aaf6-79cda65e6263',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_EMAIL_FIELD_ID,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ccd1eca7-c851-4e97-9f0e-48c7139f3ced',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e99c82d3-d2fe-4758-9253-ab2ca2b50ef8',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_TWENTY_FIELD_ID,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c03da046-30b6-402e-8d4b-10799aa5102d',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e3a787f2-77d1-4865-a5c2-c60976809d4d',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_USER_TENURE_FIELD_ID,
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 110,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '352d77f2-726b-4a75-8c9d-3117cfabedc1',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'cf06d6b0-e425-4daf-8a6d-9b66a058ae80',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '4ab63729-0b48-4b0b-ac9e-e72f5448998c',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '8c6a3315-fa47-4a12-b9fe-1775462c803d',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
position: 9,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '89011d63-e670-498e-bc8f-142f7ceafd04',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
position: 10,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '88816c57-aa5b-4efe-b53a-b288fad24ab4',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
position: 11,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '3b0ff937-1eaf-4589-80ec-23abb4f2e204',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
|
||||
position: 12,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'dee5f679-9ad9-4bc4-94a9-af28d7210699',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
|
||||
position: 13,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '25b4e175-be18-4b20-9c51-5db0a596d968',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
|
||||
position: 14,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f1706c21-d164-414c-9cf5-4cf376584bb9',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
position: 15,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '88d0f343-42c1-4d36-82df-cfdb74e785eb',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_USER_2_UPDATED_BY_FIELD_ID,
|
||||
position: 16,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
|
||||
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
} from 'src/fields/cloud-user-workspace-2-field-ids';
|
||||
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
|
||||
|
||||
export const ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID =
|
||||
'f80e44ef-43fb-409e-a003-decadaa38b3e';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID,
|
||||
name: 'all-cloud-user-workspace-2',
|
||||
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '4a0d9d93-d182-443d-8a70-3a4f25e51c50',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '14b78fc4-a6a5-471b-ba87-6aebc9a3da68',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 220,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '8c9c8a1b-d511-4706-afe7-32b22cce2e7e',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ee35eded-4222-4a87-b31c-69e415e331db',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_ARR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_MRR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
|
||||
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
|
||||
} from 'src/fields/cloud-workspace-2-field-ids';
|
||||
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
|
||||
|
||||
export const ALL_CLOUD_WORKSPACE_2_VIEW_ID =
|
||||
'3747a3a0-25a0-42f0-99d9-61a4b4a76009';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_CLOUD_WORKSPACE_2_VIEW_ID,
|
||||
name: 'all-cloud-workspace-2',
|
||||
objectUniversalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '146e3f00-8efc-46be-aafb-a6bafcefa5b0',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '10ce2149-d25b-4e21-8215-c628677bc4cb',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c8735db0-9a89-4204-ad58-b68b71c10182',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'cf3f182b-1a69-4274-a200-ef191400a9e8',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c932adb5-129b-4457-bbfa-75baaf27707f',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 110,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5f0d33f0-60f0-442c-8c2d-19bfe1c9ea49',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '904e4ecd-f997-42b6-933a-1174cf2bebdc',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f2d919e7-f782-4edb-ad1a-b99649fbbaf9',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_MRR_FIELD_ID,
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '92afa154-72f2-4b01-8376-2f5311bb40f9',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_ARR_FIELD_ID,
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c63bcd0a-f8d3-4c86-b9c0-6d638d7e9195',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
|
||||
position: 9,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '8659ca48-23b0-4fb8-b588-0919169cae4d',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
|
||||
position: 10,
|
||||
isVisible: true,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '1524079b-0e7e-41a6-80bc-85bcdfcab537',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
|
||||
position: 11,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '36a755ee-903c-4c61-b091-ebc0e687333d',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
|
||||
position: 12,
|
||||
isVisible: true,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5ac28b38-823f-421c-a177-1749b8283381',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
|
||||
position: 13,
|
||||
isVisible: true,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '6fa0bfe5-ac55-4165-8830-5c084466c7f5',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
|
||||
position: 14,
|
||||
isVisible: true,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'bee2ed03-23c2-4c99-86db-c0dd4599c669',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
|
||||
position: 15,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e8f9775b-c984-4832-800a-3dd26060c0aa',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
|
||||
position: 16,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95c4fdfc-8fe5-43ce-9885-3f0fc5b4756a',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
|
||||
position: 17,
|
||||
isVisible: true,
|
||||
size: 130,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '6448c0f1-1469-466e-99ed-c7aa015d9e38',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
|
||||
position: 18,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '082ed200-9bba-444a-9b28-3a129b7ac9d8',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
|
||||
position: 19,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd61bbf0c-48ad-428a-90a1-87b54a87bacf',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
|
||||
position: 20,
|
||||
isVisible: true,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '26058a5b-edde-4caa-a034-32b8edb54ce6',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
|
||||
position: 21,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '36931d2b-f250-4325-a877-3285dd8a797e',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
|
||||
position: 22,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c120fc92-4796-4cf4-866f-c1d8c4555321',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
|
||||
position: 23,
|
||||
isVisible: true,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a6674592-070b-41c1-baab-f360984328cc',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
|
||||
position: 24,
|
||||
isVisible: true,
|
||||
size: 110,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd3701295-ef41-46c4-8dd3-b31c27663516',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
|
||||
position: 25,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'dd22ffe7-95ed-4c67-be33-c2d1592daa32',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
|
||||
position: 26,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c7a9bcf8-d41d-4ded-b680-868e470e3228',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
|
||||
position: 27,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '398eb88f-0d12-446d-a486-d1fb262f82bb',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
|
||||
position: 28,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '52d2a1b3-e2b2-4d6b-a5dd-ea8d63c7c369',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
|
||||
position: 29,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f74fed2c-94dc-4954-ac27-79acd7e04103',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
|
||||
position: 30,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '666d0cea-e50b-4cc8-9ed5-5f5c0ec7fe7e',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
|
||||
position: 31,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '60508519-5ac4-4f96-a30f-5c75d3ce9a93',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
|
||||
position: 32,
|
||||
isVisible: true,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '3b1ca2b6-31a1-4755-a9a4-07ee9a9b8a33',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
|
||||
position: 33,
|
||||
isVisible: true,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '47ba21ab-a5c5-42b9-b1e2-7a988ece154d',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
|
||||
position: 34,
|
||||
isVisible: true,
|
||||
size: 110,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'de5e2e42-2600-488c-81d0-f700e7dd0f67',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
|
||||
position: 35,
|
||||
isVisible: true,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd6bec718-8b50-41e2-95d7-521151e2df82',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
|
||||
position: 36,
|
||||
isVisible: true,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '9aaff045-64fa-4857-a8df-7394cf43a120',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
|
||||
position: 37,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f2cb82b9-ebb4-4258-971b-061d709f28e5',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
|
||||
position: 38,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '7e631992-3ae6-478b-9874-67c3b211f05a',
|
||||
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
|
||||
position: 39,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
# Development infrastructure services only (Postgres + Redis).
|
||||
# Use this when developing locally against the source code.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.dev.yml up -d
|
||||
# docker compose -f docker-compose.dev.yml down # stop
|
||||
# docker compose -f docker-compose.dev.yml down -v # stop + wipe data
|
||||
|
||||
name: twenty-dev
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- dev-db-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: default
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
dev-db-data:
|
||||
@@ -38,7 +38,6 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
@@ -92,7 +91,6 @@ services:
|
||||
|
||||
# EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-contact@yourdomain.com}
|
||||
# EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:-"John from YourDomain"}
|
||||
# EMAIL_SYSTEM_ADDRESS: ${EMAIL_SYSTEM_ADDRESS:-system@yourdomain.com}
|
||||
# EMAIL_DRIVER: ${EMAIL_DRIVER:-smtp}
|
||||
# EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.gmail.com}
|
||||
# EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-465}
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
"EMAIL_SMTP_NO_TLS": { "type": "boolean" },
|
||||
"EMAIL_FROM_ADDRESS": { "type": "string" },
|
||||
"EMAIL_FROM_NAME": { "type": "string" },
|
||||
"EMAIL_SYSTEM_ADDRESS": { "type": "string" },
|
||||
"IS_EMAIL_VERIFICATION_REQUIRED": { "type": "boolean" },
|
||||
"EMAIL_VERIFICATION_TOKEN_EXPIRES_IN": { "type": "string" },
|
||||
"PASSWORD_RESET_TOKEN_EXPIRES_IN": { "type": "string" }
|
||||
|
||||
@@ -15,7 +15,6 @@ COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
|
||||
COPY ./packages/twenty-standard-application/package.json /app/packages/twenty-standard-application/
|
||||
|
||||
# Install all dependencies
|
||||
RUN yarn && yarn cache clean && npx nx reset
|
||||
@@ -29,13 +28,11 @@ COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
|
||||
COPY ./packages/twenty-standard-application /app/packages/twenty-standard-application
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
RUN npx nx build twenty-standard-application
|
||||
RUN npx nx run twenty-server:build
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-standard-application twenty-server
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
|
||||
|
||||
# Build the front
|
||||
FROM common-deps AS twenty-front-build
|
||||
|
||||
+2
-3
@@ -9,7 +9,7 @@ The goal here is to have a consistent codebase, which is easy to read and easy t
|
||||
|
||||
For this, it's better to be a bit more verbose than to be too concise.
|
||||
|
||||
Always keep in mind that people read code more often than they write it, specially on an open source project, where anyone can contribute.
|
||||
Always keep in mind that people read code more often than they write it, especially on an open source project, where anyone can contribute.
|
||||
|
||||
There are a lot of rules that are not defined here, but that are automatically checked by linters.
|
||||
|
||||
@@ -150,7 +150,7 @@ type MyType = {
|
||||
|
||||
### Use string literals instead of enums
|
||||
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, specially with code completion.
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, especially with code completion.
|
||||
|
||||
You can see why TypeScript recommends avoiding enums [here](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
|
||||
|
||||
@@ -288,4 +288,3 @@ An Oxlint rule, `typescript/consistent-type-imports`, enforces the no-type impor
|
||||
Please note that this rule specifically addresses rare edge cases where unintentional type imports occur. TypeScript itself discourages this practice, as mentioned in the [TypeScript 3.8 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). In most situations, you should not need to use type-only imports.
|
||||
|
||||
To ensure your code complies with this rule, make sure to run Oxlint as part of your development workflow.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "The guide for contributors (or curious developers) who want to run
|
||||
## Prerequisites
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux and MacOS">
|
||||
<Tab title="Linux and macOS">
|
||||
|
||||
Before you can install and use Twenty, make sure you install the following on your computer:
|
||||
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
@@ -31,7 +31,7 @@ wsl --install
|
||||
```
|
||||
You should now see a prompt to restart your computer. If not, restart it manually.
|
||||
|
||||
Upon restart, a powershell window will open and install Ubuntu. This may take up some time.
|
||||
Upon restart, a PowerShell window will open and install Ubuntu. This may take up some time.
|
||||
You'll see a prompt to create a username and password for your Ubuntu installation.
|
||||
|
||||
2. Install and configure git
|
||||
@@ -104,7 +104,7 @@ You should run all commands in the following steps from the root of the project.
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**Option 1 (preferred):** To provision your database locally:
|
||||
Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
Use the following link to install PostgreSQL on your Linux machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -131,7 +131,7 @@ You should run all commands in the following steps from the root of the project.
|
||||
```
|
||||
|
||||
The installer might not create the `postgres` user by default when installing
|
||||
via Homebrew on MacOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
via Homebrew on macOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
username (e.g., "john").
|
||||
To check and create the `postgres` user if necessary, follow these steps:
|
||||
```bash
|
||||
@@ -174,8 +174,8 @@ You should run all commands in the following steps from the root of the project.
|
||||
<Tab title="Windows (WSL)">
|
||||
All the following steps are to be run in the WSL terminal (within your virtual machine)
|
||||
|
||||
**Option 1:** To provision your Postgresql locally:
|
||||
Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
**Option 1:** To provision your PostgreSQL locally:
|
||||
Use the following link to install PostgreSQL on your Linux virtual machine: [PostgreSQL Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -190,10 +190,12 @@ You should run all commands in the following steps from the root of the project.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
You can now access the database at [localhost:5432](localhost:5432), with user `postgres` and password `postgres` .
|
||||
You can now access the database at `localhost:5432`.
|
||||
|
||||
If you used the Docker option above, the default credentials are user `postgres` and password `postgres`. For native PostgreSQL installations, use the credentials and roles configured on your machine.
|
||||
|
||||
## Step 4: Set up a Redis Database (cache)
|
||||
Twenty requires a redis cache to provide the best performance
|
||||
Twenty requires a Redis cache to provide the best performance.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
@@ -210,8 +212,10 @@ Twenty requires a redis cache to provide the best performance
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
Start your redis server:
|
||||
```brew services start redis```
|
||||
Start your Redis server:
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
|
||||
**Option 2:** If you have docker installed:
|
||||
```bash
|
||||
@@ -229,11 +233,11 @@ Twenty requires a redis cache to provide the best performance
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/) (free version available)
|
||||
If you need a client GUI, we recommend [Redis Insight](https://redis.io/insight/) (free version available).
|
||||
|
||||
## Step 5: Setup environment variables
|
||||
## Step 5: Set up environment variables
|
||||
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup)
|
||||
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup).
|
||||
|
||||
Copy the `.env.example` files in `/front` and `/server`:
|
||||
```bash
|
||||
|
||||
@@ -63,6 +63,12 @@ yarn twenty function:execute --preInstall
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Build the app for distribution
|
||||
yarn twenty app:build
|
||||
|
||||
# Publish the app to npm or a Twenty server
|
||||
yarn twenty app:publish
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn twenty app:uninstall
|
||||
|
||||
@@ -1224,6 +1230,113 @@ Key points:
|
||||
|
||||
Explore a minimal, end-to-end example that demonstrates objects, logic functions, front components, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Building your app
|
||||
|
||||
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Build the app (output goes to .twenty/output/)
|
||||
yarn twenty app:build
|
||||
|
||||
# Build and create a tarball (.tgz) for distribution
|
||||
yarn twenty app:build --tarball
|
||||
```
|
||||
|
||||
The build process:
|
||||
|
||||
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
|
||||
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
|
||||
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
|
||||
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
|
||||
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
|
||||
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
|
||||
7. **Optionally creates a tarball** — if `--tarball` is passed, runs `npm pack` to create a `.tgz` file ready for distribution.
|
||||
|
||||
The build output in `.twenty/output/` contains:
|
||||
|
||||
```text
|
||||
.twenty/output/
|
||||
├── manifest.json # Manifest with checksums for all built files
|
||||
├── package.json # Copied from app root
|
||||
├── yarn.lock # Copied from app root
|
||||
├── src/
|
||||
│ ├── logic-functions/ # Compiled .mjs logic function files
|
||||
│ └── front-components/ # Compiled .mjs front component files
|
||||
├── public/ # Static assets (if any)
|
||||
└── my-app-1.0.0.tgz # Only with --tarball flag
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `[appPath]` | Path to the app directory (defaults to current directory) |
|
||||
| `--tarball` | Also pack the output into a `.tgz` tarball |
|
||||
|
||||
## Publishing your app
|
||||
|
||||
Use `app:publish` to distribute your app — either to the npm registry or directly to a Twenty server.
|
||||
|
||||
### Publish to npm (default)
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
This builds the app and runs `npm publish` from the `.twenty/output/` directory. The published package can then be installed from the Twenty marketplace by any workspace.
|
||||
|
||||
### Publish to a Twenty server
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish directly to a Twenty server
|
||||
yarn twenty app:publish --server https://app.twenty.com
|
||||
```
|
||||
|
||||
This builds the app with a tarball, uploads it to the server via the `uploadAppTarball` GraphQL mutation, and triggers installation in one step. This is useful for private deployments or testing against a specific server.
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `[appPath]` | Path to the app directory (defaults to current directory) |
|
||||
| `--server <url>` | Publish to a Twenty server instead of npm |
|
||||
| `--token <token>` | Authentication token for the target server |
|
||||
| `--tag <tag>` | npm dist-tag (e.g. `beta`, `next`) — only for npm publish |
|
||||
|
||||
## Application registration
|
||||
|
||||
Before an app can be installed in a workspace, it must be **registered**. A registration is a metadata record that describes where the app comes from and how to authenticate it. This is handled automatically by the CLI in most cases.
|
||||
|
||||
### Source types
|
||||
|
||||
Each registration has a **source type** that determines how the app's files are resolved during installation:
|
||||
|
||||
| Source type | How files are resolved | Typical use case |
|
||||
|-------------|----------------------|------------------|
|
||||
| `LOCAL` | Files are synced in real-time by the CLI watcher — installation is skipped | Development with `app:dev` |
|
||||
| `NPM` | Fetched from the npm registry via the `sourcePackage` field | Published apps on npm |
|
||||
| `TARBALL` | Extracted from an uploaded `.tgz` file stored on the server | Private apps published with `--server` |
|
||||
|
||||
### How registration happens
|
||||
|
||||
- **`app:dev`** — automatically creates a `LOCAL` registration the first time you run dev mode against a workspace.
|
||||
- **`app:publish --server`** — uploads a tarball and creates (or updates) a `TARBALL` registration, then installs the app.
|
||||
- **npm marketplace** — `NPM` registrations are created when apps are synced from the npm registry into the Twenty marketplace catalog.
|
||||
- **GraphQL API** — you can also create registrations programmatically via the `createApplicationRegistration` mutation.
|
||||
|
||||
### Registration vs installation
|
||||
|
||||
**Registration** and **installation** are separate concepts:
|
||||
|
||||
- A **registration** (`ApplicationRegistration`) is a global metadata record describing the app: its name, source type, OAuth credentials, and marketplace listing status. It exists independently of any workspace.
|
||||
- An **installation** (`Application`) is a per-workspace instance. When a user installs an app, Twenty resolves the package from the registration's source, writes the built files to storage, and synchronizes the manifest (creating objects, fields, logic functions, etc.) in that workspace.
|
||||
|
||||
One registration can be installed in many workspaces. Each workspace gets its own copy of the app's files and data model.
|
||||
|
||||
### OAuth credentials
|
||||
|
||||
Each registration includes OAuth credentials (`oAuthClientId` and `oAuthClientSecret`) generated at creation time. These are used by the app to authenticate API requests on behalf of users. The client secret is returned **once** at creation — store it securely. You can rotate it later via the `rotateApplicationRegistrationClientSecret` mutation.
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 1-Click w/ Docker Compose
|
||||
|
||||
|
||||
<Warning>
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](/developers/contribute/capabilities/local-setup).
|
||||
Docker containers are for production hosting or self-hosting. For contributing, please check the [Local Setup](/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## Overview
|
||||
@@ -13,7 +13,7 @@ This guide provides step-by-step instructions to install and configure the Twent
|
||||
|
||||
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
|
||||
|
||||
See docs [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
See [Setup Environment Variables](/developers/self-host/capabilities/setup) for advanced configuration. All environment variables must be declared in the `docker-compose.yml` file at the server and/or worker level, depending on the variable.
|
||||
|
||||
## System Requirements
|
||||
|
||||
@@ -237,4 +237,3 @@ docker compose up -d
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions.
|
||||
|
||||
|
||||
|
||||
@@ -289,43 +289,57 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
|
||||
## Logic Functions
|
||||
## Logic Functions & Code Interpreter
|
||||
|
||||
Twenty supports logic functions for workflows and custom logic. The execution environment is configured via the `SERVERLESS_TYPE` environment variable.
|
||||
Twenty supports logic functions for workflows and the code interpreter for AI data analysis. Both run user-provided code and require explicit configuration for security.
|
||||
|
||||
### Security Defaults
|
||||
|
||||
**In production (NODE_ENV=production):** Both logic functions and code interpreter default to **Disabled**. You must explicitly enable them with `LOGIC_FUNCTION_TYPE` and `CODE_INTERPRETER_TYPE` if you need these features.
|
||||
|
||||
**In development (NODE_ENV=development):** Both default to **LOCAL** for convenience when running locally.
|
||||
|
||||
<Warning>
|
||||
**Security Notice:** The local driver (`SERVERLESS_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, we highly recommend using `SERVERLESS_TYPE=LAMBDA` or `SERVERLESS_TYPE=DISABLED`.
|
||||
**Security Notice:** The local driver (`LOGIC_FUNCTION_TYPE=LOCAL` or `CODE_INTERPRETER_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, use `LOGIC_FUNCTION_TYPE=LAMBDA` or `CODE_INTERPRETER_TYPE=E2B` (with sandboxing), or keep them disabled.
|
||||
</Warning>
|
||||
|
||||
### Available Drivers
|
||||
### Logic Functions - Available Drivers
|
||||
|
||||
| Driver | Environment Variable | Use Case | Security Level |
|
||||
|--------|---------------------|----------|----------------|
|
||||
| Disabled | `SERVERLESS_TYPE=DISABLED` | Disable logic functions entirely | N/A |
|
||||
| Local | `SERVERLESS_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
|
||||
| Disabled | `LOGIC_FUNCTION_TYPE=DISABLED` | Disable logic functions entirely | N/A |
|
||||
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
|
||||
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
|
||||
|
||||
### Recommended Configuration
|
||||
### Logic Functions - Recommended Configuration
|
||||
|
||||
**For development:**
|
||||
```bash
|
||||
SERVERLESS_TYPE=LOCAL # default
|
||||
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
|
||||
```
|
||||
|
||||
**For production (AWS):**
|
||||
```bash
|
||||
SERVERLESS_TYPE=LAMBDA
|
||||
SERVERLESS_LAMBDA_REGION=us-east-1
|
||||
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
LOGIC_FUNCTION_TYPE=LAMBDA
|
||||
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
|
||||
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**To disable logic functions:**
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
|
||||
```
|
||||
|
||||
### Code Interpreter - Available Drivers
|
||||
|
||||
| Driver | Environment Variable | Use Case | Security Level |
|
||||
|--------|---------------------|----------|----------------|
|
||||
| Disabled | `CODE_INTERPRETER_TYPE=DISABLED` | Disable AI code execution | N/A |
|
||||
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Development only | Low (no sandboxing) |
|
||||
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Production with sandboxed execution | High (isolated sandbox) |
|
||||
|
||||
<Note>
|
||||
When using `SERVERLESS_TYPE=DISABLED`, any attempt to execute a logic function will return an error. This is useful if you want to run Twenty without logic function capabilities.
|
||||
When using `LOGIC_FUNCTION_TYPE=DISABLED` or `CODE_INTERPRETER_TYPE=DISABLED`, any attempt to execute will return an error. This is useful if you want to run Twenty without these capabilities.
|
||||
</Note>
|
||||
|
||||
@@ -6302,6 +6302,10 @@
|
||||
"source": "/developers/extend/capabilities/apps",
|
||||
"destination": "/developers/extend/apps/getting-started"
|
||||
},
|
||||
{
|
||||
"source": "/developers/extend/mcp",
|
||||
"destination": "/user-guide/ai/capabilities/mcp"
|
||||
},
|
||||
{
|
||||
"source": "/developers/local-setup",
|
||||
"destination": "/developers/contribute/capabilities/local-setup"
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ title: دليل الأسلوب
|
||||
|
||||
لهذا، من الأفضل أن تكون تفصيلًا أكثر قليلاً بدلاً من أن تكون موجزًا للغاية.
|
||||
|
||||
دائمًا ضع في اعتبارك أن الناس يقرؤون التعليمات البرمجية أكثر مما يكتبونها، وخاصة في المشاريع مفتوحة المصدر، حيث يمكن لأي شخص المساهمة.
|
||||
دائمًا ضع في اعتبارك أن الناس يقرؤون التعليمات البرمجية أكثر مما يكتبونها، وخاصة في مشروع مفتوح المصدر، حيث يمكن لأي شخص المساهمة.
|
||||
|
||||
هناك العديد من القواعد التي لم يتم تعريفها هنا، ولكن يتم التحقق منها تلقائيًا بواسطة أدوات الفحص.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: الدليل للمساهمين (أو المطورين الفضول
|
||||
## المتطلبات الأساسية
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux و MacOS">
|
||||
<Tab title="Linux و macOS">
|
||||
|
||||
قبل أن تتمكن من تثبيت واستخدام Twenty، تأكد من تثبيت الأمور التالية على جهاز الكمبيوتر الخاص بك:
|
||||
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
@@ -103,7 +103,7 @@ cd twenty
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**الخيار 1 (المفضل):** لتوفير قاعدة بياناتك محليًا:
|
||||
استخدم الرابط التالي لتثبيت Postgresql على جهاز Linux الخاص بك: [تثبيت Postgresql](https://www.postgresql.org/download/linux/)
|
||||
استخدم الرابط التالي لتثبيت PostgreSQL على جهاز Linux الخاص بك: [تثبيت PostgreSQL](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -129,8 +129,8 @@ cd twenty
|
||||
brew services list
|
||||
```
|
||||
|
||||
المثبت قد لا ينشئ المستخدم `postgres` افتراضيًا عند التثبيت
|
||||
عبر Homebrew على MacOS. بدلاً من ذلك، فإنه ينشئ دور PostgreSQL يطابق
|
||||
قد لا يقوم المُثبِّت بإنشاء المستخدم `postgres` افتراضيًا عند التثبيت
|
||||
عبر Homebrew على macOS. بدلاً من ذلك، فإنه ينشئ دور PostgreSQL يطابق
|
||||
اسم المستخدم الخاص بك في MacOS (مثل "john").
|
||||
للتحقق وإنشاء المستخدم `postgres` إذا لزم الأمر، اتبع هذه الخطوات:
|
||||
```bash
|
||||
@@ -173,8 +173,8 @@ cd twenty
|
||||
<Tab title="ويندوز (WSL)">
|
||||
يجب أن تُنفذ جميع الخطوات التالية في تيرمينال WSL (داخل جهازك الافتراضي)
|
||||
|
||||
**الخيار 1:** لتوفير قاعدة بيانات Postgresql الخاصة بك محليًا:
|
||||
استخدم الرابط التالي لتثبيت Postgresql على جهاز Linux الافتراضي الخاص بك: [تثبيت Postgresql](https://www.postgresql.org/download/linux/)
|
||||
**الخيار 1:** لتوفير قاعدة بيانات PostgreSQL الخاصة بك محليًا:
|
||||
استخدم الرابط التالي لتثبيت PostgreSQL على جهاز Linux الافتراضي الخاص بك: [تثبيت PostgreSQL](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -189,11 +189,13 @@ cd twenty
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
يمكنك الآن الوصول إلى قاعدة البيانات على [localhost:5432](localhost:5432)، مع المستخدم `postgres` وكلمة المرور `postgres`.
|
||||
يمكنك الآن الوصول إلى قاعدة البيانات على `localhost:5432`.
|
||||
|
||||
إذا استخدمت خيار Docker أعلاه، فإن بيانات الاعتماد الافتراضية هي اسم المستخدم `postgres` وكلمة المرور `postgres`. بالنسبة لتثبيتات PostgreSQL الأصلية، استخدم بيانات الاعتماد والأدوار المُكوَّنة على جهازك.
|
||||
|
||||
## الخطوة 4: إعداد قاعدة بيانات Redis (للتخزين المؤقت)
|
||||
|
||||
يتطلب Twenty مخزن بيانات Redis لتقديم أفضل أداء
|
||||
يتطلب Twenty مخزن بيانات Redis لتقديم أفضل أداء.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
@@ -210,8 +212,10 @@ cd twenty
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
ابدأ خادم redis الخاص بك:
|
||||
`brew services start redis`
|
||||
ابدأ تشغيل خادم Redis:
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
|
||||
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
|
||||
```bash
|
||||
@@ -229,11 +233,11 @@ cd twenty
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
إذا كنت بحاجة إلى واجهة رسومية للعميل، نوصي بـ [redis insight](https://redis.io/insight/) (يتوفر إصدار مجاني)
|
||||
إذا كنت بحاجة إلى واجهة رسومية للعميل، نوصي بـ [Redis Insight](https://redis.io/insight/) (يتوفر إصدار مجاني).
|
||||
|
||||
## الخطوة 5: إعداد متغيرات البيئة
|
||||
|
||||
استخدم متغيرات البيئة أو ملفات `.env` لتكوين مشروعك. المزيد من المعلومات [هنا](/l/ar/developers/self-host/capabilities/setup)
|
||||
استخدم متغيرات البيئة أو ملفات `.env` لتكوين مشروعك. المزيد من المعلومات [هنا](/l/ar/developers/self-host/capabilities/setup).
|
||||
|
||||
انسخ ملفات `.env.example` الموجودة في `/front` و`/server`:
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ yarn twenty function:execute --preInstall
|
||||
# نفّذ دالة ما بعد التثبيت
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# ابنِ التطبيق للتوزيع
|
||||
yarn twenty app:build
|
||||
|
||||
# انشر التطبيق إلى npm أو إلى خادم Twenty
|
||||
yarn twenty app:publish
|
||||
|
||||
# أزل تثبيت التطبيق من مساحة العمل الحالية
|
||||
yarn twenty app:uninstall
|
||||
|
||||
@@ -1240,6 +1246,113 @@ uploadFile(
|
||||
|
||||
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف المنطقية والمكوّنات الأمامية ومشغّلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## بناء تطبيقك
|
||||
|
||||
بمجرد أن تطوّر تطبيقك باستخدام `app:dev`، استخدم `app:build` لإنشاء حزمة قابلة للتوزيع منه.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# ابنِ التطبيق (الإخراج يذهب إلى .twenty/output/)
|
||||
yarn twenty app:build
|
||||
|
||||
# ابنِ وأنشئ ملف tarball (.tgz) للتوزيع
|
||||
yarn twenty app:build --tarball
|
||||
```
|
||||
|
||||
عملية البناء:
|
||||
|
||||
1. **يقوم بتحليل ملف البيان والتحقق من صحته** — يقرأ جميع الكيانات `defineX()` من ملفات المصدر لديك ويُتحقّق من بنية ملف البيان.
|
||||
2. **يُصرِّف دوال المنطق ومكوّنات الواجهة** — يُجمّع مصادر TypeScript إلى ملفات ESM `.mjs` باستخدام esbuild.
|
||||
3. **يولّد قيم التحقّق** — يحسب تجزئات MD5 لكل ملف مُبنًى، وتُخزَّن في ملف البيان كـ `builtHandlerChecksum` / `builtComponentChecksum`.
|
||||
4. **ينشئ عميل API مضبوط الأنواع** — يفحص مخطط GraphQL ويُنشئ عميلَي `CoreApiClient` و`MetadataApiClient` مضبوطي الأنواع.
|
||||
5. **يشغّل فحص الأنواع لـ TypeScript** — يشغّل `tsc --noEmit` لاكتشاف أخطاء الأنواع قبل النشر.
|
||||
6. **يعيد البناء باستخدام العميل المُولَّد** — يُجري مرحلة ترجمة ثانية بحيث تُدرَج أنواع العميل المُولَّد.
|
||||
7. **ينشئ أرشيف tar اختياريًا** — إذا تم تمرير `--tarball`، يشغّل `npm pack` لإنشاء ملف `.tgz` جاهز للتوزيع.
|
||||
|
||||
مخرجات البناء في `.twenty/output/` تتضمّن:
|
||||
|
||||
```text
|
||||
.twenty/output/
|
||||
├── manifest.json # Manifest with checksums for all built files
|
||||
├── package.json # Copied from app root
|
||||
├── yarn.lock # Copied from app root
|
||||
├── src/
|
||||
│ ├── logic-functions/ # Compiled .mjs logic function files
|
||||
│ └── front-components/ # Compiled .mjs front component files
|
||||
├── public/ # Static assets (if any)
|
||||
└── my-app-1.0.0.tgz # Only with --tarball flag
|
||||
```
|
||||
|
||||
| الخيار | الوصف |
|
||||
| ----------- | -------------------------------------------------- |
|
||||
| `[appPath]` | المسار إلى دليل التطبيق (افتراضيًا: الدليل الحالي) |
|
||||
| `--tarball` | قم أيضًا بحزم المخرجات في أرشيف `.tgz` |
|
||||
|
||||
## نشر تطبيقك
|
||||
|
||||
استخدم `app:publish` لتوزيع تطبيقك — إما إلى سجل npm أو مباشرةً إلى خادم Twenty.
|
||||
|
||||
### النشر إلى npm (الإعداد الافتراضي)
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
يقوم هذا ببناء التطبيق وتشغيل `npm publish` من دليل `.twenty/output/`. بعد ذلك يمكن تثبيت الحزمة المنشورة من سوق Twenty بواسطة أي مساحة عمل.
|
||||
|
||||
### النشر إلى خادم Twenty
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish directly to a Twenty server
|
||||
yarn twenty app:publish --server https://app.twenty.com
|
||||
```
|
||||
|
||||
يقوم هذا ببناء التطبيق مع أرشيف tar، ويرفعه إلى الخادم عبر العملية `uploadAppTarball` في GraphQL، ويبدأ التثبيت في خطوة واحدة. يكون هذا مفيدًا لعمليات النشر الخاصة أو للاختبار مقابل خادم محدّد.
|
||||
|
||||
| الخيار | الوصف |
|
||||
| ----------------- | -------------------------------------------------------- |
|
||||
| `[appPath]` | المسار إلى دليل التطبيق (افتراضيًا: الدليل الحالي) |
|
||||
| `--server <url>` | انشر إلى خادم Twenty بدلًا من npm |
|
||||
| `--token <token>` | رمز المصادقة للخادم المستهدف |
|
||||
| `--tag <tag>` | علامة توزيع npm (مثل `beta`، `next`) — للنشر عبر npm فقط |
|
||||
|
||||
## تسجيل التطبيق
|
||||
|
||||
قبل أن يمكن تثبيت تطبيق في مساحة عمل، يجب أن يكون **مسجّلًا**. التسجيل هو سجل بيانات وصفية يوضّح مصدر التطبيق وكيفية مصادقته. يُعالَج هذا تلقائيًا بواسطة CLI في معظم الحالات.
|
||||
|
||||
### أنواع المصادر
|
||||
|
||||
لكل تسجيل **نوع مصدر** يحدّد كيفية تحديد ملفات التطبيق أثناء التثبيت:
|
||||
|
||||
| نوع المصدر | كيفية تحديد الملفات | حالة الاستخدام النموذجية |
|
||||
| ---------- | ------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `LOCAL` | تتم مزامنة الملفات في الوقت الفعلي بواسطة مُراقِب CLI — يتم تخطّي التثبيت | التطوير باستخدام `app:dev` |
|
||||
| `NPM` | تُجلب من سجل npm عبر الحقل `sourcePackage` | تطبيقات منشورة على npm |
|
||||
| `TARBALL` | تُستخرَج من ملف `.tgz` مرفوع ومخزَّن على الخادم | تطبيقات خاصة منشورة باستخدام `--server` |
|
||||
|
||||
### كيفية إجراء التسجيل
|
||||
|
||||
* **`app:dev`** — ينشئ تلقائيًا تسجيلًا من نوع `LOCAL` في المرة الأولى التي تشغّل فيها وضع التطوير لمساحة عمل.
|
||||
* **`app:publish --server`** — يرفع أرشيف tar وينشئ (أو يحدّث) تسجيلًا من نوع `TARBALL`، ثم يثبّت التطبيق.
|
||||
* **سوق npm** — يتم إنشاء تسجيلات `NPM` عند مزامنة التطبيقات من سجل npm إلى كتالوج سوق Twenty.
|
||||
* **واجهة برمجة تطبيقات GraphQL** — يمكنك أيضًا إنشاء التسجيلات برمجيًا عبر العملية `createApplicationRegistration`.
|
||||
|
||||
### التسجيل مقابل التثبيت
|
||||
|
||||
**التسجيل** و**التثبيت** مفهومان منفصلان:
|
||||
|
||||
* **التسجيل** (`ApplicationRegistration`) هو سجل بيانات وصفية عام يصف التطبيق: اسمه، نوع المصدر، بيانات اعتماد OAuth، وحالة إدراجه في السوق. وهو موجود بشكل مستقل عن أي مساحة عمل.
|
||||
* **التثبيت** (`Application`) هو مثيل لكل مساحة عمل. عند قيام مستخدم بتثبيت تطبيق، تقوم Twenty بحلّ الحزمة من مصدر التسجيل، وتكتب الملفات المُبنَاة إلى التخزين، وتزامن البيان التعريفي (إنشاء الكائنات والحقول ودوال المنطق، إلخ) في مساحة العمل تلك.
|
||||
|
||||
يمكن تثبيت تسجيل واحد في العديد من مساحات العمل. تحصل كل مساحة عمل على نسختها الخاصة من ملفات التطبيق ونموذج البيانات.
|
||||
|
||||
### بيانات اعتماد OAuth
|
||||
|
||||
يتضمن كل تسجيل بيانات اعتماد OAuth (`oAuthClientId` و`oAuthClientSecret`) يتم إنشاؤها وقت الإنشاء. يستخدمها التطبيق لمصادقة طلبات واجهة برمجة التطبيقات بالنيابة عن المستخدمين. يُعرَض سر العميل مرةً **واحدة** عند الإنشاء — خزّنه بأمان. يمكنك تدويره لاحقًا عبر العملية `rotateApplicationRegistrationClientSecret`.
|
||||
|
||||
## إعداد يدوي (بدون المهيئ)
|
||||
|
||||
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي واربط سكربتًا واحدًا في ملف package.json لديك:
|
||||
|
||||
@@ -3,7 +3,7 @@ title: بنقرة واحدة مع Docker Compose
|
||||
---
|
||||
|
||||
<Warning>
|
||||
الحاويات الخاصة بدوكر مخصصة للاستضافة الإنتاجية أو الاستضافة الذاتية، للتحقيق يرجى التحقق من [الإعداد المحلي](/l/ar/developers/contribute/capabilities/local-setup).
|
||||
حاويات Docker مخصصة للاستضافة في بيئة الإنتاج أو للاستضافة الذاتية. للمساهمة، يُرجى الاطلاع على [الإعداد المحلي](/l/ar/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## نظرة عامة
|
||||
@@ -12,7 +12,7 @@ title: بنقرة واحدة مع Docker Compose
|
||||
|
||||
**مهم:** عدّل الإعدادات المذكورة صراحة في هذا الدليل فقط. قد يؤدي تعديل التكوينات الأخرى إلى مشاكل.
|
||||
|
||||
راجع المستندات الخاصة بـ [إعداد متغيرات البيئة](/l/ar/developers/self-host/capabilities/setup) لإعداد متقدم. يجب إعلان جميع متغيرات البيئة في الملف docker-compose.yml على مستوى الخادم و/أو العامل بناءً على المتغير.
|
||||
راجع [إعداد متغيرات البيئة](/l/ar/developers/self-host/capabilities/setup) لإعداد متقدم. يجب إعلان جميع متغيرات البيئة في ملف `docker-compose.yml` على مستوى الخادم و/أو العامل، اعتمادًا على المتغير.
|
||||
|
||||
## متطلبات النظام
|
||||
|
||||
|
||||
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
</Warning>
|
||||
|
||||
## الوظائف المنطقية
|
||||
## الوظائف المنطقية ومفسر الشيفرة
|
||||
|
||||
تدعم Twenty الوظائف المنطقية لعمليات سير العمل والمنطق المخصص. يتم تكوين بيئة التنفيذ عبر متغير البيئة `SERVERLESS_TYPE`.
|
||||
تدعم Twenty الوظائف المنطقية لعمليات سير العمل ومفسر الشيفرة لتحليل بيانات الذكاء الاصطناعي. كلاهما يقوم بتشغيل الشيفرة المقدمة من المستخدم ويتطلب تهيئة صريحة لأغراض الأمان.
|
||||
|
||||
### الإعدادات الافتراضية للأمان
|
||||
|
||||
**في بيئة الإنتاج (NODE_ENV=production):** يكون الإعداد الافتراضي لكل من الوظائف المنطقية ومفسر الشيفرة هو **معطل**. يجب تمكينهما صراحة باستخدام `LOGIC_FUNCTION_TYPE` و`CODE_INTERPRETER_TYPE` إذا كنت تحتاج إلى هذه الميزات.
|
||||
|
||||
**في بيئة التطوير (NODE_ENV=development):** يكون الإعداد الافتراضي لكليهما **LOCAL** لتسهيل التشغيل محلياً.
|
||||
|
||||
<Warning>
|
||||
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
|
||||
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`LOGIC_FUNCTION_TYPE=LOCAL` أو `CODE_INTERPRETER_TYPE=LOCAL`) بتشغيل الشيفرة مباشرة على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوقة، استخدم `LOGIC_FUNCTION_TYPE=LAMBDA` أو `CODE_INTERPRETER_TYPE=E2B` (مع وضع الحماية)، أو اتركهما مُعطَّلَيْن.
|
||||
</Warning>
|
||||
|
||||
### برامج التشغيل المتاحة
|
||||
### الوظائف المنطقية - برامج التشغيل المتاحة
|
||||
|
||||
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
|
||||
| -------------- | -------------------------- | ------------------------------- | ----------------------------- |
|
||||
| معطل | `SERVERLESS_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
|
||||
| محلي | `SERVERLESS_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
|
||||
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
|
||||
| -------------- | ------------------------------ | ------------------------------- | ----------------------------- |
|
||||
| معطل | `LOGIC_FUNCTION_TYPE=DISABLED` | تعطيل الوظائف المنطقية بالكامل | غير متاح |
|
||||
| محلي | `LOGIC_FUNCTION_TYPE=LOCAL` | بيئات التطوير والبيئات الموثوقة | منخفض (من دون عزل) |
|
||||
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | الإنتاج مع شيفرة غير موثوق بها | مرتفع (عزل على مستوى الأجهزة) |
|
||||
|
||||
### التكوين الموصى به
|
||||
### الوظائف المنطقية - الإعداد الموصى به
|
||||
|
||||
**للتطوير:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=LOCAL # default
|
||||
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
|
||||
```
|
||||
|
||||
**للإنتاج (AWS):**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=LAMBDA
|
||||
SERVERLESS_LAMBDA_REGION=us-east-1
|
||||
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
LOGIC_FUNCTION_TYPE=LAMBDA
|
||||
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
|
||||
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**لتعطيل الوظائف المنطقية:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
|
||||
```
|
||||
|
||||
### مفسر الشيفرة - برامج التشغيل المتاحة
|
||||
|
||||
| برنامج التشغيل | متغير البيئة | حالة الاستخدام | مستوى الأمان |
|
||||
| -------------- | -------------------------------- | ------------------------------------- | ------------------------ |
|
||||
| معطل | `CODE_INTERPRETER_TYPE=DISABLED` | تعطيل تنفيذ الشيفرة بالذكاء الاصطناعي | غير متاح |
|
||||
| محلي | `CODE_INTERPRETER_TYPE=LOCAL` | للتطوير فقط | منخفض (من دون عزل) |
|
||||
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | الإنتاج مع تنفيذ ضمن صندوق رمل معزول | مرتفعة (صندوق رمل معزول) |
|
||||
|
||||
<Note>
|
||||
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة منطقية إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون إمكانات الوظائف المنطقية.
|
||||
عند استخدام `LOGIC_FUNCTION_TYPE=DISABLED` أو `CODE_INTERPRETER_TYPE=DISABLED`، سترجع أي محاولة للتنفيذ خطأً. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون هذه الإمكانات.
|
||||
</Note>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: خادم MCP
|
||||
description: اربط مساعدي الذكاء الاصطناعي بمساحة عمل Twenty الخاصة بك باستخدام بروتوكول سياق النموذج.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
MCP حاليًا في مرحلة **ألفا** وهو متاح فقط في بعض مساحات العمل. قد لا يكون مفعّلًا لمساحة عملك بعد.
|
||||
</Warning>
|
||||
|
||||
تعرض Twenty خادم [MCP](https://modelcontextprotocol.io/) بحيث تتمكّن مساعدات الذكاء الاصطناعي — Claude Desktop وClaude Code وCursor وChatGPT وغيرها — من قراءة وكتابة بيانات نظام إدارة علاقات العملاء (CRM) لديك باستخدام اللغة الطبيعية.
|
||||
|
||||
استخدم **عنوان URL لمساحة العمل** (عنوان URL الذي تستخدمه للوصول إلى Twenty) كنقطة نهاية MCP. على Twenty Cloud، قد يكون عنوان URL لمساحة العمل هو `https://{mycompany}.twenty.com` أو نطاق مخصص. الخادم متاح على:
|
||||
|
||||
| البيئة | نقطة نهاية MCP |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **السحابة** | `https://{your-workspace-url}/mcp` (على سبيل المثال: `https://mycompany.twenty.com/mcp`) |
|
||||
| **الاستضافة الذاتية** | `https://{your-domain}/mcp` |
|
||||
|
||||
## طرق المصادقة
|
||||
|
||||
لديك طريقتان لمصادقة عميل MCP: **OAuth** (مُوصى بها) أو **مفتاح API**.
|
||||
|
||||
### الخيار أ — OAuth (مُوصى به)
|
||||
|
||||
باستخدام OAuth، يفتح عميل MCP لديك نافذة متصفح لتسجيل الدخول. لا يتم تخزين أي أسرار في ملفات الإعداد، ويتم تحديث الرموز المميِّزة تلقائيًا.
|
||||
|
||||
<Note>
|
||||
يتطلب OAuth عميل MCP يدعم [مواصفة تفويض MCP](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization). تدعمه Claude Desktop وClaude Code وCursor وChatGPT.
|
||||
</Note>
|
||||
|
||||
أضِف ما يلي إلى تهيئة عميل MCP لديك، واستبدِل `{your-workspace-url}` بمضيف مساحة العمل لديك (على سبيل المثال: `mycompany.twenty.com`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"twenty": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://{your-workspace-url}/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
هذا كل شيء — لا حاجة إلى مفتاح API. عند اتصال العميل للمرة الأولى، سيفعل ما يلي:
|
||||
|
||||
1. اكتشاف بيانات التعريف الخاصة بـ OAuth لدى Twenty عبر `/.well-known/oauth-protected-resource` و`/.well-known/oauth-authorization-server`
|
||||
2. تسجيل نفسه كعميل OAuth عبر التسجيل الديناميكي للعميل (RFC 7591)
|
||||
3. فتح متصفحك لتفويض الوصول
|
||||
4. استلام الرموز المميِّزة والاتصال بخادم MCP
|
||||
|
||||
تعيد الاتصالات اللاحقة استخدام الرموز المميِّزة المخزنة وتحدِّثها تلقائيًا.
|
||||
|
||||
### الخيار ب — مفتاح API
|
||||
|
||||
إذا كان عميل MCP لديك لا يدعم OAuth، أو كنت تفضّل بيانات اعتماد ثابتة، فمرِّر مفتاح API في ترويسة `Authorization`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"twenty": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://{your-workspace-url}/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer YOUR_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Warning>
|
||||
يمنح مفتاح API الخاص بك حق الوصول إلى بيانات مساحة العمل. أبعِده عن أنظمة التحكم في الإصدارات وملفات dotfiles المشتركة.
|
||||
</Warning>
|
||||
|
||||
لإنشاء مفتاح API، انتقل إلى **Settings > APIs & Webhooks > + Create key**. راجع [واجهات برمجة التطبيقات](/l/ar/developers/extend/api#create-an-api-key) للتفاصيل.
|
||||
|
||||
## البدء السريع
|
||||
|
||||
### 1. انسخ الإعداد
|
||||
|
||||
انتقل إلى **Settings > AI > More > MCP Server** في Twenty. اختر طريقة المصادقة (OAuth أو مفتاح API)، وانسخ مقطع JSON (سيستخدم بالفعل عنوان URL لمساحة العمل لديك)، ثم الصقه في ملف إعدادات عميل MCP لديك.
|
||||
|
||||
| العميل | موقع ملف الإعداد |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) أو `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
|
||||
| **Claude Code** | `~/.claude.json` (المستخدم) أو `.mcp.json` (المشروع) |
|
||||
| **Cursor** | `.cursor/mcp.json` ضمن مشروعك، أو `~/.cursor/mcp.json` عالميًا |
|
||||
| **ChatGPT** | فعِّل وضع المطوّر في **Settings > Apps & Connectors > Advanced settings**، ثم استخدم **Create** في **Settings > Apps & Connectors** لإضافة خادم MCP |
|
||||
|
||||
### 2. الاتصال
|
||||
|
||||
أعِد تشغيل عميل MCP لديك (أو أعد تحميل ملف الإعداد). إذا كنت تستخدم OAuth فسيتم توجيهك إلى Twenty لتفويض الوصول. إذا كنت تستخدم مفتاح API فسيكون الاتصال فوريًا.
|
||||
|
||||
### 3. ابدأ باستخدامه
|
||||
|
||||
اطلب من مساعد الذكاء الاصطناعي التفاعل مع نظام إدارة علاقات العملاء (CRM) لديك:
|
||||
|
||||
* *"أرني أحدث 5 شركات تم إنشاؤها"*
|
||||
* *"أنشئ شخصًا جديدًا باسم Jane Doe في Acme Corp"*
|
||||
* *"اعثر على جميع الفرص المفتوحة التي تزيد قيمتها عن 10 آلاف دولار"*
|
||||
|
||||
## الأدوات المتاحة
|
||||
|
||||
بعد الاتصال، يوفّر خادم MCP أدوات تعكس واجهة برمجة تطبيقات Twenty (API). سير العمل الموصى به هو:
|
||||
|
||||
1. **`get_tool_catalog`** — اكتشف جميع الأدوات المتاحة
|
||||
2. **`learn_tools`** — احصل على مخطط الإدخال لأدوات محددة
|
||||
3. **`execute_tool`** — شغّل أداة
|
||||
|
||||
لا تحتاج إلى تذكّر أسماء الأدوات. اسأل مساعد الذكاء الاصطناعي عمّا يمكنه فعله وسيستدعي `get_tool_catalog` تلقائيًا.
|
||||
|
||||
## الصلاحيات
|
||||
|
||||
ترث اتصالات MCP أذونات المستخدم المُصادَق عليه (OAuth) أو الدور المُعيَّن لمفتاح API. لتقييد ما يمكن لخادم MCP القيام به:
|
||||
|
||||
* **OAuth**: ينطبق دور المستخدم في مساحة العمل.
|
||||
* **API Key**: عيِّن دورًا لمفتاح API ضمن **Settings > Roles**. راجع [الأذونات](/l/ar/user-guide/permissions-access/capabilities/permissions).
|
||||
|
||||
## التكوين ذاتي الاستضافة
|
||||
|
||||
في حالات الاستضافة الذاتية، استبدِل `{your-workspace-url}` بعنوان URL الخاص بالخادم لديك. تأكّد من أن قيمة `SERVER_URL` في بيئتك تطابق عنوان URL العام لمثيل Twenty لديك — إذ يُستخدَم ذلك لإنشاء بيانات تعريف اكتشاف OAuth.
|
||||
|
||||
```bash
|
||||
SERVER_URL=https://twenty.yourcompany.com
|
||||
```
|
||||
|
||||
تُشتق نقطة نهاية MCP ونقاط نهاية OAuth وبيانات تعريف الاكتشاف جميعها من هذه القيمة.
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
**أخطاء "Unauthorized" أو 401**
|
||||
|
||||
* OAuth: أعد التفويض عبر مسح الرموز المميِّزة المخزنة في عميل MCP لديك ثم أعد الاتصال.
|
||||
* API Key: تحقّق من أن المفتاح صالح ولم تنتهِ صلاحيته. أعِد توليده إذا لزم الأمر.
|
||||
|
||||
**عملية OAuth لا تفتح متصفحًا**
|
||||
|
||||
* تأكّد من أن عميل MCP لديك يدعم تفويض MCP. ارجع إلى طريقة مفتاح API إذا لم يكن كذلك.
|
||||
|
||||
**انتهاء مهلة الاتصال**
|
||||
|
||||
* تحقّق من إمكانية الوصول إلى عنوان URL لنقطة نهاية MCP من جهازك. بالنسبة لحالات الاستضافة الذاتية، تحقّق من أن الخادم يعمل وأن `SERVER_URL` مُعيَّن بشكل صحيح.
|
||||
@@ -6,7 +6,7 @@ description: Der Leitfaden für Mitwirkende (oder neugierige Entwickler), die Tw
|
||||
## Voraussetzungen
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux und MacOS">
|
||||
<Tab title="Linux und macOS">
|
||||
|
||||
Bevor Sie Twenty installieren und verwenden können, stellen Sie sicher, dass Sie Folgendes auf Ihrem Computer installiert haben:
|
||||
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
@@ -103,7 +103,7 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
**Option 1 (bevorzugt):** Um Ihre Datenbank lokal bereitzustellen:
|
||||
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrem Linux-Rechner zu installieren: [Postgresql-Installation](https://www.postgresql.org/download/linux/)
|
||||
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrem Linux-Rechner zu installieren: [PostgreSQL-Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -129,8 +129,8 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
|
||||
brew services list
|
||||
```
|
||||
|
||||
Der Installer erstellt möglicherweise nicht standardmäßig den Benutzer `postgres`, wenn er
|
||||
über Homebrew auf MacOS installiert wird. Stattdessen wird eine PostgreSQL-Rolle erstellt, die Ihrem macOS
|
||||
Das Installationsprogramm erstellt den Benutzer `postgres` möglicherweise nicht standardmäßig bei der Installation
|
||||
über Homebrew auf macOS. Stattdessen wird eine PostgreSQL-Rolle erstellt, die Ihrem macOS
|
||||
Benutzernamen (z. B. "john") entspricht.
|
||||
Um zu überprüfen und, falls erforderlich, den Benutzer `postgres` zu erstellen, führen Sie folgende Schritte aus:
|
||||
```bash
|
||||
@@ -174,7 +174,7 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
|
||||
Alle folgenden Schritte sind im WSL-Terminal auszuführen (innerhalb Ihrer virtuellen Maschine)
|
||||
|
||||
**Option 1:** Um Ihr PostgreSQL lokal bereitzustellen:
|
||||
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrer Linux-VM zu installieren: [Postgresql-Installation](https://www.postgresql.org/download/linux/)
|
||||
Verwenden Sie den folgenden Link, um PostgreSQL auf Ihrer Linux-VM zu installieren: [PostgreSQL-Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
@@ -189,11 +189,13 @@ Alle folgenden Befehle innerhalb des Projekts sind vom Stammverzeichnis aus ausz
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Sie können jetzt über [localhost:5432](localhost:5432) auf die Datenbank zugreifen, mit dem Benutzer `postgres` und dem Passwort `postgres`.
|
||||
Sie können nun über `localhost:5432` auf die Datenbank zugreifen.
|
||||
|
||||
Wenn Sie die oben genannte Docker-Option verwendet haben, lauten die Standardanmeldedaten Benutzer `postgres` und Passwort `postgres`. Für native PostgreSQL-Installationen verwenden Sie die auf Ihrem Rechner konfigurierten Anmeldedaten und Rollen.
|
||||
|
||||
## Schritt 4: Einrichten einer Redis-Datenbank (Cache)
|
||||
|
||||
Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten
|
||||
Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux">
|
||||
@@ -211,7 +213,9 @@ Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten
|
||||
brew install redis
|
||||
```
|
||||
Starten Sie Ihren Redis-Server:
|
||||
`brew services start redis`
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
|
||||
**Option 2:** Wenn Sie Docker installiert haben:
|
||||
```bash
|
||||
@@ -229,11 +233,11 @@ Twenty benötigt einen Redis-Cache, um die beste Leistung zu bieten
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Wenn Sie eine Client-GUI benötigen, empfehlen wir [redis insight](https://redis.io/insight/) (kostenlose Version verfügbar)
|
||||
Wenn Sie eine Client-GUI benötigen, empfehlen wir [Redis Insight](https://redis.io/insight/) (kostenlose Version verfügbar).
|
||||
|
||||
## Schritt 5: Einrichten von Umgebungsvariablen
|
||||
|
||||
Verwenden Sie Umgebungsvariablen oder `.env`-Dateien, um Ihr Projekt zu konfigurieren. Weitere Informationen [hier](/l/de/developers/self-host/capabilities/setup)
|
||||
Verwenden Sie Umgebungsvariablen oder `.env`-Dateien, um Ihr Projekt zu konfigurieren. Weitere Informationen [hier](/l/de/developers/self-host/capabilities/setup).
|
||||
|
||||
Kopieren Sie die `.env.example`-Dateien in `/front` und `/server`:
|
||||
|
||||
|
||||
@@ -64,11 +64,17 @@ yarn twenty function:execute --preInstall
|
||||
# Die Post-Installationsfunktion ausführen
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Die Anwendung für die Verteilung erstellen
|
||||
yarn twenty app:build
|
||||
|
||||
# Die Anwendung auf npm oder einen Twenty-Server veröffentlichen
|
||||
yarn twenty app:publish
|
||||
|
||||
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Hilfe zu Befehlen anzeigen
|
||||
yarn twenty help},{
|
||||
yarn twenty help
|
||||
```
|
||||
|
||||
Siehe auch: die CLI-Referenzseiten für [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) und [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -1240,6 +1246,113 @@ Hauptpunkte:
|
||||
|
||||
Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Komponenten und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## Erstellen Ihrer App
|
||||
|
||||
Sobald Sie Ihre App mit `app:dev` entwickelt haben, verwenden Sie `app:build`, um sie in ein verteilbares Paket zu kompilieren.
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Die App erstellen (Ausgabe nach .twenty/output/)
|
||||
yarn twenty app:build
|
||||
|
||||
# Build ausführen und ein Tarball (.tgz) für die Verteilung erstellen
|
||||
yarn twenty app:build --tarball
|
||||
```
|
||||
|
||||
Der Build-Prozess:
|
||||
|
||||
1. **Parst und validiert das Manifest** — liest alle `defineX()`-Entitäten aus Ihren Quelldateien und validiert die Manifeststruktur.
|
||||
2. **Kompiliert Logikfunktionen und Front-Komponenten** — bündelt TypeScript-Quellcode in ESM `.mjs`-Dateien mit esbuild.
|
||||
3. **Erzeugt Checksummen** — berechnet MD5-Hashes für jede erstellte Datei, die im Manifest als `builtHandlerChecksum` / `builtComponentChecksum` gespeichert werden.
|
||||
4. **Generiert den typisierten API-Client** — führt eine Introspektion des GraphQL-Schemas durch und generiert die typisierten Clients `CoreApiClient` und `MetadataApiClient`.
|
||||
5. **Führt eine TypeScript-Typprüfung aus** — führt `tsc --noEmit` aus, um Typfehler vor der Veröffentlichung zu erkennen.
|
||||
6. **Baut mit dem generierten Client neu** — führt einen zweiten Kompiliervorgang durch, damit die generierten Client-Typen enthalten sind.
|
||||
7. **Erstellt optional einen Tarball** — wenn `--tarball` übergeben wird, wird `npm pack` ausgeführt, um eine `.tgz`-Datei zu erstellen, die für die Verteilung bereit ist.
|
||||
|
||||
Der Build-Output in `.twenty/output/` enthält:
|
||||
|
||||
```text
|
||||
.twenty/output/
|
||||
├── manifest.json # Manifest with checksums for all built files
|
||||
├── package.json # Copied from app root
|
||||
├── yarn.lock # Copied from app root
|
||||
├── src/
|
||||
│ ├── logic-functions/ # Compiled .mjs logic function files
|
||||
│ └── front-components/ # Compiled .mjs front component files
|
||||
├── public/ # Static assets (if any)
|
||||
└── my-app-1.0.0.tgz # Only with --tarball flag
|
||||
```
|
||||
|
||||
| Option | Beschreibung |
|
||||
| ----------- | -------------------------------------------------------------- |
|
||||
| `[appPath]` | Pfad zum App-Verzeichnis (standardmäßig aktuelles Verzeichnis) |
|
||||
| `--tarball` | Den Output zusätzlich in einen `.tgz`-Tarball packen |
|
||||
|
||||
## Veröffentlichen Ihrer App
|
||||
|
||||
Verwenden Sie `app:publish`, um Ihre App zu verteilen — entweder zur npm-Registry oder direkt zu einem Twenty-Server.
|
||||
|
||||
### Bei npm veröffentlichen (Standard)
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish to npm (requires npm login)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Publish with a dist-tag (e.g. beta, next)
|
||||
yarn twenty app:publish --tag beta
|
||||
```
|
||||
|
||||
Dies baut die App und führt `npm publish` aus dem Verzeichnis `.twenty/output/` aus. Das veröffentlichte Paket kann dann von jedem Arbeitsbereich über den Twenty-Marktplatz installiert werden.
|
||||
|
||||
### Auf einem Twenty-Server veröffentlichen
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Publish directly to a Twenty server
|
||||
yarn twenty app:publish --server https://app.twenty.com
|
||||
```
|
||||
|
||||
Dies erstellt beim Build einen Tarball, lädt ihn über die GraphQL-Mutation `uploadAppTarball` auf den Server hoch und stößt die Installation in einem Schritt an. Dies ist nützlich für private Bereitstellungen oder Tests gegen einen bestimmten Server.
|
||||
|
||||
| Option | Beschreibung |
|
||||
| ----------------- | ------------------------------------------------------------------ |
|
||||
| `[appPath]` | Pfad zum App-Verzeichnis (standardmäßig aktuelles Verzeichnis) |
|
||||
| `--server <url>` | Auf einen Twenty-Server anstelle von npm veröffentlichen |
|
||||
| `--token <token>` | Authentifizierungstoken für den Zielserver |
|
||||
| `--tag <tag>` | npm dist-tag (z. B. `beta`, `next`) — nur für npm-Veröffentlichung |
|
||||
|
||||
## Anwendungsregistrierung
|
||||
|
||||
Bevor eine App in einem Arbeitsbereich installiert werden kann, muss sie **registriert** werden. Eine Registrierung ist ein Metadatensatz, der beschreibt, woher die App stammt und wie sie authentifiziert wird. Dies wird in den meisten Fällen automatisch durch die CLI erledigt.
|
||||
|
||||
### Quelltypen
|
||||
|
||||
Jede Registrierung hat einen **Quelltyp**, der bestimmt, wie die Dateien der App während der Installation aufgelöst werden:
|
||||
|
||||
| Quelltyp | Wie Dateien aufgelöst werden | Typischer Anwendungsfall |
|
||||
| --------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| `LOCAL` | Dateien werden in Echtzeit vom CLI-Watcher synchronisiert — die Installation wird übersprungen | Entwicklung mit `app:dev` |
|
||||
| `NPM` | Über das Feld `sourcePackage` aus der npm-Registry abgerufen | Veröffentlichte Apps auf npm |
|
||||
| `TARBALL` | Aus einer hochgeladenen, auf dem Server gespeicherten `.tgz`-Datei extrahiert | Private Apps, die mit `--server` veröffentlicht wurden |
|
||||
|
||||
### Wie die Registrierung erfolgt
|
||||
|
||||
* **`app:dev`** — erstellt beim ersten Ausführen des Dev-Modus für einen Arbeitsbereich automatisch eine `LOCAL`-Registrierung.
|
||||
* **`app:publish --server`** — lädt einen Tarball hoch und erstellt (oder aktualisiert) eine `TARBALL`-Registrierung und installiert anschließend die App.
|
||||
* **npm-Marktplatz** — `NPM`-Registrierungen werden erstellt, wenn Apps aus der npm-Registry in den Twenty-Marktplatzkatalog synchronisiert werden.
|
||||
* **GraphQL-API** — Sie können Registrierungen auch programmgesteuert über die Mutation `createApplicationRegistration` erstellen.
|
||||
|
||||
### Registrierung vs. Installation
|
||||
|
||||
**Registrierung** und **Installation** sind unterschiedliche Konzepte:
|
||||
|
||||
* Eine **Registrierung** (`ApplicationRegistration`) ist ein globaler Metadatensatz, der die App beschreibt: ihren Namen, den Quelltyp, die OAuth-Anmeldedaten und den Status der Marktplatzlistung. Sie existiert unabhängig von jedem Arbeitsbereich.
|
||||
* Eine **Installation** (`Application`) ist eine Instanz pro Arbeitsbereich. Wenn ein Benutzer eine App installiert, ermittelt Twenty das Paket aus der Quelle der Registrierung, schreibt die erstellten Dateien in den Speicher und synchronisiert das Manifest (wobei Objekte, Felder, Logikfunktionen usw. erstellt werden) in diesem Arbeitsbereich.
|
||||
|
||||
Eine Registrierung kann in vielen Arbeitsbereichen installiert werden. Jeder Arbeitsbereich erhält seine eigene Kopie der Dateien und des Datenmodells der App.
|
||||
|
||||
### OAuth-Anmeldedaten
|
||||
|
||||
Jede Registrierung enthält OAuth-Anmeldedaten (`oAuthClientId` und `oAuthClientSecret`), die bei der Erstellung generiert werden. Diese werden von der App verwendet, um API-Anfragen im Namen der Benutzer zu authentifizieren. Das Client-Secret wird bei der Erstellung **einmalig** zurückgegeben — bewahren Sie es sicher auf. Sie können es später über die Mutation `rotateApplicationRegistrationClientSecret` rotieren.
|
||||
|
||||
## Manuelle Einrichtung (ohne Scaffolder)
|
||||
|
||||
Wir empfehlen zwar `create-twenty-app` für das beste Einstiegserlebnis, Sie können ein Projekt aber auch manuell einrichten. Installieren Sie die CLI nicht global. Fügen Sie stattdessen `twenty-sdk` als lokale Abhängigkeit hinzu und binden Sie ein einzelnes Skript in Ihrer package.json ein:
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 1-Klick mit Docker Compose
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Docker-Container sind für die Produktion oder das Selbsthosten bestimmt. Für Beiträge siehe bitte das [Lokale Setup](/l/de/developers/contribute/capabilities/local-setup).
|
||||
Docker-Container sind für produktives Hosting oder Selbsthosting vorgesehen. Zum Mitwirken siehe [Lokale Einrichtung](/l/de/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## Überblick
|
||||
@@ -12,7 +12,7 @@ Diese Anleitung enthält Schritt-für-Schritt-Anweisungen, um die Twenty-Anwendu
|
||||
|
||||
**Wichtig:** Ändern Sie nur die in dieser Anleitung explizit erwähnten Einstellungen. Andere Konfigurationen zu ändern, kann zu Problemen führen.
|
||||
|
||||
Siehe die Dokumentation [Umgebungsvariablen einrichten](/l/de/developers/self-host/capabilities/setup) zur erweiterten Konfiguration. Alle Umgebungsvariablen müssen in der Datei docker-compose.yml auf Server- und/oder Worker-Ebene deklariert werden, je nach Variable.
|
||||
Siehe die Dokumentation [Umgebungsvariablen einrichten](/l/de/developers/self-host/capabilities/setup) zur erweiterten Konfiguration. Alle Umgebungsvariablen müssen in der Datei `docker-compose.yml` auf Server- und/oder Worker-Ebene deklariert werden, je nach Variable.
|
||||
|
||||
## Systemanforderungen
|
||||
|
||||
|
||||
@@ -297,46 +297,60 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
**Nur-Umgebungsmodus:** Wenn Sie `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` setzen, fügen Sie diese Variablen stattdessen Ihrer `.env`-Datei hinzu.
|
||||
</Warning>
|
||||
|
||||
## Logikfunktionen
|
||||
## Logikfunktionen & Code-Interpreter
|
||||
|
||||
Twenty unterstützt Logikfunktionen für Workflows und benutzerdefinierte Logik. Die Ausführungsumgebung wird über die Umgebungsvariable `SERVERLESS_TYPE` konfiguriert.
|
||||
Twenty unterstützt Logikfunktionen für Workflows und den Code-Interpreter für KI-Datenanalyse. Beide führen vom Benutzer bereitgestellten Code aus und erfordern aus Sicherheitsgründen eine explizite Konfiguration.
|
||||
|
||||
### Sicherheits-Standardeinstellungen
|
||||
|
||||
**In Produktion (NODE_ENV=production):** Sowohl Logikfunktionen als auch der Code-Interpreter sind standardmäßig **deaktiviert**. Sie müssen sie, wenn Sie diese Funktionen benötigen, explizit mit `LOGIC_FUNCTION_TYPE` und `CODE_INTERPRETER_TYPE` aktivieren.
|
||||
|
||||
**In der Entwicklung (NODE_ENV=development):** Beide sind der Einfachheit halber beim lokalen Betrieb standardmäßig **LOCAL**.
|
||||
|
||||
<Warning>
|
||||
**Sicherheitshinweis:** Der lokale Treiber (`SERVERLESS_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktivbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, empfehlen wir nachdrücklich, `SERVERLESS_TYPE=LAMBDA` oder `SERVERLESS_TYPE=DISABLED` zu verwenden.
|
||||
**Sicherheitshinweis:** Der lokale Treiber (`LOGIC_FUNCTION_TYPE=LOCAL` oder `CODE_INTERPRETER_TYPE=LOCAL`) führt Code ohne Sandbox direkt auf dem Host in einem Node.js-Prozess aus. Er sollte nur für vertrauenswürdigen Code in der Entwicklung verwendet werden. Für Produktionsbereitstellungen, die nicht vertrauenswürdigen Code verarbeiten, verwenden Sie `LOGIC_FUNCTION_TYPE=LAMBDA` oder `CODE_INTERPRETER_TYPE=E2B` (mit Sandbox-Isolierung), oder lassen Sie sie deaktiviert.
|
||||
</Warning>
|
||||
|
||||
### Verfügbare Treiber
|
||||
### Logikfunktionen - Verfügbare Treiber
|
||||
|
||||
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
|
||||
| ----------- | -------------------------- | -------------------------------------------------- | ---------------------------------- |
|
||||
| Deaktiviert | `SERVERLESS_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
|
||||
| Lokal | `SERVERLESS_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
|
||||
| Lambda | `SERVERLESS_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
|
||||
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
|
||||
| ----------- | ------------------------------ | -------------------------------------------------- | ---------------------------------- |
|
||||
| Deaktiviert | `LOGIC_FUNCTION_TYPE=DISABLED` | Logikfunktionen vollständig deaktivieren | N/A |
|
||||
| Lokal | `LOGIC_FUNCTION_TYPE=LOCAL` | Entwicklung und vertrauenswürdige Umgebungen | Niedrig (keine Sandbox) |
|
||||
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Produktivbetrieb mit nicht vertrauenswürdigem Code | Hoch (Isolation auf Hardwareebene) |
|
||||
|
||||
### Empfohlene Konfiguration
|
||||
### Logikfunktionen - Empfohlene Konfiguration
|
||||
|
||||
**Für die Entwicklung:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=LOCAL # default
|
||||
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
|
||||
```
|
||||
|
||||
**Für den Produktivbetrieb (AWS):**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=LAMBDA
|
||||
SERVERLESS_LAMBDA_REGION=us-east-1
|
||||
SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
LOGIC_FUNCTION_TYPE=LAMBDA
|
||||
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
|
||||
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
|
||||
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
|
||||
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**Zum Deaktivieren von Logikfunktionen:**
|
||||
|
||||
```bash
|
||||
SERVERLESS_TYPE=DISABLED
|
||||
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
|
||||
```
|
||||
|
||||
### Code-Interpreter - Verfügbare Treiber
|
||||
|
||||
| Treiber | Umgebungsvariable | Anwendungsfall | Sicherheitsstufe |
|
||||
| ----------- | -------------------------------- | ------------------------------------------ | ------------------------ |
|
||||
| Deaktiviert | `CODE_INTERPRETER_TYPE=DISABLED` | KI-Codeausführung deaktivieren | N/A |
|
||||
| Lokal | `CODE_INTERPRETER_TYPE=LOCAL` | Nur für die Entwicklung | Niedrig (keine Sandbox) |
|
||||
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Produktion mit Ausführung in einer Sandbox | Hoch (isolierte Sandbox) |
|
||||
|
||||
<Note>
|
||||
Bei Verwendung von `SERVERLESS_TYPE=DISABLED` führt jeder Versuch, eine Logikfunktion auszuführen, zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne Unterstützung für Logikfunktionen betreiben möchten.
|
||||
Bei Verwendung von `LOGIC_FUNCTION_TYPE=DISABLED` oder `CODE_INTERPRETER_TYPE=DISABLED` führt jeder Ausführungsversuch zu einem Fehler. Dies ist nützlich, wenn Sie Twenty ohne diese Funktionen betreiben möchten.
|
||||
</Note>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: MCP-Server
|
||||
description: Verbinden Sie KI-Assistenten mit Ihrem Twenty-Workspace über das Model Context Protocol.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
MCP befindet sich derzeit in **alpha** und ist nur in einigen Workspaces verfügbar. Möglicherweise ist es für Ihren Workspace noch nicht aktiviert.
|
||||
</Warning>
|
||||
|
||||
Twenty stellt einen [MCP](https://modelcontextprotocol.io/)-Server bereit, damit KI-Assistenten — Claude Desktop, Claude Code, Cursor, ChatGPT und andere — Ihre CRM-Daten in natürlicher Sprache lesen und schreiben können.
|
||||
|
||||
Verwenden Sie Ihre **Workspace-URL** (die URL, mit der Sie auf Twenty zugreifen) als MCP-Endpunkt. In Twenty Cloud kann Ihre Workspace-URL `https://{mycompany}.twenty.com` oder eine benutzerdefinierte Domain sein. Der Server ist verfügbar unter:
|
||||
|
||||
| Umgebung | MCP-Endpunkt |
|
||||
| ----------------- | ----------------------------------------------------------------------------- |
|
||||
| **Cloud** | `https://{your-workspace-url}/mcp` (z. B. `https://mycompany.twenty.com/mcp`) |
|
||||
| **Selbsthosting** | `https://{your-domain}/mcp` |
|
||||
|
||||
## Authentifizierungsmethoden
|
||||
|
||||
Sie haben zwei Möglichkeiten, Ihren MCP-Client zu authentifizieren: **OAuth** (empfohlen) oder **API-Schlüssel**.
|
||||
|
||||
### Option A — OAuth (empfohlen)
|
||||
|
||||
Mit OAuth öffnet Ihr MCP-Client ein Browserfenster, damit Sie sich anmelden können. Es werden keine geheimen Informationen in Konfigurationsdateien gespeichert, und Token werden automatisch erneuert.
|
||||
|
||||
<Note>
|
||||
OAuth erfordert einen MCP-Client, der die [MCP-Autorisierungsspezifikation](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) unterstützt. Claude Desktop, Claude Code, Cursor und ChatGPT unterstützen dies.
|
||||
</Note>
|
||||
|
||||
Fügen Sie dies zu Ihrer MCP-Client-Konfiguration hinzu und ersetzen Sie `{your-workspace-url}` durch den Host Ihrer Workspace-URL (z. B. `mycompany.twenty.com`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"twenty": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://{your-workspace-url}/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Das ist alles — kein API-Schlüssel erforderlich. Wenn der Client sich zum ersten Mal verbindet, wird er:
|
||||
|
||||
1. Die OAuth-Metadaten von Twenty über `/.well-known/oauth-protected-resource` und `/.well-known/oauth-authorization-server` ermitteln
|
||||
2. Sich über die dynamische Client-Registrierung (RFC 7591) als OAuth-Client registrieren
|
||||
3. Ihren Browser öffnen, um den Zugriff zu autorisieren
|
||||
4. Token empfangen und eine Verbindung zum MCP-Server herstellen
|
||||
|
||||
Nachfolgende Verbindungen verwenden die gespeicherten Token erneut und erneuern sie automatisch.
|
||||
|
||||
### Option B — API-Schlüssel
|
||||
|
||||
Wenn Ihr MCP-Client OAuth nicht unterstützt oder Sie statische Anmeldeinformationen bevorzugen, übergeben Sie einen API-Schlüssel im `Authorization`-Header:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"twenty": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://{your-workspace-url}/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer YOUR_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Ihr API-Schlüssel gewährt Zugriff auf Workspace-Daten. Halten Sie es von der Versionskontrolle und von gemeinsam genutzten Dotfiles fern.
|
||||
</Warning>
|
||||
|
||||
Um einen API-Schlüssel zu erstellen, gehen Sie zu **Settings > APIs & Webhooks > + Create key**. Details finden Sie unter [APIs](/l/de/developers/extend/api#create-an-api-key).
|
||||
|
||||
## Schnellstart
|
||||
|
||||
### 1. Konfiguration kopieren
|
||||
|
||||
Gehen Sie in Twenty zu **Settings > AI > More > MCP Server**. Wählen Sie Ihre Authentifizierungsmethode (OAuth oder API-Schlüssel), kopieren Sie das JSON-Snippet (es verwendet bereits Ihre Workspace-URL) und fügen Sie es in die Konfigurationsdatei Ihres MCP-Clients ein.
|
||||
|
||||
| Client | Speicherort der Konfigurationsdatei |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) oder `%APPDATA%\Claude\claude_desktop_config.json` (Windows) |
|
||||
| **Claude Code** | `~/.claude.json` (Benutzer) oder `.mcp.json` (Projekt) |
|
||||
| **Cursor** | `.cursor/mcp.json` in Ihrem Projekt oder `~/.cursor/mcp.json` global |
|
||||
| **ChatGPT** | Aktivieren Sie den Entwicklermodus in **Settings > Apps & Connectors > Advanced settings** und verwenden Sie dann **Create** in **Settings > Apps & Connectors**, um den MCP-Server hinzuzufügen |
|
||||
|
||||
### 2. Verbinden
|
||||
|
||||
Starten Sie Ihren MCP-Client neu (oder laden Sie die Konfiguration neu). Bei Verwendung von OAuth werden Sie zu Twenty weitergeleitet, um den Zugriff zu autorisieren. Bei Verwendung eines API-Schlüssels wird die Verbindung sofort hergestellt.
|
||||
|
||||
### 3. Jetzt loslegen
|
||||
|
||||
Bitten Sie Ihren KI-Assistenten, mit Ihrem CRM zu interagieren:
|
||||
|
||||
* *"Zeige mir die 5 zuletzt erstellten Unternehmen"*
|
||||
* *"Erstelle eine neue Person namens Jane Doe bei Acme Corp"*
|
||||
* *"Finde alle offenen Verkaufschancen mit einem Wert von mehr als $10k"*
|
||||
|
||||
## Verfügbare Tools
|
||||
|
||||
Nach der Verbindung stellt der MCP-Server Tools bereit, die die Twenty-API widerspiegeln. Der empfohlene Workflow ist:
|
||||
|
||||
1. **`get_tool_catalog`** — alle verfügbaren Tools entdecken
|
||||
2. **`learn_tools`** — das Eingabeschema für bestimmte Tools abrufen
|
||||
3. **`execute_tool`** — ein Tool ausführen
|
||||
|
||||
Sie müssen sich die Tool-Namen nicht merken. Fragen Sie Ihren KI-Assistenten, was er tun kann, und er ruft `get_tool_catalog` automatisch auf.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
MCP-Verbindungen erben die Berechtigungen des authentifizierten Benutzers (OAuth) oder die dem API-Schlüssel zugewiesene Rolle. So beschränken Sie, was der MCP-Server tun darf:
|
||||
|
||||
* **OAuth**: Es gilt die Workspace-Rolle des Benutzers.
|
||||
* **API-Schlüssel**: Weisen Sie dem API-Schlüssel unter **Settings > Roles** eine Rolle zu. Siehe [Berechtigungen](/l/de/user-guide/permissions-access/capabilities/permissions).
|
||||
|
||||
## Selbstgehostete Konfiguration
|
||||
|
||||
Für selbstgehostete Instanzen ersetzen Sie `{your-workspace-url}` durch die URL Ihres Servers. Stellen Sie sicher, dass `SERVER_URL` in Ihrer Umgebung der öffentlichen URL Ihrer Twenty-Instanz entspricht — dieser Wert wird verwendet, um die OAuth-Discovery-Metadaten zu generieren.
|
||||
|
||||
```bash
|
||||
SERVER_URL=https://twenty.yourcompany.com
|
||||
```
|
||||
|
||||
Der MCP-Endpunkt, die OAuth-Endpunkte und die Discovery-Metadaten leiten sich alle von diesem Wert ab.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
**"Unauthorized"- oder 401-Fehler**
|
||||
|
||||
* OAuth: Autorisieren Sie erneut, indem Sie die gespeicherten Token in Ihrem MCP-Client löschen und die Verbindung wiederherstellen.
|
||||
* API-Schlüssel: Überprüfen Sie, ob der Schlüssel gültig ist und nicht abgelaufen ist. Generieren Sie ihn bei Bedarf neu.
|
||||
|
||||
**Der OAuth-Flow öffnet keinen Browser**
|
||||
|
||||
* Stellen Sie sicher, dass Ihr MCP-Client MCP Authorization unterstützt. Wechseln Sie andernfalls zur API-Schlüssel-Methode.
|
||||
|
||||
**Verbindungszeitüberschreitung**
|
||||
|
||||
* Stellen Sie sicher, dass die MCP-Endpunkt-URL von Ihrem Rechner aus erreichbar ist. Bei selbstgehosteten Instanzen prüfen Sie, ob der Server läuft und `SERVER_URL` korrekt gesetzt ist.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user