e6a415b438fc8e23cfc382efa057d4399f1c0ce9
124 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
88424611ec |
Refactor and standardize isSystem field and object (#17992)
# Introduction ## Centralize system field definitions - Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`), eliminating duplication across custom object and standard app field builders - Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper ## Mark system fields as `isSystem: true` - Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector` are now properly flagged as system fields across all standard objects and custom object creation - Standard app field builders for all ~30 standard objects updated to set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy` - System-only standard objects (blocklist, calendar channels, message threads, etc.) now also include `createdBy`, `updatedBy`, `position`, `searchVector` field definitions that were previously missing ## Validate system fields on object creation - New transversal validation (`crossEntityTransversalValidation`) runs after all atomic entity validations in the build orchestrator, ensuring all 8 system fields are present with correct `type` and `isSystem: true` when an object is created - New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to resolve field names to universal identifiers for a given object - New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD` on `ObjectMetadataExceptionCode` ## Protect system fields and objects from mutation - Field validators now block update/delete of `isSystem` fields by non-system callers (`FIELD_MUTATION_NOT_ALLOWED`) - Object validators now block update/delete of `isSystem` objects by non-system callers - `POSITION` and `TS_VECTOR` field type validators replaced: instead of rejecting creation outright, they now validate that the field is named correctly (`position` / `searchVector`) and has `isSystem: true` ## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp` - New `isCallerTwentyStandardApp` utility checks whether the caller's `applicationUniversalIdentifier` matches the twenty standard app - Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`, `areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use `isCallerTwentyStandardApp` for custom suffix decisions, keeping `isSystemBuild` for mutation permission checks - `WorkspaceMigrationBuilderOptions` type updated to include `applicationUniversalIdentifier` ## Adapt frontend filtering - New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`, `searchVector`) and `isHiddenSystemField` utility to only hide truly internal fields while keeping user-facing system fields (`createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI - ~20 frontend files updated to replace `!field.isSystem` checks with `!isHiddenSystemField(field)` across record index, settings, data model, charts, workflows, spreadsheet import, aggregations, and role permissions ## Add 1.19 upgrade commands - **`backfill-system-fields-is-system`**: Raw SQL command to set `isSystem = true` on existing workspace fields matching system field names, and fix `position` field type from `NUMBER` to `POSITION` for `favorite`/`favoriteFolder` objects. Includes proper cache invalidation. - **`add-missing-system-fields-to-standard-objects`**: Codegen'd workspace migration to create missing `position`, `searchVector`, `createdBy`, `updatedBy` fields on standard objects that didn't previously have them. Runs via `WorkspaceMigrationRunnerService` in a single transaction with idempotency check. **Known limitation**: assumes all standard objects exist and are valid in the target workspace. ## Add `universalIdentifier` for system fields in standard object constants - `standard-object.constant.ts` updated to include `universalIdentifier` for `createdBy`, `updatedBy`, `position`, and `searchVector` across all standard objects - `fieldManifestType.ts` updated to support the new field manifest shape ## System relation Completely removed and backfilled all `isSystem` relation to be false false As we won't require an object to have any relation system fields ## Add integration tests - New test suite `failing-sync-application-object-system-fields` covering: missing system fields, wrong field types (`id` as TEXT, `createdAt` as TEXT, `position` as TEXT), system field deletion attempts, and system field update attempts - New test utilities: `buildDefaultObjectManifest` (builds an object manifest with all 8 system fields) and `setupApplicationForSync` (centralizes application setup) - Existing successful sync test updated to verify system fields are created with correct properties ## Next step Make the builder scope the compared entity to be the currently built app + nor twenty standard app |
||
|
|
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> |
||
|
|
21b2b65dbe |
[Fix] fix relations in apps (#17791)
When syncing an app with relation multiple times, it would fail because the relation fields could not be identified due to universalIdentifier being overwritten at field cretion |
||
|
|
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 }, }); ``` |
||
|
|
48c8fa6809 |
Refactor workspace migration update action (#17701)
# Introduction
Removing:
- `from` property from actions definition, as it's a legitimate source
of truth. The stored comparison might have been compromised since action
generation. If from is needed it should be computed from the optimistic
cache at runner lvl
- Removed the `FlatEntityPropertyUpdates` Array complexity in favor of
From
```ts
export type PropertyUpdate<T, P extends keyof T> = {
property: P;
} & FromTo<T[P]>;
```
To
```ts
export type FlatEntityUpdate<T extends AllMetadataName> = Partial<
Pick<
MetadataFlatEntity<T>,
Extract<FlatEntityPropertiesToCompare<T>, keyof MetadataFlatEntity<T>>
>
>;
```
## New interactions
From
```ts
const positionUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'position',
});
if (
isDefined(positionUpdate) &&
(!Number.isInteger(positionUpdate.to) || positionUpdate.to < 0)
) {
const toFlatNavigationMenuItem = {
...fromFlatNavigationMenuItem,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
```
To
```ts
const positionUpdate = flatEntityUpdate.position;
if (
isDefined(positionUpdate) &&
(!Number.isInteger(positionUpdate) || positionUpdate < 0)
) {
const toFlatNavigationMenuItem = {
...fromFlatNavigationMenuItem,
...flatEntityUpdate,
};
```
## `SanitizeFlatEntityUpdate`
Enforcing the `flatEntityUpdate` to only contains comparable properties
per flat entity by striping out all unexpected keys
In the future we will also move the whole validation at runner lvl at
some point
```ts
export const sanitizeFlatEntityUpdate = <T extends AllMetadataName>({
flatEntityUpdate,
metadataName,
}: {
flatEntityUpdate: FlatEntityUpdate<T>;
metadataName: T;
}): FlatEntityUpdate<T> => {
const { propertiesToCompare } =
ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName];
const initialAccumulator: FlatEntityUpdate<T> = {};
return propertiesToCompare.reduce((accumulator, property) => {
const updatedValue =
flatEntityUpdate[property as MetadataFlatEntityComparableProperties<T>];
if (updatedValue === undefined) {
return accumulator;
}
return {
...accumulator,
[property]: updatedValue,
};
}, initialAccumulator);
};
```
|
||
|
|
4fcc424e24 |
Files field - add files field display and input + filtering (#17637)
This PR introduces a new FILES field type for Twenty CRM, allowing users to attach multiple files to any record. - Files field display - Files field preview - Files field input - Files field filtering To test : 1/ need to activate feature flag `IS_FILES_FIELD_ENABLED` + create a new FILES field - display in read only, edit, inline, table - edit - filter - export closes https://github.com/twentyhq/core-team-issues/issues/2154 <img width="354" height="134" alt="Screenshot 2026-02-02 at 19 11 01" src="https://github.com/user-attachments/assets/3c3de89e-f6b6-4526-b710-e4c7ee1a6d30" /> <img width="1081" height="933" alt="Screenshot 2026-02-02 at 19 10 41" src="https://github.com/user-attachments/assets/7c9a7278-edb7-4d4a-882c-f7404689d011" /> <img width="1073" height="719" alt="Screenshot 2026-02-02 at 19 10 33" src="https://github.com/user-attachments/assets/79cc372b-56a2-4cbf-b4fe-023adc4753a8" /> |
||
|
|
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 )
|
||
|
|
97867c11e6 | Upgrade application model (#17673) | ||
|
|
d35d5c0463 |
[BREAKING_CHANGE] Deprecate remaining entities standardId (#17639)
# Introduction Following https://github.com/twentyhq/twenty/pull/17632 and https://github.com/twentyhq/twenty/pull/17572 This PR deprecates the agent, skill, field metadata and role `standardId` in favor of the `universalIdentifier` usage ## Note - Removed previous standard ids declaration modules - Twenty-sdk now re-exports the `STANDARD_OBJECTS` universalIdentifier hashmap constant - deleted some sync-metadata deadcode too ( mainly types ) |
||
|
|
7b48efb5d6 |
Fix creation of objects with acronym names (e.g. "O&J") (#17633)
Fixes https://github.com/twentyhq/twenty/issues/17544 **Problem** When users create custom objects with short acronym names like "O&J", the system generates an object name oJ. When creating relation fields, the morph field name was built using string concatenation: const morphFieldName = `target${capitalize("oJ")}`; // → "targetOJ" This produced "targetOJ", which failed validation because the camelCase check performed in `validateFlatFieldMetadataName` (camelCase(name) === name) returns "targetOj" for "targetOJ". The issue comes from consecutive camelCase() operations. **Solution** Actually, the `camelCase(name) === name` check is questionnable. What we want to check is that a name is in camelCase format, not that it corresponds to the camelCase version of a given string, while that's we are doing here. lodash does not provide camelCase validator, only camelCase convertor, so we used it as a way to validate the format of the name. We may feel like `camelCase(name) === name` checks whether a name is camel-cased, but in addition to that it is also checking for a camel case "idempotency" we don't necessarily have and do not need: for instance if an object's name is "iOS" (which could be inferred from a label "I O S"), it won't pass the check: camelCase("iOS") is "ios" and "ios" !== "iOS". The existing check with `STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX` acts as a camel case validator, so we don't need that camelCase() check. |
||
|
|
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`
|
||
|
|
f46da3eefd |
Update manifest structure (#17547)
Move all sync entities in an `entities` key. Rename functions to
logicFunctions
```json
{
application: {
...
},
entities: {
objects: [],
logicFunctions: [],
...
}
}
```
|
||
|
|
fbbd8fe967 |
[REQUIRES_CACHE_FLUSH_FOR_FIELD_AND_OBJECT]FlatFieldMetadata and FlatObjectMetadata required universal (#17557)
# Introduction
In this PR we're migrating both the `field` and `object` metadata to be
using the new `FlatEntityFromV2` that requires all the
`UniversalFlatEntityExtraProperties` to be spread at the flat entity
root.
This means that we have to update all of their flat declaration
This type swap allows to isole a specific entity migration into his own
type scope and avoid to have everything handled at once
```ts
/**
* Currently under migration but aims to replace FlatEntity afterwards
*/
export type FlatEntityFromV2<
TEntity,
TMetadataName extends AllMetadataName | undefined = undefined,
TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
TEntity,
TMetadataName
>,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];
```
## Impact
Both object and field:
- Create input transpilation utils
- from entity to flat tools
- mocks
## Note
Removed from the universal extra properties the jsonb properties that do
not contain a serialized
## Next
Next step is to incrementally make the builder and runner expect
`UniversalFlatEntity` for both of these metadata
This way we will be able to fully migrate an entity e2e typesafely
|
||
|
|
fe9d6f34ff |
[REQUIRES_FULL_CACHE_FLUSH_WHEN_RELEASED] Refactor FlatEntity to be UniversalFlatEntity superset (#17452)
# Introduction
In this PR we're refactoring the `FlatEntity` type to become a superset
of the `UniversalFlatEntity`.
Right now we're storing all the extra properties in `__universal`
property, at some point it might just be sibling to other entity and we
might rely on the `propertiesToCompare` constants and TypeScript
allowing passing a superset type into a smaller subset type
## FromTo utils
The entity to flat entity method now computes the universal information,
standardized a typing and pattern to do
## Example
Also strictly type
```ts
"bbb019ea-6205-498c-aea5-67bc53bce8a9": {
"workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419",
"universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
"applicationId": "d01b010d-b984-465b-b40b-370e954e5188",
"id": "bbb019ea-6205-498c-aea5-67bc53bce8a9",
"pageLayoutTabId": "791a512f-169f-4209-b731-aa86716668c6",
"title": "Deals by Company",
"type": "GRAPH",
"objectMetadataId": "9e14efea-df5b-4c0e-aba9-cfe455f32397",
"gridPosition": { "row": 0, "column": 6, "rowSpan": 6, "columnSpan": 6 },
"configuration": {
"color": "orange",
"orderBy": "FIELD_ASC",
"timezone": "UTC",
"displayLegend": true,
"displayDataLabel": false,
"showCenterMetric": true,
"configurationType": "PIE_CHART",
"firstDayOfTheWeek": 0,
"aggregateOperation": "COUNT",
"groupBySubFieldName": "name",
"groupByFieldMetadataId": "6673ff18-63d2-47a1-8f85-2b9b09ca27a5",
"aggregateFieldMetadataId": "8d64ee41-5dd4-4de6-945a-7c0c18399715"
},
"createdAt": "2026-01-28T14:08:52.140Z",
"updatedAt": "2026-01-28T14:08:52.140Z",
"deletedAt": null,
"__universal": {
"universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002",
"applicationUniversalIdentifier": "20202020-64aa-4b6f-b003-9c74b97cee20",
"pageLayoutTabUniversalIdentifier": "20202020-d011-4d11-8d11-da5ab0a01001",
"objectMetadataUniversalIdentifier": "20202020-9549-49dd-b2b2-883999db8938",
"gridPosition": {
"row": 0,
"column": 6,
"rowSpan": 6,
"columnSpan": 6
},
"configuration": {
"color": "orange",
"orderBy": "FIELD_ASC",
"timezone": "UTC",
"displayLegend": true,
"displayDataLabel": false,
"showCenterMetric": true,
"configurationType": "PIE_CHART",
"firstDayOfTheWeek": 0,
"aggregateOperation": "COUNT",
"groupBySubFieldName": "name",
"aggregateFieldMetadataUniversalIdentifier": "20202020-d01a-4131-8a31-f123456789ab",
"groupByFieldMetadataUniversalIdentifier": "20202020-cbac-457e-b565-adece5fc815f"
}
}
},
```
|
||
|
|
44202668fd |
[TYPES] UniversalEntity JsonbProperty and SerializedRelation (#17396)
# 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
|
||
|
|
d0bc9a94c0 |
[OBJECT_CACHE_FLUSH_REQUIRED_WHEN_RELEASED] Remove FlatObjectMetadata custom fieldMetadataIds fk aggregator property (#17438)
# 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 |
||
|
|
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
|
||
|
|
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> |
||
|
|
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 ``` |
||
|
|
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>
|
||
|
|
30c13fc947 |
Files v2 - Add new FILES field type (#17236)
In this PR : - New field type FieldMetadataType.FILES added (as jsonb in DB) - Backend: GraphQL types, validation, REST API schema, data processor handling - Frontend: field configuration in settings - Feature flag IS_FILES_FIELD_ENABLED to gate the feature - Integration tests for create/filter validation - Dev seed: Feature flag enabled + FILES field on survey result object To do in next PRs : - Backend : -- new workspaceFile controller (for download) & new workspaceFile upload resolver (for upload) -- pre-hook/post-hook to ensure fileId existence + listener to ensure cleaning - Frontend : FILES field display/edit in table view, record, ... |
||
|
|
f1be7129cb |
Allow isUnique mutation on standard field (#17120)
# Introduction As we've just [identified](https://github.com/twentyhq/twenty/pull/16981) the standard field metadata in production we can not enable the is unique comparison even for standard entities |
||
|
|
2c8d3f02e1 |
feat: upgrade to Storybook version 10 (#17110)
Upgraded to Storybook 10. We still use `@storybook/test-runner` for testing since it appears it'd require more work to move from Jest to Vitest than I initially anticipated, but I completed this PR to fix `storybook:serve:dev` - it takes time to load, but it works the way it used to with Storybook 8. https://github.com/user-attachments/assets/7afc32c6-4bcf-4b37-b83b-8d00d28dda15 |
||
|
|
df108ea040 |
Identify standard objects (#17091)
# Introduction Followup https://github.com/twentyhq/twenty/pull/16981 1/ Migration, applicationId and universalIdentifier are required on entity ( save point migration + upgrade command fallback pattern ) 2/ Backfill using previous standard ids |
||
|
|
91b788ee09 |
[REQUIRES_FIELDS_CACHE_FLUSH]Fix field entity to flat field transpiler (#17096)
Following https://github.com/twentyhq/twenty/pull/16981 UniversalIdentifier is now required in the entity so no need to fallback it |
||
|
|
d7b7e9def1 |
Cascade delete Task targets when tasks deleted - logic + migration command (#17019)
Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
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 ) |
||
|
|
0c6f4021bf |
Fix - Update searchVector when labelIdentifier is updated (#16940)
Fixes https://github.com/twentyhq/twenty/issues/16891 In next PR, validation rules will be added in migration logic |
||
|
|
a6415db775 |
Refactor workspace migration and validation error types and centralize runner optimistic rendering (#16920)
# Introduction In this PR we're: - Refactoring the workspace migration action type introducing grain over metadata and operation type ( for example operation `create` and metadata `field` ) - Thanks to above point we can now factorize the runner optimistic rendering out of each runner actions-handler file using the existing into the generic one ( -3200 lines of code here ) - Still thanks to action type refactor we're able to dynamically compose the response error type only send data when there's here. No more static counter and static summary error message. This way we won't have to re run snapshot every time we add a new entity to the engine ( huge snapshot diff here ) ## Noticeable points: - We introduce an index update action to avoid any complex typing for not having one or a tuple of actions instead. Now the drop and insert logic is directly inferred from the update action handler instead of being two action ( delete index and create index ) ## TODO - [x] Define base actions types - [x] Migrate all actions to action type and metadata name pattern ( base actions ) - [x] Refactor flat entity validation type to embed metadata name - [x] Refactor optimistic rendering within runner - [x] Refactor legacy cache invalidation switch - [x] Refactor response error format ( dynamic counter again + no empty entries ) - [x] Try factorizing and removing redundant nor unused type declaration in metadata actions type intermediary files - [x] Adapt front to new response error format ## Remarks - ~~Should create an issue for generic replace flat entity in related flat entity maps~~ overkill - Should create an issue for oneToMany foreignKey being nullable not always cascade delete optimistic rendering edge case to either docs or fix it in delete flat entity and related entity ( re-code the pg cascading behavior ) - We could also factorize the builder to only implement validators and not the intermediary file |
||
|
|
42c9ae1ebc |
Centralize metadata relations constant + simplification (#16901)
# Introduction As we introduced a new grain on relation extraction thanks to low level `SyncableEntity` and `WorkspaceRelatedEntity` we're able to strictly typesafe extract metadata entity The new constant centralizes both many to one and one to many constants metadata entity constants in a more strictly typesafe way. Remains only the flatEntityForeignKey aggregator which has to be chosen manually across all available targeted flat entity ids properties |
||
|
|
ecd41fc9cb |
Reduce complexity on groupBy query (#16803)
fixes https://github.com/twentyhq/core-team-issues/issues/2009 |
||
|
|
1d9eb07ee0 |
Relation field metadata strict validation (#16890)
# Introduction Now validating relation related object and field foreignkey integrity |
||
|
|
1128331cc1 |
Fix field item type tag (#16860)
# Introduction This has been fixed on main already 1 hour ago, this PR now only passes the field application id instead of the object applicationId that could be different for example when creating a custom field on a standard object For the moment not introducing any logic around application integrity directly and still relying on the isCustom and standardId definition This will have to be refactored once we deprecate the standardId |
||
|
|
e3ffdb0c2b |
[BREAKING_CHANGE_NESTED_WORKSPACE]Refactor FlatEntity typing in aim of introducing UniversalFlatEntity (#16701)
# Introduction
Added a `WorkspaceRelated` and `AllNonWorkspaceRelatedEntity` to
simplify the `FlatEntityFrom` that now do not expect a string literal to
omit and itself builds the related many to one entities foreign key
aggregators
We now have the type grain over relation to syncable or just workspace
related entities
Added a migrations that sets the fk on missing entities
## Next
In upcoming PR we will be able to introduce such below type
```ts
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/remove-suffix.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface';
export type UniversalFlatEntityFrom<TEntity extends SyncableEntity> = Omit<
TEntity,
| `${ExtractEntityManyToOneEntityRelationProperties<TEntity> & string}Id`
| ExtractEntityRelatedEntityProperties<TEntity>
| 'application'
| 'workspaceId'
| 'applicationId'
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
[P in ExtractEntityManyToOneEntityRelationProperties<TEntity> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifier`]: string;
} & {
[P in ExtractEntityOneToManyEntityRelationProperties<
TEntity,
SyncableEntity
> &
string as `${RemoveSuffix<P, 's'>}UniversalIdentifiers`]: string[];
};
```
|
||
|
|
720348c583 |
Add upgrade commands to backfill updatedBy field and view fields (#16845)
--- prastoin edit ## Introduction As we're deprecating the sync metadata we cannot rely on it anymore in order to create the new standard updatedBy field In this PR we introduce a command that will backfill the dynamic field on both workspace standard and custom objects, for standard object it will create a standard fields ( twenty-standard app scoped ) and for custom one it will create a custom field ( worksapce-custom-app scoped ) ## Testing method Checkout `1.14.0` `yarn` and `npx nx database:reset twenty-server` checkout PR and `yarn` `npx nx build twenty-server` and `yarn database:migrate:prod` finally running the command #### Before <img width="2244" height="1464" alt="image" src="https://github.com/user-attachments/assets/2fb866cd-13b8-4152-99b8-1fcc813b1d46" /> #### After <img width="2244" height="1464" alt="image" src="https://github.com/user-attachments/assets/b836ac06-c7b8-4add-bee1-bc1963418f29" /> #### Logs ```ts [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Found 12 objects that need updatedBy field [EntityBuilder fieldMetadata] matrix computation: 3.994ms [EntityBuilder fieldMetadata] creation validation: 1.822ms [EntityBuilder fieldMetadata] deletion validation: 0.016ms [EntityBuilder fieldMetadata] update validation: 0.009ms [EntityBuilder fieldMetadata] entity processing: 6.098ms [EntityBuilder fieldMetadata] validateAndBuild: 10.217ms [EntityBuilder index] matrix computation: 0.857ms [EntityBuilder index] creation validation: 0.003ms [EntityBuilder index] deletion validation: 0.017ms [EntityBuilder index] update validation: 0.008ms [EntityBuilder index] entity processing: 4.721ms [EntityBuilder index] validateAndBuild: 5.632ms [Runner] Initial cache retrieval: 0.07ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 7.69ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 10.531ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 5.354ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 6.134ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.507ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.745ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.113ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.585ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.223ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.221ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 5.375ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 5.943ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.363ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.997ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.03ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.982ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.88ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.977ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 9.638ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 11.297ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.581ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.248ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.847ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.732ms [Runner] Transaction execution: 82.555ms [Runner] Cache invalidation flatFieldMetadataMaps,flatIndexMaps,flatObjectMetadataMaps: 89.247ms [Runner] Total execution: 171.963ms [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Successfully backfilled updatedBy field for 12 objects in workspace 20202020-1c25-4d02-bf25-6aeccf7ea419 [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Running command on workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db 2/2 [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Starting backfill of updatedBy field for workspace 3b8e6458-5fc1-4e63-8563-008ccddaa6db [Nest] 20678 - 12/30/2025, 1:47:35 PM LOG [BackfillUpdatedByFieldCommand] Found 10 objects that need updatedBy field [EntityBuilder fieldMetadata] matrix computation: 3.224ms [EntityBuilder fieldMetadata] creation validation: 0.969ms [EntityBuilder fieldMetadata] deletion validation: 0.014ms [EntityBuilder fieldMetadata] update validation: 0.002ms [EntityBuilder fieldMetadata] entity processing: 5.149ms [EntityBuilder fieldMetadata] validateAndBuild: 8.472ms [EntityBuilder index] matrix computation: 0.785ms [EntityBuilder index] creation validation: 0.002ms [EntityBuilder index] deletion validation: 0.014ms [EntityBuilder index] update validation: 0.002ms [EntityBuilder index] entity processing: 3.818ms [EntityBuilder index] validateAndBuild: 4.637ms [Runner] Initial cache retrieval: 0.05ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.964ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.902ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.502ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.039ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.673ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 3.403ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.143ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.123ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 2.033ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.954ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.826ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.903ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.495ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.106ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 14.014ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 14.373ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 1.669ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 2.48ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForMetadata: 0.62ms [BaseWorkspaceMigrationRunnerActionHandlerService] create_field executeForWorkspaceSchema: 1.048ms [Runner] Transaction execution: 73.222ms [Runner] Cache invalidation flatFieldMetadataMaps,flatIndexMaps,flatObjectMetadataMaps: 37.703ms [Runner] Total execution: 111.071ms ``` --- --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
a0b963ef86 |
Remove viewGroup.fieldMetadataId (#16571)
Final step of https://github.com/orgs/twentyhq/projects/1/views/8?pane=issue&itemId=142348748&issue=twentyhq%7Ccore-team-issues%7C1965 Removing viewGroup.fieldMetadataId. It's already not used in FE anymore |
||
|
|
999bc84b17 |
fix(server): Favortites, attachments, timeline... (#16509)
fix(server): system objects like Favortites, attachments, timeline... should be system fields |
||
|
|
b83dc3aaff |
TimelineActivity migration to morph (#15652)
# TimelineActivity migration to morph - Creates `timelineActivities2` relations on Company, Dashboard, Note, Opportunity, Person, Task, Workflow, WorkflowRun, and WorkflowVersion entities with proper metadata and cascade delete behavior. It was required to create standard fields as well since the mapObjectMetadataByUniqueIdentifier needs it. otherwise the fields won't be considered - Feature Flag `IS_TIMELINE_ACTIVITY_MIGRATED` necessary to have the two states in parallel. It is used as a stamp once the migration has been run - Migration is done using the coreDataSource. Why ? even though is unsafe to use, the first implementation of the migration took forever on each workspace. See [this commit](https://github.com/twentyhq/twenty/pull/15652/commits/477011e8d7d4c580f79ba7ec4a8fb002a3ec86b2) The plan for this complex migration is as follows :  Note: we will need to rename fields in the release 1.12 (there is no easy way to do all this in one release) |
||
|
|
7b98804ec1 |
Fix flat field metadata date type (#16445)
# Introduction Following https://github.com/twentyhq/twenty/pull/16420 |
||
|
|
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 |
||
|
|
4996f3dd28 |
Finalize twenty standard app as workspace migration object and fields (#16353)
# Introduction Related to https://github.com/twentyhq/core-team-issues/issues/1995 In this PR we're fixing the remaining object/fields validation errors resulting from standard objects and fields now passing a validation that wasn't when using the sync metadata ## Key Changes - **Field naming**: Renamed `iCalUID` to `iCalUid` for consistent camelCase convention across calendar events - **Enum standardization**: Uppercased enum values for message channels (email→EMAIL), message participants (from→FROM, to→TO, cc→CC, bcc→BCC), and message direction (incoming→INCOMING, outgoing→OUTGOING) - **Label simplification**: Removed example values from workspace member number format labels for cleaner UI - **Migration infrastructure**: Added `isSystemBuild` flag throughout field metadata service pipeline to allow system-level updates of standard fields that bypass normal restrictions ## Migrating the existing data We've created an upgrade command that will identify using the existing object and field standard id field that needs to be updated, even though the sync metadata still in usage could have fix them ( and the goal is to deprecate it by the end of the sprint ) We will call the updateOneField for each of them, we're passing by the field service in order to battle test what are going to be the temporary way to handle standard migrations when we will start deprecating the sync metadata but haven't still refactored the v2 workspace migration to be workspace agnostic ## Twenty eng migration Tested the whole migration + upgrade on twenty eng Here are generated workspace migration Records are handled natively gracefully too ### ICalUid ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": "iCalUID", "to": "iCalUid", "property": "name" } ] } ], "workspaceId": "" } } ``` ### Incoming Outgoing None as already caps in database somehow ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [], "workspaceId": "" } } ``` ### EMAIL ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": "'email'", "to": "'EMAIL'", "property": "defaultValue" }, { "from": [ { "color": "green", "id": "", "label": "Email", "position": 0, "value": "email" }, { "color": "blue", "id": "", "label": "SMS", "position": 1, "value": "sms" } ], "to": [ { "color": "green", "id": "", "label": "Email", "position": 0, "value": "EMAIL" }, { "color": "blue", "id": "", "label": "SMS", "position": 1, "value": "SMS" } ], "property": "options" } ] } ], "workspaceId": "e" } } ``` ### MessageParticipantRole ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": "'from'", "to": "'FROM'", "property": "defaultValue" }, { "from": [ { "color": "green", "id": "", "label": "From", "position": 0, "value": "from" }, { "color": "blue", "id": "", "label": "To", "position": 1, "value": "to" }, { "color": "orange", "id": "", "label": "Cc", "position": 2, "value": "cc" }, { "color": "red", "id": "", "label": "Bcc", "position": 3, "value": "bcc" } ], "to": [ { "color": "green", "id": "", "label": "From", "position": 0, "value": "FROM" }, { "color": "blue", "id": "", "label": "To", "position": 1, "value": "TO" }, { "color": "orange", "id": "", "label": "Cc", "position": 2, "value": "CC" }, { "color": "red", "id": "", "label": "Bcc", "position": 3, "value": "BCC" } ], "property": "options" } ] } ], "workspaceId": "" } } ``` ### Workspace member number format labels ```json { "status": "success", "workspaceMigration": { "relatedFlatEntityMapsKeys": [ "flatFieldMetadataMaps", "flatIndexMaps", "flatViewFilterMaps", "flatViewGroupMaps", "flatViewMaps", "flatViewFieldMaps", "flatObjectMetadataMaps" ], "actions": [ { "type": "update_field", "fieldMetadataId": "", "objectMetadataId": "", "updates": [ { "from": [ { "color": "turquoise", "id": "", "label": "System", "position": 0, "value": "SYSTEM" }, { "color": "blue", "id": "", "label": "Commas and dot (1,234.56)", "position": 1, "value": "COMMAS_AND_DOT" }, { "color": "green", "id": "", "label": "Spaces and comma (1 234,56)", "position": 2, "value": "SPACES_AND_COMMA" }, { "color": "orange", "id": "", "label": "Dots and comma (1.234,56)", "position": 3, "value": "DOTS_AND_COMMA" }, { "color": "purple", "id": "", "label": "Apostrophe and dot (1'234.56)", "position": 4, "value": "APOSTROPHE_AND_DOT" } ], "to": [ { "color": "turquoise", "id": "", "label": "System", "position": 0, "value": "SYSTEM" }, { "color": "blue", "id": "", "label": "Commas and dot", "position": 1, "value": "COMMAS_AND_DOT" }, { "color": "green", "id": "", "label": "Spaces and comma", "position": 2, "value": "SPACES_AND_COMMA" }, { "color": "orange", "id": "", "label": "Dots and comma", "position": 3, "value": "DOTS_AND_COMMA" }, { "color": "purple", "id": "", "label": "Apostrophe and dot", "position": 4, "value": "APOSTROPHE_AND_DOT" } ], "property": "options" } ] } ], "workspaceId": "" } } ``` |
||
|
|
07cfaa78ef |
Fix unique standard field (#16371)
Fixes https://github.com/twentyhq/twenty/issues/15925 - update field metadata update logic - uniformize the way index are named - command to migrate v1-named unique index - add integration testing --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
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) |
||
|
|
28cdb02fbb |
Twenty standard application Objects and fields as allFlatEntityMaps ID non-agnostic (#16298)
# Introduction Related to https://github.com/twentyhq/core-team-issues/issues/1995 This PR introduces the basis of the `twentyStandard` application as code on demand, it's highly tied to `ids` where it will becomes workspace agnostic following the builder and runner `universalIdentifier` refactor later. The goal here to allow computing the `allFlatEntityMaps` `to` of the `twentyStandard` application on a empty workspace ( workspace creation ). Allowing installing the twenty standard app through a workspace migration instead of passing by the sync metadata Nothing done will be run in production for the moment if it's not the small validation refactor we've introduced Please note that everything introduced here will be replaced at some point by a twenty app instance when the twenty sdk is mature enough to handle of the edge cases we need here ## How we've proceeded We've been iterating over every workspace entity both objects and their fields, and transpiled them to flatEntity. Being sure we migrate the defaultValue, settings and so on accordingly. We've also compute all the ids in prior of the whole entities computation so we don't face any hoisting issue. ## Current state At the moment only handling all of the 29 standard objects and their fields Settings a unique universalIdentifier for all of them Will come views, agent role targets and so on later ## `workspace:compute-twenty-standard-migration` command This command allow generating a workspace migration that will result in installing the twenty standard app in an empty workspace It's temporary and aims to allow debugging for the moment we might not keep it in the future as it is right now It contains debug writeFileSync which is expected no worries greptile ## `LabelFieldMetadataIdentifierId` Small refactor allowing defining the label identifier field metadata id of a uuid field metadata type for system object, as some of our standard object don't have a name field and don't aim to Also please note that we might remove this build options later in the sake of the currently installed universal identifier application that we could compare with the deterministic twenty standard one ## `runFlatFieldMetadataValidators` Deprecated this pattern which was redundant and not v2 friendly pattern ## Current errors that will address in upcoming PR Current standard objects and fields metadata does not pass the validation that we have in place, as historically the sync metadata would directly consume the repositories and would just ignore the validation. This is about to change. Will handle the below errors in dedicated PRs as they will required upgrade commands in order to migrate the data, or will handle that from the sync metadata instead still to be determined but nothing critical here - camel case field metadata name - options label invalid format ```json { "status": "fail", "report": { "fieldMetadata": [ { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Name should be in camelCase", "userFriendlyMessage": { "id": "P+jdmX", "message": "Name should be in camelCase" }, "value": "iCalUID" } ], "flatEntityMinimalInformation": { "id": "68dd83cd-92c8-4233-bb28-47939bab6124", "name": "iCalUID", "objectMetadataId": "11c16ab6-9176-439e-a2db-a12c5a58a524" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"email\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "email" } }, "value": "email" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"sms\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "sms" } }, "value": "sms" } ], "flatEntityMinimalInformation": { "id": "e3caaf2a-e07d-4146-8dfc-9eef904e82c9", "name": "type", "objectMetadataId": "4b777de5-4c7b-4af4-9b92-655c0f87512b" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"incoming\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "incoming" } }, "value": "incoming" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"outgoing\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "outgoing" } }, "value": "outgoing" } ], "flatEntityMinimalInformation": { "id": "d96233a4-93be-45ea-9548-3b50f3c700cf", "name": "direction", "objectMetadataId": "480a648a-d2e5-482a-992f-ef053e1b4bb0" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"from\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "from" } }, "value": "from" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"to\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "to" } }, "value": "to" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"cc\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "cc" } }, "value": "cc" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"bcc\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "bcc" } }, "value": "bcc" } ], "flatEntityMinimalInformation": { "id": "961c598e-67c3-452d-8bb2-b92c0bc64404", "name": "role", "objectMetadataId": "8af8a13c-ff97-4cd3-b70d-52a7dc2924b4" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Label must not contain a comma", "userFriendlyMessage": { "id": "k731jp", "message": "Label must not contain a comma" }, "value": "Commas and dot (1,234.56)" }, { "code": "INVALID_FIELD_INPUT", "message": "Label must not contain a comma", "userFriendlyMessage": { "id": "k731jp", "message": "Label must not contain a comma" }, "value": "Spaces and comma (1 234,56)" }, { "code": "INVALID_FIELD_INPUT", "message": "Label must not contain a comma", "userFriendlyMessage": { "id": "k731jp", "message": "Label must not contain a comma" }, "value": "Dots and comma (1.234,56)" } ], "flatEntityMinimalInformation": { "id": "7fa20caf-2597-42e3-84e5-15a91b125b9b", "name": "numberFormat", "objectMetadataId": "a6974302-9e72-461c-aa09-9390f4ff16fc" }, "type": "create_field" } ], "objectMetadata": [], "view": [], "viewField": [], "viewGroup": [], "index": [], "serverlessFunction": [], "cronTrigger": [], "databaseEventTrigger": [], "routeTrigger": [], "viewFilter": [], "role": [], "roleTarget": [], "agent": [] } } ``` |
||
|
|
59672e3e34 |
Migrate agent v2 (#16214)
# Introduction Closes https://github.com/twentyhq/core-team-issues/issues/1980 In this PR we migrate the agent from v1 to v2. ## New FlatRoleTargetByAgentIdMaps Derivated the `flatRoleTargetMaps` to be building a `flatRoleTargetByAgentIdMaps` to ease retrieving a roleId to associate to an agent ## Coverage Added strong coverage on both failing and successful CRU agents operations --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
77409b6eb2 |
[Requires "warm" cache flush (no immediate downtime before flush)] Migrate viewGroup.fieldMetadataId -> view.mainGroupByFieldMetadataId (1/3) (#16206)
In this PR (1/3) - introduce view.mainGroupByFieldMetadataId as the new reference determining which fieldMetadataId is used in a grouped view, in order to deprecate viewGroup.fieldMetadataId which creates inconsistencies. view.mainGroupByFieldMetadataId is now filled at every view creation, though not in use yet. - Introduce a command to backfill view.mainGroupByFieldMetadataId for existing views + delete all viewGroup.fieldMetadataId with a fieldMetadataId that is not view.mainGroupByFieldMetadataId. (It should concern 37 active workspaces) - Temporarily disable the option to change a grouped view's fieldMetadataId as for now it creates inconsistencies. This feature can be reintroduced when we have done the full migration. In a next PR - (2/3) use view.mainGroupByFieldMetadataId instead of viewGroup.fieldMetadataId. In FE we may keep viewGroup.fieldMetadataId as a state (TBD). View groups will now be created / deleted as a side effect of view's mainGroupByFieldMetadataId update. - (3/3) remove viewGroup.fieldMetadataId --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
1eb2e44058 |
Refactor workspace cache service (#16208)
## Context We've recently introduced a new workspace cache service which now acts as a cache access and local storage for all workspace related data, deprecating the individual specific services. - Better performance through multiple caching/fetching strategies - Consistent data access patterns across the codebase - Reduced redis queries through MGET/MSET/PIPELINE with multiple cache keys |
||
|
|
ee08060798 |
Improve deactivated objects & fields behaviors. (#16090)
Closes [1918](https://github.com/twentyhq/core-team-issues/issues/1918). - For the first point in the issue, we just show the deactivated entries along with the deactivated text. --- - For the second point, we show a banner and control the enabled/disabled state of save button depending on whether we're allowing the user to create table with the typed name. - For example, we do not want to allow the user to create a table with reserved name, so we disable the save button without showing a banner. - Similarly, we do not want the user to create a table with a name that already exists in the database. In this case, we show a banner and we also disable the save button. - Finally, we do not want to allow the user to create a table where singular and plural name are the same. Therefore, we disable the save button for names like `works`. --- - For the third point, if we add the delete button, it logically means that we allow the user to delete a custom object/field even it has not been deactivated yet, so did that. - Upon deleting the object/field, if we wait for the metadata to refetch before we navigate, this is what we see because the path does not exist any longer after deletion and we're waiting for refetch on the path until we navigate away. https://github.com/user-attachments/assets/dbe0569c-db88-4285-851f-22551b1ca81e - To avoid this page from appearing, I replaced awaiting refetch to not awaiting refetch and redirecting while the refetch happens in the background. - Therefore, when we delete something, there is a slight delay for when it is actually cleared out from the list, but the Not Found view does not appear on the screen. https://github.com/user-attachments/assets/47f49579-ce51-4d6a-b857-72046247bb4b - I tried optimistically removing the object/field from the metadata, but it leads to some issues (crashes the app) and I have not been able to find a solution for it yet. - Therefore, instead of getting stuck at perfection and blocking myself, I stopped getting into the issue further and created this PR by ensuring that the desired functionality works. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Display deactivated objects/fields by default, add delete actions with confirmation, and unify metadata name computation (auto-suffix reserved keywords) across front/back with conflict checks in object creation. > > - **Frontend (Settings/Data Model)**: > - **Visibility/UX**: Show `Deactivated` labels for objects/fields; filters default to include inactive (`showDeactivated`/`showInactive` true); replace field action dropdown with chevron link. > - **Delete flows**: Add delete buttons for custom objects/fields with confirmation modals and background refetch to avoid Not Found flashes. > - **Creation/Edit validation**: Add name conflict detection banner in `SettingsDataModelObjectAboutForm` and disable Save on conflicts; simplify `metadataLabelSchema` to use computed name; form fields validate on change and sync API names. > - **Shared (twenty-shared/metadata)**: > - Add `computeMetadataNameFromLabel` util (slugify+camelCase) and `RESERVED_METADATA_NAME_KEYWORDS`; auto-append `Custom` to reserved names; export constants/utilities. > - **Backend**: > - Migrate to shared `computeMetadataNameFromLabel`; update validators to use shared reserved keywords with new messages; allow deletion of active custom fields/objects (keep standard guards); adjust services/decorators accordingly. > - **Tests/Stories**: > - Update unit/integration snapshots for new reserved-name messages and behaviors; add missing i18n/router decorators in stories. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5b126155606f6dbc8f7f91e2192cffb7bd2ebd2c. 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> |
||
|
|
1607aebcc6 |
Deprecate object metadata maps in favor of flat entities (#16080)
## Context Deprecating the old objectMetadataMap type in favour of split flat entities to match with our new caching. In the long run, trying to achieve: - Better performance through caching - Consistent data access patterns across the codebase - Reduced database queries Now that everything is based on flat entities, which are cached, we can finish the refactoring of workspace context cache which should already improve performances. Then the last step will be to consume that new cache in the new global datasource to get rid of the many workspace datasources stored in the server |
||
|
|
1a45576990 |
Morph-add-new-object-destination (#16027)
Add new object target to an existing morph relation (backend only) Fixes https://github.com/twentyhq/core-team-issues/issues/1898 --------- Co-authored-by: prastoin <paul@twenty.com> |