Files
twenty/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts
T
Paul Rastoin 8c951d3623 Migrate Views-xxx Index Field Object Skill to be fully universal ( all actions and metadata runner and builder ) + all metadata update actions runner (#17687)
# What this PR does
Overall naming `universal` versus `flat` is not always the most updated
and so on
Will make a big cleaning tour after I've finished the whole migration
Migrating all `view` and ( filter fields etc ) `field` `object` `index`
to the universal pattern on all `services`, `builder` and `runner`
levels

## Universal and flat optimistic tooling

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

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

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


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

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

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

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

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-09 19:02:38 +01:00

124 lines
3.6 KiB
TypeScript

import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
type TestingRuntimeContext = {
objectMetadataId: string;
numberFieldMetadataId: string;
};
type CreateOneObjectMetadataItemTestingContext = EachTestingContext<
| ((args: TestingRuntimeContext) => Partial<UpdateObjectPayload>)
| Partial<UpdateObjectPayload>
>[];
const labelIdentifierFailingTestsUseCase: CreateOneObjectMetadataItemTestingContext =
[
{
title: 'when labelIdentifier is not a uuid',
context: {
labelIdentifierFieldMetadataId: 'not-a-uuid',
},
},
{
title: 'when labelIdentifier is not a known field metadata id',
context: {
labelIdentifierFieldMetadataId: '42422020-f49c-4159-8751-76a24f47b360',
},
},
{
title: 'when labelIdentifier is not a TEXT or NAME field',
context: ({ numberFieldMetadataId }) => ({
labelIdentifierFieldMetadataId: numberFieldMetadataId,
}),
},
];
const allTestsUseCases = [...labelIdentifierFailingTestsUseCase];
describe('Object metadata update should fail', () => {
let objectMetadataId: string;
let numberFieldMetadataId: string;
beforeAll(async () => {
const { data } = await createOneObjectMetadata({
expectToFail: false,
input: {
labelPlural: 'whatevers',
labelSingular: 'whatever',
namePlural: 'whatevers',
nameSingular: 'whatever',
},
});
objectMetadataId = data.createOneObject.id;
const {
data: { createOneField },
} = await createOneFieldMetadata({
expectToFail: false,
input: {
objectMetadataId: objectMetadataId,
name: 'testName',
label: 'Test name',
isLabelSyncedWithName: true,
type: FieldMetadataType.NUMBER,
},
});
numberFieldMetadataId = createOneField.id;
});
afterAll(async () => {
await updateOneObjectMetadata({
expectToFail: false,
input: {
idToUpdate: objectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
input: {
idToDelete: objectMetadataId,
},
});
});
it.each(eachTestingContextFilter(allTestsUseCases))(
'$title',
async ({ context }) => {
const updatePayload =
typeof context === 'function'
? context({
numberFieldMetadataId,
objectMetadataId,
})
: context;
const { errors } = await updateOneObjectMetadata({
input: {
idToUpdate: objectMetadataId,
updatePayload,
},
expectToFail: true,
});
expect(errors).toBeDefined();
expect(errors).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(errors),
);
},
);
});