Commit Graph

3645 Commits

Author SHA1 Message Date
Paul Rastoin 406e269cf1 Invalidate flat cache command (#17442)
# 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
```
2026-01-26 13:08:31 +00:00
github-actions[bot] 90a496bbe8 i18n - translations (#17443)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-26 14:08:39 +01:00
Raphaël Bosi 7e7a535af0 Add front component widget (#17440)
Modified the data model and introduced a seed to introduce front
components widgets.
2026-01-26 12:45:58 +00:00
Paul Rastoin 3be05641fa Fix upgrade command order backfillStandardPageLayoutsCommand (#17430)
# Introduction
`backfillStandardPageLayoutsCommand` expects field and object metadata
to have been identified in prior to be run
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3796933563
2026-01-25 18:48:53 +00:00
Paul Rastoin 3b512164d1 [Debug log level] Print validation build result failure (#17423)
# Introduction
As per title, in order to ease debug
Motivation
https://github.com/twentyhq/twenty/issues/17413#issuecomment-3796168946
2026-01-25 11:10:37 +00:00
Abdullah. fa0615d41e Migrate attachments to morph relations + fix morph join column filtering (#17381)
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
f208fa23b1135af114f484f6d65041a816d6e718. 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>
2026-01-24 21:17:24 +00:00
Félix Malfait 389b6ce7b2 fix(twenty-server): preserve input order in createMany response (#17412)
## 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
2026-01-24 12:49:48 +00:00
Félix Malfait cd7c2864d2 fix(twenty-server): add SSRF protection to webhook requests (#17403)
## 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
2026-01-24 10:46:46 +00:00
Lucas Bordeau a07590eba5 Change formatResult to return string instead of Date object for DATE_TIME (#17407)
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.
2026-01-23 18:22:01 +00:00
Lucas Bordeau 9ecab8fb82 Fix event logic for soft-delete and restore (#17393)
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.
2026-01-23 17:55:07 +00:00
Thomas Trompette 2346efd71f Add prometheus exporter (#17392)
Create a metric endpoint and expose prometheus gauge for event stream
count.
2026-01-23 15:38:15 +00:00
nitin 4c94e650a7 fix backfill command (#17402) 2026-01-23 14:46:39 +00:00
martmull 0091ef5f6c Sync built files (#17379)
as title, upload built files to local storage

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-23 13:43:53 +01:00
Raphaël Bosi 78dd43dbb0 Release dashboards V1 (#17386)
Remove `IS_PAGE_LAYOUT_ENABLED` feature flag entirely
2026-01-23 10:30:05 +00:00
Paul Rastoin 4c93ab5259 Introduce UniversalFlatEntityFrom (#17367)
# Introduction

Creating a `UniversalFlatEntityFrom` that strips out all the relation
and foreignKey properties in order to replace them with
`UniversalIdentifier` suffix

This data type will be major for the workspace migration workspace
agnostic refactor

## Chore
- renamed `flat-entity.type` to `flat-entity-from.type.ts` ( more
accurate to exported module )
- create static test type over the field metadata entity on quite
complex utils as both coverage and documentation

## Example
Here's an example of a `UniversalFlatEntityFrom<FieldMetadataEntity>`

```ts
const universalFlatFieldMetadata: UniversalFlatFieldMetadata<FieldMetadataType.RELATION> = {
  // Base properties (from FieldMetadataEntity, excluding relations and applicationId)
  universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
  applicationUniversalIdentifier: '5800681c-088e-4e2b-9fc3-bcf6e8ec2051',
  type: FieldMetadataType.RELATION,
  name: 'firstName',
  label: 'First Name',
  defaultValue: null,
  description: 'The first name of the person',
  icon: 'IconUser',
  standardOverrides: null,
  options: null,
  settings: {
    relationType: RelationType.ONE_TO_MANY,
  },
  isCustom: false,
  isActive: true,
  isSystem: false,
  isUIReadOnly: false,
  isNullable: true,
  isUnique: false,
  isLabelSyncedWithName: true,
  morphId: null,

  // Date properties cast to string
  createdAt: '2024-01-15T10:30:00.000Z',
  updatedAt: '2024-01-15T10:30:00.000Z',

  // ManyToOne relation universal identifiers (from FieldMetadataEntity relations)
  relationTargetFieldMetadataUniversalIdentifier:
    '550e8400-e29b-41d4-a716-446655440012',
  relationTargetObjectMetadataUniversalIdentifier:
    '550e8400-e29b-41d4-a716-446655440013',

  // Join column universal identifiers (foreignKey -> universalIdentifier)
  objectMetadataUniversalIdentifier: '550e8400-e29b-41d4-a716-446655440010',

  // OneToMany relation universal identifiers (array of related entity identifiers)
  viewFieldUniversalIdentifiers: [
    '550e8400-e29b-41d4-a716-446655440020',
    '550e8400-e29b-41d4-a716-446655440021',
  ],
  viewFilterUniversalIdentifiers: ['550e8400-e29b-41d4-a716-446655440030'],
  kanbanAggregateOperationViewUniversalIdentifiers: [],
  calendarViewUniversalIdentifiers: [],
  mainGroupByFieldMetadataViewUniversalIdentifiers: [],
};
```

## Settings
Will hop on the settings typing next. Might not be dynamic but
declarative though
2026-01-22 18:30:19 +00:00
github-actions[bot] ad44852880 i18n - translations (#17374)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-22 18:22:28 +01:00
Charles Bochet 210c66b5dd Add checksum to manifest (#17368)
## Refactor app-dev state management and build utilities

- Store only `manifest` in `AppDevState` instead of full
`ManifestBuildResult`; add `sourcePath` to `FileStatus`
- Pass `sourcePaths` directly to watchers instead of
`ManifestBuildResult`
- Only reset `fileUploadStatus` for functions/components when their
source paths change
- Add pure `updateManifestChecksum` utility that returns a new manifest
without side effects
- Extract `processEsbuildResult` to deduplicate build result processing
between watchers
- Rename `serverlessFunctions` → `functions` in SDK code (API unchanged)
- Extract `writeManifestToOutput` to shared `manifest-writer.ts`
2026-01-22 18:16:55 +01:00
neo773 843fda7564 CalDav throw error If a user tries to connect an unsupported server. (#17363)
Legacy CalDav servers don't support `syncCollection` which is required
to store a `syncCursor` this PR updates the code to let user know we
don't support their server.

Source:
https://github.com/natelindev/tsdav/blob/main/src/collection.ts#L193-L194
2026-01-22 18:12:00 +01:00
Thomas Trompette f11442953c Remove if-else workflow node flag (#17370)
As title
2026-01-22 16:33:52 +00:00
github-actions[bot] fe9c8f1429 i18n - translations (#17369)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-22 17:22:50 +01:00
Thomas Trompette c008e874e5 Allow variables with dots and keys (#17361)
Fixes
https://github.com/twentyhq/private-issues/issues/410#issuecomment-3781085655

Currently, JSON keys with spaces like { "toto toto": 123 } are rejected
with "JSON keys cannot contain spaces" error. This is problematic for
HTTP requests and webhook triggers where users cannot control the
response structure.

We use Handlebars to eval variables, segment-literal bracket notation to
escape keys with special characters:
Normal: {{step.normalKey}}
With spaces: `{{step.[key with space]}}`

So we simply need to wrap segments with spaces with brackets.

This PR: 
- Create shared path utilities to wrap the variable segments when needed
- Use it in all variable generation places
- Remove the restrictions

This body is now supported:
<img width="609" height="457" alt="Capture d’écran 2026-01-22 à 16 10
13"
src="https://github.com/user-attachments/assets/e7653c0a-df1e-49af-9c9a-4b7d59a99726"
/>
2026-01-22 16:06:53 +00:00
github-actions[bot] 29c12c0ffd i18n - translations (#17366)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-22 17:03:44 +01:00
Weiko 296a3099db Refactor rls backend simplify add tests (#17357)
## Context
Removing full CRUD to RLS since we are actually using an upsert only
over the role resolver, this removes a lot of unused code (could be
re-added later but unlikely). Simplified the folder arch at the same
time
Add simple integration tests to RLS
2026-01-22 15:44:18 +00:00
Ayesha Waseem 669da1a87d fix: display current object name in morph relation picker after rename (#17209)
**Summary**

Issue #16963: When an object is renamed, the morph relation picker still
shows the old name.

**Root cause**

The picker used searchRecord.objectNameSingular from the GraphQL search
response, which can be stale after a rename.
The search record stores the object name at query time, not the current
metadata.

**Solution**

- Updated SingleRecordPickerMenuItem:
- Added useObjectMetadataItems to access current object metadata.
- Look up the current object metadata by morphItem.objectMetadataId.
- Use labelSingular (or nameSingular as fallback) instead of
searchRecord.objectNameSingular for display.
- Updated MultipleRecordPickerMenuItemContent:
- Use objectMetadataItem.labelSingular (already available as a prop)
instead of searchRecord.objectNameSingular.

---------

Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
2026-01-22 15:40:57 +00:00
github-actions[bot] f8d1454c03 i18n - translations (#17362)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-22 16:40:58 +01:00
Paul Rastoin 2a28c34a37 Refactor workspace migration runner exception handling (#17310)
# Introduction
In this PR we catch all the runner errors coming from a single workspace
migration action execution.

**1. `WorkspaceMigrationActionExecutionException`** (low-level,
action-specific)
- Thrown from action handlers, utils, and helper functions
- Contains specific error codes: `FIELD_METADATA_NOT_FOUND`,
`OBJECT_METADATA_NOT_FOUND`, `ENUM_OPERATION_FAILED`, `NOT_SUPPORTED`,
etc.
- Simple structure: `message`, `code`, `userFriendlyMessage`
- No action context - just describes what went wrong

**2. `WorkspaceMigrationRunnerException`** (high-level, runner-scoped)
- Only two codes: `INTERNAL_SERVER_ERROR` and `EXECUTION_FAILED`
- `EXECUTION_FAILED` **requires** `action` + `errors` (contains the
action context)
- `INTERNAL_SERVER_ERROR` **requires** `message` (no action context)

## Refactor
- Removed the `relatedFlatEntityMapsKeys` from the `WorkspaceMigration`
type as they're directly inferred from passed actions
- Swallowing actions rollbacks errors in order to iterate over all of
them

## Testing
Created a very straigthforward install application from workspace
migration endpoint in order to start testing the introduced
`WorkspaceMigrationActionExecutionException`
Introduced a feature flag that stop the access to the endpoint if not
enabled
Whole taken direction are totally subjective and highly prone to
mutations ( endpoint location, naming and input schema see
`ts-expect-error` comment )
cc @martmull 

## Response error
```ts
{
  "eventId": "evt_a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "extensions": {
    "action": {
      "metadataName": "fieldMetadata",
      "type": "delete",
      "universalIdentifier": "20202020-6110-4547-9fd0-2525257a2c3f"
    },
    "code": "APPLICATION_INSTALLATION_FAILED",
    "errors": {
      "metadata": {
        "code": "ENTITY_NOT_FOUND",
        "message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f"
      },
      "workspaceSchema": {
        "code": "ENTITY_NOT_FOUND",
        "message": "Could not find flat entity in maps"
      }
    },
    "exceptionEventId": "exc_f9e8d7c6-5432-10ba-fedc-ba0987654321",
    "userFriendlyMessage": "Migration execution failed."
  },
  "message": "Migration action 'delete' for 'fieldMetadata' failed",
  "name": "GraphQLError"
}
```
2026-01-22 15:05:21 +00:00
martmull a68e05682b Serverless built updates (#17351)
follow up of https://github.com/twentyhq/twenty/pull/17317
2026-01-22 15:04:29 +00:00
Abdul Rahman 4ccdbf1150 fix: remove empty else-if branches and add numbering (#17315) 2026-01-22 14:30:11 +00:00
Thomas Trompette 8bf266626c [SSE] Event stream TTL refresh (#17337)
This PR update the redis subscription iterator wrapper with an heartbeat
system. Heartbeat interval are based on event stream TTL. Those refresh
the event stream TTL and active streams in redis.

I also cleaned a few functions that were not used anymore.

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-01-22 13:32:59 +00:00
Charles Bochet d537d941a7 Add sdk build (#17335)
## Summary

Adds `app:build` command as a one-shot version of `app:dev` - builds
manifest, functions, and front components once then exits (no watching).

**Changes:**
- Added `watch` option to `FunctionsWatcher` and
`FrontComponentsWatcher` to support both watch and one-shot modes
- Created `AppBuildCommand` reusing the same build logic as `app:dev`
with `watch: false`
- Added integration tests for `app:build` on both `rich-app` and
`root-app`
- Updated manifest types: `handlerPath` → `sourceHandlerPath` +
`builtHandlerPath`, `componentPath` → `sourceComponentPath` +
`builtComponentPath`
- Updated test app `package.json` scripts to match `create-twenty-app`
template
2026-01-22 14:36:40 +01:00
Etienne 0459f25dec Files v2 - Add new workspace field file upload resolver (#17325) 2026-01-22 13:15:52 +00:00
github-actions[bot] 96bdae4582 i18n - translations (#17339)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-22 14:09:39 +01:00
Abdul Rahman aff577689f Add syncable NavigationMenuItem entity to core schema (#17232)
Implements a new syncable `navigationMenuItem` entity in the core schema
to replace the workspace `favorite` entity.

## Next Steps
- Frontend integration ([separate
PR](https://github.com/twentyhq/twenty/pull/17268))
- Data migration (separate PR)
2026-01-22 12:48:51 +00:00
Félix Malfait 8dec37b826 feat: migrate typecheck to tsgo for faster type checking (#17331)
## Summary
- Switch `twenty-server` and `fireflies` typecheck from tsc to tsgo
(~75x faster)
- Enable tsgo in VSCode via `typescript.experimental.useTsgo` setting
- Add `@ts-nocheck` to `remove-step.spec.ts` to work around tsgo
performance issue with deep spread operations

## Performance
| Package | Before (tsc) | After (tsgo) |
|---------|-------------|--------------|
| `twenty-server` | ~150s | ~2s |
| `twenty-front` | ~40s | ~2s |

## Related
- Workaround for: https://github.com/microsoft/typescript-go/issues/2551

## Test plan
- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx typecheck twenty-front` passes  
- [x] `npx nx run-many --target=typecheck --exclude=fireflies` passes
(fireflies has pre-existing type errors)
2026-01-22 13:39:07 +01:00
martmull d833914888 Disable query logs if I want to (#17329)
As tile
2026-01-22 13:38:45 +01:00
neo773 a1c112a9b9 Fix null user crash in admin panel user lookup (#17336) 2026-01-22 13:35:30 +01:00
Weiko 4c565425c5 Fix workspace resolver when billing is not enabled (#17333) 2026-01-22 13:33:29 +01:00
martmull f92c8a98a4 2114 extensibility manage serverless execution from built code (#17317)
Summary

- Serverless functions are now built when created/updated instead of at
execution time
  - Added builtHandlerPath field to track pre-built function artifacts
- Renamed base-typescript-project to seed-project with pre-built ESM
output included
- Renamed file folder enums for consistency (Functions → BuiltFunction,
SourceCode → Source)
  - supports backwards compatibility with existing serverless functions

Tested with all combinations
- storage : local driver and S3 driver
- serverless : local driver and lambda driver
2026-01-22 12:56:44 +01:00
Weiko 9ee2cd0fe3 Enable RLS in lab (#17328) 2026-01-22 12:27:11 +01:00
neo773 1ffd23764c Fix Gmail sync edge case with multi-label emails (#17318)
Gmail emails often have multiple labels - an email in "XYZ" label
typically also has "INBOX". The previous negative filter approach
(-label:inbox -label:sent...) excluded these emails entirely, even when
they had a selected label.

Switched to positive OR filtering: (label:cyz OR label:xyz-visible)
-label:spam... which correctly fetches emails with any of the selected
labels regardless of other labels they have.

This edge case wasn't caught earlier because manual testing with INBOX
selected worked fine - it only surfaced when selecting custom labels
without INBOX.
2026-01-22 12:00:45 +01:00
Weiko 670ff1583e Fix RLS entitlement check + fix role page with RLS on object without object-permission not being displayed (#17326) 2026-01-22 11:48:24 +01:00
Paul Rastoin 6aa43b68f7 Identification cleanup (#17301)
# Introduction

following https://github.com/twentyhq/twenty/pull/17279
As we've finally identified all the syncable metadata entities, which
means they're expected to have non nullable applicationId and
universalIdentifier at pg_level we can remove previous retro comp
universalIdentifier fallbacking and update the dto too

~~This needs IdentifyRemainingEntitiesMetadataCommand and
MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand
to be run~~
```ts
[Nest] 197  - 01/21/2026, 3:08:35 PM     LOG [MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand] Successfully run MakeRemainingEntitiesUniversalIdentifierAndApplicationIdNotNullableMigrationCommand
```
2026-01-21 16:00:23 +00:00
Paul Rastoin 05f0d572e9 Refactor runner delete action to be workspace agnostic (#17276)
# Introduction

Straight to the point refactor passing only universalIdentifier in
workspace migration delete actions instead of id so it can be run on any
workspace

Related to
https://github.com/orgs/twentyhq/projects/1/views/8?pane=issue&itemId=132343284&issue=twentyhq%7Ccore-team-issues%7C1641
2026-01-21 15:57:52 +00:00
Thomas Trompette 2920dec6ba Add logs to debug workflow crons (#17302)
We have 3 crons that get a lot of timeout in sentry. 
Adding logs to help debugging.
2026-01-21 15:49:45 +00:00
nitin 26a70c23e9 [Dashboards] fix line chart duplicate widget bleed by hashing series IDs (#17303)
closes last issue in the bug bucket -
https://github.com/twentyhq/core-team-issues/issues/2109

<img width="695" height="41" alt="CleanShot 2026-01-21 at 19 40 38"
src="https://github.com/user-attachments/assets/c6872e11-1e36-4b4e-ab06-9943c22cfab2"
/>


 ### Root cause - 
line chart series have stable IDs, so Apollo normalizes them across
duplicated widgets and X‑axis edits leak
 
 ### Fix - 
prefix series IDs with a deterministic hash of objectMetadataId +
configuration
 
 before - 
 


https://github.com/user-attachments/assets/538eded8-7036-415a-b707-a10007c3aa11


 


after - 


https://github.com/user-attachments/assets/cf9ca89a-e7c9-4106-b6cd-fbc072d9a233
2026-01-21 15:39:29 +00:00
Paul Rastoin 54a9a1087b Fix identify remaining entities command to cover soft-deleted rows (#17304)
# Introduction
Some entities aren't passing the migration due to not covering the soft
deleted rows

## Entities that implements soft deletion
Inserted with universal programmatically
- rowLevelPermissionPredicate
- rowLevelPermissionPredicateGroup
- pageLayout
- pageLayoutTab
- pageLayoutWidget

Legacy might contains null
- viewFilterGroup
- serverlessFunction
- viewSort
2026-01-21 15:36:26 +01:00
neo773 2ecc1ba897 fix gmail message import policy (#17272)
Gmail sync was importing messages from folders users had explicitly
disabled because the folder filtering logic lived in the parent service
but the Gmail driver wasn't aware of the import policy when building its
API queries.
Moved messageFolderImportPolicy down to the driver level so Gmail can
- Build proper -label: exclusions during initial sync
- Added `buildGmailLabelSearchName` as our syntax for nested folders was
wrong
- Filter out messages from disabled folders during incremental sync via
history API
2026-01-21 12:49:58 +00:00
Paul Rastoin 596b7cc62d Deprecate nullable syncableEntity (#17279)
# Introduction
As we've been identifying both standard and custom entities for all the
metadata that had standard
We now still need to identify all custom entities enforcing them to have
an `applicationId` and `universalIdentifier`

In this PR we've removed the `SyncableEntityRequired` in favor requiring
props directly in the `SyncableEntity`
Which means that all metadata in db will now expect non nullable
applicationId and universalIdentifier across the whole application

Will add some type cleanup later in
https://github.com/twentyhq/twenty/pull/17277
2026-01-21 10:23:55 +00:00
github-actions[bot] 6b97e3b428 i18n - translations (#17296)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-01-20 22:21:02 +01:00
Félix Malfait 3ed67b825e feat: implement generic many-to-many junction relation support (#16820)
## Overview

This PR implements **generic many-to-many relation support** through
junction tables (also known as associative entities or join tables).
This replaces the need for hardcoded taskTarget/noteTarget logic and
provides a flexible foundation for modeling complex entity
relationships.

## Architecture

### Data Model

Many-to-many relationships are implemented using a **junction object
pattern**:

```
┌─────────┐       ┌──────────────────┐       ┌─────────┐
│  Pet    │──────>│   PetRocket      │<──────│ Rocket  │
│         │ 1:N   │  (junction)      │  N:1  │         │
│ rockets ├───────┤ pet    : Pet     ├───────┤         │
└─────────┘       │ rocket : Rocket  │       └─────────┘
                  └──────────────────┘
```

The junction object (PetRocket) has:
- A `MANY_TO_ONE` relation to **Pet** (the source)
- A `MANY_TO_ONE` relation to **Rocket** (the target)

The source object (Pet) has a `ONE_TO_MANY` relation pointing to the
junction, with **field settings** that specify which target field to
follow.

### Field Settings Schema

Junction configuration is stored in `FieldMetadataRelationSettings`:

```typescript
{
  relationType: "ONE_TO_MANY",
  // Points to the target field on the junction object
  junctionTargetFieldId?: string;      // For regular relations
  junctionTargetMorphId?: string;      // For polymorphic relations
}
```

**Two configuration modes:**
1. **`junctionTargetFieldId`** - References a specific `RELATION` field
on the junction
2. **`junctionTargetMorphId`** - References a `morphId` group for
polymorphic targets (e.g., link to Person OR Company)

### GraphQL Query Generation

When a junction relation is detected, the GraphQL fields are generated
to fetch the nested target:

```graphql
query GetPetWithRockets {
  pet(id: "...") {
    rockets {           # ONE_TO_MANY to junction
      id
      rocket {          # Target field on junction
        id
        name
        __typename
      }
    }
  }
}
```

For polymorphic junction targets:
```graphql
caretakerPerson { id, name }
caretakerCompany { id, name }
```

## Frontend Architecture

### Display Flow

1. **Detection**: `hasJunctionConfig()` checks if field has junction
settings
2. **Config Resolution**: `getJunctionConfig()` resolves junction object
metadata and target fields
3. **Record Extraction**: `extractTargetRecordsFromJunction()` extracts
target records from junction records
4. **Rendering**: Target records displayed as chips (not junction
records)

### Edit Flow

1. **Picker Opening**: Initializes the multi-record picker with:
   - Searchable object types (derived from junction target fields)
   - Pre-selected items (extracted from existing junction records)
   
2. **Selection Handling**: Manages create/delete of junction records:
   - **Select**: Creates new junction record with source + target IDs
   - **Deselect**: Finds and deletes the junction record
- **Optimistic Updates**: Manually updates Recoil store before API call

### Key Trade-offs

| Decision | Trade-off |
|----------|-----------|
| Junction records managed manually | More control over optimistic
updates, but requires manual cache management |
| Settings stored per-field | Flexible (same junction can power
different views), but requires UI to configure |
| Polymorphic via morphId groups | Supports N target types, but adds
query complexity |
| Feature flag gated | Safe rollout, but requires flag management |

## Backend Changes

- **Validation**: Junction target field must exist and be a valid
`MANY_TO_ONE` relation
- **Settings**: Extended `FieldMetadataRelationSettings` type with
junction fields
- **Dev Seeder**: Added sample junction objects (PetRocket,
EmploymentHistory, PetCareAgreement) for testing

## How to Test

1. Enable the `IS_JUNCTION_RELATIONS_ENABLED` feature flag
2. Create objects with junction pattern (Pet → PetRocket → Rocket)
3. Configure the junction target in field settings (advanced mode)
4. Verify:
- Display shows target objects (Rockets), not junction records
(PetRockets)
   - Picker allows selecting/deselecting targets
   - Changes persist correctly



https://github.com/user-attachments/assets/d04f057a-228c-4de8-af48-76bb2d72cac1

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-01-20 21:58:13 +01:00