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
This commit is contained in:
Paul Rastoin
2025-10-02 18:07:34 +02:00
committed by GitHub
parent d4b83df3f5
commit 170f6d27c5
240 changed files with 11385 additions and 17744 deletions
@@ -0,0 +1,423 @@
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 { EMPTY_ORCHESTRATOR_ACTIONS_REPORT } from 'src/engine/workspace-manager/workspace-migration-v2/constant/empty-orchestrator-actions-report.constant';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util';
import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type CreateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
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: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [
{
type: 'create_object',
flatObjectMetadata: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'user',
namePlural: 'users',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
updated: [],
deleted: [],
},
fieldMetadata: {
created: [
{
type: 'create_field',
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,
],
updated: [],
deleted: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {},
expectCreateObjectActionPerObjectMetadataId: {
'object-1': 1,
},
},
},
},
{
title:
'should keep separate field actions when no matching object action exists',
context: {
input: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [],
updated: [],
deleted: [],
},
fieldMetadata: {
created: [
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'secondName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-3',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-3',
name: 'lastName',
}),
],
} satisfies CreateFieldAction,
],
updated: [],
deleted: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {
'object-1': 1,
},
expectCreateObjectActionPerObjectMetadataId: {},
},
},
},
{
title: 'should handle multiple objects with their respective fields',
context: {
input: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [
{
type: 'create_object',
flatObjectMetadata: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'user',
namePlural: 'users',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
{
type: 'create_object',
flatObjectMetadata: getFlatObjectMetadataMock({
universalIdentifier: 'object-2',
id: 'object-2',
nameSingular: 'company',
namePlural: 'companies',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
updated: [],
deleted: [],
},
fieldMetadata: {
created: [
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create_field',
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,
],
updated: [],
deleted: [],
},
} 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: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [
{
type: 'create_object',
flatObjectMetadata: getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
id: 'object-1',
nameSingular: 'user',
namePlural: 'users',
}),
flatFieldMetadatas: [],
} satisfies CreateObjectAction,
],
updated: [],
deleted: [],
},
fieldMetadata: {
created: [
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create_field',
objectMetadataId: 'object-2',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-2',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'orphanField',
}),
],
} satisfies CreateFieldAction,
],
updated: [],
deleted: [],
},
} 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: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [],
updated: [],
deleted: [],
},
fieldMetadata: {
created: [
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-1',
name: 'firstName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-2',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-2',
name: 'lastName',
}),
],
} satisfies CreateFieldAction,
{
type: 'create_field',
objectMetadataId: 'object-1',
flatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: 'field-3',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
id: 'field-3',
name: 'email',
}),
],
} satisfies CreateFieldAction,
],
updated: [],
deleted: [],
},
} satisfies OrchestratorActionsReport,
expected: {
expectCreateFieldActionPerObjectMetadataId: {
'object-1': 1,
},
expectCreateObjectActionPerObjectMetadataId: {},
},
},
},
{
title: 'should handle empty actions report',
context: {
input: EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
expected: {
expectCreateFieldActionPerObjectMetadataId: {},
expectCreateObjectActionPerObjectMetadataId: {},
},
},
},
];
test.each(eachTestingContextFilter(testCases))(
'$title',
({ context: { input, expected } }) => {
const result =
aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions({
orchestratorActionsReport: input,
});
const fieldActions = result.fieldMetadata.created 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
.created as CreateObjectAction[];
const objectActionCounts = objectActions.reduce(
(acc, action) => {
acc[action.flatObjectMetadata.id] =
(acc[action.flatObjectMetadata.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);
},
);
});
@@ -0,0 +1,254 @@
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { EMPTY_ORCHESTRATOR_ACTIONS_REPORT } from 'src/engine/workspace-manager/workspace-migration-v2/constant/empty-orchestrator-actions-report.constant';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util';
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
type DeleteAggregationTestCase = EachTestingContext<{
input: OrchestratorActionsReport;
expected: {
expectDeleteFieldActionPerObjectMetadataId: Record<string, number>;
expectDeleteObjectActionPerObjectMetadataId: Record<string, number>;
};
}>;
describe('aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions', () => {
const testCases: DeleteAggregationTestCase[] = [
{
title: 'should remove field actions when parent object is being deleted',
context: {
input: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_object',
objectMetadataId: 'object-1',
} satisfies DeleteObjectAction,
],
},
fieldMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_field',
fieldMetadataId: 'field-1',
objectMetadataId: 'object-1',
} satisfies DeleteFieldAction,
{
type: 'delete_field',
fieldMetadataId: 'field-2',
objectMetadataId: 'object-1',
} satisfies DeleteFieldAction,
],
},
} satisfies OrchestratorActionsReport,
expected: {
expectDeleteFieldActionPerObjectMetadataId: {},
expectDeleteObjectActionPerObjectMetadataId: {
'object-1': 1,
},
},
},
},
{
title: 'should keep field actions when no parent object is being deleted',
context: {
input: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [],
updated: [],
deleted: [],
},
fieldMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_field',
fieldMetadataId: 'field-1',
objectMetadataId: 'object-1',
} satisfies DeleteFieldAction,
],
},
} satisfies OrchestratorActionsReport,
expected: {
expectDeleteFieldActionPerObjectMetadataId: {
'object-1': 1,
},
expectDeleteObjectActionPerObjectMetadataId: {},
},
},
},
{
title:
'should handle mixed scenario with some fields removed and some kept',
context: {
input: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_object',
objectMetadataId: 'object-1',
} satisfies DeleteObjectAction,
],
},
fieldMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_field',
fieldMetadataId: 'field-1',
objectMetadataId: 'object-1',
} satisfies DeleteFieldAction,
{
type: 'delete_field',
fieldMetadataId: 'field-2',
objectMetadataId: 'object-2',
} satisfies DeleteFieldAction,
],
},
} satisfies OrchestratorActionsReport,
expected: {
expectDeleteFieldActionPerObjectMetadataId: {
'object-2': 1,
},
expectDeleteObjectActionPerObjectMetadataId: {
'object-1': 1,
},
},
},
},
{
title: 'should handle multiple objects with mixed field deletions',
context: {
input: {
...EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
objectMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_object',
objectMetadataId: 'object-1',
} satisfies DeleteObjectAction,
{
type: 'delete_object',
objectMetadataId: 'object-2',
} satisfies DeleteObjectAction,
],
},
fieldMetadata: {
created: [],
updated: [],
deleted: [
{
type: 'delete_field',
fieldMetadataId: 'field-1',
objectMetadataId: 'object-1',
} satisfies DeleteFieldAction,
{
type: 'delete_field',
fieldMetadataId: 'field-2',
objectMetadataId: 'object-2',
} satisfies DeleteFieldAction,
{
type: 'delete_field',
fieldMetadataId: 'field-3',
objectMetadataId: 'object-3',
} satisfies DeleteFieldAction,
],
},
} satisfies OrchestratorActionsReport,
expected: {
expectDeleteFieldActionPerObjectMetadataId: {
'object-3': 1,
},
expectDeleteObjectActionPerObjectMetadataId: {
'object-1': 1,
'object-2': 1,
},
},
},
},
{
title: 'should handle empty actions report',
context: {
input: EMPTY_ORCHESTRATOR_ACTIONS_REPORT,
expected: {
expectDeleteFieldActionPerObjectMetadataId: {},
expectDeleteObjectActionPerObjectMetadataId: {},
},
},
},
];
test.each(eachTestingContextFilter(testCases))(
'$title',
({ context: { input, expected } }) => {
const result =
aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions({
orchestratorActionsReport: input,
});
const fieldActions = result.fieldMetadata.deleted as DeleteFieldAction[];
const fieldActionCounts = fieldActions.reduce(
(acc, action) => {
acc[action.objectMetadataId] =
(acc[action.objectMetadataId] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const objectActions = result.objectMetadata
.deleted as DeleteObjectAction[];
const objectActionCounts = objectActions.reduce(
(acc, action) => {
acc[action.objectMetadataId] =
(acc[action.objectMetadataId] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
Object.entries(
expected.expectDeleteFieldActionPerObjectMetadataId,
).forEach(([objectId, expectedCount]) => {
expect(fieldActionCounts[objectId]).toBe(expectedCount);
});
Object.entries(
expected.expectDeleteObjectActionPerObjectMetadataId,
).forEach(([objectId, expectedCount]) => {
expect(objectActionCounts[objectId]).toBe(expectedCount);
});
// Check total counts
const expectedTotalFieldActions = Object.values(
expected.expectDeleteFieldActionPerObjectMetadataId,
).reduce((sum, count) => sum + count, 0);
const expectedTotalObjectActions = Object.values(
expected.expectDeleteObjectActionPerObjectMetadataId,
).reduce((sum, count) => sum + count, 0);
expect(fieldActions).toHaveLength(expectedTotalFieldActions);
expect(objectActions).toHaveLength(expectedTotalObjectActions);
},
);
});
@@ -0,0 +1,111 @@
import { isDefined } from 'twenty-shared/utils';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type CreateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
type AggregateOrchestratorActionsReportCreateObjectAndCreateFieldActionsArgs = {
orchestratorActionsReport: OrchestratorActionsReport;
};
type AggregatedActions = {
createdFieldActionByObjectMetadataId: Record<string, CreateFieldAction>;
createdObjectActionByObjectMetadataId: Record<string, CreateObjectAction>;
};
export const aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions =
({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportCreateObjectAndCreateFieldActionsArgs): OrchestratorActionsReport => {
const initialCreatedObjectActionByObjectMetadataId = (
orchestratorActionsReport.objectMetadata.created as CreateObjectAction[]
).reduce(
(acc, createObjectAction) => ({
...acc,
[createObjectAction.flatObjectMetadata.id]: createObjectAction,
}),
{},
);
const initialAccumulator: AggregatedActions = {
createdFieldActionByObjectMetadataId: {},
createdObjectActionByObjectMetadataId:
initialCreatedObjectActionByObjectMetadataId,
};
const {
createdFieldActionByObjectMetadataId,
createdObjectActionByObjectMetadataId,
} = (
orchestratorActionsReport.fieldMetadata.created as CreateFieldAction[]
).reduce<AggregatedActions>(
(
{
createdObjectActionByObjectMetadataId,
createdFieldActionByObjectMetadataId,
},
createFieldAction,
) => {
const existingCreateObjectAction =
createdObjectActionByObjectMetadataId[
createFieldAction.objectMetadataId
];
if (isDefined(existingCreateObjectAction)) {
return {
createdObjectActionByObjectMetadataId: {
...createdObjectActionByObjectMetadataId,
[createFieldAction.objectMetadataId]: {
...existingCreateObjectAction,
flatFieldMetadatas: [
...existingCreateObjectAction.flatFieldMetadatas,
...createFieldAction.flatFieldMetadatas,
],
},
},
createdFieldActionByObjectMetadataId,
};
}
const existingCreateFieldAction =
createdFieldActionByObjectMetadataId[
createFieldAction.objectMetadataId
];
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,
created: Object.values(createdFieldActionByObjectMetadataId),
},
objectMetadata: {
...orchestratorActionsReport.objectMetadata,
created: Object.values(createdObjectActionByObjectMetadataId),
},
};
};
@@ -0,0 +1,63 @@
import { isDefined } from 'twenty-shared/utils';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
type AggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActionsArgs = {
orchestratorActionsReport: OrchestratorActionsReport;
};
type AggregatedActions = {
deleteFieldActionByFieldMetadataId: Record<string, DeleteFieldAction>;
};
export const aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions =
({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActionsArgs): OrchestratorActionsReport => {
const deleteObjectActionByObjectMetadataId = (
orchestratorActionsReport.objectMetadata.deleted as DeleteObjectAction[]
).reduce<Record<string, DeleteObjectAction>>(
(acc, deleteObjectAction) => ({
...acc,
[deleteObjectAction.objectMetadataId]: deleteObjectAction,
}),
{},
);
const initialAccumulator: AggregatedActions = {
deleteFieldActionByFieldMetadataId: {},
};
const { deleteFieldActionByFieldMetadataId } = (
orchestratorActionsReport.fieldMetadata.deleted as DeleteFieldAction[]
).reduce<AggregatedActions>(
({ deleteFieldActionByFieldMetadataId }, deleteFieldAction) => {
const fieldParentObjectDeleteObjectAction =
deleteObjectActionByObjectMetadataId[
deleteFieldAction.objectMetadataId
];
if (isDefined(fieldParentObjectDeleteObjectAction)) {
return {
deleteFieldActionByFieldMetadataId,
};
}
return {
deleteFieldActionByFieldMetadataId: {
...deleteFieldActionByFieldMetadataId,
[deleteFieldAction.fieldMetadataId]: deleteFieldAction,
},
};
},
initialAccumulator,
);
return {
...orchestratorActionsReport,
fieldMetadata: {
...orchestratorActionsReport.fieldMetadata,
deleted: Object.values(deleteFieldActionByFieldMetadataId),
},
};
};
@@ -0,0 +1,23 @@
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
import { aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util';
import { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util';
type AggregateOrchestratorActionsReportArgs = {
orchestratorActionsReport: OrchestratorActionsReport;
};
export const aggregateOrchestratorActionsReport = ({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportArgs) => {
const aggregatedOrchestratorActionsReport = [
aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions,
aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions,
].reduce(
(currentOrchestratorActionsReport, aggregator) =>
aggregator({
orchestratorActionsReport: currentOrchestratorActionsReport,
}),
orchestratorActionsReport,
);
return { aggregatedOrchestratorActionsReport };
};
@@ -1,56 +0,0 @@
import { type FromTo } from 'twenty-shared/types';
export type DeletedCreatedUpdatedMatrix<T> = {
created: T[];
deleted: T[];
updated: FromTo<T>[];
};
export type CustomDeletedCreatedUpdatedMatrix<TLabel extends string, TInput> = {
[P in keyof DeletedCreatedUpdatedMatrix<TInput> as `${P}${Capitalize<TLabel>}`]: DeletedCreatedUpdatedMatrix<TInput>[P];
};
export type UniversalIdentifierItem = {
universalIdentifier: string;
};
export const deletedCreatedUpdatedMatrixDispatcher = <
T extends UniversalIdentifierItem,
>({
from,
to,
}: FromTo<T[]>): DeletedCreatedUpdatedMatrix<T> => {
const initialDispatcher: DeletedCreatedUpdatedMatrix<T> = {
created: [],
updated: [],
deleted: [],
};
const fromMap = new Map(from.map((obj) => [obj.universalIdentifier, obj]));
const toMap = new Map(to.map((obj) => [obj.universalIdentifier, obj]));
for (const [identifier, fromObj] of fromMap) {
if (!toMap.has(identifier)) {
initialDispatcher.deleted.push(fromObj);
}
}
for (const [identifier, toObj] of toMap) {
if (!fromMap.has(identifier)) {
initialDispatcher.created.push(toObj);
}
}
for (const [identifier, fromObj] of fromMap) {
const toObj = toMap.get(identifier);
if (toObj) {
initialDispatcher.updated.push({
from: fromObj,
to: toObj,
});
}
}
return initialDispatcher;
};
@@ -0,0 +1,83 @@
import { type FromTo } from 'twenty-shared/types';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { type AllFlatEntities } from 'src/engine/core-modules/common/types/all-flat-entities.type';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
export type DeletedCreatedUpdatedMatrix<T extends AllFlatEntities> = {
createdFlatEntityMaps: FlatEntityMaps<T>;
deletedFlatEntityMaps: FlatEntityMaps<T>;
updatedFlatEntityMaps: FromTo<FlatEntityMaps<T>>;
};
export type UniversalIdentifierItem = {
universalIdentifier: string;
};
export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
T extends AllFlatEntities,
>({
from,
to,
buildOptions,
}: FromTo<T[]> & {
buildOptions: WorkspaceMigrationBuilderOptions;
}): DeletedCreatedUpdatedMatrix<T> => {
const initialDispatcher: DeletedCreatedUpdatedMatrix<T> = {
createdFlatEntityMaps: EMPTY_FLAT_ENTITY_MAPS,
deletedFlatEntityMaps: EMPTY_FLAT_ENTITY_MAPS,
updatedFlatEntityMaps: {
from: EMPTY_FLAT_ENTITY_MAPS,
to: EMPTY_FLAT_ENTITY_MAPS,
},
};
const fromMap = new Map(from.map((obj) => [obj.universalIdentifier, obj]));
const toMap = new Map(to.map((obj) => [obj.universalIdentifier, obj]));
if (buildOptions.inferDeletionFromMissingEntities) {
for (const [universalIdentifier, fromEntity] of fromMap) {
if (!toMap.has(universalIdentifier)) {
initialDispatcher.deletedFlatEntityMaps =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: fromEntity,
flatEntityMaps: initialDispatcher.deletedFlatEntityMaps,
});
}
}
}
for (const [universalIdentifier, toFlatEntity] of toMap) {
if (!fromMap.has(universalIdentifier)) {
initialDispatcher.createdFlatEntityMaps =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: toFlatEntity,
flatEntityMaps: initialDispatcher.createdFlatEntityMaps,
});
}
}
for (const [universalIdentifier, fromFlatEntity] of fromMap) {
const toFlatEntity = toMap.get(universalIdentifier);
// TODO Perf improvement compare directly here to avoid mapping the whole workspaces + avoid compare duplication
// Would not have FromTo anymore but an PropertyUpdates[]
if (toFlatEntity) {
initialDispatcher.updatedFlatEntityMaps.from =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: fromFlatEntity,
flatEntityMaps: initialDispatcher.updatedFlatEntityMaps.from,
});
initialDispatcher.updatedFlatEntityMaps.to =
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: toFlatEntity,
flatEntityMaps: initialDispatcher.updatedFlatEntityMaps.to,
});
}
}
return initialDispatcher;
};