f0c3883fd1c23ba93f8ae30338dc0f50132e8e46
3751 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f0c3883fd1 |
fix(command-menu-item): persist overrides after save and add reset-to-default (#21623)
## Context Command menu items are an overridable entity (like page-layout / FIELDS widgets), but the override flow in layout-customization mode was broken: - **Move / pin-unpin / hide-label didn't persist.** `useSaveCommandMenuItemsDraft` fired the `updateCommandMenuItem` mutations (backend persisted correctly) but never wrote the result back into `metadataStoreState`, the source the live menu and edit panel read from. So the UI reverted on exit and changes only showed after a hard reload. - **No true "reset to default".** Existing reset controls only reverted the draft to the last-saved values (which still contained overrides). there was no way to clear overrides back to the original values after a save. Notes - removed the footer "Reset to default" button. This is not clear to me how we want to build, let's re-implement better in the next version - reset are done on click and not delayed on the save. This is similar to other reset to default on page layouts where we actually usually reload the component and this is because the FE has no idea what's original VS override from the response itself <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21623?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. --> |
||
|
|
2de60d7ea1 |
chore(server): temporary diagnostic logging for empty verification email body (#21628)
## What Adds **temporary** diagnostic logging to `EmailVerificationService.sendVerificationEmail` so we can capture the real error behind the empty verification email body in deployed environments. ## Why Verification emails are delivered with an **empty body** (subject is fine). The body is `<!DOCTYPE html …><!--$!--><template></template><!--/$-->` — an **errored React Suspense boundary**. `@react-email/render`'s `render()` wraps the email in `<Suspense>` and streams via `renderToReadableStream` **without an `onError` handler**, so any throw during SSR is swallowed into the errored boundary and the body ships empty. In production React also strips the error text from the markup, so the cause is invisible. This could **not** be reproduced locally on `main` (renders fine in dev, in the production-focused `yarn workspaces focus --production` install layout, and on the React 18 + react-email 6 dep set), so we need the error from a deployed environment. ## What it logs When the rendered html is empty or contains `<!--$!-->`, it logs (prefix `EMAIL_VERIFICATION_RENDER_DEBUG`): - locale, trigger, html length, and the first 400 chars of the html; - the **real error + stack**, obtained by re-rendering synchronously with `renderToStaticMarkup` (which re-throws instead of swallowing). No behavior change on the happy path — the block only runs when rendering already failed. ## How to use Deploy, trigger a verification email (sign up / resend), then: ``` grep -i "EMAIL_VERIFICATION_RENDER_DEBUG" <twenty-server logs> ``` ## Revert Remove this block once the root cause is identified. |
||
|
|
206120677b |
chore: bump version to 2.15.0 (#21624)
## 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/21624?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> |
||
|
|
fdab89ae02 |
Move twenty-client-sdk to dev dep (#21611)
# Introduction The `twenty-client-sdk` is always provided and injected at runtime by the twenty-server instance Which mean that even if in your app locally you're using twenty-client-sdk `1.0` installing this app on twenty instance `2.0` will result in injecting another `twenty-client-sdk` That's the expected behavior and tradeof The twenty-app devdep should only be used to guide local devxp following typesafety and so on A user can still locally generated its own twenty-client-sdk and publish it if necessary <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21611?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. --> |
||
|
|
97871131a1 |
fix(server): mitigate integration-test OOM flakiness (#21588)
## Problem `server-integration-test` shards have been failing intermittently across unrelated PRs with a distinctive signature: the shard exits code 1 with **no jest assertion failure, no `Test Suites:` summary, and no V8 `JavaScript heap out of memory` error** — the process just dies mid-run. Failures hit random shards and clear on re-run (e.g. an unrelated branch failed shard 6 once, then passed 3× on identical code), while the `merge_group` gate stays green. ### Root cause Each shard runs a **single in-band jest process** that boots one shared NestJS app (`globalSetup` → `app.listen`) and holds it for the entire shard, driving heavy metadata migrations + cache rebuilds in that one process. `NODE_OPTIONS=--max-old-space-size=12288` let V8 grow to 12 GB — *above* the `ubuntu-latest` runner's available RAM (16 GB, shared with Postgres/Redis/ClickHouse). V8 therefore deferred aggressive GC and grew past physical memory, so the **OS OOM-killer killed the process before V8 hit its own ceiling** — which is why there's no heap error and no jest summary, just a silent exit. ## Changes (CI/test-only — prod runtime untouched) - **Lower the integration jest heap cap `12288` → `6144`** so V8 self-limits below physical headroom instead of being OS-killed. Counterintuitively safer: a real leak now surfaces as a *visible* heap error naming the test, rather than a silent death. (`database:reset` keeps 12288 — it runs alone, before jest.) - **Add `--logHeapUsage`** to the integration jest runs to expose the per-file heap trend for confirming/pinpointing the growth. - **Split integration tests across 16 shards (was 10)** to lower the peak working set per shard. - **Make perf logging a first-class `LoggerService` tool** (per @prastoin's review): add `LoggerService.perf()` and unify the existing `time()`/`timeEnd()` helpers into `perfTime()`/`perfTimeEnd()` (now routed through the driver), all gated by a new `PERF_LOG_ENABLED` config var. It **defaults on** so real environments keep emitting the install-perf logs, and `.env.test` sets it `false` to mute the per-action flood in integration tests. The `application-manifest` and `validate-build` services were moved from the built-in `Logger` to `LoggerService` to use it. ## Notes - `--max-old-space-size` lives only in the `test:integration` nx target; it is **not** the prod server heap setting, so prod is unaffected. - This is mitigation. If `--logHeapUsage` shows monotonic growth across files, there's a real accumulation in the long-lived app (retained flat-maps / metadata cache) worth a follow-up heap-snapshot fix. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21588?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. --> |
||
|
|
2f6a267b68 |
chore(server): remove stray comment in flat-entity-maps spec (#21599)
Follow-up to #21585: removes an explanatory comment in the test that slipped through before merge. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21599?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. --> |
||
|
|
8a866dba54 |
Add call recording schema and meeting bot scaffold (#21584)
## Summary - add 2.13 upgrade commands for call recording request status and dropping CalendarEvent recordingPreference - remove the recording preference from the core CalendarEvent standard object - add a scaffold-generated twenty-meeting-bot app with logo and the CalendarEvent meetingBotPreference field ## Tests - yarn install - yarn lint - yarn twenty dev:typecheck - git diff --check <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21584?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. --> |
||
|
|
4be76e3fd1 |
Support morph relations in workflow record nodes (#21403)
## Support morph (polymorphic) relations in workflow record nodes
Morph relations (e.g. a polymorphic `Owner` on `Pet` targeting `Person`
or `Company`) were not selectable in the workflow **Create / Update /
Upsert Record** nodes. This PR adds full support for setting them.
### What changed
**Frontend**
- `shouldDisplayFormField`: allow `MORPH_RELATION` (many-to-one) so
morph fields appear in record forms.
- New `FormMorphRelationToOneFieldInput`: a polymorphic record picker
across the morph's target objects, storing a self-describing value `{
targetObjectMetadataId, id }`.
- Wired the morph branch into `FormFieldInput`.
**Backend**
- New `formatWorkflowRecordMorphRelationFields` util: resolves the form
value (stored under the base field name, e.g. `owner`) into the correct
per-target join column (`ownerCompanyId`), nulling siblings to keep
exactly one target referenced.
- Wired into the create / update / upsert workflow actions (update also
expands `fieldsToUpdate` to the concrete join columns).
### Permissions handling
- The picker's search is scoped to only the morph targets the user can
read (`canReadObjectRecords`), so it no longer breaks when a target
object is inaccessible.
- If an existing value points to an object the user can't read, the
field shows the reused **"Not shared"** lock display instead of an empty
field, while remaining editable when other targets are readable.
### Notes
- No data schema / migration changes — reuses the existing per-target
morph columns and stores the selection in the existing workflow step
JSON settings.
<img width="607" height="717" alt="Screenshot 2026-06-10 at 14 57 40"
src="https://github.com/user-attachments/assets/496442a1-04a5-40f8-8b56-b28e38b00d5a"
/>
Also handles the case where the selected record is not readable
<img width="596" height="737" alt="image"
src="https://github.com/user-attachments/assets/c5ffb94e-3838-4db5-853e-f8e490331f23"
/>
|
||
|
|
02d6e2d76f |
perf(server): avoid O(n²) when building flat entity maps (#21585)
## Problem
After 2.13, server CPU stepped up and stayed up. Sentry profiling of
`POST /metadata` pins it on
`addFlatEntityToFlatEntityMapsThroughMutationOrThrow` — ~26% self-time
plus a long tail, turning metadata-write requests into multi-second
(~18s observed) operations.
The hot stack is:
```
WorkspaceMigrationValidateBuildAndRunService.computeAllRelatedFlatEntityMaps
└ getSubFlatEntityMapsByApplicationIdsOrThrow
└ addFlatEntityToFlatEntityMapsThroughMutationOrThrow
```
Every metadata migration rebuilds the twenty-standard application's flat
sub-maps — thousands of entities, across every involved metadata type —
through this util.
## Root cause
`addFlatEntityToFlatEntityMapsThroughMutationOrThrow` maintains
`universalIdentifiersByApplicationId` and deduped each insert with
`Array.includes`:
```ts
if (!existingUniversalIdentifiers.includes(flatEntity.universalIdentifier)) {
existingUniversalIdentifiers.push(flatEntity.universalIdentifier);
}
```
That scan is O(n) per insert, so building a map for an application with
`n` entities is **O(n²)**. The twenty-standard application groups
thousands of standard entities under one `applicationId`, so its sub-map
rebuild dominates.
The dedup is also redundant: the function throws `ENTITY_ALREADY_EXISTS`
at the top if the `universalIdentifier` is already in
`byUniversalIdentifier`, and every id pushed to the per-application list
is also written there. So reaching the push guarantees the id is new —
the `.includes()` is always `false`.
## Fix
Drop the scan and push directly → map building is **O(n)**. Behavior is
unchanged (the early throw already enforces uniqueness).
## Test
Adds a unit spec covering indexing, the no-`applicationId` case, the
duplicate throw, and a 20k-entity build that completes instantly (guards
against re-introducing the quadratic).
## Follow-up (separate PR)
This is the bleed-stopper. The deeper issue is that
`computeAllInvolvedApplicationIds` pulls the **entire** twenty-standard
application into the dependency set of every migration and rebuilds
those sub-maps per request instead of caching them. Worth scoping the
dependency set to referenced entities (or caching the standard-app
sub-maps), which I'll raise separately.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21585?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. -->
|
||
|
|
0a99f784eb |
fix(front): dedupe morph relation fields in view field pickers (#21580)
## Issue Reported in quality-feedbacks: **"Issues with morph relation view field"** — a morph relation column added to a view **disappears after refresh** (and can be added several times). ## Root cause — the SSE metadata sync A morph relation is stored as **one `fieldMetadata` row per target object**, all sharing a `morphId`. Collapsing those rows into the single field that represents the relation is a **read-time projection** in the server's `objects.fieldsList` resolver — it is *not* a storage invariant, and the rows are never merged. The frontend metadata store is kept in sync with the raw rows **one row at a time over SSE** (`MetadataStoreSSEEffect`): every metadata change broadcasts a single created/updated record that's pushed straight into the store. Creating a morph relation creates N rows (one per target), so **N `create` events arrive and N raw sub-fields land in the store — bypassing the `fieldsList` projection entirely.** The view-field pickers read straight from that store, so they saw the morph relation **once per target**. Each could be added as a column referencing a different sub-field id; after a refresh the view reloads from the projected (deduped) data, the non-survivor columns no longer resolve, and they disappear. ## Fix & architecture note Because the store deliberately mirrors raw rows (that's what the SSE sync maintains), the fix applies the **same read-time projection on the client** — deduping morph rows by `morphId` in `useActiveFieldMetadataItems` — rather than filtering rows at each insert path (SSE, optimistic create, …). This matches how the backend already models morph fields and is robust regardless of which path delivered the rows. The survivor-selection rule (which sub-field id represents the relation) now lives in `twenty-shared` (`pickMorphGroupSurvivor`) so client and server can't drift. |
||
|
|
5f59ae20bf |
chore: bump version to 2.14.0 (#21593)
## 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/21593?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> |
||
|
|
fb4608e437 |
chore(deps): upgrade Tier-1 deps (googleapis 173, gaxios 7, express 5, jsdom 29, date-fns 4, stripe 20) (#21570)
## What Security-driven upgrade of the biggest-drift Tier-1 dependencies (staying on latest = staying patched). Bundled because they share the lockfile and the googleapis/gaxios pair must move together. | Package | From | To | Gap | |---|---|---|---| | googleapis | 105.0.0 | **173.0.0** | 68 majors | | gaxios | 5.1.3 | **7.1.5** | 2 majors | | express | 4.22.2 | **5.2.1** | 1 major | | jsdom | 26.1.0 | **29.1.1** | 3 majors | | date-fns | 2.30.0 | **4.4.0** | 2 majors | | date-fns-tz | 2.0.0 | **3.2.0** | 1 major | | stripe | 19.3.1 | **20.4.1** | 1 major | `yarn npm audit` reports **0 high/critical** advisories before and after. ## Code changes - **gaxios v7** — `GaxiosError.code` is now `string | number` (guard the calendar network-error check by `typeof`); `GaxiosError` config/response use `URL` + `Headers`; and crucially the v7 constructor drops `response.data` unless `bodyUsed` is set — updated the synthetic gmail error mocks accordingly (production gaxios sets it, so real error parsing is unaffected). - **google-auth-library / gaxios dedup** — `googleapis-common@8.0.2` exact-pins `google-auth-library@10.5.0` + `gaxios@7.1.3` while `googleapis` pulls `^10.2.0`; the two copies made `OAuth2Client`/`GaxiosError` type-identities diverge across every gmail/calendar service. Added two singleton `resolutions` (documented inline in root `package.json`). - **express 5** — no source changes. `@nestjs/platform-express@11.1.24` already resolves `express@5.2.1` internally; the old `4.22.2` pin was the override. - **jsdom 29** — no source changes, but it now pulls ESM-only transitive deps (`@csstools/*` `.mjs`, `parse5`, `entities`, `tough-cookie`, `@exodus/bytes`). Extended the server jest `transformIgnorePatterns` allowlist and added `.mjs` to the transform/extensions so jest can load jsdom. - **stripe 20** — `Subscription` gained a required `customer_account` field; added to mocks. No runtime changes. - **date-fns v4** — `Locale` is no longer ambient (import explicitly in 5 files); per-locale entrypoints dropped the typed `default` export (the locale loader now reads the single named export); fixed the default locale import in `formatTimeZoneLabel`. ## Tests - Full suites green locally: **twenty-server 5709 passed**, **twenty-front 4937 passed**, twenty-ui / twenty-ui-deprecated green; typecheck + builds (swc + vite) + lint all pass. - Added regression tests for the two runtime behaviors these upgrades touch and that had no coverage: - `getDateFnsLocale` — named-export locale resolution (date-fns v4). - `sanitizeFile` — jsdom 29 + DOMPurify still strips `<script>`/event handlers from uploaded SVGs (security guard). ## Deliberately deferred (not in this PR) - **stripe → 21/22**: stripe **21** bundles a runtime `Decimal` type for money fields **and** jumps the pinned API version to `2026-03-25.dahlia` (changes webhook/billing payload behavior) — too risky to fold into a deps bump on billing code. stripe **22** additionally drops the node10-resolvable `types` entry, which would force a repo-wide `moduleResolution` change. Capped at the latest clean **20.x**. - **openid-client → 6**: v6 is a full functional rewrite and its passport strategy manages the OAuth `state` internally, but our SSO flow uses `state` to carry `identityProviderId` across the shared `/auth/oidc/callback`. That needs an auth-flow redesign (session-carried provider id) on Enterprise SSO code with no integration harness — it deserves its own focused PR rather than riding along here. ## Tier-1 source Originated from a dependency-drift audit; remaining Tier-1 items (date-fns done here) plus Tier-2/3 follow-ups tracked separately. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21570?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. --> |
||
|
|
25b0e4d81c |
fix: #19173 correct labels and icons for custom object default relations (#19224)
**### Problem**
When creating a custom Data Model object, the auto-generated Note and
Task relations had incorrect labels ("Note Targets", "Task Targets") and
a wrong hardcoded icon (IconBuildingSkyscraper).
Expected behavior is to use user-friendly labels ("Notes", "Tasks") and
proper icons, consistent with standard objects like Company and Person.
**Root causes:**
* `icon` in `createFieldInput` was hardcoded to
`'IconBuildingSkyscraper'`
* `label` was derived from `targetFlatObjectMetadata.labelPlural`, which
returns system labels (e.g., "Note Targets") instead of display labels
---
**Fix**
* Added `sourceFieldOverridesByRelationObjectNameSingular` map to define
correct labels and icons for all default relation types
* Ensures consistency with standard objects
Mappings:
* noteTarget: "Note Targets" → "Notes", IconBuildingSkyscraper →
IconNotes
* taskTarget: "Task Targets" → "Tasks", IconBuildingSkyscraper →
IconCheckbox
* attachment: "Attachments" → "Attachments", IconBuildingSkyscraper →
IconFileImport
* timelineActivity: "Timeline Activities" → "Timeline Activities",
IconBuildingSkyscraper → IconTimelineEvent
* favorite: "Favorites" → "Favorites", IconBuildingSkyscraper →
IconHeart
* Added type safety using:
`satisfies Record<(typeof
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS)[number], ...>`
This ensures new default relations must be explicitly defined
* Renamed variable:
`icon` → `targetFieldIcon`
for better clarity (it is only used for the target field)
---
**Limitations**
* Applies only to newly created custom objects
* Existing objects will keep incorrect labels/icons
* Requires a separate data migration to fix existing data
---
**Testing**
1. Go to Settings → Data Model
2. Create a new custom object
3. Verify:
* Labels show "Notes" and "Tasks" (not "Note Targets"/"Task Targets")
* Icons match those used in standard objects (e.g., Company, Person)
---
## Update (reworked while merging main)
The original approach was reworked:
- The label/icon mapping now lives in a shared
`STANDARD_RELATION_FIELD_PROPERTIES_BY_RELATION_OBJECT` constant
(`msg`-based, so labels stay translatable), used as the single source of
truth. Dropped the unused `favorite` entry.
- Standard objects now reference that same constant explicitly at each
call site (uniformization) instead of duplicating the values. Objects
that intentionally differ keep their explicit overrides: note/task →
`Relations`, person/workspaceMember → `Events`, workflow attachments →
`IconFileUpload`.
- Fixed an unrelated typo found along the way: Company's
`timelineActivities` icon was `IconIconTimelineEvent`.
- For the history (supersedes the "Limitations" above): added a `2.9.0`
workspace upgrade command
`upgrade:2-9:fix-standard-relation-field-labels-icons` that re-syncs
**standard** objects' default relation labels/icons against the source
of truth. It deliberately leaves **custom** objects untouched — their
relation fields are user-editable and must not be overwritten by an
upgrade.
## Testing / Verification
Verified locally end-to-end:
**New custom objects**
- Created a custom object via the Data Model UI and via the metadata API
— its note/task/attachment/timeline relations now show `Notes` / `Tasks`
/ `Attachments` / `Timeline Activities` with the correct icons instead
of `Note Targets` + `IconBuildingSkyscraper`.
**Standard uniformization (value-preserving)**
- Re-seeded a workspace on this branch and inspected all 25
default-relation field definitions across the 10 standard objects: every
canonical value is unchanged, every intentional variant (Relations /
Events / IconFileUpload) is preserved, and the only diff vs `main` is
the Company `IconIconTimelineEvent` → `IconTimelineEvent` fix.
**Upgrade command (existing workspaces)**
- Simulated a real upgrade: seeded a workspace on `main` (Company icon
typo present), created a custom object via the metadata API (it came out
with the old buggy labels, as expected on `main`), then switched to this
branch and ran the command.
- Confirmed via both the metadata API and direct DB inspection:
Company's standard `timelineActivities` icon healed to
`IconTimelineEvent`, while the custom object's relations were left
untouched.
- Idempotent: re-running reports "already up to date".
**CI**: typecheck, lint, server unit tests, and all server
integration-test shards green.
---------
Co-authored-by: Manish Kumar <manishkumar@Mac.lan>
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
7c0136b97b |
feat(deps): migrate frontend to React 19 (#21531)
## What Migrates the frontend stack from **React 18.3 → 19.2**. The website, sdk, companion and emails packages were already on React 19; this brings the remaining holdouts (`twenty-front`, `twenty-ui`, `twenty-ui-deprecated`, `twenty-front-component-renderer`) and `twenty-server`'s email rendering onto 19, and pins a single React version repo-wide. ## Why React 18.x is now the legacy line. Staying current keeps us on the patched/maintained branch and unblocks downstream library majors (react-router 7, mantine 9, etc.) that require React 19 peers. ## Dependency bumps (required by React 19 peers / removed APIs) | Package | From | To | Reason | |---|---|---|---| | react / react-dom | 18.3.1 | 19.2.3 | core | | @hello-pangea/dnd | 16 | 18 | peer `^18 \|\| ^19` | | react-datepicker | 6 | 9 | v<7 used removed `findDOMNode`; drops `@types/react-datepicker` | | react-data-grid | beta.13 | beta.59 | peer `^19.2`; new render API | | graphiql (+ @graphiql/react, plugin-explorer) | 3 / 0.23 / 1 | 5 / 0.37 / 5.1 | peer `^18 \|\| ^19` | | react-helmet-async | 1.3 | **@dr.pogodin/react-helmet** 3.2 | upstream caps peer at `^18`; drop-in React 19 fork | A `resolutions` pin enforces a single React (19.2.3) + `@types/react` (19.2.14) across the monorepo to avoid duplicate copies / type-identity splits. Versions are the aged lockfile patches (clears the `npmMinimalAgeGate`). ## Code changes - **Global `JSX` shim** (`react-jsx-global.d.ts` per package): React 19 moved the `JSX` namespace under `React.JSX`; several deps' published types (notably `@linaria/react`'s `styled.d.ts`, which types every `styled.x` via `keyof JSX.IntrinsicElements`) still reference the global namespace. Without the shim, every styled component degrades to `any` props. - **Ref nullability**: `useRef<T>(null)` now returns `RefObject<T | null>`; widened consumer prop/hook ref types accordingly (incl. the shared `useListenClickOutside`). - **react-datepicker v9**: `onChange`/`onSelect` accept `Date | null`, `calendarStartDay` typing, `ReactDatePickerProps`→`DatePickerProps`, relaxed the dynamic `selectsMultiple` discriminated union. - **react-data-grid beta.59**: `formatter`→`renderCell`, `editor`→`renderEditCell`, `headerRenderer`→`renderHeaderCell`, `components`→`renderers`, `onRowClick`→`onCellClick`, object-shaped `useRowSelection`, Set-based selection. - **dnd style cast**: `@radix-ui/react-popper` augments `CSSProperties` with a `--radix-*` index signature that dnd's closed `DraggingStyle` doesn't satisfy → cast at the spread. ## Status / testing - ✅ `typecheck` green: twenty-front, twenty-ui, twenty-ui-deprecated, twenty-front-component-renderer, twenty-server - ⏳ build / lint / unit tests / storybook+argos / runtime smoke-test in progress Draft until local + CI verification completes. Notable behavior to QA manually: spreadsheet import (data-grid), date pickers, drag-and-drop boards/lists, GraphQL playground, page titles/favicon. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21531?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. --> |
||
|
|
09f0c9e29a |
fix(address): coerce addressLat/addressLng to numbers in ORM result formatting (#21542)
## Fixes #21390 Saved addresses render as the **"Empty"** placeholder in the record detail / side panel when `addressLat`/`addressLng` are populated (e.g. after picking a Google autocomplete suggestion). The list view shows the address correctly. ## Root cause `addressLat`/`addressLng` are `NUMERIC` composite subfields, stored as Postgres `numeric` columns — which the `pg` driver returns as **strings** to preserve precision. The ORM result formatter already normalizes this for Currency, but not for Address. In `packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts`, `formatCompositeFieldValue` had a case for `CURRENCY.amountMicros` (`parseInt`) but **no case for `ADDRESS`**, so coordinates were passed through as raw strings. This only breaks the **record detail**, not the list view, because: - The **standard GraphQL (Yoga) path** masks it — the `BigFloat` scalar's `serialize()` runs `parseFloat()` and quietly turns the string into a number on the wire. - The **direct-execution path** formats results itself and bypasses scalar serialization, so the string reaches the frontend. There, `addressFieldValueSchema` validates lat/lng with `z.number()` → `isFieldAddressValue` returns `false` → `isFieldValueEmpty` returns `true` → `RecordInlineCellDisplayMode` renders the placeholder. The table cell renders the value directly with no empty-check, so the list view is unaffected. ## Why it surfaced now The `z.number()` constraint on lat/lng is old ("latent since the address guard was introduced"). The trigger was **#19254 (2026-04-03) "Remove direct execution feature flag"**, which made direct execution always-on for workspace queries — the same PR added string→number coercion for aggregates but not for composite subfields. **#21033** (the PR the issue blames) only made `addressStreet1` nullable; it didn't touch lat/lng, but by fixing the overlapping null-street1 case it isolated and exposed this one. ## Fix Add the `ADDRESS` case to `formatCompositeFieldValue`, mirroring Currency. Coordinates are fractional, so `parseFloat` is used (Currency uses `parseInt` because micros are integers). This is the exact operation the `BigFloat` scalar already performs, so there is no behavior change on the standard path — it just makes direct execution consistent, and lat/lng are now numbers everywhere (matching `FieldAddressValue`). No frontend change is needed. ## Scope check — similar bugs in other field types/composites This bug class = a transforming scalar `serialize` that direct execution doesn't replicate. The only scalar that changes a pg-returned type for a real field is `BigFloat` (`NUMERIC` → number). The only `NUMERIC` fields are the two composite subfields: - `CURRENCY.amountMicros` — already handled ✅ - `ADDRESS.addressLat` / `addressLng` — fixed here ✅ Standalone `NUMERIC` is not user-creatable (it's in `SettingsExcludedFieldType`). Other scalars were checked and don't diverge: `Date.serialize` is identity; `NUMBER`/`POSITION` are stored as `float8` and returned as numbers (and `NUMBER` is already coerced in direct execution); `DATE_TIME` resolves to the same ISO string via both paths. So `ADDRESS` was the last gap. ## Tests - New `format-result.util.spec.ts`: `addressLat`/`addressLng` strings parse to numbers, already-number coordinates pass through, numeric-looking text subfields (e.g. `addressPostcode: "10001"`) are **not** coerced, and the existing Currency `amountMicros` coercion still holds. - `npx jest format-result.util.spec` → 4 passed - `npx nx lint:diff-with-main twenty-server` → 0 warnings, 0 errors - `npx nx typecheck twenty-server` → pass ## Follow-ups (not in this PR) - The cross-path parity integration test (#18972) doesn't cover a record with address coordinates — worth adding so this class can't regress. - `formatAddressDisplay` falls back to `ALLOWED_ADDRESS_SUBFIELDS` (which includes lat/lng) when a field has no `subFields` configured, unlike `getEnabledAddressSubFields` (which falls back to the text-only `DEFAULT_VISIBLE_ADDRESS_SUBFIELDS`). Harmless now that coordinates are numbers (filtered by `isNonEmptyString`), but a latent inconsistency. https://claude.ai/code/session_011pf9KQn4UDZGr4V4k8rRHh --- _Generated by [Claude Code](https://claude.ai/code/session_011pf9KQn4UDZGr4V4k8rRHh)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21542?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: Claude <noreply@anthropic.com> |
||
|
|
5d0a4b8db4 |
fix(twenty-front): keep paging record board columns past the second page (#21348)
Fixes #21355 ## Problem On a Record Board (Kanban) view, columns that contain more than 20 records stop loading at exactly 20. The initial query loads the first page, one automatic fetch brings the column to 20, and then the loading placeholder at the bottom of the column **spins forever** — scrolling all the way down triggers no further requests. Every column is permanently capped at `2 * RECORD_BOARD_QUERY_PAGE_SIZE` (20) records. ### Steps to reproduce 1. Open any object in board view, grouped by a field where at least one group has > 20 matching records. 2. Wait for the board to load — the first 20 cards in the large column appear. 3. Scroll that column to the bottom. **Expected:** more cards load as you approach the bottom, until the column is exhausted. **Actual:** the placeholder stays forever; no additional group-by request is fired. ## Root cause An **edge-triggered consumer reading a level signal that is stuck high.** The fetch-more trigger (`RecordBoardFetchMoreInViewTriggerComponent`) is an `IntersectionObserver` sentinel that writes its `inView` state into the board-level `recordBoardShouldFetchMoreComponentState`. Its `rootMargin` is: ```ts const rootMargin = `${estimatedCardHeight * RECORD_BOARD_QUERY_PAGE_SIZE * 2}px`; ``` With `estimatedCardHeight ≈ 130px` and `RECORD_BOARD_QUERY_PAGE_SIZE = 10`, that's ~2600px — roughly two pages, i.e. as tall as the entire already-loaded board. So the sentinel reports `inView = true` across the whole loaded board, and the boolean **latches `true` after the first auto-fetch and never toggles back**. The consumer in `RecordBoardQueryEffect` only reacts to the **false→true edge** of that boolean, and `triggerRecordBoardFetchMore` is a stable `useCallback`. Once the boolean is stuck `true` and the dependency array stops changing, the effect never re-runs — so it fetches exactly once. The signal is *level* ("the bottom is in view, keep loading") but it's consumed as an *edge* ("the bottom just appeared, load once"), and the oversized `rootMargin` guarantees the level is permanently high so the single edge never repeats. The large `rootMargin` is intentional prefetch buffering and is not the bug; the consumer simply needs to keep paging while the signal is high. ## Fix Make the consumer **re-arm** the trigger after every page that actually returned records: 1. `useTriggerRecordBoardFetchMore` now returns a `boolean` — `true` only once at least one column received records this round, `false` on every early-exit / empty result. 2. `RecordBoardQueryEffect` resets `recordBoardShouldFetchMoreComponentState` to `false` after a **productive** fetch. The sentinel is still inside the inflated `rootMargin`, so the observer immediately re-asserts `true`, which re-runs the effect and fetches the next page. The loop terminates naturally and never spins: - **Buffer filled** — enough cards load that the sentinel finally leaves the `rootMargin` → observer reports `false` → loop stops. As the user scrolls, it re-arms (normal infinite scroll). - **Columns exhausted** — `triggerRecordBoardFetchMore` returns `false` (per-column `shouldFetchMore` flags already get set `false` when a page returns `< PAGE_SIZE`), so the boolean is not reset and no further fetch fires — no busy-loop on a fully-loaded board. The existing `recordBoardIsFetchingMore` re-entrancy guard prevents any overlapping/double fetch during the round-trip. ## Test - `npx nx typecheck twenty-front` → passes - `npx nx lint twenty-front` (oxlint --type-aware + oxfmt) → 0 warnings, 0 errors, formatting clean - Manually verified on a board with columns of 38 and 74 records: pre-fix both froze at 20; post-fix they page to completion on scroll, and a fully-loaded board issues no extra requests. ## Notes / alternatives considered - **Shrinking `rootMargin`** would mask the bug for tall boards but defeat the intended prefetch buffering and reintroduce it whenever the buffer is smaller than the loaded content. The level/edge mismatch is the real defect. - **Moving the loop into the trigger component** was rejected — it only knows `inView`, not whether a fetch was productive or whether columns are exhausted, so self-looping there would increase coupling. The query effect is the right owner of fetch orchestration. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
c05f01f2a3 |
fix(server): repair 2.13 isUIReadOnly→isUIEditable rename fallout (#21504) (#21537)
## Context Follow-up to #21504 ("Rename isUIReadOnly to isUIEditable, add isUICreatable…"), which surfaced two issues: 1. **`column FieldMetadataEntity.isUIReadOnly does not exist`** on twenty-main.com. 2. **`cross-version-upgrade` CI failure** — `SyncStandardUiCapabilityFlags` aborts the v1.22 → 2.13 upgrade. ## Fix 1 — don't drop `isUIReadOnly` in the 2.13 rename command The 2.13 fast instance command physically dropped `isUIReadOnly` from `core."fieldMetadata"` and `core."objectMetadata"`. But migrations run in an ArgoCD **PreSync** hook **before** the new pods roll out (`charts/prod-eu/apps/twenty-server` migration Job is `hook: PreSync`, sync-wave `2`; the api/worker Deployments are sync-wave `10`). So the **previous** release's pods keep serving and still `SELECT isUIReadOnly`, throwing `column ... does not exist` from the moment the column is dropped until the rollout finishes. This keeps the column (already hidden from the app via `@WasRemovedInUpgrade` on both entities) and **defers the physical drop** to a later release. Since 2.13 hasn't shipped to self-hosters yet, the committed command is amended in place. Both tables handled; `isUICreatable` (new, additive column) is unaffected. The eventual physical drop + GraphQL-compat removals are tracked in twentyhq/core-team-issues#2542. ## Fix 2 — allow `isUIEditable` updates on relation field metadata `SyncStandardUiCapabilityFlags` re-syncs `isUIEditable` on standard fields, including morph/relation fields (the activityTargets `target*` relations). The flat-field-metadata validator only permits a fixed property allow-list on relation fields, which omitted `isUIEditable`, so the command failed with `FIELD_MUTATION_NOT_ALLOWED` and aborted the upgrade (leaving workspaces in a FAILED state). `isUIEditable` is a per-field UI-affordance flag that applies to relation fields too, so it's added to the relation-field updatable properties (a constant used **only** by that validator — no diff-engine side effects). ## Coherence notes - Object-level is covered: the drop is deferred on **both** tables, and `ObjectMetadataEntity` has the identical decorators. - `isUICreatable` needs no change: object-only and additive (no drop → no rolling-deploy hazard), and never reaches the field relation allow-list. - The object-metadata validator has no relation allow-list, so there's no object-level analog to change. ## Verification - `nx typecheck twenty-server` ✅ (the `satisfies` guard holds — `isUIEditable` is a `toCompare` property of `fieldMetadata`) - `oxlint --type-aware` + `oxfmt --check` ✅ on changed files - Fix 2's path is exercised end-to-end by the `cross-version-upgrade` CI that originally caught it. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
fefb6cdb94 |
feat(page-layout): add number format option to aggregate chart widget (#21521)
## Context Closes #21522 Large values in the dashboard **Number** widget are always abbreviated (e.g. `1300090` → `1.3m`) with no way to display the full number. Following a discussion with the core team who were interested in this feature (https://discordapp.com/channels/1130383047699738754/1509604545381142649) , this adds a **Format** option in the **Style** section of the Number (aggregate chart) widget, letting users choose between **Short** (abbreviated, current behavior) and **Full** (complete number with thousand separators). Only the displayed value of the Number widget is affected — axes, labels and tooltips of other chart types are intentionally left untouched. ## What's inside **Server** - New `ChartNumberFormat` GraphQL enum (`SHORT` / `FULL`), following the `AxisNameDisplay` pattern - The existing — and previously unused — `format` field on `AggregateChartConfigurationDTO` is now typed with this enum and validated with `@IsEnum` - The dashboard AI tool schema (`widget.schema.ts`) accepts the new `format` option - Regenerated GraphQL types and the `twenty-client-sdk` metadata client to reflect the enum **Front** - New **Format** setting in the Style section of the Number widget settings, with a Short/Full selection dropdown (same pattern as the Axis name setting) - `transformAggregateRawValueIntoAggregateDisplayValue` takes an optional `numberFormat`: - `FULL` → full number via `formatNumber` (currency values keep up to 2 decimals) - `SHORT` → abbreviated via `formatToShortNumber` - not set → behavior unchanged (currency short, number full), so existing widgets and the record table/board footers render exactly as before ## Screenshots | Full UI Look | <img width="1917" height="955" alt="Twenty_Showcas_FullShort" src="https://github.com/user-attachments/assets/05d05779-395d-4e1a-8ff0-964f6fbef182" /> | Menu UI Look | <img width="291" height="308" alt="Screenshot_2" src="https://github.com/user-attachments/assets/82b5a1de-32fe-46ec-a9b8-add11ab4c6cd" /> ## Tests - Extended `transformAggregateRawValueIntoAggregateDisplayValue` unit tests with SHORT/FULL cases for currency and number fields - Updated the page-layout-widget creation/update integration tests and snapshots to use `ChartNumberFormat.SHORT` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21521?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: Charles Bochet <charles@twenty.com> |
||
|
|
5d892bdfd0 |
[WIP] Feat/marketing emails (#21173)
Marketing/campaign emails on top of the emailing-domain (SES) feature:
send a broadcast to a hand-picked list, with per-customer-domain
unsubscribe links and opt-out-only **unsubscribe topics**.
## Model
Standard objects (workspace schema, flat-metadata):
- `messageCampaign` — a campaign send (subject, body template, from
address, status, list, optional unsubscribe topic).
- `messageList` + `messageListMember` — the hand-picked audience (person
↔ list join). A campaign's recipients are its list's members; everyone
is sendable unless suppressed.
Core entities (`core` schema, workspace-scoped — readable by the public
unsubscribe flow without a workspace context):
- `unsubscribeTopic` — an opt-out-only category (name, description,
visibility). There is no opt-in subscription state.
- `messageSuppression` — the single consent store: a row with
`unsubscribeTopicId` NULL is a global block; a row with an
`unsubscribeTopicId` and reason `UNSUBSCRIBE` is a per-topic opt-out.
Two partial unique indexes dedupe global vs per-topic rows (Postgres
treats NULLs as distinct).
- `emailingDomain` — the workspace's SES sending domain,
auto-provisioned when an email channel is added (and cleaned up when its
last channel is removed), with verification status + DNS records.
Campaign messages reuse the existing `message` / `messageThread` /
`messageParticipant` model — one outbound `message` per recipient with a
`deliveryStatus` state machine.
## Sending
- `sendMessageCampaign` resolves the audience **under the caller's
permissions**, creates the campaign, and enqueues a single fan-out job
(the request never materializes per-recipient rows or jobs).
- The fan-out job materializes one QUEUED message per recipient
(deterministic ids → idempotent re-runs, reconciles crash-orphaned rows)
and fans out per-recipient send jobs carrying **only ids**.
- Each send job renders per-recipient `{{variable}}` merge fields and
sends via `EmailingDomainSenderService`, which applies suppression
(global + per-topic) and the unsubscribe footer/headers. Suppressed
recipients are recorded `SKIPPED`.
- The campaign finalizes `SENT`, or `SENT_WITH_ERRORS` if any recipient
terminally failed.
- `previewMessageCampaignAudience` returns a pre-send breakdown (total /
without-email / duplicate / globally-unsubscribed / topic-unsubscribed /
sendable), shown as a hint under the composer pickers.
## Unsubscribe
- Encrypted (AES-256-GCM) token carrying workspaceId, address, optional
`unsubscribeTopicId`, `issuedAt`, and a `preview` flag.
- One-click POST (RFC 8058) + `mailto:` — topic-scoped when the token
carries a topic, global otherwise.
- Preferences page: a checkbox per visible topic (checked = still
receiving); submitting creates per-topic opt-outs for unchecked topics
and lifts re-checked ones (UNSUBSCRIBE only — never
`BOUNCE`/`COMPLAINT`, never a global block).
- A **Preview** action in settings opens the live page via a
preview-claim token; opt-out POSTs are no-ops for preview tokens, so
previewing never mutates state.
- SES webhooks: inbound unsubscribe + outbound bounce/complaint →
suppression (race-safe against at-least-once delivery, with reason
escalation that never downgrades).
- Per-customer unsubscribe hostname (Cloudflare DNS); sends are gated on
it being active, except in LOG/demo mode.
## Architecture
Campaign orchestration, suppression, the sender, the unsubscribe
controller, and the SES webhook handlers live in `src/modules/emailing`
+ `src/modules/messaging-webhooks` (the workspace-feature layer).
`core-modules/emailing-domain` keeps the SES driver, domain
provisioning, the `unsubscribeTopic` / `messageSuppression` core
entities, and the unsubscribe token/hostname plumbing. Domain creation
is validated (`CreateEmailingDomainInput` — domain-format regex,
lowercased) before any value reaches SES or the unsubscribe hostname.
## Frontend
- Campaign composer side panel (from / list / unsubscribe topic /
subject / body) with a live audience-preview hint.
- Email settings: email channels each showing their auto-provisioned
sending domain in a single section (status + DNS records + a "Check
verification" action), plus an **Unsubscribe Topics** section to
create/manage topics and preview the recipient page. A demo-mode banner
is shown when the LOG driver is active.
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
|
||
|
|
76f69efb43 |
Keep synced messages and events when removing a workspace member (#21443)
## Context Removing a workspace member deletes their connected accounts, which cascades into deleting every message and calendar event those accounts synced. For a CRM, losing the email history of departed teammates is a big deal. ## What this does Connected accounts are now kept and reassigned instead of deleted when a member is removed: - Ownership moves to the acting user (whoever removed the member). When members remove themselves (leave workspace, account deletion), it falls back to the oldest admin. - OAuth tokens are revoked, credentials wiped, message/calendar channels get `isSyncEnabled = false`, and the account is stamped with a new `archivedAt` column (fast instance command included). - Synced messages, threads and calendar events stay in the workspace. Channel visibility settings keep applying as before, since channels and associations survive. - The reassigned account appears in the new owner's Settings → Accounts, where it can still be deleted (with its data) like any other account. The transfer happens synchronously during removal, while the member's userWorkspace row still exists. This also removes `DeleteWorkspaceMemberConnectedAccountsCleanupJob` and its listener: the async job had to reconstruct the account-owner link from rows the removal flow had just deleted, which was race-prone (see 2181fb541e). Archived accounts are excluded from the workflow send-email default account resolution, and both removal confirmation modals now mention what happens to synced data. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
f63f053444 |
CommandMenuItem overridable entity (#21486)
## Context Second PR of the overridable-entities track (after #21436 for views): command menu items become overridable so that edits on non-owned items are stored as overrides instead of mutating the row, and deletion/deactivation becomes reversible. ## What this does - `CommandMenuItemEntity` now extends `OverridableEntity<CommandMenuItemOverrides>` (adds `isActive` + `overrides`). All editable properties are overridable for now (to discuss). - **Update**: mutations on a command item not owned by the caller (standard items) are written into `overrides`; reads merge them in the DTO. The command palette edit mode (pin, reorder, shortLabel) now preserves standard values, "Reset label to default" gains true post-save semantics. - **Delete**: protected items are deactivated (`isActive = false`) instead of deleted; custom items still hard-delete. - **Object deactivate/enable toggle**: now flips `isActive` on the command item (merged into the main migration call) instead of delete/recreate; a create-if-missing fallback covers legacy deactivated objects. - **Front**: inactive command items are filtered out of the palette selector. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21486?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: Charles Bochet <charles@twenty.com> |
||
|
|
036d9a2bcf |
feat: gate record creation on isUICreatable only, decoupled from isSystem (#21527)
## Context The generic "create a record" UI affordance previously required `!isSystem`, conflating two distinct concerns: **"hidden from Data Model"** and **"not user-creatable"**. This blocked legitimately creatable system objects (e.g. marketing message lists kept `isSystem: true` only to stay out of the Data Model). This PR makes creatability depend on `isUICreatable` alone, so visibility (`isSystem`) and creatability (`isUICreatable`) become independent. ## Changes - **Front gate** (`canCreateRecordsForObjectMetadataItem.ts`): drop the `!isSystem` clause — creatability is now `isUICreatable && !readOnly`. Updated comment + unit test. - **Command menu** (`standard-command-menu-item.constant.ts`): drop the matching `not objectMetadataItem.isSystem` clause from the `createNewRecord` availability expression so the "Create new X" command mirrors the front gate. Existing workspaces get this via the already-present `SyncCreateRecordCommandAvailabilityExpressionCommand`, which re-syncs from the live definition. - **Standard object audit** (`create-standard-flat-object-metadata.util.ts`): the 15 sync/system-created standard objects that relied on `!isSystem` to stay non-creatable now set `isUICreatable: false` (attachment, blocklist, calendar*/message*/note/task targets, message, messageThread, messageParticipant, timelineActivity, callRecording, workflowAutomatedTrigger, …). `workflowRun`/`workflowVersion`/`workspaceMember` were already `false`. Non-system objects (company, person, note, opportunity, dashboard, task, workflow) are untouched. - **Backfill**: new fast instance command (`2-13-…-1781277480000-backfill-non-ui-creatable-standard-system-objects.ts`) runs `UPDATE core.objectMetadata SET isUICreatable = false` for those standard system objects (symmetric `down`), registered in `instance-commands.constant.ts`. `isSystem` and Data-Model visibility logic are unchanged. ## Verification - Front gate unit test (7 pass), standard-application suite incl. callRecording (10 pass) - `oxlint` clean on all changed files - `twenty-server` and `twenty-front` typecheck green 🤖 https://claude.ai/code/session_01TF4kjD56hHjP31wkHPxPv3 --- _Generated by [Claude Code](https://claude.ai/code/session_01TF4kjD56hHjP31wkHPxPv3)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21527?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: Claude <noreply@anthropic.com> |
||
|
|
d2fbc165b6 |
fix(messaging): emit channel and account deletion events from core metadata services (#21491)
/closes #21425 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21491?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. --> |
||
|
|
869680a5a1 |
fix(deps): esbuild ^0.28.1 floors + vite 7→8 (rolldown) upgrade (#21517)
## What this does Resolves the remaining esbuild security alerts on packages we own, and upgrades the repo to **Vite 8** (which drops esbuild entirely in favour of rolldown/oxc). ### 1. esbuild → `^0.28.1` (security) - Raised the declared `esbuild` floor in `twenty-sdk` and the logic-function common-layer (both were `^0.25.0`, which can only resolve to a vulnerable version). These are our packages, so this is just declaring the patched version — clears Dependabot **#1467** and **#1468**. ### 2. Vite 7 → 8 - Bumped `vite` to `^8` in the 5 packages that declare it, and `@vitejs/plugin-react-swc` to `^4.3.1` (the only plugin that needed a bump for Vite 8; everything else already supports it). - `twenty-front` keeps esbuild minification, so esbuild is now an explicit (patched) devDependency there — Vite 8 no longer ships it. ### Two Vite-8 fallout fixes (bundler internals changed) - **Storybook tests:** added React to `optimizeDeps.include` so Vite's dep optimizer doesn't re-bundle React mid-run and break in-flight imports in browser-mode tests. - **`hex-rgb`:** it's ESM-only and broke rolldown's CJS interop (a default import resolved to the wrong thing under jest). Replaced its one use with a tiny inline hex→rgb parse and dropped the dependency. ## Verified Vite resolves to a single `8.0.16` with no esbuild in its tree. Builds pass on Vite 8/rolldown: `twenty-front` production build, the SDKs, and Storybook; the previously-failing front and storybook test jobs now pass; `yarn install --immutable` is clean. ## Note This doesn't close root alert **#1469** — esbuild is still pulled by other third-party tools (storybook, tsx, lingui, zapier, etc.) that haven't shipped a patched release. The vulnerable code path (esbuild's dev server) isn't used here, so that one is best dismissed as not-affected. |
||
|
|
050f7dcf85 |
fix(server): allow ordering operators on SELECT and RATING fields (#21506)
## Problem
On a record **show page**, prev/next navigation fails with toasts like:
```
Invalid filter : Operator "lt" is not valid for this "severity" SELECT field
```
…but only when the record is reached via in-app navigation from an index
view sorted by a SELECT field (a hard reload is clean).
## Root cause
The record show page paginates with **keyset (cursor) pagination**.
`useRecordShowPagePagination` builds `before`/`after` filters via
`computeCursorArgFilter`, which emits `{ field: { gt|lt: value } }` for
**every** field in the parent view's `orderBy`, regardless of type.
When a view is sorted by a **SELECT** field (e.g. the *All Bugs* view
sorts by `severity`), the keyset filter becomes `{ severity: { gt:
"HIGH" } }` / `{ severity: { lt: "HIGH" } }`. SELECT/RATING fields use
`ENUM_FILTER_OPERATORS` (`eq, neq, in, containsAny, is, isEmptyArray`) —
no ordering operators — so filter validation rejects them, breaking both
the neighbour queries and the rank-in-view ("X of Y") count query. A
hard reload has no parent-view `orderBy`, so neighbour queries are
skipped → no error.
This isn't a technical limitation: the column is comparable, `ORDER BY
severity` works, and the backend already emits the same comparison for
its own cursor path (`buildCursorWhereCondition`) — that path just
bypasses the user-filter allowlist. Ordering operators were omitted from
`ENUM_FILTER_OPERATORS` because they aren't meaningful *user* filters
for categorical fields, but keyset pagination legitimately needs them.
## Fix
Add `gt/gte/lt/lte` to `ENUM_FILTER_OPERATORS`, so SELECT/RATING keyset
filters are accepted — exactly as `UUID_FILTER_OPERATORS` already
carries these operators for the `id` cursor tiebreaker. The filter UI
never offers these operands for SELECT, so this only enables the
keyset/cursor use case.
## Tests
- `get-operators-for-field-type.util.spec.ts`: SELECT/RATING now include
`gt/gte/lt/lte`.
- `validate-operator-for-field-type-or-throw.util.spec.ts`: regression —
ordering operators on a SELECT field no longer throw.
|
||
|
|
1efa3567ef |
Rename isUIReadOnly to isUIEditable, add isUICreatable, expose both to app developers (#21504)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# UI capability flags: `isUIEditable` + `isUICreatable`
## Per-verb capability model
This PR replaces the negative `isUIReadOnly` metadata flag with
positive, per-verb capability flags (à la Salesforce
`createable`/`updateable`):
- **`isUIEditable: boolean`, default `true`** — rename of `isUIReadOnly`
with inverted polarity, on **both** `objectMetadata` and
`fieldMetadata`. It is one concept ("can the user edit this through the
generic UI?") at two altitudes, so it carries one name at both levels.
- **`isUICreatable: boolean`, default `true`** — new, **object-level
only** (fields have no create verb). When `false`, no generic UI
affordance to create a record of this object appears anywhere (table "+"
buttons, board column add, calendar add, relation-section "Add new",
record picker "Add new", command-menu create action and its keyboard
shortcut).
Both flags are **UI-affordance flags only**: the server does not block
create/edit mutations based on them, so the system, API, and workflows
continue to mutate these records freely. They are orthogonal statements
about the object's nature with no implication rule in the data model.
Because today's inline creation UX creates a blank record the user must
then edit, the frontend create predicate currently requires both
`isUICreatable` and effective editability.
There is no CREATE permission in `ObjectPermissions`; the frontend keeps
gating creation on `canUpdateObjectRecords` as a proxy, ANDed with the
new flags.
## Unified create predicate
All generic creation entry points now flow through one predicate,
`canCreateRecordsForObjectMetadataItem` (`isUICreatable` && not
`isSystem` && not effectively read-only, where effective read-only
covers `isUIEditable`, `isRemote`, and the `canUpdateObjectRecords`
proxy via `isObjectMetadataReadOnly`). This deletes the previously
hardcoded suppression lists:
- `isRecordTableCreateDisabled.ts` and its hardcoded
`WorkflowRun`/`WorkflowVersion` list — deleted; those objects (plus
`workspaceMember`) now declare `isUICreatable: false` in the standard
application instead.
- The hardcoded `workspaceMember` guard inside
`useAddNewRecordAndOpenSidePanel.ts` — deleted.
- The `CREATE_NEW_RECORD` command menu item's availability expression
now checks `objectMetadataItem.isUICreatable`, `isUIEditable`,
`isSystem`, and `isRemote`; a workspace upgrade command re-syncs the
expression in existing workspaces.
Component-local conditions (soft-delete filter active, layout
customization mode) stay in their components.
## GraphQL compatibility and removal plan
The schema delta versus main is **purely additive plus deprecations —
zero breaking changes**:
- `isUIReadOnly` remains on both the ObjectMetadata and FieldMetadata
GraphQL output types for **one release** as a deprecated field computed
as `!isUIEditable` (`deprecationReason: 'Use isUIEditable'`). The Twenty
frontend no longer queries it.
- `isUIReadOnly` also remains on the **input side** for one release
(`CreateFieldInput`, `UpdateFieldInput`, `FieldFilter`, `ObjectFilter`),
keeping the schema shape identical to main for those members. On create
it acts as a legacy alias mapped to `!isUIReadOnly` (`isUIEditable` wins
when both are provided); on update it is ignored, exactly as on main (it
was never an editable property). Filtering on the deprecated member
keeps working until the column is dropped at upgrade time; after that it
is a deprecated no-op surface kept only for schema compatibility.
**Removal plan for next release: drop `isUIReadOnly` from the output
DTOs (and resolvers' `@ResolveField`s), from the input/filter types,
from the create-input mapping, and the `@WasRemovedInUpgrade`-retained
entity columns and decorators.**
## ⚠️ Webhook / database-event payload shape change
The `database-event-payload` type in `twenty-shared` got a clean rename
(no alias): metadata snapshots in webhook and database-event payloads
now carry `isUIEditable` (and `isUICreatable` at object level) **instead
of** `isUIReadOnly`, with inverted polarity. Consumers of these payloads
that read `isUIReadOnly` must switch to `isUIEditable`.
## New manifest properties (app-developer DX)
Application developers can now set these flags in their app manifests
(purely additive — existing manifests and older `twenty-sdk` versions
are unaffected, defaults apply when omitted):
- `objects[].isUICreatable?: boolean` (default `true`)
- `objects[].isUIEditable?: boolean` (default `true`)
- `fields[].isUIEditable?: boolean` (default `true`)
The manifest converters previously hardcoded `isUIReadOnly: false`; they
now read the manifest values with `?? true` defaults. The types are
re-exported through `twenty-sdk` from `twenty-shared`.
## Migration & backfill
- One fast instance command: adds `isUIEditable` (NOT NULL default
`true`) on `core."objectMetadata"` and `core."fieldMetadata"`, backfills
`isUIEditable = false` exactly where `isUIReadOnly = true`, drops
`isUIReadOnly`, and adds `isUICreatable` (default `true`) on
`objectMetadata`. The `down` is the exact inverse. Uses `ADD/DROP COLUMN
IF (NOT) EXISTS`, matching the 2-12 drop-`isCustom` precedent. Verified
up and down in separate transactions against a dev database with exact
backfill counts.
- **Cross-version upgrade safety (multi-version self-hosted jumps):**
the upgrade sequence interleaves per version (instance → workspace
commands), so pre-2.13 workspace commands run **before** the 2.13 rename
when an old instance jumps several versions. Following the `isCustom`
precedent: `isUIEditable`/`isUICreatable` are marked
`@WasIntroducedInUpgrade` and `isUIReadOnly` stays on both entities as
`@WasRemovedInUpgrade`, so the upgrade-aware entity metadata adapter
hides the not-yet-existing columns (and keeps the legacy column live) at
pre-2.13 cursors. **No committed upgrade command outside the 2-13
directory is modified**: the old 1-21/2-8/2-9 commands keep their
original `isUIReadOnly: true` inputs, which still compile (entity
property retained, deprecated create-input alias mapped) and still
produce the correct legacy column writes pre-rename.
- A 2-13 workspace command (`sync-standard-ui-capability-flags`)
re-syncs `isUICreatable` **and** `isUIEditable` on standard objects and
`isUIEditable` on standard fields from the standard-application
definitions. This backfills `isUICreatable: false` on
`workflowRun`/`workflowVersion`/`workspaceMember` and heals fields
created mid-cross-upgrade by pre-2.13 commands (whose hidden
`isUIEditable` value cannot reach the insert). Both 2-13 sync commands
pass `isSystemBuild: true` — the flat metadata validator otherwise
rejects direct updates to system objects (verified against a
deliberately drifted dev database; the run is idempotent).
- A second 2-13 workspace command re-syncs the create-record command
availability expression.
## Testing
- Unit tests for `canCreateRecordsForObjectMetadataItem`
(flag/permission/system combinations) and for the manifest converters
(flags set / omitted → defaults).
- Full `upgrade --dry-run` boots the sequence (107 steps) and validates
the upgrade-aware decorator references; both 2-13 sync commands verified
end to end against real drift and re-run idempotently.
- Schema verified by live introspection after the input-alias restore:
all four input/filter members match main, output deprecations intact;
frontend metadata types and `twenty-client-sdk` schema regenerated from
the running server.
- Read-only-related and touched jest suites pass on both packages;
typecheck and lint pass on `twenty-server` and `twenty-front`.
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a
href="https://cursor.com/agents/bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/background-agent?bcId=bc-0f3e04cb-b04a-40be-8330-5609c4538e8a"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21504?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: Cursor Agent <cursoragent@cursor.com>
|
||
|
|
9bb98fa5b5 |
fix(billing): don't crash when workspace has no active subscription (#21510)
## Problem
Sentry (high severity, SLA-breaching): `Billing Subscription Not Found:
No active subscription found for workspace …`
The `billingSubscription` workspace-cache provider
(`WorkspaceBillingSubscriptionCacheService.computeForCache`) called
`getCurrentBillingSubscriptionOrThrow`. For a workspace whose
subscription is fully canceled, `getCurrentBillingSubscription` filters
out `Canceled` and returns `undefined`, so the provider **threw**
`BILLING_SUBSCRIPTION_NOT_FOUND`.
That cache key is read on every usage-recording path:
- workflow execution
(`WorkflowExecutorWorkspaceService.sendWorkflowNodeRunEvent`)
- AI usage (`AiBillingService`)
- logic-function execution (`LogicFunctionExecutorService`)
- app charges (`AppBillingService`)
- the gate `BillingUsageService.canFeatureBeUsed` /
`hasAvailableCredits` / `decrementAvailableCreditsInCache`
- the cancellation webhook
(`invalidateAndRecompute('billingSubscription')`)
So any of these throws an unhandled exception for a
no-active-subscription workspace. The intent was clearly to tolerate
this state — `canFeatureBeUsed` already guards with
`isDefined(billingSubscription)` and the workflow runner logs *"there is
no subscription for this workspace"* — but the throwing provider made
those guards unreachable.
## Fix
- `computeForCache` now returns `FlatBillingSubscription | null` via the
non-throwing `getCurrentBillingSubscription`, and the cache type allows
`null`.
- Every consumer guards the absent case (`isDefined` / optional
chaining) and no-ops: usage events still emit with an undefined
`periodStart`, credits aren't decremented, `hasAvailableCredits` returns
`false`.
- `getCurrentBillingSubscriptionOrThrow` is **left untouched** for the
many callers (resolver, subscription-update, etc.) that genuinely
require a subscription.
## Test
Adds `workspace-billing-subscription-cache.service.spec.ts`: the
provider returns `null` when there's no active subscription (regression)
and the flattened subscription when one exists.
All 142 tests across the billing / ai-billing / workflow-executor suites
pass; `oxlint --type-aware` and `oxfmt` are clean.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21510?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. -->
|
||
|
|
ba94c3b857 |
feat(workflow): idempotent stop + retry failed runs from failing step (#21458)
https://github.com/user-attachments/assets/5a25396f-8959-4bd8-93cb-1187559ffe5f ## Summary Two workflow-run improvements, with all non-trivial logic isolated in pure, unit-tested utils. ### 1. Idempotent stop `stopWorkflowRun` no longer throws when a run is already in a terminal status (`COMPLETED` / `FAILED` / `STOPPED`) or already `STOPPING`; it returns the run unchanged. This fixes: - bulk stop aborting on the first non-stoppable run in a mixed/select-all selection, - the click-vs-processing race on a single run (run finishes between click and mutation). It also releases the cached not-started throttle slot when stopping a `NOT_STARTED` run (prevents counter drift), and ends runs with no `state` directly. ### 2. Retry a failed run from the failing step New `retryWorkflowRun` mutation (same guards/passthrough as `stopWorkflowRun`). It resets the failed step(s) to `NOT_STARTED`, flips the run to `RUNNING`, and enqueues a `RunWorkflowJob` with the steps to re-execute; downstream execution and status computation are unchanged. Logic lives in pure utils: - `build-retry-step-infos.util.ts` - decides per failed step what to reset; delegates iterator-specific logic to `build-retry-iterator-step-infos.util.ts` (an iterator that failed mid-loop is restored to `RUNNING` with cursor preserved, an iterator that failed itself restarts its whole loop). - `get-runnable-step-ids.util.ts` - reuses the executor's `shouldExecuteStep` to also resume branches that never started (avoids hangs), excluding loop-interior steps. The service method only orchestrates; the job's status check is a race guard (retriability is enforced in the service before enqueue). A "Retry" command menu item surfaces only for `FAILED` runs (`someEquals(selectedRecords, "status", "FAILED")`). ### 3. Keep the run diagram visible across regenerations The run diagram is regenerated on every run state change, producing fresh nodes without the dimensions Reactflow had measured. Reactflow hides unmeasured nodes until it re-measures them, so the diagram could flicker and disappear when the last regeneration before going idle left nodes unmeasured (reproducible after retrying a failed run). The regenerated nodes now carry over the previously measured dimensions (by id) so they stay rendered. ## Test plan - [x] Unit tests for both retry utils (9 cases: plain failed step, non-failed untouched, iterator mid-loop restore, iterator self-failure, frontier parent gating, entry steps, loop-interior exclusion, parallel branches) - [x] `twenty-server` + `twenty-front` typecheck - [x] `lint:diff-with-main` clean for both packages - [x] Manual: retry a failed run repeatedly and confirm the diagram stays visible - [ ] Manual: stop a COMPLETED/mixed selection (no error), retry a failed run and confirm it resumes from the failing step |
||
|
|
a8a8bbb2ed |
feat(workflow): add offset to Find Records node for pagination (#21484)
<img width="471" height="362" alt="Capture d’écran 2026-06-12 à 15 38 45" src="https://github.com/user-attachments/assets/9656d3a6-6f56-4587-add6-55c0a0a32482" /> ## Summary The workflow Find Records (search) node previously exposed only `objectName`, `filter`, `sort`, and `limit` (capped at `QUERY_MAX_RECORDS` = 200), with no way to page beyond the first page of results. This adds an optional **Offset** to the node so a workflow can fetch an arbitrary page (`offset = pageIndex * limit`) while keeping the same filter and sort. The underlying `FindRecordsService` already accepts `offset` (it forwards it to the query runner's `skip`, and stabilizes ordering with an `id` tiebreaker), so this change just threads `offset` through the remaining layers: - `workflowFindRecordsActionSettingsSchema` (shared zod schema) — new optional `offset` - `FindRecordsInput` type — new optional `offset?: number` - `find-records.workflow-action.ts` — forwards `offset` to `FindRecordsService.execute` - `WorkflowEditActionFindRecords.tsx` — new "Offset" number input (non-negative, defaults to 0) with form state + persistence - Default `FIND_RECORDS` step settings — `offset: 0` ### Notes / non-goals - Offset-only, single page: the node returns one page. Looping over all pages inside one run is not included (the Iterator action loops a static array and cannot re-query). The node output already returns `totalCount`, so a workflow can compute total pages as `ceil(totalCount / limit)`. - Offset on very large/changing datasets can be slow or skip/duplicate rows; cursor/keyset pagination would be a future follow-up. ## Test plan - [x] Create a Find Records node, set Limit=50, Offset=0 → returns first page - [x] Set Offset=50 with the same filter/sort → returns the second page (no overlap) - [x] Negative offset shows a validation error and is not saved - [x] Existing Find Records nodes (no offset stored) still run, defaulting to offset 0 - [x] Typecheck/lint pass in CI <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21484?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. --> |
||
|
|
49026a7368 |
chore: bump version to 2.13.0 (#21492)
## 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/21492?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> |
||
|
|
7b6b624041 |
fix(server): bypass stale workspace cache when resolving currentUser during onboarding (#21461)
## Context On twenty-main (multi-replica), signing up and naming a workspace lands on a black screen at `/create/profile`; a manual refresh fixes it. From the network trace: the post-activation `currentUser` response carries `onboardingStatus: PROFILE_CREATION` (fresh) together with a non-ACTIVE `currentWorkspace` and `workspaceMember: null` (stale). ## Root cause `activateWorkspace` invalidates the core entity cache only on the instance that served the mutation. When the follow-up `currentUser` query is routed to a sibling instance, the auth context carries a memoized pre-activation workspace snapshot. A stale transient workspace cascades: - `workspaceMember`/`workspaceMembers` resolve to null/empty (`loadWorkspaceMember` skips non-active workspaces), permissions fall back to defaults - the client's metadata store never loads (`MinimalMetadataLoadEffect` skips non-active workspaces), so `MinimalMetadataGater` shows the loading skeleton forever on `/create/profile` #20322 fixed the same staleness for `onboardingStatus` by reading the workspace fresh from the database in the resolver — which is why the status is fresh while the workspace object isn't, and why the client navigates to a page it can't render. #21480 bounds the staleness window to the designed 10s (absolute memoizer TTL), but signup lives entirely inside that window: the client reads `currentUser` ~1s after `activateWorkspace` and never refetches while stuck. ## Fix Apply the #20322 approach at the workspace status resolution layer: `UserService.refreshWorkspaceIfPendingOrOngoingCreation` re-reads the workspace from the database when the auth-context copy is in a transient activation status (`PENDING_CREATION`/`ONGOING_CREATION`). Used in: - `UserResolver.currentUser` — fresh `currentWorkspace` and permissions - `UserService.loadWorkspaceMember` / `loadWorkspaceMembers` — covers the `workspaceMember`/`workspaceMembers` resolve fields No-op for active workspaces; the extra database read only happens for workspaces mid-creation. ## Test plan - Full `user.service.spec.ts` suite passes; lint and format clean. - After deploy to twenty-main: sign up, name the workspace, verify `/create/profile` renders the profile form with a populated `workspaceMember` and ACTIVE `currentWorkspace` without refreshing. |
||
|
|
214dc70b67 |
Fix missing WasIntroducedInUpgrade for overridable view entity (#21483)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21483?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. --> |
||
|
|
08a36aa68f |
fix(server): restore absolute TTL in PromiseMemoizer (#21480)
## Context `PromiseMemoizer` sits in front of the staged lookup (local cache → Redis hash validation → Redis data → DB recompute) of both `CoreEntityCacheService` and `WorkspaceCacheService` (10s TTL each). The Redis hash check is the **only** cross-instance invalidation mechanism — there is no pub/sub — and it runs only when the memo entry expires. The TTL is currently **sliding**: every read refreshes `lastUsed`, and eviction compares against time-since-last-read. So any entry read more often than every 10s on a given instance never revalidates, and that instance serves stale data for as long as traffic continues. Affected data: auth-context entities (workspace, user, userWorkspace), API key revocations, role/permission maps, RLS predicates, feature flags, and all metadata maps. Observed manifestation: after `activateWorkspace`, a sibling instance kept serving a `PENDING_CREATION` workspace snapshot (kept alive indefinitely by the client's own polling), stranding signup on a permanent loading skeleton at `/create/profile` (#21461). Same staleness class as #20322 and the CI flakes investigated in #21435. ## Why it was sliding #11444 (April 2025) deliberately changed the TTL from absolute to sliding because the memoizer's then-consumer was the TypeORM datasource storage: absolute expiry was destroying datasources that were actively in use (`onDelete` → `destroy()`), causing worker `Connection terminated` errors. That consumer no longer exists — datasources moved to `GlobalWorkspaceOrmManager`, and neither remaining consumer passes `onDelete` or holds resources needing keep-alive. ## Fix Restore absolute expiry: `expiresAt` is set at write time and never refreshed on read. Every instance now re-enters the staged lookup (and thus the Redis hash validation) at least once per TTL, restoring the designed ≤10s cross-instance staleness ceiling. Concurrent dedup (`pending` map) and `onDelete` plumbing are unchanged. ## Test plan - New regression test: reads at half-TTL intervals must not extend an entry's lifetime (fails on the sliding implementation, passes now). - Full `promise-memoizer.storage.spec.ts` and `workspace-cache.service.spec.ts` suites pass (25 tests); lint and format clean. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21480?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. --> |
||
|
|
e334551da9 |
(Fix) Upsert no longer rewrites position on existing records (#21375)
## Fix: upsert no longer rewrites `position` on existing records ### Problem `createX(..., upsert: true)` resets the `position` of records that resolve to an **update**, even when the payload doesn't include a `position`. The create-many/upsert runner backfills `position` (to `"first"`) in `computeArgs` over the **whole batch**, before records are split into insert vs update. So existing rows get a freshly recomputed `position` written on every upsert. For callers that re-upsert their full dataset on a schedule (e.g. a daily sync), this rewrites `position` for every record on each run and drifts the values steadily negative — and it floods audit/event logs with position churn. The dedicated `updateOne`/`updateMany` runners already pass `shouldBackfillPositionIfUndefined: false`; the upsert path did not. ### Fix Only backfill `position` for records that are actually inserted: - `computeArgs` now passes `shouldBackfillPositionIfUndefined: !args.upsert` in both the create-many and create-one runners, so undefined positions are left untouched on upsert. - `performUpsertOperation` backfills `"first"` positions for `recordsToInsert` only, **after** categorization, via `RecordPositionService`. Explicit `position` values (`"first"`, `"last"`, or a number) in the payload are still honored. Plain (non-upsert) create behavior is unchanged. ### Behavior | Scenario | Before | After | |---|---|---| | Upsert updates existing row, no `position` sent | `position` rewritten | `position` untouched | | Upsert inserts new row, no `position` sent | gets `"first"` | gets `"first"` (unchanged) | | Explicit `position` on upsert | applied | applied | | Plain create | unchanged | unchanged | |
||
|
|
fefd9d7704 |
feat(workflow) - Add validation layer (#21422)
Add workflow validation framework and consolidate output schema types/search logic into twenty-shared This PR introduces a comprehensive workflow validation system that catches configuration errors at build-time, and consolidates the fragmented output-schema type definitions and variable-search logic from the front-end into twenty-shared **Workflow validation** — A new system that checks workflows for errors before activation: graph connectivity (unreachable steps, dangling references), step parameter schemas (via Zod), variable references (typos, wrong step order), and workspace metadata (non-existent objects). Returns structured errors/warnings with "did you mean?" suggestions. Runs automatically after create_complete_workflow and update_workflow_version_step, and is also available as a standalone validate_workflow tool. **Output schema consolidation** — Moves all output schema types and the variable-search logic from scattered front-end files into twenty-shared, replacing ~800 lines of duplicated per-schema-type code with a single unified searchVariableInOutputSchema dispatcher. To do : - validation on CODE and AGENT step --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e6d730cd75 |
chore: sync AI model catalog from models.dev (#21476)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21476?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
69a7e614ff |
fix: restore isCustom gate in metadata label resolvers (#21432)
## Context #21228 removed the stored `isCustom` column and, with it, the `isCustom` early-return in `resolveObjectMetadataStandardOverride` / `resolveFieldMetadataStandardOverride`, on the assumption that falling through the `standardOverrides` checks was equivalent. It isn't: custom object/field labels now reach the Lingui lookup. A custom label that collides with a standard catalog string (e.g. a custom field labeled "Status") gets translated for non-English locales against the user's intent, and every other custom label pays a hash + catalog miss — and, in production, an "Uncompiled message detected" warning (#21415) — on each metadata resolution. ## Fix Restore the gate. `isCustom` is no longer stored, so call sites that build the resolver input from flat entities (dataloader, minimal-metadata, view controller, command-menu-item navigation context) derive it via `belongsToTwentyStandardApp`; GraphQL resolvers keep passing DTOs, which already carry the derived value. ## Testing - Unit tests for both resolvers, including a new regression test: a custom label matching a standard catalog entry is returned verbatim, Lingui never called. --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
cfb9772179 |
feat(server): convert view to overridable entity (#21436)
## Context Every entity created as a side effect of object creation must support the overridable pattern (`isActive` + `overrides` + override routing) before we can re-own side effects to their true application. Starting with View. viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already extend `OverridableEntity`. This PR brings `view` to the same pattern. ## What this does - `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds `isActive` boolean + `overrides` jsonb). All editable view properties are overridable; the 3 fieldMetadata foreign keys are converted to/from universal identifiers like viewField's `viewFieldGroupId`. - **Update**: mutations on a view not owned by the caller (e.g. standard views like "All Companies") are written into `overrides` instead of mutating the row. Reads merge overrides in the DTO. - **Delete/destroy**: views not owned by the caller are deactivated (`isActive = false`) instead of deleted. ~~- **INDEX invariant**: `key = INDEX` views can only be created via object-creation side effect. The API now rejects creating, deleting or destroying INDEX views (object-deletion cascade is unaffected). This was not really needed for this migration but was flagged during implementation.~~ - **Front**: views with `isActive = false` are filtered out of the views selector. - Fast instance command adds the two columns (`2-12-instance-command-fast-...-view-overridable-entity.ts`). ## Notes - Custom (caller-owned) views behave exactly as before: direct updates, soft delete. - View-group side effects (kanban groups) are computed on the override-merged view so overridden `mainGroupByFieldMetadataId` works. |
||
|
|
41cdd83367 |
fix(ai): correct RICH_TEXT and MORPH_RELATION record filter operators (#21106)
## Problem
The AI find-records tool generates filter schemas via
`generateFieldFilterZodSchema`. `RICH_TEXT` currently shares the `TEXT`
case, so the agent is told it can use scalar text operators
(`like`/`ilike`/`startsWith`/`endsWith`/`eq`/…) directly on a rich-text
field.
But `RICH_TEXT` is a **composite** type (`markdown` + `blocknote`
sub-fields, see `rich-text.composite-type.ts`). Applying a scalar
operator to the composite root throws at query time:
```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Sub field "ilike" not found for composite type: RICH_TEXT
```
`FindRecordsService` catches and returns `success: false`, so the agent
retries mid-turn — burning latency/tokens — and can **never** search
rich-text body content (note bodies, `about`, etc.).
## Fix
Give `RICH_TEXT` its own case in the filter-schema generator that
exposes the `markdown` and `blocknote` sub-fields, each carrying the
text operators — mirroring the existing composite patterns for `EMAILS`
(`primaryEmail`), `PHONES` (`primaryPhoneNumber`), `LINKS`
(`primaryLinkUrl`), `FULL_NAME`, and `ADDRESS`.
So the agent now emits:
```jsonc
{ "noteBody": { "markdown": { "ilike": "%onboarding%" } } } // valid composite sub-field filter
```
instead of:
```jsonc
{ "noteBody": { "ilike": "%onboarding%" } } // throws on composite root
```
This both **stops the throw** and **makes rich-text content actually
searchable** (the original intent). `TEXT` keeps its existing root-level
scalar operators unchanged.
## Test
Added `__tests__/field-filters.zod-schema.spec.ts`:
- `RICH_TEXT` routes pattern operators onto `markdown` / `blocknote`
- root-level scalar operators on `RICH_TEXT` are no longer accepted
- `TEXT` root-level operators unchanged
## Notes
- No DB/schema migration; render/tool-schema layer only.
- Reproduced against `twentycrm/twenty:latest`; the faulting code is
unchanged on `main` as of this PR.
---------
Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
|
||
|
|
503c689f37 |
security: upgrade typeorm to 0.3.26 (CVE-2025-60542) (#21456)
## Context Retry of the typeorm upgrade that was pulled out of #21448 after CI showed "intermittently lossy metadata sync". **The investigation exonerated typeorm**: the postcard/seed failures were a pre-existing bug in `@ptc-org/nestjs-query-typeorm`'s batched relation paging (global LIMIT across parents) that scan-order luck had been hiding — reproduced byte-for-byte on typeorm **0.3.20** against a frozen repro DB. That bug is fixed in #21455, which this PR is stacked on (base branch = `charles/fix-nestjs-query-batch-relation-paging`; will retarget to main when it merges). ## Changes - typeorm `0.3.20` → `0.3.26` ([CVE-2025-60542](https://github.com/advisories/GHSA-q2pj-6v73-8rgj), MEDIUM). The CVE lives in TypeORM's MySQL path (`sqlstring`/`stringifyObjects`); Postgres-only Twenty never exercises it — this is scanner hygiene + staying current. - The local yarn patch (`PickKeysByType` + `DeleteResult.generatedMaps`) applies **verbatim** to 0.3.26 (verified against the pristine tarball) — renamed to `typeorm+0.3.26.patch`. - `WorkspaceRepository.query` restricted override adapted to the generic `query<T = any>()` base signature introduced in 0.3.24 (one-line change, still throws `RAW_SQL_NOT_ALLOWED`). - 0.3.26 ships `uuid ^11` natively → the scoped `typeorm/uuid` resolution from #21441 and its `//resolutions` comment clause (including the now-disproven "lossy sync" warning) are removed. ## Why we're confident this time The original failure signature was fully understood, not just retried: - On a frozen failing DB, **all fieldMetadata rows + workspace columns were intact** — only the batched metadata API read was truncated (`LIMIT 501` over 558 rows, no ORDER BY). - Same DB, typeorm 0.3.20: identical truncation, identical SQL → not a typeorm regression. - With #21455 applied: postcard install/uninstall stress loop **12/12 green on typeorm 0.3.26** (previously failed within 1–2 iterations), API returns 558/558 fields. ## Verification - `npx nx typecheck twenty-server` — clean - Full `twenty-server` unit suite — green (5651 passed) - `group-by-resolver` integration suite — 19/19 on a fresh 0.3.26-seeded test DB - Postcard app-sync stress loop — 12/12 on this exact stack - Lockfile: typeorm 0.3.26 + new `sql-highlight` dep, `esbuild`/uuid entries untouched |
||
|
|
d0884bd708 |
Fix missing datetime filter type (#21451)
Currently datetime fields are only typed to be filtered by string Add a proper typing to match gql filters ## Before <img width="750" height="492" alt="image" src="https://github.com/user-attachments/assets/ff3a5423-3bb0-4295-84c9-e404489354f6" /> ## After <img width="537" height="511" alt="image" src="https://github.com/user-attachments/assets/d8c8219f-b7de-41b0-96cb-5adbfda7a91d" /> |
||
|
|
0ac4f237c0 |
fix(server): stop redundant lambda rebuilds causing build-lock acquisition failures (#21442)
## Context `Lambda invocation failed for function '<id>' during build: Failed to acquire lock for key: lambda-build:<id>` fires ~1000 times/day in production. ## Root cause `LambdaExecutorManagerService.buildExecutor` re-checks `canSkip` inside the `lambda-build:<functionId>` lock, but the re-check reuses the `flatApplication` snapshot captured when the request started. `canSkip` depends on `!flatApplication.isSdkLayerStale`, so: 1. An app sync/install regenerates the SDK client and sets `isSdkLayerStale = true` 2. All in-flight executions of the function fail `canSkip` and queue on the lock 3. The first holder rebuilds and `markSdkLayerFresh` clears the flag in DB + workspace cache 4. Queued waiters can't see that fix — their in-memory snapshot still says stale — so **each waiter redoes the full rebuild serially** (download SDK archive, delete + republish layer, update function config, wait for update) 5. The lock is held back-to-back for minutes; everyone deeper in the queue exhausts the 120s retry budget and throws The local driver already handles this correctly (`LocalLayerManagerService` refreshes the flat application from the workspace cache inside its lock); the lambda driver missed it. ## Fix - Refresh `flatApplication` from the workspace cache inside the lock before re-checking `canSkip`, so waiters skip in ~100ms once the first holder finishes - Degrade gracefully on lock-acquisition timeout: re-check build status with fresh data and proceed with the invocation if the executor is already usable, instead of failing the run (introduces a typed `CacheLockAcquisitionError` so only that case is caught) ## Test plan - [x] `cache-lock.service.spec.ts` passes - [x] `lint:diff-with-main` + typecheck pass - [ ] Monitor `Failed to acquire lock for key: lambda-build:*` error rate in production after deploy |
||
|
|
184c4948d6 |
security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
|
||
|
|
303c415dd1 |
fix(ai) - add logs + remove dashboard building (#21440)
- add logs for thread finishing without agent message - add logs to monitor toolCall token usage - remove dashboard building via AI (before fixing it) - fix Anthropic compute |
||
|
|
a6fcbf58e4 |
fix(billing) - enable upgrade if invoice already paid (#21450)
Had an issue concerning a user with credits, then invoice automatically paid. Upgrade failed |
||
|
|
462dd3b0e9 |
security: uuid CVE — bump bullmq/msal/blocknote + scoped resolutions for the rest (Dependabot alert 1289) (#21441)
Closes the uuid Dependabot alert — [1289](https://github.com/twentyhq/twenty/security/dependabot/1289) — by **upgrading the parents that bump cleanly** and **scope-resolving only the ones that genuinely can't**. `uuid < 11.1.1` (buffer-bounds check in v3/v5/v6) is pulled by ~9 transitives. ### Bumped (parent upgrade — drops uuid<11, no behavior change; typecheck verified) - **bullmq** 5.40.0 → 5.78.0 — also aligned **ioredis** 5.6.0 → 5.10.1 (bullmq pins it) and fixed the renamed `Job.returnValue→returnvalue` / `stackTrace→stacktrace` (now `string[]|null`) in `admin-panel-queue.service.ts`. - **@azure/msal-node** ^3.8.4 → ^5.2.3 (5.2.4 was age-gate-quarantined). - **@blocknote/** ×5 ^0.47.3 → ^0.51.4. ### Scope-resolved to uuid 11.1.1 (no clean bump exists) - **sockjs** (latest; pinned by webpack-dev-server) and **@ptc-org/nestjs-query-typeorm** (9.4.0 *is* latest, pins `^10`) — no version drops uuid. - **typeorm** — a `patch:` dep / ORM core, too risky to bump. - **node-ical** 0.26 (type-model overhaul → caldav-parser rewrite) and **googleapis** 173 (Gmail/OAuth, 105→173) — large breaking migrations; **deferred to dedicated PRs**. - **@cypress/request** — transitive (cypress isn't a direct dep). Resolutions are **per-package** and preserve the intentional **uuid 13.x** (twenty-sdk / create-twenty-app). ### Verification - `twenty-server` typecheck ✓ (0 errors), `twenty-front` typecheck ✓ (0 errors). - `yarn install --immutable` ✓; every uuid resolves to **11.1.1** or **13.0.2**. - bullmq/msal/typeorm runtime exercised by the **server integration tests**; @blocknote by the **storybook tests** in CI. |
||
|
|
233a6f9fb1 |
fix(server): register Lingui message compiler to stop "Uncompiled message detected" log flood (#21416)
## Summary Fixes #21415. Server-side `` t`…` `` macro calls (e.g. `FlatEntityMapsException`) resolve against Lingui's global `i18n` singleton, which `I18nService` never loads a catalog into and never registers a messages compiler on. With no `_messageCompiler` set, every such lookup logs `Uncompiled message detected!` and falls back to the raw string — flooding server logs (enough to hit hosting log rate limits) and disabling ICU interpolation on those messages. The warning is emitted unconditionally by `@lingui/core` (not `NODE_ENV`-gated). ## Changes Register `compileMessage` via `setMessagesCompiler` in `I18nService.loadTranslations()`: - on the global `i18n` singleton (used by the `t` macro), and - on each per-locale instance. `@lingui/message-utils` is already a transitive **runtime** dependency of `@lingui/core` (`@lingui/core@5.1.2 → @lingui/message-utils@^5.1.2`), so no new dependency is added. ## Testing Deployed on a real instance: before, the server emitted hundreds of `Uncompiled message detected` lines/sec (saturating the log rate limit); after, steady-state shows `0`, and the worker (which shares the code path) likewise shows `0`. App behaviour is unchanged — messages already rendered via fallback; this silences the warning and enables ICU on the affected messages. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
20c83e1f86 |
fix(kanban): preserve scroll on board re-init + propagate same-column reorders via SSE (#20637)
closes https://discord.com/channels/1130383047699738754/1504130730840821860 https://github.com/user-attachments/assets/d5833031-01c6-4e46-b699-c29c42435a53 ## Summary Fixes two related issues with the kanban (board view) collaboration experience: 1. **Scroll-to-top on every data change** — `triggerRecordBoardInitialQuery` always scrolled the board to the top, even when re-initializing for a single-record data change (SSE echo of your own mutation, a collaborator's update). Scroll reset only makes sense when the dataset itself changes (filter / sort / group). 2. **Same-column reorders by other users did not propagate** — the server's diff function stripped `FieldMetadataType.POSITION`, so position-only updates produced empty `updatedFields` and short-circuited event emission entirely. SSE clients never received them. ## What's in here - **Frontend** — `useTriggerRecordBoardInitialQuery` now exposes a `triggerRecordBoardInitialQueryWithoutScrollReset` variant; data-driven re-inits in `RecordBoardDataChangedEffect` use it, while genuine filter / sort / group changes keep the scroll-resetting `triggerRecordBoardInitialQuery`. `getRecordBoardEffectsForUpdateInputs` classifies each update as `trigger-initial-query` / `reposition-records` / `none`. For position- or group-only changes we skip the re-query and reposition records in place in the store (`useRepositionRecordsOnBoard`), which avoids the flicker and preserves scroll. - **Server** — removes `POSITION` from `objectRecordChangedValues`' strip list, so position-only updates emit a non-empty diff and flow through SSE. Position is now treated as a field like any other across all event consumers (SSE, webhooks, workflows, logic functions); a trigger with an explicit field filter still excludes it. |
||
|
|
941c9e7586 |
fix: match relation field filters in optimistic & RLS record matchers (#21301)
Closes #21345. ## What It should be caused by the GraphQL optimistic query. `isRecordMatchingFilter` (front, Apollo optimistic cache) and `isRecordMatchingRLSRowLevelPermissionPredicate` (server, RLS) now handle a view filter that targets a **relation field object** (e.g. an "is (not) empty" filter on a relation) by matching against the related record id, instead of throwing. <img width="3436" height="2250" alt="CleanShot 2026-06-08 at 06 44 01@2x" src="https://github.com/user-attachments/assets/1dccbd1e-133c-4f4a-a0a9-7ccd02a9a0ae" /> <img width="1496" height="380" alt="CleanShot 2026-06-08 at 06 45 34@2x" src="https://github.com/user-attachments/assets/e5c2071e-69df-4d99-bb7d-66d503a175b6" /> ## Why Both matchers only implemented the relation **join column** branch (`fooId`) and threw `Not implemented yet, use UUID filter instead on the corresponding "fooId" field` for the relation field itself (`foo`). In practice the UI still stores relation filters keyed on the relation object, so any view with such a filter made every create/update/delete on that object throw: the optimistic effect re-evaluates all active view filters against the changed record and hits the unimplemented branch. Repro: add a self-relation field on People (e.g. "Referred By"), put it in a view filter as "is not empty", then edit any Person. The optimistic update throws. ## Behaviour change | Scenario | Before | After | |---|---|---| | View filter on relation object (`referredBy is not empty`), then edit a record | Throws `Not implemented yet...` | Record matched by related id; update succeeds | | Filter on relation join column (`referredById`) | Worked | Unchanged | ## Test plan ```bash cd packages/twenty-front && npx jest isRecordMatchingFilter cd packages/twenty-server && npx jest is-record-matching-rls-row-level-permission-predicate ``` - [x] Front: relation `is empty` / `is not empty` / `in` match by related id; join-column path still passes (20/20) - [x] Server: relation `is empty` / `is not empty` match by related id (9/9) - [x] `lint:diff-with-main` clean on both packages Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
6e147a548b |
security: clear twenty-apps & seed-dependencies CVE alerts (#21410)
Clears the Oneleet/dependency CVE alerts from the `twenty-apps` example/internal app lockfiles and the application-package `seed-dependencies` template — all via parent/direct dependency upgrades, **no `resolutions` overrides**. ## Lock refresh (non-breaking, within existing ranges) - **postcss** 8.5.8/8.5.9 → 8.5.15 — CVE-2026-41305 — postcard, hello-world, self-hosting - **ip-address** 10.1.0 → 10.2.0 — CVE-2026-42338 — postcard, hello-world, self-hosting, twenty-for-twenty - **yaml** 1.10.2 → 1.10.3 — CVE-2026-33532 — call-recording ## seed-dependencies (direct/parent bumps) - **uuid** `^10.0.0 → ^11.1.1` (direct) — CVE-2026-41907 - **body-parser** `^1.20.4 → ^1.20.5`, which pulls **qs** 6.15.2 — CVE-2026-8723 - **socks** 2.8.3 → 2.8.9 (refresh), which pulls **ip-address** 10.2.0 — CVE-2026-42338 ## twenty-for-twenty - **resend** 6.12.0 → 6.12.4 (refresh): 6.12.4 drops the `svix` dep that pulled the vulnerable **uuid** 10.0.0, leaving only uuid 13.0.2 — CVE-2026-41907 All flagged packages were transitive (except the direct seed-deps `uuid`); no app source changes. |