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`
This commit is contained in:
Paul Rastoin
2026-02-02 13:22:38 +01:00
committed by GitHub
parent f0bc9fcb43
commit bd9688421f
235 changed files with 4536 additions and 2739 deletions
@@ -153,6 +153,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect an upd
"updatedFlatEntityMaps": {
"byId": {
"field-id-1": {
"universalIdentifier": "universal-identifier-1",
"updates": [
{
"from": false,
@@ -283,6 +284,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"updatedFlatEntityMaps": {
"byId": {
"field-id-1": {
"universalIdentifier": "universal-identifier-1",
"updates": [
{
"from": true,
@@ -0,0 +1,278 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-actions-report.constant';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { aggregateNonRelationFieldsIntoObjectActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-non-relation-fields-into-object-actions.util';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
describe('aggregateNonRelationFieldsIntoObjectActions', () => {
it('should merge non-relation fields into matching create-object actions', () => {
const objectUniversalId = 'object-1';
const fieldUniversalId = 'field-1';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: objectUniversalId,
nameSingular: 'testObject',
namePlural: 'testObjects',
}),
universalFlatFieldMetadatas: [],
} satisfies UniversalCreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'testField',
}),
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'generated-field-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateNonRelationFieldsIntoObjectActions({
orchestratorActionsReport: input,
});
// Field should be merged into object action
expect(result.objectMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: fieldUniversalId },
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'generated-field-id',
},
},
]);
// No remaining field actions
expect(result.fieldMetadata.create).toHaveLength(0);
});
it('should keep relation fields in separate create-field actions', () => {
const objectUniversalId = 'object-1';
const relationFieldUniversalId = 'relation-field-1';
const targetFieldUniversalId = 'target-field-1';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: objectUniversalId,
nameSingular: 'testObject',
namePlural: 'testObjects',
}),
universalFlatFieldMetadatas: [],
} satisfies UniversalCreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: relationFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationField',
relationTargetFieldMetadataUniversalIdentifier:
targetFieldUniversalId,
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
],
fieldIdByUniversalIdentifier: {
[relationFieldUniversalId]: 'generated-relation-field-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateNonRelationFieldsIntoObjectActions({
orchestratorActionsReport: input,
});
// Object action should have no fields merged
expect(result.objectMetadata.create).toMatchObject([
{ universalFlatFieldMetadatas: [] },
]);
// Relation field should remain in field actions
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: relationFieldUniversalId },
],
},
]);
});
it('should keep fields for existing objects in separate create-field actions', () => {
const existingObjectUniversalId = 'existing-object';
const fieldUniversalId = 'field-for-existing-object';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [], // No objects being created
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'existing-object-metadata-id',
objectMetadataUniversalIdentifier: existingObjectUniversalId,
type: FieldMetadataType.TEXT,
name: 'fieldForExistingObject',
}),
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'generated-field-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateNonRelationFieldsIntoObjectActions({
orchestratorActionsReport: input,
});
// No object actions
expect(result.objectMetadata.create).toHaveLength(0);
// Field should remain in field actions (no matching object to merge into)
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: fieldUniversalId },
],
},
]);
});
it('should handle mixed relation and non-relation fields', () => {
const objectUniversalId = 'object-1';
const textFieldUniversalId = 'text-field';
const relationFieldUniversalId = 'relation-field';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: objectUniversalId,
nameSingular: 'testObject',
namePlural: 'testObjects',
}),
universalFlatFieldMetadatas: [],
} satisfies UniversalCreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: textFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'textField',
}),
getFlatFieldMetadataMock({
universalIdentifier: relationFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationField',
relationTargetFieldMetadataUniversalIdentifier: 'target-field',
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
],
fieldIdByUniversalIdentifier: {
[textFieldUniversalId]: 'text-field-id',
[relationFieldUniversalId]: 'relation-field-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateNonRelationFieldsIntoObjectActions({
orchestratorActionsReport: input,
});
// Text field should be merged into object action
expect(result.objectMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: textFieldUniversalId },
],
},
]);
// Relation field should remain in field actions
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: relationFieldUniversalId },
],
},
]);
});
});
@@ -1,491 +0,0 @@
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-actions-report.constant';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util';
import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type CreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
type CreateAggregationTestCase = EachTestingContext<{
input: OrchestratorActionsReport;
expected: {
expectCreateFieldActionPerObjectMetadataId: Record<string, number>;
expectCreateObjectActionPerObjectMetadataId: Record<string, number>;
};
}>;
describe('aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions', () => {
const testCases: CreateAggregationTestCase[] = [
{
title:
'should aggregate single object with multiple fields into one object action',
context: {
input: {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'user',
namePlural: 'users',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'lastName',
}),
],
} satisfies CreateFieldAction,
],
update: [],
delete: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {},
expectCreateObjectActionPerObjectMetadataId: {
'object-1': 1,
},
},
},
},
{
title:
'should keep separate field actions when no matching object action exists',
context: {
input: {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'secondName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-3',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-3',
name: 'lastName',
}),
],
} satisfies CreateFieldAction,
],
update: [],
delete: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {
'object-1': 1,
},
expectCreateObjectActionPerObjectMetadataId: {},
},
},
},
{
title: 'should handle multiple objects with their respective fields',
context: {
input: {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'user',
namePlural: 'users',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: 'object-2',
id: 'object-2',
nameSingular: 'company',
namePlural: 'companies',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-2',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-2',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'name',
}),
getFlatFieldMetadataMock({
universalIdentifier: 'field-3',
objectMetadataId: 'object-2',
type: FieldMetadataType.TEXT,
id: 'field-3',
name: 'industry',
}),
],
} satisfies CreateFieldAction,
],
update: [],
delete: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {},
expectCreateObjectActionPerObjectMetadataId: {
'object-1': 1,
'object-2': 1,
},
},
},
},
{
title:
'should handle mixed scenario with some fields merged and some kept separate',
context: {
input: {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'user',
namePlural: 'users',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-2',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-2',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'orphanField',
}),
],
} satisfies CreateFieldAction,
],
update: [],
delete: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {
'object-2': 1,
},
expectCreateObjectActionPerObjectMetadataId: {
'object-1': 1,
},
},
},
},
{
title:
'should aggregate multiple field actions for the same object when no create object action exists',
context: {
input: {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'lastName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-3',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-3',
name: 'email',
}),
],
} satisfies CreateFieldAction,
],
update: [],
delete: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {
'object-1': 1,
},
expectCreateObjectActionPerObjectMetadataId: {},
},
},
},
{
title: 'should handle empty actions report',
context: {
input: createEmptyOrchestratorActionsReport(),
expected: {
expectCreateFieldActionPerObjectMetadataId: {},
expectCreateObjectActionPerObjectMetadataId: {},
},
},
},
{
title:
'should not merge create_field actions containing relation fields into create_object',
context: {
input: {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
create: [
{
type: 'create',
metadataName: 'objectMetadata',
flatEntity: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'attachment',
namePlural: 'attachments',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
update: [],
delete: [],
},
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.RELATION,
id: 'field-1',
name: 'author',
}),
],
} satisfies CreateFieldAction,
],
update: [],
delete: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {
'object-1': 1,
},
expectCreateObjectActionPerObjectMetadataId: {
'object-1': 1,
},
},
},
},
];
test.each(eachTestingContextFilter(testCases))(
'$title',
({ context: { input, expected } }) => {
const result =
aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions({
orchestratorActionsReport: input,
});
const fieldActions = result.fieldMetadata.create as CreateFieldAction[];
const fieldActionCounts = fieldActions.reduce(
(acc, action) => {
acc[action.objectMetadataId] =
(acc[action.objectMetadataId] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const objectActions = result.objectMetadata
.create as CreateObjectAction[];
const objectActionCounts = objectActions.reduce(
(acc, action) => {
acc[action.flatEntity.id] = (acc[action.flatEntity.id] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
Object.entries(
expected.expectCreateFieldActionPerObjectMetadataId,
).forEach(([objectId, expectedCount]) => {
expect(fieldActionCounts[objectId]).toBe(expectedCount);
});
Object.entries(
expected.expectCreateObjectActionPerObjectMetadataId,
).forEach(([objectId, expectedCount]) => {
expect(objectActionCounts[objectId]).toBe(expectedCount);
});
// Check total counts
const expectedTotalFieldActions = Object.values(
expected.expectCreateFieldActionPerObjectMetadataId,
).reduce((sum, count) => sum + count, 0);
const expectedTotalObjectActions = Object.values(
expected.expectCreateObjectActionPerObjectMetadataId,
).reduce((sum, count) => sum + count, 0);
expect(fieldActions).toHaveLength(expectedTotalFieldActions);
expect(objectActions).toHaveLength(expectedTotalObjectActions);
},
);
});
@@ -8,7 +8,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-actions-report.constant';
import { aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util';
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type UniversalUpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions', () => {
it('should move searchVector update actions to the end of the update list', () => {
@@ -20,7 +20,7 @@ describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldA
{
type: 'update',
metadataName: 'fieldMetadata',
entityId: 'search-vector-field-1',
universalIdentifier: 'search-vector-field-1',
updates: [
{
property: 'label',
@@ -28,11 +28,11 @@ describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldA
to: 'Updated Search Vector',
},
],
} satisfies UpdateFieldAction,
} satisfies UniversalUpdateFieldAction,
{
type: 'update',
metadataName: 'fieldMetadata',
entityId: 'regular-field-1',
universalIdentifier: 'regular-field-1',
updates: [
{
property: 'label',
@@ -40,11 +40,11 @@ describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldA
to: 'Updated First Name',
},
],
} satisfies UpdateFieldAction,
} satisfies UniversalUpdateFieldAction,
{
type: 'update',
metadataName: 'fieldMetadata',
entityId: 'regular-field-2',
universalIdentifier: 'regular-field-2',
updates: [
{
property: 'label',
@@ -52,7 +52,7 @@ describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldA
to: 'Updated Last Name',
},
],
} satisfies UpdateFieldAction,
} satisfies UniversalUpdateFieldAction,
],
delete: [],
},
@@ -98,11 +98,13 @@ describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldA
);
const updateFieldActions = result.fieldMetadata
.update as UpdateFieldAction[];
.update as UniversalUpdateFieldAction[];
const actualEntityIds = updateFieldActions.map((action) => action.entityId);
const actualUniversalIdentifiers = updateFieldActions.map(
(action) => action.universalIdentifier,
);
expect(actualEntityIds).toEqual([
expect(actualUniversalIdentifiers).toEqual([
'regular-field-1',
'regular-field-2',
'search-vector-field-1',
@@ -0,0 +1,282 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-actions-report.constant';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { aggregateRelationFieldPairs } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-relation-field-pairs.util';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
describe('aggregateRelationFieldPairs', () => {
it('should bundle relation field pairs into single action with merged fieldIdByUniversalIdentifier', () => {
const attachmentObjectId = 'attachment-object';
const taskObjectId = 'task-object';
const targetTaskFieldId = 'target-task-field';
const attachmentsFieldId = 'attachments-field';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: targetTaskFieldId,
objectMetadataId: 'attachment-object-metadata-id',
objectMetadataUniversalIdentifier: attachmentObjectId,
type: FieldMetadataType.RELATION,
name: 'targetTask',
relationTargetFieldMetadataUniversalIdentifier:
attachmentsFieldId,
relationTargetObjectMetadataUniversalIdentifier: taskObjectId,
}),
],
fieldIdByUniversalIdentifier: {
[targetTaskFieldId]: 'target-task-generated-id',
},
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: attachmentsFieldId,
objectMetadataId: 'task-object-metadata-id',
objectMetadataUniversalIdentifier: taskObjectId,
type: FieldMetadataType.RELATION,
name: 'attachments',
relationTargetFieldMetadataUniversalIdentifier:
targetTaskFieldId,
relationTargetObjectMetadataUniversalIdentifier:
attachmentObjectId,
}),
],
fieldIdByUniversalIdentifier: {
[attachmentsFieldId]: 'attachments-generated-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateRelationFieldPairs({
orchestratorActionsReport: input,
});
// Should be bundled into a single action with both fields and merged ID map
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: targetTaskFieldId },
{ universalIdentifier: attachmentsFieldId },
],
fieldIdByUniversalIdentifier: {
[targetTaskFieldId]: 'target-task-generated-id',
[attachmentsFieldId]: 'attachments-generated-id',
},
},
]);
});
it('should keep standalone fields (no relation target) as separate actions', () => {
const fieldUniversalId = 'standalone-field';
const objectUniversalId = 'some-object';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'some-object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'standaloneField',
}),
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'standalone-field-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateRelationFieldPairs({
orchestratorActionsReport: input,
});
// Should remain as single action
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: fieldUniversalId },
],
},
]);
});
it('should handle relation field with target not being created (existing field)', () => {
const relationFieldId = 'new-relation-field';
const existingTargetFieldId = 'existing-target-field';
const objectUniversalId = 'some-object';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: relationFieldId,
objectMetadataId: 'some-object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationToExisting',
relationTargetFieldMetadataUniversalIdentifier:
existingTargetFieldId,
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
],
fieldIdByUniversalIdentifier: {
[relationFieldId]: 'new-relation-field-id',
},
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateRelationFieldPairs({
orchestratorActionsReport: input,
});
// Should remain as single action (target not being created)
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [{ universalIdentifier: relationFieldId }],
},
]);
});
it('should handle multiple independent relation pairs', () => {
const pair1FieldA = 'pair1-field-a';
const pair1FieldB = 'pair1-field-b';
const pair2FieldA = 'pair2-field-a';
const pair2FieldB = 'pair2-field-b';
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
fieldMetadata: {
create: [
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair1FieldA,
objectMetadataId: 'object-1-metadata-id',
objectMetadataUniversalIdentifier: 'object-1',
type: FieldMetadataType.RELATION,
name: 'pair1FieldA',
relationTargetFieldMetadataUniversalIdentifier: pair1FieldB,
relationTargetObjectMetadataUniversalIdentifier: 'object-2',
}),
],
fieldIdByUniversalIdentifier: { [pair1FieldA]: 'id-1a' },
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair1FieldB,
objectMetadataId: 'object-2-metadata-id',
objectMetadataUniversalIdentifier: 'object-2',
type: FieldMetadataType.RELATION,
name: 'pair1FieldB',
relationTargetFieldMetadataUniversalIdentifier: pair1FieldA,
relationTargetObjectMetadataUniversalIdentifier: 'object-1',
}),
],
fieldIdByUniversalIdentifier: { [pair1FieldB]: 'id-1b' },
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair2FieldA,
objectMetadataId: 'object-3-metadata-id',
objectMetadataUniversalIdentifier: 'object-3',
type: FieldMetadataType.RELATION,
name: 'pair2FieldA',
relationTargetFieldMetadataUniversalIdentifier: pair2FieldB,
relationTargetObjectMetadataUniversalIdentifier: 'object-4',
}),
],
fieldIdByUniversalIdentifier: { [pair2FieldA]: 'id-2a' },
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair2FieldB,
objectMetadataId: 'object-4-metadata-id',
objectMetadataUniversalIdentifier: 'object-4',
type: FieldMetadataType.RELATION,
name: 'pair2FieldB',
relationTargetFieldMetadataUniversalIdentifier: pair2FieldA,
relationTargetObjectMetadataUniversalIdentifier: 'object-3',
}),
],
fieldIdByUniversalIdentifier: { [pair2FieldB]: 'id-2b' },
} satisfies UniversalCreateFieldAction,
],
update: [],
delete: [],
},
};
const result = aggregateRelationFieldPairs({
orchestratorActionsReport: input,
});
// Should result in 2 bundled actions (one per pair), each with merged fieldIdByUniversalIdentifier
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: pair1FieldA },
{ universalIdentifier: pair1FieldB },
],
fieldIdByUniversalIdentifier: {
[pair1FieldA]: 'id-1a',
[pair1FieldB]: 'id-1b',
},
},
{
universalFlatFieldMetadatas: [
{ universalIdentifier: pair2FieldA },
{ universalIdentifier: pair2FieldB },
],
fieldIdByUniversalIdentifier: {
[pair2FieldA]: 'id-2a',
[pair2FieldB]: 'id-2b',
},
},
]);
});
});
@@ -0,0 +1,106 @@
import { isDefined } from 'twenty-shared/utils';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { mergeFieldIdByUniversalIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
export const aggregateNonRelationFieldsIntoObjectActions = ({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
const createObjectActions = orchestratorActionsReport.objectMetadata.create;
const createFieldActions = orchestratorActionsReport.fieldMetadata.create;
const createObjectActionByObjectUniversalIdentifier = new Map<
string,
UniversalCreateObjectAction
>();
for (const createObjectAction of createObjectActions) {
createObjectActionByObjectUniversalIdentifier.set(
createObjectAction.flatEntity.universalIdentifier,
createObjectAction,
);
}
const remainingCreateFieldActions: UniversalCreateFieldAction[] = [];
for (const createFieldAction of createFieldActions) {
const nonRelationFields: UniversalFlatFieldMetadata[] = [];
const relationFields: UniversalFlatFieldMetadata[] = [];
for (const field of createFieldAction.universalFlatFieldMetadatas) {
if (isMorphOrRelationUniversalFlatFieldMetadata(field)) {
relationFields.push(field);
} else {
nonRelationFields.push(field);
}
}
const nonRelationFieldsByObjectUniversalIdentifier = new Map<
string,
UniversalFlatFieldMetadata[]
>();
for (const field of nonRelationFields) {
const objectUniversalId = field.objectMetadataUniversalIdentifier;
const existing =
nonRelationFieldsByObjectUniversalIdentifier.get(objectUniversalId);
if (isDefined(existing)) {
existing.push(field);
} else {
nonRelationFieldsByObjectUniversalIdentifier.set(objectUniversalId, [
field,
]);
}
}
const unmergedNonRelationFields: UniversalFlatFieldMetadata[] = [];
for (const [
objectUniversalId,
fields,
] of nonRelationFieldsByObjectUniversalIdentifier) {
const matchingObjectAction =
createObjectActionByObjectUniversalIdentifier.get(objectUniversalId);
if (isDefined(matchingObjectAction)) {
matchingObjectAction.universalFlatFieldMetadatas.push(...fields);
matchingObjectAction.fieldIdByUniversalIdentifier =
mergeFieldIdByUniversalIdentifier(
matchingObjectAction.fieldIdByUniversalIdentifier,
createFieldAction.fieldIdByUniversalIdentifier,
);
} else {
unmergedNonRelationFields.push(...fields);
}
}
const remainingFields = [...unmergedNonRelationFields, ...relationFields];
if (remainingFields.length > 0) {
remainingCreateFieldActions.push({
...createFieldAction,
universalFlatFieldMetadatas: remainingFields,
});
}
}
return {
...orchestratorActionsReport,
objectMetadata: {
...orchestratorActionsReport.objectMetadata,
create: Array.from(
createObjectActionByObjectUniversalIdentifier.values(),
),
},
fieldMetadata: {
...orchestratorActionsReport.fieldMetadata,
create: remainingCreateFieldActions,
},
};
};
@@ -1,189 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type CreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
type AggregatedActions = {
createdFieldActionByObjectMetadataId: Record<string, CreateFieldAction>;
createdObjectActionByObjectMetadataId: Record<string, CreateObjectAction>;
};
export const aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions =
({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
const initialCreatedObjectActionByObjectMetadataId = (
orchestratorActionsReport.objectMetadata.create as CreateObjectAction[]
).reduce(
(acc, createObjectAction) => ({
...acc,
[createObjectAction.flatEntity.id]: createObjectAction,
}),
{} as Record<string, CreateObjectAction>,
);
const initialAccumulator: AggregatedActions = {
createdFieldActionByObjectMetadataId: {},
createdObjectActionByObjectMetadataId:
initialCreatedObjectActionByObjectMetadataId,
};
const {
createdFieldActionByObjectMetadataId,
createdObjectActionByObjectMetadataId,
} = (
orchestratorActionsReport.fieldMetadata.create as CreateFieldAction[]
).reduce<AggregatedActions>(
(
{
createdObjectActionByObjectMetadataId,
createdFieldActionByObjectMetadataId,
},
createFieldAction,
) => {
const existingCreateObjectAction =
createdObjectActionByObjectMetadataId[
createFieldAction.objectMetadataId
];
const existingCreateFieldAction =
createdFieldActionByObjectMetadataId[
createFieldAction.objectMetadataId
];
const initialAccumulator: {
morphOrRelationFlatFieldMetadatas: FlatFieldMetadata[];
otherFlatFieldMetadatas: FlatFieldMetadata[];
} = {
morphOrRelationFlatFieldMetadatas: [],
otherFlatFieldMetadatas: [],
};
const { morphOrRelationFlatFieldMetadatas, otherFlatFieldMetadatas } =
createFieldAction.flatFieldMetadatas.reduce(
(acc, flatFieldMetadata) => {
if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) {
return {
...acc,
morphOrRelationFlatFieldMetadatas: [
...acc.morphOrRelationFlatFieldMetadatas,
flatFieldMetadata,
],
};
}
return {
...acc,
otherFlatFieldMetadatas: [
...acc.otherFlatFieldMetadatas,
flatFieldMetadata,
],
};
},
initialAccumulator,
);
if (isDefined(existingCreateObjectAction)) {
if (isDefined(existingCreateFieldAction)) {
return {
createdObjectActionByObjectMetadataId: {
...createdObjectActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...existingCreateObjectAction,
flatFieldMetadatas: [
...existingCreateObjectAction.flatFieldMetadatas,
...otherFlatFieldMetadatas,
],
},
},
createdFieldActionByObjectMetadataId: {
...createdFieldActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...existingCreateFieldAction,
flatFieldMetadatas: [
...existingCreateFieldAction.flatFieldMetadatas,
...morphOrRelationFlatFieldMetadatas,
],
},
},
};
}
if (morphOrRelationFlatFieldMetadatas.length > 0) {
return {
createdObjectActionByObjectMetadataId: {
...createdObjectActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...existingCreateObjectAction,
flatFieldMetadatas: [
...existingCreateObjectAction.flatFieldMetadatas,
...otherFlatFieldMetadatas,
],
},
},
createdFieldActionByObjectMetadataId: {
...createdFieldActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...createFieldAction,
flatFieldMetadatas: morphOrRelationFlatFieldMetadatas,
},
},
};
}
return {
createdObjectActionByObjectMetadataId: {
...createdObjectActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...existingCreateObjectAction,
flatFieldMetadatas: [
...existingCreateObjectAction.flatFieldMetadatas,
...otherFlatFieldMetadatas,
],
},
},
createdFieldActionByObjectMetadataId,
};
}
if (isDefined(existingCreateFieldAction)) {
return {
createdFieldActionByObjectMetadataId: {
...createdFieldActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...existingCreateFieldAction,
flatFieldMetadatas: [
...existingCreateFieldAction.flatFieldMetadatas,
...createFieldAction.flatFieldMetadatas,
],
},
},
createdObjectActionByObjectMetadataId,
};
}
return {
createdFieldActionByObjectMetadataId: {
...createdFieldActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: createFieldAction,
},
createdObjectActionByObjectMetadataId,
};
},
initialAccumulator,
);
return {
...orchestratorActionsReport,
fieldMetadata: {
...orchestratorActionsReport.fieldMetadata,
create: Object.values(createdFieldActionByObjectMetadataId),
},
objectMetadata: {
...orchestratorActionsReport.objectMetadata,
create: Object.values(createdObjectActionByObjectMetadataId),
},
};
};
@@ -1,10 +1,10 @@
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type UniversalUpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
export const aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions =
({
@@ -13,22 +13,21 @@ export const aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFie
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
assertIsDefinedOrThrow(flatFieldMetadataMaps);
const updateFieldActions = orchestratorActionsReport.fieldMetadata
.update as UpdateFieldAction[];
const updateFieldActions = orchestratorActionsReport.fieldMetadata.update;
const { searchVectorUpdateFieldActions, otherUpdateFieldActions } =
updateFieldActions.reduce<{
searchVectorUpdateFieldActions: UpdateFieldAction[];
otherUpdateFieldActions: UpdateFieldAction[];
searchVectorUpdateFieldActions: UniversalUpdateFieldAction[];
otherUpdateFieldActions: UniversalUpdateFieldAction[];
}>(
(acc, updateFieldAction) => {
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
const flatFieldMetadata = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityId: updateFieldAction.entityId,
universalIdentifier: updateFieldAction.universalIdentifier,
});
const isSearchVectorUpdateFieldAction =
flatFieldMetadata?.name === SEARCH_VECTOR_FIELD.name;
flatFieldMetadata.name === SEARCH_VECTOR_FIELD.name;
if (isSearchVectorUpdateFieldAction) {
return {
@@ -1,13 +1,15 @@
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util';
import { aggregateNonRelationFieldsIntoObjectActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-non-relation-fields-into-object-actions.util';
import { aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util';
import { aggregateRelationFieldPairs } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-relation-field-pairs.util';
export const aggregateOrchestratorActionsReport = ({
orchestratorActionsReport,
flatFieldMetadataMaps,
}: AggregateOrchestratorActionsReportArgs) => {
const aggregatedOrchestratorActionsReport = [
aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions,
aggregateNonRelationFieldsIntoObjectActions,
aggregateRelationFieldPairs,
aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions,
].reduce(
(currentOrchestratorActionsReport, aggregator) =>
@@ -0,0 +1,84 @@
import { isDefined } from 'twenty-shared/utils';
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { mergeFieldIdByUniversalIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
type FieldWithActionContext = {
field: UniversalFlatFieldMetadata;
fieldIdByUniversalIdentifier: Record<string, string> | undefined;
};
export const aggregateRelationFieldPairs = ({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
const createFieldActions = orchestratorActionsReport.fieldMetadata.create;
const fieldByUniversalIdentifier = new Map<string, FieldWithActionContext>();
for (const action of createFieldActions) {
for (const field of action.universalFlatFieldMetadatas) {
fieldByUniversalIdentifier.set(field.universalIdentifier, {
field,
fieldIdByUniversalIdentifier: action.fieldIdByUniversalIdentifier,
});
}
}
const processedFieldUniversalIdentifiers = new Set<string>();
const aggregatedCreateFieldActions: UniversalCreateFieldAction[] = [];
for (const [
universalIdentifier,
fieldContext,
] of fieldByUniversalIdentifier) {
if (processedFieldUniversalIdentifiers.has(universalIdentifier)) {
continue;
}
const { field, fieldIdByUniversalIdentifier } = fieldContext;
processedFieldUniversalIdentifiers.add(universalIdentifier);
const fieldsToBundle: UniversalFlatFieldMetadata[] = [field];
let mergedFieldIdMap = fieldIdByUniversalIdentifier;
const targetUniversalIdentifier =
field.relationTargetFieldMetadataUniversalIdentifier;
if (isDefined(targetUniversalIdentifier)) {
const targetFieldContext = fieldByUniversalIdentifier.get(
targetUniversalIdentifier,
);
if (
isDefined(targetFieldContext) &&
!processedFieldUniversalIdentifiers.has(targetUniversalIdentifier)
) {
fieldsToBundle.push(targetFieldContext.field);
processedFieldUniversalIdentifiers.add(targetUniversalIdentifier);
mergedFieldIdMap = mergeFieldIdByUniversalIdentifier(
mergedFieldIdMap,
targetFieldContext.fieldIdByUniversalIdentifier,
);
}
}
aggregatedCreateFieldActions.push({
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: fieldsToBundle,
fieldIdByUniversalIdentifier: mergedFieldIdMap,
});
}
return {
...orchestratorActionsReport,
fieldMetadata: {
...orchestratorActionsReport.fieldMetadata,
create: aggregatedCreateFieldActions,
},
};
};
@@ -22,6 +22,8 @@ export type DeletedCreatedUpdatedMatrix<T extends AllMetadataName> = {
string,
{
updates: FlatEntityPropertiesUpdates<T>;
// TMP remove when maps is universal based
universalIdentifier: string;
}
>;
};
@@ -104,6 +106,7 @@ export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
}
initialDispatcher.updatedFlatEntityMaps.byId[toFlatEntity.id] = {
universalIdentifier: fromFlatEntity.universalIdentifier,
updates,
};
}
@@ -1,6 +1,6 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataWorkspaceMigrationActionsRecord } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
import { type MetadataUniversalWorkspaceMigrationActionsRecord } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
export const getMetadataEmptyWorkspaceMigrationActionRecord = <
T extends AllMetadataName,
@@ -11,4 +11,4 @@ export const getMetadataEmptyWorkspaceMigrationActionRecord = <
create: [],
delete: [],
update: [],
}) as MetadataWorkspaceMigrationActionsRecord<T>;
}) as MetadataUniversalWorkspaceMigrationActionsRecord<T>;
@@ -0,0 +1,15 @@
import { isDefined } from 'twenty-shared/utils';
export const mergeFieldIdByUniversalIdentifier = (
existing: Record<string, string> | undefined,
incoming: Record<string, string> | undefined,
): Record<string, string> | undefined => {
if (!isDefined(existing) && !isDefined(incoming)) {
return undefined;
}
return {
...existing,
...incoming,
};
};