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:
+423
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
+254
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
+111
@@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
+63
@@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
+23
@@ -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 };
|
||||
};
|
||||
-56
@@ -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;
|
||||
};
|
||||
+83
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user