Commit Graph

28 Commits

Author SHA1 Message Date
Marie bc28e1557c Introduce updateWorkspaceMemberSettings and clarify product (#19441)
## Summary

Introduces a dedicated **metadata** mutation to update **standard
(non-custom)** workspace member settings, moves profile-related UI to
use it, and aligns **workspace member** record permissions with the rest
of the CRM so users cannot escalate visibility via RLS by editing their
own member record.

## Product behaviour

### Profile and appearance (standard fields)

- Users can still update **their own** standard workspace member fields
that the product exposes in **Settings / Profile** (e.g. name, locale,
color scheme, avatar flow) via the new
**`updateWorkspaceMemberSettings`** mutation.
- The mutation returns a **boolean**; the app **merges** the updated
fields into local state so the UI stays in sync without refetching the
full workspace member record.
- **Locale** changes also keep **`userWorkspace`** in sync when a locale
is present in the payload (including from the workspace `updateOne` path
when applicable).

### Custom fields on workspace members

- The dedicated metadata mutation **rejects** any **custom** workspace
member field (and unknown keys). Those updates must go through the
normal **object** `updateOne` pipeline, which is subject to **object-
and field-level** permissions like other records. But since we don't
have object- and field-level permission configuration for system objects
yet, this permission is derived from Workspace member settings
permission.
- **Workspace member** is no longer exempt from ORM permission
validation for updates merely because it is a **system** object. Users
who **do not** have workspace member access (e.g. no **Workspace
members** settings permission and no equivalent broad settings access on
the role) **cannot** use `updateOne` on `workspaceMember` to change
**custom** (or other) fields on their own row—even though that row is
used for RLS predicates.
- This closes a path where someone could widen what they can see by
writing to fields that drive row-level rules.

### Who can change another member

- Updating **another** user’s workspace member still requires
**Workspace members** (or equivalent) settings permission, consistent
with admin tooling.
2026-04-14 16:29:00 +00:00
Baptiste Devessier e9aa6f47e5 Allow users to create Fields and Field widget (#18801)
- Allow users to create a Fields widget or a Field widget; **this PR is
focused on Fields widgets as Field widget can't be configured yet**
- Automatically create a filled view when the user creates a draft
Fields widget in edit mode


https://github.com/user-attachments/assets/b2dbba52-c614-44cd-bf6c-095ce9d4ec26

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-23 00:44:49 +01:00
Weiko 1cb4c98cb3 Add dataloader and read from cache for view entities (#18594)
## Context
Improve view resolution using cache and dataloader

## Performance Comparison

|Run|Main (no DataLoaders/cache)|Feature Branch (DataLoaders +
cache)|Speedup|
|---|---|---|---|
|1 (cold)|418ms|95ms|~4.4x faster|
|2|42ms|19ms|~2.2x faster|
|3|37ms|19ms|~1.9x faster|
|4|39ms|12ms|~3.2x faster|
|5|33ms|13ms|~2.5x faster|

The biggest improvement is to use dataloaders for the multiple relations
associated with views. Cache is a bit less significant since there are
other cache mechanism such as PostgreSQL buffer cache but it will
probably be more meaningful with bigger workspaces
2026-03-12 18:05:30 +00:00
Félix Malfait 0e89c96170 feat: add npm and tarball app distribution with upgrade mechanism (#18358)
## Summary

- **npm + tarball app distribution**: Apps can be installed from the npm
registry (public or private) or uploaded as `.tar.gz` tarballs, with
`AppRegistrationSourceType` tracking the origin
- **Upgrade mechanism**: `AppUpgradeService` checks for newer versions,
supports rollback for npm-sourced apps, and a cron job runs every 6
hours to update `latestAvailableVersion` on registrations
- **Security hardening**: Tarball extraction uses path traversal
protection, and `enableScripts: false` in `.yarnrc.yml` disables all
lifecycle scripts during `yarn install` to prevent RCE
- **Frontend**: "Install from npm" and "Upload tarball" modals, upgrade
button on app detail page, blue "Update" badge on installed apps table
when a newer version is available
- **Marketplace catalog sync**: Hourly cron job syncs a hardcoded
catalog index into `ApplicationRegistration` entities
- **Integration tests**: Coverage for install, upgrade, tarball upload,
and catalog sync flows

## Backend changes

| Area | Files |
|------|-------|
| Entity & migration | `ApplicationRegistrationEntity` (sourceType,
sourcePackage, latestAvailableVersion), `ApplicationEntity`
(applicationRegistrationId), migration |
| Services | `AppPackageResolverService`, `ApplicationInstallService`,
`AppUpgradeService`, `MarketplaceCatalogSyncService` |
| Cron jobs | `MarketplaceCatalogSyncCronJob` (hourly),
`AppVersionCheckCronJob` (every 6h) |
| REST endpoint | `AppRegistrationUploadController` — tarball upload
with secure extraction |
| Resolver | `MarketplaceResolver` — simplified `installMarketplaceApp`
(removed redundant `sourcePackage` arg) |
| Security | `.yarnrc.yml` — `enableScripts: false` to block postinstall
RCE |

## Frontend changes

| Area | Files |
|------|-------|
| Modals | `SettingsInstallNpmAppModal`, `SettingsUploadTarballModal`,
`SettingsAppModalLayout` |
| Hooks | `useUploadAppTarball`, `useInstallMarketplaceApp` (cleaned up)
|
| Upgrade UI | `SettingsApplicationVersionContainer`,
`SettingsApplicationDetailAboutTab` |
| Badge | `SettingsApplicationTableRow` — blue "Update" tag,
`SettingsApplicationsInstalledTab` — fetches registrations for version
comparison |
| Styling | Migrated to Linaria (matching main) |

## Test plan

- [ ] Install an app from npm via the "Install from npm" modal
- [ ] Upload a `.tar.gz` tarball via the "Upload tarball" modal
- [ ] Verify upgrade badge appears when `latestAvailableVersion >
version`
- [ ] Verify upgrade flow from app detail page
- [ ] Run integration tests: `app-distribution.integration-spec.ts`,
`marketplace-catalog-sync.integration-spec.ts`
- [ ] Verify `enableScripts: false` blocks postinstall scripts during
yarn install


Made with [Cursor](https://cursor.com)
2026-03-05 10:34:08 +01:00
Baptiste Devessier 00209f7e2c Wire fields widget to backend + basic edition (#17965)
Closes https://github.com/twentyhq/core-team-issues/issues/2215
Closes https://github.com/twentyhq/core-team-issues/issues/2216
Closes https://github.com/twentyhq/core-team-issues/issues/2218

## Fields widget edition demo


https://github.com/user-attachments/assets/08626d70-8fcb-4ae2-9222-10ef57e75f90

## Dashboards still work


https://github.com/user-attachments/assets/aa9a9c45-a0a2-481e-b132-bc52254778da
2026-02-20 13:18:00 +00:00
Arun 8e6b267ff3 Allow DATE_TIME IS operand to filter on a whole day (#17529)
Fixes:  https://github.com/twentyhq/core-team-issues/issues/2027

We've replaced the DATE_TIME picker with DATE picker, and changed the
logic to filter for complete day period.



https://github.com/user-attachments/assets/ba7e1078-bab3-4c62-a803-d6a851f14b7d

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2026-02-18 13:44:05 +00:00
Thomas Trompette b4e924b671 Sync views and navigation items (#18003)
Both objects are necessary to fully enjoy objects within applications

<img width="770" height="311" alt="Capture d’écran 2026-02-17 à 15 19
43"
src="https://github.com/user-attachments/assets/48c51fa4-63f4-45b2-a40a-df73f3aa79be"
/>
2026-02-17 16:32:41 +01:00
Paul Rastoin 2b7b05de2e [OBJECT_MANIFEST_BREAKING_CHANGE] Sync returns workspace migration (#17918)
# Introduction
In this PR we start returning a workspace migration post sync so it can
committed and provided within the tarball

## Universal aggregators utils
Created two utils
### deleteUniversalFlatEntityForeignKeyAggregators
Used when building a universal create action, a newly created actions
should not contain any aggregated foreign key so they won't be codegen
in the workspace migration but also they are overriden at uninversal to
flat transpilation anw

### resetUniversalFlatEntityForeignKeyAggregators
Used before validating a new flat entity creation, some validator will
consume the fk aggregator in order to validate integrity, but of
optimstically provided it can result to errors. To avoid caller
responsability we override them here

## create-field-action refactor
Refactored the universal and flat field create action to be following
the base actions in order to ease typing
Also it was tailored to handle unlimited amount of flat field metadata
in the same actions whereas in the reality we were always only sending
at max 2 ( for relation fields )

Note: relation field has to be provided at the same as if not optimistic
would fail to retrieve circular universal identifiers

## ObjectManifest
Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier`

## Integration test
Created an integration test that creates an app, sync a first manifest
and a second implying update workspace migration action generation
2026-02-16 15:00:24 +01:00
Paul Rastoin 8c951d3623 Migrate Views-xxx Index Field Object Skill to be fully universal ( all actions and metadata runner and builder ) + all metadata update actions runner (#17687)
# What this PR does
Overall naming `universal` versus `flat` is not always the most updated
and so on
Will make a big cleaning tour after I've finished the whole migration
Migrating all `view` and ( filter fields etc ) `field` `object` `index`
to the universal pattern on all `services`, `builder` and `runner`
levels

## Universal and flat optimistic tooling

`addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow`
and its delete counterpart maintain the consistency of
`UniversalFlatEntityMaps` when an entity is created or removed. Beyond
inserting/removing the entity from its own maps, they walk through
`ALL_UNIVERSAL_METADATA_RELATIONS` to update the **aggregator arrays**
on related parent entities — the add appends the new entity's
`universalIdentifier` to the parent's aggregator (e.g. a new viewField's
identifier gets appended to its parent view's
`viewFieldUniversalIdentifiers`), and the delete filters it out. This
keeps the maps in sync so that diff computations and relation lookups
remain accurate throughout the migration building process.

## ALL_UNIVERSAL_METADATA_RELATIONS
`ALL_UNIVERSAL_METADATA_RELATIONS` is the universal counterpart of
`ALL_METADATA_RELATIONS`. It maps each metadata entity to its
many-to-one and one-to-many relations using universal foreign keys
(`*UniversalIdentifier`) instead of database IDs (`*Id`). This allows
migration actions to reference related entities in a workspace-agnostic
way. Relations that are workspace-specific (e.g. `workspace`,
`dataSource`, `userWorkspace`) are set to `null` and skipped during
resolution.

## `workspaceMigrationCreateIdEnrichment`
Reserved to API metadata ( will be able to validate at app installation
lvl )
- Workspace migration `create` actions now carry an optional `id` (and
`fieldIdByUniversalIdentifier` for object/field actions) so that
caller-provided IDs flow through the entire build-validate-run pipeline.
- New `enrichCreateWorkspaceMigrationActionsWithIds` utility resolves
`universalIdentifier → id` mappings after the builder runs and injects
them into the migration actions before the runner persists entities.
- Runner action handlers use the provided IDs instead of generating new
UUIDs, enabling deterministic entity creation for synchronization
workflows.


## `resolveUniversalUpdateRelationIdentifiersToIds`
`resolveUniversalUpdateRelationIdentifiersToIds` converts universal
identifiers (workspace-agnostic, stable keys) in a migration update
payload into concrete database UUIDs, so the update can be applied to a
specific workspace.

It iterates over the many-to-one relations defined in
`ALL_UNIVERSAL_METADATA_RELATIONS` for the given entity type, replaces
each `*UniversalIdentifier` property with its corresponding `*Id` by
looking up the target entity in `allFlatEntityMaps`, and throws if a
non-null identifier can't be resolved.

Used by all `update` action handlers in
`transpileUniversalActionToFlatAction`, avoiding duplicated resolution
logic across handlers.

## What this PR does not
- Migrating twenty-standard declaration to universal
- Migrating all the inputs transpilers to universal
- Migrating all metadata to be fully universal ( we still need to
de-scope the type of all of them and refactor their validator very close
)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-09 19:02:38 +01:00
Paul Rastoin 3dc5b162c7 Spread in parent and requires FlatEntity.__universal (#17753)
# Introduction
Requiring the spreaded `__universal` record that aggregates all the
universal identifier ( relations fk and aggregators ) of an entity to
its root

It's blockin for https://github.com/twentyhq/twenty/pull/17687 to be
finalized because if we don't we would have to migrated all related
entities at once in order for them to always have the universal
properties

## `resolveEntityRelationUniversalIdentifiers`
Introduced `resolveEntityRelationUniversalIdentifiers` a centralized
utility that resolves foreign key IDs to universal identifiers using
ALL_METADATA_RELATIONS metadata. It provides strict typing for both
input (foreign keys) and output (universal identifiers), with
nullability dynamically inferred from entity relation types.
Strictly and dynamically typed for both output and input

To do so added a new type and const/runtime grain to
ALL_METADATA_RELATIONS `isNullable`to many-to-one entries, derived from
the entity relation property types. And fixed incorrectly typed typeorm
entities

### Usage
```ts
  const {
    availabilityObjectMetadataUniversalIdentifier,
    frontComponentUniversalIdentifier,
  } = resolveEntityRelationUniversalIdentifiers({
    metadataName: 'commandMenuItem',
    foreignKeyValues: {
      availabilityObjectMetadataId:
        createCommandMenuItemInput.availabilityObjectMetadataId,
      frontComponentId: createCommandMenuItemInput.frontComponentId,
    },
    flatEntityMaps: { flatObjectMetadataMaps, flatFrontComponentMaps },
  });
```
2026-02-06 17:02:02 +00:00
Paul Rastoin 476bdf764c Refactor flat entity maps to be universal oriented (#17665)
# Introduction
In preparation of the workspace agnostic builder, we're migrating
`FlatEntityMaps` to be universal identifier oriented and based
As in the builder context there're won't be any ids at all
Please also note that the FlatEntity is a UniversalFlatEntity superset

From
```ts
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';

export type FlatEntityMaps<T extends SyncableFlatEntity> = {
  byId: Partial<Record<string, T>>;
  idByUniversalIdentifier: Partial<Record<string, string>>;
  universalIdentifiersByApplicationId: Partial<Record<string, string[]>>;
};
```

To
```ts
export type FlatEntityMaps<
  T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
> = {
  byUniversalIdentifier: Partial<Record<string, T>>;
  universalIdentifierById: Partial<Record<string, string>>;
  universalIdentifiersByApplicationId: Partial<Record<string, string[]>>; // this might make more sense to be migrated to universalIdentifiersByApplicationUniversalIdentifier but it's the main topic of this PR
};
```

## Low level maps tools
Had to refactor find | create | delete | replace | find-many | get-sub
tools ( through mutations and or throw equivalent )
2026-02-03 16:16:02 +00:00
Paul Rastoin bd9688421f ObjectMetadata and FieldMetadata agnostic workspace migration runner (#17572)
# Introduction
Important note: This PR officially deprecates the `standardId`, about to
drop col and entity property after this has been merged

Important note2: Haven't updated the optimistic tool to also update the
universal identifier aggregators only the ids one, they should not be
consumed in the runner context -> need to improve typing or either the
optimistic tooling

In this PR we're introducing all the devxp allowing future metadata
incremental universal migration -> this has an impact on all existing
metadata actions handler ( explaining its size )
This PR also introduce workspace agnostic create update actions runner
for both field and object metadata in order to battle test the described
above devxp

Noting that these two metadata are the most complex to handle

Notes:
- A workspace migration is now highly bind to a
`applicationUniversalIdentifier`. Though we don't strictly validate
application scope for the moment

## Next
Migrate both object and field builder to universal comparison

## Universal Actions vs Flat Actions Architecture

### Concept

The migration system uses a two-phase action model:

1. **Universal Actions** - Actions defined using `universalIdentifier`
(stable, portable identifiers like `standardId` + `applicationId`)
2. **Flat Actions** - Actions defined using database `entityId` (UUIDs
specific to a workspace)

### Why This Separation?

- **Universal actions are portable**: They can be serialized, stored,
and replayed across different workspaces
- **Flat actions are executable**: They contain the actual database IDs
needed to perform operations
- **Decoupling**: The builder produces universal actions; the runner
transpiles them to flat actions at execution time

### Transpiler Pattern

Each action handler must implement
`transpileUniversalActionToFlatAction()`:

```typescript
@Injectable()
export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
  'create',
  'fieldMetadata',
) {
  override async transpileUniversalActionToFlatAction(
    context: WorkspaceMigrationActionRunnerArgs<UniversalCreateFieldAction>,
  ): Promise<FlatCreateFieldAction> {
    // Resolve universal identifiers to database IDs
    const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({
      flatEntityMaps: allFlatEntityMaps.flatObjectMetadataMaps,
      universalIdentifier: action.objectMetadataUniversalIdentifier,
    });
    
    return {
      type: action.type,
      metadataName: action.metadataName,
      objectMetadataId: flatObjectMetadata.id, // Resolved ID
      flatFieldMetadatas: /* ... transpiled entities ... */,
    };
  }
}
```

### Action Handler Base Class

`BaseWorkspaceMigrationRunnerActionHandlerService<TActionType,
TMetadataName>` provides:

- **`transpileUniversalActionToFlatAction()`** - Abstract method each
handler must implement
- **`transpileUniversalDeleteActionToFlatDeleteAction()`** - Shared
helper for delete actions

## FlatEntityMaps custom properties
Introduced a `TWithCustomMapsProperties` generic parameter to control
whether custom indexing structures are included:

- **`false` (default)**: Returns `FlatEntityMaps<MetadataFlatEntity<T>>`
- used in builder/runner contexts
- **`true`**: Returns the full maps type with custom properties (e.g.,
`byUserWorkspaceIdAndFolderId`) - used in cache contexts

## Create Field Actions Refactor

Refactored create-field actions to support relation field pairs
bundling.

**Problem:** Relation fields (e.g., `Attachment.targetTask` ↔
`Task.attachments`) couldn't resolve each other's IDs during
transpilation because they were in separate actions with independent
`fieldIdByUniversalIdentifier` maps.

**Solution:** 
- Removed `objectMetadataUniversalIdentifier` from
`UniversalCreateFieldAction` and `objectMetadataId` from
`FlatCreateFieldAction` - each field now carries its own
- Runner groups fields by object internally and processes each table
separately
- Split aggregator into two focused utilities:
- `aggregateNonRelationFieldsIntoObjectActions` - merges non-relation
fields into object actions
- `aggregateRelationFieldPairs` - bundles relation pairs with shared
`fieldIdByUniversalIdentifier`
2026-02-02 12:22:38 +00:00
Marie 0001c2e7d0 [Apps] Apps marketplace (first draft) (#17562)
https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4

How it works
- `manifest.json` files are now committed when apps are published to our
repo
- to display available apps, from the server, we read into our github
repo, using a pod-scoped cache
- feature flagged
- app installation will be behind permission gate MARKETPLACE_APPS

Limitations and what is yet to develop
- content and settings tabs
- installed apps tab
- app installation
- test and potentially fix reading from .manifest.json once [Reshape
manifest
structure](https://github.com/twentyhq/core-team-issues/issues/2183) is
done. additional work is expected on assets notably. (couldnt properly
do it here as manifest.json will only be committed after this pr)
- we only read in community/ folder for now - we may want tochange that
- the cache is rather artisanal for now and scoped by pod - we may want
to change that
2026-01-30 17:32:32 +00:00
Paul Rastoin 942d2fef83 Remove sync-metadata and IS_WORKSPACE_CREATION_V2_ENABLED feature flag (#16997)
# Introduction
Followup of
https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738
close https://github.com/twentyhq/core-team-issues/issues/1910

We've completely decom the `sync-metadata` in production. We're now then
removing its implementation in favor of the v2.

## TODO:
- [x] Remove sync-metadata implem and commands
- [x] Remove workspace decorators 
- [x] Type each deprecated field to deprecated on their workspaceEntity
- [x] Remove the `workspace-sync-metadata` folder entirely
- [x] remove workspace migration
- [x] workspace migration removal migration
- [x] remove the `v2` references from workspace manager file names
- [x] remove the `v2` references from workspace manager modules
- [ ] Double check impact on translation file path updates


## Note
- Removed the gate logic
- Remains some service v2 naming, serverless needs to be migrated on v2
fully
- Removed workspaceMigration service app health consumption, making it
always returning up ( no more down ) cc @FelixMalfait ( quite obsolete
health check now, will require complete refactor once we introduce inter
app dependency etc )
2026-01-08 15:45:12 +00:00
Lucas Bordeau 0b5be7caa3 Refactored Date to Temporal in critical date zones (#16544)
Fixes https://github.com/twentyhq/twenty/issues/16110

This PR implements Temporal to replace the legacy Date object, in all
features that are time zone sensitive. (around 80% of the app)

Here we define a few utils to handle Temporal primitives and obtain an
easier DX for timezone manipulation, front end and back end.

This PR deactivates the usage of timezone from the graph configuration,
because for now it's always UTC and is not really relevant, let's handle
that later.

Workflows code and backend only code that don't take user input are
using UTC time zone, the affected utils have not been refactored yet
because this PR is big enough.

# New way of filtering on date intervals

As we'll progressively rollup Temporal everywhere in the codebase and
remove `Date` JS object everywhere possible, we'll use the way to filter
that is recommended by Temporal.

This way of filtering on date intervals involves half-open intervals,
and is the preferred way to avoid edge-cases with DST and smallest time
increment edge-case.

## Filtering endOfX with DST edge-cases

Some day-light save time shifts involve having no existing hour, or even
day on certain days, for example Samoa Islands have no 30th of December
2011 : https://www.timeanddate.com/news/time/samoa-dateline.html, it
jumps from 29th to 31st, so filtering on `< next period start` makes it
easier to let the date library handle the strict inferior comparison,
than filtering on `≤ end of period` and trying to compute manually the
end of the period.

For example for Samoa Islands, is end of day `2011-12-29T23:59:59.999`
or is it `2011-12-30T23:59:59.999` ? If you say I don't need to know and
compute it, because I want everything strictly before
`2011-12-29T00:00:00 + start of next day (according to the library which
knows those edge-cases)`, then you have a 100% deterministic way of
computing date intervals in any timezone, for any day of any year.

Of course the Samoa example is an extreme one, but more common ones
involve DST shifts of 1 hour, which are still problematic on certain
days of the year.

## Computing the exact _end of period_

Having an open interval filtering, with `[included - included]` instead
of half-open `[included - excluded)`, forces to compute the open end of
an interval, which often involves taking an arbitrary unit like minute,
second, microsecond or nanosecond, which will lead to edge-case of
unhandled values.

For example, let's say my code computes endOfDay by setting the time to
`23:59:59.999`, if another library, API, or anything else, ends up
giving me a date-time with another time precision `23:59:59.999999999`
(down to the nanosecond), then this date-time will be filtered out,
while it should not.

The good deterministic way to avoid 100% of those complex bugs is to
create a half-open filter :

`≥ start of period` to `< start of next period` 

For example : 

`≥ 2025-01-01T00:00:00` to `< 2025-01-02T00:00:00` instead of `≥
2025-01-01T00:00:00` to `≤ 2025-01-01T23:59:59.999`

Because, `2025-01-01T00:00:00` = `2025-01-01T00:00:00.000` =
`2025-01-01T00:00:00.000000` = `2025-01-01T00:00:00.000000000` => no
risk of error in computing start of period

But `2025-01-01T23:59:59` ≠ `2025-01-01T23:59:59.999` ≠
`2025-01-01T23:59:59.999999` ≠ `2025-01-01T23:59:59.999999999` =>
existing risk of error in computing end of period

This is why an half-open interval has no risk of error in computing a
date-time interval filter.

Here is a link to this debate :
https://github.com/tc39/proposal-temporal/issues/2568

> For this reason, we recommend not calculating the exact nanosecond at
the end of the day if it's not absolutely necessary. For example, if
it's needed for <= comparisons, we recommend just changing the
comparison code. So instead of <= zdtEndOfDay your code could be <
zdtStartOfNextDay which is easier to calculate and not subject to the
issue of not knowing which unit is the right one.
>
> [Justin Grant](https://github.com/justingrant), top contributor of
Temporal

## Application to our codebase

Applying this half-open filtering paradigm to our codebase means we
would have to rename `IS_AFTER` to `IS_AFTER_OR_EQUAL` and to keep
`IS_BEFORE` (or even `IS_STRICTLY_BEFORE`) to make this half-open
interval self-explanatory everywhere in the codebase, this will avoid
any confusion.

See the relevant issue :
https://github.com/twentyhq/core-team-issues/issues/2010

In the mean time, we'll keep this operand and add this semantic in the
naming everywhere possible.

## Example with a different user timezone 

Example on a graph grouped by week in timezone Pacific/Samoa, on a
computer running on Europe/Paris :

<img width="342" height="511" alt="image"
src="https://github.com/user-attachments/assets/9e7d5121-ecc4-4233-835b-f59293fbd8c8"
/>

Then the associated data in the table view, with our **half-open
date-time filter** :

<img width="804" height="262" alt="image"
src="https://github.com/user-attachments/assets/28efe1d7-d2fc-4aec-b521-bada7f980447"
/>

And the associated SQL query result to see how DATE_TRUNC in Postgres
applies its internal start of week logic :

<img width="709" height="220" alt="image"
src="https://github.com/user-attachments/assets/4d0542e1-eaae-4b4b-afa9-5005f48ffdca"
/>

The associated SQL query without parameters to test in your SQL client :

```SQL
SELECT "opportunity"."closeDate" as "close_date", TO_CHAR(DATE_TRUNC('week', "opportunity"."closeDate", 'Pacific/Samoa') AT TIME ZONE 'Pacific/Samoa', 'YYYY-MM-DD') AS "DATE_TRUNC by week start in timezone Pacific/Samoa", "opportunity"."name" FROM "workspace_1wgvd1injqtife6y4rvfbu3h5"."opportunity" "opportunity" ORDER BY "opportunity"."closeDate" ASC NULLS LAST
```

# Date picker simplification (not in this PR)

Our DatePicker component, which is wrapping `react-datepicker` library
component, is now exposing plain dates as string instead of Date object.
The Date object is still used internally to manage the library
component, but since the date picker calendar is only manipulating plain
dates, there is no need to add timezone management to it, and no need to
expose a handleChange with Date object.

The timezone management relies on date time inputs now.

The modification has been made in a previous PR :
https://github.com/twentyhq/twenty/issues/15377 but it's good to
reference it here.

# Calendar feature refactor

Calendar feature has been refactored to rely on Temporal.PlainDate as
much as possible, while leaving some date-fns utils to avoid re-coding
them.

Since the trick is to use utils to convert back and from Date object in
exec env reliably, we can do it everywhere we need to interface legacy
Date object utils and Temporal related code.

## TimeZone is now shown on Calendar :

<img width="894" height="958" alt="image"
src="https://github.com/user-attachments/assets/231f8107-fad6-4786-b532-456692c20f1d"
/>

## Month picker has been refactored 

<img width="503" height="266" alt="image"
src="https://github.com/user-attachments/assets/cb90bc34-6c4d-436d-93bc-4b6fb00de7f5"
/>

Since the days weren't useful, the picker has been refactored to remove
the days.

# Miscellaneous 
- Fixed a bug with drag and drop edge-case with 2 items in a list.

# Improvements 

## Lots of chained operations
It would be nice to create small utils to avoid repeated chained
operations, but that is how Temporal is designed, a very small set of
primitive operations that allow to compose everything needed. Maybe
we'll have wrappers on top of Temporal in the coming years.

## Creation of Temporal objects is throwing errors

If the input is badly formatted Temporal will throw, we might want to
adopt a global strategy to avoid that.

Example : 

```ts
const newPlainDate = Temporal.PlainDate.from('bad-string'); // Will throw
```
2025-12-23 17:40:26 +00:00
Félix Malfait 3cea19baf4 feat(ai): add view management tools for AI chat (#16495)
## Summary

Adds a new **VIEW** tool category for the AI chat, enabling it to work
with views:

- **get-views**: List views in the workspace, optionally filtered by
object metadata ID
- **get-view-query-parameters**: Convert a view's filters and sorts into
GraphQL query parameters that can be passed to existing `find_*` data
tools
- **create-view**, **update-view**, **delete-view**: CRUD operations for
view management

### Key design decisions

1. **No pagination duplication**: Instead of creating a
`find-records-from-view` tool that would duplicate pagination logic,
`get-view-query-parameters` returns filter/sort parameters that the AI
can pass to existing record-fetching tools.

2. **Permission model**:
- Read tools (get-views, get-view-query-parameters) are available to all
users
   - Write tools require the `VIEW` permission
   - UNLISTED views can only be modified by their creator

3. **Leverages existing utilities**: Uses
`computeRecordGqlOperationFilter` from `twenty-shared` for filter
conversion.

### Files changed

- Added `ViewToolProvider`, `ViewToolsFactory`, and
`ViewQueryParamsService`
- Added `VIEW` to `ToolCategory` enum and tool registry
- Updated `chat-execution.service.ts` to include view tools in the
catalog and pass viewId in browsing context
- Extracted shared `formatValidationErrors` utility to reduce
duplication

## Test plan

- [x] Unit tests for `ViewToolsFactory` 
- [x] Unit tests for `ViewQueryParamsService`
- [x] Lint and typecheck pass
2025-12-11 15:05:42 +01:00
Charles Bochet a18203934c Fix flat entity maps date serialization (#16420)
Changes:
- as we store date in redis as serialized, let's make all flatEntity
dates as string. This requires changing FlatEntity types and making sure
that entity are converted to flatEntity and flatEntity to dtos
2025-12-09 16:41:50 +01:00
Paul Rastoin c8f541618d Refactor validate build and run for configuration to be less verbose and more reliable (#16343)
# Introduction
Refactored the api `validateBuildAndRunWorkspaceMigration` to be require
less configuration but to infer required args dynamically depending on
provided metadata maps to compare

## `inferDeletionFromMissingEntities`
Is not dynamically computed avoiding any miss configuration issue and
any missleading devxp

## Maps computation

Making only one call to redis to build both dependency and to be
compared entity maps. It does not matter to avoid passing a about to
compared flat entity maps to could also be a depedency, it's handled
directly in the builder setup optimistic cache logic

Please note that the flat maps used for the service input transpilation
might differ from the one that we will dynamically compute and inject in
the builder. Leading to do 2 redis calls but also race condition prone
validation error
We prefer that this occurs at the builder rather than at the runner
level as the pg instance is not cache and reflect the real state of a
given workspace
In a nutshell, there's a possible race condition between cache
invalidation and computation in both service input transpilers and
builder but we're totally ok with that
2025-12-05 15:41:56 +00:00
Marie 7ce22d5c7e breaking (soft) - Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (2/3) (#16277)
Should be merged once https://github.com/twentyhq/twenty/pull/16206 has
been released + command run to prod

In this PR
- Remove usage of viewGroup.fieldMetadataId, both in BE and FE states. 
- But we still need to properly populate it until we fully remove
viewGroup.fieldMetadataId from db and ORM entity (upcoming 3rd PR out of
3). fieldMetadataId was removed from CoreViewGroup type and
CreateViewGroupInput and is determined BE-side based on the associated
view's mainGroupByFieldMetadataId. **I expect this means a downtime on
viewGroup creation, until both FE and BE are deployed and cache is
flushed.** This seems acceptable to me as it only regards viewGroup
creation.
    - this information is replaced by view.mainGroupByFieldMetadataID
- Handle view group creation, update and deletion in the BE as a
side-effect of a view creation, update or deletion. Optimistic effects
are still used
- Add validation at view creation or update regarding
mainGroupByFieldMetadata

Left to do in 3rd PR
- Remove viewGroup.fieldMetadataId from db and ORM entity
- Restore feature allowing to update an existing grouped view's group by
field (already OK on BE side but need to rebuild FE optimistic)
2025-12-05 14:00:03 +00:00
Paul Rastoin a829ae62e2 Remove IS_WORKSPACE_MIGRATION_V2_ENABLED feature flag (#16280)
# Introduction
closes https://github.com/twentyhq/core-team-issues/issues/1911
2025-12-03 15:47:08 +00:00
Paul Rastoin f9ab09c404 Metadata api create entity in workspace custom app (#15911)
# Introduction
Cleaner and fewer scope version of
https://github.com/twentyhq/twenty/pull/15745 ( removed sync-metadata
hack through, too ambitious migration and upgrade )

Please note that this PR won't have any interaction with the existing
sync-metadata
Which mean that the sync metadata does not update the standard entities
applicationId and universalIdentifier, and it won't we will deprecate it
on favor of a workspace migration aka twenty-standard app installation

## API Metadata
Any operation going through the api metadata nows automatically scope
the related entity to the workspace custom application instance. (
optionally passing an applicationId to allow current hacky implem of app
sync service )

We need to either ignore the tests or remove the cli status check from
the blocking status badges for a PR to be merged

## New workspace
Already handled in previous
https://github.com/twentyhq/twenty/pull/15625, when a workspace is
created it gets created a twenty standard and custom workspace instance

All his views and permissions will be prefilled to the its twenty
standard app instance with a specific universalIdentifier

## New universalIdentifier
At the contrary as before with standardIds, universalIdentifier are
unique for a given workspace
This means that createdAt field of both object company and opportunity
will have a unique universalIdentifier whereas they share the same
standardId

## FlatApplication
Introduced the flatApplication and cache. Will migrate existing
`MetadataName` to be `SyncableMetadataName` in a following PR

## What's next
Next we will describe a twenty standard app configuration as json that
will be used to generate a workspace migration that will be run instead
of the sync metadata, in a nutshell we aim to deprecated the sync
metadata
So we can standardize any entity to have a non nullable applicationId
and universalIdentifier

## Upgrade command
Introduced an upgrade command that will create a custom workspace
instance for any workspace that do not have one in order to align with
the new behavior when creating a new workspace
2025-11-23 22:35:17 +01:00
Félix Malfait 9a80164cf3 Add comprehensive permission guard coverage across GraphQL and REST endpoints (#15739)
This PR enhances our security model by ensuring all GraphQL resolvers
and REST API endpoints have appropriate permission guards.

## Changes

### ESLint Rules
- Enhanced `graphql-resolvers-should-be-guarded` to require permission
guards on all resolvers (Query, Mutation, Subscription), not just
mutations
- Enhanced `rest-api-methods-should-be-guarded` to require permission
guards on all REST endpoints (GET, POST, PUT, PATCH, DELETE), not just
mutating methods
- Both rules now enforce consistent security: authentication guards +
permission guards for all endpoints

### Permission Guards Added

**Public Endpoints** - Added `NoPermissionGuard`:
- Auth-related queries (checkUserExists, findWorkspaceFromInviteHash,
validatePasswordResetToken)
- Billing webhooks (Stripe callbacks)
- SSO callbacks (SAML authentication)
- Workflow webhooks
- Cloudflare webhooks
- Route trigger endpoints
- GraphQL subscriptions
- Current workspace queries
- Geo-map address autocomplete
- View-related read operations (view-field, view-filter, view-group,
view-sort, view-filter-group)

**Settings Permission Guards** - Added `SettingsPermissionGuard`:
- API Keys management: `PermissionFlagType.API_KEYS_AND_WEBHOOKS`
- Webhooks management: `PermissionFlagType.API_KEYS_AND_WEBHOOKS`
- Page Layouts (write operations): `PermissionFlagType.LAYOUTS`
- REST Metadata API: `PermissionFlagType.DATA_MODEL`
- Agent operations: `PermissionFlagType.AI`
- Remote servers: `PermissionFlagType.DATA_MODEL`
- Remote tables: `PermissionFlagType.DATA_MODEL`
- Serverless functions: `PermissionFlagType.WORKFLOWS`

**Custom Permission Guards** - Added `CustomPermissionGuard`:
- REST Core API (permissions checked at query execution layer)
- Timeline calendar events (permission checks in service layer)
- Timeline messaging (permission checks in service layer)
- Search operations (permission checks in service layer)
- View operations (permission checks via dedicated view permission
guards)

### View Permission Guards
- Created dedicated `FindManyViewsPermissionGuard` and
`FindOneViewPermissionGuard` for reading views
- Created `CreateViewPermissionGuard` for view creation with
visibility-based permission checks
- All view child entities (view-field, view-filter, view-sort,
view-group, view-filter-group) use `NoPermissionGuard` for reads
- Write operations on view child entities use dedicated permission
guards that check parent view access

### Page Layout Permissions
- Read operations (GET/Query) now use `NoPermissionGuard` - users can
view layouts without LAYOUTS permission
- Write operations (POST/PATCH/DELETE/Mutation) require
`SettingsPermissionGuard(PermissionFlagType.LAYOUTS)`
- Applied consistently across page-layout, page-layout-tab, and
page-layout-widget endpoints

## Security Model
All endpoints now follow a consistent pattern:
1. **Authentication**: `UserAuthGuard`, `WorkspaceAuthGuard`, or
`PublicEndpointGuard`
2. **Authorization**: One of:
- `SettingsPermissionGuard(PermissionFlagType.XXX)` - for settings/admin
operations
- `CustomPermissionGuard` - when permissions are checked in service/data
layer
   - `NoPermissionGuard` - for public or non-sensitive read operations

The ESLint rules automatically enforce this pattern going forward.

## Stats
- 47 files changed
- 603 insertions, 163 deletions
- 3 new guard files created
2025-11-10 12:17:38 +01:00
Félix Malfait cff17db6cb Enhance role-check system with stricter checks (#15392)
## Overview

This PR strengthens our permission system by introducing more granular
role-based access control across the platform.

## Changes

### New Permissions Added
- **Applications** - Control who can install and manage applications
- **Layouts** - Control who can customize page layouts and UI structure
- **AI** - Control access to AI features and agents
- **Upload File** - Separate permission for file uploads
- **Download File** - Separate permission for file downloads (frontend
visibility)

### Security Enhancements
- Implemented whitelist-based validation for workspace field updates
- Added explicit permission guards to core entity resolvers
- Enhanced ESLint rule to enforce permission checks on all mutations
- Created `CustomPermissionGuard` and `NoPermissionGuard` for better
code documentation

### Affected Components
- Core entity resolvers: webhooks, files, domains, applications,
layouts, postgres credentials
- Workspace update mutations now use whitelist validation
- Settings UI updated with new permission controls

### Developer Experience
- ESLint now catches missing permission guards during development
- Explicit guard markers make permission requirements clear in code
review
- Comprehensive test coverage for new permission logic

## Testing
-  All TypeScript type checks pass
-  ESLint validation passes
-  New permission guards properly enforced
-  Frontend UI displays new permissions correctly

## Migration Notes
Existing workspaces will need to assign the new permissions to roles as
needed. By default, all new permissions are set to `false` for non-admin
roles.
2025-11-07 15:37:17 +01:00
Paul Rastoin 45473218d3 Field deactivation side effect views calendar kanban viewFields (#15180)
# Introduction
Handling both:
- field deactivation side effect on view fields, view filters and views
- field deactivation side effect on view that targets it as
`kanbanAggregateFieldMetadataId`
- field deactivation side effect on view that targets it as
`calendarFieldMetadataId`

## Coverage
added coverage
```ts
 PASS  test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-deletes-views.integration-spec.ts (13.132 s)
  kanban-aggregate-field-deactivation-nullifies-kanban-properties
    ✓ should nullify kanban properties when field used as kanbanAggregateOperationFieldMetadataId is deactivated (3923 ms)
    ✓ should not modify views when field not used as kanbanAggregateOperationFieldMetadataId is deactivated (2958 ms)
    ✓ should nullify kanban properties on multiple views when they all use the same field as kanbanAggregateOperationFieldMetadataId (2542 ms)
    ✓ should nullify kanban properties when views have different aggregate operations on same field (3380 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        13.154 s
```

```ts
 PASS  test/integration/metadata/suites/field-metadata/view-group-field-deactivation-deletes-views.integration-spec.ts (12.639 s)
  view-group-field-deactivation-deletes-views
    ✓ should delete view when field used in view group is deactivated (3469 ms)
    ✓ should not delete view when field not used in view group is deactivated (3109 ms)
    ✓ should delete multiple views when they all use the same field in view groups (2741 ms)
    ✓ should handle deactivation when view has multiple view groups with different fields (3008 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        12.664 s
```

```ts
 PASS  test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts (14.579 s)
  calendar-field-deactivation-deletes-views
    ✓ should delete view when field used as calendarFieldMetadataId is deactivated (3388 ms)
    ✓ should not delete view when field not used as calendarFieldMetadataId is deactivated (2438 ms)
    ✓ should delete multiple views when they all use the same field as calendarFieldMetadataId (2635 ms)
    ✓ should handle deactivation when views have different calendar layouts on same field (3195 ms)
    ✓ should delete calendar view but not other view types when calendar field is deactivated (2682 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        14.601 s, estimated 15 s
```

## View soft deletion
We decided to remove the soft deletion grain on all the views, in this
PR context we've only removed soft deleted validation requirement on any
view entities

## Conclusion

close https://github.com/twentyhq/core-team-issues/issues/1754
2025-10-21 16:12:03 +02:00
Paul Rastoin 3462a2e288 ViewGroup and ViewFilters side effect in v2 (#15096)
# Introduction

### Summary
Implements side effect handling for `ViewGroup` and `ViewFilters` when
field metadata is updated in the v2 architecture. This ensures that
view-related records are properly maintained when enum field options are
modified, deleted, or created.

### Side effects
- **Side Effect System**: Added side effect handling for field metadata
updates that manages related view groups and view filters
- **Enum Field Updates**: When enum field options are modified, the
system now:
- **View Groups**: Creates new groups for added options, updates
existing groups for modified options, and deletes groups for removed
options
- **View Filters**: Updates filter values to reflect option changes and
removes filters that reference deleted options

### Enum runner fix
Update now works for both atomic enum and array enum ( multi select for
instance )

### Compute flat entity maps from to
Standardized this method usage across v2 services
Next step is to require dependencies dynamically

## Conclusion
closes https://github.com/twentyhq/core-team-issues/issues/1649
2025-10-17 17:09:26 +02:00
Weiko c0ed246a03 Add is unique in migration v2 + refactor inferDeletionFromMissingEntities (#15070)
## Goal
- inferDeletionFromMissingEntities is now a map instead of a single
bool, allowing us to parameterise it based on the entity we want to
compare
- Adding index creation/update when field isUnique is set to true and
index deletion when isUnique is false (for now)

close https://github.com/twentyhq/core-team-issues/issues/1346
2025-10-15 16:08:13 +02:00
Paul Rastoin 6188c72f74 Simplify and enhance v2 type devxp (#15032)
# Introduction
This PR introduces a huge type refactor that will leverage dynamic intra
entity optimistic flat maps update in the future and also a more
granular cache invalidation enhancing performances

close https://github.com/twentyhq/core-team-issues/issues/1717
close https://github.com/twentyhq/core-team-issues/issues/1716
close https://github.com/twentyhq/core-team-issues/issues/1643

## What's done

### Comparators centralization
Comparator is now done through global configuration as const for each
metadata names
Thanks to 

Note:
Definition of standard is evolving, standard is now scoped to an app.
Meaning that a manifest should be able to update its own standards
objects but on other app standards ones ? Each synchronizable entities
will have a standardOverrides ?

## Typing refactor

### `AllFlatEntityTypesByMetadataName`

**Single source of truth for the complete type ecosystem**, mapping each
metadata name to its entity types, flat entities, and migration actions:

```typescript
export type AllFlatEntityTypesByMetadataName = {
  fieldMetadata: {
    actions: { created: CreateFieldAction; updated: UpdateFieldAction; deleted: DeleteFieldAction; };
    flatEntity: FlatFieldMetadata;
    entity: FieldMetadataEntity;
  };
  objectMetadata: { /* ... */ };
  // ... all 10 metadata types
};
```

### `ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS`

**Explicitly declares database relationships** between entities with
compile-time validation:

```typescript
export const ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS = {
  viewField: {
    view: 'viewId',
    fieldMetadata: 'fieldMetadataId',
  },
  cronTrigger: {
    serverlessFunction: 'serverlessFunctionId',
  },
  // ... all relations
} as const satisfies MetadataNameAndRelations;
```

### `ALL_FLAT_ENTITY_CONFIGURATION`

**Centralizes comparison and serialization logic** for each metadata
type:

```typescript
export const ALL_FLAT_ENTITY_CONFIGURATION = {
  fieldMetadata: {
    propertiesToCompare: ['name', 'type', 'label', 'defaultValue', /* ... */],
    propertiesToStringify: ['options', 'settings', 'defaultValue'],
  },
  objectMetadata: {
    propertiesToCompare: ['nameSingular', 'namePlural', 'isActive', /* ... */],
    propertiesToStringify: [],
  },
  // ... all metadata types
} as const satisfies AllFlatEntityConfiguration;
```

## Combined Impact

These three configurations work together to create a **strongly-typed,
centrally-managed metadata system**:

1. **`AllFlatEntityTypesByMetadataName`** defines *what exists*
2. **`ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS`** defines *how they
relate*
3. **`ALL_FLAT_ENTITY_CONFIGURATION`** defines *how to compare and
serialize them*

**Result:** Builders and validators become thin wrappers around
type-safe, configuration-driven logic instead of containing scattered,
error-prone manual implementations.

## What's next

### StandardOverrides standardization
Every metadata entity can be a standard one for a workspace if it's an
installed app, which means it might not expose the whole entity api to
be editable through an import dynamically
The standard overrides logic should not be applied to Fields and Objects
but to every entities

At the moment we have a logic of `EDITABLE_PROPERTIES` through the api,
and also `STANDARD_OVERRIDEDABLE_PROPERTIES`
This should be configuration centered like `propertiesToCompare` and
`propertiesToStringify`.
Scoping this PR to two last for the moment. As update dispatch to
standardOverrides could be considered as a side effect prefer waiting to
start the side effect refactor

### Granular Optimistic deprecation
With this new grain at runtime we will be able to add a flat entity and
dispatch its addition to related flat maps, so we don't have to describe
an optimistic method for each flat entity operations

See `addFlatEntityToFlatEntityAndRelatedEntityMapsOrThrow`

Note: Still in wip and included in this PR but about to create a new one
to integrate these utils and remove existing methods

### ValidateBuildAndRun dynamic args typed defintion
We should restrain the devxp to send expected flat maps entity as at
least from to or dependency as we now have the grain both a type lvl and
runtime to do so
It should not be possible in the devxp to forgot adding the views to the
v2 builder when passing the view field anymore ( that would lead to
permanent validation error in view field integrity checks )

## Conclusion
Thanks for reading and reviewing !
Any suggestions are more than welcomed ! ( same as for questions too ! )
2025-10-13 10:31:34 +02:00
Paul Rastoin 59fbe35a8c Move view in metadata-modules/ and create atomic folder + module for each view entity (#14990)
# Introduction
Preparing view-filter and view-group introduction in v2 core engine
Moving view from `core-modules` to `metadata-modules`

## What happened

### Created dedicated modules for each view entity:
- ViewFieldModule
- ViewFilterModule
- ViewFilterGroupModule
- ViewGroupModule
- ViewSortModule

### Each module is now completely independent with its own:
- Controller
- Resolver
- Service
- Entity

### Created dedicated abstraction metadata module folder for:
- flat-view-field
- flat-view

### Dependencies 
- Eleminated circular dep on ViewModule to all others ones
- Granular import not importing the whole viewModule anymore everywhere


close https://github.com/twentyhq/core-team-issues/issues/1703
2025-10-09 15:18:15 +02:00