Remove sync-metadata and IS_WORKSPACE_CREATION_V2_ENABLED feature flag (#16997)
# Introduction Followup of https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738 close https://github.com/twentyhq/core-team-issues/issues/1910 We've completely decom the `sync-metadata` in production. We're now then removing its implementation in favor of the v2. ## TODO: - [x] Remove sync-metadata implem and commands - [x] Remove workspace decorators - [x] Type each deprecated field to deprecated on their workspaceEntity - [x] Remove the `workspace-sync-metadata` folder entirely - [x] remove workspace migration - [x] workspace migration removal migration - [x] remove the `v2` references from workspace manager file names - [x] remove the `v2` references from workspace manager modules - [ ] Double check impact on translation file path updates ## Note - Removed the gate logic - Remains some service v2 naming, serverless needs to be migrated on v2 fully - Removed workspaceMigration service app health consumption, making it always returning up ( no more down ) cc @FelixMalfait ( quite obsolete health check now, will require complete refactor once we introduce inter app dependency etc )
This commit is contained in:
+275
@@ -0,0 +1,275 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a created entity 1`] = `
|
||||
{
|
||||
"createdFlatEntityMaps": {
|
||||
"byId": {
|
||||
"field-id-1": {
|
||||
"applicationId": "application-id-1",
|
||||
"calendarViewIds": [],
|
||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"icon": "icon",
|
||||
"id": "field-id-1",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
"isLabelSyncedWithName": false,
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": null,
|
||||
"relationTargetObjectMetadataId": null,
|
||||
"settings": null,
|
||||
"standardId": null,
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"universalIdentifier": "universal-identifier-1",
|
||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"workspaceId": "workspace-id-1",
|
||||
},
|
||||
},
|
||||
"idByUniversalIdentifier": {
|
||||
"universal-identifier-1": "field-id-1",
|
||||
},
|
||||
"universalIdentifiersByApplicationId": {
|
||||
"application-id-1": [
|
||||
"universal-identifier-1",
|
||||
],
|
||||
},
|
||||
},
|
||||
"deletedFlatEntityMaps": {
|
||||
"byId": {},
|
||||
"idByUniversalIdentifier": {},
|
||||
"universalIdentifiersByApplicationId": {},
|
||||
},
|
||||
"updatedFlatEntityMaps": {
|
||||
"byId": {},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a deleted entity 1`] = `
|
||||
{
|
||||
"createdFlatEntityMaps": {
|
||||
"byId": {},
|
||||
"idByUniversalIdentifier": {},
|
||||
"universalIdentifiersByApplicationId": {},
|
||||
},
|
||||
"deletedFlatEntityMaps": {
|
||||
"byId": {
|
||||
"field-id-1": {
|
||||
"applicationId": "application-id-1",
|
||||
"calendarViewIds": [],
|
||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"icon": "icon",
|
||||
"id": "field-id-1",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
"isLabelSyncedWithName": false,
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": null,
|
||||
"relationTargetObjectMetadataId": null,
|
||||
"settings": null,
|
||||
"standardId": null,
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"universalIdentifier": "universal-identifier-1",
|
||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"workspaceId": "workspace-id-1",
|
||||
},
|
||||
},
|
||||
"idByUniversalIdentifier": {
|
||||
"universal-identifier-1": "field-id-1",
|
||||
},
|
||||
"universalIdentifiersByApplicationId": {
|
||||
"application-id-1": [
|
||||
"universal-identifier-1",
|
||||
],
|
||||
},
|
||||
},
|
||||
"updatedFlatEntityMaps": {
|
||||
"byId": {},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect an updated entity 1`] = `
|
||||
{
|
||||
"createdFlatEntityMaps": {
|
||||
"byId": {},
|
||||
"idByUniversalIdentifier": {},
|
||||
"universalIdentifiersByApplicationId": {},
|
||||
},
|
||||
"deletedFlatEntityMaps": {
|
||||
"byId": {},
|
||||
"idByUniversalIdentifier": {},
|
||||
"universalIdentifiersByApplicationId": {},
|
||||
},
|
||||
"updatedFlatEntityMaps": {
|
||||
"byId": {
|
||||
"field-id-1": {
|
||||
"updates": [
|
||||
{
|
||||
"from": false,
|
||||
"property": "isActive",
|
||||
"to": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect created, deleted and updated entities 1`] = `
|
||||
{
|
||||
"createdFlatEntityMaps": {
|
||||
"byId": {
|
||||
"field-id-3": {
|
||||
"applicationId": "application-id-1",
|
||||
"calendarViewIds": [],
|
||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"icon": "icon",
|
||||
"id": "field-id-3",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
"isLabelSyncedWithName": false,
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": null,
|
||||
"relationTargetObjectMetadataId": null,
|
||||
"settings": null,
|
||||
"standardId": null,
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"universalIdentifier": "universal-identifier-3",
|
||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"workspaceId": "workspace-id-1",
|
||||
},
|
||||
},
|
||||
"idByUniversalIdentifier": {
|
||||
"universal-identifier-3": "field-id-3",
|
||||
},
|
||||
"universalIdentifiersByApplicationId": {
|
||||
"application-id-1": [
|
||||
"universal-identifier-3",
|
||||
],
|
||||
},
|
||||
},
|
||||
"deletedFlatEntityMaps": {
|
||||
"byId": {
|
||||
"field-id-2": {
|
||||
"applicationId": "application-id-1",
|
||||
"calendarViewIds": [],
|
||||
"createdAt": "2024-01-01T00:00:00.000Z",
|
||||
"defaultValue": null,
|
||||
"description": "default flat field metadata description",
|
||||
"icon": "icon",
|
||||
"id": "field-id-2",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
"isLabelSyncedWithName": false,
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "flat field metadata label",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
"morphId": null,
|
||||
"name": "flatFieldMetadataName",
|
||||
"objectMetadataId": "object-metadata-id-1",
|
||||
"options": null,
|
||||
"relationTargetFieldMetadataId": null,
|
||||
"relationTargetObjectMetadataId": null,
|
||||
"settings": null,
|
||||
"standardId": null,
|
||||
"standardOverrides": null,
|
||||
"type": "TEXT",
|
||||
"universalIdentifier": "universal-identifier-2",
|
||||
"updatedAt": "2024-01-01T00:00:00.000Z",
|
||||
"viewFieldIds": [],
|
||||
"viewFilterIds": [],
|
||||
"workspaceId": "workspace-id-1",
|
||||
},
|
||||
},
|
||||
"idByUniversalIdentifier": {
|
||||
"universal-identifier-2": "field-id-2",
|
||||
},
|
||||
"universalIdentifiersByApplicationId": {
|
||||
"application-id-1": [
|
||||
"universal-identifier-2",
|
||||
],
|
||||
},
|
||||
},
|
||||
"updatedFlatEntityMaps": {
|
||||
"byId": {
|
||||
"field-id-1": {
|
||||
"updates": [
|
||||
{
|
||||
"from": true,
|
||||
"property": "isActive",
|
||||
"to": false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should not detect deleted entities when inferDeletionFromMissingEntities is false 1`] = `
|
||||
{
|
||||
"createdFlatEntityMaps": {
|
||||
"byId": {},
|
||||
"idByUniversalIdentifier": {},
|
||||
"universalIdentifiersByApplicationId": {},
|
||||
},
|
||||
"deletedFlatEntityMaps": {
|
||||
"byId": {},
|
||||
"idByUniversalIdentifier": {},
|
||||
"universalIdentifiersByApplicationId": {},
|
||||
},
|
||||
"updatedFlatEntityMaps": {
|
||||
"byId": {},
|
||||
},
|
||||
}
|
||||
`;
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
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 { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util';
|
||||
import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
|
||||
|
||||
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: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-1',
|
||||
} satisfies DeleteObjectAction,
|
||||
],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: '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: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: '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: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-1',
|
||||
} satisfies DeleteObjectAction,
|
||||
],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: '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: {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
objectMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-1',
|
||||
} satisfies DeleteObjectAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
entityId: 'object-2',
|
||||
} satisfies DeleteObjectAction,
|
||||
],
|
||||
},
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: [
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'field-2',
|
||||
objectMetadataId: 'object-2',
|
||||
} satisfies DeleteFieldAction,
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: '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: createEmptyOrchestratorActionsReport(),
|
||||
expected: {
|
||||
expectDeleteFieldActionPerObjectMetadataId: {},
|
||||
expectDeleteObjectActionPerObjectMetadataId: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(eachTestingContextFilter(testCases))(
|
||||
'$title',
|
||||
({ context: { input, expected } }) => {
|
||||
const result =
|
||||
aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions({
|
||||
orchestratorActionsReport: input,
|
||||
});
|
||||
|
||||
const fieldActions = result.fieldMetadata.delete 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
|
||||
.delete as DeleteObjectAction[];
|
||||
const objectActionCounts = objectActions.reduce(
|
||||
(acc, action) => {
|
||||
acc[action.entityId] = (acc[action.entityId] || 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
|
||||
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
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';
|
||||
|
||||
describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions', () => {
|
||||
it('should move searchVector update actions to the end of the update list', () => {
|
||||
const orchestratorActionsReport = {
|
||||
...createEmptyOrchestratorActionsReport(),
|
||||
fieldMetadata: {
|
||||
create: [],
|
||||
update: [
|
||||
{
|
||||
type: 'update',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'search-vector-field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
updates: [
|
||||
{
|
||||
property: 'label',
|
||||
from: 'Search Vector',
|
||||
to: 'Updated Search Vector',
|
||||
},
|
||||
],
|
||||
} satisfies UpdateFieldAction,
|
||||
{
|
||||
type: 'update',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'regular-field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
updates: [
|
||||
{
|
||||
property: 'label',
|
||||
from: 'First Name',
|
||||
to: 'Updated First Name',
|
||||
},
|
||||
],
|
||||
} satisfies UpdateFieldAction,
|
||||
{
|
||||
type: 'update',
|
||||
metadataName: 'fieldMetadata',
|
||||
entityId: 'regular-field-2',
|
||||
objectMetadataId: 'object-1',
|
||||
updates: [
|
||||
{
|
||||
property: 'label',
|
||||
from: 'Last Name',
|
||||
to: 'Updated Last Name',
|
||||
},
|
||||
],
|
||||
} satisfies UpdateFieldAction,
|
||||
],
|
||||
delete: [],
|
||||
},
|
||||
};
|
||||
|
||||
const flatFieldMetadataMaps = [
|
||||
getFlatFieldMetadataMock({
|
||||
universalIdentifier: 'search-vector-field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
id: 'search-vector-field-1',
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
}),
|
||||
getFlatFieldMetadataMock({
|
||||
universalIdentifier: 'regular-field-1',
|
||||
objectMetadataId: 'object-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
id: 'regular-field-1',
|
||||
name: 'firstName',
|
||||
}),
|
||||
getFlatFieldMetadataMock({
|
||||
universalIdentifier: 'regular-field-2',
|
||||
objectMetadataId: 'object-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
id: 'regular-field-2',
|
||||
name: 'lastName',
|
||||
}),
|
||||
].reduce<FlatEntityMaps<FlatFieldMetadata>>(
|
||||
(flatEntityMaps, field) =>
|
||||
addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity: field,
|
||||
flatEntityMaps,
|
||||
}),
|
||||
createEmptyFlatEntityMaps() as FlatEntityMaps<FlatFieldMetadata>,
|
||||
);
|
||||
|
||||
const result =
|
||||
aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions(
|
||||
{
|
||||
orchestratorActionsReport,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
const updateFieldActions = result.fieldMetadata
|
||||
.update as UpdateFieldAction[];
|
||||
|
||||
const actualEntityIds = updateFieldActions.map((action) => action.entityId);
|
||||
|
||||
expect(actualEntityIds).toEqual([
|
||||
'regular-field-1',
|
||||
'regular-field-2',
|
||||
'search-vector-field-1',
|
||||
]);
|
||||
|
||||
expect(result.fieldMetadata.create).toEqual(
|
||||
orchestratorActionsReport.fieldMetadata.create,
|
||||
);
|
||||
expect(result.fieldMetadata.delete).toEqual(
|
||||
orchestratorActionsReport.fieldMetadata.delete,
|
||||
);
|
||||
expect(result.objectMetadata).toEqual(
|
||||
orchestratorActionsReport.objectMetadata,
|
||||
);
|
||||
});
|
||||
});
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
|
||||
import { flatEntityDeletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration/utils/flat-entity-deleted-created-updated-matrix-dispatcher.util';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
type TestContext = FromTo<MetadataFlatEntity<'fieldMetadata'>[]> & {
|
||||
metadataName: 'fieldMetadata';
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
};
|
||||
|
||||
describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
|
||||
const testCases = [
|
||||
{
|
||||
title: 'It should detect a created entity',
|
||||
context: {
|
||||
from: [],
|
||||
to: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
],
|
||||
metadataName: 'fieldMetadata',
|
||||
buildOptions: {
|
||||
inferDeletionFromMissingEntities: { fieldMetadata: false },
|
||||
isSystemBuild: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'It should detect a deleted entity',
|
||||
context: {
|
||||
from: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
],
|
||||
to: [],
|
||||
metadataName: 'fieldMetadata',
|
||||
buildOptions: {
|
||||
inferDeletionFromMissingEntities: { fieldMetadata: true },
|
||||
isSystemBuild: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'It should detect an updated entity',
|
||||
context: {
|
||||
from: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
isActive: false,
|
||||
}),
|
||||
],
|
||||
to: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
isActive: true,
|
||||
}),
|
||||
],
|
||||
metadataName: 'fieldMetadata',
|
||||
buildOptions: {
|
||||
inferDeletionFromMissingEntities: { fieldMetadata: false },
|
||||
isSystemBuild: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'It should detect created, deleted and updated entities',
|
||||
context: {
|
||||
from: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
isActive: true,
|
||||
}),
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-2',
|
||||
id: 'field-id-2',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
],
|
||||
to: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
isActive: false,
|
||||
}),
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-3',
|
||||
id: 'field-id-3',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
],
|
||||
metadataName: 'fieldMetadata',
|
||||
buildOptions: {
|
||||
inferDeletionFromMissingEntities: { fieldMetadata: true },
|
||||
isSystemBuild: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'It should not detect deleted entities when inferDeletionFromMissingEntities is false',
|
||||
context: {
|
||||
from: [
|
||||
getFlatFieldMetadataMock({
|
||||
objectMetadataId: 'object-metadata-id-1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'universal-identifier-1',
|
||||
id: 'field-id-1',
|
||||
workspaceId: 'workspace-id-1',
|
||||
applicationId: 'application-id-1',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
],
|
||||
to: [],
|
||||
metadataName: 'fieldMetadata',
|
||||
buildOptions: {
|
||||
inferDeletionFromMissingEntities: { fieldMetadata: false },
|
||||
isSystemBuild: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as EachTestingContext<TestContext>[];
|
||||
|
||||
test.each(eachTestingContextFilter(testCases))(
|
||||
'$title',
|
||||
({ context: { from, to, metadataName, buildOptions } }) => {
|
||||
const result = flatEntityDeletedCreatedUpdatedMatrixDispatcher({
|
||||
from: from as any,
|
||||
to: to as any,
|
||||
metadataName,
|
||||
buildOptions,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
import {
|
||||
type CircularDependencyValidationResult,
|
||||
validateFlatEntityCircularDependency,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/utils/validate-flat-entity-circular-dependency.util';
|
||||
|
||||
type TestFlatEntity = SyncableFlatEntity & {
|
||||
parentId: string | null;
|
||||
};
|
||||
|
||||
const createFlatEntityMaps = (
|
||||
entities: TestFlatEntity[],
|
||||
): FlatEntityMaps<TestFlatEntity> => ({
|
||||
byId: Object.fromEntries(entities.map((entity) => [entity.id, entity])),
|
||||
idByUniversalIdentifier: Object.fromEntries(
|
||||
entities.map((entity) => [entity.universalIdentifier, entity.id]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const createTestEntity = (
|
||||
id: string,
|
||||
parentId: string | null = null,
|
||||
): TestFlatEntity => ({
|
||||
id,
|
||||
parentId,
|
||||
universalIdentifier: id,
|
||||
applicationId: 'test-app',
|
||||
workspaceId: 'test-workspace',
|
||||
});
|
||||
|
||||
type TestContext = {
|
||||
flatEntityId: string;
|
||||
flatEntityParentId: string;
|
||||
maxDepth?: number;
|
||||
entities: TestFlatEntity[];
|
||||
expected: CircularDependencyValidationResult;
|
||||
};
|
||||
|
||||
type ValidateCircularDependencyTestCase = EachTestingContext<TestContext>;
|
||||
|
||||
const testCases: ValidateCircularDependencyTestCase[] = [
|
||||
{
|
||||
title: 'should fail with self_reference when entity is its own parent',
|
||||
context: {
|
||||
flatEntityId: 'entity-1',
|
||||
flatEntityParentId: 'entity-1',
|
||||
entities: [],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'self_reference',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed with depth 2 when parent has no parent',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
entities: [createTestEntity('entity-parent', null)],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed with depth 3 when parent has a grandparent',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
entities: [
|
||||
createTestEntity('entity-grandparent', null),
|
||||
createTestEntity('entity-parent', 'entity-grandparent'),
|
||||
],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fail with circular_dependency when parent points back to entity',
|
||||
context: {
|
||||
flatEntityId: 'entity-a',
|
||||
flatEntityParentId: 'entity-b',
|
||||
entities: [createTestEntity('entity-b', 'entity-a')],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fail with circular_dependency when chain creates a cycle (A -> B -> C -> A)',
|
||||
context: {
|
||||
flatEntityId: 'entity-a',
|
||||
flatEntityParentId: 'entity-b',
|
||||
entities: [
|
||||
createTestEntity('entity-b', 'entity-c'),
|
||||
createTestEntity('entity-c', 'entity-a'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fail with circular_dependency when ancestors form a cycle among themselves',
|
||||
context: {
|
||||
flatEntityId: 'entity-new',
|
||||
flatEntityParentId: 'entity-a',
|
||||
entities: [
|
||||
createTestEntity('entity-a', 'entity-b'),
|
||||
createTestEntity('entity-b', 'entity-a'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed when parent does not exist in maps',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'non-existent-parent',
|
||||
entities: [],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should fail with max_depth_exceeded when depth exceeds maxDepth',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
maxDepth: 2,
|
||||
entities: [
|
||||
createTestEntity('entity-grandparent', null),
|
||||
createTestEntity('entity-parent', 'entity-grandparent'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed when depth equals maxDepth',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
maxDepth: 3,
|
||||
entities: [
|
||||
createTestEntity('entity-grandparent', null),
|
||||
createTestEntity('entity-parent', 'entity-grandparent'),
|
||||
],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed with depth 2 when maxDepth is 2 and parent is root',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
maxDepth: 2,
|
||||
entities: [createTestEntity('entity-parent', null)],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should check max depth before circular dependency to prevent infinite loop',
|
||||
context: {
|
||||
flatEntityId: 'entity-new',
|
||||
flatEntityParentId: 'entity-a',
|
||||
maxDepth: 2,
|
||||
entities: [
|
||||
createTestEntity('entity-a', 'entity-b'),
|
||||
createTestEntity('entity-b', 'entity-c'),
|
||||
createTestEntity('entity-c', 'entity-d'),
|
||||
createTestEntity('entity-d', 'entity-a'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle deep valid hierarchy without maxDepth',
|
||||
context: {
|
||||
flatEntityId: 'entity-new',
|
||||
flatEntityParentId: 'entity-1',
|
||||
entities: [
|
||||
createTestEntity('entity-5', null),
|
||||
createTestEntity('entity-4', 'entity-5'),
|
||||
createTestEntity('entity-3', 'entity-4'),
|
||||
createTestEntity('entity-2', 'entity-3'),
|
||||
createTestEntity('entity-1', 'entity-2'),
|
||||
],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('validateFlatEntityCircularDependency', () => {
|
||||
test.each(testCases)('$title', ({ context }) => {
|
||||
const flatEntityMaps = createFlatEntityMaps(context.entities);
|
||||
|
||||
const result = validateFlatEntityCircularDependency({
|
||||
flatEntityId: context.flatEntityId,
|
||||
flatEntityParentId: context.flatEntityParentId,
|
||||
maxDepth: context.maxDepth,
|
||||
parentIdKey: 'parentId',
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
expect(result).toEqual(context.expected);
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
|
||||
type AddFlatEntityToFlatEntityMapsThroughMutationOrThrowArgs<
|
||||
T extends SyncableFlatEntity,
|
||||
> = {
|
||||
flatEntity: T;
|
||||
flatEntityMapsToMutate: FlatEntityMaps<T>;
|
||||
};
|
||||
|
||||
export const addFlatEntityToFlatEntityMapsThroughMutationOrThrow = <
|
||||
T extends SyncableFlatEntity,
|
||||
>({
|
||||
flatEntity,
|
||||
flatEntityMapsToMutate,
|
||||
}: AddFlatEntityToFlatEntityMapsThroughMutationOrThrowArgs<T>): void => {
|
||||
if (isDefined(flatEntityMapsToMutate.byId[flatEntity.id])) {
|
||||
throw new FlatEntityMapsException(
|
||||
'addFlatEntityToFlatEntityMapsThroughMutationOrThrow: flat entity to add already exists',
|
||||
FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
|
||||
);
|
||||
}
|
||||
|
||||
flatEntityMapsToMutate.byId[flatEntity.id] = flatEntity;
|
||||
|
||||
flatEntityMapsToMutate.idByUniversalIdentifier[
|
||||
flatEntity.universalIdentifier
|
||||
] = flatEntity.id;
|
||||
|
||||
if (isDefined(flatEntity.applicationId)) {
|
||||
const existingUniversalIdentifiers =
|
||||
flatEntityMapsToMutate.universalIdentifiersByApplicationId[
|
||||
flatEntity.applicationId
|
||||
];
|
||||
|
||||
if (isDefined(existingUniversalIdentifiers)) {
|
||||
if (
|
||||
!existingUniversalIdentifiers.includes(flatEntity.universalIdentifier)
|
||||
) {
|
||||
existingUniversalIdentifiers.push(flatEntity.universalIdentifier);
|
||||
}
|
||||
} else {
|
||||
flatEntityMapsToMutate.universalIdentifiersByApplicationId[
|
||||
flatEntity.applicationId
|
||||
] = [flatEntity.universalIdentifier];
|
||||
}
|
||||
}
|
||||
};
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
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),
|
||||
},
|
||||
};
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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 DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
|
||||
|
||||
type AggregatedActions = {
|
||||
deleteFieldActionByFieldMetadataId: Record<string, DeleteFieldAction>;
|
||||
};
|
||||
export const aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions =
|
||||
({
|
||||
orchestratorActionsReport,
|
||||
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
|
||||
const deleteObjectActionByObjectMetadataId = (
|
||||
orchestratorActionsReport.objectMetadata.delete as DeleteObjectAction[]
|
||||
).reduce<Record<string, DeleteObjectAction>>(
|
||||
(acc, deleteObjectAction) => ({
|
||||
...acc,
|
||||
[deleteObjectAction.entityId]: deleteObjectAction,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const initialAccumulator: AggregatedActions = {
|
||||
deleteFieldActionByFieldMetadataId: {},
|
||||
};
|
||||
|
||||
const { deleteFieldActionByFieldMetadataId } = (
|
||||
orchestratorActionsReport.fieldMetadata.delete as DeleteFieldAction[]
|
||||
).reduce<AggregatedActions>(
|
||||
({ deleteFieldActionByFieldMetadataId }, deleteFieldAction) => {
|
||||
const fieldParentObjectDeleteObjectAction =
|
||||
deleteObjectActionByObjectMetadataId[
|
||||
deleteFieldAction.objectMetadataId
|
||||
];
|
||||
|
||||
if (isDefined(fieldParentObjectDeleteObjectAction)) {
|
||||
return {
|
||||
deleteFieldActionByFieldMetadataId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deleteFieldActionByFieldMetadataId: {
|
||||
...deleteFieldActionByFieldMetadataId,
|
||||
[deleteFieldAction.entityId]: deleteFieldAction,
|
||||
},
|
||||
};
|
||||
},
|
||||
initialAccumulator,
|
||||
);
|
||||
|
||||
return {
|
||||
...orchestratorActionsReport,
|
||||
fieldMetadata: {
|
||||
...orchestratorActionsReport.fieldMetadata,
|
||||
delete: Object.values(deleteFieldActionByFieldMetadataId),
|
||||
},
|
||||
};
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
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 { 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';
|
||||
|
||||
export const aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions =
|
||||
({
|
||||
orchestratorActionsReport,
|
||||
flatFieldMetadataMaps,
|
||||
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
|
||||
assertIsDefinedOrThrow(flatFieldMetadataMaps);
|
||||
|
||||
const updateFieldActions = orchestratorActionsReport.fieldMetadata
|
||||
.update as UpdateFieldAction[];
|
||||
|
||||
const { searchVectorUpdateFieldActions, otherUpdateFieldActions } =
|
||||
updateFieldActions.reduce<{
|
||||
searchVectorUpdateFieldActions: UpdateFieldAction[];
|
||||
otherUpdateFieldActions: UpdateFieldAction[];
|
||||
}>(
|
||||
(acc, updateFieldAction) => {
|
||||
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: updateFieldAction.entityId,
|
||||
});
|
||||
|
||||
const isSearchVectorUpdateFieldAction =
|
||||
flatFieldMetadata?.name === SEARCH_VECTOR_FIELD.name;
|
||||
|
||||
if (isSearchVectorUpdateFieldAction) {
|
||||
return {
|
||||
...acc,
|
||||
searchVectorUpdateFieldActions: [
|
||||
...acc.searchVectorUpdateFieldActions,
|
||||
updateFieldAction,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
otherUpdateFieldActions: [
|
||||
...acc.otherUpdateFieldActions,
|
||||
updateFieldAction,
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
searchVectorUpdateFieldActions: [],
|
||||
otherUpdateFieldActions: [],
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...orchestratorActionsReport,
|
||||
fieldMetadata: {
|
||||
...orchestratorActionsReport.fieldMetadata,
|
||||
update: [...otherUpdateFieldActions, ...searchVectorUpdateFieldActions],
|
||||
},
|
||||
};
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
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 { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util';
|
||||
import { aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util';
|
||||
|
||||
export const aggregateOrchestratorActionsReport = ({
|
||||
orchestratorActionsReport,
|
||||
flatFieldMetadataMaps,
|
||||
}: AggregateOrchestratorActionsReportArgs) => {
|
||||
const aggregatedOrchestratorActionsReport = [
|
||||
aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions,
|
||||
aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions,
|
||||
aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions,
|
||||
].reduce(
|
||||
(currentOrchestratorActionsReport, aggregator) =>
|
||||
aggregator({
|
||||
orchestratorActionsReport: currentOrchestratorActionsReport,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
orchestratorActionsReport,
|
||||
);
|
||||
|
||||
return { aggregatedOrchestratorActionsReport };
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
|
||||
|
||||
type ComputePostgresEnumNameParams = {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
};
|
||||
|
||||
export const computePostgresEnumName = ({
|
||||
tableName,
|
||||
columnName,
|
||||
}: ComputePostgresEnumNameParams): string => {
|
||||
return removeSqlDDLInjection(`${tableName}_${columnName}_enum`);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { type RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
|
||||
export const convertOnDeleteActionToOnDelete = (
|
||||
onDeleteAction: RelationOnDeleteAction | undefined,
|
||||
): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | undefined => {
|
||||
if (!onDeleteAction) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (onDeleteAction) {
|
||||
case 'CASCADE':
|
||||
return 'CASCADE';
|
||||
case 'SET_NULL':
|
||||
return 'SET NULL';
|
||||
case 'RESTRICT':
|
||||
return 'RESTRICT';
|
||||
case 'NO_ACTION':
|
||||
return 'NO ACTION';
|
||||
default:
|
||||
throw new Error('Invalid onDeleteAction');
|
||||
}
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
|
||||
export type DeleteFlatEntityFromFlatEntityMapsThroughMutationOrThrowArgs<
|
||||
T extends SyncableFlatEntity,
|
||||
> = {
|
||||
entityToDeleteId: string;
|
||||
flatEntityMapsToMutate: FlatEntityMaps<T>;
|
||||
};
|
||||
|
||||
export const deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow = <
|
||||
T extends SyncableFlatEntity,
|
||||
>({
|
||||
flatEntityMapsToMutate,
|
||||
entityToDeleteId,
|
||||
}: DeleteFlatEntityFromFlatEntityMapsThroughMutationOrThrowArgs<T>): void => {
|
||||
const entityToDelete = flatEntityMapsToMutate.byId[entityToDeleteId];
|
||||
|
||||
if (!isDefined(entityToDelete)) {
|
||||
throw new FlatEntityMapsException(
|
||||
'deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow: entity to delete not found',
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
delete flatEntityMapsToMutate.byId[entityToDeleteId];
|
||||
|
||||
delete flatEntityMapsToMutate.idByUniversalIdentifier[
|
||||
entityToDelete.universalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(entityToDelete.applicationId)) {
|
||||
const universalIdentifiers =
|
||||
flatEntityMapsToMutate.universalIdentifiersByApplicationId[
|
||||
entityToDelete.applicationId
|
||||
];
|
||||
|
||||
if (isDefined(universalIdentifiers)) {
|
||||
const index = universalIdentifiers.indexOf(
|
||||
entityToDelete.universalIdentifier,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
universalIdentifiers.splice(index, 1);
|
||||
}
|
||||
|
||||
if (universalIdentifiers.length === 0) {
|
||||
delete flatEntityMapsToMutate.universalIdentifiersByApplicationId[
|
||||
entityToDelete.applicationId
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type';
|
||||
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
import { type FlatEntityPropertyUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-property-update.type';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { isPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/is-property-update.util';
|
||||
|
||||
export const findFlatEntityPropertyUpdate = <
|
||||
T extends AllMetadataName,
|
||||
P extends Extract<
|
||||
FlatEntityPropertiesToCompare<T>,
|
||||
keyof MetadataFlatEntity<T>
|
||||
>,
|
||||
>({
|
||||
property,
|
||||
flatEntityUpdates,
|
||||
}: {
|
||||
flatEntityUpdates: FlatEntityPropertiesUpdates<T>;
|
||||
property: P;
|
||||
}) =>
|
||||
flatEntityUpdates.find((update): update is FlatEntityPropertyUpdate<T, P> =>
|
||||
isPropertyUpdate(update, property),
|
||||
);
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type';
|
||||
import { type FlatEntityPropertiesToStringify } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-stringify.type';
|
||||
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
import { compareTwoFlatEntity } from 'src/engine/metadata-modules/flat-entity/utils/compare-two-flat-entity.util';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
export type DeletedCreatedUpdatedMatrix<T extends AllMetadataName> = {
|
||||
createdFlatEntityMaps: MetadataFlatEntityMaps<T>;
|
||||
deletedFlatEntityMaps: MetadataFlatEntityMaps<T>;
|
||||
updatedFlatEntityMaps: {
|
||||
byId: Record<
|
||||
string,
|
||||
{
|
||||
updates: FlatEntityPropertiesUpdates<T>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
export type UniversalIdentifierItem = {
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
type FlatEntityDeletedCreatedUpdatedMatrixDispatcherArgs<
|
||||
T extends AllMetadataName,
|
||||
> = FromTo<MetadataFlatEntity<T>[]> & {
|
||||
metadataName: T;
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
};
|
||||
|
||||
export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
|
||||
T extends AllMetadataName,
|
||||
>({
|
||||
from,
|
||||
to,
|
||||
metadataName,
|
||||
buildOptions,
|
||||
}: FlatEntityDeletedCreatedUpdatedMatrixDispatcherArgs<T>): DeletedCreatedUpdatedMatrix<T> => {
|
||||
const initialDispatcher: DeletedCreatedUpdatedMatrix<T> = {
|
||||
createdFlatEntityMaps: createEmptyFlatEntityMaps(),
|
||||
deletedFlatEntityMaps: createEmptyFlatEntityMaps(),
|
||||
updatedFlatEntityMaps: { byId: {} },
|
||||
};
|
||||
|
||||
const fromMap = new Map(from.map((obj) => [obj.universalIdentifier, obj]));
|
||||
const toMap = new Map(to.map((obj) => [obj.universalIdentifier, obj]));
|
||||
|
||||
if (shouldInferDeletionFromMissingEntities({ buildOptions, metadataName })) {
|
||||
for (const [universalIdentifier, fromEntity] of fromMap) {
|
||||
if (toMap.has(universalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: fromEntity,
|
||||
flatEntityMapsToMutate: initialDispatcher.deletedFlatEntityMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [universalIdentifier, toFlatEntity] of toMap) {
|
||||
if (fromMap.has(universalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: toFlatEntity,
|
||||
flatEntityMapsToMutate: initialDispatcher.createdFlatEntityMaps,
|
||||
});
|
||||
}
|
||||
|
||||
const { propertiesToCompare, propertiesToStringify } =
|
||||
ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName];
|
||||
|
||||
for (const [universalIdentifier, fromFlatEntity] of fromMap) {
|
||||
const toFlatEntity = toMap.get(universalIdentifier);
|
||||
|
||||
if (!isDefined(toFlatEntity)) {
|
||||
continue;
|
||||
}
|
||||
const updates = compareTwoFlatEntity({
|
||||
fromFlatEntity,
|
||||
toFlatEntity,
|
||||
propertiesToCompare: propertiesToCompare as unknown as Extract<
|
||||
FlatEntityPropertiesToCompare<T>,
|
||||
keyof MetadataFlatEntity<T>
|
||||
>[],
|
||||
propertiesToStringify: propertiesToStringify as unknown as Extract<
|
||||
FlatEntityPropertiesToStringify<T>,
|
||||
keyof MetadataFlatEntity<T>
|
||||
>[],
|
||||
});
|
||||
|
||||
if (updates.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
initialDispatcher.updatedFlatEntityMaps.byId[toFlatEntity.id] = {
|
||||
updates,
|
||||
};
|
||||
}
|
||||
|
||||
return initialDispatcher;
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type MetadataWorkspaceMigrationActionsRecord } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
|
||||
|
||||
export const getMetadataEmptyWorkspaceMigrationActionRecord = <
|
||||
T extends AllMetadataName,
|
||||
>(
|
||||
_metadataName: T,
|
||||
) =>
|
||||
({
|
||||
create: [],
|
||||
delete: [],
|
||||
update: [],
|
||||
}) as MetadataWorkspaceMigrationActionsRecord<T>;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/types/property-update.type';
|
||||
|
||||
export const isPropertyUpdate = <T, P extends keyof T>(
|
||||
update: PropertyUpdate<T, keyof T>,
|
||||
property: P,
|
||||
): update is PropertyUpdate<T, P> => {
|
||||
return update.property === property;
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const removeSqlDDLInjection = (value: string): string => {
|
||||
return value.replace(/[^a-zA-Z0-9_]/g, '');
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
import { deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/delete-flat-entity-from-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
export type ReplaceFlatEntityInFlatEntityMapsThroughMutationOrThrowArgs<
|
||||
T extends SyncableFlatEntity,
|
||||
> = {
|
||||
flatEntity: T;
|
||||
flatEntityMapsToMutate: FlatEntityMaps<T>;
|
||||
};
|
||||
|
||||
export const replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow = <
|
||||
T extends SyncableFlatEntity,
|
||||
>({
|
||||
flatEntity,
|
||||
flatEntityMapsToMutate,
|
||||
}: ReplaceFlatEntityInFlatEntityMapsThroughMutationOrThrowArgs<T>): void => {
|
||||
deleteFlatEntityFromFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntityMapsToMutate,
|
||||
entityToDeleteId: flatEntity.id,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity,
|
||||
flatEntityMapsToMutate,
|
||||
});
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const shouldInferDeletionFromMissingEntities = ({
|
||||
buildOptions,
|
||||
metadataName,
|
||||
}: {
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
metadataName: AllMetadataName;
|
||||
}): boolean => {
|
||||
return (
|
||||
buildOptions.inferDeletionFromMissingEntities === true ||
|
||||
buildOptions.inferDeletionFromMissingEntities?.[metadataName] === true
|
||||
);
|
||||
};
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
|
||||
export type CircularDependencyValidationSuccess = {
|
||||
status: 'success';
|
||||
depth: number;
|
||||
};
|
||||
|
||||
export type CircularDependencyValidationFailure = {
|
||||
status: 'fail';
|
||||
reason: 'self_reference' | 'circular_dependency' | 'max_depth_exceeded';
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
export type CircularDependencyValidationResult =
|
||||
| CircularDependencyValidationSuccess
|
||||
| CircularDependencyValidationFailure;
|
||||
|
||||
export const validateFlatEntityCircularDependency = <
|
||||
T extends SyncableFlatEntity,
|
||||
K extends keyof T,
|
||||
>({
|
||||
flatEntityId,
|
||||
flatEntityParentId,
|
||||
maxDepth,
|
||||
parentIdKey,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
flatEntityId: string;
|
||||
flatEntityParentId: string;
|
||||
maxDepth?: number;
|
||||
parentIdKey: K;
|
||||
flatEntityMaps: FlatEntityMaps<T>;
|
||||
}): CircularDependencyValidationResult => {
|
||||
// Direct self-reference check
|
||||
if (flatEntityId === flatEntityParentId) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'self_reference',
|
||||
};
|
||||
}
|
||||
|
||||
// Traverse ancestor chain to detect cycles and measure depth
|
||||
const visited = new Set<string>();
|
||||
let currentParentId: string | null | undefined = flatEntityParentId;
|
||||
let depth = 1;
|
||||
|
||||
while (isDefined(currentParentId)) {
|
||||
if (isDefined(maxDepth) && depth > maxDepth) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for circular dependency
|
||||
if (currentParentId === flatEntityId) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for cycle in ancestors (already visited node)
|
||||
if (visited.has(currentParentId)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
visited.add(currentParentId);
|
||||
|
||||
const parentEntity: T | undefined = flatEntityMaps.byId[currentParentId];
|
||||
|
||||
if (!isDefined(parentEntity)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextParentId: T[K] = parentEntity[parentIdKey];
|
||||
|
||||
currentParentId =
|
||||
isDefined(nextParentId) && typeof nextParentId === 'string'
|
||||
? nextParentId
|
||||
: undefined;
|
||||
depth++;
|
||||
}
|
||||
|
||||
// Check max depth for the final depth value
|
||||
if (isDefined(maxDepth) && depth > maxDepth) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
depth,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user