a9d996ff7eb502a6c3afeeaf09f72a42c340d5aa
424 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a9d996ff7e |
Clarify application licensing and add trademark policy (#23564)
## What - `twenty-sdk`, `twenty-client-sdk`, `create-twenty-app`, `twenty-shared` and `twenty-ui` are now MIT (package.json + LICENSE files). The SDKs are bundled into third-party applications and app front components import twenty-ui, so these need a permissive license for apps to be licensable by their authors. `twenty-shared` is included because both SDKs inline it at build time; an MIT SDK bundling AGPL code would defeat the purpose. Apps under `packages/twenty-apps` were already MIT. - Added a "Twenty Application Exception" to LICENSE (additional permission under AGPLv3 section 7): applications that interact with Twenty through the app platform interfaces (APIs, manifests, logic functions, front components, SDKs) are not subject to copyleft and can be licensed freely by their authors. Modifying Twenty itself remains fully AGPL, including the network clause. - Rewrote the LICENSE intro to describe the three licensing zones (AGPL, Enterprise-marked files, MIT packages) and fixed the intro incorrectly saying "GPL". - Added TRADEMARK.md: what anyone can do without asking (self-host, "built on Twenty", forks under their own name) and what requires permission (using the name or logo for a product, domain, or hosted offering). ## Why Gives app developers and partners legal certainty that building on the platform does not pull their apps under AGPL, while the core stays AGPL. The exception and trademark wording should get a legal review before being announced. |
||
|
|
65155fe50c |
feat(apps): add enqueueJob to run a logic function on the workers (#23527)
Closes twentyhq/core-team-issues#2742 A logic function run is capped by its own `timeoutSeconds` (900s max), so anything that can't finish in one run — a full re-sync, a per-record fan-out, a rate-limited third-party API — had no way to continue. This adds a way to hand that work to the workers. ## What it looks like for an app author ```ts import { enqueueJob } from 'twenty-sdk/logic-function'; await enqueueJob({ logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33', payload: { cursor: nextCursor }, retryLimit: 3, priority: 2, delayMs: 60_000, }); ``` The target runs in its own process with its own timeout budget. The classic shape is a function that enqueues *itself* with the next cursor until there is nothing left. ## Changes **twenty-shared** — `EnqueueJobInput` / `EnqueueJobOptions` / `EnqueueJobResult` in `application`. **twenty-server** — new `application-job` module under `core-modules/application`, following the `application-key-value` pattern: - `enqueueJob` mutation on the metadata API, `@AuthApplication`-scoped - the lookup is scoped to `applicationId` + `workspaceId` — that's the authorization boundary, an app can only enqueue its own logic functions, anything else is `LOGIC_FUNCTION_NOT_FOUND` - pushes a `LogicFunctionTriggerJob` onto the existing `logicFunctionQueue`, so the enqueued run goes through the same executor (and the same execution throttling) as every other trigger - the queued run inherits the caller's `userId`/`userWorkspaceId`, so its app access token carries the same permissions as the function that queued it **Job options** are range-checked via `ResolverValidationPipe`, since the values come from application code and an unbounded delay or retry count would let an app pin work in the shared queue: | Option | Default | Range | |--------|---------|-------| | `retryLimit` | `0` | `0`–`10` | | `priority` | queue default | `1`–`10` (lower first) | | `delayMs` | `0` | `0`–7 days | `retryLimit` defaults to `0` rather than inheriting the server-route path's `3`: retries re-run the whole handler, so opting in should be the author's explicit choice. **twenty-sdk** — `enqueueJob` in `twenty-sdk/logic-function`, same shape as `runAgent`/`kv`. **Docs** — new "Background Jobs" page under Extend → Apps → Logic, plus nav and overview entries. **Generated** — regenerated `twenty-front/src/generated-metadata` and `twenty-client-sdk/src/metadata/generated` for the new mutation. ## Tests - `application-job.service.spec.ts` — 5 unit tests: job options mapping, defaults, acting-user propagation, application-scoped lookup, not-found - `enqueue-job.integration-spec.ts` — 5 integration tests: rejects a non-`APPLICATION_ACCESS` token, enqueues a function the app owns, rejects a function owned by another application, rejects an unknown identifier, rejects out-of-range options All green locally, along with `typecheck` for `twenty-server`/`twenty-sdk` and oxlint/oxfmt on the touched files. ## Notes for review - The target is addressed by `universalIdentifier`, matching `runAgent({ agentUniversalIdentifier })` and `ServerRouteDispatchResult.targetLogicFunctionUniversalIdentifier`. Addressing by `name` would be friendlier, but logic function names aren't validated for uniqueness within an app — happy to add it as a convenience if you'd rather. - `enqueueJob` returns as soon as the job is accepted; it can't return the target's result, since the queue driver's `add` returns void. Documented, with a pointer to the KV store for handing results back. --- _Generated by [Claude Code](https://claude.ai/code/session_01QrYvGonS3HMdeuMAVjs5hR)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23527?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
4ec65ed08d |
System view tooling explicit params key naming (#23506)
# Introduction View field system always result from a field existence, the application universal identifier should be the related field one Same but for views and object <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23506?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
0c545bcdeb |
[BREAKING-CHANGE] Centralize system View viewField side effect (#23081)
# Introduction Closes https://github.com/twentyhq/core-team-issues/issues/2669 Part of the `isSystemSideEffect` engine-ownership effort. Until now, a custom object's default **INDEX** table view (`All {objectLabelPlural}`) and its view fields were built imperatively in `ObjectMetadataService` with random `v4()` identifiers, while `twenty-standard` authored its own copies with hardcoded literals. The two never converged, an object rename could drift the view, and nothing marked these rows as engine-owned. This PR makes the metadata side-effect engine the **single owner** of the INDEX view and its view fields, on name-free deterministic identifiers, for custom and standard objects alike. ## Core design - **Name-free deterministic identity.** The INDEX view identifier derives from `object identifier + ViewKey.INDEX` (`getSystemViewUniversalIdentifier`); each view-field identifier derives from `view identifier + field identifier` (`getViewFieldUniversalIdentifier`). An object rename (with a pinned object identifier) keeps the same view, losslessly. - **`isSystemSideEffect: true` is provenance.** Every INDEX view / view field the engine emits is flagged system-owned, so manifest deletion inference never drops it. The flag follows the view: a view field inherits its parent view's flag. - **The engine is the sole owner of the INDEX view.** It always emits it; a caller providing one with the same derived identifier is a genuine conflict surfaced by the engine's reserved-identifier collision, not silently deferred. ## Changes ### Shared (`twenty-shared`) - `getIndexViewUniversalIdentifier` → `getSystemViewUniversalIdentifier`, now taking a `viewKey` (generalizes to any singleton engine-owned view). - Standard field identifiers extracted into a new `STANDARD_OBJECT_FIELDS` constant, so both an object's `fields` and its INDEX view read the same field identifiers. - `buildStandardObjectIndexView` derives the standard INDEX view + view-field identifiers from `STANDARD_OBJECT_FIELDS`, replacing the hardcoded literals in `standard-object.constant.ts`. ### Metadata side-effect engine (custom objects) - **`objectSystemFieldsAndIndexViewOnCreate`** (replaces `objectSystemFieldsOnCreate`): on object creation, provisions the 7 reserved system fields **and** the INDEX view with one view field per displayable system field, all `isSystemSideEffect: true`. - **`fieldIndexViewFieldOnCreate`** (new): on field creation, provisions the field's INDEX view field. Object created in the same batch → visible, positioned before the system view fields; pre-existing object → hidden, appended (preserving the historical `createOneField` behavior). Both branches resolve the INDEX view by its derived identifier (single map access, never a scan). - **`fieldSystemViewFieldsOnDelete`** (new): on field deletion, cascade-deletes every engine-owned view field displaying it. - **`objectSystemSideEffectsOnDelete`** (extended): now also cascade-deletes the object's engine-owned views and their view fields (in addition to system fields, indexes, searchFieldMetadata). Every lookup walks a foreign-key aggregator down from the deleted object, so the work is proportional to what the object owns, never to workspace size. - Object-create and field-create positions are derived from the same caller-input field list, so the INDEX view layout is contiguous with no handler-ordering dependency. - `view` / `viewField` added to the side-effect companion metadata names for `fieldMetadata` and `objectMetadata`. ### Reserved-identifier invariant A caller can never define an entity whose identifier collides with one a system side effect produces: caller inputs are forced `isSystemSideEffect: false` at every entry point (API and app-manifest transpilers), and the engine raises `RESERVED_SYSTEM_UNIVERSAL_IDENTIFIER`, aborting the operation, when a system emission lands on a caller-claimed identifier. Covered by a new engine-level test. ### Caller-side provisioning removed The imperative INDEX view + view-field provisioning is removed from `ObjectMetadataService.createOneObject`. The record-page `FIELDS_WIDGET` view is intentionally left caller-side and deferred to the follow-up (see below). ### `twenty-standard` convergence Standard INDEX views and their view fields converge on the same derived-identifier + `isSystemSideEffect: true` scheme as the engine. `twenty-standard` syncs through the from/to migration path (which never runs the side-effect engine), so it authors this INDEX surface itself, matching what the engine produces for custom objects. ## Rollout Two `2.26.0` workspace commands, running after the `2.25` messageCampaign commands: - `upgrade:2-26:reconcile-index-view-universal-identifier` re-owns the INDEX views of the **twenty-standard and workspace-custom applications** and all their view fields to the derived identifiers with `isSystemSideEffect: true`, in a single per-workspace transaction. Each view field identifier is keyed on the application of the **displayed field** (an app or user column on a standard INDEX view converges too). Soft-deleted views and view fields are skipped: one can coexist with an active successor on the same derivation inputs and both would derive the same identifier. Children reference the view by primary key, so the re-own is lossless. - `upgrade:2-26:demote-and-backfill-application-index-view` handles **manifest-installed applications**, which never had their INDEX view auto-provisioned: every caller-authored INDEX view of another application is demoted to `key: null` (a plain additional view under its manifest identifier), then every application object gets the engine-owned INDEX view and its full view-field layout backfilled through the migration pipeline's legacy path (no side-effect expansion), views committed before view fields across applications since a view field belongs to the application owning its field. Idempotent and retry-safe: engine-owned INDEX views are neither demoted nor re-backfilled, and view creation and view-field creation are gated independently, so a retry after a partial failure still backfills the missing view fields of an already-committed view. Both support `--dry-run` and invalidate the full flat-maps closure (parents aggregate the re-owned identifiers, children resolve them as universal foreign keys, and page-layout widget universal configurations resolve view PKs at cache-build time). The `2.25` `upgrade:2-25:add-message-campaign-name-field` command is adapted to resolve the campaign INDEX view by its INDEX key on the object instead of by universal identifier: it now runs before the reconcile, on workspaces still holding legacy identifiers. ## ⚠️ Breaking change This PR **mutates 187 previously hardcoded universal identifiers** — the standard objects' INDEX views and their view fields (the literals removed from `standard-object.constant.ts`), now derived. - **Handled by the `2.26` commands above** for all existing workspaces. - **The INDEX key is now engine-reserved.** The flat view validator rejects caller-created INDEX views (API and manifest inputs are forced `isSystemSideEffect: false`) and enforces a single non-deleted INDEX view per object; `view.key` is no longer a comparable/updatable property, so no writer can promote or demote a view after creation. `ViewManifest.key` is deprecated and ignored (manifest views are always additional views, so old apps keep syncing and demoted views are not promoted back); the REST/GraphQL create path now rejects `key: INDEX`. In-repo example apps (`hello-world`, `document-generator`) no longer declare it. - **12 declared-but-never-seeded standard INDEX view field identifiers deleted** (the former `preservedViewFields` on `timelineActivity`, `workflowRun` and `workspaceMember`): after the reconcile, no workspace row references them. - **`computeFlatViewFieldsToCreate` now derives view field identifiers** instead of drawing `v4()` ones, which also changes what the committed `1-23` record-page backfill produces going forward (deliberate, documented in-code). - **Record-page views and view fields are not affected** (identifiers unchanged). - **In-repo apps: `twenty-last-contact` updated.** It was the only app declaring explicit INDEX view fields (10 columns across `allPeople` / `allCompanies` / `allOpportunities`) through manifest `viewFields`. Those target identifiers are now engine-owned and derived, so the manifest inputs no longer resolve and install failed with `View not found`. The app now declares only its fields; the engine's `fieldIndexViewFieldOnCreate` provisions the matching INDEX view field automatically. No other app under `packages/twenty-apps` references any of the 187 mutated identifiers, and apps that target standard views point at record-page views (e.g. `real-estate` → `opportunityRecordPageFields`) or their own objects (`twenty-partners`), all unchanged. ### Loss of granularity for app maintainers The engine now owns the INDEX view field of every field a caller adds to an object, so app maintainers lose direct control over those columns. Previously an app could target the engine-owned INDEX view with an explicit manifest `viewField` and set its `position` and `isVisible`. Now `fieldIndexViewFieldOnCreate` appends a **hidden** view field in caller-input order on field creation, so: - Columns an app previously showed at a **dedicated position** and **visible** (e.g. `twenty-last-contact`'s last-contact columns) become **hidden** and **appended in input order** after install. - There is currently **no manifest way to override** the engine-provisioned INDEX view field's position, visibility, or size. This is a deliberate regression accepted for the sake of single-ownership, and app maintainers should expect their INDEX columns to move/hide after upgrading. A follow-up override API will let maintainers reclaim per-field control over the engine-provisioned INDEX view field. ## Testing - Unit specs for each handler: object create (system fields + INDEX view/view fields, override, position offset), field create (same-batch vs existing-object, non-displayable noop, no-INDEX-view noop), field delete, object delete (fields/indexes/searchFieldMetadata/views/view fields cascade, reverse-relation view field on another object). - Engine-level test for the reserved-identifier collision. - `twenty-standard` guard test that its INDEX views/view fields stay on the derived scheme and stay system-owned. - Integration test: full engine provisioning of the INDEX view/view fields on object creation, same view id preserved across an object rename, and cascade delete on object deletion. ## Follow-up The full record-page stack (record-page view, its view fields, view field groups, page layout / tab / widget) is still built imperatively and moves into the engine in https://github.com/twentyhq/core-team-issues/issues/2721. |
||
|
|
4730542087 |
chore: bump version to 2.26.0 (#23451)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23451?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
4f9fd6f674 |
feat(applications): restore the application custom settings tab (#23256)
## Summary Restores the application **custom settings tab** feature that was removed in #22156. This reverts that removal so applications can again expose a custom settings tab via a front component. ## Changes - Restore the `SettingsApplicationCustomTab` component and its tab entry/rendering in `SettingsApplicationDetails`. - `ApplicationManifestMigrationService` syncs `settingsCustomTabFrontComponent` from application manifests again (`syncDefaultRoleAndSettingsCustomTab`), resolving the front component from `settingsCustomTabFrontComponentUniversalIdentifier`. - Remove the deprecation annotations added by #22156: - `ApplicationDTO.settingsCustomTabFrontComponentId` (drop GraphQL `@deprecated`) - `ApplicationManifest.settingsCustomTabFrontComponentUniversalIdentifier` - the `settingsCustomTabFrontComponentId` column comment on `ApplicationEntity` - Regenerate the corresponding GraphQL schema/types to drop the `@deprecated` reason. The DB column was never dropped, so no schema migration is required. --- _Generated by [Claude Code](https://claude.ai/code/session_01A6aoLa5kZjba9C3uwo6nay)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23256?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
6cc7ed7570 |
Make solo tabs first-class: derived presentation, native editing, unified widget header (#23109)
## Why
Full-page record tabs (Timeline, Tasks, Notes, Files, Emails, Calendar,
Flow) were encoded by storing a `CANVAS` layout mode. That made them a
separate species: editing one didn't feel native (no drag handles, no
way to add a second widget, the tab couldn't adapt), and the widget
pipeline was full of `layoutMode === CANVAS` branches.
This PR replaces the stored mode with two derived rules and one unified
header grammar:
> **Presentation is derived from content, never stored.**
> A list tab with exactly **one widget** renders it **solo**
(full-bleed, it owns the tab). Anything else is a **stack** of boxed
cards. **Edit mode always shows the stack structure.**
No widget taxonomy, no per-type branches: any lone widget owns its tab.
## What
**Presentation model**
- `getTabPresentation({ widgets, layoutMode, isInEditMode })`: solo iff
a list tab has exactly one widget in view mode; grid tabs (dashboards)
and edit mode are always stacks. The pinned left panel is always a
column (a surface rule, not a widget rule).
- Solo view rendering is identical to the old CANVAS rendering
(container height, internal scroll).
- Stacked widgets in the main tab area get one bounded slot rule
(`max-height` + own scroll) so no widget swallows the tab;
pinned/side-column stacks keep their flowing behavior. This only binds
on user-composed mixed tabs, which could not exist before.
**Native editing (the point of the PR)**
- Every record-page tab is edited through the same vertical-list editor:
drag handle, reorder, remove, add widget. Add a second widget to a
Timeline tab and it becomes a stack; remove back down to one and it's
solo again. Nothing is stored, nothing to migrate.
- Fixes the stuck-drag bug found while testing the preview: widgets
publishing header info republished a fresh object on every render
(activity cards build their action from non-memoized hook returns), and
since the widget chrome reads that state above the widget content, any
tab with an activity card sat in an infinite render loop. The loop
starved React's transition lane, which dnd-kit's drop teardown waits on,
so the drag clone and drop outlines froze on screen after a drop. The
header hook now republishes only on real value changes and routes
onClick through a stable wrapper, so callers need no memoization. The
page-layout drag provider also disables the Feedback drop animation so
clone cleanup is synchronous at drop time.
**Unified widget header API**
- A widget's content can publish header info to its chrome via
`usePublishWidgetHeaderInfo({ count, primaryAction })`: a count rendered
in grey next to the title, and a primary action (icon button with
accessible name) on the right in view mode. Instance-scoped state keyed
by widget id, so third-party widgets (front components) can use the same
seam later; the hook no-ops outside a page layout (stories, previews)
and is safe to call with inline, non-memoized values.
- A solo widget's header only appears when the widget published
something: the tab label already names it, so a bare title row adds
nothing. Timeline/Flow tabs stay exactly as today.
- Emails, Tasks, Notes, Files, Calendar publish their count (query
totals, not loaded-page lengths) and action (Compose, New task, New
note, Add file) and stop rendering internal title rows ("Inbox 12", "All
5"): exactly one header per widget everywhere, same grammar.
`ComposeEmailButton`, `AddTaskButton` and the title/button plumbing in
`NoteList`/`AttachmentList`/`TaskList` are deleted.
**Object-aware tabs**
- The hardcoded `SYSTEM_OBJECT_TABS` title allowlist is gone. A tab
renders based on whether the target object supports its widgets: widgets
that read through a relation (Tasks, Notes, Files, Timeline) require the
relation field to exist and be active, while Emails and Calendar
aggregate through the messaging timeline, so a missing participants
relation is fine (Company) and a deactivated one is an explicit opt-out.
System objects on the shared default layout keep exactly Home +
Timeline, now by derivation instead of hardcoded titles.
**Data cleanup**
- Seeds (frontend defaults, server standard template, `twenty app`
scaffolder, docs) write `VERTICAL_LIST`;
`PageLayoutTabLayoutMode.CANVAS` is `@deprecated`, kept read-only for
layouts persisted before this change (they render correctly through the
derivation; no data migration, by design: an in-place flip can't pass
the widget-position/tab-layoutMode validator atomically, and it isn't
needed).
- Locale catalogs are intentionally untouched: the i18n pipeline
extracts and translates the new header labels on main; they fall back to
their English source until then.
## Deliberate view-mode changes (approved)
- A lone widget of any type now owns its tab full-bleed: lone Fields tab
(mobile/side panel), lone rich-text Note tab, lone chart, and the
message-thread page lose their card box.
- Activity tabs show the unified header (title, grey count, + action)
instead of their internal "Inbox 12"-style rows.
Everything else is pixel-parity, including solo scroll behavior and
dashboards.
## Test plan
- `nx typecheck twenty-front` / `twenty-server`: clean; oxlint/oxfmt on
the changeset: clean
- 239 suites / 1474 tests across page-layout, activities, side-panel
pass, including new tests for `getTabPresentation` (count-based,
edit-mode override) and `usePublishWidgetHeaderInfo` (publish, cleanup
on unmount, no-op outside a widget, referential stability across
re-renders with inline actions, latest-onClick wrapper)
- `getTabsRenderableForTargetObject` tests covering missing vs
deactivated relations, Emails/Calendar without a participants relation,
and non-relation widgets
- Stuck-drag repro verified fixed end to end against a local stack with
an instrumented dnd-kit: before the fix the affected tab committed ~65
renders/second at idle and drops never tore down; after it, idle commits
are flat and every drop cleans up
|
||
|
|
fb52635d2a | Add defineUninstallLogicFunction hook for applications (#23227) | ||
|
|
148dc6dfaa |
Let server route resolvers answer the caller synchronously (#23233)
## Problem
A server route resolver can only return a dispatch target (`{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`), and
`ServerRouteTriggerService` always acks `202 {queued:true}`. The target
function runs off the queue, after the response has been sent, so its
return value can never reach the caller.
That makes it impossible to integrate a provider whose webhook URL has
to be proven with a handshake on the same response. Slack's Events API
is the case that surfaced it: `url_verification` sends `{ type,
challenge }` and will not accept the Request URL unless the challenge
comes back on that POST.
## Change
A resolver may now return a `Response` (the existing
`LogicFunctionHttpResponse`) instead of a dispatch target. The route
sends it as-is via `buildRouteTriggerResponse` and enqueues nothing.
- Reuses the marker and builder that HTTP route triggers already use, so
there is no new response shape.
- Dispatch results behave exactly as before; the resolver error path is
unchanged, just hoisted out of `parseResolverResult` so it runs before
the branch.
- SDK: `ServerRouteResolverResult` becomes `ServerRouteDispatchResult |
LogicFunctionHttpResponse`.
Additive: a resolver that returns a dispatch target sees no behavior
change. Previously, returning this shape threw
`RESOLVER_INVALID_RESULT`.
## Testing
`server-route-trigger.service.spec.ts` gains a case asserting the
resolver's response is sent verbatim and nothing is enqueued. 15/15
pass.
## Context
Split out of #22984 (Slack conversational assistant), which needs this
to complete the Slack Events URL verification. That PR depends on this
one merging first.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23233?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
2c79093b74 |
feat: agent roleUniversalIdentifier for manifest-driven role assignment (#23206)
## Summary - Adds optional `roleUniversalIdentifier` on `AgentManifest` / `defineAgent` so apps can declaratively assign a role to an agent (same config shape as `defaultRoleUniversalIdentifier`). - Wires `agentUniversalIdentifier` as a sync many-to-one FK on `roleTarget`, and emits a deterministic `roleTarget` from the agent during app sync (create / update / delete). - Enables app agents (e.g. Slack assistant) to get a role on install without postInstall hooks or manual admin assignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23206?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6623901eb4 |
chore: bump version to 2.25.0 (#23221)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23221?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
04d1c2035c |
feat(connections): run a logic function on connection provider connect (#23167)
## What Adds an optional `onConnectLogicFunctionUniversalIdentifier` field to the connection provider manifest. When set, the referenced logic function is dispatched right after an OAuth connection is successfully established for that provider. This gives apps a first-class "on connect" hook — e.g. the Slack app can resolve the workspace's `team_id` via `auth.test` and claim the `team_id -> workspaceId` mapping in the SERVER key-value store immediately on connect, instead of racing against later events. Follow-up to the app key-value store PR (#23089). ## How - **twenty-shared**: add `onConnectLogicFunctionUniversalIdentifier` to `ConnectionProviderManifest`. - **twenty-sdk**: expose the field in `defineConnectionProvider` and validate it is a UUID `universalIdentifier`. - **twenty-server**: - add a nullable `onConnectLogicFunctionUniversalIdentifier` column to `ConnectionProviderEntity` (+ fast instance command / migration). - map the field through the <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23167?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
3ee8b72aa3 |
twenty-sdk env var to disable prov check (#23155)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23155?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
4c4a154d31 |
key-value storage for applications (#23089)
## What
Key-value storage for applications, as proposed in
twentyhq/core-team-issues#2391 — built on the existing `keyValuePair`
entity:
- Nullable `applicationId` relation on `keyValuePair` + a new
`APPLICATION_VARIABLE` type (fast instance command included)
- GraphQL CRUD on the metadata schema (`appKeyValue`, `setAppKeyValue`,
`deleteAppKeyValue`), requiring an `APPLICATION_ACCESS` token —
`applicationId` always comes from the token, never from arguments, so
apps can't touch each other's entries
- `kv.get` / `kv.set` / `kv.delete` helpers in
`twenty-sdk/logic-function`
## Scopes
- **`INSTALL`** (default): entries are private to one workspace install;
arbitrary JSON values
- **`GLOBAL`**: entries are shared across every install of the app, with
claim semantics — the value is always the claiming `workspaceId` and
only that workspace can overwrite or delete the key (guarded writes,
race-safe via insert-if-absent)
Since `applicationId` identifies an install (one row per workspace),
GLOBAL entries are stored under the registration owner workspace's
install so all installs of the same app share one namespace.
The GLOBAL scope is what enables cross-workspace webhook routing: e.g.
the Slack app's `serverRoute` resolver (running in the owner workspace)
can resolve `kv.get('slack:team:' + team_id, { scope: 'GLOBAL' })` to
find the workspace that connected that Slack team — without a workspace
being able to hijack another's mapping.
## Follow-ups
- Wire the Slack assistant PR (#22984) to write the claim at connect
time and read it in the events resolver
- `kv.*` access from front components
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23089?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
10a313dc7f |
chore: bump version to 2.24.0 (#23152)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23152?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
6ece4ce1b1 |
chore: bump npm packages to 2.23.0-alpha.1 (#23084)
## Summary Bumps the published npm packages to a prerelease `2.23.0-alpha.1` version: - `twenty-sdk` - `twenty-client-sdk` - `create-twenty-app` Cross-package references between these use `workspace:*`, so no dependency version updates were needed. --- _Generated by [Claude Code](https://claude.ai/code/session_014Q11uYzAGECHPLN4Mjng5f)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23084?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1be5a0e54a |
System side effect relations (#22882)
Closes twentyhq/core-team-issues#2667 ## What Default relations to the standard relation objects (`timelineActivities`, `attachments`, `noteTargets`, `taskTargets`) are now fully owned by the **metadata side-effect engine**. Neither the API transpilers nor the SDK manifest builder provision them anymore: any object creation, rename or deletion — regardless of the caller — goes through the same engine handlers. ## Why - Provisioning was duplicated across the API path and the SDK manifest builder, with diverging behavior. - Universal identifiers of relation fields were derived from object **names**, so renaming an object mutated them and forced lossy delete+create cycles on manifest sync. ## How ### Engine-owned lifecycle (side-effect handlers) - `objectSystemRelationsOnCreate`: provisions the 8 forward/reverse relation fields (+ join column indexes) when an object is created. - `objectSystemRelationsOnUpdate`: renames the reverse morph fields (`target<ObjectName>`) when their host object is renamed — a lossless `fieldMetadata.update`. - `objectSystemSideEffectsOnDelete`: cascades deletion of engine-owned fields/indexes when the object is deleted. - The API transpilers and the SDK `buildManifest` no longer inject these fields; `isSystemSideEffect: true` marks engine-owned entities, guarded by a granular property allowlist (only `isActive` is user-editable) and excluded from manifest deletion inference. ### Name-free deterministic universal identifiers New `getSystemRelationFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier, relationTargetObjectUniversalIdentifier })` in `twenty-shared`, exported from `twenty-sdk/define`. The identifier is keyed on the two **object** identifiers instead of field names (direction encoded by argument order), so object renames never mutate relation field identifiers. It cannot collide with the name-based `getFieldUniversalIdentifier` derivation (field names cannot contain `:`). ### twenty-standard re-owned All 48 forward/reverse system relation field declarations in `STANDARD_OBJECTS` now pin the derived name-free identifiers (computed inline via the shared util) and carry `isSystemSideEffect: true`, with labels/icons declared explicitly (translated via `msg`). `twenty-standard` is projected as if the engine had generated these fields itself. ### 2.23 upgrade commands - `reconcile-system-relation-field-universal-identifier`: structurally matches existing default relation fields per workspace and backfills the derived universal identifiers, `isSystemSideEffect` flags, and standard labels/icons. - `upgrade-people-data-labs-application`: upgrades installed PDL apps to `1.0.7` right after the backfill to close the desync window (its views reference the re-derived identifiers). ### Misc - `people-data-labs` `1.0.7`: views temporarily pin the new derived identifiers (TODO: import from the next released `twenty-sdk`). - `UpgradeStatusModule` split out of `UpgradeModule` so the application module cluster can consume upgrade status/migration services without importing the versioned command bundles (fixes a require cycle that crashed boot). - Docs: `system-fields.mdx` documents the system relation fields and their resolver; `sync-and-recovery.mdx` plan example no longer shows auto-injected relations. ## Known red CI `people-data-labs (dockerhub-latest)` fails by design until the 2.23 server image is published: the app pins the new identifiers which only exist on a 2.23 server. The `local` leg (server built from this branch) is green. ## System fields are no longer manifest-authorable (accepted regression) The manifest converter no longer derives `isSystem` / `isSystemSideEffect` from field names. Reserved-system-named manifest fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are now skipped at conversion time when they carry the exact derived universal identifier (keeps manifests built with older SDKs installable), and rejected with `INVALID_INPUT` when they pin any other identifier. System fields are therefore fully engine-canonical: nothing a manifest carries can produce a system-flagged entity anymore. **Accepted regression**: a manifest can no longer influence system field properties at all. Previously a (legacy) re-declaration could shape them at creation — which actually produced broken system fields, e.g. a nullable, non-unique `id` — and could still toggle the allowlisted `isActive` / `universalSettings` afterwards. We consider this acceptable for now: per-app granularity over system fields will be reintroduced later through the **override framework**, which will also settle update semantics by forbidding direct updates over `isSystemSideEffect: true` entities and expressing divergence as overrides. `isSystemSideEffect`-only entities (the default relation fields provisioned by this PR) still have no engine-level update guard (see Follow-up below); that part is unchanged and also lands with the overrides refactor. ## Follow-up `isSystemSideEffect` field update/delete guards intentionally live at the API layer (`sanitize-raw-update-field-input.ts`, `from-delete-field-input-...util.ts`) rather than in the engine-level `FlatFieldMetadataValidatorService`. Moving them into the validator requires threading operation-origin (direct field mutation vs engine cascade) through the migration matrix, otherwise legitimate object rename/delete cascades (which carry `isSystemBuild=false`) would be rejected. Tracked in twentyhq/core-team-issues#2671. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22882?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8d84a0b9f3 |
feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context Follow-up to #22609. Lets a non-admin developer claim ownership of a public Twenty app they published to npm, then request a marketplace listing that a server admin reviews. Marketplace state is per-instance for now. ## Claiming - Developer tab gets a **Claim an application** section: look up an unclaimed npm app by package name or universal identifier. - Ownership is proven with GitHub OAuth against the package's npm provenance (trusted publishing): the connected account must own the GitHub account or organization the package was published from. - Errors from the GitHub callback come back as a code and are shown inline with a link to the relevant documentation. - The old one-click claim stays admin-only. - A **Sync catalog** button triggers a catalog refresh instead of waiting for the hourly cron. - Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag. ## Listing requests - Catalog-synced apps are created **unlisted**; a data migration unlists previously auto-listed unclaimed npm apps (owned or vetted rows are left untouched). - Owners request a listing from the Distribution tab (logo + description required); a server admin approves or rejects it from a **Listing requests** section in the Admin Panel. ## Screenshots <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/> <img width="1512" height="828" alt="image" src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
716e67a276 |
Copy application source files during build and install (#22991)
## Summary This is a requirement for the 2-way sync feature - Copy logic function and front component source files into the build output during SDK app builds. - Share application file list construction between install and build paths so source and built artifacts stay aligned. - Allow app installs to skip missing optional source files for backward compatibility while still requiring built artifacts. - Extend watcher handling to track source-file uploads and restart when the source set changes. - Update integration coverage to assert built outputs and copied source files for minimal apps. <img width="940" height="165" alt="Screenshot 2026-07-17 at 14 53 40" src="https://github.com/user-attachments/assets/b9306990-5fc0-4866-a390-3bb8c21a88ad" /> <img width="579" height="181" alt="Screenshot 2026-07-17 at 14 53 57" src="https://github.com/user-attachments/assets/af282318-76c3-49a9-b62a-833a0aadc688" /> |
||
|
|
d99f57bc5e |
chore: bump version to 2.23.0 (#22975)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22975?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
0dbae2eda3 |
Address #22827 review comments and converge application file endpoints (#22868)
Follow-up to #22827, addressing the review comments left around merge time and applying the endpoint convergence discussed afterwards. ## Review comments from #22827 - **Swallowed error in dev sync asset read** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571513265)): the swallow is intentional (a missing public asset must not fail the whole dev sync) but it now logs a warning with the asset path and error, and the registration keeps its previously stored file for that path instead of losing it. - **`isAbsoluteUrl` location** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571524234)): moved to `twenty-shared/utils/url`. The server, and now also `twenty-sdk`'s `normalize-application-assets`, use the shared util. - **Soft delete vs file cleanup** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571589558)): per review, deleting a registration is now a hard delete. Stored assets (bytes + rows) are deleted with it, dependent rows are removed by their existing FK cascades, and installed applications keep working with their registration link nulled. No soft-delete/cron mechanism. - **Asset cap too generous** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571595745)): lowered to 10MB per review and documented in the publishing and public-assets docs pages. - **One missing image retriggers a full asset sync** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571646243)): `storeRegistrationAssets` now takes `skipAlreadyStoredPaths`; the catalog sync passes it when the package version is unchanged, so only assets missing a stored file are fetched instead of re-downloading everything. - **`existing.logo` already contains the new logo** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571667847)): correct, `updateFromManifest` runs first, so the previous "keep fileId when the path did not change" guard compared the new logo against itself. The fileId preservation is now keyed on the stored server file for the exact path (files are unique per `(applicationRegistrationId, path)`): a changed logo path no longer inherits the old file's id, and a transient download failure on an unchanged path still keeps the working file. This also removed the fileId-preservation bookkeeping from `storeRegistrationAssets`. ## Endpoint convergence - **Path-addressed public route for registration assets**: `GET /file/server/application-registration/:fileId` is replaced by `GET /files/application-registrations/:registrationId/*path`, mirroring the manifest's public-folder paths and leaving room for a future `:version` segment. Assets stay addressable by stable ids server-side; the fileId now only marks a path as stored. No URL is ever persisted (all are built at query time), and the old route never shipped in a release, so there is nothing to migrate. - **`Application.logoUrl` resolved server-side**: new `ResolveField` on the `Application` type builds the `/public-assets/...` display URL (or passes absolute URLs through). `useApplicationChipData` now reads it from `currentWorkspace.installedApplications`, and the frontend `buildApplicationLogoUrl` util is deleted, so clients no longer construct file URLs themselves. ## Validation - Unit: `file.controller.spec` (route renamed, traversal case added), `server-file-storage.service.spec` (`findServerFile`, `deleteByApplicationRegistrationId`), `application-registration-asset-url.service.spec` (new URL shape, url-encoding), new `isAbsoluteUrl` test; all application/file suites pass. - Live against a local server: new route serves tarball and rehosted npm assets with `public, max-age=3600` (nested paths included), 404s on missing files, unknown registrations, traversal attempts, and the removed old route; `findManyApplicationRegistrations` returns path-addressed URLs for stored assets, CDN fallback for npm, absolute passthrough; `installedApplications.logoUrl` resolves the public-assets URL and stays null for logo-less apps. Registration hard delete verified against the DB: file rows cascade, application rows keep a nulled registration link. - Typecheck + lint on twenty-server, twenty-front, twenty-shared, twenty-sdk; metadata codegen and client-sdk regenerated. |
||
|
|
055e8b5335 |
Add tab param to open a record side panel page on a specific tab (#22905)
## Context
`CommandOpenSidePanelPage` (and `openSidePanelPage` in the front
component SDK) could open a record in the side panel, but always landed
on the default tab. This adds an optional `tab` param to the
`ViewRecord` page params so an app command can open a record directly on
a specific tab.
## What changed
- **twenty-sdk**: `OpenSidePanelPageParams` `ViewRecord` variant accepts
an optional `tab` (a page layout tab id). Since
`CommandOpenSidePanelPage` props are `OpenSidePanelPageParams`, the
component picks it up automatically.
- **twenty-front**:
- New `setRecordPageActiveTabId` util resolves the record page layout
for the object (custom layout from the store, or the default layout id)
and presets `activeTabIdComponentState` on the tab list instance
(`${pageLayoutId}-tab-list-${recordId}`), which is shared by the side
panel and the full record page.
- `useOpenRecordInSidePanel` accepts `tab` and presets the active tab
before navigating; it also applies when the record is already open in
the side panel (tab switch only).
- `useFrontComponentExecutionContext` forwards `tab` to the side panel
open, and presets the tab when falling back to full-page navigation
(mobile, or objects that can't open in the side panel).
- **Docs**: mention the optional `tab` id in the
`CommandOpenSidePanelPage` description.
Unknown tab ids are harmless: `PageLayoutTabListEffect` falls back to
the layout's default tab when the preset id doesn't exist in the layout.
Dashboards are skipped since their layout id comes from record data, not
object metadata.
## Tests
- `useOpenRecordInSidePanel`: new test asserting the active tab atom is
preset on the correct tab list instance id.
- `useFrontComponentExecutionContext`: new tests for tab passthrough to
the side panel and tab preset on full-page fallback.
- `npx nx typecheck twenty-front`, `typecheck twenty-sdk`,
`lint:diff-with-main twenty-front`, `lint twenty-sdk` all green.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VaWY1H9RZqgJkytqZakvZi)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22905?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
ca437d374f |
chore: bump version to 2.22.0 (#22867)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22867?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
b2a4bb0e0c |
docs(apps): add Targeting System Fields page (#22856)
## What
Adds a docs page teaching app developers how to reference auto-created
**system fields** (`createdAt`, `updatedAt`, `id`, …) from views and
other entities, and makes the API it documents real by exporting
`generateDefaultFieldUniversalIdentifier` from the SDK.
## Why
System fields are provisioned by the server, so they're never declared
with `defineField()` and have no importable `universalIdentifier`
constant. Since 2.19 their universal identifier is derived
deterministically from the application id, the object id and the field
name. Hardcoding an invented id fails sync with `INVALID_VIEW_DATA:
Field metadata not found` (this is exactly what broke the
twenty-partners `createdAt` view column).
The twenty-partners app already imports
`generateDefaultFieldUniversalIdentifier` from `twenty-sdk/define`, but
the function was never exported from the SDK. This PR adds the export
and documents the pattern.
## Changes
- **New page** `data/system-fields.mdx` — "Targeting System Fields":
- Lists the 8 system fields (`id`, `createdAt`, `updatedAt`,
`deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`).
- Explains the deterministic derivation and the sync error from
hardcoding ids.
- Documents `generateDefaultFieldUniversalIdentifier({
applicationUniversalIdentifier, objectUniversalIdentifier, fieldName })`
with a full `defineView` example.
- Contrasts with standard objects (use
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<object>.fields.<field>.universalIdentifier`)
and notes that `name` is a default, not system, field.
- **SDK export** — new `generate-default-field-universal-identifier.ts`
wrapping the existing `getFieldUniversalIdentifier` from
`twenty-shared/application` (`name` → `fieldName`), exported from
`define/index.ts`.
- Registered the page in `docs.json` (Data group) and cross-linked it
from the Views doc.
## Notes
`node_modules` isn't installed in this environment, so `nx typecheck`
wasn't run. The wrapper is a signature-matched pass-through and the
`twenty-shared/application` subpath + `getFieldUniversalIdentifier`
barrel export were both verified to exist.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017B7VivHcqZYjn3ukestY9U)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22856?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
ab9e6f30b8 |
chore: bump version to 2.21.0 (#22820)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22820?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
945724e016 |
accepts any valid expression, not just an identifier (#22778)
Fixing and issue with front component definition look at the unit test to see the actual fix |
||
|
|
9e20e2222a |
Fix front-component serving on Safari, kill stale presigned caching, and cache built bundles client-side (#22672)
## Context Built front-component bundles are served via `GET /rest/front-components/:id/:cacheKey`. On S3-backed storage (Twenty Cloud) the endpoint used to 302-redirect the worker's authenticated fetch to a presigned S3 URL. That redirect caused two bugs, and fixing it removed the caching the redirect was accidentally providing — so this PR also adds a proper client-side cache. Closes twentyhq/core-team-issues#2653. ### Bug 1 — Safari 403 (Authorization header forwarded across redirect) The renderer worker fetches the bundle with `Authorization: Bearer`. The controller answered with a 302 to a presigned S3 URL. Per the Fetch spec, browsers must strip `Authorization` on a cross-origin redirect. Chrome/Firefox do, but Safari/WebKit forwards it, so S3 receives both a query-string signature and an `Authorization` header and rejects with `InvalidArgument: Only one auth mechanism allowed`. Result: front components never load in Safari on S3-backed storage. ### Bug 2 — 302 cached publicly (browser-independent) The redirect branch set no `Cache-Control`, so a CDN could cache it far beyond the presigned URL's TTL (`STORAGE_S3_PRESIGNED_URL_EXPIRES_IN`, 900s). Consequences: any client re-served the cached 302 after 15 min hits an expired signature (403, also affects Chrome), and the cached redirect containing a live presigned URL is served to unauthenticated requests (short-lived auth bypass). ### Regression this introduces — warm-load caching lost Marking the handoff `no-store` (Bug 2 fix) is correct, but it means the built bundle is no longer cached anywhere on the S3 path. The browser HTTP cache cannot compensate: the presigned URL that actually returns the bytes carries a fresh `X-Amz-Date`/`X-Amz-Signature` on every request, so each download is a brand-new cache key and never hits. Net effect without mitigation: every worker mount re-downloads the full bundle. ## What changed - **Front components return a 200 JSON body instead of a 302.** The controller now responds `200 { url }` with `Cache-Control: private, no-store`. The worker parses the JSON and issues a separate header-less `fetch(url)` to S3. No redirect means the `Authorization` header is never forwarded, making it browser-independent, and the handoff carrying the presigned URL is never cached. The stream path (local storage) is unchanged. - **Client-side bundle cache in the renderer (restores warm loads).** `fetchComponentSource` wraps the fetch chain in a `CacheStorage` layer keyed by the **content-addressed** `/front-components/:id/:checksum.js` URL. A hit returns the stored bundle and skips **both** the `no-store` handoff to Twenty and the S3 download — restoring cross-session warm loads without ever persisting a presigned credential. Because `CacheStorage` is writable by any same-origin code (including the untrusted component code this cache feeds), cached content is verified against the sha-256 checksum embedded in the URL on every read, and evicted on mismatch. Caching degrades to a plain fetch where `CacheStorage` or WebCrypto is unavailable. - **sha-256 checksums for built front components.** The SDK build and workspace prefill now fingerprint built front-component bundles with sha-256 (WebCrypto has no md5), enabling the integrity check above. Other file folders keep md5. Legacy md5-fingerprinted URLs (32-hex) simply bypass the cache — already-synced components keep working and start benefiting from caching on their next build/sync. - **WebKit e2e coverage.** Added a `webkit` project to the postcard example's Playwright config mirroring `chrome` (shared setup + storageState), plus iframe/worker diagnostics logging so front-component failures surface in the test log. `TZ` is pinned to `Europe/Paris` because WebKit on Linux ignores Playwright's `timezoneId` emulation and rejects the runner's legacy `CET` alias, which crashed the record page before the component could render. ### Why we hand off to S3 instead of streaming through Twenty On S3-backed storage we deliberately **do not** proxy/stream the bundle bytes through the API. The controller returns the presigned URL and the worker fetches the content directly from S3, for two reasons: - **Server CPU/bandwidth.** Streaming every bundle on every cold load would put the API server on the hot path for all front-component content. Handing off to S3 keeps that load off the server. - **Domain isolation.** Front-component content is fetched from the object-storage domain (e.g. `s3.domain.com`), a different origin than the API and the front app. Serving untrusted/app-authored bundle content from a separate domain than `twenty.com` keeps it off the app's origin. The stream path is kept only as the local-storage fallback (no S3/presign available), where these concerns don't apply. ## Examples ### The JSON handoff (S3 path) ```http GET /rest/front-components/d3b07384-.../a1b2c3d4.js HTTP/1.1 Host: twenty.com Authorization: Bearer <worker-token> ``` ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: private, no-store {"url":"https://s3.domain.com/bucket/.../checkout-widget.mjs?X-Amz-Date=20260709T091500Z&X-Amz-Expires=900&...&X-Amz-Signature=AAAA1111..."} ``` The worker then fetches that presigned URL **without** headers (the Safari fix) and gets the bundle bytes. ### Why the browser HTTP cache can't reuse it | | Load 1 (09:15) | Load 2 (09:30) | Same key? | |---|---|---|---| | Twenty handoff URL | `.../a1b2c3d4.js` | `.../a1b2c3d4.js` | ✅ but response is `no-store` | | Presigned `X-Amz-Signature` | `AAAA1111...` | `ZZZZ9999...` | ❌ | | Effective S3 URL (the HTTP cache key) | `...&X-Amz-Signature=AAAA1111...` | `...&X-Amz-Signature=ZZZZ9999...` | ❌ new key → miss | ### What the CacheStorage layer stores ``` key = https://twenty.com/rest/front-components/d3b07384-.../a1b2c3d4.js (stable, chosen by us) value = <bundle JS bytes> (NOT the presigned URL) ``` Keying by the stable logical URL (not the volatile URL the bytes arrived from) is the one thing the native HTTP cache can't express. The presigned URL is used once and discarded. ### Invalidation No TTL and no explicit delete — invalidation is by key change. A rebuild changes the checksum → changes the URL → guaranteed miss on the new key. The old entry is orphaned and reclaimed by normal browser eviction (quota/LRU; Safari ITP after 7 idle days). Global invalidation lever: bump the cache name suffix (`front-component-source-v1`). ## Deploy note — front/server release window Old frontend bundles (already-open tabs) hitting the new server receive the JSON handoff where they expect raw JS and fail to render until the tab is reloaded. The other direction is safe: the new worker against an old server follows the 302 transparently (the content-type check falls through to `response.text()`). Accepted as a short deploy-window trade-off. ## Follow-ups (not in this PR) - The client-side cache is a bridge for the `no-store` presigned handoff. If built components are later served from a stable, non-signed, public-by-URL path (they are already content-addressed by checksum, so `immutable` is safe), the browser + CDN cache natively and this custom layer can be removed. - `GET /file/:fileFolder/:id` presigned 302s still carry no `Cache-Control`. An explicit policy there (bounded `private, max-age` below the presigned TTL) was prototyped in this PR and deliberately dropped to keep the scope on front components — the file path authenticates via a query-param token (part of any cache key), so its exposure differs and deserves its own PR. ## Non-goals Per the issue, file serving keeps its query-param token + 302 model. Native browser loads (`<img>`, downloads) cannot do a two-step fetch and already work on Safari. The public-asset redirect is left untouched since its caching is intentional. ## Test plan - Renderer: `fetchComponentSource.spec.ts` covers cache miss + write, verified cache hit (no network), poisoned-entry eviction, checksum-mismatch (never cached), non-fingerprinted and legacy-md5 URL bypass, and the no-`CacheStorage` / no-WebCrypto fallbacks. `fetchComponentSourceFromNetwork.spec.ts` covers the direct JS response, the JSON handoff follow-through (header-less presigned fetch), and error mapping. - e2e: the postcard front-component spec now runs on both Chromium and WebKit against prod-parity storage (S3 + Lambda). - `oxlint` + `oxfmt` clean; typecheck passes on changed packages. ### Reproduction proof — Safari was always broken (e2e probe) We ran the prod-parity postcard e2e suite (S3 storage + Lambda) with WebKit against **`main` without this fix**, via a throwaway probe PR: twentyhq/twenty#22717. Result — [ci-privileged run 29015624468](https://github.com/twentyhq/ci-privileged/actions/runs/29015624468): ``` 1 failed [webkit] › card-front-component.spec.ts:61 › renders the postcard name and status badge in the record preview 2 passed (1.4m) ``` `[webkit]` times out waiting for `getByTestId('postcard-card')` to become visible (*element(s) not found*) while the Chromium run of the same spec passes. This confirms the front component **never rendered in Safari** on S3-backed storage prior to this PR — it is a genuine, browser-specific bug, not a flake. The fix in this PR is expected to turn that same `[webkit]` assertion green. Note: running the WebKit tests in CI requires the WebKit browser binary and its system dependencies in the e2e job (now installed via `npx playwright install --with-deps chromium webkit`). |
||
|
|
1c8b8970fd | Allow CLI dev mode on catalog-synced apps without mutating the shared registration (#22756) | ||
|
|
23cae2040a |
Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.
This makes assets always bundled files:
- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.
Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.
Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
|
||
|
|
1fe6156cf5 |
fix(twenty-sdk): restore require/__filename/__dirname in rolldown ESM CLI bundle (#22711)
## Problem Fixes #22708. `twenty-sdk` 2.19.0 CLI commands run through the ESM entrypoint (`node dist/cli.mjs dev --once`, `app:uninstall`, ...) crash on startup: ``` Error: Calling `require` for "fs" in an environment that doesn't expose the `require` function. ``` ## Root cause The 2.19.0 release switched bundling from esbuild to **rolldown** (Vite 7 → 8). Rolldown **inlines CommonJS dependencies** into the ESM output (`dist/cli.mjs` grows from ~6k to ~136k lines). Those third-party CJS modules — e.g. `typescript`, pulled in via `ts-morph` — call `require(...)` and read `__filename` / `__dirname` at load time. None of those exist in an ES module: - `require(...)` is routed through rolldown's interop shim, which **throws** when `require` is absent (i.e. in a `.mjs` file). - `__filename` / `__dirname` are simply `ReferenceError: … is not defined in ES module scope`. esbuild (2.18.0) injected these CJS globals for node-targeted ESM output; rolldown does not. Because the offending usage lives in **bundled third-party CJS**, prefixing our own imports could not fix it. ## Fix Add a banner to the **ESM output only** in `vite.config.node.ts` that recreates the CJS globals from `import.meta.url`: ```js import { createRequire as __twentyCreateRequire } from 'node:module'; import { fileURLToPath as __twentyFileURLToPath } from 'node:url'; import { dirname as __twentyDirname } from 'node:path'; const require = __twentyCreateRequire(import.meta.url); const __filename = __twentyFileURLToPath(import.meta.url); const __dirname = __twentyDirname(__filename); ``` This is the same `createRequire` pattern the repo already uses for the `twenty-oxlint-rules` ESM build. The CJS output already provides all three, so the banner is not applied there. This PR also prefixes the SDK CLI's own Node-builtin imports with `node:` (using native ESM imports instead of the interop shim for our own code) — good hygiene and guarded by a unit test, but note the **banner is the actual bug fix**. ## Verification (built and run locally) - Built the node bundle and reproduced the crash on the pre-fix build (`Calling require for "fs"`), then a follow-on `__filename is not defined` once `require` was restored. - With the banner, ran the previously-crashing commands against the built `dist/cli.mjs`: - `--help` → prints usage, exit 0 - `dev --once` → reaches "Checking server… Cannot reach Twenty server" (normal, no local server) - `app:uninstall` → reaches the interactive confirmation prompt - CJS bin (`dist/cli.cjs`) still works. ## Tests - `cli-esm-bundle-startup.integration.spec.ts` — runs the **built** `dist/cli.mjs --help` and asserts no require/ESM-scope crash. Demonstrated **red without the banner, green with it**. It runs in the `sdk-test` job (which builds the SDK before tests); it fails loudly in CI if the artifact is missing and skips locally when unbuilt, so it is never silently green in CI. - `node-builtin-import-protocol.test.ts` — guards the `node:`-prefix hygiene across the CLI source. Note: the existing `sdk-e2e-test` never caught this because it runs the CLI via `tsx` on the TypeScript **source**, which has no rolldown shim — only the bundled `.mjs` reproduces the crash. |
||
|
|
60fd322b49 |
Centralize system field side effects + search field metadata (#22594)
## Introduction Closes twentyhq/core-team-issues#2635 and twentyhq/core-team-issues#2642 and twentyhq/core-team-issues#2589 Object system fields (`searchVector` + its GIN index + `searchFieldMetadata`, the reserved system fields, default relations) were provisioned through several scattered, path-specific code paths. As a result the **app-manifest sync path** authored objects with an empty/`NULL` `searchVector` and **zero `searchFieldMetadata`**, so app-owned objects shipped a broken generated search column (see #22657). The generation logic also lived partly in imperative services rather than in the metadata side-effect engine, and relied on non-deterministic (`v4`) universal identifiers that `twenty apply` could not converge, destroying manually backfilled rows. This PR centralizes every object-creation system side effect into the **metadata side-effect engine**, extends the engine to keep search metadata consistent on field delete and object relabel, makes the standard app's search identifiers deterministic, and ships upgrade commands to reconcile existing workspaces. ## What changed ### Side effects moved into the metadata side-effect engine New dedicated, self-contained handlers — so every write path (API and app manifest) gets identical results, and side effects never trigger other side effects. **Object create / delete** (`handlers/object-metadata`) * **`objectSystemFieldsOnCreate`** — generates the 7 reserved system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`). * **`objectSearchVectorOnCreate`** — provisions the full-text search surface as one unit: the `searchVector` `TS_VECTOR` field, its backing GIN index, and the `searchFieldMetadata` row (for searchable objects whose label identifier is a searchable field) that keeps `searchVector` populated instead of `NULL`. * **`objectSystemSideEffectsOnDelete`** — tears the above down on object deletion. **Search-metadata consistency on relabel / field delete** (new — these are what close the manifest-path gaps) * **`objectSearchVectorOnUpdate`** (`handlers/object-metadata`) — when a searchable object is relabeled onto a new searchable field, provisions the `searchFieldMetadata` row that indexes it. Relabeling is **additive**: existing rows (e.g. the provisioned `name` row) are preserved, so the previous label identifier stays searchable. Mirrors the API update path so a manifest re-sync that changes the label identifier reaches search parity. No-ops for junction objects (`id` label identifier) and non-searchable field types. * **`fieldSearchFieldMetadataOnDelete`** (`handlers/field-metadata`) — when a field is deleted, cascade-deletes every `searchFieldMetadata` row that indexes it. `searchFieldMetadata` is excluded from manifest deletion inference, so this explicit cascade is what covers **both the API and manifest paths** (the object-scoped DB cascade only fires on object deletion). Uses the `searchFieldMetadataUniversalIdentifiers` aggregator on the flat field for an O(k) lookup instead of scanning all rows. The **default `name` field and default relations are now caller-provided default fields** (SDK autocomplete on the manifest path, input transpiler on the API path) rather than system side effects — removing duplicate name generation, the imperative `build-default-*-for-custom-object` utilities, and the ad-hoc system-field integrity validator. ### Deterministic identifiers for the standard app The twenty-standard search GIN index and `searchFieldMetadata` now derive deterministic universal identifiers (`getIndexUniversalIdentifier` / `getSearchFieldUniversalIdentifier`) instead of `v4`, so `twenty apply` converges instead of recreating. ### Upgrade commands (`2-20`) to reconcile existing workspaces **Instance commands** (run once per instance; ordered fast → slow → workspace): 1. **`AddIsSystemSideEffectToSearchFieldMetadata`** (fast) — adds the `isSystemSideEffect` column to `core.searchFieldMetadata`. Defaults to `true`, which also correctly backfills every existing row since `searchFieldMetadata` is always system-derived (never user-authored). 2. **`BackfillNameFieldIsSystemSideEffect`** (slow) — re-flags existing `name` fields from `isSystemSideEffect: true` → `false`, since the default `name` field is now a caller-provided default like any other user-owned field (it was provisioned as `true` in 2.15 → 2.19). This is a pure data backfill, so the bulk `UPDATE` lives in `runDataMigration()` rather than `up()` — keeping it out of the fast schema transaction avoids holding an `ACCESS EXCLUSIVE` lock that could stall reads during the deploy. Slow instance commands still run before every workspace command of the version, so the fresh value is in place before the search-reconcile workspace commands recompute the `fieldMetadata` flat-entity cache. Scoping by name alone is safe (no engine-owned field is named `name`); `down()` is best-effort (pre-2.15 `false` rows are indistinguishable from flipped ones). **Workspace commands** (idempotent, dry-run supported): 1. **`reconcile-search-vector-gin-index-universal-identifier`** — re-owns every searchVector GIN index UID to its deterministic value (all applications), then backfills the missing GIN index for installed-app objects. 2. **`reconcile-search-field-metadata`** — re-owns every `searchFieldMetadata` UID (all applications), then backfills the missing rows for installed-app searchable objects. 3. **`rebuild-installed-app-search-vectors`** — rebuilds the `searchVector` column of every installed-app `TS_VECTOR` field, once the index and rows exist. Design notes: * **Re-own is global** (twenty-standard, workspace-custom, installed) — a UID convergence keyed on each row's own application. * **Backfill is installed-app only** — standard/custom objects already have these rows via the manifest funnel. * Re-own runs **before** backfill and is transaction-guarded; a failure aborts that workspace to avoid a unique-identifier collision. ## Tests * Integration: app manifest sync now asserts system fields + searchable objects (searchVector, GIN index, searchFieldMetadata) are created; a new relabel suite drives three manifest syncs and asserts records stay searchable through the old + new label identifiers and lose searchability when a field is removed; removed the obsolete system-fields-integrity suite/snapshots. * Unit: per-handler side-effect specs (including the new `objectSearchVectorOnUpdate` and `fieldSearchFieldMetadataOnDelete` handlers), and per-util specs for the re-own / backfill operation builders and the GIN-index classifier. ## Upgrade / migration notes * Existing workspaces converge on the next upgrade run via the `2-20` instance + workspace commands (idempotent, dry-run supported). * Backfill and rebuild go through the workspace-migration runner (automatic cache invalidation); the re-own step invalidates only the affected flat-entity maps directly. * The cross-version upgrade CI now flushes the cache before running the upgrade, so the new version recomputes every flat-entity map from the database instead of reading blobs the old version serialized in an older shape. ## Follow-up * `object-metadata.service.ts` still carries a `TODO: remove once default view fields move to the metadata side effect engine` — default view fields are the next candidate to move into the engine. * A single manifest sync cannot yet both create a field and relabel the object onto it, because `objectMetadata.update` is ordered before `fieldMetadata.create` in the migration runner. Tracked in twentyhq/core-team-issues#2655; to be fixed in a follow-up. |
||
|
|
cdabef6429 |
fix(sdk): report the connected server's real version in dev version check (#22670)
## Summary The `yarn twenty app dev` version row was reading the "local server" version from the `twenty-app-dev` Docker container's baked-in `APP_VERSION` env var. When the CLI is actually pointed at a separately running instance (or a stale `twenty-app-dev` container is lying around), this reported a version unrelated to the server serving requests — e.g. showing `Server v2.5.3` and a bogus "days behind" warning while the real instance was on `2.16.1`. This changes the version resolution to ask the server the CLI is connected to for its real version over HTTP, falling back to Docker inspection only when the server can't be reached. - Add `getServerVersionFromApi`, which reads the version from the public, no-auth `/.well-known/mcp/server-card.json` endpoint (its `version` is the server's `APP_VERSION`). Returns `null` gracefully on timeout, non-OK responses, or missing/`0.0.0`/non-semver values. - `getVersionInfo` now uses `getServerVersionFromApi() ?? getLocalServerVersion(containerName)`, running the API call in parallel with the Docker Hub published-versions fetch. All downstream logic (`isMinorOrMajorBehind`, `daysBehind`, the dev UI row, and the headless warning) now operates on the real version. |
||
|
|
97e21c3403 |
feat(ai): optionally preselect fast or smart model when opening Ask AI (#22679)
Allow front components to preselect the AI model when opening the Ask AI side panel with a preprompt. - Add an optional `model: 'FAST' | 'SMART'` to the preprompt in the openSidePanelPage AskAI params of the front-component SDK. - openAskAiPageWithPreprompt resolves FAST to the workspace fast model and SMART to the workspace default (null selection = pinned default model), writing to agentChatUserSelectedModelState. |
||
|
|
163c96c2e5 |
Validate range version app dev sync (#22625)
# Introduction Also now validating the workspace version when running a sync manifest <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22625?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9423af7f67 |
feat(server): add public marketplace resolver for vetted app catalog (#22647)
## What Adds a public GraphQL resolver so unauthenticated clients (the public website) can read the listed/vetted marketplace catalog without a workspace token. - `MarketplacePublicResolver` (metadata schema) exposes two public queries guarded by `PublicEndpointGuard` + `NoPermissionGuard`: - `publicMarketplaceApps` - `publicMarketplaceAppDetail(universalIdentifier)` Both delegate to the existing `MarketplaceQueryService` (no new logic, no new REST routing). The existing workspace-guarded `findManyMarketplaceApps` / `findMarketplaceAppDetail` queries are untouched. - Adds a shared `ApplicationCategory` type in `twenty-shared` (known values plus `string` for backward compatibility) used to type `ApplicationManifest.category`. A warning is logged server-side when an app declares a category outside the known set. ## Why This is the backend half of the public apps marketplace on the website. Splitting it out so the server-side catalog exposure can be reviewed independently from the website UI. ## Follow-up The website PR (the `/apps` marketplace UI) consumes `publicMarketplaceApps` and should merge after this one. --- _Generated by [Claude Code](https://claude.ai/code/session_01GBfegArtJcoiTLSsnWPH8R)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22647?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: martmull <martin@twenty.com> |
||
|
|
d746909184 |
feat(sdk): validate graph page-layout widgets at build time (#22559)
When an app defines a graph widget (aggregate, pie, bar or line chart), the built manifest can carry the wrong key and the server rejects it at sync time with a confusing "aggregate field is required" error. The SDK type already requires `aggregateFieldMetadataUniversalIdentifier` and renames the raw `aggregateFieldMetadataId` at compile time. But the manifest build runs esbuild with no type checking, so a wrong or missing key slips through and only fails later on the server. This adds a build-time check that mirrors the server validator, with a hint pointing at the right key when the raw one was used. It is non-breaking since correctly authored apps already use the universal key. Tests: unit tests on the validator, plus a real graph widget added to the rich-app fixture so the integration and e2e suites cover the happy path. |
||
|
|
3b1a0ef3e6 |
chore: bump version to 2.20.0 (#22639)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22639?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
9086b031e8 |
chore: bump sdk packages to 2.19.0-alpha.1 prerelease (#22599)
## Summary - Bumps `twenty-sdk`, `twenty-client-sdk` and `create-twenty-app` from `2.19.0` to `2.19.0-alpha.1` so the CD pipeline can publish a prerelease of the SDK. ## Context #22565 made system field universal identifiers deterministic and fail-closed: any app package built with SDK ≤ 2.18 carries legacy system field identifiers in its `manifest.json` and is now rejected at install/sync time on servers running `main`. Rebuilding the apps requires a published SDK carrying the new derivation. Publishing `2.19.0-alpha.1` lets us rebuild all apps in `packages/twenty-apps` (follow-up PR) against the new derivation while keeping `latest` on `2.18.0` for authors targeting prod, which has not run the 2.19 backfill yet. ⚠️ The publish job must tag this prerelease under a non-`latest` dist-tag (e.g. `next`): the server resolves app installs and the upgrade version check against the `latest` dist-tag. Server version constants (`TWENTY_CURRENT_VERSION`, etc.) are intentionally untouched. ## Test plan - [ ] Verify the three package versions are `2.19.0-alpha.1` - [ ] Verify the publish workflow in the CD repo tags the release as `next` (not `latest`) Made with [Cursor](https://cursor.com) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22599?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8580cd6f27 |
feat(ai): open Ask AI side panel with a preprompt in two modes (#22582)
Add the ability to open the Ask AI side panel pre-filled with a prompt from any frontend component, with a mode to control whether the message is sent automatically or left for the user to review. - agentChatPrepromptState: holds the pending preprompt and its mode (PREFILL = fill only, SEND = fill and auto-submit) - useOpenAskAiPageWithPreprompt: seeds the new-thread draft, opens a fresh Ask AI thread and stores the preprompt intent - AgentChatPrepromptEffect: applies the intent once the chat editor and send listener are mounted, either restoring the editor content or dispatching the send event and clearing the editor <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22582?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6c40c7b91a |
Deterministic system field universal identifier (#22565)
# Introduction Close twentyhq/core-team-issues#2641 Auto-provisioned field metadata used to get its `universalIdentifier` from three unrelated sources: random `v4()` on the server when creating custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc `v5` derivation in the SDK manifest build. This PR unifies all of them behind the shared `getFieldUniversalIdentifier` derivation: ``` universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName) ``` ## Ownership model The rollout is built on an explicit split of who owns a field's universal identifier: - **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are **server-owned**. Their universal identifiers are always the deterministic derivation, on **every** application (standard, workspace-custom, installed). Clients cannot provide custom values: a temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects any non-derived system field identifier at migration build time. This check stands in until system fields are generated exclusively server side by the metadata side-effect engine and stripped from client inputs — at which point it becomes structurally impossible to send one. - **`name` is a default field, not a system field**: it is auto-provisioned when absent (server side for custom objects, SDK side for application objects) but authors can define their own. It is only derived where it is guaranteed to be auto-provisioned. In particular, standard objects keep their **historical hardcoded** `name` identifiers: the standard app authors its `name` fields like any installed app would, and moving those identifiers would break every installed application referencing them (e.g. views on `opportunity.name`). - **User-created and author-provided fields** keep random / explicit identifiers, untouched. ## Server - `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of the existing type/`isSystem` checks, that each system field's `universalIdentifier` equals the deterministic derivation. Runs for every object creation going through the migration orchestrator: app sync, custom object creation, standard provisioning - `build-default-flat-field-metadatas-for-custom-object.util.ts` derives the system field identifiers (and the auto-provisioned `name`) with `getFieldUniversalIdentifier` instead of `v4()` - `build-default-relation-flat-field-metadatas-for-custom-object.util.ts` derives both the forward and the reverse default relation field identifiers deterministically - `generateMorphOrRelationFlatFieldMetadataPair` accepts optional `sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so callers can inject deterministic values; user-created relations still default to `v4()` ## twenty-shared - `STANDARD_OBJECTS` system field identifiers (the 8) are now computed at module load via `buildStandardObjectSystemFields`; `name` and every other identifier keep their hardcoded values - New snapshot test pinning **every** universal identifier of `STANDARD_OBJECTS`: any identifier change now requires an explicit snapshot update and should ship with a coordinated backfill ## SDK (breaking, pre-GA) - `generateDefaultFieldUniversalIdentifier` delegates to `getFieldUniversalIdentifier` and now requires `applicationUniversalIdentifier` - Reverse default relation field identifiers are derived from the field's real coordinates (standard object UID + actual field name, e.g. `targetRocket` on `attachment`) instead of the legacy custom-object UID + synthetic `${fieldName}Inverse` hash input. Field *names* are unchanged - The manifest build threads the application universal identifier through default field injection (two-pass over object configs) - `twenty dev:add` now resolves the application universal identifier upfront and refuses to scaffold anything until `defineApplication` declares one — no more `fill-later` placeholder for the app UID in generated files ## Upgrade A 2.19 **workspace command** backfills existing `fieldMetadata.universalIdentifier` rows to the deterministic derivation. Coverage follows the ownership model: - **The 8 system fields**: taken over for **every application**, whatever value they currently hold. This is both safe and required now that sync rejects non-derived values — leaving a row unconverged would make its application unsyncable - **`name`**: workspace-custom app → always taken over (server-generated, no author to clobber); installed applications → only rows still carrying the legacy SDK derivation are recomputed, author-provided identifiers are never touched; standard app → never touched (hardcoded in `STANDARD_OBJECTS`) - **Default relation fields**: workspace-custom app → forward fields on custom objects and reverse fields on the standard relation objects; installed applications → legacy-derivation probe only All identifiers of a workspace are updated inside a single transaction, then the command flushes the field-metadata-related workspace caches and bumps the metadata version. Stored `applicationRegistration.manifest` snapshots are intentionally **not** rewritten: installs and upgrades always sync from the `manifest.json` inside the resolved package (npm/tarball), the stored column is only used for display/marketplace purposes. ## Breaking behavior for old packages (fail closed) Packages built with an older SDK carry legacy system field identifiers in their tarball `manifest.json`. Installing or upgrading such a package now fails with an explicit `INVALID_SYSTEM_FIELD` validation error ("universal identifier is not deterministic") instead of silently mismatching against the backfilled rows and triggering a destructive delete+create. The remediation is to rebuild the package with the new SDK; the backfill has already converged the installed rows, so the rebuilt manifest syncs cleanly. ## Test plan - [x] `twenty-sdk` unit tests (526 tests) and typecheck - [x] `twenty-shared` unit tests (1635 tests) including the `STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte identical to `main` - [x] Lint and typecheck clean on all touched packages - [x] Integration: create a custom object and verify system + default relation field identifiers match the deterministic derivation (`create-one-object-metadata-deterministic-field-universal-identifiers`, 13 assertions passing) - [x] Integration: `failing-sync-application-object-system-fields` extended with a non-derived system field identifier case; all identifiers in the spec pinned deterministically so snapshots embedding expected/actual values are stable across runs (verified with a double run) - [x] Integration: all application sync suites pass with the derived system field identifiers now required by the `buildDefaultObjectManifest` test helper (9 suites, 20 tests) - [x] Full test-database reset: standard app provisioning and seeded workspaces pass the new validation - [x] SDK manifest build verified on the postcard example app: all auto-generated default field identifiers match the derivation - [ ] Run `upgrade:2-19:backfill-deterministic-field-universal-identifiers` (dry-run then real) on a seeded workspace and verify identifier convergence with a rebuilt app manifest |
||
|
|
25fe66565c |
feat(applications): add type and options to application variables (#22157)
## Before <img width="1452" height="709" alt="image" src="https://github.com/user-attachments/assets/cd384ffa-cbe6-49d5-a807-ca8d580f55a9" /> <img width="1074" height="452" alt="image" src="https://github.com/user-attachments/assets/720d38db-3495-4032-8831-17d24ec6a7e7" /> ## After <img width="1421" height="865" alt="image" src="https://github.com/user-attachments/assets/2275c996-c895-4800-8324-2aa2ddfddd43" /> <img width="1348" height="870" alt="image" src="https://github.com/user-attachments/assets/3e1a891d-6db0-4cbd-870a-2a5bbde4929d" /> ## Summary Adds typed application variables with optional select **options**. This is the other half of #22059, split out from the custom-settings-tab removal. ## Changes - **Shared types**: `ApplicationVariable` / `ServerVariables` gain an optional `type` (a `FieldMetadataType` subset — `TEXT`, `BOOLEAN`, `NUMBER`, `DATE`, `SELECT`, `MULTI_SELECT`, `RAW_JSON`, `RICH_TEXT`, `ARRAY`, …) and select `options`. New `serializeApplicationVariableValue` / `deserializeApplicationVariableValue` helpers convert typed values to/from the encrypted string storage. - **Server**: `type`/`options` columns on `applicationVariable` and `applicationRegistrationVariable` (entities + DTOs), a fast `2-17` instance command, manifest processing via the serialization helpers, and a `QueryDeepPartialEntity` cast where the manifest JSON column is persisted. - **Frontend**: a polymorphic `SettingsApplicationVariableInput` that renders the native `Form*` field component for each type (boolean, number, date/date-time, select, multi-select, array, raw JSON, rich text, text); fragment/query updates to fetch `type`/`options`. - **SDK**: `defineApplication` validates that `SELECT`/`MULTI_SELECT` variables declare non-empty `options` at build time (since `options` is kept structurally optional for TypeORM/SDK compatibility). Variables default to `TEXT` when no type is given, so existing manifests are unaffected. ## Notes The generated GraphQL artifacts (`type`/`options` on the variable types) are regenerated by codegen; that change accompanies this PR. https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23 --- _Generated by [Claude Code](https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22157?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
29e48e16ba |
[Breaking change] fix: make pageLayout type field required (#22450)
fixes https://github.com/twentyhq/twenty/issues/22251 **Summary** - Fixes #22251 — NavigationMenuItem with type PAGE_LAYOUT returns 404 "Off track" for custom standalone pages - Makes type a required field in PageLayoutManifest instead of relying on a fallback default to RECORD_PAGE - Adds PageLayoutType enum to twenty-shared and exports it from the SDK for app developers - Adds build-time validation in definePageLayout to reject manifests missing type - Updates the CLI add command to prompt users to select a page layout type interactively **Root cause** When definePageLayout was called without type, the manifest converter defaulted to RECORD_PAGE. The frontend route guard at /page/:id then rejected it (only STANDALONE_PAGE is allowed), producing a 404. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22450?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
11b2990dd6 |
fix(twenty-sdk): stop dev-mode OOM by caching compiled manifest modules (#22435)
## Context Closes twentyhq/core-team-issues#2601 `twenty dev` crashed with a Node.js heap OOM (`FATAL ERROR: Reached heap limit — JavaScript heap out of memory`) after a while of editing. ## Root cause `loadModule()` in `manifest-extract-config-from-file.ts` compiled every manifest-defining file with **`vm.compileFunction`** on every manifest rebuild. V8 pins every function compiled through the `vm` module and never releases it ([nodejs/node#35375](https://github.com/nodejs/node/issues/35375)). In the dev loop this is on the hottest path and heavily amplified: - `runSyncPipeline` → `buildManifest` re-globs **all** `.ts/.tsx` files and recompiles every entity file on **every** sync — not just the edited one. - A single save triggers 2+ full rebuilds (the manifest watcher change → `scheduleSync`, then the esbuild watcher's `handleFileBuilt` → `scheduleSync` again). - Each compiled unit is the full esbuild bundle — hundreds of KB, up to MBs for front components (React/JSX inlined). So over an hour of editing, thousands of `vm.compileFunction` calls × large source, all permanently retained → multi-GB heap → crash. This matches the reported profile exactly. Investigation ruled out (with evidence): chokidar watchers (disposed on restart), ts-morph/`createProgram` (dead code, not in the dev loop — typecheck runs in child `tsc` processes), the event log (hard-capped at 200), Ink timers/subscriptions (all cleaned up), and graphql-sse (only used by `logs`). ## Change Keep only the **latest build per file**, keyed by file path. Each cache entry stores the file's last bundled-output hash and its compiled wrapper: - Rebuild with **unchanged** output → reuse the existing wrapper (no recompile). - Output **changed** → overwrite the entry, so the file's previous build is dropped instead of accumulating. This bounds the cache to one entry per file rather than one per rebuild, so old builds no longer pile up in the heap. The wrapper is still executed fresh into a new module shim on every call, so extraction behavior is unchanged. One file changed: `packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts`. ## Test - Behavior of `extractManifestFromFile` is unchanged (fresh execution per call); only redundant recompilation is eliminated and stale builds are dropped. - Note: `yarn install` could not complete in the authoring sandbox (a git-based transitive dep of `twenty-desktop` is blocked by the proxy), so lint/typecheck/tests were not run locally — relying on CI. ## Follow-ups (not in this PR) - Redundant **double-sync per save** (`start-watchers-orchestrator-step.ts`) triggers two full rebuilds per edit. - Latent **event-log display bug**: new events stop appearing once the 200-event cap is reached. |
||
|
|
3c13dc1ab8 |
feat(twenty-sdk): include app readme in published package (#22431)
## Context When running `yarn twenty app:publish`, no README was included in the npm-published app package. Closes twentyhq/core-team-issues#2632 ## What changed - Added `copy-readme-to-output.ts`, which finds the app's root readme file (matched case-insensitively, preferring the markdown variant, mirroring how npm ranks README candidates) and copies it into the build output directory (`.twenty/output/`). - Wired `copyReadmeToOutput` into `buildApplication` — the shared build path used by `publish`, `build`, and `dev` — so the readme is present when `npm publish`/`npm pack` runs from the output directory. npm only ships a README when the file lives in the package root, which for published apps is `.twenty/output/`. The readme is not tracked in the manifest checksums; it is a pure npm packaging artifact, so it is only copied into the output directory and does not affect app installation/validation. ## Tests - Added unit tests for `findReadmeFileName` (case-insensitivity, markdown preference, ignoring unrelated files) and `copyReadmeToOutput` (copies the readme into the output dir; no-ops when the app has no readme). https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL --- _Generated by [Claude Code](https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22431?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
55ed4b7adb |
feat(sdk): translate front-component strings with t()/Trans/useTranslate (#22301)
## What
Lets app **front components** localize the strings they render,
extending the
existing application-translation pipeline (which today only covers
manifest
labels) to component source. App authors mark strings with a small,
familiar
API; the build extracts and bakes them; the runtime resolves them for
the
user's locale.
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
<Trans>Loading postcard…</Trans>
<Trans context="card-title">Untitled</Trans> // disambiguation
const empty = t('No content yet…'); // works outside JSX
<p>{t('Saved {count} cards', { count })}</p> // interpolation
const STATUSES = [{ id: 'draft', label: msg('Draft') }]; // lazy descriptor
```
## How
- **Runtime** (`twenty-sdk/front-component`): `t()` (eager, usable
anywhere —
event handlers, helpers, module scope), `msg()` (lazy descriptor),
`<Trans>`
(reactive JSX), `useTranslate()` / `useLocale()`. Source-string
fallback,
`{name}` interpolation, and `context` disambiguation. No build-time
macro —
these are plain runtime functions.
- **Extraction**: a `ts-morph` scan collects `t()`/`msg()`/`<Trans>`
strings
from component source into the same `locales/*.json` catalogs the
manifest
pipeline already writes (`twenty dev:translations-extract`).
- **Delivery**: `twenty dev:build` bakes the compiled per-locale catalog
into
each front-component bundle via an esbuild banner, so the runtime
resolves
with **no server or renderer changes**. Locale comes from the execution
context that already flows to the worker.
The catalog key and `generateMessageId` hashing are shared between the
node
extractor and the browser runtime; `<Trans>` text whitespace is
normalized
identically on both sides so multi-line elements resolve.
## Design notes
- Reuses the existing `extract → compile → manifest.translations`
contract and
`generateMessageId`, so component strings flow through the same
machinery as
manifest labels.
- Self-contained in `twenty-sdk` + a shared pure helper; the server is
untouched.
## Scope / follow-ups
- `twenty dev` (watch) does not bake catalogs yet — preview shows source
strings; use `twenty dev:build` (documented). Wiring the watcher is a
follow-up.
- Usage is documented in twenty-docs under **Apps → Translations**
(`developers/extend/apps/translations`).
## Tests
Unit tests for the catalog-key/interpolation helpers, the runtime
resolver
(hit/miss/context/fallback/interpolation), and the ts-morph extractor
(static `t`/`msg`/`<Trans>`, dynamic-skip, dedup, multi-line
whitespace), plus a
compile test for context→messageId. Verified with an adversarial review
pass.
https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22301?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
1a475d0edd |
feat(twenty-sdk): terraform-style plan/apply for app metadata sync (#22372)
## What & why Syncing a Twenty app's metadata is destructive (removing a field/object drops the backing column/table), but the only preview was `dev --once --dry-run`, which collapsed every change into one line per entity — no before/after, no color, no destructive warning, and no confirmation before a real sync. This introduces a `terraform plan`-style flow. The server's `syncApplication(manifest, dryRun)` already returns a complete `SyncAction[]` (create/update/delete with per-attribute `before`/`after`), so this is a CLI-only change — **no server changes**. ## Command surface `plan` previews, `apply` applies; `dev` is the watch wrapper over the same engine. | Command | Behavior | | --- | --- | | `twenty plan [appPath]` | Render the full plan, read-only | | `twenty apply [appPath]` | Plan → confirm on destructive → apply | | `twenty dev --once` | **Deprecated** alias of `twenty apply` (still works, warns) | | `twenty dev --once --dry-run` | **Deprecated** alias of `twenty plan` (still works, warns) | | `twenty dev` (watch) | Compact summary; inline `[y/N]` confirm on destructive saves | | `-f, --force` | Skip the destructive gate (on `apply` and `dev`) | ## Plan output ``` Twenty will perform the following actions: # objectMetadata "rocket" will be created + nameSingular = "rocket" + labelSingular = "Rocket" # fieldMetadata "name" will be updated in-place ~ label = "Name" -> "Launch name" ~ isNullable = true -> false # fieldMetadata "legacyCode" will be destroyed - name = "legacyCode" Plan: 1 to add, 1 to change, 1 to destroy. Warning: 1 destructive change(s) will permanently delete data. - fieldMetadata "legacyCode" — drops the column and its data Destroys are irreversible. Review carefully before applying. ``` Grouped by metadata type, ordered create → update → destroy, `=` aligned per block. Internal keys (`id`, `workspaceId`, `*Id`, timestamps, nulls) are filtered; updates show only changed keys via the server `diff`. ## Destructive safety gate The server applies the manifest diff atomically, so every apply path computes the plan read-only first, then decides whether to apply: - **`twenty apply` / `dev --once`** — interactive `y/N` prompt when the plan deletes metadata; `--force` skips; **fails closed** (exit 1) in CI / non-TTY. - **`dev` (watch)** — creates/updates auto-apply with the compact summary; a save that deletes metadata shows an inline `y/N` prompt in the Ink UI. **Declining cleanly stops the watch** (exit 1) rather than leaving the session in a nagging/blocked state — since the atomic apply would otherwise also block the additive changes on every subsequent save until resolved. `dev --force` applies deletions without asking. ## Notes - `twenty apply` / `dev --once` now do one extra **read-only** dry-run before applying (to compute the plan + gate). `--force` skips it. - The watch sync step now skips API-client regeneration on any non-synced outcome (error or decline), avoiding a partial client write during shutdown. - The Ink watch UI keeps its existing compact summary; the full plan renders only on the plain-console surfaces — `dev` watch output is unchanged in the common case. ## Test plan - `npx nx typecheck twenty-sdk` ✓ - `npx nx lint twenty-sdk` ✓ - Unit tests (vitest): renderer (`format-sync-actions-plan.spec.ts`) + confirm gate (`confirm-destructive-apply.spec.ts`); existing summary / sync-step specs still green. - Manual against `simple-app` + a local server: `plan`, `apply` (destructive prompt + `--force` + non-TTY fail-closed), and the `dev` watch inline confirm (incl. decline → stop). |
||
|
|
06515408d1 |
fix: drop domain from computed remote name when subdomain present (#22376)
## Context Closes twentyhq/core-team-issues#2619 When adding a remote with `yarn twenty remote:add` and a URL like `https://martin-s-workspace.twenty.com`, the computed remote name ended up as `martin-s-workspace-twenty-com`. The apex domain (`twenty.com`) should be dropped when a subdomain is present, so the name should be `martin-s-workspace`. ## What changed - Extracted `deriveRemoteName` from `remote/index.ts` into its own module `remote/derive-remote-name.ts`. - When the host has a subdomain (more than two labels), only the subdomain labels are used (joined with dashes) — the apex domain is dropped. - Hosts with no subdomain keep the full host (`twenty.com` → `twenty-com`). - Single-label hosts like `localhost` are preserved. - IPv4 addresses are kept intact (`127.0.0.1` → `127-0-0-1`). - Invalid URLs still fall back to `remote`. ## Tests Added `derive-remote-name.spec.ts` covering subdomain, multi-label subdomain, apex-only host, `localhost`, IPv4, and invalid-URL cases. All 6 pass. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22376?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ef7b480063 |
chore: bump version to 2.19.0 (#22363)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22363?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
3031891491 |
improve dry run logs: show entity names and changed fields (#22299)
## Summary Before this change, dry run logs showed raw UUIDs for `update` and `delete` actions, making it hard to understand what changed: ``` updated fieldMetadata 94265b02-25b4-4bd3-9dae-669f9e983c0f updated fieldMetadata 12920ff8-b04f-46d8-97a8-016390dfb2df ``` After this change, logs show human-readable names when available, plus which fields were modified: ``` updated fieldMetadata myField (94265b02-25b4-4bd3-9dae-669f9e983c0f) [label, description changed] updated fieldMetadata anotherField (12920ff8-b04f-46d8-97a8-016390dfb2df) [isActive changed] ``` ### Changes - **`twenty-shared`** — Extended `SyncUpdateAction` and `SyncDeleteAction` types to include an optional `flatEntity` (with `name`, `nameSingular`, `universalIdentifier`) and `diff` (map of changed field names to before/after values). These fields are already populated by the server-side workspace migration builder but were missing from the shared contract. - **`twenty-sdk`** — Updated `formatSyncActionsSummary` to: - Show `name (uuid)` for update/delete actions when a human-readable name is available via `flatEntity` - Append `[field1, field2 changed]` for update actions when a `diff` is present - Keep the existing behavior for create actions (name only, no uuid since there's no top-level identifier) - Updated and extended tests to cover the new display formats. |
||
|
|
0dc6272da5 |
Remove twenty-ui reexport from the SDK and use twenty-ui directly (#22326)
## What & why Removes the `twenty-sdk/ui` reexport. Apps now use Twenty UI by installing [`twenty-ui@1.0.0-alpha.1`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1) from npm and importing its subpaths directly. The reexport re-exported types that didn't resolve, forcing typecheck workarounds. ## Changes - **twenty-sdk**: delete `src/ui/index.ts`, drop the `./ui` export, remove it from the browser vite build, and rewire the CLI manifest-mock to `twenty-ui` (`.css` falls through to the empty-CSS loader). `twenty-ui` stays a devDependency for the CLI fixture tests. - **Renderer + create-twenty-app template**: import from `twenty-ui` subpaths; the template pins `twenty-ui@1.0.0-alpha.1`. - **Docs**: new "Using Twenty UI components" section (install + subpath imports + `useTheme()` for theme tokens), codex references, and the cross-doc-contract validator. The `twenty-for-twenty` / `twenty-slack` example apps are intentionally left on `twenty-sdk/ui`: they consume the published SDK (which still ships `./ui`), and `twenty-ui@1.0.0-alpha.1` requires react 19 + a `monaco-editor` peer the react-18 apps can't satisfy. They migrate once the SDK is republished. |