b1a781fbd9245f45efd84ef8a2d3b226a3319655
21 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d6c8be7df |
[Apps] Fix - app-synced object should be searchable (#19206)
## Summary - **Make app-synced objects searchable**: `isSearchable` was hardcoded to `false` and the `searchVector` field was missing the `GENERATED ALWAYS AS (...)` expression, causing all records to have a `NULL` search vector and be excluded from search results. Fixed by defaulting `isSearchable` to `true` (configurable via the object manifest), computing the `asExpression` from the label identifier field, and allowing the update-field-action-handler to handle the `null` → defined `asExpression` transition. - **Make `isSearchable` updatable on an object**: The property had `toCompare: false` in the entity properties configuration, so updates via the API were silently ignored and never persisted. Fixed by setting `toCompare: true`. |
||
|
|
c4e55d08ff |
fix: allow identical singular and plural labels for objects (#18678)
## Summary Closes #18673 Some languages (e.g., German "Unternehmen") and even English words (sheep, deer, aircraft, series) have identical singular and plural forms. Twenty previously blocked saving when labels matched, making it impossible to correctly name objects in these cases. - **Labels** are purely display strings — removed the equality validation from both the frontend Zod schema and backend validator - **API names** (nameSingular/namePlural) must stay different since they generate distinct GraphQL resolvers (`findOne` vs `findMany`, `createOne` vs `createMany`, etc.) and REST endpoints — this validation is preserved - Added a shared `computeMetadataNamesFromLabels` util in `twenty-shared` that auto-appends `'s'` to the plural API name when both labels produce the same camelCase name (e.g., "Unternehmen" → `unternehmen` / `unternehmens`) - Both the frontend form and backend sync-check use the same shared util — single source of truth, no duplicated logic **No retroactive impact**: since the old code prevented identical labels from ever being saved, no existing workspace has `labelSingular === labelPlural`. ## Test plan - [x] New unit tests for `computeMetadataNamesFromLabels` (7 tests: standard labels, Sheep, Unternehmen, Aircraft, empty labels, different labels, applyCustomSuffix) - [x] Updated frontend schema validation tests (identical labels with different names now passes; identical names still fails) - [x] Updated backend integration test cases (removed identical-label failing cases) - [ ] Manual: create a new object with identical singular/plural labels (e.g. "Sheep" / "Sheep") — should save successfully with API names `sheep` / `sheeps` - [ ] Manual: verify existing objects with different labels still work unchanged Made with [Cursor](https://cursor.com) |
||
|
|
f080b952eb |
Add manifest integration tests (#18203)
- add integration test on sync manifest endpoint - fix invalid standard uuid + migration command |
||
|
|
af0af0a237 |
Fix cross app installation (#18151)
# Introduction - Fixing app dependencyFlatEntityMaps load ( to load current app + dependent app ) - Fix empty flat entity maps and undefined optimistic override - Refactor the optimistic rendering to be mutation oriented at orchestrator level |
||
|
|
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> |
||
|
|
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 )
|
||
|
|
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. |
||
|
|
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>
|
||
|
|
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 ) |
||
|
|
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": "" } } ``` |
||
|
|
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": [] } } ``` |
||
|
|
eedb163131 |
Field metadata and object metadata v1 relicas (#16230)
# Introduction Related https://github.com/twentyhq/core-team-issues/issues/1911 Nearly done with all functional v1 implemen removal Will remain dead code methods that I will detect using knip |
||
|
|
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 ! ) |
||
|
|
e577c2d746 |
Update user friendly errors for translations (#15000)
Force msg typing instead of string for user friendly errors |
||
|
|
23e21cbeea |
Label identifier validation v2 (#14867)
# Introduction Adding a hacky way to validate object against fields before fields validation ( bi-directional validation process ) If you encounter an identical setup we will add a specific devXp as cleanup validation but for the moment this seems enough close https://github.com/twentyhq/core-team-issues/issues/1639 |
||
|
|
170f6d27c5 |
Extract field metadata build out of object metadata build (#14763)
# Introduction Extracting the legacy fields build and dispatch out of the object one to follow the generic flat entity build, also update caches entries for object ## Main tasks - flat field map cache - flat field builder - refactored the dispatch matrix to return flat entity maps instead of flat entity arrays - orchestrator aggregator - removing legacy code - making universal identifier aka standardId of standard field for custom object deterministically dynamic - perfs debug logs for v2 ## TODO - [x] Refactor the generic entity builder to be dependency flat maps in order to main foreign keys list in flat parent - [x] Refactor the flat object metadata to contain the array of related fields and avoid costy find object fields - [ ] Improve the create field handler to handle multiple field at once - [ ] Refactor the dispatch to embbed the comparison - [ ] Improve perf by extracting from elements out of existing - [ ] Fix the labelIdentifierId validators on object before field creation ( integ tests are in failing mode ) ## Debug logs snippet ```ts [EntityBuilder fieldMetadata] matrix computation: 0.027ms [EntityBuilder fieldMetadata] creation validation: 0.001ms [EntityBuilder fieldMetadata] deletion validation: 0.293ms [EntityBuilder fieldMetadata] update validation: 0.006ms [EntityBuilder fieldMetadata] entity processing: 0.363ms [EntityBuilder fieldMetadata] validateAndBuild: 0.455ms [EntityBuilder index] matrix computation: 0.005ms [EntityBuilder index] creation validation: 0.001ms [EntityBuilder index] deletion validation: 0.146ms [EntityBuilder index] update validation: 0.004ms [EntityBuilder index] entity processing: 0.199ms [EntityBuilder index] validateAndBuild: 0.228ms [Runner] Initial cache retrieval: 0.549ms [BaseWorkspaceMigrationRunnerActionHandlerService] delete_index executeForWorkspaceSchema: 11.665ms [BaseWorkspaceMigrationRunnerActionHandlerService] delete_index executeForMetadata: 12.864ms [BaseWorkspaceMigrationRunnerActionHandlerService] delete_field executeForWorkspaceSchema: 1.476ms [BaseWorkspaceMigrationRunnerActionHandlerService] delete_field executeForMetadata: 6.816ms [BaseWorkspaceMigrationRunnerActionHandlerService] delete_field executeForWorkspaceSchema: 0.062ms [BaseWorkspaceMigrationRunnerActionHandlerService] delete_field executeForMetadata: 0.889ms [Runner] Transaction execution: 23.434ms [Runner] Cache invalidation: 316.662ms [Runner] Total execution: 340.767ms ``` As you can see cache invalidation is way to long, we could replace the cache by the optimistic in the end |
||
|
|
e264d7f32b |
fix: Enable Lingui recommended rules and fix all translation violations (#14133)
- Enable lingui/no-single-variables-to-translate and all other
recommended Lingui rules
- Fix single variable translation patterns (t`${variable}` → variable)
- Fix expression-in-message violations by extracting variables
- Fix t-call-in-function violations by moving translations inside
functions
- Update ESLint configs to use linguiPlugin.configs['flat/recommended']
- Clean up unused imports and improve translation patterns
|
||
|
|
e99c67d6b5 |
Fail slow + structured validator error response scoped by object and fields (#13980)
# Introduction
Example of returned response: ( snapshot copy don't mind jest expect any
values )
```json
{
"extensions": {
"code": "BAD_USER_INPUT",
"errors": {
"fieldMetadata": [
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
{
"errors": [
{
"code": "INVALID_FIELD_INPUT",
"message": "Name is too long",
"userFriendlyMessage": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"id": Any<String>,
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"objectMetadataId": Any<String>,
"operation": "create_field",
},
],
"objectMetadata": [
{
"errors": [
{
"code": "INVALID_OBJECT_INPUT",
"message": "Name is too long",
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
],
"fields": [],
"id": Any<String>,
"namePlural": "listingas",
"nameSingular": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"operation": "create_object",
},
],
},
"message": "Validation failed for 1 object(s) and 5 field(s)",
"summary": {
"invalidFields": 5,
"invalidObjects": 1,
"totalErrors": 6,
},
"userFriendlyMessage": "Validation failed for 1 object(s) and 5 field(s)",
},
"message": "Multiple validation errors occurred while creating object",
"name": "GraphQLError",
}
```
## Note
We should put in place integrations tests on REST API too, in order to
ensure we receive the same response error
|
||
|
|
6629092645 |
Refactor builder to embed Object validation create/delete/update (#13934)
# Introduction Moving validation directly in the builder that has the perfect granularity to do it. When importing we won't have to infer and dispatch on the operation nature ( update delete create ) and validate accordingly ## Objects Only migrated object validation for the moment even though create object involves a validate flat field metadata creation call too ## TODO - improve `otherFlatObjectMetadataMapsToValidate` naming too vague ## Next - handle fields validation within fields actions build - Unit test coverage validation issue on builder and validate - integration test plugging with new feature flag ## Manual tested - Update - Delete - Create TODO |
||
|
|
ad77d2f462 |
ObjectMetadataServiceV2.updateOne (#13891)
# Introduction Last service to get migrated, `updateOne` object metadata service v2 |