Compare commits

..

12 Commits

Author SHA1 Message Date
Thomas Trompette ccdaa31273 WIP¨ 2026-03-20 15:25:35 +01:00
Thomas Trompette f4f7ccd0e1 Improve workflow metrics with faillure reason 2026-03-19 15:30:54 +01:00
dependabot[bot] 0d27a255a9 chore(deps): bump nodemailer from 7.0.11 to 7.0.13 (#18754)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.11
to 7.0.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/releases">nodemailer's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.13</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2>v7.0.12</h2>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md">nodemailer's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13">7.0.13</a>
(2026-01-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>downgrade transient connection error logs to warn level (<a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f">4c041db</a>)</li>
</ul>
<h2><a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12">7.0.12</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)
(<a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84">053ce6a</a>)</li>
<li>use 8bit encoding for message/rfc822 attachments (<a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b">adf8611</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/893119505aa25723dd9a7d90c8dfd223d28a0cc4"><code>8931195</code></a>
chore(master): release 7.0.13 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1798">#1798</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/9398d633d2c1bf5bf08a0e258ff17d5d7a45f0e6"><code>9398d63</code></a>
Bumped deps</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/4c041db85d560e98bc5e1fd5d5a191835c5b7d2f"><code>4c041db</code></a>
fix: downgrade transient connection error logs to warn level</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/a208a0bc86a315d037d5be8849fab5862c488baa"><code>a208a0b</code></a>
chore(master): release 7.0.12 (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1785">#1785</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/053ce6a772a7c608e6bee7f58ebe9900afbd9b84"><code>053ce6a</code></a>
fix: added support for REQUIRETLS (<a
href="https://redirect.github.com/nodemailer/nodemailer/issues/1793">#1793</a>)</li>
<li><a
href="https://github.com/nodemailer/nodemailer/commit/adf86113217b23ff3cd1191af5cd1d360fcc313b"><code>adf8611</code></a>
fix: use 8bit encoding for message/rfc822 attachments</li>
<li>See full diff in <a
href="https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.13">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nodemailer&package-manager=npm_and_yarn&previous-version=7.0.11&new-version=7.0.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-19 09:22:17 +00:00
dependabot[bot] ef2a113a16 chore(deps): bump @dagrejs/dagre from 1.1.3 to 1.1.8 (#18753)
Bumps [@dagrejs/dagre](https://github.com/dagrejs/dagre) from 1.1.3 to
1.1.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dagrejs/dagre/commit/7e4d15f191678f7f05f3c86d9071a193230e7e00"><code>7e4d15f</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/d3908e2c13148c9143db585accc10ae0b6634657"><code>d3908e2</code></a>
Bumping version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/ce295f8e073c4fe96c9e36ecf08ae2940e5e6a10"><code>ce295f8</code></a>
Build for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/b64b9057726eee17f24f73579ba0668527276448"><code>b64b905</code></a>
Bumping the version</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/de169d24c13d06c1e9c560f4f4f8f98650109b94"><code>de169d2</code></a>
Merge pull request <a
href="https://redirect.github.com/dagrejs/dagre/issues/481">#481</a>
from Nathan-Fenner/nf/improve-network-simplex-perform...</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/065e0d8374f4c1c35a7cb4b84df37aaa31598d86"><code>065e0d8</code></a>
improve performance of graph node ranking</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/00d3178d671e49de9c032e3abd281dc9f2739e73"><code>00d3178</code></a>
Typo</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/3982a69d2b323b06aa969a4ec09829d37fe6e7bd"><code>3982a69</code></a>
Bump version and set as pre-release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/1339f5516508dba0cbcc4ef1c0587e7384bec23d"><code>1339f55</code></a>
Building for release</li>
<li><a
href="https://github.com/dagrejs/dagre/commit/9459f01bc815f16b87db727821d8401acbad2cd3"><code>9459f01</code></a>
Bumping the version</li>
<li>Additional commits viewable in <a
href="https://github.com/dagrejs/dagre/compare/v1.1.3...v1.1.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@dagrejs/dagre&package-manager=npm_and_yarn&previous-version=1.1.3&new-version=1.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:11:58 +00:00
dependabot[bot] e0e25eac2b chore(deps): bump @ai-sdk/mistral from 3.0.20 to 3.0.25 (#18755)
Bumps [@ai-sdk/mistral](https://github.com/vercel/ai) from 3.0.20 to
3.0.25.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/ai/commit/4c1613ac8ff9f638da3a25fa6a1aa40362eb0e7c"><code>4c1613a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13131">#13131</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/64ac0fdd80da66d1e920a8a6309ebe05b9d686ad"><code>64ac0fd</code></a>
Backport: fix(security): validate redirect targets in download functions
to p...</li>
<li><a
href="https://github.com/vercel/ai/commit/f622bf85c114f810b00dd2825b56e044bce578a7"><code>f622bf8</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13126">#13126</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/e2a59ef9273ef2ad97c8aec5bf5c0a7b9e8c436c"><code>e2a59ef</code></a>
Backport: fix(provider/google): preserve groundingMetadata when streamed
befo...</li>
<li><a
href="https://github.com/vercel/ai/commit/ebf43a6765e6fa337e8d5b6cf065625a1794c21c"><code>ebf43a6</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13120">#13120</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/258900473214256af40b035b66f15362cbc53287"><code>2589004</code></a>
Backport: feat(openai): add GPT-5.4 model support (<a
href="https://redirect.github.com/vercel/ai/issues/13117">#13117</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/d23121fd71036611e0142a9233de4d1a1f5d54c0"><code>d23121f</code></a>
Backport: chore(ai): add optional ChatRequestOptions to
`addToolApprovalRespo...</li>
<li><a
href="https://github.com/vercel/ai/commit/55a2acf625c78d5c68ac2abc02bc26c3cd0ffa6e"><code>55a2acf</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/13101">#13101</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/45d71c381b756e03310f81bdd322aa7460cb5a15"><code>45d71c3</code></a>
Backport: fix(google): use VALIDATED function calling mode when any tool
has ...</li>
<li><a
href="https://github.com/vercel/ai/commit/ee92dc766422fffde3bae06437510296c4e860a8"><code>ee92dc7</code></a>
ci(release): add <code>--tag ai-v6</code> to <code>ci:release</code>
script (<a
href="https://redirect.github.com/vercel/ai/issues/13096">#13096</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/mistral@3.0.20...@ai-sdk/mistral@3.0.25">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@ai-sdk/mistral&package-manager=npm_and_yarn&previous-version=3.0.20&new-version=3.0.25)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 09:10:58 +00:00
Charles Bochet 8ab8f80687 fix: use unique concurrency group per merge queue entry (#18756)
## Summary

- Fixes merge queue PRs blocking each other by changing the concurrency
group in `ci-merge-queue.yaml`
- The old concurrency group used `merge_group.base_ref` which resolves
to `refs/heads/main` for every PR, causing all merge queue entries to
serialize behind a single concurrency slot
- Now uses `github.ref` (unique per entry:
`refs/heads/gh-readonly-queue/main/pr-NUMBER-SHA`), matching what all
other CI workflows already do

## Recommended ruleset changes (in GitHub Settings > Rules > Rulesets >
"CI Status Checks")

- **Grouping strategy**: Switch `ALLGREEN` to `NONE` -- each PR is still
tested against the correct base (including all PRs ahead of it in the
queue), but failures only affect the failing PR instead of ejecting the
entire group. `max_entries_to_build: 5` still allows parallel
speculative testing.
- **`min_entries_to_merge_wait_minutes`**: Reduce from 5 to 1 -- the
5-minute wait adds unnecessary latency to every merge.

## Test plan

- [ ] Enqueue 2+ PRs in the merge queue and verify both trigger e2e
tests in parallel instead of one blocking the other
2026-03-19 10:10:47 +01:00
github-actions[bot] a370a26b79 i18n - docs translations (#18749)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-19 09:36:37 +01:00
martmull 2f095c8903 Scaffold light twenty app dev container (#18734)
as title
2026-03-18 20:10:54 +01:00
Charles Bochet 394a3cef15 fix: restore ViewFilter value stringification lost in converter removal (#18745)
## Summary

- **Restores `convertViewFilterValueToString()` calls** that were lost
when the converter layer was removed in #18667. The GraphQL
`ViewFilter.value` is typed as `JSON` (can be a string, array, or
object), but the frontend type system expects a `string`. Without
stringification, SELECT/MULTI_SELECT filter values (e.g. `['LOST']`)
reach `arrayOfStringsOrVariablesSchema` as raw arrays, causing a
`ZodError: expected string, received array`.
- **Fixes applied at two data boundary points**: `splitViewWithRelated`
(primary entry from metadata store) and `mapViewFiltersToFilters` (which
also accepts `GqlViewFilter[]` directly).

Fixes a production regression introduced by #18667.

## Test plan

- [ ] Apply a SELECT filter (e.g. filter Opportunities by Stage =
"Lost") — should no longer throw ZodError
- [ ] Apply a MULTI_SELECT filter — should work correctly
- [ ] Verify filters with multiple selected values work (e.g. Stage is
"Lost" or "Won")
- [ ] Verify empty filters and "is not" operands still work
- [ ] Verify filters loaded from saved views still work after page
refresh


Made with [Cursor](https://cursor.com)
2026-03-18 19:06:43 +01:00
github-actions[bot] 189c564840 i18n - translations (#18742)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-18 18:19:45 +01:00
Abdul Rahman 6d4d1a97bc Migrate object permission to syncable entity (#18609)
Closes [#2223](https://github.com/twentyhq/core-team-issues/issues/2223)
2026-03-18 16:32:18 +00:00
BugIsGod 8ccaf635bc fix: replace > button with button selector in styled wrappers (#18568)
fix CSS selector > button not reaching Button component's inner element
### Reproduce
- when you click Data model and create a new field, you will see this
bug.
- When you import record and check the height of remove button in the
final validation step.

### Root Reason
the Button component internally renders a wrapper div around the
<button> element, so > button only reaches the intermediate div, not the
actual button.

### Fix
- padding-right not applied → chevron overlapped with text
- ValidationStep: height: 24px not applied → Remove button was 32px
instead of 24px

<img width="1288" height="376" alt="image"
src="https://github.com/user-attachments/assets/885cd8b0-1fe2-484a-8425-70f52b784ecb"
/>

<img width="1001" height="291" alt="image"
src="https://github.com/user-attachments/assets/0b489478-8fbc-4f7e-886a-38012eeb07ef"
/>
2026-03-18 15:34:33 +00:00
316 changed files with 5697 additions and 11013 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
.git
.env
node_modules
**/node_modules
.nx/cache
packages/twenty-server/.env
+2 -2
View File
@@ -10,8 +10,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
e2e-test:
+27 -12
View File
@@ -25,25 +25,25 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
- Docker (for the local Twenty dev server)
## Quick start
```bash
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Get help and list all available commands
yarn twenty help
# The scaffolder can automatically:
# 1. Start a local Twenty server (Docker)
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
# 3. Authenticate your app via OAuth
# Authenticate with your Twenty server
yarn twenty remote add --local
# Add a new entity to your application (guided)
yarn twenty add
# Or do it manually:
yarn twenty server start # Start local Twenty server
yarn twenty remote add --local # Authenticate via OAuth
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built with the SDK — both available via `twenty-sdk/clients`)
yarn twenty dev
# Watch your application's function logs
@@ -107,10 +107,24 @@ npx create-twenty-app@latest my-app -m
- `skills/example-skill.ts` — Example AI agent skill definition
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## Local server
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker). You can also manage it manually:
```bash
yarn twenty server start # Start (pulls image if needed)
yarn twenty server status # Check if it's healthy
yarn twenty server logs # Stream logs
yarn twenty server stop # Stop (data is preserved)
yarn twenty server reset # Wipe all data and start fresh
```
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace.
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
@@ -153,8 +167,9 @@ Our team reviews contributions for quality, security, and reusability before mer
## Troubleshooting
- Auth prompts not appearing: run `yarn twenty remote add --local` again and verify the API key permissions.
- Types not generated: ensure `yarn twenty dev` is running — it autogenerates the typed client.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add --local`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.8.0-canary.0",
"version": "0.8.0-canary.1",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -1,60 +1,11 @@
This is a [Twenty](https://twenty.com) application project bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
## Getting Started
Start development mode — it auto-connects to your local Twenty server at localhost:3000:
```bash
yarn twenty 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
# Remotes & authentication
yarn twenty remote add --local # Connect to local Twenty server
yarn twenty remote add <url> # Connect to a remote server (OAuth)
yarn twenty remote list # List all configured remotes
yarn twenty remote switch # Switch default remote
yarn twenty remote status # Check auth status
yarn twenty remote remove <name> # Remove a remote
# Development
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty build # Build the application
yarn twenty deploy # Deploy to a Twenty server
yarn twenty publish # Publish to npm
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty exec # Execute a function with JSON payload
yarn twenty logs # Stream function logs
yarn twenty uninstall # Uninstall app from server
```
## Integration Tests
If your project includes the example integration test (`src/__tests__/app-install.integration-test.ts`), you can run it with:
```bash
# Make sure a Twenty server is running at http://localhost:3000
yarn test
```
The test builds and installs the app, then verifies it appears in the applications list. Test configuration (API URL and API key) is defined in `vitest.config.ts`.
## LLMs instructions
Main docs and pitfalls are available in LLMS.md file.
Run `yarn twenty help` to list all available commands.
## 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!
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/capabilities/apps)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)
@@ -46,6 +46,7 @@ export class CreateAppCommand {
await fs.ensureDir(appDirectory);
console.log(chalk.gray(' Scaffolding project files...'));
await copyBaseApplicationProject({
appName,
appDisplayName,
@@ -54,29 +55,20 @@ export class CreateAppCommand {
exampleOptions,
});
console.log(chalk.gray(' Installing dependencies...'));
await install(appDirectory);
console.log(chalk.gray(' Initializing git repository...'));
await tryGitInit(appDirectory);
let localResult: LocalInstanceResult = { running: false };
if (!options.skipLocalInstance) {
const { needsLocalInstance } = await inquirer.prompt([
{
type: 'confirm',
name: 'needsLocalInstance',
message:
'Do you need a local instance of Twenty? Recommended if you not have one already.',
default: true,
},
]);
// Auto-detect a running server first
localResult = await setupLocalInstance(appDirectory);
if (needsLocalInstance) {
localResult = await setupLocalInstance();
}
if (isDefined(localResult.apiKey)) {
this.runAuthLogin(appDirectory, localResult.apiKey);
if (localResult.running && localResult.serverUrl) {
await this.connectToLocal(appDirectory, localResult.serverUrl);
}
}
@@ -201,23 +193,29 @@ export class CreateAppCommand {
appDirectory: string;
appName: string;
}): void {
console.log(chalk.blue('🎯 Creating Twenty Application'));
console.log(chalk.gray(`📁 Directory: ${appDirectory}`));
console.log(chalk.gray(`📝 Name: ${appName}`));
console.log(chalk.blue('Creating Twenty Application'));
console.log(chalk.gray(` Directory: ${appDirectory}`));
console.log(chalk.gray(` Name: ${appName}`));
console.log('');
}
private runAuthLogin(appDirectory: string, apiKey: string): void {
private async connectToLocal(
appDirectory: string,
serverUrl: string,
): Promise<void> {
try {
execSync(
`yarn twenty auth:login --api-key "${apiKey}" --api-url http://localhost:3000`,
{ cwd: appDirectory, stdio: 'inherit' },
`npx nx run twenty-sdk:start -- remote add ${serverUrl} --as local`,
{
cwd: appDirectory,
stdio: 'inherit',
},
);
console.log(chalk.green('Authenticated with local Twenty instance.'));
console.log(chalk.green('Authenticated with local Twenty instance.'));
} catch {
console.log(
chalk.yellow(
'⚠️ Auto auth:login failed. Run `yarn twenty auth:login` manually.',
'Authentication skipped. Run `npx nx run twenty-sdk:start -- remote add --local` manually.',
),
);
}
@@ -229,27 +227,21 @@ export class CreateAppCommand {
): void {
const dirName = appDirectory.split('/').reverse()[0] ?? '';
console.log(chalk.green('Application created!'));
console.log(chalk.green('Application created!'));
console.log('');
console.log(chalk.blue('Next steps:'));
console.log(chalk.gray(` cd ${dirName}`));
if (localResult.apiKey) {
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else if (localResult.running) {
if (!localResult.running) {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
} else {
console.log(
chalk.gray(
' yarn twenty remote add --local # Authenticate with Twenty',
),
);
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
}
console.log(
chalk.gray(' yarn twenty dev # Start dev mode'),
);
}
}
@@ -1,194 +1,101 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { execSync } from 'node:child_process';
import { isDefined } from 'twenty-shared/utils';
import { platform } from 'node:os';
const INSTALL_SCRIPT_URL =
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh';
const DEFAULT_PORT = 2020;
const SERVER_CONTAINER = 'twenty-server-1';
const DB_CONTAINER = 'twenty-db-1';
// Minimal health check — the full implementation lives in twenty-sdk
const isServerReady = async (port: number): Promise<boolean> => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const isDockerAvailable = (): boolean => {
try {
execSync('docker compose version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isDockerRunning = (): boolean => {
try {
execSync('docker info', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isTwentyServerRunning = async (): Promise<boolean> => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch('http://localhost:3000/healthz', {
const response = await fetch(`http://localhost:${port}/healthz`, {
signal: controller.signal,
});
clearTimeout(timeoutId);
const body = await response.json();
return body.status === 'ok';
} catch {
return false;
}
};
const getActiveWorkspaceId = (): string | null => {
try {
const result = execSync(
`docker exec ${DB_CONTAINER} psql -U postgres -d default -t -c "SELECT id FROM core.workspace WHERE \\"activationStatus\\" = 'ACTIVE' LIMIT 1"`,
{ encoding: 'utf-8' },
).trim();
return result || null;
} catch {
return null;
}
};
const generateApiKeyToken = (workspaceId: string): string | null => {
try {
const output = execSync(
`docker exec -e NODE_ENV=development ${SERVER_CONTAINER} yarn command:prod workspace:generate-api-key -w ${workspaceId}`,
{ encoding: 'utf-8' },
);
const TOKEN_PREFIX = 'TOKEN:';
const tokenLine = output
.trim()
.split('\n')
.find((line) => line.includes(TOKEN_PREFIX));
if (!tokenLine) {
return null;
}
const tokenStartIndex =
tokenLine.indexOf(TOKEN_PREFIX) + TOKEN_PREFIX.length;
return tokenLine.slice(tokenStartIndex).trim();
} catch {
return null;
} finally {
clearTimeout(timeoutId);
}
};
export type LocalInstanceResult = {
running: boolean;
apiKey?: string;
serverUrl?: string;
};
export const setupLocalInstance = async (): Promise<LocalInstanceResult> => {
export const setupLocalInstance = async (
appDirectory: string,
): Promise<LocalInstanceResult> => {
console.log('');
console.log(chalk.blue('🐳 Setting up local Twenty instance...'));
console.log(chalk.blue('Setting up local Twenty instance...'));
if (await isTwentyServerRunning()) {
console.log(
chalk.green('✅ Twenty server is already running on localhost:3000.'),
);
} else {
if (!isDockerAvailable()) {
console.log(
chalk.yellow(
'⚠️ Docker Compose is not installed. Please install Docker first.',
),
);
console.log(chalk.gray(' See https://docs.docker.com/get-docker/'));
if (await isServerReady(DEFAULT_PORT)) {
const serverUrl = `http://localhost:${DEFAULT_PORT}`;
return { running: false };
}
console.log(chalk.green(`Twenty server detected on ${serverUrl}.`));
if (!isDockerRunning()) {
console.log(
chalk.yellow(
'⚠️ Docker is not running. Please start Docker and try again.',
),
);
return { running: false };
}
try {
execSync(`bash <(curl -sL ${INSTALL_SCRIPT_URL})`, {
stdio: 'inherit',
shell: '/bin/bash',
});
} catch {
console.log(
chalk.yellow('⚠️ Local instance setup did not complete successfully.'),
);
return { running: false };
}
return { running: true, serverUrl };
}
// Delegate to `twenty server start` from the scaffolded app
console.log(chalk.gray('Starting local Twenty server...'));
try {
execSync('yarn twenty server start', {
cwd: appDirectory,
stdio: 'inherit',
});
} catch {
console.log(
chalk.yellow(
'Failed to start Twenty server. Run `yarn twenty server start` manually.',
),
);
return { running: false };
}
console.log(chalk.gray('Waiting for Twenty to be ready...'));
const startTime = Date.now();
const timeoutMs = 180 * 1000;
while (Date.now() - startTime < timeoutMs) {
if (await isServerReady(DEFAULT_PORT)) {
const serverUrl = `http://localhost:${DEFAULT_PORT}`;
console.log(chalk.green(`Twenty server is running on ${serverUrl}.`));
console.log(
chalk.gray(
'Workspace ready — login with tim@apple.dev / tim@apple.dev',
),
);
const openCommand = platform() === 'darwin' ? 'open' : 'xdg-open';
try {
execSync(`${openCommand} ${serverUrl}`, { stdio: 'ignore' });
} catch {
// Ignore if browser can't be opened
}
return { running: true, serverUrl };
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
console.log('');
console.log(
chalk.blue(
'👉 Please create your workspace in the browser before continuing.',
chalk.yellow(
'Twenty server did not become healthy in time. Check: yarn twenty server logs',
),
);
const { workspaceCreated } = await inquirer.prompt([
{
type: 'confirm',
name: 'workspaceCreated',
message: 'Have you finished creating your workspace?',
default: true,
},
]);
if (!workspaceCreated) {
console.log(
chalk.yellow(
'⚠️ Skipping API key generation. Run `yarn twenty remote add --local` manually after creating your workspace.',
),
);
return { running: true };
}
console.log(chalk.blue('🔑 Generating API key for your workspace...'));
const workspaceId = getActiveWorkspaceId();
if (!isDefined(workspaceId)) {
console.log(
chalk.yellow(
'⚠️ No active workspace found. Make sure you completed the signup flow, then run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
const apiKey = generateApiKeyToken(workspaceId);
if (!isDefined(apiKey)) {
console.log(
chalk.yellow(
'⚠️ Could not generate API key. Run `yarn twenty auth:login` manually.',
),
);
return { running: true };
}
console.log(chalk.green('✅ API key generated for your workspace.'));
return { running: true, apiKey };
return { running: false };
};
@@ -93,7 +93,7 @@ import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
const assertServerIsReachable = async () => {
@@ -1,38 +0,0 @@
# 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
@@ -1 +0,0 @@
24.5.0
@@ -1 +0,0 @@
nodeLinker: node-modules
@@ -1,9 +0,0 @@
## Base documentation
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
@@ -1,51 +0,0 @@
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!
@@ -1,29 +0,0 @@
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
},
},
];
@@ -1,28 +0,0 @@
{
"name": "twentyfortwenty",
"version": "0.1.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty",
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"dependencies": {
"twenty-sdk": "latest",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"eslint": "^9.32.0",
"react": "^18.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0"
}
}
@@ -1,32 +0,0 @@
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
import { defineApplication } from 'twenty-sdk';
export default defineApplication({
universalIdentifier: '0fac8de4-9d11-492b-9e6a-577e11e1c442',
displayName: 'Twenty for Twenty',
description: '',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
applicationVariables: {
CLICKHOUSE_URL: {
universalIdentifier: 'b204304f-d3d3-4ae2-aafa-24b21c159181',
description:
'The URL of the ClickHouse server, including the protocol and port (e.g., http://localhost:8123)',
isSecret: true,
},
CLICKHOUSE_USERNAME: {
universalIdentifier: '711e8ea8-8b19-4cf0-82ab-ab44417312bd',
description: 'The username for authenticating with the ClickHouse server',
isSecret: true,
},
CLICKHOUSE_PASSWORD: {
universalIdentifier: 'faba7ed7-9f94-4202-944b-c25c683e9504',
description: 'The password for authenticating with the ClickHouse server',
isSecret: true,
},
CLICKHOUSE_DATABASE: {
universalIdentifier: '3e6698fa-0c00-49e5-9c4d-34d5b177bff3',
description: 'The name of the ClickHouse database to connect to',
isSecret: true,
},
},
});
@@ -1,37 +0,0 @@
// Field metadata identifiers for cloudUser2 object
// These constants are used by both the object definition and views
export const CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID =
'56e3d6ce-d57e-4ded-830f-958eb18d4d36';
export const CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID =
'1f7062c1-0e96-4599-a59c-6d7427794bb1';
export const CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID =
'e134ce6b-5ab0-4e9c-ba32-36d0851aca21';
export const CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID =
'f305d341-7b12-4939-9299-e5e1b6d1591e';
export const CLOUD_USER_2_EMAIL_FIELD_ID =
'ee110a77-34d7-4c8b-bc10-8560b9e2333a';
export const CLOUD_USER_2_FULL_NAME_FIELD_ID =
'3c9540be-6e42-40f2-8598-973117bbe105';
export const CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID =
'efaa6e54-8019-454c-a556-2e340f0b156d';
export const CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID =
'86a07ca4-420e-4c82-82f8-8f8dfbfc2dc4';
export const CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID =
'87bf8c09-1863-4c65-811f-6fb54a87238e';
export const CLOUD_USER_2_IS_TWENTY_FIELD_ID =
'f6dcd9f3-3f7e-4aea-84a5-41282a68910d';
export const CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID =
'643ab4a2-b6f2-4d11-9173-6859884b8781';
export const CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID =
'294e95cb-13b3-4163-95f8-e4f31837cb47';
export const CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID =
'8dacb4f1-b4ef-42d6-a917-17c78d6273a7';
export const CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID =
'c7741332-d9b6-42ed-84e2-8afa184b58f3';
export const CLOUD_USER_2_UPDATED_BY_FIELD_ID =
'639c9a30-2926-44c3-ab44-100dbda91c64';
export const CLOUD_USER_2_USER_TENURE_FIELD_ID =
'0cc9ca63-c06f-4422-9325-5e99f98d05ed';
export const CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID =
'5af6ae62-b741-471b-90bb-7fb6678fc8c9';
@@ -1,30 +0,0 @@
import {
defineField,
FieldType,
OnDeleteAction,
RelationType,
} from 'twenty-sdk';
import {
CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
} from 'src/fields/cloud-user-workspaces-on-cloud-user.field';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
export default defineField({
universalIdentifier: CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudUser2',
label: 'Cloud User 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'cloudUser2Id',
},
});
@@ -1,11 +0,0 @@
// Field metadata identifiers for cloudUserWorkspace2 object
// These constants are used by both the object definition and views
export const CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID =
'ad149693-0df5-4346-90ff-39e6d945b90f';
export const CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID =
'c5d02b59-ebaf-4604-809c-ce3f6e50dedc';
export const CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID =
'6a0f8018-be3c-49b0-bb91-8d3e6b44adec';
export const CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID =
'29d47f11-1e45-4c67-b495-fcfa45bb67e2';
@@ -1,24 +0,0 @@
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
export const CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID =
'2290d722-d4b1-47ab-a895-4e2c3163a541';
export const CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID =
'49b08cf9-15e1-4583-826f-943fe3c6b0e8';
export default defineField({
universalIdentifier: CLOUD_USER_WORKSPACES_ON_CLOUD_USER_ID,
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudUserWorkspaces2',
label: 'Cloud User Workspaces 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_USER_ON_CLOUD_USER_WORKSPACE_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -1,24 +0,0 @@
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
export const CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID =
'90d9dfc7-7058-4d1c-aac2-1505bd7cb827';
export const CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID =
'7bcd2dda-d1f8-4aa7-a83f-6cf240be80b2';
export default defineField({
universalIdentifier: CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
objectUniversalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudUserWorkspaces2',
label: 'Cloud User Workspaces 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -1,27 +0,0 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
export const CLOUD_USERS_2_ON_PERSON_ID =
'f238e91c-11d8-4a27-a39b-c9fa3515d4a0';
export const PERSON_ON_CLOUD_USER_2_ID = '08d3aff0-7548-4a99-a097-20a0ad2c9ee7';
export default defineField({
universalIdentifier: CLOUD_USERS_2_ON_PERSON_ID,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.RELATION,
name: 'cloudUsers2',
label: 'Cloud Users 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: PERSON_ON_CLOUD_USER_2_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
@@ -1,83 +0,0 @@
// Field metadata identifiers for cloudWorkspace2 object
// These constants are used by both the object definition and views
export const CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID =
'c30799d5-adc0-49be-82a1-6d41b5aa9e82';
export const CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID =
'bfa2f91d-1c6c-48de-bb11-ac2a1e2545a9';
export const CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID =
'f9f2ef3a-8235-4d21-9a45-d29cc2124456';
export const CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID =
'fbeaab89-77e0-4e13-97ed-caeb757a38c7';
export const CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID =
'7d3d4ba3-5af2-4248-afc7-7f4ce7c82805';
export const CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID =
'56e99339-dcc4-4950-ae64-1b6ae326af2c';
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID =
'e9c703e6-9ffc-497d-9755-018724ac8d66';
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID =
'81d8f622-2366-481f-b0e8-b14fb7efa4d5';
export const CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID =
'3e53d606-1afa-4d49-be62-52f4bb1cb49e';
export const CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID =
'3fdd272c-73dd-432e-99c7-782f237a7cd8';
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID =
'1931f3cd-a984-41e3-b8f5-510f67b508e6';
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID =
'1f6a0ef6-3faf-4157-88a7-f624b7655c6b';
export const CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID =
'ab0f46aa-9fef-40c6-a229-6ed4e05867d4';
export const CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID =
'38095859-2efb-4f40-86a1-aa883fd88e3d';
export const CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID =
'4ebd8329-a18c-4606-9ee0-f32fb1c4aa6f';
export const CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID =
'754f137f-b759-47da-8b57-7de71ad34d37';
export const CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID =
'1627e6b8-e87a-4048-b954-8d2fe09b67c8';
export const CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID =
'1d8bb210-b50d-4b38-a611-d78ba865cbae';
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID =
'42879662-d4fc-4806-bcfb-fdb586827491';
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID =
'44b2c12f-aedd-459a-848a-ade9fc4620b7';
export const CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID =
'cac2d3e0-c2ee-4e22-bd96-4fa051a36f73';
export const CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID =
'45cbddfa-54cc-4d94-88d2-db7a7a550414';
export const CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID =
'a6686308-dfcf-4abb-be0f-e8f48cc579d9';
export const CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID =
'0432af7e-7575-486e-a022-f4a625a8ac14';
export const CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID =
'40854f41-4175-4b14-9567-c6bfb3f5d441';
export const CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID =
'44aba339-be2e-4364-a894-cf730081c8a6';
export const CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID =
'f11ddfc2-3356-453a-a6e1-494b6c6b12f6';
export const CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID =
'8fb18f52-44e9-4288-87c1-9650222efb05';
export const CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID =
'8f5dfc54-213b-43b2-a502-631f0f3240d1';
export const CLOUD_WORKSPACE_2_MRR_FIELD_ID =
'67125b6c-ff41-459f-8153-a338ed11c4f0';
export const CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID =
'0c38b297-0e9d-47cc-8f8f-a0e5402413ce';
export const CLOUD_WORKSPACE_2_ARR_FIELD_ID =
'f932f94b-c48b-4ef3-9eea-0c8dad370d89';
export const CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID =
'ae5da0c9-e9ac-401b-ad48-c37a31c6ecd1';
export const CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID =
'ef44daa9-d272-4680-a19c-caf2bac32b70';
export const CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID =
'a88e8cab-333f-44a3-980e-f2cc22871aee';
export const CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID =
'fccc73da-2b6e-434b-921b-e3bcbab1c56c';
export const CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID =
'488b38b4-971e-4c33-b60d-7e7934911579';
export const CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID =
'3815b992-a919-4c52-8629-b4b5f89a6060';
export const CLOUD_WORKSPACE_2_TAGS_FIELD_ID =
'0f41485f-4868-4bd6-8d76-709a6b7a54e3';
export const CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID =
'252cdff1-facb-4f66-8608-6138a585d099';
@@ -1,25 +0,0 @@
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk';
import {
CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
} from 'src/fields/cloud-user-workspaces-on-cloud-workspace.field';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
export default defineField({
universalIdentifier: CLOUD_WORKSPACE_ON_CLOUD_USER_WORKSPACE_ID,
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'cloudWorkspace2',
label: 'Cloud Workspace 2',
relationTargetObjectMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACES_ON_CLOUD_WORKSPACE_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'cloudWorkspace2Id',
},
});
@@ -1,29 +0,0 @@
import {
defineField,
FieldType,
OnDeleteAction,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import {
CLOUD_USERS_2_ON_PERSON_ID,
PERSON_ON_CLOUD_USER_2_ID,
} from 'src/fields/cloud-users-2-on-person.field';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
export default defineField({
universalIdentifier: PERSON_ON_CLOUD_USER_2_ID,
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: CLOUD_USERS_2_ON_PERSON_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
@@ -1,49 +0,0 @@
import { defineLogicFunction } from 'twenty-sdk';
import { syncCloudUserWorkspaces } from 'src/logic-functions/sync-product-data/sync-cloud-user-workspaces';
import { syncCloudUsers } from 'src/logic-functions/sync-product-data/sync-cloud-users';
import { syncCloudWorkspaces } from 'src/logic-functions/sync-product-data/sync-cloud-workspaces';
const handler = async (): Promise<{ message: string }> => {
try {
console.log('Starting product data sync');
const cloudUsersResult = await syncCloudUsers();
console.log(
`Cloud users sync complete: ${cloudUsersResult.syncedCount} users`,
);
const cloudWorkspacesResult = await syncCloudWorkspaces();
console.log(
`Cloud workspaces sync complete: ${cloudWorkspacesResult.syncedCount} workspaces`,
);
const cloudUserWorkspacesResult = await syncCloudUserWorkspaces();
console.log(
`Cloud user workspaces sync complete: ${cloudUserWorkspacesResult.syncedCount} user workspaces`,
);
return {
message: `Product data sync complete — ${cloudUsersResult.syncedCount} users, ${cloudWorkspacesResult.syncedCount} workspaces, ${cloudUserWorkspacesResult.syncedCount} user workspaces`,
};
} catch (err) {
console.log(err);
throw err;
}
};
export default defineLogicFunction({
universalIdentifier: '3897e059-715e-4a4b-b165-c44f17d2e30a',
name: 'sync-product-data',
description:
'Syncs cloud users, cloud workspaces, and cloud user workspaces from ClickHouse',
timeoutSeconds: 120,
handler,
cronTriggerSettings: {
pattern: '*/10 * * * *',
},
});
@@ -1,93 +0,0 @@
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
import { twentyClient } from 'src/shared/twenty-client';
const clickHouseUserWorkspaceSchema = z.object({
id: z.string(),
workspaceId: z.string(),
userId: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string(),
is_active_membership: z.coerce.boolean(),
});
type ClickHouseUserWorkspace = z.infer<typeof clickHouseUserWorkspaceSchema>;
const fetchUserWorkspacesFromClickHouse = async (): Promise<
ClickHouseUserWorkspace[]
> => {
const { clickHouseDatabase } = getApplicationConfig();
// const nowDate = 'now()'
const nowDate = "'2026-02-04 14:28:52.000'";
const query = `
SELECT
*
FROM (
SELECT
*,
row_number() OVER (PARTITION BY id ORDER BY updatedAt DESC) AS rn
FROM
${clickHouseDatabase}.user_workspace
WHERE
updatedAt >= ${nowDate} - INTERVAL 500 MINUTE
AND
updatedAt <= ${nowDate}
)
WHERE
rn = 1
FORMAT
JSONEachRow;
`;
return fetchFromClickHouse(query, clickHouseUserWorkspaceSchema);
};
const buildCloudUserWorkspaceInput = (
userWorkspace: ClickHouseUserWorkspace,
) => ({
id: userWorkspace.id,
twentyUserIdentifier: userWorkspace.userId,
twentyWorkspaceIdentifier: userWorkspace.workspaceId,
idOfTheUserWorkspace: userWorkspace.id,
cloudUser2Id: userWorkspace.userId,
cloudWorkspace2Id: userWorkspace.workspaceId,
});
export const syncCloudUserWorkspaces = async (): Promise<{
syncedCount: number;
}> => {
const userWorkspaces = await fetchUserWorkspacesFromClickHouse();
console.log(
`Fetched ${userWorkspaces.length} user workspaces from ClickHouse`,
);
if (userWorkspaces.length === 0) {
return { syncedCount: 0 };
}
const cloudUserWorkspaceInputs = userWorkspaces.map(
buildCloudUserWorkspaceInput,
);
console.log(
`Batch-upserting ${cloudUserWorkspaceInputs.length} cloud user workspaces`,
);
await twentyClient.mutation({
createCloudUserWorkspaces2: {
__args: {
data: cloudUserWorkspaceInputs,
upsert: true,
},
__scalar: true,
},
});
return { syncedCount: userWorkspaces.length };
};
@@ -1,268 +0,0 @@
import { enumCloudUser2ActivityStatusEnum } from 'twenty-sdk/generated';
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
import { clickHouseDateToIso } from 'src/shared/clickhouse-date-to-iso';
import { twentyClient } from 'src/shared/twenty-client';
const clickHouseUserSchema = z.object({
userId: z.uuid(),
firstName: z.string(),
lastName: z.string(),
email: z.email(),
fullName: z.string(),
isEmailVerified: z.boolean(),
disabled: z.boolean(),
canImpersonate: z.boolean(),
canAccessFullAdminPanel: z.boolean(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string(),
locale: z.string(),
createdDate: z.string(),
workspaceCount: z.coerce.number(),
workspaceIds: z.string(),
workspaceDomains: z.string(),
firstActivityDate: z.string(),
lastActivityDate: z.string(),
lastWorkspaceId: z.string(),
totalPageviews: z.coerce.number(),
pageviewsLast30d: z.coerce.number(),
pageviewsLast7d: z.coerce.number(),
pageviewsLast24h: z.coerce.number(),
userAgeDays: z.coerce.number(),
daysSinceLastActivity: z.coerce.number(),
isActiveLast30d: z.boolean(),
isActiveLast7d: z.boolean(),
isActiveLast24h: z.boolean(),
activityStatus: z
.string()
.transform((val) => val.toUpperCase())
.pipe(z.enum(enumCloudUser2ActivityStatusEnum)),
avgDailyPageviewsLast30d: z.coerce.number(),
isTwenty: z.coerce.boolean(),
maxWorkspaceMembers: z.coerce.number(),
inTrial: z.boolean(),
});
type ClickHouseUser = z.infer<typeof clickHouseUserSchema>;
const fetchUsersFromClickHouse = async (): Promise<ClickHouseUser[]> => {
const { clickHouseDatabase } = getApplicationConfig();
// const nowDate = 'now()'
const nowDate = "'2026-02-04 14:28:52.000'";
const query = `
SELECT
*
FROM
${clickHouseDatabase}.user
WHERE
lastActivityDate >= ${nowDate} - INTERVAL 500 MINUTE
AND
lastActivityDate <= ${nowDate}
FORMAT
JSONEachRow;
`;
return fetchFromClickHouse(query, clickHouseUserSchema);
};
const fetchAllPeopleFromTwentyByEmail = async (emails: string[]) => {
if (emails.length === 0) {
return [];
}
const allPeople = await twentyClient.query({
people: {
edges: {
node: {
id: true,
emails: {
primaryEmail: true,
},
},
},
__args: {
filter: {
emails: {
primaryEmail: {
in: emails,
},
},
},
},
},
});
return allPeople.people?.edges.map((edge) => edge.node) ?? [];
};
const buildCloudUserInput = ({
user,
personId,
}: {
user: ClickHouseUser;
personId: string;
}) => ({
id: user.userId,
name: user.fullName,
email: {
primaryEmail: user.email,
},
personId,
fullName: {
lastName: user.lastName,
firstName: user.firstName,
},
isTwenty: user.isTwenty,
userTenure: user.userAgeDays,
isActiveL7d: user.isActiveLast7d,
isActiveL24h: user.isActiveLast24h,
isActiveL30d: user.isActiveLast30d,
pageViewsL7d: user.pageviewsLast7d,
pageViewsL24h: user.pageviewsLast24h,
pageViewsL30d: user.pageviewsLast30d,
activityStatus: user.activityStatus,
workspaceCount: user.workspaceCount,
lastActivityDate: clickHouseDateToIso(user.lastActivityDate),
dataLastUpdatedAt: new Date().toISOString(),
avgDailyPageviewsLast30d: user.avgDailyPageviewsLast30d,
daysSinceLastActivity: user.daysSinceLastActivity,
});
export const syncCloudUsers = async (): Promise<{
syncedCount: number;
}> => {
const users = await fetchUsersFromClickHouse();
console.log('Fetched users from ClickHouse', users);
const emails = users.map((user) => user.email);
console.log('Fetching people from Twenty with emails', emails);
const people = await fetchAllPeopleFromTwentyByEmail(emails);
console.log('Fetched people from Twenty', people);
// Build a map of email -> personId from existing people
const emailToPersonId = new Map<string, string>();
for (const person of people) {
if (person.emails?.primaryEmail !== undefined) {
emailToPersonId.set(person.emails.primaryEmail.toLowerCase(), person.id);
}
}
// Partition users into those with/without an existing person
const usersWithoutPerson = users.filter(
(user) => !emailToPersonId.has(user.email.toLowerCase()),
);
// Step 1: Batch-create missing people
const newlyCreatedPersonIds: string[] = [];
if (usersWithoutPerson.length > 0) {
console.log(`Batch-creating ${usersWithoutPerson.length} people records`);
const createPeopleResult = await twentyClient.mutation({
createPeople: {
__args: {
data: usersWithoutPerson.map((user) => ({
name: {
firstName: user.firstName,
lastName: user.lastName,
},
emails: {
primaryEmail: user.email,
},
})),
},
id: true,
emails: {
primaryEmail: true,
},
},
});
const createdPeople = createPeopleResult.createPeople ?? [];
for (const person of createdPeople) {
if (
person.emails?.primaryEmail === undefined ||
person.id === undefined
) {
continue;
}
newlyCreatedPersonIds.push(person.id);
emailToPersonId.set(person.emails.primaryEmail.toLowerCase(), person.id);
}
}
// Step 2: Batch-upsert all cloud users, with rollback on failure
const cloudUserInputs = users.map((user) => {
const personId = emailToPersonId.get(user.email.toLowerCase());
if (personId === undefined) {
throw new Error(
`No personId found for user ${user.email} — this should not happen`,
);
}
return buildCloudUserInput({ user, personId });
});
try {
console.log(`Batch-upserting ${cloudUserInputs.length} cloud users`);
await twentyClient.mutation({
createCloudUsers2: {
__args: {
data: cloudUserInputs,
upsert: true,
},
__scalar: true,
},
});
} catch (cloudUserError) {
console.log(
'Cloud user upsert failed, rolling back newly created people',
cloudUserError,
);
// Rollback: hard-delete people that were created in step 1
if (newlyCreatedPersonIds.length > 0) {
try {
await twentyClient.mutation({
destroyPeople: {
__args: {
filter: {
id: {
in: newlyCreatedPersonIds,
},
},
},
id: true,
},
});
console.log(
`Rolled back ${newlyCreatedPersonIds.length} newly created people`,
);
} catch (rollbackError) {
console.log(
'Rollback of newly created people also failed',
rollbackError,
);
}
}
throw cloudUserError;
}
return { syncedCount: users.length };
};
@@ -1,201 +0,0 @@
import {
enumCloudWorkspace2ActivationStatusEnum,
enumCloudWorkspace2PaymentFrequencyEnum,
enumCloudWorkspace2SubscriptionStatusEnum,
} from 'twenty-sdk/generated';
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
import { fetchFromClickHouse } from 'src/shared/clickhouse-client';
import { clickHouseDateToIso } from 'src/shared/clickhouse-date-to-iso';
import { twentyClient } from 'src/shared/twenty-client';
const clickHouseWorkspaceSchema = z.object({
workspaceId: z.string(),
workspaceName: z.string(),
subdomain: z.string(),
customDomain: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
deletedAt: z.string(),
activationStatus: z
.string()
.transform((val) => {
const upper = val.toUpperCase();
if (upper === '') return 'EMPTY';
const valid: string[] = Object.values(
enumCloudWorkspace2ActivationStatusEnum,
);
return valid.includes(upper) ? upper : 'EMPTY';
})
.pipe(z.enum(enumCloudWorkspace2ActivationStatusEnum)),
createdDate: z.string(),
lastPageviewDate: z.string(),
pageviewsLast30d: z.coerce.number(),
pageviewsLast7d: z.coerce.number(),
pageviewsLast24h: z.coerce.number(),
totalEverActiveUsers: z.coerce.number(),
totalWorkspaceUsers: z.coerce.number(),
activeUsersLast30d: z.coerce.number(),
activeUsersLast7d: z.coerce.number(),
activeUsersLast24h: z.coerce.number(),
isActiveLast30d: z.coerce.boolean(),
isActiveLast7d: z.coerce.boolean(),
isActiveLast24h: z.coerce.boolean(),
activeUserRatioLast30d: z.coerce.number(),
activeUserRatioLast7d: z.coerce.number(),
workspaceAgeDays: z.coerce.number(),
totalEvents: z.coerce.number(),
eventsLast30d: z.coerce.number(),
eventsPerUser: z.coerce.number(),
subscription_status: z
.string()
.transform((val) => {
const upper = val.toUpperCase();
if (upper === '') return 'EMPTY';
const valid: string[] = Object.values(
enumCloudWorkspace2SubscriptionStatusEnum,
);
return valid.includes(upper) ? upper : 'OTHER';
})
.pipe(z.enum(enumCloudWorkspace2SubscriptionStatusEnum)),
payment_frequency: z
.string()
.transform((val) => {
const upper = val.toUpperCase();
if (upper === '') return 'EMPTY';
const valid: string[] = Object.values(
enumCloudWorkspace2PaymentFrequencyEnum,
);
return valid.includes(upper) ? upper : 'OTHER';
})
.pipe(z.enum(enumCloudWorkspace2PaymentFrequencyEnum)),
trial_status: z.string(),
mrr: z.coerce.number(),
potential_arr: z.coerce.number(),
arr: z.coerce.number().nullable(),
next_renewal_date: z.string(),
workspace_domain: z.string(),
domain_source: z.string(),
creatorUserId: z.string(),
creatorEmail: z.string(),
creator_domain_type: z.string(),
primary_business_domain: z.string(),
business_domain_user_count: z.coerce.number(),
isTwenty: z.coerce.boolean(),
});
type ClickHouseWorkspace = z.infer<typeof clickHouseWorkspaceSchema>;
const fetchWorkspacesFromClickHouse =
async (): Promise<ClickHouseWorkspace[]> => {
const { clickHouseDatabase } = getApplicationConfig();
// const nowDate = 'now()'
const nowDate = "'2026-02-04 14:28:52.000'";
const query = `
SELECT
*
FROM (
SELECT
*,
row_number() OVER (PARTITION BY workspaceId ORDER BY updatedAt DESC) AS rn
FROM
${clickHouseDatabase}.workspace
WHERE
updatedAt >= ${nowDate} - INTERVAL 500 MINUTE
AND
updatedAt <= ${nowDate}
)
WHERE
rn = 1
FORMAT
JSONEachRow;
`;
return fetchFromClickHouse(query, clickHouseWorkspaceSchema);
};
// Convert a dollar amount to micros (1 USD = 1_000_000 micros).
const dollarsToAmountMicros = (dollars: number) =>
Math.round(dollars * 1_000_000);
const buildCloudWorkspaceInput = (workspace: ClickHouseWorkspace) => ({
id: workspace.workspaceId,
name: workspace.workspaceName,
subDomain: workspace.subdomain,
customDomain: {
primaryLinkUrl: workspace.customDomain,
primaryLinkLabel: '',
},
activationStatus: workspace.activationStatus,
subscriptionStatus: workspace.subscription_status,
paymentFrequency: workspace.payment_frequency,
lastPageViewDate: workspace.lastPageviewDate,
pageViewsL30D: workspace.pageviewsLast30d,
pageViewsL7D: workspace.pageviewsLast7d,
pageViewsL24H: workspace.pageviewsLast24h,
totalEverActiveWorkspaceUsers: workspace.totalEverActiveUsers,
totalWorkspaceUsers: workspace.totalWorkspaceUsers,
activeUsersL30D: workspace.activeUsersLast30d,
activeUsersL7D: workspace.activeUsersLast7d,
activeUsersL24H: workspace.activeUsersLast24h,
isActiveL30D: workspace.isActiveLast30d,
isActiveL7D: workspace.isActiveLast7d,
isActiveL24H: workspace.isActiveLast24h,
workspaceTenure: workspace.workspaceAgeDays,
numberOfEventsTotal: workspace.totalEvents,
numberOfEventsL30D: workspace.eventsLast30d,
mrr: {
amountMicros: dollarsToAmountMicros(workspace.mrr),
currencyCode: 'USD',
},
potentialArr: {
amountMicros: dollarsToAmountMicros(workspace.potential_arr),
currencyCode: 'USD',
},
arr: {
amountMicros: dollarsToAmountMicros(workspace.arr ?? 0),
currencyCode: 'USD',
},
nextRenewalDate: clickHouseDateToIso(workspace.next_renewal_date),
creatorEmail: {
primaryEmail: workspace.creatorEmail,
},
workspaceBusinessDomain: {
primaryLinkUrl: workspace.primary_business_domain,
primaryLinkLabel: '',
},
dataLastUpdatedAt: new Date().toISOString(),
});
export const syncCloudWorkspaces = async (): Promise<{
syncedCount: number;
}> => {
const workspaces = await fetchWorkspacesFromClickHouse();
console.log(`Fetched ${workspaces.length} workspaces from ClickHouse`);
if (workspaces.length === 0) {
return { syncedCount: 0 };
}
const cloudWorkspaceInputs = workspaces.map(buildCloudWorkspaceInput);
console.log(
`Batch-upserting ${cloudWorkspaceInputs.length} cloud workspaces`,
);
await twentyClient.mutation({
createCloudWorkspaces2: {
__args: {
data: cloudWorkspaceInputs,
upsert: true,
},
__scalar: true,
},
});
return { syncedCount: workspaces.length };
};
@@ -1,13 +0,0 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { ALL_CLOUD_USER_2_VIEW_ID } from 'src/views/all-cloud-user-2';
export default defineNavigationMenuItem({
universalIdentifier: 'ac6df084-0f0f-404a-b3d1-b084cb624d76',
type: NavigationMenuItemType.VIEW,
name: 'cloud-user-2',
icon: 'IconList',
position: 0,
viewUniversalIdentifier: ALL_CLOUD_USER_2_VIEW_ID,
});
@@ -1,13 +0,0 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID } from 'src/views/all-cloud-user-workspace-2';
export default defineNavigationMenuItem({
universalIdentifier: 'e29251f0-78ad-493f-8f99-ec54ab0b5378',
type: NavigationMenuItemType.VIEW,
name: 'cloud-user-workspace-2',
icon: 'IconList',
position: 0,
viewUniversalIdentifier: ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID,
});
@@ -1,13 +0,0 @@
import { defineNavigationMenuItem } from 'twenty-sdk';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { ALL_CLOUD_WORKSPACE_2_VIEW_ID } from 'src/views/all-cloud-workspace-2';
export default defineNavigationMenuItem({
universalIdentifier: '0ba6315e-0d79-47a5-8c05-29dec621a647',
type: NavigationMenuItemType.VIEW,
name: 'cloud-workspace-2',
icon: 'IconList',
position: 0,
viewUniversalIdentifier: ALL_CLOUD_WORKSPACE_2_VIEW_ID,
});
@@ -1,171 +0,0 @@
import { defineObject, FieldType } from 'twenty-sdk';
import {
CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
CLOUD_USER_2_EMAIL_FIELD_ID,
CLOUD_USER_2_FULL_NAME_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_USER_2_IS_TWENTY_FIELD_ID,
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_USER_2_UPDATED_BY_FIELD_ID,
CLOUD_USER_2_USER_TENURE_FIELD_ID,
CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
} from 'src/fields/cloud-user-2-field-ids';
export const CLOUD_USER_2_UNIVERSAL_IDENTIFIER =
'da264c72-df22-49b3-98e3-21cf6013e671';
export default defineObject({
universalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
nameSingular: 'cloudUser2',
namePlural: 'cloudUsers2',
labelSingular: 'Cloud user 2',
labelPlural: 'Cloud users 2',
icon: 'IconBox',
fields: [
{
universalIdentifier: CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
type: FieldType.SELECT,
name: 'activityStatus',
label: 'Activity Status',
options: [
{
id: '141d21f3-71b0-4b37-a3d2-5d91d43d0493',
value: 'ACTIVE',
label: 'Active',
position: 0,
color: 'green',
},
{
id: 'adb1d290-750f-468e-99ee-ed892bcb8974',
value: 'RECENT',
label: 'Recent',
position: 1,
color: 'blue',
},
{
id: '96f24b75-a671-4727-a78f-5a72419370ea',
value: 'DORMANT',
label: 'Dormant',
position: 2,
color: 'orange',
},
{
id: '88ddbef9-16a8-4643-9726-5b271cd477fa',
value: 'INACTIVE',
label: 'Inactive',
position: 3,
color: 'blue',
},
],
},
{
universalIdentifier: CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'avgDailyPageviewsLast30d',
label: 'Avg Daily Page Views',
},
{
universalIdentifier: CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
type: FieldType.DATE_TIME,
name: 'dataLastUpdatedAt',
label: 'Data last updated at',
},
{
universalIdentifier: CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
type: FieldType.NUMBER,
name: 'daysSinceLastActivity',
label: 'Days Since Last Activity',
},
{
universalIdentifier: CLOUD_USER_2_EMAIL_FIELD_ID,
type: FieldType.EMAILS,
name: 'email',
label: 'Email',
},
{
universalIdentifier: CLOUD_USER_2_FULL_NAME_FIELD_ID,
type: FieldType.FULL_NAME,
name: 'fullName',
label: 'Full Name',
},
{
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL24h',
label: 'Is Active L24H',
defaultValue: true,
},
{
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL30d',
label: 'Is Active L30D',
defaultValue: false,
},
{
universalIdentifier: CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL7d',
label: 'Is Active L7D',
defaultValue: true,
},
{
universalIdentifier: CLOUD_USER_2_IS_TWENTY_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isTwenty',
label: 'Is Twenty',
defaultValue: false,
},
{
universalIdentifier: CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
type: FieldType.DATE_TIME,
name: 'lastActivityDate',
label: 'Last Activity Date',
},
{
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL24h',
label: 'Page Views L24H',
},
{
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL30d',
label: 'Page Views L30D',
},
{
universalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL7d',
label: 'Page Views L7D',
},
{
universalIdentifier: CLOUD_USER_2_UPDATED_BY_FIELD_ID,
type: FieldType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
},
{
universalIdentifier: CLOUD_USER_2_USER_TENURE_FIELD_ID,
type: FieldType.NUMBER,
name: 'userTenure',
label: 'User tenure',
},
{
universalIdentifier: CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
type: FieldType.NUMBER,
name: 'workspaceCount',
label: 'Workspace count',
},
],
});
@@ -1,70 +0,0 @@
import { defineObject, FieldType } from 'twenty-sdk';
import {
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
} from 'src/fields/cloud-user-workspace-2-field-ids';
export const CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER =
'14d6e1f4-c513-4766-9210-bc5dc8294e51';
export default defineObject({
universalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
nameSingular: 'cloudUserWorkspace2',
namePlural: 'cloudUserWorkspaces2',
labelSingular: 'Cloud user workspace 2',
labelPlural: 'Cloud user workspaces 2',
icon: 'IconBox',
fields: [
{
universalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
type: FieldType.TEXT,
name: 'twentyUserIdentifier',
label: 'Twenty User Identifier',
icon: 'IconMan',
isNullable: true,
defaultValue: null,
universalSettings: {
displayedMaxRows: 0,
},
},
{
universalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
type: FieldType.TEXT,
name: 'twentyWorkspaceIdentifier',
label: 'Twenty Workspace Identifier',
icon: 'IconScreenShare',
isNullable: true,
defaultValue: null,
universalSettings: {
displayedMaxRows: 0,
},
},
{
universalIdentifier:
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
type: FieldType.TEXT,
name: 'idOfTheUserWorkspace',
label: 'Id of the user workspace',
icon: 'IconTypography',
isNullable: true,
defaultValue: null,
universalSettings: {
displayedMaxRows: 0,
},
},
{
universalIdentifier: CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
type: FieldType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
description: 'The user who last updated the record',
icon: 'IconUserCircle',
isNullable: true,
},
],
});
@@ -1,582 +0,0 @@
import { defineObject, FieldType } from 'twenty-sdk';
import {
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
CLOUD_WORKSPACE_2_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
CLOUD_WORKSPACE_2_MRR_FIELD_ID,
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
} from 'src/fields/cloud-workspace-2-field-ids';
export const CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER =
'77376ed3-19c1-4859-bde5-90f19ad02113';
export default defineObject({
universalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
nameSingular: 'cloudWorkspace2',
namePlural: 'cloudWorkspaces2',
labelSingular: 'Cloud workspace 2',
labelPlural: 'Cloud workspaces 2',
icon: 'IconBox',
fields: [
{
universalIdentifier: CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
type: FieldType.LINKS,
name: 'customDomain',
label: 'Custom domain',
description: 'Custom domain set up by the customer to use their own.',
icon: 'IconLink',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
type: FieldType.LINKS,
name: 'workspaceBusinessDomain',
label: 'Workspace Business Domain',
icon: 'IconLink',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
type: FieldType.LINKS,
name: 'companyLinkedin',
label: 'Company LinkedIn',
icon: 'IconBrandLinkedin',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
type: FieldType.DATE,
name: 'dataLastUpdatedAt',
label: 'Data last updated at',
description: 'by workflow',
icon: 'IconCalendarEvent',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
type: FieldType.DATE,
name: 'lastPageViewDate',
label: 'Last Page View Date',
icon: 'IconCalendarEvent',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
type: FieldType.NUMBER,
name: 'totalEverActiveWorkspaceUsers',
label: 'Total ever active workspace users',
icon: 'IconUsers',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'activeUsersL30D',
label: 'Active Users L30D',
icon: 'IconUsersGroup',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
type: FieldType.NUMBER,
name: 'activeUsersL7D',
label: 'Active Users L7D',
icon: 'IconUsersGroup',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
type: FieldType.NUMBER,
name: 'activeUsersL24H',
label: 'Active Users L24H',
icon: 'IconUsersGroup',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
type: FieldType.NUMBER,
name: 'workspaceTenure',
label: 'Workspace tenure',
description: 'in days',
icon: 'IconCalendarHeart',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL30D',
label: 'Page Views L30D',
icon: 'IconClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL7D',
label: 'Page Views L7D',
icon: 'IconClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
type: FieldType.NUMBER,
name: 'pageViewsL24H',
label: 'Page Views L24H',
icon: 'IconClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
type: FieldType.NUMBER,
name: 'totalWorkspaceUsers',
label: 'Total workspace users',
icon: 'IconUsers',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
type: FieldType.NUMBER,
name: 'numberOfEventsL30D',
label: 'Number of events L30D',
icon: 'IconHandClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
type: FieldType.NUMBER,
name: 'numberOfEventsTotal',
label: 'Number of events total',
icon: 'IconHandClick',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
type: FieldType.NUMBER,
name: 'alexaRank',
label: 'Alexa Rank',
icon: 'IconNumber9',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
type: FieldType.NUMBER,
name: 'employees',
label: 'Employees',
icon: 'IconUsers',
isNullable: true,
defaultValue: null,
universalSettings: {
type: 'number',
decimals: 0,
},
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL7D',
label: 'Is Active L7D',
icon: 'IconActivity',
isNullable: true,
defaultValue: false,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL24H',
label: 'Is Active L24H',
icon: 'IconActivity',
isNullable: true,
defaultValue: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isActiveL30D',
label: 'Is Active L30D',
icon: 'IconActivity',
isNullable: true,
defaultValue: false,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
type: FieldType.BOOLEAN,
name: 'isEnriched',
label: 'Is Enriched',
icon: 'IconToggleLeft',
isNullable: true,
defaultValue: false,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
type: FieldType.TEXT,
name: 'industry',
label: 'Industry',
icon: 'IconWorldLongitude',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
type: FieldType.TEXT,
name: 'companyFoundedYear',
label: 'Company Founded Year',
icon: 'IconCalendarPlus',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
type: FieldType.TEXT,
name: 'description',
label: 'Description',
icon: 'IconBaselineDensitySmall',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
type: FieldType.TEXT,
name: 'latestFundingStage',
label: 'Latest Funding Stage',
icon: 'IconBellDollar',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
type: FieldType.TEXT,
name: 'companyName',
label: 'Company Name',
icon: 'IconTypography',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
type: FieldType.TEXT,
name: 'subDomain',
label: 'Sub domain',
description:
'This is the subdomain chosen by customers to access their workspace',
icon: 'IconLink',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
type: FieldType.CURRENCY,
name: 'annualRevenue',
label: 'Annual Revenue',
icon: 'IconCurrencyDollar',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_MRR_FIELD_ID,
type: FieldType.CURRENCY,
name: 'mrr',
label: 'MRR',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
type: FieldType.CURRENCY,
name: 'potentialArr',
label: 'Potential ARR',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ARR_FIELD_ID,
type: FieldType.CURRENCY,
name: 'arr',
label: 'ARR',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
type: FieldType.CURRENCY,
name: 'totalFunding',
label: 'Total Funding',
icon: 'IconMoneybag',
isNullable: true,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
type: FieldType.SELECT,
name: 'activationStatus',
label: 'Activation Status',
icon: 'IconTag',
isNullable: true,
defaultValue: null,
options: [
{
id: 'c41ee49c-e629-431e-8601-183726f93315',
color: 'yellow',
label: 'Pending creation',
value: 'PENDING_CREATION',
position: 0,
},
{
id: 'cb82ad14-5fcb-4ed0-af58-c92f84b45d2b',
color: 'gray',
label: 'Suspended',
value: 'SUSPENDED',
position: 1,
},
{
id: 'b1084bc8-0efd-44ce-974c-7fb44c310be5',
color: 'green',
label: 'Active',
value: 'ACTIVE',
position: 2,
},
{
id: 'b5b33c8e-eb3f-4758-99c9-d2f39c011b96',
color: 'blue',
label: 'Ongoing creation',
value: 'ONGOING_CREATION',
position: 3,
},
{
id: '2ae41a69-c521-4545-9e0b-f1f941ceee83',
color: 'purple',
label: 'Empty',
value: 'EMPTY',
position: 4,
},
],
},
{
universalIdentifier: CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
type: FieldType.SELECT,
name: 'subscriptionStatus',
label: 'Subscription Status',
icon: 'IconTag',
isNullable: true,
defaultValue: null,
options: [
{
id: 'b5ee9a91-0a15-4cbe-a36e-198f5bae431d',
color: 'green',
label: 'Active',
value: 'ACTIVE',
position: 0,
},
{
id: '78837243-38d9-44d4-8b9c-a6c0ca082ba8',
color: 'blue',
label: 'Trialing',
value: 'TRIALING',
position: 1,
},
{
id: '6b27ee33-3c30-4694-a607-19264765b11c',
color: 'gray',
label: 'Canceled',
value: 'CANCELED',
position: 2,
},
{
id: '05118231-18b6-4589-be23-8f1368c0182f',
color: 'yellow',
label: 'Other',
value: 'OTHER',
position: 3,
},
{
id: 'ea83a7ff-4fe0-4c56-88f8-cf0bdf76cb08',
color: 'gray',
label: 'Empty',
value: 'EMPTY',
position: 4,
},
],
},
{
universalIdentifier: CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
type: FieldType.SELECT,
name: 'paymentFrequency',
label: 'Payment Frequency',
icon: 'IconCurrencyDollar',
isNullable: true,
defaultValue: null,
options: [
{
id: 'fa97d4fb-12bf-4cf4-b252-14b1c657d3b6',
color: 'sky',
label: 'Month',
value: 'MONTH',
position: 0,
},
{
id: 'c09c3f1d-31e7-4b29-969a-a4304f04abe0',
color: 'purple',
label: 'Year',
value: 'YEAR',
position: 1,
},
{
id: 'a91d77ed-90ad-41fc-83ae-e688c981abb2',
color: 'gray',
label: 'Other',
value: 'OTHER',
position: 2,
},
{
id: '5c595203-1b33-430b-abd8-8bc80aeedbd7',
color: 'red',
label: 'Empty',
value: 'EMPTY',
position: 3,
},
],
},
{
universalIdentifier: CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
type: FieldType.DATE_TIME,
name: 'nextRenewalDate',
label: 'Next Renewal Date',
icon: 'IconCalendarClock',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
type: FieldType.EMAILS,
name: 'creatorEmail',
label: 'Creator Email',
icon: 'IconMail',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
type: FieldType.ARRAY,
name: 'tags',
label: 'Tags',
icon: 'IconBracketsContain',
isNullable: true,
defaultValue: null,
},
{
universalIdentifier: CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
type: FieldType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
description: 'The user who last updated the record',
icon: 'IconUserCircle',
isNullable: true,
},
],
});
@@ -1,14 +0,0 @@
import { defineRole } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'4bd740cc-f6a1-40c5-b46b-e2a4dbf5af04';
export default defineRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Twenty for Twenty default function role',
description: 'Twenty for Twenty default function role',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
});
@@ -1,25 +0,0 @@
import { z } from 'zod';
const applicationConfigSchema = z
.object({
CLICKHOUSE_DATABASE: z.string().nonempty(),
CLICKHOUSE_URL: z.url(),
CLICKHOUSE_USERNAME: z.string().nonempty(),
CLICKHOUSE_PASSWORD: z.string().nonempty(),
});
export const getApplicationConfig = () => {
const env = applicationConfigSchema.parse({
CLICKHOUSE_DATABASE: process.env.CLICKHOUSE_DATABASE,
CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,
CLICKHOUSE_USERNAME: process.env.CLICKHOUSE_USERNAME,
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
});
return {
clickHouseDatabase: env.CLICKHOUSE_DATABASE,
clickHouseUrl: env.CLICKHOUSE_URL,
clickHouseUsername: env.CLICKHOUSE_USERNAME,
clickHousePassword: env.CLICKHOUSE_PASSWORD,
};
};
@@ -1,43 +0,0 @@
import { z } from 'zod';
import { getApplicationConfig } from 'src/shared/application-config';
// Fetches rows from ClickHouse using a raw SQL query,
// parses the JSONEachRow response, and validates each row against the provided Zod schema.
export const fetchFromClickHouse = async <T>(
query: string,
schema: z.ZodType<T>,
): Promise<T[]> => {
const { clickHouseUrl, clickHouseUsername, clickHousePassword } =
getApplicationConfig();
const response = await fetch(clickHouseUrl, {
method: 'POST',
headers: {
Authorization:
'Basic ' +
Buffer.from(`${clickHouseUsername}:${clickHousePassword}`).toString(
'base64',
),
'Content-Type': 'text/plain',
},
body: query,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`ClickHouse error: ${response.status} - ${errorText}`);
}
// Format is a list of JSON objects, one per line (JSONEachRow format).
const text = await response.text();
const rows = text
.trim()
.split('\n')
.filter((line) => line.trim())
.map((line) => JSON.parse(line));
return z.array(schema).parse(rows);
};
@@ -1,16 +0,0 @@
// ClickHouse returns DateTime64 values in the format 'YYYY-MM-DD HH:mm:ss.SSSSSS'
// (space-separated, up to 6 fractional digits). Twenty's DATE_TIME fields expect
// ISO 8601 format ('YYYY-MM-DDTHH:mm:ss.SSSZ', at most 3 fractional digits).
//
// Additionally, ClickHouse uses '1970-01-01 00:00:00.000000' as a sentinel for
// null/empty dates. We map that to null.
const CLICKHOUSE_EPOCH_SENTINEL = '1970-01-01 00:00:00.000000';
export const clickHouseDateToIso = (value: string): string | null => {
if (!value || value === CLICKHOUSE_EPOCH_SENTINEL) {
return null;
}
return new Date(value.replace(' ', 'T') + 'Z').toISOString();
};
@@ -1,3 +0,0 @@
import Twenty from 'twenty-sdk/generated';
export const twentyClient = new Twenty();
@@ -1,159 +0,0 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
CLOUD_USER_2_EMAIL_FIELD_ID,
CLOUD_USER_2_FULL_NAME_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_USER_2_IS_TWENTY_FIELD_ID,
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_USER_2_UPDATED_BY_FIELD_ID,
CLOUD_USER_2_USER_TENURE_FIELD_ID,
CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
} from 'src/fields/cloud-user-2-field-ids';
import { CLOUD_USER_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-2';
export const ALL_CLOUD_USER_2_VIEW_ID = 'd6137e11-dbcd-4824-85d3-42fe1ca48cb6';
export default defineView({
universalIdentifier: ALL_CLOUD_USER_2_VIEW_ID,
name: 'all-cloud-user-2',
objectUniversalIdentifier: CLOUD_USER_2_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
type: ViewType.TABLE,
fields: [
{
universalIdentifier: '9e82305e-9fd9-4bf7-8cc5-098eb35a748e',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_FULL_NAME_FIELD_ID,
position: 0,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'ea737839-bcb7-4da1-aaf6-79cda65e6263',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_EMAIL_FIELD_ID,
position: 1,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ccd1eca7-c851-4e97-9f0e-48c7139f3ced',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_ACTIVITY_STATUS_FIELD_ID,
position: 2,
isVisible: true,
size: 130,
},
{
universalIdentifier: 'e99c82d3-d2fe-4758-9253-ab2ca2b50ef8',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_TWENTY_FIELD_ID,
position: 3,
isVisible: true,
size: 100,
},
{
universalIdentifier: 'c03da046-30b6-402e-8d4b-10799aa5102d',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_WORKSPACE_COUNT_FIELD_ID,
position: 4,
isVisible: true,
size: 130,
},
{
universalIdentifier: 'e3a787f2-77d1-4865-a5c2-c60976809d4d',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_USER_TENURE_FIELD_ID,
position: 5,
isVisible: true,
size: 110,
},
{
universalIdentifier: '352d77f2-726b-4a75-8c9d-3117cfabedc1',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L24H_FIELD_ID,
position: 6,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'cf06d6b0-e425-4daf-8a6d-9b66a058ae80',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L7D_FIELD_ID,
position: 7,
isVisible: true,
size: 120,
},
{
universalIdentifier: '4ab63729-0b48-4b0b-ac9e-e72f5448998c',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_IS_ACTIVE_L30D_FIELD_ID,
position: 8,
isVisible: true,
size: 120,
},
{
universalIdentifier: '8c6a3315-fa47-4a12-b9fe-1775462c803d',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L24H_FIELD_ID,
position: 9,
isVisible: true,
size: 130,
},
{
universalIdentifier: '89011d63-e670-498e-bc8f-142f7ceafd04',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L7D_FIELD_ID,
position: 10,
isVisible: true,
size: 130,
},
{
universalIdentifier: '88816c57-aa5b-4efe-b53a-b288fad24ab4',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_PAGE_VIEWS_L30D_FIELD_ID,
position: 11,
isVisible: true,
size: 130,
},
{
universalIdentifier: '3b0ff937-1eaf-4589-80ec-23abb4f2e204',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_AVG_DAILY_PAGEVIEWS_LAST_30D_FIELD_ID,
position: 12,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'dee5f679-9ad9-4bc4-94a9-af28d7210699',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_DAYS_SINCE_LAST_ACTIVITY_FIELD_ID,
position: 13,
isVisible: true,
size: 150,
},
{
universalIdentifier: '25b4e175-be18-4b20-9c51-5db0a596d968',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_LAST_ACTIVITY_DATE_FIELD_ID,
position: 14,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f1706c21-d164-414c-9cf5-4cf376584bb9',
fieldMetadataUniversalIdentifier:
CLOUD_USER_2_DATA_LAST_UPDATED_AT_FIELD_ID,
position: 15,
isVisible: true,
size: 150,
},
{
universalIdentifier: '88d0f343-42c1-4d36-82df-cfdb74e785eb',
fieldMetadataUniversalIdentifier: CLOUD_USER_2_UPDATED_BY_FIELD_ID,
position: 16,
isVisible: true,
size: 150,
},
],
});
@@ -1,56 +0,0 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
} from 'src/fields/cloud-user-workspace-2-field-ids';
import { CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-user-workspace-2';
export const ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID =
'f80e44ef-43fb-409e-a003-decadaa38b3e';
export default defineView({
universalIdentifier: ALL_CLOUD_USER_WORKSPACE_2_VIEW_ID,
name: 'all-cloud-user-workspace-2',
objectUniversalIdentifier: CLOUD_USER_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
type: ViewType.TABLE,
fields: [
{
universalIdentifier: '4a0d9d93-d182-443d-8a70-3a4f25e51c50',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_USER_IDENTIFIER_FIELD_ID,
position: 0,
isVisible: true,
size: 200,
},
{
universalIdentifier: '14b78fc4-a6a5-471b-ba87-6aebc9a3da68',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_TWENTY_WORKSPACE_IDENTIFIER_FIELD_ID,
position: 1,
isVisible: true,
size: 220,
},
{
universalIdentifier: '8c9c8a1b-d511-4706-afe7-32b22cce2e7e',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_ID_OF_THE_USER_WORKSPACE_FIELD_ID,
position: 2,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'ee35eded-4222-4a87-b31c-69e415e331db',
fieldMetadataUniversalIdentifier:
CLOUD_USER_WORKSPACE_2_UPDATED_BY_FIELD_ID,
position: 3,
isVisible: true,
size: 150,
},
],
});
@@ -1,369 +0,0 @@
import { defineView } from 'twenty-sdk';
import { ViewType } from 'twenty-shared/types';
import {
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
CLOUD_WORKSPACE_2_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
CLOUD_WORKSPACE_2_MRR_FIELD_ID,
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
} from 'src/fields/cloud-workspace-2-field-ids';
import { CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER } from 'src/objects/cloud-workspace-2';
export const ALL_CLOUD_WORKSPACE_2_VIEW_ID =
'3747a3a0-25a0-42f0-99d9-61a4b4a76009';
export default defineView({
universalIdentifier: ALL_CLOUD_WORKSPACE_2_VIEW_ID,
name: 'all-cloud-workspace-2',
objectUniversalIdentifier: CLOUD_WORKSPACE_2_UNIVERSAL_IDENTIFIER,
icon: 'IconList',
position: 0,
type: ViewType.TABLE,
fields: [
{
universalIdentifier: '146e3f00-8efc-46be-aafb-a6bafcefa5b0',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_COMPANY_NAME_FIELD_ID,
position: 0,
isVisible: true,
size: 180,
},
{
universalIdentifier: '10ce2149-d25b-4e21-8215-c628677bc4cb',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_SUB_DOMAIN_FIELD_ID,
position: 1,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'c8735db0-9a89-4204-ad58-b68b71c10182',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_DESCRIPTION_FIELD_ID,
position: 2,
isVisible: true,
size: 250,
},
{
universalIdentifier: 'cf3f182b-1a69-4274-a200-ef191400a9e8',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_INDUSTRY_FIELD_ID,
position: 3,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'c932adb5-129b-4457-bbfa-75baaf27707f',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_EMPLOYEES_FIELD_ID,
position: 4,
isVisible: true,
size: 110,
},
{
universalIdentifier: '5f0d33f0-60f0-442c-8c2d-19bfe1c9ea49',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_SUBSCRIPTION_STATUS_FIELD_ID,
position: 5,
isVisible: true,
size: 150,
},
{
universalIdentifier: '904e4ecd-f997-42b6-933a-1174cf2bebdc',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVATION_STATUS_FIELD_ID,
position: 6,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f2d919e7-f782-4edb-ad1a-b99649fbbaf9',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_MRR_FIELD_ID,
position: 7,
isVisible: true,
size: 120,
},
{
universalIdentifier: '92afa154-72f2-4b01-8376-2f5311bb40f9',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_ARR_FIELD_ID,
position: 8,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'c63bcd0a-f8d3-4c86-b9c0-6d638d7e9195',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_POTENTIAL_ARR_FIELD_ID,
position: 9,
isVisible: true,
size: 130,
},
{
universalIdentifier: '8659ca48-23b0-4fb8-b588-0919169cae4d',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ANNUAL_REVENUE_FIELD_ID,
position: 10,
isVisible: true,
size: 140,
},
{
universalIdentifier: '1524079b-0e7e-41a6-80bc-85bcdfcab537',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_FUNDING_FIELD_ID,
position: 11,
isVisible: true,
size: 130,
},
{
universalIdentifier: '36a755ee-903c-4c61-b091-ebc0e687333d',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVE_USERS_L24H_FIELD_ID,
position: 12,
isVisible: true,
size: 140,
},
{
universalIdentifier: '5ac28b38-823f-421c-a177-1749b8283381',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVE_USERS_L7D_FIELD_ID,
position: 13,
isVisible: true,
size: 140,
},
{
universalIdentifier: '6fa0bfe5-ac55-4165-8830-5c084466c7f5',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_ACTIVE_USERS_L30D_FIELD_ID,
position: 14,
isVisible: true,
size: 140,
},
{
universalIdentifier: 'bee2ed03-23c2-4c99-86db-c0dd4599c669',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAGE_VIEWS_L24H_FIELD_ID,
position: 15,
isVisible: true,
size: 130,
},
{
universalIdentifier: 'e8f9775b-c984-4832-800a-3dd26060c0aa',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAGE_VIEWS_L7D_FIELD_ID,
position: 16,
isVisible: true,
size: 130,
},
{
universalIdentifier: '95c4fdfc-8fe5-43ce-9885-3f0fc5b4756a',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAGE_VIEWS_L30D_FIELD_ID,
position: 17,
isVisible: true,
size: 130,
},
{
universalIdentifier: '6448c0f1-1469-466e-99ed-c7aa015d9e38',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_WORKSPACE_USERS_FIELD_ID,
position: 18,
isVisible: true,
size: 150,
},
{
universalIdentifier: '082ed200-9bba-444a-9b28-3a129b7ac9d8',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_TOTAL_EVER_ACTIVE_WORKSPACE_USERS_FIELD_ID,
position: 19,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'd61bbf0c-48ad-428a-90a1-87b54a87bacf',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_WORKSPACE_TENURE_FIELD_ID,
position: 20,
isVisible: true,
size: 140,
},
{
universalIdentifier: '26058a5b-edde-4caa-a034-32b8edb54ce6',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_IS_ACTIVE_L24H_FIELD_ID,
position: 21,
isVisible: true,
size: 120,
},
{
universalIdentifier: '36931d2b-f250-4325-a877-3285dd8a797e',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_IS_ACTIVE_L7D_FIELD_ID,
position: 22,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'c120fc92-4796-4cf4-866f-c1d8c4555321',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_IS_ACTIVE_L30D_FIELD_ID,
position: 23,
isVisible: true,
size: 120,
},
{
universalIdentifier: 'a6674592-070b-41c1-baab-f360984328cc',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_IS_ENRICHED_FIELD_ID,
position: 24,
isVisible: true,
size: 110,
},
{
universalIdentifier: 'd3701295-ef41-46c4-8dd3-b31c27663516',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_CUSTOM_DOMAIN_FIELD_ID,
position: 25,
isVisible: true,
size: 180,
},
{
universalIdentifier: 'dd22ffe7-95ed-4c67-be33-c2d1592daa32',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_WORKSPACE_BUSINESS_DOMAIN_FIELD_ID,
position: 26,
isVisible: true,
size: 200,
},
{
universalIdentifier: 'c7a9bcf8-d41d-4ded-b680-868e470e3228',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_COMPANY_LINKEDIN_FIELD_ID,
position: 27,
isVisible: true,
size: 180,
},
{
universalIdentifier: '398eb88f-0d12-446d-a486-d1fb262f82bb',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_CREATOR_EMAIL_FIELD_ID,
position: 28,
isVisible: true,
size: 200,
},
{
universalIdentifier: '52d2a1b3-e2b2-4d6b-a5dd-ea8d63c7c369',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_TAGS_FIELD_ID,
position: 29,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f74fed2c-94dc-4954-ac27-79acd7e04103',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_PAYMENT_FREQUENCY_FIELD_ID,
position: 30,
isVisible: true,
size: 150,
},
{
universalIdentifier: '666d0cea-e50b-4cc8-9ed5-5f5c0ec7fe7e',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_NEXT_RENEWAL_DATE_FIELD_ID,
position: 31,
isVisible: true,
size: 150,
},
{
universalIdentifier: '60508519-5ac4-4f96-a30f-5c75d3ce9a93',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_L30D_FIELD_ID,
position: 32,
isVisible: true,
size: 160,
},
{
universalIdentifier: '3b1ca2b6-31a1-4755-a9a4-07ee9a9b8a33',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_NUMBER_OF_EVENTS_TOTAL_FIELD_ID,
position: 33,
isVisible: true,
size: 160,
},
{
universalIdentifier: '47ba21ab-a5c5-42b9-b1e2-7a988ece154d',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_ALEXA_RANK_FIELD_ID,
position: 34,
isVisible: true,
size: 110,
},
{
universalIdentifier: 'de5e2e42-2600-488c-81d0-f700e7dd0f67',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_COMPANY_FOUNDED_YEAR_FIELD_ID,
position: 35,
isVisible: true,
size: 160,
},
{
universalIdentifier: 'd6bec718-8b50-41e2-95d7-521151e2df82',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_LATEST_FUNDING_STAGE_FIELD_ID,
position: 36,
isVisible: true,
size: 160,
},
{
universalIdentifier: '9aaff045-64fa-4857-a8df-7394cf43a120',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_LAST_PAGE_VIEW_DATE_FIELD_ID,
position: 37,
isVisible: true,
size: 150,
},
{
universalIdentifier: 'f2cb82b9-ebb4-4258-971b-061d709f28e5',
fieldMetadataUniversalIdentifier:
CLOUD_WORKSPACE_2_DATA_LAST_UPDATED_AT_FIELD_ID,
position: 38,
isVisible: true,
size: 150,
},
{
universalIdentifier: '7e631992-3ae6-478b-9874-67c3b211f05a',
fieldMetadataUniversalIdentifier: CLOUD_WORKSPACE_2_UPDATED_BY_FIELD_ID,
position: 39,
isVisible: true,
size: 150,
},
],
});
@@ -1,31 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,130 @@
ARG APP_VERSION
# === Stage 1: Common dependencies ===
FROM node:22-alpine AS common-deps
WORKDIR /app
COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
COPY ./.yarn/releases /app/.yarn/releases
COPY ./.yarn/patches /app/.yarn/patches
COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
COPY ./packages/twenty-sdk/package.json /app/packages/twenty-sdk/
RUN yarn && yarn cache clean && npx nx reset
# === Stage 2: Build server from source ===
FROM common-deps AS twenty-server-build
COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:build
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-sdk twenty-server
# === Stage 3: Build frontend from source ===
# Requires ~10GB Docker memory. To skip, pre-build on host first:
# npx nx build twenty-front
# The COPY below will pick up packages/twenty-front/build/ if it exists.
FROM common-deps AS twenty-front-build
COPY --from=twenty-server-build /app/package.json /tmp/.build-sentinel
COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-sdk /app/packages/twenty-sdk
RUN if [ -d /app/packages/twenty-front/build ]; then \
echo "Using pre-built frontend from host"; \
else \
NODE_OPTIONS="--max-old-space-size=8192" npx nx build twenty-front; \
fi
# === Stage 4: s6-overlay ===
FROM alpine:3.20 AS s6-fetch
ARG S6_OVERLAY_VERSION=3.2.0.2
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then echo "aarch64" > /tmp/s6arch; \
else echo "x86_64" > /tmp/s6arch; fi
RUN S6_ARCH=$(cat /tmp/s6arch) && \
wget -O /tmp/s6-overlay-noarch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
wget -O /tmp/s6-overlay-noarch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" && \
wget -O /tmp/s6-overlay-arch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
wget -O /tmp/s6-overlay-arch.tar.xz.sha256 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" && \
cd /tmp && \
NOARCH_SUM=$(awk '{print $1}' s6-overlay-noarch.tar.xz.sha256) && \
ARCH_SUM=$(awk '{print $1}' s6-overlay-arch.tar.xz.sha256) && \
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
# === Stage 5: Final all-in-one image ===
FROM node:22-alpine
# s6-overlay
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
COPY --from=s6-fetch /tmp/s6-overlay-arch.tar.xz /tmp/
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \
&& rm /tmp/s6-overlay-*.tar.xz
# Infrastructure: Postgres, Redis, and utilities
RUN apk add --no-cache \
postgresql16 postgresql16-contrib \
redis \
curl jq su-exec
# tsx for database setup scripts
RUN npm install -g tsx
# Twenty application (both server and frontend built from source)
COPY --from=twenty-server-build /app /app
COPY --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
# s6 service definitions
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
# Data directories
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
# Tell s6-overlay to preserve container environment variables
ENV S6_KEEP_ENV=1
# Environment defaults
ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
SERVER_URL=http://localhost:2020 \
REDIS_URL=redis://localhost:6379 \
STORAGE_TYPE=local \
APP_SECRET=twenty-app-dev-secret-not-for-production \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=3000 \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
EXPOSE 3000
VOLUME ["/data/postgres", "/app/.local-storage"]
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
ENTRYPOINT ["/init"]
@@ -0,0 +1 @@
/bin/sh /etc/s6-overlay/scripts/init-db.sh
@@ -0,0 +1,13 @@
#!/bin/sh
# Initialize PostgreSQL data directory if empty
if [ ! -f /data/postgres/PG_VERSION ]; then
echo "Initializing PostgreSQL data directory..."
su-exec postgres initdb -D /data/postgres --auth=trust --encoding=UTF8
# Allow local connections without password
echo "host all all 127.0.0.1/32 trust" >> /data/postgres/pg_hba.conf
echo "host all all ::1/128 trust" >> /data/postgres/pg_hba.conf
fi
exec su-exec postgres postgres -D /data/postgres \
-c listen_addresses=localhost \
-c unix_socket_directories=/tmp
@@ -0,0 +1,6 @@
#!/bin/sh
exec redis-server \
--dir /data/redis \
--maxmemory-policy noeviction \
--bind 127.0.0.1 \
--protected-mode yes
@@ -0,0 +1,3 @@
#!/bin/sh
cd /app/packages/twenty-server
exec yarn start:prod
@@ -0,0 +1,3 @@
#!/bin/sh
cd /app/packages/twenty-server
exec yarn worker:prod
@@ -0,0 +1,56 @@
#!/bin/sh
set -e
# Wait for PostgreSQL to be ready (timeout after 60s)
echo "Waiting for PostgreSQL..."
TRIES=0
until su-exec postgres pg_isready -h localhost; do
TRIES=$((TRIES + 1))
if [ "$TRIES" -ge 120 ]; then
echo "ERROR: PostgreSQL did not become ready within 60s"
exit 1
fi
sleep 0.5
done
echo "PostgreSQL is ready."
# Create role if it doesn't exist
su-exec postgres psql -h localhost -tc \
"SELECT 1 FROM pg_roles WHERE rolname='twenty'" | grep -q 1 \
|| su-exec postgres psql -h localhost -c "CREATE ROLE twenty WITH LOGIN PASSWORD 'twenty' SUPERUSER"
# Create database if it doesn't exist
su-exec postgres psql -h localhost -tc \
"SELECT 1 FROM pg_database WHERE datname='default'" | grep -q 1 \
|| su-exec postgres createdb -h localhost -O twenty default
# Run Twenty database setup and migrations
cd /app/packages/twenty-server
has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running initial setup..."
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
fi
# Always run migrations (idempotent — skips already-applied ones)
yarn database:migrate:prod
yarn command:prod cache:flush
yarn command:prod upgrade
yarn command:prod cache:flush
# Only seed on first boot — check if the dev workspace already exists
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
if [ "$has_workspace" = "f" ]; then
echo "Seeding app dev data..."
yarn command:prod workspace:seed:dev --light || true
else
echo "Dev workspace already seeded, skipping."
fi
echo "Database initialization complete."
@@ -20,19 +20,51 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
## Prerequisites
- Node.js 24+ and Yarn 4
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
- Docker (for the local Twenty dev server)
## Getting Started
Create a new app using the official scaffolder, then authenticate and start developing:
Create a new app using the official scaffolder. It can automatically start a local Twenty instance for you:
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty app:dev
yarn twenty dev
```
### Local Server Management
The SDK includes commands to manage a local Twenty dev server (all-in-one Docker image with PostgreSQL, Redis, server, and worker):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
The local server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`), so you can start developing immediately without any manual setup.
### Authentication
Connect your app to the local server using OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
The scaffolder supports two modes for controlling which example files are included:
@@ -21,19 +21,51 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
## المتطلبات الأساسية
* Node.js 24+ وYarn 4
* مساحة عمل Twenty ومفتاح واجهة برمجة التطبيقات (أنشئ واحدًا على https://app.twenty.com/settings/api-webhooks)
* Docker (لخادم تطوير Twenty المحلي)
## البدء
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
أنشئ تطبيقًا جديدًا باستخدام المولّد الرسمي. يمكنه بدء مثيل محلي من Twenty تلقائيًا لك:
```bash filename="Terminal"
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
# إنشاء تطبيق جديد — ستعرض واجهة سطر الأوامر خيار بدء خادم Twenty محلي
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
yarn twenty app:dev
yarn twenty dev
```
### إدارة الخادم المحلي
يتضمن SDK أوامر لإدارة خادم تطوير Twenty محلي (صورة Docker متكاملة تتضمن PostgreSQL وRedis والخادم والعامل):
```bash filename="Terminal"
# ابدأ الخادم المحلي (يسحب الصورة إذا لزم الأمر)
yarn twenty server start
# تحقّق من حالة الخادم
yarn twenty server status
# بثّ سجلات الخادم
yarn twenty server logs
# أوقف الخادم
yarn twenty server stop
# أعد ضبط جميع البيانات وابدأ من جديد
yarn twenty server reset
```
يأتي الخادم المحلي مهيأً مسبقًا بمساحة عمل ومستخدم (`tim@apple.dev` / `tim@apple.dev`)، بحيث يمكنك البدء في التطوير فورًا دون أي إعداد يدوي.
### المصادقة
وصّل تطبيقك بالخادم المحلي باستخدام OAuth:
```bash filename="Terminal"
# المصادقة عبر OAuth (يفتح المتصفح)
yarn twenty remote add --local
```
يدعم المُنشئ وضعين للتحكم في ملفات الأمثلة التي سيتم تضمينها:
@@ -21,19 +21,51 @@ Mit Apps können Sie Twenty-Anpassungen **als Code** erstellen und verwalten. An
## Voraussetzungen
* Node.js 24+ und Yarn 4
* Ein Twenty-Workspace und ein API-Schlüssel (unter https://app.twenty.com/settings/api-webhooks erstellen)
* Docker (für den lokalen Twenty-Dev-Server)
## Erste Schritte
Erstellen Sie mit dem offiziellen Scaffolder eine neue App, authentifizieren Sie sich und beginnen Sie mit der Entwicklung:
Erstelle eine neue App mit dem offiziellen Scaffolder. Der Scaffolder kann für dich automatisch eine lokale Twenty-Instanz starten:
```bash filename="Terminal"
# Eine neue App erstellen (enthält standardmäßig alle Beispiele)
# Eine neue App erstellen — die CLI bietet an, einen lokalen Twenty-Server zu starten
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Dev-Modus starten: synchronisiert lokale Änderungen automatisch mit deinem Arbeitsbereich
yarn twenty app:dev
yarn twenty dev
```
### Lokale Serververwaltung
Das SDK enthält Befehle zur Verwaltung eines lokalen Twenty-Dev-Servers (All-in-One-Docker-Image mit PostgreSQL, Redis, Server und Worker):
```bash filename="Terminal"
# Den lokalen Server starten (lädt das Image bei Bedarf herunter)
yarn twenty server start
# Serverstatus prüfen
yarn twenty server status
# Serverprotokolle streamen
yarn twenty server logs
# Server stoppen
yarn twenty server stop
# Alle Daten zurücksetzen und neu starten
yarn twenty server reset
```
Der lokale Server ist bereits mit einem Arbeitsbereich und einem Benutzer (`tim@apple.dev` / `tim@apple.dev`) vorbefüllt, sodass Sie ohne manuelle Einrichtung sofort mit der Entwicklung beginnen können.
### Authentifizierung
Verbinden Sie Ihre App mithilfe von OAuth mit dem lokalen Server:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Das Scaffolding-Tool unterstützt zwei Modi, um zu steuern, welche Beispieldateien enthalten sind:
@@ -21,19 +21,51 @@ Le app ti consentono di creare e gestire le personalizzazioni di Twenty **come c
## Prerequisiti
* Node.js 24+ e Yarn 4
* Uno spazio di lavoro Twenty e una chiave API (creane una su https://app.twenty.com/settings/api-webhooks)
* Docker (per il server di sviluppo locale di Twenty)
## Per iniziare
Crea una nuova app utilizzando lo scaffolder ufficiale, quindi autenticati e inizia a sviluppare:
Crea una nuova app utilizzando lo scaffolder ufficiale. Può avviare automaticamente un'istanza locale di Twenty per te:
```bash filename="Terminal"
# Crea lo scaffold di una nuova app (include tutti gli esempi per impostazione predefinita)
# Crea lo scaffold di una nuova app — la CLI offrirà di avviare un server locale di Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Avvia la modalità di sviluppo: sincronizza automaticamente le modifiche locali con il tuo workspace
yarn twenty app:dev
yarn twenty dev
```
### Gestione del server locale
L'SDK include comandi per gestire un server di sviluppo locale di Twenty (immagine Docker all-in-one con PostgreSQL, Redis, server e worker):
```bash filename="Terminal"
# Avvia il server locale (scarica l'immagine se necessario)
yarn twenty server start
# Verifica lo stato del server
yarn twenty server status
# Segui i log del server
yarn twenty server logs
# Arresta il server
yarn twenty server stop
# Reimposta tutti i dati e riparti da zero
yarn twenty server reset
```
Il server locale è preconfigurato con uno spazio di lavoro e un utente (`tim@apple.dev` / `tim@apple.dev`), così puoi iniziare a sviluppare immediatamente senza alcuna configurazione manuale.
### Autenticazione
Collega la tua app al server locale tramite OAuth:
```bash filename="Terminal"
# Autenticati tramite OAuth (apre il browser)
yarn twenty remote add --local
```
Lo strumento di scaffolding supporta due modalità per controllare quali file di esempio vengono inclusi:
@@ -21,19 +21,51 @@ Os aplicativos permitem criar e gerenciar personalizações do Twenty **como có
## Pré-requisitos
* Node.js 24+ e Yarn 4
* Um espaço de trabalho do Twenty e uma chave de API (crie uma em https://app.twenty.com/settings/api-webhooks)
* Docker (para o servidor de desenvolvimento local do Twenty)
## Primeiros passos
Crie um novo aplicativo usando o gerador oficial, depois autentique-se e comece a desenvolver:
Crie um novo app usando o gerador oficial de estrutura. Ele pode iniciar automaticamente uma instância local do Twenty para você:
```bash filename="Terminal"
# Criar a estrutura de um novo app (inclui todos os exemplos por padrão)
# Criar a estrutura de um novo app — a CLI oferecerá iniciar um servidor local do Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Iniciar modo de desenvolvimento: sincroniza automaticamente as alterações locais com seu workspace
yarn twenty app:dev
yarn twenty dev
```
### Gerenciamento do Servidor Local
O SDK inclui comandos para gerenciar um servidor de desenvolvimento local do Twenty (imagem Docker all-in-one com PostgreSQL, Redis, servidor e worker):
```bash filename="Terminal"
# Iniciar o servidor local (faz pull da imagem se necessário)
yarn twenty server start
# Verificar o status do servidor
yarn twenty server status
# Transmitir os logs do servidor
yarn twenty server logs
# Parar o servidor
yarn twenty server stop
# Redefinir todos os dados e começar do zero
yarn twenty server reset
```
O servidor local já vem pré-configurado com um espaço de trabalho e um usuário (`tim@apple.dev` / `tim@apple.dev`), para que você possa começar a desenvolver imediatamente, sem qualquer configuração manual.
### Autenticação
Conecte seu aplicativo ao servidor local usando OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
O gerador de estrutura oferece suporte a dois modos para controlar quais arquivos de exemplo são incluídos:
@@ -21,19 +21,51 @@ Aplicațiile vă permit să construiți și să gestionați personalizările Twe
## Cerințe
* Node.js 24+ și Yarn 4
* Un spațiu de lucru Twenty și o cheie API (creați una la https://app.twenty.com/settings/api-webhooks)
* Docker (pentru serverul local de dezvoltare Twenty)
## Începeți
Creați o aplicație nouă folosind generatorul oficial, apoi autentificați-vă și începeți să dezvoltați:
Creează o aplicație nouă folosind generatorul oficial. Poate porni automat o instanță Twenty locală pentru tine:
```bash filename="Terminal"
# Creează scheletul unei aplicații noi (include toate exemplele în mod implicit)
# Creează scheletul unei aplicații noi — CLI-ul îți va oferi opțiunea de a porni un server Twenty local
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Pornește modul de dezvoltare: sincronizează automat modificările locale cu spațiul tău de lucru
yarn twenty app:dev
yarn twenty dev
```
### Gestionarea serverului local
SDK-ul include comenzi pentru a gestiona un server local de dezvoltare Twenty (imagine Docker all-in-one cu PostgreSQL, Redis, server și worker):
```bash filename="Terminal"
# Pornește serverul local (descarcă imaginea dacă este necesar)
yarn twenty server start
# Verifică starea serverului
yarn twenty server status
# Afișează în timp real jurnalele serverului
yarn twenty server logs
# Oprește serverul
yarn twenty server stop
# Resetează toate datele și pornește de la zero
yarn twenty server reset
```
Serverul local vine preconfigurat cu un spațiu de lucru și un utilizator (`tim@apple.dev` / `tim@apple.dev`), astfel încât să poți începe să dezvolți imediat, fără nicio configurare manuală.
### Autentificare
Conectează-ți aplicația la serverul local folosind OAuth:
```bash filename="Terminal"
# Autentifică-te prin OAuth (se deschide browserul)
yarn twenty remote add --local
```
Generatorul de schelet acceptă două moduri pentru a controla ce fișiere de exemplu sunt incluse:
@@ -21,19 +21,51 @@ description: Создавайте и управляйте настройками
## Требования
* Node.js 24+ и Yarn 4
* Рабочее пространство Twenty и ключ API (создайте его на https://app.twenty.com/settings/api-webhooks)
* Docker (для локального сервера разработки Twenty)
## Начало работы
Создайте новое приложение с помощью официального генератора, затем выполните аутентификацию и начните разработку:
Создайте новое приложение с помощью официального генератора каркаса. Он может автоматически запустить локальный экземпляр Twenty:
```bash filename="Terminal"
# Создать каркас нового приложения (по умолчанию включает все примеры)
# Создать каркас нового приложения — CLI предложит запустить локальный сервер Twenty
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Запустить режим разработки: автоматически синхронизирует локальные изменения с вашим рабочим пространством
yarn twenty app:dev
yarn twenty dev
```
### Управление локальным сервером
SDK включает команды для управления локальным сервером разработки Twenty (универсальный образ Docker с PostgreSQL, Redis, сервером и воркером):
```bash filename="Terminal"
# Запустить локальный сервер (при необходимости будет загружен образ)
yarn twenty server start
# Проверить статус сервера
yarn twenty server status
# Просмотр логов сервера в реальном времени
yarn twenty server logs
# Остановить сервер
yarn twenty server stop
# Сбросить все данные и начать с нуля
yarn twenty server reset
```
Локальный сервер уже содержит рабочее пространство и пользователя (`tim@apple.dev` / `tim@apple.dev`), так что вы можете сразу начать разработку без какой-либо ручной настройки.
### Аутентификация
Подключите своё приложение к локальному серверу с помощью OAuth:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
Генератор каркаса поддерживает два режима для управления тем, какие файлы-примеры включаются:
@@ -21,19 +21,51 @@ Uygulamalar, Twenty özelleştirmelerini **kod olarak** oluşturup yönetmenizi
## Ön Gereksinimler
* Node.js 24+ ve Yarn 4
* Bir Twenty çalışma alanı ve bir API anahtarı (https://app.twenty.com/settings/api-webhooks adresinde oluşturun)
* Docker (yerel Twenty geliştirme sunucusu için)
## Başlarken
Resmi scaffolder aracını kullanarak yeni bir uygulama oluşturun, ardından kimlik doğrulaması yapıp geliştirmeye başlayın:
Resmi iskelet oluşturucusunu kullanarak yeni bir uygulama oluşturun. Sizin için otomatik olarak yerel bir Twenty örneğini başlatabilir:
```bash filename="Terminal"
# Yeni bir uygulamanın iskeletini oluşturun (varsayılan olarak tüm örnekleri içerir)
# Yeni bir uygulamanın iskeletini oluşturun — CLI yerel bir Twenty sunucusunu başlatmayı önerecektir
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Geliştirme modunu başlatın: yerel değişiklikleri otomatik olarak çalışma alanınızla senkronize eder
yarn twenty app:dev
# Geliştirme modunu başlatın: yerel değişiklikleri çalışma alanınızla otomatik olarak senkronize eder
yarn twenty dev
```
### Yerel Sunucu Yönetimi
SDK, yerel bir Twenty geliştirme sunucusunu yönetmek için komutlar içerir (PostgreSQL, Redis, sunucu ve worker içeren hepsi bir arada Docker imajı):
```bash filename="Terminal"
# Yerel sunucuyu başlatın (gerekirse imajı indirir)
yarn twenty server start
# Sunucu durumunu kontrol edin
yarn twenty server status
# Sunucu günlüklerini akış olarak görüntüleyin
yarn twenty server logs
# Sunucuyu durdurun
yarn twenty server stop
# Tüm verileri sıfırlayın ve temiz bir başlangıç yapın
yarn twenty server reset
```
Yerel sunucu, bir çalışma alanı ve kullanıcıyla (`tim@apple.dev` / `tim@apple.dev`) önceden yapılandırılmış olarak gelir; böylece herhangi bir manuel kurulum gerektirmeden hemen geliştirmeye başlayabilirsiniz.
### Kimlik Doğrulama
Uygulamanızı OAuth kullanarak yerel sunucuya bağlayın:
```bash filename="Terminal"
# Authenticate via OAuth (opens browser)
yarn twenty remote add --local
```
İskelet oluşturucu, hangi örnek dosyaların dahil edileceğini kontrol etmek için iki modu destekler:
@@ -21,19 +21,51 @@ description: 以代码的形式构建并管理 Twenty 自定义项。
## 先决条件
* Node.js 24+ 和 Yarn 4
* 一个 Twenty 工作空间和一个 API 密钥(在 https://app.twenty.com/settings/api-webhooks 创建)
* Docker (用于本地 Twenty 开发服务器)
## 开始使用
使用官方脚手架创建一个新应用,然后进行身份验证并开始开发
使用官方脚手架创建一个新应用。 它可以为你自动启动一个本地 Twenty 实例
```bash filename="Terminal"
# 创建一个新应用脚手架(默认包含所有示例)
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# 启动开发模式:会将本地更改自动同步到你的工作区
yarn twenty app:dev
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
### 本地服务器管理
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
本地服务器预置了一个工作区和用户 (`tim@apple.dev` / `tim@apple.dev`),因此你可以无需任何手动设置即可立即开始开发。
### 身份验证
使用 OAuth 将你的应用连接到本地服务器:
```bash filename="Terminal"
# 通过 OAuth 进行身份验证 (将打开浏览器)
yarn twenty remote add --local
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
@@ -256,6 +256,7 @@ export enum AllMetadataName {
logicFunction = 'logicFunction',
navigationMenuItem = 'navigationMenuItem',
objectMetadata = 'objectMetadata',
objectPermission = 'objectPermission',
pageLayout = 'pageLayout',
pageLayoutTab = 'pageLayoutTab',
pageLayoutWidget = 'pageLayoutWidget',
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
+5
View File
@@ -9859,6 +9859,11 @@ msgstr "Object Events"
msgid "Object ID"
msgstr "Object ID"
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr "object permission"
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
@@ -9864,6 +9864,11 @@ msgstr ""
msgid "Object ID"
msgstr ""
#. js-lingui-id: wZpAXW
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "object permission"
msgstr ""
#. js-lingui-id: ecLxgy
#: src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRowOptionsDropdown.tsx
msgid "Object permission options"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More