## Context
Introduces a middleware that automatically sets the workspace auth
context in AsyncLocalStorage for HTTP requests, making it available
throughout the request lifecycle without explicit parameter passing.
The motivation behind this change is to reduce boilerplate and simplify
the developer experience when working with workspace data in HTTP
request handlers.
The Problem (Before)
Every HTTP request handler that needed to access workspace data had to:
- Extract auth-related info from decorators (@AuthWorkspace(),
@AuthUserWorkspaceId(), etc.) in controller/resolver and pass down to
services
- Build or pass the authContext explicitly (sometimes with type
assertion which was flaky)
Then call executeInWorkspaceContext(authContext, async () => { ... })
## Changes
- Add WorkspaceAuthContextMiddleware that extracts auth context from the
request and stores it in AsyncLocalStorage
- Register middleware for GraphQL, metadata, and REST routes (runs after
hydration middlewares)
- Simplify executeInWorkspaceContext signature: fn is now the first
parameter, authContext is optional second
- If authContext is not provided, it's automatically retrieved from the
storage (set by middleware)
- Update all callers (~120 files) to use the new parameter order
- Fixes a bug in search where system auth context was used, bypassing
RLS feature.
This PR reverts the change that was made to turn `Date` objects into ISO
string, because right now there are too many places in the code that use
both, either Date or ISO string from an entity returned by the querying
layer.
Unifying everything with ISO string is clearly the long term goal, but
right now it is too difficult, we should first refactor each part to ISO
string, then at the end remove Date from the codebase.
This PR fixes a bug that arises in `formatResult` following up the
recent refactor for DATE_TIME :
https://github.com/twentyhq/twenty/pull/17407
In the case of workflows, we pass a plain object to `formatResult` :
```ts
{
before: null,
after: '2026-01-27T10:59:15.525Z'
}
```
So a case has been added to handle this.
@Weiko are we ok with this more restrictive else-if part in this util ?
We could also just put a `continue` for unknown shapes.
Added example output showing what the roles table should look like after
creating the postgres role
Co-authored-by: Tom Larson <tomlarson@Toms-MacBook-Pro.local>
## Implement Navigation Menu Items Frontend
Implements the frontend for navigation menu items, the new system
replacing favorites.
### Changes
- Added GraphQL fragments and queries for navigation menu items
- Added hooks for managing navigation menu items (create, update,
delete, sorting, filtering)
- Updated components to use navigation menu items instead of favorites
- Added test coverage for utility functions
### Migration Note
The favorites and navigation menu item modules currently exist in
parallel. The favorites code will be removed once all data has been
migrated to navigation menu items.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Replaces Favorites with feature-flagged `NavigationMenuItem` across
frontend and backend, while keeping Favorites as fallback until
migration completes.
>
> - UI: new `navigation-menu-item` components (folders, orphan items,
drag provider/droppable, icons, skeleton), dispatcher components to
switch from Favorites, and updated “Add to favorites” action to create
`NavigationMenuItem` when `IS_NAVIGATION_MENU_ITEM_ENABLED`
> - DnD: shared `validateAndExtractFolderId` and droppable id utils
moved to `ui/layout/draggable-list`; favorites DnD updated to use shared
utils
> - GraphQL (client): add fragments, queries, mutations, hooks
(create/update/delete/find), and generated types; added
`RecordIdentifier` and `targetRecordIdentifier` on `NavigationMenuItem`
> - Prefetch: new prefetch state/effect for navigation menu items; skip
favorites prefetch when flag enabled
> - Backend: add DTOs (`NavigationMenuItem`, `RecordIdentifier`),
resolver `targetRecordIdentifier` field, service logic to fetch record
identifiers with permission-aware access and image signing,
`getRecordImageIdentifier` util, entity relation to `view`, and
migration adding FK on `viewId`
> - Feature flags & seeding: add `IS_NAVIGATION_MENU_ITEM_ENABLED` to
enums, dev seeder enables it; standard app seeds workspace navigation
menu items instead of favorites when flag on
> - Tests: add unit tests for sorting/labels/folder id and related utils
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c99746f08b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
## Today
CTA is confusing
<img width="1442" height="947" alt="image"
src="https://github.com/user-attachments/assets/d5dd6229-e78d-4068-acbc-b5766b725b14"
/>
## Summary
- Replace the "Add account" button text with "Finish Setup" at the end
of the calendar account configuration flow
- Change the button icon from a plus sign (`IconPlus`) to a floppy disk
(`IconDeviceFloppy`) to better reflect saving/completing the setup
This PR fixes the inconsistency between Calendar and Table views when
trying to edit createdAt date field.
Previously, calendar cards could be dragged even though the createdAt
field are UI read-only, resulting in the confusing and inconsistent
behavior compared to the Table view where the field is not editable.
The issue was caused by the drag logic checking only user permissions
and not the field’s UI read-only metadata. This change updates the
calendar drag logic to also check for isUIReadOnly, ensuring that
records are not draggable when the selected calendar date field is
read-only.
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
Previously the command would have been `old` renaming only any `owner`
field on `opportunity` object
Now we also search for any colliding `joinColumnName` with `ownerId`
which is also introduced by the new standard owner field
In a nutshell: previously gracefully handling existing custom `owner`
field collision but not for RELATION types that has an additional
collision surface: `joinColumnName`
Related
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3799872079
Fixes issue where focusing the record title input would reset the title
to empty. This ensures the draft value is properly initialized from the
field value if it's undefined or empty when the input is focused.
Fixes#17437
---------
Co-authored-by: Daniel <daniel@example.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
In this PR we're introducing mainly two branded type signatures for both
`JsonbProperty` entities properties and `SerializedRelation` (jsonb
serialized property storing another entity id).
Allowing to dynamically map over them later in order to build universal
`jsonb` `serialized` relations.
## `JsonbProperty`
A branded wrapper type that marks entity properties stored as PostgreSQL
JSONB columns. It adds a phantom brand `__JsonbPropertyBrand__` to
object types while leaving primitives unchanged. The branded key is
optional and typed as never, also omitted when transpiled to
`UniversalFlat`
**Should be used at entities lvl only:**
```typescript
@Column({ type: 'jsonb', nullable: false })
gridPosition: JsonbProperty<GridPosition>;
@Column({ nullable: false, type: 'jsonb', default: [] })
publishedVersions: JsonbProperty<string[]>;
```
## `SerializedRelation`
A branded string type that marks foreign key IDs stored inside JSONB
objects. These are entity references serialized within a JSONB column
rather than being a regular database foreign key.
**Usage in jsonb property generic***
```ts
type FieldMetadataRelationSettings = {
relationType: RelationType;
onDelete?: RelationOnDeleteAction;
joinColumnName?: string | null;
junctionTargetFieldId?: SerializedRelation;
};
```
## `FormatJsonbSerializedRelation<T>`
A transformation type that processes JSONB properties for universal
entity mapping. It:
1. Detects properties with the `JsonbProperty` brand
2. Finds `SerializedRelation` properties
3. Renames them from `*Id` to `*UniversalIdentifier`
4. Removes the brand from the output type ( optional though )
```typescript
// Input: JsonbProperty<{ targetFieldMetadataId: SerializedRelation }>
// Output: { targetFieldMetadataUniversalIdentifier: SerializedRelation }
```
## Result
An example of the dynamic type mapping, through a type-test example
```ts
type SettingsTestCase = UniversalFlatFieldMetadata<
| FieldMetadataType.RELATION
| FieldMetadataType.NUMBER
| FieldMetadataType.TEXT
>['settings']
type SettingsExpectedResult =
| {
relationType: RelationType;
onDelete?: RelationOnDeleteAction | undefined;
joinColumnName?: string | null | undefined;
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
}
| {
dataType?: NumberDataType | undefined;
decimals?: number | undefined;
type?: FieldNumberVariant | undefined;
}
| {
displayedMaxRows?: number | undefined;
}
| null;
type Assertions = [
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
]
```
## Remarks
- Removed duplicated twenty-server and twenty-shared typed
- Removed class validator instances for default value that were not used
at runtime, we will refactor that to add validation across all entities
following a same pattern
# Introduction
Currently refactoring `flatEntity` typing, encountering some tsc errors
due to this fk aggregator custom override
It shall now follow generic pattern leading to be named `fieldIds`
Needs to flush object cache when released
fixes#16340
we are updating the cache partially to prevent the flow of the corrupted
fields into the cache
the fields get corrupted during the mutation process potentially
overriding the recoil cache as we are passing the `newRecordCache`
directly in `upsertRecordsInStore`, the newRecordCache is thin and hence
the recoil wipes out the fields that are undefined or empty
we prevent this by extracting the partial data ( only the data which is
being updated in the field ) and passing it to `upsertRecordsInStore`,
as it only touched the specific fields and updates the data, leaving the
other fields untouched.
https://github.com/user-attachments/assets/5256bef7-70c3-47b3-b2ce-dd02ee1a2de8
---------
Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
A command allowing to invalidate flat cache entries. Extends the active
or suspended workspace coverage.
## Args
### --metadataName
If provided will invalidate metadata and related metadata cache entry (
can be repeated see usage )
### --all-metadata
Will invalidate all metadata entries
## Usage example
```
npx nx command twenty-server cache:flat-cache-invalidate --metadataName viewFilter --metadataName objectMetadata
```
```
npx nx command twenty-server cache:flat-cache-invalidate --all-metadata -w 0000-0000-0000-0000
```
## Summary
- **Fix junction relation toggle not being saved**: The form schema
wasn't tracking the `settings` field, so changes to
`junctionTargetFieldId` weren't marked as dirty
- **Add type-safe documentation paths**: Generate TypeScript constants
from `base-structure.json` to prevent broken documentation links
- **Create many-to-many relations documentation**: Step-by-step guide
for building many-to-many relations using junction objects
- **Update `getDocumentationUrl`**: Now uses shared constants from
`twenty-shared` for base URL, default path, and supported languages
## Key Changes
### Junction Toggle Fix
- Added `settings` field to the form schema in
`SettingsDataModelFieldRelationForm.tsx`
- Fixed the toggle to properly merge settings when updating
`junctionTargetFieldId`
### Type-Safe Documentation Paths
- New constants in `twenty-shared/constants`:
- `DOCUMENTATION_PATHS` - All 161 documentation paths as typed constants
- `DOCUMENTATION_SUPPORTED_LANGUAGES` - 14 supported languages
- `DOCUMENTATION_BASE_URL` / `DOCUMENTATION_DEFAULT_PATH`
- Generator script: `yarn docs:generate-paths`
- CI integration: Added to `docs-i18n-pull.yaml` workflow
### Documentation
- New article:
`/user-guide/data-model/how-tos/create-many-to-many-relations`
- Updated `/user-guide/data-model/capabilities/relation-fields.mdx` with
Lab warning and link
## Test plan
- [ ] Verify junction toggle saves correctly when enabled/disabled
- [ ] Verify documentation link opens correct localized page
- [ ] Verify `yarn docs:generate-paths` regenerates paths correctly
This fixes the #15896. For problem statement. Please refer to the shared
video mentioned in the issue.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Aligns filter defaults across UI by centralizing initial value
computation.
>
> - Add boolean handling in `useGetInitialFilterValue` to return
`value/displayValue` of `'false'` when operand is `IS`
> - Update `WorkflowDropdownStepOutputItems` to use
`getInitialFilterValue` for initial `value` instead of an empty string,
based on field type and default operand
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3ce05fa31a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Closes [1744](https://github.com/twentyhq/core-team-issues/issues/1744).
This PR migrates attachments to morph relations behind a feature flag,
following the TimelineActivity pattern. it introduces the
`IS_ATTACHMENT_MIGRATED` flag, updates standard field metadata and
indexes to use morph relations, adds a workspace migration that renames
`attachment.*Id` columns to `target*Id` and converts the corresponding
field metadata to `MORPH_RELATION` with a shared `morphId`. On the
frontend, attachment read/write paths now switch to `target*Id` when the
flag is enabled.
It also fixes optimistic filtering for morph join columns. The metadata
API deduplicates morph fields, so attachments now expose a single target
field of type `MORPH_RELATION` plus a `morphRelations` array listing
each target object. Because only one `settings.joinColumnName` is
returned (e.g. `targetRocketId`), filters like `targetCompanyId` don’t
map to any field and the optimistic cache code throws.
`doesMorphRelationJoinColumnMatch` resolves this by computing all valid
join column names from `morphRelations` using
`computeMorphRelationFieldName` and comparing them to the filter key.
That makes filters like `targetCompanyId` resolvable even with a single
target field, so attachment uploads and list matching no longer crash.
<img width="477" height="474" alt="image"
src="https://github.com/user-attachments/assets/50e19418-3438-4d1e-9f1f-1bc1a03174a9"
/>
<br />
<br />
Today the metadata API returns one morph field called `target` and a
list of possible targets (`morphRelations`), but it does not tell us the
join column for each target. That’s why the Frontend had to compute join
column names.
If we want to fix this at the API level, there are two options:
- Add join column names to each target in `morphRelations` (e.g. company
→ `targetCompanyId`). This is additive and low‑risk.
- Return each target as its own field (`targetCompany`, `targetPerson`,
etc.) instead of a single target. This is a larger change because it
changes the shape of metadata and would require more UI updates.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Introduces morph relations for attachments behind
`IS_ATTACHMENT_MIGRATED`, aligning server schema/metadata and frontend
behavior.
>
> - Adds `IS_ATTACHMENT_MIGRATED` flag (frontend/server) and
seeds/defaults; updates generated GraphQL enums
> - New workspace upgrade `1.17` command migrates data: renames
`attachment.*Id` → `target*Id` and converts related fields to
`MORPH_RELATION` with shared `morphId`
> - Updates standard field metadata and indexes to `target*` (attachment
+ related objects), dev seeds, snapshots, and workspace entity types
> - Frontend: switches attachment read/write filters via
`getActivityTargetObjectFieldIdName` using the feature flag; updates
hooks/components (`useAttachments`, `useUploadAttachmentFile`, editors);
expands `Attachment` type
> - Fixes optimistic cache filtering to recognize morph join columns in
`isRecordMatchingFilter` by computing valid join-column keys from
`morphRelations`
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f208fa23b1. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
### Summary
The style guide currently discourages type imports, which contradicts
the enforced ESLint rule
`@typescript-eslint/consistent-type-imports` and existing code usage
(e.g. Storybook stories).
This PR updates the documentation to recommend inline type imports,
aligning the style guide
with tooling and preventing contributor confusion.
### Why
- Aligns documentation with enforced ESLint configuration
- Prevents ESLint auto-fix conflicts
- Improves contributor experience
Fixes #<### Summary
The style guide currently discourages type imports, which contradicts
the enforced ESLint rule
`@typescript-eslint/consistent-type-imports` and existing code usage
(e.g. Storybook stories).
This PR updates the documentation to recommend inline type imports,
aligning the style guide
with tooling and preventing contributor confusion.
### Why
- Aligns documentation with enforced ESLint configuration
- Prevents ESLint auto-fix conflicts
- Improves contributor experience
Fixes #<#17222>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Updates documentation to match tooling and current usage.
>
> - Replaces "Enforcing No-Type Imports" with **Type Imports**,
recommending inline type imports
> - Updates examples to prefer `import { type X } from ...` and avoid
importing types as runtime values
> - Clarifies ESLint `@typescript-eslint/consistent-type-imports`
configuration to prefer explicit type imports and enforce
`inline-type-imports` fix style
> - Changes confined to
`packages/twenty-docs/l/fr/developers/contribute/capabilities/frontend-development/style-guide.mdx`
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e2250e43ad. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Fixes#16388
Now we have used the metadata fetching from network using
resetVirtualizationBecauseDataChanged(), as we navigate back to the
table after updating the field content. As the virtualised table uses
the "cache-first" strategy, it fails to give fresh data from the data
base
Please let me know i we need to add the loading state (and it's UI
requirements) while it fetches the cell content, as currently the cell
is empty until it's populated by fresh data
https://github.com/user-attachments/assets/901085ba-4337-4a5d-86c8-15ac84250e8f
---------
Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
## Summary
- The `createMany` API was returning records in arbitrary order because
`fetchUpsertedRecords` used `WHERE id IN (...)` without `ORDER BY`
- This caused flaky tests that assumed input order was preserved (e.g.,
`people-merge-many.integration-spec.ts`)
- Fixed by reordering the fetched records to match the original input
order from `generatedMaps`
## Root Cause
```typescript
// Before: No ORDER BY, so SQL returns in arbitrary order
const upsertedRecords = await queryBuilder
.where({ id: In(objectRecords.generatedMaps.map((record) => record.id)) })
.getMany(); // Returns in arbitrary order!
```
## Fix
```typescript
// After: Reorder results to match original input order
const orderedIds = objectRecords.generatedMaps.map((record) => record.id);
const upsertedRecords = await queryBuilder
.where({ id: In(orderedIds) })
.getMany();
// Preserve original input order
const recordsById = new Map(upsertedRecords.map((record) => [record.id, record]));
return orderedIds
.map((id) => recordsById.get(id))
.filter((record) => record !== undefined);
```
## Test plan
- [x] Run `people-merge-many.integration-spec.ts` multiple times -
passes consistently
- [x] Lint passes
## Summary
- Adds SSRF (Server-Side Request Forgery) protection to webhook requests
by using the same secure axios adapter already used by HTTP workflow
actions
- Prevents webhooks from making requests to private/internal IP
addresses (10.x, 192.168.x, 172.16-31.x, 169.254.x, localhost)
- Adds specific error logging when a webhook fails due to SSRF
protection
## Context
The HTTP workflow tool (`HTTP_REQUEST` action) already had SSRF
protection via `HTTP_TOOL_SAFE_MODE_ENABLED`, but webhooks were using
`HttpService` directly without this protection. This inconsistency meant
users could potentially configure webhooks to probe internal
infrastructure.
### What's protected now:
| Feature | Before | After |
|---------|--------|-------|
| HTTP Workflow Action | Protected (secure adapter) | Protected (secure
adapter) |
| Webhooks | **Unprotected** | Protected (secure adapter) |
### The secure adapter validates:
1. Protocol must be `http:` or `https:`
2. DNS resolution of hostname
3. Resolved IP must not be in private ranges
## Test plan
- [ ] Configure a webhook with an external URL (e.g.,
`https://webhook.site`) - should work
- [ ] Configure a webhook with `http://localhost:3000` - should fail
with SSRF error in audit log
- [ ] Configure a webhook with `http://10.0.0.1/test` - should fail with
SSRF error in audit log
- [ ] Configure a webhook with a domain that resolves to a private IP -
should fail
This PR modifies our broadly used `formatResult` util to counter act
TypeORM transforming any date time to a `Date` object.
Instead we return the ISO string for any `DATE_TIME`, this way we're not
transporting Date object from one function to another in the backend.
We do this because there was problems working with events utils that
take string date time in parameters and received Date objects.
As this is a recurring problem and because it's an opinionated choice
from TypeORM, we chose to switch to string only in our codebase, from
TypeORM's output to frontend.
This PR changes the shape and logic of SSE events `DELETE` and
`RESTORE`, because they behave like `UPDATE` events in practice, they
should share the same logic.
Before this PR, it was impossible for the frontend to obtain the
`deletedAt` value, and the logic to handle soft-delete and restore would
have been flawed.
Because there is a typing confusion in the parameters of
`formatTwentyOrmEventToDatabaseBatchEvent`, due to TypeORM, we also
update this util to only accept an array of records, instead of `T |
T[]`. We should improve our TypeORM layer in the future.
Also the naming was not clear, so we clearly use `recordsAfter` and
`recordsBefore` as much as possible, because that is what we have at the
end in events.
Events are sent from their respective query builders, so these last ones
have been updated also.
Because TypeORM `soft-remove` operation only returns record ids, we add
`.getMany()` to fetch all fields for soft-removed records, so that our
event can have before and after.