Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d5104eb18 | ||
|
|
225f185278 | ||
|
|
ca1d49c6cd | ||
|
|
2f9c94d9a0 | ||
|
|
a9c4920fcc | ||
|
|
07dd27f6c4 | ||
|
|
7a2e397ad1 | ||
|
|
8a3b96d911 | ||
|
|
132a19f688 | ||
|
|
3bfdc2c83f | ||
|
|
8b26020a0b | ||
|
|
b1107c823a | ||
|
|
5c4a1f931a | ||
|
|
4266f4022a | ||
|
|
b2b3a3f860 | ||
|
|
d48c58640c | ||
|
|
005223de8c | ||
|
|
2f09fb8c04 | ||
|
|
083df3e7ca | ||
|
|
2e9624858c | ||
|
|
58e37a118c | ||
|
|
0b766464e4 | ||
|
|
802a5b0af6 | ||
|
|
ae291c99ba | ||
|
|
7809f83e72 | ||
|
|
27847f6ac6 | ||
|
|
6351c6c1c6 | ||
|
|
20a2c3836e | ||
|
|
1eb284c87f | ||
|
|
c4140f85df | ||
|
|
9c4b0f526c | ||
|
|
37bcb35391 | ||
|
|
78a0197643 | ||
|
|
ff3326a53b | ||
|
|
5e92fb4fc6 | ||
|
|
1a8be234de | ||
|
|
d021f7e369 | ||
|
|
1b67ba6a75 | ||
|
|
5afc46ebd3 | ||
|
|
a06abb1d60 | ||
|
|
2a5b2746c9 |
@@ -0,0 +1,80 @@
|
||||
name: Spawn Twenty Docker Image
|
||||
description: >
|
||||
Starts a full Twenty instance (server, worker, database, redis) using Docker
|
||||
Compose. The server is available at http://localhost:3000 for subsequent steps
|
||||
in the caller's job.
|
||||
Pulls the specified semver image tag from Docker Hub.
|
||||
Designed to be consumed from external repositories (e.g., twenty-app).
|
||||
|
||||
inputs:
|
||||
twenty-version:
|
||||
description: 'Twenty Docker Hub image tag as semver (e.g., v0.40.0, v1.0.0).'
|
||||
required: true
|
||||
twenty-repository:
|
||||
description: 'Twenty repository to checkout docker compose files from.'
|
||||
required: false
|
||||
default: 'twentyhq/twenty'
|
||||
github-token:
|
||||
description: 'GitHub token for cross-repo checkout. Required when calling from an external repository.'
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
|
||||
outputs:
|
||||
server-url:
|
||||
description: 'URL where the Twenty server can be reached'
|
||||
value: http://localhost:3000
|
||||
access-token:
|
||||
description: 'Admin access token for the Twenty instance'
|
||||
value: ${{ steps.admin-token.outputs.access-token }}
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Validate version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${{ inputs.twenty-version }}"
|
||||
if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::twenty-version must be a semver tag (e.g., v0.40.0). Got: '$VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout docker compose files
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.twenty-repository }}
|
||||
ref: ${{ inputs.twenty-version }}
|
||||
token: ${{ inputs.github-token }}
|
||||
sparse-checkout: |
|
||||
packages/twenty-docker
|
||||
sparse-checkout-cone-mode: false
|
||||
path: .twenty-spawn
|
||||
|
||||
- name: Prepare environment
|
||||
shell: bash
|
||||
working-directory: ./.twenty-spawn/packages/twenty-docker
|
||||
run: |
|
||||
cp .env.example .env
|
||||
echo "" >> .env
|
||||
echo "TAG=${{ inputs.twenty-version }}" >> .env
|
||||
echo "APP_SECRET=replace_me_with_a_random_string" >> .env
|
||||
echo "SERVER_URL=http://localhost:3000" >> .env
|
||||
|
||||
- name: Start Twenty instance
|
||||
shell: bash
|
||||
working-directory: ./.twenty-spawn/packages/twenty-docker
|
||||
run: |
|
||||
docker compose up -d --wait || {
|
||||
echo "::error::Docker compose failed to start or health checks timed out"
|
||||
docker compose logs
|
||||
exit 1
|
||||
}
|
||||
echo "Twenty instance is ready at http://localhost:3000"
|
||||
|
||||
- name: Set admin access token
|
||||
id: admin-token
|
||||
shell: bash
|
||||
run: |
|
||||
ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik"
|
||||
echo "::add-mask::$ACCESS_TOKEN"
|
||||
echo "access-token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT"
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy-main:
|
||||
timeout-minutes: 3
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy-tag:
|
||||
timeout-minutes: 3
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
|
||||
@@ -16,7 +16,7 @@ permissions:
|
||||
jobs:
|
||||
changed-files:
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
||||
steps:
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
ci-create-app-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, create-app-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
ci-emails-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, emails-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -14,8 +14,8 @@ concurrency:
|
||||
|
||||
env:
|
||||
# restore-cache action adds 'v4-' prefix and '-<branch>-<sha>' suffix to the key
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-depot-ubuntu-24.04-8-runner-${{ github.ref_name }}-${{ github.sha }}
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-ubuntu-latest-8-cores-runner
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-ubuntu-latest-8-cores-runner-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
steps:
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
# path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
# merge-reports-and-check-coverage:
|
||||
# timeout-minutes: 30
|
||||
# runs-on: depot-ubuntu-24.04
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: front-sb-test
|
||||
# env:
|
||||
# PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
if: false
|
||||
needs: front-sb-build
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
TASK_CACHE_KEY: front-task-${{ matrix.task }}
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
steps:
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
# path: packages/twenty-front/build
|
||||
# retention-days: 1
|
||||
e2e-test:
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check-e2e, front-build]
|
||||
if: |
|
||||
always() &&
|
||||
@@ -349,7 +349,7 @@ jobs:
|
||||
ci-front-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
ci-e2e-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check-e2e, e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -25,7 +25,7 @@ defaults:
|
||||
jobs:
|
||||
create_pr:
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -15,7 +15,7 @@ defaults:
|
||||
jobs:
|
||||
tag_and_release:
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
||||
steps:
|
||||
- name: Check PR Author
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: [changed-files-check, sdk-test]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, sdk-test, sdk-e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: server-setup
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: server-setup
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
strategy:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
ci-shared-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, shared-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
ci-test-docker-compose-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -22,7 +22,7 @@ concurrency:
|
||||
jobs:
|
||||
danger-js:
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'closed'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
congratulate:
|
||||
timeout-minutes: 3
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
ci-website-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, website-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
name: CI Zapier
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -14,6 +12,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
SERVER_SETUP_CACHE_KEY: server-setup
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
@@ -23,14 +24,81 @@ jobs:
|
||||
packages/twenty-server/**
|
||||
!packages/twenty-zapier/package.json
|
||||
!packages/twenty-zapier/CHANGELOG.md
|
||||
zapier-test:
|
||||
server-setup:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest-8-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: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Server / Write .env
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Server / Build
|
||||
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: Server / Start
|
||||
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: Start worker
|
||||
run: |
|
||||
npx nx run twenty-server:worker &
|
||||
echo "Worker started"
|
||||
|
||||
- name: Zapier / Build
|
||||
run: npx nx build twenty-zapier
|
||||
|
||||
- name: Zapier / Run Tests
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:zapier
|
||||
tasks: test
|
||||
|
||||
zapier-test:
|
||||
needs: server-setup
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test, validate]
|
||||
task: [lint, typecheck, validate]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
@@ -52,7 +120,7 @@ jobs:
|
||||
ci-zapier-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, zapier-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
claude-cross-repo:
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -34,7 +34,7 @@ concurrency:
|
||||
jobs:
|
||||
pull_docs_translations:
|
||||
name: Pull docs translations
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
push_docs:
|
||||
name: Push documentation to Crowdin
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -32,7 +32,7 @@ concurrency:
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -17,7 +17,7 @@ concurrency:
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: 'Preview Environment Dispatch'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -34,7 +33,7 @@ jobs:
|
||||
)
|
||||
)
|
||||
timeout-minutes: 5
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger preview environment workflow
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
diff --git a/esm/cache.js b/esm/cache.js
|
||||
index 07cf6d7dd99effb9c3464b620ba67a7f445224f5..248bb527923499a6be8065ee7a3613b55819c58c 100644
|
||||
--- a/esm/cache.js
|
||||
+++ b/esm/cache.js
|
||||
@@ -69,17 +69,20 @@ export class TransformCacheCollection {
|
||||
this.invalidate(cacheName, filename);
|
||||
});
|
||||
}
|
||||
- invalidateIfChanged(filename, content) {
|
||||
+ invalidateIfChanged(filename, content, _visited) {
|
||||
+ const visited = _visited || new Set();
|
||||
+ if (visited.has(filename)) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ visited.add(filename);
|
||||
const fileEntrypoint = this.get('entrypoints', filename);
|
||||
|
||||
- // We need to check all dependencies of the file
|
||||
- // because they might have changed as well.
|
||||
if (fileEntrypoint) {
|
||||
for (const [, dependency] of fileEntrypoint.dependencies) {
|
||||
const dependencyFilename = dependency.resolved;
|
||||
if (dependencyFilename) {
|
||||
const dependencyContent = fs.readFileSync(dependencyFilename, 'utf8');
|
||||
- this.invalidateIfChanged(dependencyFilename, dependencyContent);
|
||||
+ this.invalidateIfChanged(dependencyFilename, dependencyContent, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git a/lib/cache.js b/lib/cache.js
|
||||
index 0762ed7d3c39b31000f7aa7d8156da15403c8e64..6955410cd3c9ec53cf7a01c8346abc4c47fff791 100644
|
||||
--- a/lib/cache.js
|
||||
+++ b/lib/cache.js
|
||||
@@ -77,17 +77,20 @@ class TransformCacheCollection {
|
||||
this.invalidate(cacheName, filename);
|
||||
});
|
||||
}
|
||||
- invalidateIfChanged(filename, content) {
|
||||
+ invalidateIfChanged(filename, content, _visited) {
|
||||
+ const visited = _visited || new Set();
|
||||
+ if (visited.has(filename)) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ visited.add(filename);
|
||||
const fileEntrypoint = this.get('entrypoints', filename);
|
||||
|
||||
- // We need to check all dependencies of the file
|
||||
- // because they might have changed as well.
|
||||
if (fileEntrypoint) {
|
||||
for (const [, dependency] of fileEntrypoint.dependencies) {
|
||||
const dependencyFilename = dependency.resolved;
|
||||
if (dependencyFilename) {
|
||||
const dependencyContent = _nodeFs.default.readFileSync(dependencyFilename, 'utf8');
|
||||
- this.invalidateIfChanged(dependencyFilename, dependencyContent);
|
||||
+ this.invalidateIfChanged(dependencyFilename, dependencyContent, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Emotion (styling), Vite
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
@@ -175,7 +175,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Emotion** for styling with styled-components pattern
|
||||
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
|
||||
@@ -109,7 +109,7 @@ Below are a few features we have implemented to date:
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Nx](https://nx.dev/)
|
||||
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
|
||||
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Emotion](https://emotion.sh/) and [Lingui](https://lingui.dev/)
|
||||
- [React](https://reactjs.org/), with [Jotai](https://jotai.org/), [Linaria](https://linaria.dev/) and [Lingui](https://lingui.dev/)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
# npm-Based App Distribution for Twenty
|
||||
|
||||
*Technical Design Document -- February 2026*
|
||||
|
||||
## Overview
|
||||
|
||||
Add npm registry support for distributing Twenty apps (public and private), with per-AppRegistration registry overrides, direct tarball upload as escape hatch, and version upgrade detection.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The marketplace install flow (currently a TODO in the resolver and frontend) will be implemented separately. This plan provides the infrastructure that install flow will call into.
|
||||
- The existing `app:dev` flow (individual file uploads via CLI) remains unchanged.
|
||||
- The existing GitHub-based marketplace discovery remains as a curated fallback.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Developer
|
||||
/ | \
|
||||
npm publish | twenty app:push
|
||||
/ | \
|
||||
npmjs.com Private Reg Server REST Upload
|
||||
| | |
|
||||
v v v
|
||||
[Discovery Layer] [Direct Upload]
|
||||
npm search API |
|
||||
GitHub curated list |
|
||||
| |
|
||||
v |
|
||||
MarketplaceService |
|
||||
(sourcePackage in DTO) |
|
||||
| |
|
||||
v v
|
||||
AppPackageResolverService <--------+
|
||||
.npmrc generation
|
||||
yarn add / tarball extract
|
||||
|
|
||||
v
|
||||
ApplicationSyncService (existing)
|
||||
WorkspaceMigrationRunnerService
|
||||
|
|
||||
v
|
||||
AppRegistration + Application entities
|
||||
```
|
||||
|
||||
## Phase 1: Entity and Config Changes
|
||||
|
||||
### 1a. Extend AppRegistrationEntity
|
||||
|
||||
Add four columns to `application-registration.entity.ts`:
|
||||
|
||||
- **`sourcePackage`** (text, nullable) -- npm package name, e.g. `"twenty-app-fireflies"` or `"@myorg/twenty-app-crm"`. Null for tarball-only or OAuth-only apps.
|
||||
- **`tarballFileId`** (uuid, nullable) -- FK to a FileEntity storing a directly-uploaded `.tar.gz`. Null when the app comes from npm.
|
||||
- **`registryUrl`** (text, nullable) -- per-registration npm registry override. Null means "use the server default `APP_REGISTRY_URL`." This is how a single server can pull public apps from npmjs.com while pulling `@mycompany/*` apps from GitHub Packages.
|
||||
- **`latestAvailableVersion`** (text, nullable) -- cached latest version from the registry, updated periodically. Compared against `Application.version` to surface upgrade availability.
|
||||
|
||||
**Source resolution priority:**
|
||||
|
||||
1. `sourcePackage` is set → resolve from npm via `yarn add`
|
||||
2. `tarballFileId` is set → extract from file storage
|
||||
3. Neither → OAuth-only app, no server-side code
|
||||
|
||||
### 1b. Extend ApplicationEntity.sourceType
|
||||
|
||||
Widen the `sourceType` union from `'local'` to `'local' | 'npm' | 'tarball'`:
|
||||
|
||||
- `'local'` -- existing behavior (CLI `app:dev`, individual file uploads, workspace-custom)
|
||||
- `'npm'` -- installed from an npm registry via `yarn add`
|
||||
- `'tarball'` -- installed from a directly-uploaded tarball
|
||||
|
||||
This lets the system distinguish how an app was installed, which matters for upgrade logic (npm apps can check the registry for newer versions; tarball apps cannot).
|
||||
|
||||
### 1c. Add server-wide config variables
|
||||
|
||||
Add a new `APP_REGISTRY_CONFIG` group to ConfigVariablesGroup:
|
||||
|
||||
- **`APP_REGISTRY_URL`** (string, default `https://registry.npmjs.org`) -- default npm registry URL
|
||||
- **`APP_REGISTRY_TOKEN`** (string, optional, sensitive) -- auth token for the default registry
|
||||
|
||||
### 1d. Generate migration
|
||||
|
||||
TypeORM migration adding `sourcePackage`, `tarballFileId`, `registryUrl`, `latestAvailableVersion` to `core.applicationRegistration`.
|
||||
|
||||
## Phase 2: App Package Resolver Service
|
||||
|
||||
### 2a. Create AppPackageResolverService
|
||||
|
||||
New service with core method:
|
||||
|
||||
```
|
||||
resolvePackage(appRegistration, options?: { targetVersion? }) → ResolvedPackage | null
|
||||
```
|
||||
|
||||
Returns `{ manifestPath, packageJsonPath, filesDir }` or null for OAuth-only apps.
|
||||
|
||||
**Resolution logic:**
|
||||
|
||||
```
|
||||
if sourcePackage:
|
||||
1. Determine registry: appRegistration.registryUrl ?? APP_REGISTRY_URL
|
||||
2. Determine auth token for the resolved registry
|
||||
3. Generate temporary .npmrc in an isolated working directory
|
||||
4. Run: yarn add <sourcePackage>@<targetVersion ?? latest>
|
||||
5. Read manifest from node_modules/<sourcePackage>/.twenty/output/manifest.json
|
||||
6. Return paths
|
||||
|
||||
if tarballFileId:
|
||||
1. Download tarball from FileStorageService
|
||||
2. Extract to temporary directory
|
||||
3. Read manifest from extracted files
|
||||
4. Return paths
|
||||
|
||||
else:
|
||||
return null (OAuth-only)
|
||||
```
|
||||
|
||||
### 2b. Isolated working directories
|
||||
|
||||
Each resolution runs in a temporary directory under `{os.tmpdir()}/twenty-app-resolver/{uuid}/`. This avoids contaminating the server's own `node_modules` and isolates apps from each other. Cleaned up after files are copied to storage.
|
||||
|
||||
### 2c. .npmrc generation
|
||||
|
||||
For scoped packages (`@scope/twenty-app-*`):
|
||||
|
||||
```
|
||||
@scope:registry=https://npm.pkg.github.com
|
||||
//npm.pkg.github.com/:_authToken=TOKEN
|
||||
```
|
||||
|
||||
For unscoped packages with a non-default registry:
|
||||
|
||||
```
|
||||
registry=https://my-verdaccio.internal:4873
|
||||
//my-verdaccio.internal:4873/:_authToken=TOKEN
|
||||
```
|
||||
|
||||
### 2d. Post-resolution file transfer
|
||||
|
||||
After resolving, copies files into the app's storage path using the existing FileStorageService layout:
|
||||
|
||||
```
|
||||
{workspaceId}/{applicationUniversalIdentifier}/
|
||||
built-logic-function/...
|
||||
built-front-component/...
|
||||
dependencies/package.json
|
||||
dependencies/yarn.lock
|
||||
public-asset/...
|
||||
source/...
|
||||
```
|
||||
|
||||
This reuses the same FileFolder enum paths that `app:dev` uses, so downstream ApplicationSyncService works unchanged.
|
||||
|
||||
## Phase 3: Marketplace Discovery via npm
|
||||
|
||||
### 3a. Update MarketplaceService
|
||||
|
||||
Add npm-based discovery alongside the existing GitHub path:
|
||||
|
||||
- Query npm search API: `GET {registryUrl}/-/v1/search?text=keywords:twenty-app&size=250`
|
||||
- Map each result to MarketplaceAppDTO using package.json metadata
|
||||
|
||||
**Merge strategy:**
|
||||
|
||||
1. Fetch from npm search API (apps with `keywords: ["twenty-app"]`)
|
||||
2. Fetch from GitHub (existing curated list)
|
||||
3. Merge by `universalIdentifier` -- GitHub entries override npm entries (allowing curation)
|
||||
4. Cache merged result with existing 1-hour TTL
|
||||
|
||||
### 3b. Add sourcePackage to MarketplaceAppDTO
|
||||
|
||||
The DTO needs a `sourcePackage: string | null` field so the install flow knows which npm package to resolve. For npm-discovered apps, this is the package name. For GitHub-only apps, this is null.
|
||||
|
||||
## Phase 4: Version Upgrade Support
|
||||
|
||||
### 4a. Create AppUpgradeService
|
||||
|
||||
**Periodic version check (npm-sourced apps only):**
|
||||
|
||||
- Fetches `{registryUrl}/{sourcePackage}/latest` from the npm registry
|
||||
- Stores result in `AppRegistration.latestAvailableVersion`
|
||||
- Frontend compares against `Application.version` to show "Update available"
|
||||
|
||||
**Upgrade trigger:**
|
||||
|
||||
1. Resolve the new version via AppPackageResolverService
|
||||
2. Sync via existing ApplicationSyncService (triggers workspace migration for schema changes)
|
||||
3. Update Application.version
|
||||
|
||||
**Rollback strategy:** If sync fails (e.g., migration validation error), re-resolve the previous version and re-sync. This is possible because npm retains all published versions.
|
||||
|
||||
### 4b. Version check scheduling
|
||||
|
||||
Lightweight cron or check-on-access pattern. Iterates over AppRegistrations where `sourcePackage IS NOT NULL` and calls `checkForUpdates()`. Frequency: once per hour, matching the existing marketplace cache TTL.
|
||||
|
||||
## Phase 5: SDK CLI Commands
|
||||
|
||||
### 5a. Finalize `twenty app:build`
|
||||
|
||||
Ensure `.twenty/output/` is npm-publishable. The build step generates a `package.json` in the output directory:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "twenty-app-fireflies",
|
||||
"version": "1.2.0",
|
||||
"keywords": ["twenty-app"],
|
||||
"twenty": {
|
||||
"universalIdentifier": "a4df0c0f-c65e-44e5-8436-24814182d4ac"
|
||||
},
|
||||
"files": ["manifest.json", "built-logic-function", "built-front-component", "public-asset"]
|
||||
}
|
||||
```
|
||||
|
||||
The developer then publishes with standard `npm publish` -- no custom command needed.
|
||||
|
||||
### 5b. `twenty app:pack` (new command)
|
||||
|
||||
```
|
||||
twenty app:pack [appPath]
|
||||
```
|
||||
|
||||
- Runs `app:build` if `.twenty/output/` doesn't exist or is stale
|
||||
- Uses existing TarballService to create `{name}-{version}.tar.gz`
|
||||
- Outputs the file path for manual distribution
|
||||
|
||||
### 5c. `twenty app:push` (new command)
|
||||
|
||||
```
|
||||
twenty app:push [appPath] --server <url> --token <token>
|
||||
```
|
||||
|
||||
- Runs `app:pack` to produce the tarball
|
||||
- Reads universalIdentifier from manifest to find or create the AppRegistration
|
||||
- Uploads via `POST /api/app-registrations/upload-tarball`
|
||||
- Reports success with the registration ID
|
||||
- Reuses `twenty auth:login` credentials if `--server` is not specified
|
||||
|
||||
## Phase 6: Server Tarball Upload Endpoint
|
||||
|
||||
### 6a. REST controller
|
||||
|
||||
```
|
||||
POST /api/app-registrations/upload-tarball
|
||||
Content-Type: multipart/form-data
|
||||
Body: tarball file + optional universalIdentifier
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
|
||||
- Max file size: 50MB
|
||||
- Must be a valid `.tar.gz`
|
||||
- Extracted contents must contain `manifest.json` with a valid `universalIdentifier`
|
||||
- The `universalIdentifier` must not conflict with an existing registration owned by a different user
|
||||
|
||||
**Flow:**
|
||||
|
||||
1. Extract tarball to temp directory
|
||||
2. Validate manifest structure
|
||||
3. Find or create AppRegistration by universalIdentifier
|
||||
4. Store tarball in FileStorageService under `FileFolder.AppTarball`
|
||||
5. Set `tarballFileId` on the AppRegistration
|
||||
6. Return the AppRegistration entity
|
||||
|
||||
### 6b. Add FileFolder.AppTarball
|
||||
|
||||
New enum value `AppTarball = 'app-tarball'` in FileFolder.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|---|---|
|
||||
| Per-AppRegistration registry override | `registryUrl` on the entity allows mixing registries. Public apps from npmjs.com, private from GitHub Packages/Verdaccio. Server-wide `APP_REGISTRY_URL` is the fallback. |
|
||||
| npm publish is standard | No custom publish infra. Free versioning, README, `npm audit`, download stats, proven auth model. |
|
||||
| Tarball as escape hatch | Air-gapped environments, CI pipelines, one-off installs. Cannot auto-upgrade. |
|
||||
| sourceType distinction | `'npm' \| 'tarball' \| 'local'` lets the system know which upgrade path is available. Only npm apps can check for newer versions. |
|
||||
| Backward compatible | `app:dev` flow unchanged. GitHub marketplace unchanged. All new fields nullable. |
|
||||
| Upgrade rollback | Re-resolve previous version from npm on failure. Safe because npm never deletes published versions. |
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **npm unreachable**: Timeout after 30s, throw clear error. App remains at current installed version.
|
||||
- **Package name conflicts**: The `universalIdentifier` in the `twenty` field of `package.json` is the source of truth, not the npm package name. Two packages with the same universalIdentifier conflict at the AppRegistration level (unique index).
|
||||
- **Scoped vs unscoped packages**: Both work. Scoped packages naturally route to a private registry via `.npmrc` scope mapping.
|
||||
- **Multiple workspaces, same server**: AppRegistration is server-level (core schema). Application is workspace-level. One AppRegistration can be installed in multiple workspaces at different versions.
|
||||
Binary file not shown.
@@ -276,7 +276,7 @@
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
"application": {
|
||||
"style": "@emotion/styled",
|
||||
"style": "@linaria/react",
|
||||
"linter": "eslint",
|
||||
"bundler": "vite",
|
||||
"compiler": "swc",
|
||||
@@ -284,7 +284,7 @@
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"library": {
|
||||
"style": "@emotion/styled",
|
||||
"style": "@linaria/react",
|
||||
"linter": "eslint",
|
||||
"bundler": "vite",
|
||||
"compiler": "swc",
|
||||
@@ -292,7 +292,7 @@
|
||||
"projectNameAndRootFormat": "derived"
|
||||
},
|
||||
"component": {
|
||||
"style": "@emotion/styled"
|
||||
"style": "@linaria/react"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+5
-3
@@ -2,14 +2,13 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.17",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@sniptt/guards": "^0.2.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@wyw-in-js/babel-preset": "^1.0.6",
|
||||
"@wyw-in-js/vite": "^0.7.0",
|
||||
"archiver": "^7.0.1",
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
@@ -41,6 +40,7 @@
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"lodash.upperfirst": "^4.3.1",
|
||||
"microdiff": "^1.3.2",
|
||||
"next-with-linaria": "^1.3.0",
|
||||
"planer": "^1.2.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -202,7 +202,9 @@
|
||||
"typescript": "5.9.2",
|
||||
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
||||
"@lingui/core": "5.1.2",
|
||||
"@types/qs": "6.9.16"
|
||||
"@types/qs": "6.9.16",
|
||||
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
|
||||
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
|
||||
@@ -15,6 +15,7 @@ const jestConfig = {
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^package.json$': '<rootDir>/package.json',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js'],
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.3",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -27,5 +27,5 @@
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.integration-test.ts"]
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
env: {
|
||||
NODE_ENV: 'integration',
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -115,7 +115,6 @@ export class CreateAppCommand {
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleSkill: false,
|
||||
includeIntegrationTest: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,7 +127,6 @@ export class CreateAppCommand {
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeIntegrationTest: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,32 +171,19 @@ export class CreateAppCommand {
|
||||
value: 'skill',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Integration test (vitest test verifying app installation)',
|
||||
value: 'integrationTest',
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const includeField = selectedExamples.includes('field');
|
||||
const includeView = selectedExamples.includes('view');
|
||||
const includeIntegrationTest =
|
||||
selectedExamples.includes('integrationTest');
|
||||
const includeObject =
|
||||
selectedExamples.includes('object') ||
|
||||
includeField ||
|
||||
includeView ||
|
||||
includeIntegrationTest;
|
||||
selectedExamples.includes('object') || includeField || includeView;
|
||||
|
||||
if (
|
||||
(includeField || includeView || includeIntegrationTest) &&
|
||||
!selectedExamples.includes('object')
|
||||
) {
|
||||
if ((includeField || includeView) && !selectedExamples.includes('object')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Note: Example object auto-included because example field/view/integration test depends on it.',
|
||||
'Note: Example object auto-included because example field/view depends on it.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -212,7 +197,6 @@ export class CreateAppCommand {
|
||||
includeExampleNavigationMenuItem:
|
||||
selectedExamples.includes('navigationMenuItem'),
|
||||
includeExampleSkill: selectedExamples.includes('skill'),
|
||||
includeIntegrationTest,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,5 +8,4 @@ export type ExampleOptions = {
|
||||
includeExampleView: boolean;
|
||||
includeExampleNavigationMenuItem: boolean;
|
||||
includeExampleSkill: boolean;
|
||||
includeIntegrationTest: boolean;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import * as fs from 'fs-extra';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import createTwentyAppPackageJson from 'package.json';
|
||||
|
||||
jest.mock('fs-extra', () => {
|
||||
const actual = jest.requireActual('fs-extra');
|
||||
@@ -24,7 +25,6 @@ const ALL_EXAMPLES: ExampleOptions = {
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeIntegrationTest: true,
|
||||
};
|
||||
|
||||
const NO_EXAMPLES: ExampleOptions = {
|
||||
@@ -35,7 +35,6 @@ const NO_EXAMPLES: ExampleOptions = {
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeIntegrationTest: false,
|
||||
};
|
||||
|
||||
describe('copyBaseApplicationProject', () => {
|
||||
@@ -90,7 +89,9 @@ describe('copyBaseApplicationProject', () => {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
expect(packageJson.name).toBe('my-test-app');
|
||||
expect(packageJson.version).toBe('0.1.0');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('latest');
|
||||
expect(packageJson.devDependencies['twenty-sdk']).toBe(
|
||||
createTwentyAppPackageJson.version,
|
||||
);
|
||||
expect(packageJson.scripts['twenty']).toBe('twenty');
|
||||
});
|
||||
|
||||
@@ -354,12 +355,6 @@ describe('copyBaseApplicationProject', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// Install functions should always exist
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
@@ -433,11 +428,6 @@ describe('copyBaseApplicationProject', () => {
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -456,7 +446,6 @@ describe('copyBaseApplicationProject', () => {
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeIntegrationTest: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -494,7 +483,6 @@ describe('copyBaseApplicationProject', () => {
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeIntegrationTest: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -750,76 +738,6 @@ describe('copyBaseApplicationProject', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test', () => {
|
||||
it('should create app-install.integration-test.ts with correct structure when enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const testPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'__tests__',
|
||||
'app-install.integration-test.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(testPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(testPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { appBuild, appUninstall } from 'twenty-sdk/cli'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { MetadataApiClient } from 'twenty-sdk/generated'",
|
||||
);
|
||||
expect(content).toContain('assertServerIsReachable');
|
||||
expect(content).toContain('appBuild');
|
||||
expect(content).toContain('appUninstall');
|
||||
expect(content).toContain('findManyApplications');
|
||||
});
|
||||
|
||||
it('should include vitest and test scripts in package.json when enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
join(testAppDirectory, 'package.json'),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.test).toBe('vitest run');
|
||||
expect(packageJson.scripts['test:watch']).toBe('vitest');
|
||||
expect(packageJson.devDependencies.vitest).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not include vitest or test scripts when disabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: NO_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
join(testAppDirectory, 'package.json'),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.test).toBeUndefined();
|
||||
expect(packageJson.scripts['test:watch']).toBeUndefined();
|
||||
expect(packageJson.devDependencies.vitest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('post-install logic function', () => {
|
||||
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
|
||||
import { type ExampleOptions } from '@/types/scaffolding-options';
|
||||
import createTwentyAppPackageJson from 'package.json';
|
||||
|
||||
const SRC_FOLDER = 'src';
|
||||
|
||||
@@ -22,11 +23,7 @@ export const copyBaseApplicationProject = async ({
|
||||
}) => {
|
||||
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
|
||||
|
||||
await createPackageJson({
|
||||
appName,
|
||||
appDirectory,
|
||||
includeIntegrationTest: exampleOptions.includeIntegrationTest,
|
||||
});
|
||||
await createPackageJson({ appName, appDirectory });
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
@@ -101,14 +98,6 @@ export const copyBaseApplicationProject = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeIntegrationTest) {
|
||||
await createIntegrationTest({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: '__tests__',
|
||||
fileName: 'app-install.integration-test.ts',
|
||||
});
|
||||
}
|
||||
|
||||
await createDefaultPreInstallFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
@@ -158,8 +147,7 @@ generated
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty/*
|
||||
!.twenty/output/
|
||||
.twenty
|
||||
|
||||
# production
|
||||
/build
|
||||
@@ -508,113 +496,6 @@ export default defineSkill({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createIntegrationTest = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const content = `import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = path.resolve(__dirname, '../..');
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
|
||||
const readApiKeyFromConfig = (): string | undefined => {
|
||||
const configPath = path.join(os.homedir(), '.twenty', 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
const defaultProfile = config.profiles?.default;
|
||||
|
||||
return defaultProfile?.apiKey ?? config.apiKey;
|
||||
};
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
try {
|
||||
const response = await fetch(\`\${TWENTY_API_URL}/healthz\`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`Server returned \${response.status}\`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
\`Twenty server is not reachable at \${TWENTY_API_URL}. \` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
describe('App installation', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(\`[build] \${message}\`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
\`App build failed: \${buildResult.error?.message ?? 'Unknown error'}\`,
|
||||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
\`App uninstall failed: \${uninstallResult.error?.message ?? 'Unknown error'}\`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const apiKey = readApiKeyFromConfig();
|
||||
const metadataClient = new MetadataApiClient({
|
||||
url: \`\${TWENTY_API_URL}/metadata\`,
|
||||
headers: {
|
||||
Authorization: \`Bearer \${apiKey}\`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.findManyApplications.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
@@ -646,33 +527,10 @@ export default defineApplication({
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
includeIntegrationTest,
|
||||
}: {
|
||||
appName: string;
|
||||
appDirectory: string;
|
||||
includeIntegrationTest: boolean;
|
||||
}) => {
|
||||
const scripts: Record<string, string> = {
|
||||
twenty: 'twenty',
|
||||
lint: 'eslint',
|
||||
'lint:fix': 'eslint --fix',
|
||||
};
|
||||
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
};
|
||||
|
||||
if (includeIntegrationTest) {
|
||||
scripts.test = 'vitest run';
|
||||
scripts['test:watch'] = 'vitest';
|
||||
devDependencies.vitest = '^3.1.1';
|
||||
}
|
||||
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
version: '0.1.0',
|
||||
@@ -683,11 +541,21 @@ const createPackageJson = async ({
|
||||
yarn: '>=4.0.2',
|
||||
},
|
||||
packageManager: '[email protected]',
|
||||
scripts,
|
||||
dependencies: {
|
||||
'twenty-sdk': 'latest',
|
||||
scripts: {
|
||||
twenty: 'twenty',
|
||||
lint: 'eslint',
|
||||
'lint:fix': 'eslint --fix',
|
||||
},
|
||||
dependencies: {},
|
||||
devDependencies: {
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
},
|
||||
devDependencies,
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"noEmit": true,
|
||||
"types": ["jest", "node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"package.json": ["./package.json"]
|
||||
},
|
||||
"jsx": "react"
|
||||
},
|
||||
|
||||
@@ -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,12 @@
|
||||
## 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
|
||||
|
||||
## UUID requirement
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
@@ -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,27 @@
|
||||
{
|
||||
"name": "apollo-enrich",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"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,54 @@
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: 'ac1d2ed1-8835-4bd4-9043-28b46fdda465',
|
||||
displayName: 'Apollo enrichment',
|
||||
description: 'Data enrichment with Apollo to keep your data accurate',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
settingsCustomTabFrontComponentUniversalIdentifier: '50d59f7c-eada-4731-aacd-8e45371e1040',
|
||||
applicationVariables: {
|
||||
APOLLO_CLIENT_ID: {
|
||||
universalIdentifier: '5852219e-7757-463e-9e7c-80980203794c',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Apollo Client ID',
|
||||
},
|
||||
APOLLO_CLIENT_SECRET: {
|
||||
universalIdentifier: 'a032349d-9458-4381-8505-82547276434a',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Apollo Client Secret',
|
||||
},
|
||||
APOLLO_OAUTH_URL: {
|
||||
universalIdentifier: '1d42411c-5809-4093-873a-8121b1302475',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description: 'Apollo OAuth URL',
|
||||
},
|
||||
APOLLO_REDIRECT_URI: {
|
||||
universalIdentifier: 'c8d9e0f1-2a3b-4c5d-6e7f-8a9b0c1d2e3f',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description: 'Apollo OAuth redirect URI',
|
||||
},
|
||||
APOLLO_REGISTERED_URL: {
|
||||
universalIdentifier: '672a6fce-5565-43bc-9a3b-7f2c33620770',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description: 'Apollo registered URL',
|
||||
},
|
||||
APOLLO_ACCESS_TOKEN: {
|
||||
universalIdentifier: '672a6fce-5565-43bc-9a3b-7f2c33620771',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Apollo access token',
|
||||
},
|
||||
APOLLO_REFRESH_TOKEN: {
|
||||
universalIdentifier: '672a6fce-5565-43bc-9a3b-7f2c33620772',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Apollo refresh token',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'da15cfc6-3657-457d-8757-4ba11b5bb6e1',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'apolloFoundedYear',
|
||||
label: 'Founded Year',
|
||||
description: 'Year the company was founded, from Apollo enrichment',
|
||||
icon: 'IconCalendar',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '505532f5-1fc5-4a58-8074-ba9b48650dbc',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.TEXT,
|
||||
name: 'apolloIndustry',
|
||||
label: 'Apollo Industry',
|
||||
description: 'Industry classification from Apollo enrichment',
|
||||
icon: 'IconBuildingFactory',
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'be15e062-b065-48b4-979c-65b9a50e0cb1',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.TEXT,
|
||||
name: 'apolloShortDescription',
|
||||
label: 'Apollo Description',
|
||||
description: 'Short company description from Apollo enrichment',
|
||||
icon: 'IconFileDescription',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'apolloTotalFunding',
|
||||
label: 'Total Funding',
|
||||
description: 'Total funding raised by the company, from Apollo enrichment',
|
||||
icon: 'IconCash',
|
||||
});
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { OAuthApplicationVariables } from 'src/logic-functions/get-oauth-application-variables';
|
||||
import { VERIFY_PAGE_PATH } from 'src/logic-functions/get-verify-page';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.h3`
|
||||
color: #333;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
`;
|
||||
|
||||
const StyledSectionSubtitle = styled.p`
|
||||
color: #818181;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
margin: 0 0 12px 0;
|
||||
`;
|
||||
|
||||
const StyledCard = styled.div`
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid #ebebeb;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
`;
|
||||
|
||||
const StyledTextContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
color: #333;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.span`
|
||||
color: #818181;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
align-items: center;
|
||||
background: #5e5adb;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
color: #fafafa;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
gap: 4px;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
text-decoration: none;
|
||||
transition: background 0.1s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #4b47b8;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #3c3996;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledConnectedStatus = styled.span`
|
||||
align-items: center;
|
||||
background: #10b981;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.img`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
const APOLLO_ICON_URL = 'https://twenty-icons.com/apollo.io';
|
||||
|
||||
const fetchOAuthApplicationVariables = async (): Promise<OAuthApplicationVariables> => {
|
||||
const backEndUrl = `${process.env.TWENTY_API_URL}/s/oauth/application-variables`;
|
||||
const response = await fetch(backEndUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const buildOAuthUrl = (oauthApplicationVariables: OAuthApplicationVariables): string => {
|
||||
const { apolloOAuthUrl, apolloClientId, apolloRegisteredUrl } = oauthApplicationVariables;
|
||||
const redirectUri = `${apolloRegisteredUrl}auth/oauth-propagator/callback`;
|
||||
const state = encodeURIComponent(`${process.env.TWENTY_API_URL}/s${VERIFY_PAGE_PATH}`);
|
||||
return `${apolloOAuthUrl}?client_id=${apolloClientId}&redirect_uri=${redirectUri}&state=${state}&response_type=code`;
|
||||
};
|
||||
|
||||
const ApolloOAuthCta = () => {
|
||||
const [oauthApplicationVariables, setOAuthApplicationVariables] =
|
||||
useState<OAuthApplicationVariables | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOAuthApplicationVariables()
|
||||
.then(setOAuthApplicationVariables)
|
||||
.catch(setError)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>Connect to Apollo</StyledSectionTitle>
|
||||
<StyledSectionSubtitle>Enrich your contacts with Apollo data</StyledSectionSubtitle>
|
||||
<StyledCard>
|
||||
<StyledIconContainer>
|
||||
<StyledIcon src={APOLLO_ICON_URL} alt="Apollo" />
|
||||
</StyledIconContainer>
|
||||
<StyledTextContainer>
|
||||
<StyledTitle>Apollo OAuth</StyledTitle>
|
||||
<StyledDescription>Loading...</StyledDescription>
|
||||
</StyledTextContainer>
|
||||
</StyledCard>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !oauthApplicationVariables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isConnected = Boolean(oauthApplicationVariables.apolloAccessToken);
|
||||
const oauthUrl = buildOAuthUrl(oauthApplicationVariables);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledSectionTitle>Connect to Apollo</StyledSectionTitle>
|
||||
<StyledSectionSubtitle>Enrich your contacts with Apollo data</StyledSectionSubtitle>
|
||||
<StyledCard>
|
||||
<StyledIconContainer>
|
||||
<StyledIcon src={APOLLO_ICON_URL} alt="Apollo" />
|
||||
</StyledIconContainer>
|
||||
<StyledTextContainer>
|
||||
<StyledTitle>Apollo OAuth</StyledTitle>
|
||||
<StyledDescription>
|
||||
{isConnected
|
||||
? 'Your Apollo account is connected'
|
||||
: 'Connect your Apollo account to enrich contacts'}
|
||||
</StyledDescription>
|
||||
</StyledTextContainer>
|
||||
{isConnected ? (
|
||||
<StyledConnectedStatus>
|
||||
✓ Connected
|
||||
</StyledConnectedStatus>
|
||||
) : (
|
||||
<StyledLink href={oauthUrl} rel="noopener noreferrer">
|
||||
Connect
|
||||
</StyledLink>
|
||||
)}
|
||||
</StyledCard>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '50d59f7c-eada-4731-aacd-8e45371e1040',
|
||||
name: 'apollo-oauth-cta',
|
||||
description: 'CTA button to connect to Apollo Enrichment via OAuth',
|
||||
component: ApolloOAuthCta,
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { defineLogicFunction, RoutePayload } from "twenty-sdk";
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
|
||||
|
||||
export const OAUTH_TOKEN_PAIRS_PATH = '/oauth/token-pairs';
|
||||
|
||||
type ApolloTokenResponse = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
const getAuthenticationTokenPairs = async (
|
||||
code: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<ApolloTokenResponse> => {
|
||||
const formData = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code: code,
|
||||
redirect_uri: 'https://hjsm0q38-3000.uks1.devtunnels.ms/auth/oauth-propagator/callback',
|
||||
});
|
||||
|
||||
const response = await fetch('https://app.apollo.io/api/v1/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to exchange code for tokens: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const handler = async (event: RoutePayload): Promise<any> => {
|
||||
const { queryStringParameters: { code } } = event;
|
||||
|
||||
if (!code) {
|
||||
throw new Error('Code is required');
|
||||
}
|
||||
|
||||
const apolloClientId = process.env.APOLLO_CLIENT_ID ?? '';
|
||||
const apolloClientSecret = process.env.APOLLO_CLIENT_SECRET ?? '';
|
||||
const applicationId = process.env.APPLICATION_ID ?? '';
|
||||
|
||||
|
||||
const metadataClient = new MetadataApiClient({});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const tokenPairs = await getAuthenticationTokenPairs(
|
||||
code,
|
||||
apolloClientId,
|
||||
apolloClientSecret,
|
||||
);
|
||||
|
||||
await metadataClient.mutation({
|
||||
updateOneApplicationVariable: {
|
||||
__args: {
|
||||
key: 'APOLLO_ACCESS_TOKEN',
|
||||
value: tokenPairs.access_token,
|
||||
applicationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await metadataClient.mutation({
|
||||
updateOneApplicationVariable: {
|
||||
__args: {
|
||||
key: 'APOLLO_REFRESH_TOKEN',
|
||||
value: tokenPairs.refresh_token,
|
||||
applicationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
return {tokenPairs};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '7ccc63a7-ece1-44c0-adbe-805a1baea03a',
|
||||
name: 'get-authentication-token-pairs',
|
||||
description: 'Returns the Apollo authentication token pairs',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: OAUTH_TOKEN_PAIRS_PATH,
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
export type OAuthApplicationVariables = {
|
||||
apolloClientId: string;
|
||||
apolloRegisteredUrl: string;
|
||||
apolloOAuthUrl: string;
|
||||
apolloAccessToken: string;
|
||||
apolloRefreshToken: string;
|
||||
};
|
||||
|
||||
const handler = async (): Promise<OAuthApplicationVariables> => {
|
||||
const apolloClientId = process.env.APOLLO_CLIENT_ID ?? '';
|
||||
const apolloRegisteredUrl = process.env.APOLLO_REGISTERED_URL ?? '';
|
||||
const apolloOAuthUrl = process.env.APOLLO_OAUTH_URL ?? '';
|
||||
const apolloAccessToken = process.env.APOLLO_ACCESS_TOKEN ?? '';
|
||||
const apolloRefreshToken = process.env.APOLLO_REFRESH_TOKEN ?? '';
|
||||
|
||||
return { apolloClientId, apolloRegisteredUrl, apolloOAuthUrl, apolloAccessToken, apolloRefreshToken };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'b7c3e8f1-9d4a-4e2b-8f6c-1a5d3e7b9c2f',
|
||||
name: 'get-oauth-application-variables',
|
||||
description: 'Returns the Apollo OAuth authorization URL',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/oauth/application-variables',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { defineLogicFunction } from "twenty-sdk";
|
||||
|
||||
export const VERIFY_PAGE_PATH = '/oauth/verify';
|
||||
|
||||
const buildVerifyPageHtml = (applicationId: string): string => `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Apollo OAuth - Verifying...</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; }
|
||||
.loading { color: #6b7280; }
|
||||
.success { color: #10b981; }
|
||||
.error { color: #ef4444; }
|
||||
.spinner { border: 3px solid #f3f4f6; border-top: 3px solid #3b82f6; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
<script>
|
||||
(async function() {
|
||||
const applicationId = ${JSON.stringify(applicationId)};
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const baseUrl = window.location.origin;
|
||||
|
||||
function showError(message) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('spinner').style.display = 'none';
|
||||
document.getElementById('title').textContent = '✗ Connection Failed';
|
||||
document.getElementById('title').className = 'error';
|
||||
document.getElementById('status').textContent = message;
|
||||
});
|
||||
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'APOLLO_OAUTH_ERROR', error: message }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
showError('Authorization code is missing. Please try connecting again.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
baseUrl + '/s/oauth/token-pairs?code=' + encodeURIComponent(code),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error('Failed to get tokens: ' + response.status + ' - ' + errorText);
|
||||
}
|
||||
|
||||
const tokens = await response.json();
|
||||
|
||||
window.location.href = 'http://apple.localhost:3001/settings/applications/' + applicationId + '#custom';
|
||||
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="spinner" id="spinner"></div>
|
||||
<h1 class="loading" id="title">Connecting to Apollo...</h1>
|
||||
<p id="status">Please wait while we complete the connection.</p>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const handler = async (): Promise<string> => {
|
||||
const applicationId = process.env.APPLICATION_ID ?? '';
|
||||
|
||||
return buildVerifyPageHtml(applicationId);
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '4d74950a-d9c1-4c66-a799-89c1aea4e6b0',
|
||||
name: 'get-verify-page',
|
||||
description: 'Returns the Apollo OAuth verify page',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: VERIFY_PAGE_PATH,
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
defineLogicFunction,
|
||||
type DatabaseEventPayload,
|
||||
type ObjectRecordUpdateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/generated';
|
||||
|
||||
|
||||
|
||||
type CompanyRecord = {
|
||||
id: string;
|
||||
name?: string;
|
||||
domainName?: {
|
||||
primaryLinkUrl?: string;
|
||||
primaryLinkLabel?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ApolloOrganization = {
|
||||
name?: string;
|
||||
website_url?: string;
|
||||
linkedin_url?: string;
|
||||
twitter_url?: string;
|
||||
estimated_num_employees?: number;
|
||||
annual_revenue?: number;
|
||||
total_funding?: number;
|
||||
street_address?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
postal_code?: string;
|
||||
country?: string;
|
||||
short_description?: string;
|
||||
industry?: string;
|
||||
founded_year?: number;
|
||||
};
|
||||
|
||||
type ApolloEnrichResponse = {
|
||||
organization?: ApolloOrganization;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const extractDomain = (
|
||||
domainName?: CompanyRecord['domainName'],
|
||||
): string | undefined => {
|
||||
const url = domainName?.primaryLinkUrl;
|
||||
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const hostname = new URL(
|
||||
url.startsWith('http') ? url : `https://${url}`,
|
||||
).hostname;
|
||||
|
||||
return hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
return url.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchApolloEnrichment = async (
|
||||
domain: string,
|
||||
): Promise<ApolloOrganization | undefined> => {
|
||||
const response = await fetch(
|
||||
`https://api.apollo.io/api/v1/organizations/enrich?domain=${encodeURIComponent(domain)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.APOLLO_ACCESS_TOKEN ?? ''}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data: ApolloEnrichResponse = await response.json();
|
||||
|
||||
return data.organization;
|
||||
};
|
||||
|
||||
const buildCompanyUpdateData = (
|
||||
apolloOrganization: ApolloOrganization,
|
||||
): Record<string, unknown> => {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
if (apolloOrganization.name) {
|
||||
updateData.name = apolloOrganization.name;
|
||||
}
|
||||
|
||||
if (apolloOrganization.estimated_num_employees) {
|
||||
updateData.employees = apolloOrganization.estimated_num_employees;
|
||||
}
|
||||
|
||||
if (apolloOrganization.linkedin_url) {
|
||||
updateData.linkedinLink = {
|
||||
primaryLinkUrl: apolloOrganization.linkedin_url,
|
||||
primaryLinkLabel: 'LinkedIn',
|
||||
};
|
||||
}
|
||||
|
||||
if (apolloOrganization.twitter_url) {
|
||||
updateData.xLink = {
|
||||
primaryLinkUrl: apolloOrganization.twitter_url,
|
||||
primaryLinkLabel: 'X',
|
||||
};
|
||||
}
|
||||
|
||||
if (apolloOrganization.annual_revenue) {
|
||||
updateData.annualRecurringRevenue = {
|
||||
amountMicros: apolloOrganization.annual_revenue * 1_000_000,
|
||||
currencyCode: 'USD',
|
||||
};
|
||||
}
|
||||
|
||||
const hasAddress =
|
||||
apolloOrganization.street_address ||
|
||||
apolloOrganization.city ||
|
||||
apolloOrganization.state ||
|
||||
apolloOrganization.country;
|
||||
|
||||
if (hasAddress) {
|
||||
updateData.address = {
|
||||
addressStreet1: apolloOrganization.street_address ?? '',
|
||||
addressCity: apolloOrganization.city ?? '',
|
||||
addressState: apolloOrganization.state ?? '',
|
||||
addressPostcode: apolloOrganization.postal_code ?? '',
|
||||
addressCountry: apolloOrganization.country ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
if (apolloOrganization.industry) {
|
||||
updateData.apolloIndustry = apolloOrganization.industry;
|
||||
}
|
||||
|
||||
if (apolloOrganization.short_description) {
|
||||
updateData.apolloShortDescription = apolloOrganization.short_description;
|
||||
}
|
||||
|
||||
if (apolloOrganization.founded_year) {
|
||||
updateData.apolloFoundedYear = apolloOrganization.founded_year;
|
||||
}
|
||||
|
||||
if (apolloOrganization.total_funding) {
|
||||
updateData.apolloTotalFunding = {
|
||||
amountMicros: apolloOrganization.total_funding * 1_000_000,
|
||||
currencyCode: 'USD',
|
||||
};
|
||||
}
|
||||
|
||||
return updateData;
|
||||
};
|
||||
|
||||
const updateCompanyInTwenty = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, unknown>,
|
||||
): Promise<void> => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}: no result`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
type CompanyUpdateEvent = DatabaseEventPayload<
|
||||
ObjectRecordUpdateEvent<CompanyRecord>
|
||||
>;
|
||||
|
||||
const handler = async (
|
||||
event: CompanyUpdateEvent,
|
||||
): Promise<object | undefined> => {
|
||||
|
||||
|
||||
const { recordId, properties } = event;
|
||||
const { after: companyAfter } = properties;
|
||||
|
||||
const domain = extractDomain(companyAfter?.domainName);
|
||||
|
||||
if (!domain) {
|
||||
return { skipped: true, reason: 'no domain found on company' };
|
||||
}
|
||||
|
||||
const apolloOrganization = await fetchApolloEnrichment(domain);
|
||||
|
||||
if (!apolloOrganization) {
|
||||
return {
|
||||
skipped: true,
|
||||
reason: `no Apollo data found for ${domain}`,
|
||||
};
|
||||
}
|
||||
|
||||
const updateData = buildCompanyUpdateData(apolloOrganization);
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return { skipped: true, reason: 'no enrichment data to apply' };
|
||||
}
|
||||
|
||||
|
||||
await updateCompanyInTwenty(recordId, updateData);
|
||||
|
||||
const result = {
|
||||
enriched: true,
|
||||
companyId: recordId,
|
||||
domain,
|
||||
updatedFields: Object.keys(updateData),
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '6248b3fe-a8af-404a-8e38-19df98f73d81',
|
||||
name: 'on-company-updated',
|
||||
description:
|
||||
'Enriches company data from Apollo when the company domain is updated',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: ['domainName'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '08292efc-d7ba-4ec3-ab95-e7c33bd3a3bc',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'af7cd86e-149e-466a-8d60-312b6e46d604',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b8faae3f-e174-43fa-ab94-715712ae26cb';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Apollo enrich default function role',
|
||||
description: 'Apollo enrich default function role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: true,
|
||||
});
|
||||
@@ -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
@@ -9,19 +9,18 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"create-entity": "twenty app add",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"auth": "twenty auth login",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"twenty": "twenty",
|
||||
"auth": "twenty auth:login",
|
||||
"dev": "twenty app:dev",
|
||||
"build": "twenty app:build",
|
||||
"typecheck": "twenty app:typecheck",
|
||||
"uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "portal:../../twenty-sdk"
|
||||
"twenty-sdk": "0.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"vitest": "^3.1.1"
|
||||
"@types/node": "^24.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = path.resolve(__dirname, '../..');
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
|
||||
const POST_CARD_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'54b589ca-eeed-4950-a176-358418b85c05';
|
||||
|
||||
const readApiKeyFromConfig = (): string | undefined => {
|
||||
const configPath = path.join(os.homedir(), '.twenty', 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
const defaultProfile = config.profiles?.default;
|
||||
|
||||
return defaultProfile?.apiKey ?? config.apiKey;
|
||||
};
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
try {
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status}`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
describe('Hello World app installation', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
`App build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have the postCard object in object metadata after installation', async () => {
|
||||
const apiKey = readApiKeyFromConfig();
|
||||
const metadataClient = new MetadataApiClient({
|
||||
url: `${TWENTY_API_URL}/metadata`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await metadataClient.query({
|
||||
objects: {
|
||||
__args: {
|
||||
paging: { first: 200 },
|
||||
filter: {},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
isActive: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postCardObject = result.objects.edges.find(
|
||||
(edge) =>
|
||||
edge.node.universalIdentifier ===
|
||||
POST_CARD_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(postCardObject).toMatchObject({
|
||||
node: {
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
env: {
|
||||
// The SDK's ConfigService reads credentials from ~/.twenty/config.json
|
||||
// but falls back to a temp dir when NODE_ENV=test.
|
||||
NODE_ENV: 'integration',
|
||||
// MetadataApiClient defaults to TWENTY_API_URL for its GraphQL endpoint.
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1117,7 +1117,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5":
|
||||
"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0":
|
||||
version: 1.5.5
|
||||
resolution: "@jridgewell/sourcemap-codec@npm:1.5.5"
|
||||
checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0
|
||||
@@ -1472,24 +1472,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/chai@npm:^5.2.2":
|
||||
version: 5.2.3
|
||||
resolution: "@types/chai@npm:5.2.3"
|
||||
dependencies:
|
||||
"@types/deep-eql": "npm:*"
|
||||
assertion-error: "npm:^2.0.1"
|
||||
checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/deep-eql@npm:*":
|
||||
version: 4.0.2
|
||||
resolution: "@types/deep-eql@npm:4.0.2"
|
||||
checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0":
|
||||
"@types/estree@npm:1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "@types/estree@npm:1.0.8"
|
||||
checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
|
||||
@@ -1579,89 +1562,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/expect@npm:3.2.4"
|
||||
dependencies:
|
||||
"@types/chai": "npm:^5.2.2"
|
||||
"@vitest/spy": "npm:3.2.4"
|
||||
"@vitest/utils": "npm:3.2.4"
|
||||
chai: "npm:^5.2.0"
|
||||
tinyrainbow: "npm:^2.0.0"
|
||||
checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/mocker@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/mocker@npm:3.2.4"
|
||||
dependencies:
|
||||
"@vitest/spy": "npm:3.2.4"
|
||||
estree-walker: "npm:^3.0.3"
|
||||
magic-string: "npm:^0.30.17"
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/pretty-format@npm:3.2.4, @vitest/pretty-format@npm:^3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/pretty-format@npm:3.2.4"
|
||||
dependencies:
|
||||
tinyrainbow: "npm:^2.0.0"
|
||||
checksum: 10c0/5ad7d4278e067390d7d633e307fee8103958806a419ca380aec0e33fae71b44a64415f7a9b4bc11635d3c13d4a9186111c581d3cef9c65cc317e68f077456887
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/runner@npm:3.2.4"
|
||||
dependencies:
|
||||
"@vitest/utils": "npm:3.2.4"
|
||||
pathe: "npm:^2.0.3"
|
||||
strip-literal: "npm:^3.0.0"
|
||||
checksum: 10c0/e8be51666c72b3668ae3ea348b0196656a4a5adb836cb5e270720885d9517421815b0d6c98bfdf1795ed02b994b7bfb2b21566ee356a40021f5bf4f6ed4e418a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/snapshot@npm:3.2.4"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:3.2.4"
|
||||
magic-string: "npm:^0.30.17"
|
||||
pathe: "npm:^2.0.3"
|
||||
checksum: 10c0/f8301a3d7d1559fd3d59ed51176dd52e1ed5c2d23aa6d8d6aa18787ef46e295056bc726a021698d8454c16ed825ecba163362f42fa90258bb4a98cfd2c9424fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/spy@npm:3.2.4"
|
||||
dependencies:
|
||||
tinyspy: "npm:^4.0.3"
|
||||
checksum: 10c0/6ebf0b4697dc238476d6b6a60c76ba9eb1dd8167a307e30f08f64149612fd50227682b876420e4c2e09a76334e73f72e3ebf0e350714dc22474258292e202024
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "@vitest/utils@npm:3.2.4"
|
||||
dependencies:
|
||||
"@vitest/pretty-format": "npm:3.2.4"
|
||||
loupe: "npm:^3.1.4"
|
||||
tinyrainbow: "npm:^2.0.0"
|
||||
checksum: 10c0/024a9b8c8bcc12cf40183c246c244b52ecff861c6deb3477cbf487ac8781ad44c68a9c5fd69f8c1361878e55b97c10d99d511f2597f1f7244b5e5101d028ba64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@zag-js/accordion@npm:1.35.2":
|
||||
version: 1.35.2
|
||||
resolution: "@zag-js/accordion@npm:1.35.2"
|
||||
@@ -2742,13 +2642,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"assertion-error@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "assertion-error@npm:2.0.1"
|
||||
checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-function@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-function@npm:1.0.0"
|
||||
@@ -2980,13 +2873,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cac@npm:^6.7.14":
|
||||
version: 6.7.14
|
||||
resolution: "cac@npm:6.7.14"
|
||||
checksum: 10c0/4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cacache@npm:^20.0.1":
|
||||
version: 20.0.3
|
||||
resolution: "cacache@npm:20.0.3"
|
||||
@@ -3040,19 +2926,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chai@npm:^5.2.0":
|
||||
version: 5.3.3
|
||||
resolution: "chai@npm:5.3.3"
|
||||
dependencies:
|
||||
assertion-error: "npm:^2.0.1"
|
||||
check-error: "npm:^2.1.1"
|
||||
deep-eql: "npm:^5.0.1"
|
||||
loupe: "npm:^3.1.0"
|
||||
pathval: "npm:^2.0.0"
|
||||
checksum: 10c0/b360fd4d38861622e5010c2f709736988b05c7f31042305fa3f4e9911f6adb80ccfb4e302068bf8ed10e835c2e2520cba0f5edc13d878b886987e5aa62483f53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^1.0.0, chalk@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "chalk@npm:1.1.3"
|
||||
@@ -3091,13 +2964,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"check-error@npm:^2.1.1":
|
||||
version: 2.1.3
|
||||
resolution: "check-error@npm:2.1.3"
|
||||
checksum: 10c0/878e99038fb6476316b74668cd6a498c7e66df3efe48158fa40db80a06ba4258742ac3ee2229c4a2a98c5e73f5dff84eb3e50ceb6b65bbd8f831eafc8338607d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:^4.0.0":
|
||||
version: 4.0.3
|
||||
resolution: "chokidar@npm:4.0.3"
|
||||
@@ -3339,7 +3205,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4, debug@npm:^4.4.1":
|
||||
"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4":
|
||||
version: 4.4.3
|
||||
resolution: "debug@npm:4.4.3"
|
||||
dependencies:
|
||||
@@ -3358,13 +3224,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-eql@npm:^5.0.1":
|
||||
version: 5.0.2
|
||||
resolution: "deep-eql@npm:5.0.2"
|
||||
checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delayed-stream@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "delayed-stream@npm:1.0.0"
|
||||
@@ -3471,13 +3330,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-module-lexer@npm:^1.7.0":
|
||||
version: 1.7.0
|
||||
resolution: "es-module-lexer@npm:1.7.0"
|
||||
checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "es-object-atoms@npm:1.1.1"
|
||||
@@ -3710,15 +3562,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"estree-walker@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "estree-walker@npm:3.0.3"
|
||||
dependencies:
|
||||
"@types/estree": "npm:^1.0.0"
|
||||
checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-target-shim@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "event-target-shim@npm:5.0.1"
|
||||
@@ -3749,13 +3592,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expect-type@npm:^1.2.1":
|
||||
version: 1.3.0
|
||||
resolution: "expect-type@npm:1.3.0"
|
||||
checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exponential-backoff@npm:^3.1.1":
|
||||
version: 3.1.3
|
||||
resolution: "exponential-backoff@npm:3.1.3"
|
||||
@@ -4163,8 +3999,7 @@ __metadata:
|
||||
resolution: "hello-world@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
twenty-sdk: "portal:../../twenty-sdk"
|
||||
vitest: "npm:^3.1.1"
|
||||
twenty-sdk: "npm:0.6.3"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -4530,13 +4365,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-tokens@npm:^9.0.1":
|
||||
version: 9.0.1
|
||||
resolution: "js-tokens@npm:9.0.1"
|
||||
checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsesc@npm:^3.0.2":
|
||||
version: 3.1.0
|
||||
resolution: "jsesc@npm:3.1.0"
|
||||
@@ -4718,13 +4546,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loupe@npm:^3.1.0, loupe@npm:^3.1.4":
|
||||
version: 3.2.1
|
||||
resolution: "loupe@npm:3.2.1"
|
||||
checksum: 10c0/910c872cba291309664c2d094368d31a68907b6f5913e989d301b5c25f30e97d76d77f23ab3bf3b46d0f601ff0b6af8810c10c31b91d2c6b2f132809ca2cc705
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^10.2.0":
|
||||
version: 10.4.3
|
||||
resolution: "lru-cache@npm:10.4.3"
|
||||
@@ -4739,15 +4560,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^0.30.17":
|
||||
version: 0.30.21
|
||||
resolution: "magic-string@npm:0.30.21"
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec": "npm:^1.5.5"
|
||||
checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-fetch-happen@npm:^15.0.0":
|
||||
version: 15.0.4
|
||||
resolution: "make-fetch-happen@npm:15.0.4"
|
||||
@@ -5244,20 +5056,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pathe@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "pathe@npm:2.0.3"
|
||||
checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pathval@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "pathval@npm:2.0.1"
|
||||
checksum: 10c0/460f4709479fbf2c45903a65655fc8f0a5f6d808f989173aeef5fdea4ff4f303dc13f7870303999add60ec49d4c14733895c0a869392e9866f1091fa64fd7581
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"perfect-freehand@npm:^1.2.2":
|
||||
version: 1.2.3
|
||||
resolution: "perfect-freehand@npm:1.2.3"
|
||||
@@ -5279,7 +5077,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3":
|
||||
"picomatch@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "picomatch@npm:4.0.3"
|
||||
checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2
|
||||
@@ -5797,13 +5595,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"siginfo@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "siginfo@npm:2.0.0"
|
||||
checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7":
|
||||
version: 3.0.7
|
||||
resolution: "signal-exit@npm:3.0.7"
|
||||
@@ -5912,20 +5703,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stackback@npm:0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "stackback@npm:0.0.2"
|
||||
checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"std-env@npm:^3.9.0":
|
||||
version: 3.10.0
|
||||
resolution: "std-env@npm:3.10.0"
|
||||
checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.21.0":
|
||||
version: 2.23.0
|
||||
resolution: "streamx@npm:2.23.0"
|
||||
@@ -6045,15 +5822,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-literal@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "strip-literal@npm:3.1.0"
|
||||
dependencies:
|
||||
js-tokens: "npm:^9.0.1"
|
||||
checksum: 10c0/50918f669915d9ad0fe4b7599902b735f853f2201c97791ead00104a654259c0c61bc2bc8fa3db05109339b61f4cf09e47b94ecc874ffbd0e013965223893af8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stylis@npm:4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "stylis@npm:4.2.0"
|
||||
@@ -6149,21 +5917,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinybench@npm:^2.9.0":
|
||||
version: 2.9.0
|
||||
resolution: "tinybench@npm:2.9.0"
|
||||
checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyexec@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "tinyexec@npm:0.3.2"
|
||||
checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15":
|
||||
"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15":
|
||||
version: 0.2.15
|
||||
resolution: "tinyglobby@npm:0.2.15"
|
||||
dependencies:
|
||||
@@ -6173,27 +5927,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinypool@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "tinypool@npm:1.1.1"
|
||||
checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyrainbow@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "tinyrainbow@npm:2.0.0"
|
||||
checksum: 10c0/c83c52bef4e0ae7fb8ec6a722f70b5b6fa8d8be1c85792e829f56c0e1be94ab70b293c032dc5048d4d37cfe678f1f5babb04bdc65fd123098800148ca989184f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyspy@npm:^4.0.3":
|
||||
version: 4.0.4
|
||||
resolution: "tinyspy@npm:4.0.4"
|
||||
checksum: 10c0/a8020fc17799251e06a8398dcc352601d2770aa91c556b9531ecd7a12581161fd1c14e81cbdaff0c1306c93bfdde8ff6d1c1a3f9bbe6d91604f0fd4e01e2f1eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tmp@npm:^0.0.33":
|
||||
version: 0.0.33
|
||||
resolution: "tmp@npm:0.0.33"
|
||||
@@ -6247,9 +5980,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@portal:../../twenty-sdk::locator=hello-world%40workspace%3A.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "twenty-sdk@portal:../../twenty-sdk::locator=hello-world%40workspace%3A."
|
||||
"twenty-sdk@npm:0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "twenty-sdk@npm:0.6.3"
|
||||
dependencies:
|
||||
"@chakra-ui/react": "npm:^3.33.0"
|
||||
"@emotion/react": "npm:^11.14.0"
|
||||
@@ -6285,8 +6018,9 @@ __metadata:
|
||||
zod: "npm:^4.1.11"
|
||||
bin:
|
||||
twenty: dist/cli.cjs
|
||||
checksum: 10c0/44bc143d6125393effeb749aaa9795b22591d2add713485304df46cc0bbbe8c1155ff0f833f848658bb3ae2857276c4c830649a3fb9d3e56c8bca458107bcc88
|
||||
languageName: node
|
||||
linkType: soft
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^0.21.3":
|
||||
version: 0.21.3
|
||||
@@ -6423,21 +6157,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-node@npm:3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "vite-node@npm:3.2.4"
|
||||
dependencies:
|
||||
cac: "npm:^6.7.14"
|
||||
debug: "npm:^4.4.1"
|
||||
es-module-lexer: "npm:^1.7.0"
|
||||
pathe: "npm:^2.0.3"
|
||||
vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
bin:
|
||||
vite-node: vite-node.mjs
|
||||
checksum: 10c0/6ceca67c002f8ef6397d58b9539f80f2b5d79e103a18367288b3f00a8ab55affa3d711d86d9112fce5a7fa658a212a087a005a045eb8f4758947dd99af2a6c6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-tsconfig-paths@npm:^4.2.1":
|
||||
version: 4.3.2
|
||||
resolution: "vite-tsconfig-paths@npm:4.3.2"
|
||||
@@ -6454,7 +6173,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0, vite@npm:^7.0.0":
|
||||
"vite@npm:^7.0.0":
|
||||
version: 7.3.1
|
||||
resolution: "vite@npm:7.3.1"
|
||||
dependencies:
|
||||
@@ -6509,62 +6228,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:^3.1.1":
|
||||
version: 3.2.4
|
||||
resolution: "vitest@npm:3.2.4"
|
||||
dependencies:
|
||||
"@types/chai": "npm:^5.2.2"
|
||||
"@vitest/expect": "npm:3.2.4"
|
||||
"@vitest/mocker": "npm:3.2.4"
|
||||
"@vitest/pretty-format": "npm:^3.2.4"
|
||||
"@vitest/runner": "npm:3.2.4"
|
||||
"@vitest/snapshot": "npm:3.2.4"
|
||||
"@vitest/spy": "npm:3.2.4"
|
||||
"@vitest/utils": "npm:3.2.4"
|
||||
chai: "npm:^5.2.0"
|
||||
debug: "npm:^4.4.1"
|
||||
expect-type: "npm:^1.2.1"
|
||||
magic-string: "npm:^0.30.17"
|
||||
pathe: "npm:^2.0.3"
|
||||
picomatch: "npm:^4.0.2"
|
||||
std-env: "npm:^3.9.0"
|
||||
tinybench: "npm:^2.9.0"
|
||||
tinyexec: "npm:^0.3.2"
|
||||
tinyglobby: "npm:^0.2.14"
|
||||
tinypool: "npm:^1.1.1"
|
||||
tinyrainbow: "npm:^2.0.0"
|
||||
vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
vite-node: "npm:3.2.4"
|
||||
why-is-node-running: "npm:^2.3.0"
|
||||
peerDependencies:
|
||||
"@edge-runtime/vm": "*"
|
||||
"@types/debug": ^4.1.12
|
||||
"@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
"@vitest/browser": 3.2.4
|
||||
"@vitest/ui": 3.2.4
|
||||
happy-dom: "*"
|
||||
jsdom: "*"
|
||||
peerDependenciesMeta:
|
||||
"@edge-runtime/vm":
|
||||
optional: true
|
||||
"@types/debug":
|
||||
optional: true
|
||||
"@types/node":
|
||||
optional: true
|
||||
"@vitest/browser":
|
||||
optional: true
|
||||
"@vitest/ui":
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
bin:
|
||||
vitest: vitest.mjs
|
||||
checksum: 10c0/5bf53ede3ae6a0e08956d72dab279ae90503f6b5a05298a6a5e6ef47d2fd1ab386aaf48fafa61ed07a0ebfe9e371772f1ccbe5c258dd765206a8218bf2eb79eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webidl-conversions@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "webidl-conversions@npm:3.0.1"
|
||||
@@ -6611,18 +6274,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"why-is-node-running@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "why-is-node-running@npm:2.3.0"
|
||||
dependencies:
|
||||
siginfo: "npm:^2.0.0"
|
||||
stackback: "npm:0.0.2"
|
||||
bin:
|
||||
why-is-node-running: cli.js
|
||||
checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"widest-line@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "widest-line@npm:5.0.0"
|
||||
|
||||
@@ -1,2 +1,37 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -2,25 +2,6 @@
|
||||
|
||||
Used to manage billing and telemetry of self-hosted instances
|
||||
|
||||
## Requirements
|
||||
- twenty-cli `npm install -g twenty-cli`
|
||||
- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one
|
||||
|
||||
|
||||
## Install to your Twenty workspace
|
||||
|
||||
```bash
|
||||
twenty auth login
|
||||
twenty app sync
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
This application requires the following environment variables to be set:
|
||||
|
||||
- `TWENTY_API_URL`: The Twenty instance API URL where selfHostingUser records will be created
|
||||
- `TWENTY_API_KEY`: API key for authentication (generate at `/settings/api-webhooks`)
|
||||
|
||||
## Features
|
||||
|
||||
### Telemetry Webhook
|
||||
|
||||
@@ -9,26 +9,13 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"auth:login": "twenty auth login",
|
||||
"auth:logout": "twenty auth logout",
|
||||
"auth:status": "twenty auth status",
|
||||
"auth:switch": "twenty auth switch",
|
||||
"auth:list": "twenty auth list",
|
||||
"app:dev": "twenty app dev",
|
||||
"app:sync": "twenty app sync",
|
||||
"entity:add": "twenty entity add",
|
||||
"function:logs": "twenty function logs",
|
||||
"function:execute": "twenty function execute",
|
||||
"app:uninstall": "twenty app uninstall",
|
||||
"help": "twenty help",
|
||||
"twenty": "twenty",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"@types/node": "^24.7.2",
|
||||
"twenty-sdk": "0.6.2"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "a7070f46-3158-4b40-828f-8e6b1febc233"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineApp } from 'twenty-sdk';
|
||||
|
||||
export default defineApp({
|
||||
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
|
||||
displayName: 'Self Hosting',
|
||||
description: 'Used to manage billing and telemetry of self-hosted instances',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
description: 'Twenty API key for creating selfHostingUser records',
|
||||
isSecret: true,
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: 'b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e',
|
||||
description: 'Twenty API URL (e.g., https://api.twenty.com)',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { FieldType, defineObject } from 'twenty-sdk';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
|
||||
nameSingular: 'selfHostingUser',
|
||||
namePlural: 'selfHostingUsers',
|
||||
labelSingular: 'Self Hosting User',
|
||||
labelPlural: 'Self Hosting Users',
|
||||
fields: [
|
||||
{
|
||||
type: FieldType.EMAILS,
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
description: 'The email of the self hosting user',
|
||||
universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
import { defineFunction } from 'twenty-sdk';
|
||||
import { createClient } from '../../generated';
|
||||
|
||||
// TODO: import from twenty-sdk when 0.4.0 is deployed
|
||||
type ServerlessFunctionEvent<TBody = object> = {
|
||||
headers: Record<string, string | undefined>;
|
||||
queryStringParameters: Record<string, string | undefined>;
|
||||
pathParameters: Record<string, string | undefined>;
|
||||
body: TBody | null;
|
||||
isBase64Encoded: boolean;
|
||||
requestContext: {
|
||||
http: {
|
||||
method: string;
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type TelemetryEventPayload = {
|
||||
action: string;
|
||||
timestamp: string;
|
||||
version: string;
|
||||
payload: {
|
||||
userId: string | null;
|
||||
workspaceId: string | null;
|
||||
payload?: {
|
||||
events?: Array<{
|
||||
userId?: string;
|
||||
userEmail?: string;
|
||||
userFirstName?: string;
|
||||
userLastName?: string;
|
||||
locale?: string;
|
||||
serverUrl?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const main = async (
|
||||
params: ServerlessFunctionEvent<TelemetryEventPayload>,
|
||||
): Promise<{ success: boolean; message: string; error?: string }> => {
|
||||
try {
|
||||
const { action, payload } = params.body || {};
|
||||
|
||||
if (action !== 'user_signup') {
|
||||
return {
|
||||
success: true,
|
||||
message: `Event type '${action}' ignored`,
|
||||
};
|
||||
}
|
||||
|
||||
const userEmail =
|
||||
payload?.payload?.events?.[0]?.userEmail ||
|
||||
payload?.payload?.events?.[0]?.userId;
|
||||
|
||||
if (!userEmail) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No email found in telemetry event',
|
||||
error: 'Missing userEmail in payload',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
userEmail.toLowerCase().includes('example') ||
|
||||
userEmail.toLowerCase().includes('test')
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
message: `Email '${userEmail}' ignored (contains test/example data)`,
|
||||
};
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
url: `${process.env.TWENTY_API_URL}/graphql`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Create or update selfHostingUser record
|
||||
const result = await client.mutation({
|
||||
createSelfHostingUser: {
|
||||
__args: {
|
||||
data: {
|
||||
name:
|
||||
payload?.payload?.events?.[0]?.userFirstName +
|
||||
' ' +
|
||||
payload?.payload?.events?.[0]?.userLastName,
|
||||
email: {
|
||||
primaryEmail: userEmail,
|
||||
additionalEmails: null,
|
||||
},
|
||||
},
|
||||
upsert: true,
|
||||
},
|
||||
id: true,
|
||||
email: {
|
||||
primaryEmail: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Self hosting user created/updated: ${result.createSelfHostingUser?.id}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to process telemetry event',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default defineFunction({
|
||||
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
|
||||
name: 'telemetry-webhook',
|
||||
timeoutSeconds: 5,
|
||||
handler: main,
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '7c8e3f5a-9b4c-4d1e-8f2a-1b3c4d5e6f7a',
|
||||
type: 'route',
|
||||
path: '/webhook/telemetry',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
|
||||
displayName: 'Self Hosting',
|
||||
description: 'Used to manage billing and telemetry of self-hosted instances',
|
||||
defaultRoleUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
|
||||
});
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
export const UNIVERSAL_IDENTIFIERS = {
|
||||
objects: {
|
||||
selfHostingUser: {
|
||||
universalIdentifier: '06f3fb53-599e-4c6b-9df6-8f731973afd7',
|
||||
fields: {
|
||||
name: { universalIdentifier: '682cccbf-9f37-4290-a94c-902c771f61e4' },
|
||||
email: { universalIdentifier: 'a4b7892c-431a-4d44-973e-a5481652704f' },
|
||||
personId: {
|
||||
universalIdentifier: 'b453a43c-1512-48ca-8604-db750ad3ffb8',
|
||||
},
|
||||
domain: {
|
||||
universalIdentifier: '1dfa7d4e-c8f5-4639-b58e-3392a8789f76',
|
||||
},
|
||||
userWorkspaceId: {
|
||||
universalIdentifier: '297a7d6b-e407-4b2d-8c03-8964bc1b7805',
|
||||
},
|
||||
userId: {
|
||||
universalIdentifier: '5c7ba3ce-1473-4e3d-8e7c-31816fcb87d8',
|
||||
},
|
||||
locale: {
|
||||
universalIdentifier: '7b39df37-a22e-4f38-ae77-91cf3ee7c076',
|
||||
},
|
||||
serverUrl: {
|
||||
universalIdentifier: 'f2516b77-2912-4cbb-8838-46ac5a5465d9',
|
||||
},
|
||||
serverId: {
|
||||
universalIdentifier: 'e68a2b15-786d-4e9d-a74d-6d6d577ae721',
|
||||
},
|
||||
numberOfEmailsWithSameDomain: {
|
||||
universalIdentifier: '0bf05db0-6771-4400-91ca-1579ec11e76e',
|
||||
},
|
||||
isEnriched: {
|
||||
universalIdentifier: 'fefe9fd6-23ae-4046-b60b-64d17e9ff7ed',
|
||||
},
|
||||
triedToBeEnriched: {
|
||||
universalIdentifier: 'd32c8cc3-8855-453d-bb7d-9c9c0b3f2128',
|
||||
},
|
||||
isPersonalEmail: {
|
||||
universalIdentifier: 'f4568391-9474-4ed8-8cbb-e36d86e0f5f9',
|
||||
},
|
||||
isTwenty: {
|
||||
universalIdentifier: 'b1acef1f-7c10-47a9-899e-aaca45b36e04',
|
||||
},
|
||||
personCity: {
|
||||
universalIdentifier: 'ca733484-e595-4257-9ca9-9a7802fb8bcb',
|
||||
},
|
||||
personCountry: {
|
||||
universalIdentifier: '18c06357-1b50-4d5b-82cf-1f71f286fbe4',
|
||||
},
|
||||
personJobFunction: {
|
||||
universalIdentifier: '26e7e2c7-ea83-41e0-8c07-1fc2549a3fb4',
|
||||
},
|
||||
personJobTitle: {
|
||||
universalIdentifier: '177908e9-1ca6-4762-9518-0df966d3e9fc',
|
||||
},
|
||||
personLinkedIn: {
|
||||
universalIdentifier: '3515683f-7f9f-4b6d-9b16-614824d277b7',
|
||||
},
|
||||
personSeniority: {
|
||||
universalIdentifier: '8b63855a-5915-4d6a-a6ed-ef7d8f8e5dd1',
|
||||
},
|
||||
companyAlexaRank: {
|
||||
universalIdentifier: '7c61335b-cd4b-4eae-8b02-0db746913e36',
|
||||
},
|
||||
companyAnnualRevenue: {
|
||||
universalIdentifier: 'a2367973-aa12-42c2-9577-fe868f61b83b',
|
||||
},
|
||||
companyAnnualRevenuePrinted: {
|
||||
universalIdentifier: 'bc02b6af-8f48-4fde-920d-1fd3e2a8557b',
|
||||
},
|
||||
companyDescription: {
|
||||
universalIdentifier: 'a9bb622e-56b6-42ba-8b03-17a47d707409',
|
||||
},
|
||||
companyEmployees: {
|
||||
universalIdentifier: '8e1dbc58-d444-470f-b8fe-9eed8da4b59e',
|
||||
},
|
||||
companyFoundedYear: {
|
||||
universalIdentifier: '3cf95527-5064-43ab-bf5e-421eb45fac5f',
|
||||
},
|
||||
companyFundingLatestStage: {
|
||||
universalIdentifier: 'a7dcd92a-6811-490b-a8dd-fad1c19091a1',
|
||||
},
|
||||
companyFundingTotalAmount: {
|
||||
universalIdentifier: '6fca8a11-b49a-4081-a7c9-9646f43ad7aa',
|
||||
},
|
||||
companyFundingTotalAmountPrinted: {
|
||||
universalIdentifier: '0078f0f0-2262-4c74-aaf8-4061c6c8a1f3',
|
||||
},
|
||||
companyIndustries: {
|
||||
universalIdentifier: '6b971b9c-6ef5-4497-989e-f9a7c72720cf',
|
||||
},
|
||||
companyIndustry: {
|
||||
universalIdentifier: 'ab84e651-d35b-4e02-8d69-1740af3e22f7',
|
||||
},
|
||||
companyLinkedIn: {
|
||||
universalIdentifier: '4c44b956-f880-434f-b4cd-854b82076e56',
|
||||
},
|
||||
companyName: {
|
||||
universalIdentifier: '1a25412b-f9ce-4406-ac53-f20d1ab8c5ea',
|
||||
},
|
||||
companyTags: {
|
||||
universalIdentifier: 'ceb64d0b-1203-4c6d-af00-39b668f5f891',
|
||||
},
|
||||
companyTech: {
|
||||
universalIdentifier: '11dd57c3-06bb-4722-bb65-96d0a899ca91',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
defaultRole: {
|
||||
universalIdentifier: '66972e19-9fdb-4336-87ce-442a17fd179c',
|
||||
},
|
||||
},
|
||||
views: {
|
||||
selfHostingUserView: {
|
||||
universalIdentifier: 'e903f0ee-52cb-4537-aca8-8940e30b023d',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export const SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER =
|
||||
'9507f244-fdea-47d5-a734-725d4dae43da';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER,
|
||||
name: 'selfHostingUsers',
|
||||
label: 'Self hosting users',
|
||||
description: 'Self hosting user related to the person',
|
||||
type: FieldType.RELATION,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
|
||||
.universalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
isNullable: true,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
defineLogicFunction,
|
||||
type DatabaseEventPayload,
|
||||
type ObjectRecordCreateEvent,
|
||||
type ObjectRecordUpdateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { SELF_HOSTING_USER_NAME_SINGULAR } from 'src/objects/selfHostingUser.object';
|
||||
import { type SelfHostingUser } from 'twenty-sdk/generated/core';
|
||||
import { CoreApiClient } from 'twenty-sdk/generated';
|
||||
|
||||
const handler = async (
|
||||
params: DatabaseEventPayload<
|
||||
| ObjectRecordCreateEvent<SelfHostingUser>
|
||||
| ObjectRecordUpdateEvent<SelfHostingUser>
|
||||
>,
|
||||
) => {
|
||||
const [object, action] = params.name.split('.');
|
||||
|
||||
if (object !== SELF_HOSTING_USER_NAME_SINGULAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['created', 'updated'].includes(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = params.properties.after.email?.primaryEmail;
|
||||
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const { people } = await client.query({
|
||||
people: {
|
||||
edges: { node: { id: true } },
|
||||
__args: {
|
||||
filter: {
|
||||
emails: {
|
||||
primaryEmail: { eq: email },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let personId = people?.edges[0]?.node?.id;
|
||||
|
||||
if (!personId) {
|
||||
const { createPerson } = await client.mutation({
|
||||
createPerson: {
|
||||
__args: {
|
||||
data: {
|
||||
name: {
|
||||
firstName: params.properties.after.name?.firstName,
|
||||
lastName: params.properties.after.name?.lastName,
|
||||
},
|
||||
emails: {
|
||||
primaryEmail: email,
|
||||
},
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
personId = createPerson?.id;
|
||||
}
|
||||
|
||||
await client.mutation({
|
||||
updateSelfHostingUser: {
|
||||
__args: {
|
||||
id: params.properties.after.id,
|
||||
data: {
|
||||
personId,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '87f0293a-997a-4c7b-85e2-e77462ccf0c5',
|
||||
name: 'match-telemetry-event-with-people',
|
||||
description:
|
||||
'Matches self hosting users with existing people based on email address',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: `${SELF_HOSTING_USER_NAME_SINGULAR}.*`,
|
||||
},
|
||||
});
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/generated';
|
||||
import { type TelemetryEvent } from 'src/logic-functions/types/telemetry-event.type';
|
||||
|
||||
export const main = async (
|
||||
params: RoutePayload<TelemetryEvent>,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
error?: string;
|
||||
}> => {
|
||||
try {
|
||||
const {
|
||||
action,
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
userId,
|
||||
userEmail,
|
||||
userFirstName,
|
||||
userLastName,
|
||||
locale,
|
||||
serverUrl,
|
||||
serverId,
|
||||
} = params.body || {};
|
||||
|
||||
if (action !== 'user_signup') {
|
||||
return {
|
||||
success: true,
|
||||
message: `Event type '${action}' ignored`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!userEmail) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'No email found in telemetry event',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
userEmail.toLowerCase().includes('example') ||
|
||||
userEmail.toLowerCase().includes('test')
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
message: `Email '${userEmail}' ignored (contains test/example data)`,
|
||||
};
|
||||
}
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
let existingSelfHostingUserId: string | undefined = undefined;
|
||||
try {
|
||||
const { selfHostingUser: existingSelfHostingUser } = await client.query({
|
||||
selfHostingUser: {
|
||||
__args: {
|
||||
filter: {
|
||||
email: { primaryEmail: { eq: userEmail } },
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
existingSelfHostingUserId = existingSelfHostingUser?.id;
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
|
||||
if (existingSelfHostingUserId) {
|
||||
await client.mutation({
|
||||
updateSelfHostingUser: {
|
||||
__args: {
|
||||
id: existingSelfHostingUserId,
|
||||
data: {
|
||||
name: { firstName: userFirstName, lastName: userLastName },
|
||||
email: { primaryEmail: userEmail, additionalEmails: null },
|
||||
userWorkspaceId,
|
||||
userId,
|
||||
locale,
|
||||
serverUrl,
|
||||
serverId,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Self hosting user ${existingSelfHostingUserId} updated`,
|
||||
};
|
||||
}
|
||||
|
||||
const { createSelfHostingUser } = await client.mutation({
|
||||
createSelfHostingUser: {
|
||||
__args: {
|
||||
data: {
|
||||
name: { firstName: userFirstName, lastName: userLastName },
|
||||
email: { primaryEmail: userEmail, additionalEmails: null },
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
userId,
|
||||
locale,
|
||||
serverUrl,
|
||||
serverId,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Self hosting user ${createSelfHostingUser?.id} created`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to process telemetry event',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '10104201-622b-4a5e-9f27-8f2af19b2a3c',
|
||||
name: 'telemetry-webhook',
|
||||
timeoutSeconds: 10,
|
||||
handler: main,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/webhook/telemetry',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export type TelemetryEvent = {
|
||||
action: string;
|
||||
workspaceId?: string;
|
||||
userWorkspaceId?: string;
|
||||
userId: string;
|
||||
userEmail?: string;
|
||||
userFirstName?: string;
|
||||
userLastName?: string;
|
||||
locale?: string;
|
||||
serverUrl: string;
|
||||
serverId: string;
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'fe3aaca4-9eda-4565-b215-5d268fbf8164',
|
||||
name: 'Self host user',
|
||||
icon: 'IconList',
|
||||
position: 1,
|
||||
viewUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
});
|
||||
@@ -0,0 +1,354 @@
|
||||
import {
|
||||
defineObject,
|
||||
FieldType,
|
||||
RelationType,
|
||||
OnDeleteAction,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
import { SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER } from 'src/fields/self-hosting-user-id';
|
||||
|
||||
export const SELF_HOSTING_USER_NAME_SINGULAR = 'selfHostingUser';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
|
||||
nameSingular: SELF_HOSTING_USER_NAME_SINGULAR,
|
||||
namePlural: 'selfHostingUsers',
|
||||
labelSingular: 'Self Hosting User',
|
||||
labelPlural: 'Self Hosting Users',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
|
||||
.universalIdentifier,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
description: 'Person matching with the self hosting user',
|
||||
type: FieldType.RELATION,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
SELF_HOSTING_USER_ID_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
isNullable: true,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: FieldType.FULL_NAME,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.name
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.EMAILS,
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
description: 'The email of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.email
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.LINKS,
|
||||
name: 'domain',
|
||||
label: 'Domain',
|
||||
description:
|
||||
'Domain extracted from the email address (e.g. domain.com / https://domain.com/)',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.domain
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.UUID,
|
||||
name: 'userWorkspaceId',
|
||||
label: 'User workspace Id',
|
||||
description: 'User workspace id of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userWorkspaceId
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.UUID,
|
||||
name: 'userId',
|
||||
label: 'User Id',
|
||||
description: 'User id of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userId
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'locale',
|
||||
label: 'Locale',
|
||||
description: 'Locale of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.locale
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'serverUrl',
|
||||
label: 'Server url',
|
||||
description: 'Server url of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverUrl
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'serverId',
|
||||
label: 'Server id',
|
||||
description: 'Server id of the self hosting user',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverId
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
name: 'numberOfEmailsWithSameDomain',
|
||||
label: 'Number of Emails with Same Domain',
|
||||
description:
|
||||
'Aggregated count of self hosting users sharing the same business domain',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.numberOfEmailsWithSameDomain.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isEnriched',
|
||||
label: 'Is Enriched',
|
||||
description: 'Whether the record has been enriched',
|
||||
defaultValue: false,
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isEnriched
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'triedToBeEnriched',
|
||||
label: 'Tried to Be Enriched',
|
||||
description: 'Whether an enrichment attempt has been made',
|
||||
defaultValue: false,
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.triedToBeEnriched
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isPersonalEmail',
|
||||
label: 'Is Personal Email',
|
||||
description: 'Whether the email is a personal email address',
|
||||
defaultValue: true,
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isPersonalEmail
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'isTwenty',
|
||||
label: 'Is Twenty',
|
||||
description: 'Whether the user is from Twenty',
|
||||
defaultValue: false,
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isTwenty
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'personCity',
|
||||
label: 'Person City',
|
||||
description: 'City of the person',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCity
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'personCountry',
|
||||
label: 'Person Country',
|
||||
description: 'Country of the person',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCountry
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'personJobFunction',
|
||||
label: 'Person Job Function',
|
||||
description: 'Job function of the person',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobFunction
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'personJobTitle',
|
||||
label: 'Person Job Title',
|
||||
description: 'Job title of the person',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobTitle
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.LINKS,
|
||||
name: 'personLinkedIn',
|
||||
label: 'Person LinkedIn',
|
||||
description: 'LinkedIn profile of the person',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personLinkedIn
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'personSeniority',
|
||||
label: 'Person Seniority',
|
||||
description: 'Seniority level of the person',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personSeniority
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
name: 'companyAlexaRank',
|
||||
label: 'Company Alexa Rank',
|
||||
description: 'Alexa rank of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyAlexaRank
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'companyAnnualRevenue',
|
||||
label: 'Company Annual Revenue',
|
||||
description: 'Annual revenue of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyAnnualRevenue.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyAnnualRevenuePrinted',
|
||||
label: 'Company Annual Revenue Printed',
|
||||
description: 'Formatted annual revenue of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyAnnualRevenuePrinted.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyDescription',
|
||||
label: 'Company Description',
|
||||
description: 'Description of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyDescription
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
name: 'companyEmployees',
|
||||
label: 'Company Employees',
|
||||
description: 'Number of employees at the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyEmployees
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyFoundedYear',
|
||||
label: 'Company Founded Year',
|
||||
description: 'Year the company was founded',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyFoundedYear
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyFundingLatestStage',
|
||||
label: 'Company Funding Latest Stage',
|
||||
description: 'Latest funding stage of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyFundingLatestStage.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.NUMBER,
|
||||
name: 'companyFundingTotalAmount',
|
||||
label: 'Company Funding Total Amount',
|
||||
description: 'Total funding amount of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyFundingTotalAmount.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyFundingTotalAmountPrinted',
|
||||
label: 'Company Funding Total Amount Printed',
|
||||
description: 'Formatted total funding amount of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyFundingTotalAmountPrinted.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyIndustries',
|
||||
label: 'Company Industries',
|
||||
description: 'Industries the company operates in',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustries
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyIndustry',
|
||||
label: 'Company Industry',
|
||||
description: 'Primary industry of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustry
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.LINKS,
|
||||
name: 'companyLinkedIn',
|
||||
label: 'Company LinkedIn',
|
||||
description: 'LinkedIn page of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyLinkedIn
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.TEXT,
|
||||
name: 'companyName',
|
||||
label: 'Company Name',
|
||||
description: 'Name of the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyName
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.ARRAY,
|
||||
name: 'companyTags',
|
||||
label: 'Company Tags',
|
||||
description: 'Tags associated with the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTags
|
||||
.universalIdentifier,
|
||||
},
|
||||
{
|
||||
type: FieldType.ARRAY,
|
||||
name: 'companyTech',
|
||||
label: 'Company Tech',
|
||||
description: 'Technologies used by the company',
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTech
|
||||
.universalIdentifier,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
|
||||
label: 'default role',
|
||||
description: 'Add a description for your role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
@@ -0,0 +1,308 @@
|
||||
import { defineView } from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
name: 'Self hosting users',
|
||||
objectUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.universalIdentifier,
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '243a2401-cd13-440c-8dcd-649e26df36bc',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.name
|
||||
.universalIdentifier,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'dfa75ef8-d40d-416f-9f1c-3e86edfa9fce',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.email
|
||||
.universalIdentifier,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '15cc9215-eb48-4487-a92e-a25d8e99702f',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.domain
|
||||
.universalIdentifier,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '0f9e4f63-3664-443a-9f06-8a6cc04c1d90',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personId
|
||||
.universalIdentifier,
|
||||
position: 2.1,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'dcf88ae8-e71d-452f-b51e-d88cbc6dd273',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userWorkspaceId
|
||||
.universalIdentifier,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'aad70516-936b-41d1-b6c6-961a22299761',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.userId
|
||||
.universalIdentifier,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '8c210eb0-bdda-476e-9f98-42f909872f2a',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.locale
|
||||
.universalIdentifier,
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '367abe85-11c4-440f-80a2-663edd6b4231',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverUrl
|
||||
.universalIdentifier,
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '32c199d6-ebf3-434b-81b4-e2b59a0518b7',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.serverId
|
||||
.universalIdentifier,
|
||||
position: 6.1,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '924ee786-ab93-44be-9d21-941ff9ffe1ac',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.numberOfEmailsWithSameDomain.universalIdentifier,
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2feadf3d-e251-4356-add8-7fa70dea5401',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isEnriched
|
||||
.universalIdentifier,
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'de252ae6-c723-4bf7-96cf-d93f5a539f36',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.triedToBeEnriched
|
||||
.universalIdentifier,
|
||||
position: 9,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'b121e8e6-b3eb-4f6c-b67e-c7c6d19e1bc5',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isPersonalEmail
|
||||
.universalIdentifier,
|
||||
position: 10,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '0ada0bcc-8d6b-4df6-bcc1-78ba14cb04e6',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.isTwenty
|
||||
.universalIdentifier,
|
||||
position: 11,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ec7c8d51-ea63-41bd-9eb1-995835b94218',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCity
|
||||
.universalIdentifier,
|
||||
position: 12,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '7522dd84-0d23-48e7-85dd-f0a8d9e275f8',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personCountry
|
||||
.universalIdentifier,
|
||||
position: 13,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '54191cb9-4d5c-466e-affb-d9ba4adeff87',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobFunction
|
||||
.universalIdentifier,
|
||||
position: 14,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ace75fc7-fb20-4e53-a9a2-6a7529befaf0',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personJobTitle
|
||||
.universalIdentifier,
|
||||
position: 15,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0b42d61-4553-42eb-aca4-327b9bf9f30e',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personLinkedIn
|
||||
.universalIdentifier,
|
||||
position: 16,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '74bc7dd2-fe53-4ff4-8778-2768f3439571',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.personSeniority
|
||||
.universalIdentifier,
|
||||
position: 17,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '61b34f41-8d56-472d-ab1e-414703c6ca12',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyAlexaRank
|
||||
.universalIdentifier,
|
||||
position: 18,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5bb7d36b-6a73-4832-b41e-f67130a4708f',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyAnnualRevenue.universalIdentifier,
|
||||
position: 19,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ae6f23ce-006c-41dd-82a1-e9fe7b65bce3',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyAnnualRevenuePrinted.universalIdentifier,
|
||||
position: 20,
|
||||
isVisible: true,
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'dd2a4728-a743-43bb-b096-9e7bd5125e56',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyDescription
|
||||
.universalIdentifier,
|
||||
position: 21,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ecca02c9-db2e-41e2-b571-b5db75054b56',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyEmployees
|
||||
.universalIdentifier,
|
||||
position: 22,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2e76775b-f8b8-4184-8cd3-72d2b93edaa2',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyFoundedYear
|
||||
.universalIdentifier,
|
||||
position: 23,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a7eb002c-6f0c-48ba-a9eb-247c498ad9bd',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyFundingLatestStage.universalIdentifier,
|
||||
position: 24,
|
||||
isVisible: true,
|
||||
size: 240,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '61be97f6-20da-4b2b-861d-32345e0f9953',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyFundingTotalAmount.universalIdentifier,
|
||||
position: 25,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '55720810-3120-4e76-bcf2-2da9517edbbc',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields
|
||||
.companyFundingTotalAmountPrinted.universalIdentifier,
|
||||
position: 26,
|
||||
isVisible: true,
|
||||
size: 280,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '01e31752-cbc1-499a-8ecf-504dd402d7e2',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustries
|
||||
.universalIdentifier,
|
||||
position: 27,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2e1c8b8b-469b-483e-8348-1fe3d1764e17',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyIndustry
|
||||
.universalIdentifier,
|
||||
position: 28,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '976cc8ae-6cf8-4c30-8da4-5bf61e799893',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyLinkedIn
|
||||
.universalIdentifier,
|
||||
position: 29,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5f0776b3-2849-4b9b-82f0-baa38c6d889d',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyName
|
||||
.universalIdentifier,
|
||||
position: 30,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '86f0397a-2924-4e5c-a610-3c9ad7bb4923',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTags
|
||||
.universalIdentifier,
|
||||
position: 31,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '562084f4-1242-4e60-868b-1d9b268a35b0',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.objects.selfHostingUser.fields.companyTech
|
||||
.universalIdentifier,
|
||||
position: 32,
|
||||
isVisible: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -5,13 +5,14 @@
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
@@ -26,10 +27,5 @@
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### Use StyledComponents
|
||||
|
||||
Style the components with [styled-components](https://emotion.sh/docs/styled).
|
||||
Style the components with [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
|
||||
@@ -136,7 +136,7 @@ That's expected as user is unauthorized when logged out since its identity is no
|
||||
Comment out checker plugin in `packages/twenty-ui/vite-config.ts` like in example below
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### استخدام مكونات منسقة
|
||||
|
||||
قم بتنسيق المكونات باستخدام [styled-components](https://emotion.sh/docs/styled).
|
||||
قم بتنسيق المكونات باستخدام [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ سيء
|
||||
|
||||
@@ -145,7 +145,7 @@ npx nx worker twenty-server
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### Používejte StyledComponents
|
||||
|
||||
Styling komponenty s [styled-components](https://emotion.sh/docs/styled).
|
||||
Stylujte komponenty pomocí [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ Špatné
|
||||
|
||||
@@ -146,7 +146,7 @@ Zakomentujte plugin checker v `packages/twenty-ui/vite-config.ts` jako v příkl
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### Verwenden Sie StyledComponents
|
||||
|
||||
Stylen Sie die Komponenten mit [styled-components](https://emotion.sh/docs/styled).
|
||||
Stylen Sie die Komponenten mit [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ Schlecht
|
||||
|
||||
@@ -145,7 +145,7 @@ Kommentieren Sie das Checker-Plugin in `packages/twenty-ui/vite-config.ts` wie i
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### Usar StyledComponents
|
||||
|
||||
Estiliza los componentes con [styled-components](https://emotion.sh/docs/styled).
|
||||
Estiliza los componentes con [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
|
||||
@@ -145,7 +145,7 @@ Comente el plugin checker en `packages/twenty-ui/vite-config.ts` como en el ejem
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### Utilisez StyledComponents
|
||||
|
||||
Styliser les composants avec [styled-components](https://emotion.sh/docs/styled).
|
||||
Styliser les composants avec [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
|
||||
@@ -145,7 +145,7 @@ Commentez le plugin de vérification dans `packages/twenty-ui/vite-config.ts` co
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### Usa StyledComponents
|
||||
|
||||
Stile i componenti con [styled-components](https://emotion.sh/docs/styled).
|
||||
Stilizza i componenti con [Linaria styled](https://github.com/callstack/linaria).
|
||||
|
||||
```tsx
|
||||
// ❌ Male
|
||||
|
||||
@@ -146,7 +146,7 @@ Commenta il plugin checker in `packages/twenty-ui/vite-config.ts` come mostrato
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ setHotkeyScopeAndMemorizePreviousScope(
|
||||
|
||||
### StyledComponentsを使用する
|
||||
|
||||
コンポーネントを[styled-components](https://emotion.sh/docs/styled)でスタイル設定する。
|
||||
コンポーネントを[Linaria styled](https://github.com/callstack/linaria)でスタイル設定する。
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
|
||||
@@ -152,7 +152,7 @@ npx nx worker twenty-server
|
||||
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
react({ jsxImportSource: 'react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user