diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/get-sub-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/get-sub-flat-entity-maps.util.ts deleted file mode 100644 index fd348a6ee9d..00000000000 --- a/packages/twenty-server/src/engine/core-modules/common/utils/get-sub-flat-entity-maps.util.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant'; -import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; -import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; - -export const getSubFlatEntityMaps = ({ - flatEntityIds, - flatEntityMaps, -}: { - flatEntityMaps: FlatEntityMaps; - flatEntityIds: string[]; -}): FlatEntityMaps => { - return flatEntityIds.reduce>((acc, flatEntityId) => { - const flatEntity = findFlatEntityByIdInFlatEntityMaps({ - flatEntityId, - flatEntityMaps, - }); - - if (!isDefined(flatEntity)) { - return acc; - } - - return addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity, - flatEntityMaps: acc, - }); - }, EMPTY_FLAT_ENTITY_MAPS); -}; diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 7ff87df1e03..194469efea8 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -337,6 +337,7 @@ export class DataloaderService { >(async (dataLoaderParams: IndexFieldMetadataLoaderPayload[]) => { const workspaceId = dataLoaderParams[0].workspaceId; + // This computes the old cache :thinking: const { objectMetadataMaps } = await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( { workspaceId }, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index b33e5bd725e..37e7968a016 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -5,6 +5,9 @@ import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; @@ -64,19 +67,26 @@ export class FieldMetadataServiceV2 { deleteOneFieldInput: DeleteOneFieldInput; workspaceId: string; }): Promise { - const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + const { + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + flatIndexMaps: existingFlatIndexMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatEntities: ['flatObjectMetadataMaps'], + flatEntities: ['flatObjectMetadataMaps', 'flatIndexMaps'], }, ); - const flatFieldMetadatasToDelete = - fromDeleteFieldInputToFlatFieldMetadatasToDelete({ - deleteOneFieldInput, - existingFlatObjectMetadataMaps, - }); + const { + flatFieldMetadatasToDelete, + flatIndexesToDelete, + flatIndexesToUpdate, + } = fromDeleteFieldInputToFlatFieldMetadatasToDelete({ + deleteOneFieldInput, + existingFlatObjectMetadataMaps, + existingFlatIndexMaps, + }); const fromFlatObjectMetadataMaps = getSubFlatObjectMetadataMapsOrThrow({ flatObjectMetadataMaps: existingFlatObjectMetadataMaps, @@ -100,6 +110,23 @@ export class FieldMetadataServiceV2 { fromFlatObjectMetadataMaps, ); + const toFlatIndexMapsWithUpdatedFlatIndex = flatIndexesToUpdate.reduce( + (flatIndexMaps, flatIndex) => + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: flatIndex, + flatEntityMaps: flatIndexMaps, + }), + existingFlatIndexMaps, + ); + const toFlatIndexMaps = flatIndexesToDelete.reduce( + (flatIndexMaps, flatIndex) => + deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: flatIndex.id, + flatEntityMaps: flatIndexMaps, + }), + toFlatIndexMapsWithUpdatedFlatIndex, + ); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -112,6 +139,10 @@ export class FieldMetadataServiceV2 { from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, }, + flatIndexMaps: { + from: existingFlatIndexMaps, + to: toFlatIndexMaps, + }, }, workspaceId, }, @@ -136,16 +167,20 @@ export class FieldMetadataServiceV2 { updateFieldInput: UpdateFieldInput; workspaceId: string; }): Promise { - const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + const { + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + flatIndexMaps: existingFlatIndexMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatEntities: ['flatObjectMetadataMaps'], + flatEntities: ['flatObjectMetadataMaps', 'flatIndexMaps'], }, ); const inputTranspilationResult = fromUpdateFieldInputToFlatFieldMetadata({ - existingFlatObjectMetadataMaps, + flatIndexMaps: existingFlatIndexMaps, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, updateFieldInput, }); @@ -153,8 +188,10 @@ export class FieldMetadataServiceV2 { throw inputTranspilationResult.error; } - const optimisticallyUpdatedFlatFieldMetadatas = - inputTranspilationResult.result; + const { + flatFieldMetadatasToUpdate: optimisticallyUpdatedFlatFieldMetadatas, + flatIndexMetadatasToUpdate, + } = inputTranspilationResult.result; const objectMetadataIdWithRelatedObjectMetadataIds = [ ...new Set( @@ -181,6 +218,15 @@ export class FieldMetadataServiceV2 { fromFlatObjectMetadataMaps, ); + const toFlatIndexMaps = flatIndexMetadatasToUpdate.reduce( + (flatIndexMaps, flatIndexMetadata) => + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: flatIndexMetadata, + flatEntityMaps: flatIndexMaps, + }), + existingFlatIndexMaps, + ); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -189,6 +235,10 @@ export class FieldMetadataServiceV2 { from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, }, + flatIndexMaps: { + from: existingFlatIndexMaps, + to: toFlatIndexMaps, + }, }, buildOptions: { isSystemBuild: false, @@ -224,15 +274,20 @@ export class FieldMetadataServiceV2 { return []; } - const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + const { + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + flatIndexMaps: existingFlatIndexMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatEntities: ['flatObjectMetadataMaps'], + flatEntities: ['flatObjectMetadataMaps', 'flatIndexMaps'], }, ); - const allTranspiledTranspilationInputs = []; + const allTranspiledTranspilationInputs: Awaited< + ReturnType + >[] = []; for (const createInput of createFieldInputs) { allTranspiledTranspilationInputs.push( @@ -249,8 +304,18 @@ export class FieldMetadataServiceV2 { 'Multiple validation errors occurred while creating field', ); - const flatFieldMetadatasToCreate = allTranspiledTranspilationInputs.flatMap( - ({ result }) => result, + const { + flatFieldMetadatas: flatFieldMetadatasToCreate, + indexMetadatas: flatIndexMetadatasToCreate, + } = allTranspiledTranspilationInputs.reduce( + (acc, { result }) => ({ + flatFieldMetadatas: [ + ...acc.flatFieldMetadatas, + ...result.flatFieldMetadatas, + ], + indexMetadatas: [...acc.indexMetadatas, ...result.indexMetadatas], + }), + { flatFieldMetadatas: [], indexMetadatas: [] }, ); const impactedObjectMetadataIds = Array.from( @@ -277,6 +342,15 @@ export class FieldMetadataServiceV2 { }), ); + const toFlatIndexMaps = flatIndexMetadatasToCreate.reduce( + (flatIndexMaps, flatIndex) => + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatIndex, + flatEntityMaps: flatIndexMaps, + }), + existingFlatIndexMaps, + ); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -285,6 +359,10 @@ export class FieldMetadataServiceV2 { from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, }, + flatIndexMaps: { + from: existingFlatIndexMaps, + to: toFlatIndexMaps, + }, }, buildOptions: { isSystemBuild: false, @@ -305,7 +383,7 @@ export class FieldMetadataServiceV2 { where: { name: In( allTranspiledTranspilationInputs.map( - ({ result: flatFieldMetadatas }) => flatFieldMetadatas[0].name, + ({ result: { flatFieldMetadatas } }) => flatFieldMetadatas[0].name, ), ), workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap index 49a30d81ddd..95ebf808f7b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap @@ -83,12 +83,189 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test suite Success cases should create morph relation field metadata with valid input on rocket object to pet object 1`] = ` { - "result": [ - { - "createdAt": Any, - "defaultValue": null, - "description": "new field description", - "flatRelationTargetFieldMetadata": { + "result": { + "flatFieldMetadatas": [ + { + "createdAt": Any, + "defaultValue": null, + "description": "new field description", + "flatRelationTargetFieldMetadata": { + "createdAt": Any, + "defaultValue": null, + "description": null, + "flatRelationTargetFieldMetadata": { + "createdAt": Any, + "defaultValue": null, + "description": "new field description", + "flatRelationTargetFieldMetadata": null, + "flatRelationTargetObjectMetadata": { + "createdAt": Any, + "description": null, + "duplicateCriteria": null, + "flatFieldMetadatas": [], + "icon": "IconCat", + "id": Any, + "imageIdentifierFieldMetadataId": null, + "isActive": true, + "isAuditLogged": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isRemote": false, + "isSearchable": true, + "isSystem": false, + "isUIReadOnly": false, + "labelIdentifierFieldMetadataId": Any, + "labelPlural": "Pets", + "labelSingular": "Pet", + "namePlural": "pets", + "nameSingular": "pet", + "shortcut": null, + "standardId": null, + "standardOverrides": null, + "targetTableName": "DEPRECATED", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "icon": "IconRelationOneToMany", + "id": Any, + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": null, + "label": "newFieldLabel", + "morphId": Any, + "name": "newFieldPets", + "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadataId": Any, + "settings": { + "relationType": "ONE_TO_MANY", + }, + "standardId": null, + "standardOverrides": null, + "type": "MORPH_RELATION", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "flatRelationTargetObjectMetadata": { + "createdAt": Any, + "description": "A rocket", + "duplicateCriteria": null, + "flatFieldMetadatas": [], + "icon": "IconRocket", + "id": Any, + "imageIdentifierFieldMetadataId": null, + "isActive": true, + "isAuditLogged": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isRemote": false, + "isSearchable": true, + "isSystem": false, + "isUIReadOnly": false, + "labelIdentifierFieldMetadataId": Any, + "labelPlural": "Rockets", + "labelSingular": "Rocket", + "namePlural": "rockets", + "nameSingular": "rocket", + "shortcut": null, + "standardId": null, + "standardOverrides": null, + "targetTableName": "DEPRECATED", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "icon": "IconPet", + "id": Any, + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": null, + "label": "Pet", + "morphId": null, + "name": "pet", + "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadataId": Any, + "settings": { + "joinColumnName": "petId", + "onDelete": "SET_NULL", + "relationType": "MANY_TO_ONE", + }, + "standardId": null, + "standardOverrides": null, + "type": "RELATION", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "flatRelationTargetObjectMetadata": { + "createdAt": Any, + "description": null, + "duplicateCriteria": null, + "flatFieldMetadatas": [], + "icon": "IconCat", + "id": Any, + "imageIdentifierFieldMetadataId": null, + "isActive": true, + "isAuditLogged": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isRemote": false, + "isSearchable": true, + "isSystem": false, + "isUIReadOnly": false, + "labelIdentifierFieldMetadataId": Any, + "labelPlural": "Pets", + "labelSingular": "Pet", + "namePlural": "pets", + "nameSingular": "pet", + "shortcut": null, + "standardId": null, + "standardOverrides": null, + "targetTableName": "DEPRECATED", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "icon": "IconRelationOneToMany", + "id": Any, + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": null, + "label": "newFieldLabel", + "morphId": Any, + "name": "newFieldPets", + "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadataId": Any, + "settings": { + "relationType": "ONE_TO_MANY", + }, + "standardId": null, + "standardOverrides": null, + "type": "MORPH_RELATION", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + { "createdAt": Any, "defaultValue": null, "description": null, @@ -102,7 +279,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "description": null, "duplicateCriteria": null, "flatFieldMetadatas": [], - "flatIndexMetadatas": [], "icon": "IconCat", "id": Any, "imageIdentifierFieldMetadataId": null, @@ -158,7 +334,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "description": "A rocket", "duplicateCriteria": null, "flatFieldMetadatas": [], - "flatIndexMetadatas": [], "icon": "IconRocket", "id": Any, "imageIdentifierFieldMetadataId": null, @@ -211,95 +386,154 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "workspaceId": Any, }, - "flatRelationTargetObjectMetadata": { - "createdAt": Any, - "description": null, - "duplicateCriteria": null, - "flatFieldMetadatas": [], - "flatIndexMetadatas": [], - "icon": "IconCat", - "id": Any, - "imageIdentifierFieldMetadataId": null, - "isActive": true, - "isAuditLogged": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isRemote": false, - "isSearchable": true, - "isSystem": false, - "isUIReadOnly": false, - "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Pets", - "labelSingular": "Pet", - "namePlural": "pets", - "nameSingular": "pet", - "shortcut": null, - "standardId": null, - "standardOverrides": null, - "targetTableName": "DEPRECATED", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "icon": "IconRelationOneToMany", - "id": Any, - "isActive": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": null, - "label": "newFieldLabel", - "morphId": Any, - "name": "newFieldPets", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": null, - "standardOverrides": null, - "type": "MORPH_RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - { - "createdAt": Any, - "defaultValue": null, - "description": null, - "flatRelationTargetFieldMetadata": { + { "createdAt": Any, "defaultValue": null, "description": "new field description", - "flatRelationTargetFieldMetadata": null, + "flatRelationTargetFieldMetadata": { + "createdAt": Any, + "defaultValue": null, + "description": null, + "flatRelationTargetFieldMetadata": { + "createdAt": Any, + "defaultValue": null, + "description": "new field description", + "flatRelationTargetFieldMetadata": null, + "flatRelationTargetObjectMetadata": { + "createdAt": Any, + "description": "A company", + "duplicateCriteria": null, + "flatFieldMetadatas": [], + "icon": "IconBuildingSkyscraper", + "id": Any, + "imageIdentifierFieldMetadataId": null, + "isActive": true, + "isAuditLogged": true, + "isCustom": false, + "isLabelSyncedWithName": false, + "isRemote": false, + "isSearchable": true, + "isSystem": false, + "isUIReadOnly": false, + "labelIdentifierFieldMetadataId": Any, + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "companies", + "nameSingular": "company", + "shortcut": "C", + "standardId": Any, + "standardOverrides": null, + "targetTableName": "DEPRECATED", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "icon": "IconRelationOneToMany", + "id": Any, + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": null, + "label": "newFieldLabel", + "morphId": Any, + "name": "newFieldCompanies", + "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadataId": Any, + "settings": { + "relationType": "ONE_TO_MANY", + }, + "standardId": null, + "standardOverrides": null, + "type": "MORPH_RELATION", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "flatRelationTargetObjectMetadata": { + "createdAt": Any, + "description": "A rocket", + "duplicateCriteria": null, + "flatFieldMetadatas": [], + "icon": "IconRocket", + "id": Any, + "imageIdentifierFieldMetadataId": null, + "isActive": true, + "isAuditLogged": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isRemote": false, + "isSearchable": true, + "isSystem": false, + "isUIReadOnly": false, + "labelIdentifierFieldMetadataId": Any, + "labelPlural": "Rockets", + "labelSingular": "Rocket", + "namePlural": "rockets", + "nameSingular": "rocket", + "shortcut": null, + "standardId": null, + "standardOverrides": null, + "targetTableName": "DEPRECATED", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, + "icon": "IconBuilding", + "id": Any, + "isActive": true, + "isCustom": true, + "isLabelSyncedWithName": false, + "isNullable": true, + "isSystem": false, + "isUIReadOnly": false, + "isUnique": null, + "label": "Company", + "morphId": null, + "name": "company", + "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadataId": Any, + "settings": { + "joinColumnName": "companyId", + "onDelete": "SET_NULL", + "relationType": "MANY_TO_ONE", + }, + "standardId": null, + "standardOverrides": null, + "type": "RELATION", + "universalIdentifier": Any, + "updatedAt": Any, + "workspaceId": Any, + }, "flatRelationTargetObjectMetadata": { "createdAt": Any, - "description": null, + "description": "A company", "duplicateCriteria": null, "flatFieldMetadatas": [], - "flatIndexMetadatas": [], - "icon": "IconCat", + "icon": "IconBuildingSkyscraper", "id": Any, "imageIdentifierFieldMetadataId": null, "isActive": true, "isAuditLogged": true, - "isCustom": true, + "isCustom": false, "isLabelSyncedWithName": false, "isRemote": false, "isSearchable": true, "isSystem": false, "isUIReadOnly": false, "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Pets", - "labelSingular": "Pet", - "namePlural": "pets", - "nameSingular": "pet", - "shortcut": null, - "standardId": null, + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "companies", + "nameSingular": "company", + "shortcut": "C", + "standardId": Any, "standardOverrides": null, "targetTableName": "DEPRECATED", "universalIdentifier": Any, @@ -317,7 +551,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "isUnique": null, "label": "newFieldLabel", "morphId": Any, - "name": "newFieldPets", + "name": "newFieldCompanies", "objectMetadataId": Any, "options": null, "relationTargetFieldMetadataId": Any, @@ -332,69 +566,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "workspaceId": Any, }, - "flatRelationTargetObjectMetadata": { - "createdAt": Any, - "description": "A rocket", - "duplicateCriteria": null, - "flatFieldMetadatas": [], - "flatIndexMetadatas": [], - "icon": "IconRocket", - "id": Any, - "imageIdentifierFieldMetadataId": null, - "isActive": true, - "isAuditLogged": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isRemote": false, - "isSearchable": true, - "isSystem": false, - "isUIReadOnly": false, - "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Rockets", - "labelSingular": "Rocket", - "namePlural": "rockets", - "nameSingular": "rocket", - "shortcut": null, - "standardId": null, - "standardOverrides": null, - "targetTableName": "DEPRECATED", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "icon": "IconPet", - "id": Any, - "isActive": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": null, - "label": "Pet", - "morphId": null, - "name": "pet", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "petId", - "onDelete": "SET_NULL", - "relationType": "MANY_TO_ONE", - }, - "standardId": null, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - { - "createdAt": Any, - "defaultValue": null, - "description": "new field description", - "flatRelationTargetFieldMetadata": { + { "createdAt": Any, "defaultValue": null, "description": null, @@ -408,7 +580,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "description": "A company", "duplicateCriteria": null, "flatFieldMetadatas": [], - "flatIndexMetadatas": [], "icon": "IconBuildingSkyscraper", "id": Any, "imageIdentifierFieldMetadataId": null, @@ -464,7 +635,6 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "description": "A rocket", "duplicateCriteria": null, "flatFieldMetadatas": [], - "flatIndexMetadatas": [], "icon": "IconRocket", "id": Any, "imageIdentifierFieldMetadataId": null, @@ -517,186 +687,56 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "updatedAt": Any, "workspaceId": Any, }, - "flatRelationTargetObjectMetadata": { - "createdAt": Any, - "description": "A company", - "duplicateCriteria": null, - "flatFieldMetadatas": [], - "flatIndexMetadatas": [], - "icon": "IconBuildingSkyscraper", - "id": Any, - "imageIdentifierFieldMetadataId": null, - "isActive": true, - "isAuditLogged": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isRemote": false, - "isSearchable": true, - "isSystem": false, - "isUIReadOnly": false, - "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Companies", - "labelSingular": "Company", - "namePlural": "companies", - "nameSingular": "company", - "shortcut": "C", - "standardId": Any, - "standardOverrides": null, - "targetTableName": "DEPRECATED", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "icon": "IconRelationOneToMany", - "id": Any, - "isActive": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": null, - "label": "newFieldLabel", - "morphId": Any, - "name": "newFieldCompanies", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": null, - "standardOverrides": null, - "type": "MORPH_RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - { - "createdAt": Any, - "defaultValue": null, - "description": null, - "flatRelationTargetFieldMetadata": { + ], + "indexMetadatas": [ + { "createdAt": Any, - "defaultValue": null, - "description": "new field description", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": { - "createdAt": Any, - "description": "A company", - "duplicateCriteria": null, - "flatFieldMetadatas": [], - "flatIndexMetadatas": [], - "icon": "IconBuildingSkyscraper", - "id": Any, - "imageIdentifierFieldMetadataId": null, - "isActive": true, - "isAuditLogged": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isRemote": false, - "isSearchable": true, - "isSystem": false, - "isUIReadOnly": false, - "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Companies", - "labelSingular": "Company", - "namePlural": "companies", - "nameSingular": "company", - "shortcut": "C", - "standardId": Any, - "standardOverrides": null, - "targetTableName": "DEPRECATED", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "icon": "IconRelationOneToMany", + "flatIndexFieldMetadatas": [ + { + "createdAt": Any, + "fieldMetadataId": Any, + "id": Any, + "indexMetadataId": Any, + "order": 0, + "updatedAt": Any, + }, + ], "id": Any, - "isActive": true, + "indexType": "BTREE", + "indexWhereClause": null, "isCustom": true, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": null, - "label": "newFieldLabel", - "morphId": Any, - "name": "newFieldCompanies", + "isUnique": false, + "name": "IDX_f687e4e4252800dddd8e5518362", "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": null, - "standardOverrides": null, - "type": "MORPH_RELATION", "universalIdentifier": Any, "updatedAt": Any, "workspaceId": Any, }, - "flatRelationTargetObjectMetadata": { - "createdAt": Any, - "description": "A rocket", - "duplicateCriteria": null, - "flatFieldMetadatas": [], - "flatIndexMetadatas": [], - "icon": "IconRocket", + { + "createdAt": Any, + "flatIndexFieldMetadatas": [ + { + "createdAt": Any, + "fieldMetadataId": Any, + "id": Any, + "indexMetadataId": Any, + "order": 0, + "updatedAt": Any, + }, + ], "id": Any, - "imageIdentifierFieldMetadataId": null, - "isActive": true, - "isAuditLogged": true, + "indexType": "BTREE", + "indexWhereClause": null, "isCustom": true, - "isLabelSyncedWithName": false, - "isRemote": false, - "isSearchable": true, - "isSystem": false, - "isUIReadOnly": false, - "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Rockets", - "labelSingular": "Rocket", - "namePlural": "rockets", - "nameSingular": "rocket", - "shortcut": null, - "standardId": null, - "standardOverrides": null, - "targetTableName": "DEPRECATED", + "isUnique": false, + "name": "IDX_cb15d901e889e25d0a9acecb595", + "objectMetadataId": Any, "universalIdentifier": Any, - "updatedAt": Any, + "updatedAt": Any, "workspaceId": Any, }, - "icon": "IconBuilding", - "id": Any, - "isActive": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": null, - "label": "Company", - "morphId": null, - "name": "company", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "companyId", - "onDelete": "SET_NULL", - "relationType": "MANY_TO_ONE", - }, - "standardId": null, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - ], + ], + }, "status": "success", } `; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts index 9099b322f10..fc35b048e5b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts @@ -87,7 +87,7 @@ describe('fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test s if (result.status !== 'success') { throw new Error('Should never occur, typecheck'); } - expect(result.result.length).toBe( + expect(result.result.flatFieldMetadatas.length).toBe( input.rawCreateFieldInput.morphRelationsCreationPayload.length * 2, ); expect(result).toMatchSnapshot( diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts new file mode 100644 index 00000000000..37b27ee48ee --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant.ts @@ -0,0 +1,14 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +export const FLAT_FIELD_METADATA_EDITABLE_PROPERTIES = [ + 'defaultValue', + 'description', + 'icon', + 'isActive', + 'isLabelSyncedWithName', + 'isUnique', + 'label', + 'name', + 'options', + 'settings', +] as const satisfies (keyof FlatFieldMetadata)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts index 1b8a9b7ec0d..ec0c5c08c6b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant.ts @@ -1,15 +1,7 @@ +import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE = [ - 'defaultValue', - 'description', - 'icon', - 'isActive', - 'isLabelSyncedWithName', - 'isUnique', - 'label', - 'name', - 'options', + ...FLAT_FIELD_METADATA_EDITABLE_PROPERTIES, 'standardOverrides', - 'settings', ] as const satisfies (keyof FlatFieldMetadata)[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts new file mode 100644 index 00000000000..097444c01f7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant.ts @@ -0,0 +1,4 @@ +import { type FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; + +export type FlatFieldMetadataEditableProperties = + (typeof FLAT_FIELD_METADATA_EDITABLE_PROPERTIES)[number]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts index a1af7d4675d..f2617e24615 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util.ts @@ -17,6 +17,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m import { fromMorphRelationCreateFieldInputToFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util'; import { fromRelationCreateFieldInputToFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util'; import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util'; @@ -31,7 +32,10 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ workspaceId, existingFlatObjectMetadataMaps, }: FromCreateFieldInputToFlatObjectMetadataArgs): Promise< - FieldInputTranspilationResult + FieldInputTranspilationResult<{ + flatFieldMetadatas: FlatFieldMetadata[]; + indexMetadatas: FlatIndexMetadata[]; + }> > => { if (rawCreateFieldInput.isRemoteCreation) { return { @@ -99,15 +103,18 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ case FieldMetadataType.RATING: { return { status: 'success', - result: [ - { - ...commonFlatFieldMetadata, - type: createFieldInput.type, - settings: null, - defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ? - options: generateRatingOptions(), - } satisfies FlatFieldMetadata, - ], + result: { + flatFieldMetadatas: [ + { + ...commonFlatFieldMetadata, + type: createFieldInput.type, + settings: null, + defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ? + options: generateRatingOptions(), + } satisfies FlatFieldMetadata, + ], + indexMetadatas: [], + }, }; } case FieldMetadataType.SELECT: @@ -124,15 +131,18 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ return { status: 'success', - result: [ - { - ...commonFlatFieldMetadata, - type: createFieldInput.type, - options, - defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ? - settings: null, - } satisfies FlatFieldMetadata, - ], + result: { + flatFieldMetadatas: [ + { + ...commonFlatFieldMetadata, + type: createFieldInput.type, + options, + defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ? + settings: null, + } satisfies FlatFieldMetadata, + ], + indexMetadatas: [], + }, }; } case FieldMetadataType.TS_VECTOR: { @@ -165,12 +175,15 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({ case FieldMetadataType.ARRAY: { return { status: 'success', - result: [ - { - ...commonFlatFieldMetadata, - type: createFieldInput.type, - }, - ], + result: { + flatFieldMetadatas: [ + { + ...commonFlatFieldMetadata, + type: createFieldInput.type, + }, + ], + indexMetadatas: [], + }, }; } default: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts index c55d180b7a7..a0365d86e49 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts @@ -3,6 +3,7 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { FieldMetadataException, @@ -10,17 +11,27 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; +import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; type FromDeleteFieldInputToFlatFieldMetadatasToDeleteArgs = { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; + existingFlatIndexMaps: FlatEntityMaps; deleteOneFieldInput: DeleteOneFieldInput; }; +// TODO refactor as a side effect service export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({ existingFlatObjectMetadataMaps, deleteOneFieldInput: rawDeleteOneInput, -}: FromDeleteFieldInputToFlatFieldMetadatasToDeleteArgs): FlatFieldMetadata[] => { + existingFlatIndexMaps, +}: FromDeleteFieldInputToFlatFieldMetadatasToDeleteArgs): { + flatFieldMetadatasToDelete: FlatFieldMetadata[]; + flatIndexesToUpdate: FlatIndexMetadata[]; + flatIndexesToDelete: FlatIndexMetadata[]; +} => { const { id: fieldMetadataToDeleteId } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawDeleteOneInput, @@ -46,5 +57,96 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({ flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }); - return [flatFieldMetadataToDelete, ...relatedFlatFieldMetadataToDelete]; + const flatFieldMetadatasToDelete = [ + flatFieldMetadataToDelete, + ...relatedFlatFieldMetadataToDelete, + ]; + + const flatIndexMap = new Map(); + const allFlatIndexes = Object.values(existingFlatIndexMaps.byId).filter( + isDefined, + ); + + for (const flatFieldMetadata of flatFieldMetadatasToDelete) { + allFlatIndexes.forEach((flatIndex) => { + const flatIndexFromMap = flatIndexMap.get(flatIndex.id); + + if (isDefined(flatIndexFromMap)) { + const updatedFlatIndexFields = + flatIndexFromMap.flatIndexFieldMetadatas.filter( + (flatIndexField) => + flatIndexField.fieldMetadataId !== flatFieldMetadata.id, + ); + + flatIndexMap.set(flatIndexFromMap.id, { + ...flatIndexFromMap, + flatIndexFieldMetadatas: updatedFlatIndexFields, + }); + + return; + } + + if ( + flatIndex.objectMetadataId !== flatFieldMetadata.objectMetadataId || + !flatIndex.flatIndexFieldMetadatas.some( + (flatIndexField) => + flatIndexField.fieldMetadataId === flatFieldMetadata.id, + ) + ) { + return; + } + + const updatedFlatIndexFields = flatIndex.flatIndexFieldMetadatas.filter( + (flatIndexField) => + flatIndexField.fieldMetadataId !== flatFieldMetadata.id, + ); + + flatIndexMap.set(flatIndex.id, { + ...flatIndex, + flatIndexFieldMetadatas: updatedFlatIndexFields, + }); + }); + } + + const { flatIndexesToDelete, flatIndexesToUpdate } = [ + ...flatIndexMap.values(), + ].reduce<{ + flatIndexesToUpdate: FlatIndexMetadata[]; + flatIndexesToDelete: FlatIndexMetadata[]; + }>( + (acc, flatIndex) => { + if (flatIndex.flatIndexFieldMetadatas.length === 0) { + return { + ...acc, + flatIndexesToDelete: [...acc.flatIndexesToDelete, flatIndex], + }; + } + + const flatObjectMetadata = + findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + objectMetadataId: flatIndex.objectMetadataId, + }); + + const newIndex = generateFlatIndexMetadataWithNameOrThrow({ + flatObjectMetadata, + flatIndex, + }); + + return { + ...acc, + flatIndexesToUpdate: [...acc.flatIndexesToUpdate, newIndex], + }; + }, + { + flatIndexesToDelete: [], + flatIndexesToUpdate: [], + }, + ); + + return { + flatFieldMetadatasToDelete, + flatIndexesToDelete, + flatIndexesToUpdate, + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts index 1e06ee6e50d..ba3016354f3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -5,13 +5,13 @@ import { v4 } from 'uuid'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util'; import { validateMorphRelationCreationPayload } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -29,9 +29,10 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({ sourceFlatObjectMetadata, workspaceId, }: FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs): Promise< - FieldInputTranspilationResult< - FlatFieldMetadata[] - > + FieldInputTranspilationResult<{ + flatFieldMetadatas: FlatFieldMetadata[]; + indexMetadatas: FlatIndexMetadata[]; + }> > => { const rawMorphCreationPayload = createFieldInput.morphRelationsCreationPayload; @@ -65,8 +66,8 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({ const morphRelationCreationPayload = morphRelationCreationPayloadValidation.result; const morphId = v4(); - const flatFieldMetadatas = morphRelationCreationPayload.flatMap( - ({ relationCreationPayload, targetFlatObjectMetadata }) => { + const flatFieldsAndIndexes = morphRelationCreationPayload.reduce( + (acc, { relationCreationPayload, targetFlatObjectMetadata }) => { const currentMorphRelationFieldName = computeMorphRelationFieldName({ fieldName: createFieldInput.name, relationType: relationCreationPayload.type, @@ -77,23 +78,33 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({ name: currentMorphRelationFieldName, }); - return generateMorphOrRelationFlatFieldMetadataPair({ - createFieldInput: { - ...createFieldInput, - relationCreationPayload, - name: currentMorphRelationFieldName, - }, - sourceFlatObjectMetadataJoinColumnName, - sourceFlatObjectMetadata, - targetFlatObjectMetadata, - workspaceId, - morphId, - }); + const { flatFieldMetadatas, indexMetadatas } = + generateMorphOrRelationFlatFieldMetadataPair({ + createFieldInput: { + ...createFieldInput, + relationCreationPayload, + name: currentMorphRelationFieldName, + }, + sourceFlatObjectMetadataJoinColumnName, + sourceFlatObjectMetadata, + targetFlatObjectMetadata, + workspaceId, + morphId, + }); + + return { + indexMetadatas: [...acc.indexMetadatas, ...indexMetadatas], + flatFieldMetadatas: [...acc.flatFieldMetadatas, ...flatFieldMetadatas], + }; + }, + { + indexMetadatas: [], + flatFieldMetadatas: [], }, ); return { status: 'success', - result: flatFieldMetadatas, + result: flatFieldsAndIndexes, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts index f780e5d9646..9278ed9f0f8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-relation-create-field-input-to-flat-field-metadatas.util.ts @@ -6,8 +6,10 @@ import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadat import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util'; +import { + generateMorphOrRelationFlatFieldMetadataPair, + type SourceTargetMorphOrRelationFlatFieldAndFlatIndex, +} from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util'; import { validateRelationCreationPayload } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-relation-creation-payload.util'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -26,7 +28,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({ createFieldInput, workspaceId, }: FromRelationCreateFieldInputToFlatFieldMetadataArgs): Promise< - FieldInputTranspilationResult[]> + FieldInputTranspilationResult > => { const rawCreationPayload = createFieldInput.relationCreationPayload; @@ -53,7 +55,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({ const { relationCreationPayload, targetFlatObjectMetadata } = relationValidationResult.result; - const flatFieldMetadatas = generateMorphOrRelationFlatFieldMetadataPair({ + const generateResult = generateMorphOrRelationFlatFieldMetadataPair({ createFieldInput: { ...createFieldInput, relationCreationPayload, @@ -69,7 +71,6 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({ return { status: 'success', - result: - flatFieldMetadatas as FlatFieldMetadata[], + result: generateResult, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index 2f734640b3b..9d0f8b4c20e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -5,71 +5,120 @@ import { } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant'; import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FieldMetadataStandardOverridesProperties } from 'src/engine/metadata-modules/field-metadata/types/field-metadata-standard-overrides-properties.type'; -import { FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-properties-to-compare.constant'; +import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; -import { type FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-properties-to-compare.type'; +import { type FlatFieldMetadataEditableProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util'; -import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; +import { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; +import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; import { fromFlatObjectMetadataWithFlatFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-with-flat-field-maps-to-flat-object-metadatas.util'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; -const applyUpdatesToFlatFieldMetadata = ({ - updatedEditableFieldProperties, - flatFieldMetadata, -}: { - updatedEditableFieldProperties: SanitizedUpdateFieldInput; +type UpdatedFlatFieldMetadataAndIndexToUpdate = { flatFieldMetadata: FlatFieldMetadata; -}) => { - return fieldMetadataEditableProperties.reduce((acc, property) => { - let newValue = updatedEditableFieldProperties[property]; - - if (property === 'options' && isDefined(newValue)) { - newValue = updatedEditableFieldProperties[property]?.map((option) => ({ - id: v4(), - ...option, - })); - } - - return { - ...acc, - ...(newValue !== undefined ? { [property]: newValue } : {}), - }; - }, flatFieldMetadata); + flatIndexMetadataToUpdate: FlatIndexMetadata[]; }; -const fieldMetadataEditableProperties = - FLAT_FIELD_METADATA_PROPERTIES_TO_COMPARE.filter( - ( - property, - ): property is Exclude< - FlatFieldMetadataPropertiesToCompare, - 'standardOverrides' - > => property !== 'standardOverrides', - ); - type SanitizedUpdateFieldInput = ReturnType< typeof extractAndSanitizeObjectStringFields< UpdateFieldInput, - (typeof fieldMetadataEditableProperties)[number][] + FlatFieldMetadataEditableProperties[] > >; +type ApplyUpdatesToFlatFieldMetadataArgs = { + updatedEditableFieldProperties: SanitizedUpdateFieldInput; + fromFlatFieldMetadata: FlatFieldMetadata; +} & Pick; + +const applyUpdatesToFlatFieldMetadata = ({ + updatedEditableFieldProperties, + fromFlatFieldMetadata, + flatObjectMetadataMaps, + flatIndexMaps, +}: ApplyUpdatesToFlatFieldMetadataArgs) => { + return FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.reduce( + ({ flatFieldMetadata, flatIndexMetadataToUpdate }, property) => { + const updatedPropertyValue = updatedEditableFieldProperties[property]; + const isPropertyUpdated = + updatedPropertyValue !== undefined && + flatFieldMetadata[property] !== updatedPropertyValue; + + if (!isPropertyUpdated) { + return { + flatFieldMetadata, + flatIndexMetadataToUpdate, + }; + } + const updatedFlatFieldMetadata = { + ...flatFieldMetadata, + [property]: updatedPropertyValue, + }; + + if (property === 'options') { + updatedFlatFieldMetadata.options = + updatedEditableFieldProperties[property]?.map((option) => ({ + id: v4(), + ...option, + })) ?? []; + } + + let newFlatIndexMetadataToUpdate: FlatIndexMetadata[] = []; + + if (property === 'name') { + const flatObjectMetadata = + findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ + flatObjectMetadataMaps, + objectMetadataId: flatFieldMetadata.objectMetadataId, + }); + + newFlatIndexMetadataToUpdate = + recomputeIndexOnFlatFieldMetadataNameUpdate({ + flatObjectMetadata, + fromFlatFieldMetadata, + toFlatFieldMetadata: { + name: updatedFlatFieldMetadata.name, + }, + flatIndexMaps, + }); + } + + return { + flatFieldMetadata: updatedFlatFieldMetadata, + flatIndexMetadataToUpdate: [ + ...flatIndexMetadataToUpdate, + ...newFlatIndexMetadataToUpdate, + ], + }; + }, + { + flatFieldMetadata: structuredClone(fromFlatFieldMetadata), + flatIndexMetadataToUpdate: [], + }, + ); +}; + type FromUpdateFieldInputToFlatFieldMetadataArgs = { - existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; updateFieldInput: UpdateFieldInput; +} & Pick; + +type FlatFieldMetadataAndIndexToUpdate = { + flatFieldMetadatasToUpdate: FlatFieldMetadata[]; + flatIndexMetadatasToUpdate: FlatIndexMetadata[]; }; export const fromUpdateFieldInputToFlatFieldMetadata = ({ - existingFlatObjectMetadataMaps, + flatIndexMaps, + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, updateFieldInput: rawUpdateFieldInput, -}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult< - FlatFieldMetadata[] -> => { +}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { const updateFieldInputInformalProperties = extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [ 'objectMetadataId', @@ -77,7 +126,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ ]); const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( rawUpdateFieldInput, - fieldMetadataEditableProperties, + FLAT_FIELD_METADATA_EDITABLE_PROPERTIES, ); const existingFlatFieldMetadataToUpdate = @@ -168,7 +217,10 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ return { status: 'success', - result: [updatedStandardFlatFieldMetadata], + result: { + flatFieldMetadatasToUpdate: [updatedStandardFlatFieldMetadata], + flatIndexMetadatasToUpdate: [], + }, }; } @@ -184,12 +236,32 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ ]; const optimisticiallyUpdatedFlatFieldMetadatas = - flatFieldMetadatasToUpdate.map((flatFieldMetadata) => { - return applyUpdatesToFlatFieldMetadata({ - flatFieldMetadata, - updatedEditableFieldProperties, - }); - }); + flatFieldMetadatasToUpdate.reduce( + (acc, fromFlatFieldMetadata) => { + const { flatFieldMetadata, flatIndexMetadataToUpdate } = + applyUpdatesToFlatFieldMetadata({ + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + fromFlatFieldMetadata, + flatIndexMaps, + updatedEditableFieldProperties, + }); + + return { + flatFieldMetadatasToUpdate: [ + ...acc.flatFieldMetadatasToUpdate, + flatFieldMetadata, + ], + flatIndexMetadatasToUpdate: [ + ...acc.flatIndexMetadatasToUpdate, + ...flatIndexMetadataToUpdate, + ], + }; + }, + { + flatFieldMetadatasToUpdate: [], + flatIndexMetadatasToUpdate: [], + }, + ); return { status: 'success', diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts index 86eeb60dcdb..b67a55caa93 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts @@ -6,7 +6,10 @@ import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modul import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @@ -42,6 +45,12 @@ type GenerateMorphOrRelationFlatFieldMetadataPairArgs = { workspaceId: string; morphId?: string | null; }; + +export type SourceTargetMorphOrRelationFlatFieldAndFlatIndex = { + flatFieldMetadatas: FlatFieldMetadata[]; + indexMetadatas: FlatIndexMetadata[]; +}; + export const generateMorphOrRelationFlatFieldMetadataPair = ({ createFieldInput, sourceFlatObjectMetadata, @@ -49,7 +58,7 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({ workspaceId, sourceFlatObjectMetadataJoinColumnName, morphId = null, -}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): FlatFieldMetadata[] => { +}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): SourceTargetMorphOrRelationFlatFieldAndFlatIndex => { const { relationCreationPayload } = createFieldInput; const sourceFlatFieldMetadataSettings = @@ -117,11 +126,64 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({ flatRelationTargetObjectMetadata: sourceFlatObjectMetadata, }; - return [ - { - ...sourceFlatFieldMetadata, - flatRelationTargetFieldMetadata: targetFlatFieldMetadata, - }, - targetFlatFieldMetadata, - ] satisfies FlatFieldMetadata[]; + const indexId = v4(); + const createdAt = new Date(); + const indexMetadata: FlatIndexMetadata = + generateFlatIndexMetadataWithNameOrThrow({ + flatIndex: { + createdAt, + flatIndexFieldMetadatas: [ + { + createdAt, + fieldMetadataId: + relationCreationPayload.type === RelationType.MANY_TO_ONE + ? sourceFlatFieldMetadata.id + : targetFlatFieldMetadata.id, + id: v4(), + indexMetadataId: indexId, + order: 0, + updatedAt: createdAt, + }, + ], + id: indexId, + indexType: IndexType.BTREE, + indexWhereClause: null, + isCustom: true, + isUnique: false, + objectMetadataId: + relationCreationPayload.type === RelationType.MANY_TO_ONE + ? sourceFlatObjectMetadata.id + : targetFlatObjectMetadata.id, + universalIdentifier: indexId, + updatedAt: createdAt, + workspaceId, + }, + flatObjectMetadata: (relationCreationPayload.type === + RelationType.MANY_TO_ONE + ? { + ...sourceFlatObjectMetadata, + flatFieldMetadatas: [ + ...sourceFlatObjectMetadata.flatFieldMetadatas, + sourceFlatFieldMetadata, + ], + } + : { + ...targetFlatObjectMetadata, + flatFieldMetadatas: [ + ...targetFlatObjectMetadata.flatFieldMetadatas, + targetFlatFieldMetadata, + ], + }) as FlatObjectMetadata, + }); + + return { + flatFieldMetadatas: [ + { + ...sourceFlatFieldMetadata, + flatRelationTargetFieldMetadata: targetFlatFieldMetadata, + }, + targetFlatFieldMetadata, + ], + indexMetadatas: [indexMetadata], + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts new file mode 100644 index 00000000000..1130b465f8f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-index-on-flat-field-metadata-name-update.util.ts @@ -0,0 +1,60 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; + +type RecomputeIndexOnFlatFieldMetadataNameUpdateArgs = { + flatObjectMetadata: FlatObjectMetadata; + fromFlatFieldMetadata: FlatFieldMetadata; + toFlatFieldMetadata: Pick; +} & Pick; + +export const recomputeIndexOnFlatFieldMetadataNameUpdate = ({ + fromFlatFieldMetadata, + toFlatFieldMetadata, + flatObjectMetadata, + flatIndexMaps, +}: RecomputeIndexOnFlatFieldMetadataNameUpdateArgs): FlatIndexMetadata[] => { + const relatedFlatIndexMetadata = Object.values(flatIndexMaps.byId).filter( + (flatIndexMetadata): flatIndexMetadata is FlatIndexMetadata => + isDefined(flatIndexMetadata) && + flatIndexMetadata.objectMetadataId === + fromFlatFieldMetadata.objectMetadataId && + flatIndexMetadata.flatIndexFieldMetadatas.some( + (flatIndexField) => + flatIndexField.fieldMetadataId === fromFlatFieldMetadata.id, + ), + ); + + if (relatedFlatIndexMetadata.length === 0) { + return []; + } + + const optimisticFlatObjectMetadata = { + ...flatObjectMetadata, + flatFieldMetadatas: flatObjectMetadata.flatFieldMetadatas.map( + (flatFieldMetadata) => { + if (flatFieldMetadata.id === fromFlatFieldMetadata.id) { + return { + ...flatFieldMetadata, + name: toFlatFieldMetadata.name, + }; + } + + return flatFieldMetadata; + }, + ), + }; + + return relatedFlatIndexMetadata.map((flatIndex) => { + const newIndex = generateFlatIndexMetadataWithNameOrThrow({ + flatObjectMetadata: optimisticFlatObjectMetadata, + flatIndex, + }); + + return newIndex; + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type.ts index 3b036d2adbd..b8840c820a2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type.ts @@ -9,16 +9,18 @@ export type IndexMetadataRelationProperties = MetadataEntitiesRelationTarget >; +export type FlatIndexFieldMetadata = Omit< + IndexFieldMetadataEntity, + ExtractRecordTypeOrmRelationProperties< + IndexFieldMetadataEntity, + MetadataEntitiesRelationTarget + > +>; + export type FlatIndexMetadata = Omit< IndexMetadataEntity, IndexMetadataRelationProperties > & { universalIdentifier: string; - flatIndexFieldMetadatas: Omit< - IndexFieldMetadataEntity, - ExtractRecordTypeOrmRelationProperties< - IndexFieldMetadataEntity, - MetadataEntitiesRelationTarget - > - >[]; + flatIndexFieldMetadatas: FlatIndexFieldMetadata[]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/attachment-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/attachment-flat-object.mock.ts index 27dc6cbcdab..db96f13d788 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/attachment-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/attachment-flat-object.mock.ts @@ -24,6 +24,5 @@ export const ATTACHMENT_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a', - flatIndexMetadatas: [], flatFieldMetadatas: Object.values(ATTACHMENT_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock.ts index 4206fe7a78b..fd747e6a5ab 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock.ts @@ -24,6 +24,5 @@ export const COMPANY_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', - flatIndexMetadatas: [], flatFieldMetadatas: Object.values(COMPANY_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-flat-object.mock.ts index 24226298dde..1f26dc1639d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-flat-object.mock.ts @@ -24,6 +24,5 @@ export const FAVORITE_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-ab56-4e05-92a3-e2414a499860', - flatIndexMetadatas: [], flatFieldMetadatas: Object.values(FAVORITE_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-folder-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-folder-flat-object.mock.ts index a755d23f8c3..50377b710c2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-folder-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/favorite-folder-flat-object.mock.ts @@ -24,6 +24,5 @@ export const FAVORITE_FOLDER_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-7cf8-401f-8211-a9587d27fd2d', - flatIndexMetadatas: [], flatFieldMetadatas: Object.values(FAVORITE_FOLDER_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts index 7c9a5417e3a..ee6f5e5c562 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock.ts @@ -13,7 +13,7 @@ export const getFlatObjectMetadataMock = ( return { flatFieldMetadatas: [], - flatIndexMetadatas: [], + description: 'default flat object metadata description', icon: 'icon', id: faker.string.uuid(), diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-flat-object.mock.ts index 6a36f3fc469..f030fdf8090 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-flat-object.mock.ts @@ -24,6 +24,6 @@ export const NOTE_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-0b00-45cd-b6f6-6cd806fc6804', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(NOTE_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-target-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-target-flat-object.mock.ts index 240e12e3ce4..e9640d1d416 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-target-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/note-target-flat-object.mock.ts @@ -24,6 +24,6 @@ export const NOTE_TARGET_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-fff0-4b44-be82-bda313884400', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(NOTETARGET_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/opportunity-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/opportunity-flat-object.mock.ts index 16787d8e560..9aa464a7f24 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/opportunity-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/opportunity-flat-object.mock.ts @@ -24,6 +24,5 @@ export const OPPORTUNITY_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-9549-49dd-b2b2-883999db8938', - flatIndexMetadatas: [], flatFieldMetadatas: Object.values(OPPORTUNITY_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/person-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/person-flat-object.mock.ts index 9fbcf52c910..8b388b99b94 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/person-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/person-flat-object.mock.ts @@ -24,6 +24,6 @@ export const PERSON_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-e674-48e5-a542-72570eee7213', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(PERSON_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock.ts index 3b50f39cab6..79b7afcd97e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock.ts @@ -24,6 +24,6 @@ export const PET_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: 'd34e0f07-1b8c-4de0-938e-599cf05e1f7f', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(PET_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock.ts index 10b73812d9c..9b8fa631d1f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/rocket-flat-object.mock.ts @@ -24,6 +24,6 @@ export const ROCKET_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: 'd78ec657-74a4-4652-a350-1f44ff62970a', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(ROCKET_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-flat-object.mock.ts index 3a5b9e3febf..cf0feba12d1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-flat-object.mock.ts @@ -24,6 +24,6 @@ export const TASK_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-1ba1-48ba-bc83-ef7e5990ed10', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(TASK_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-target-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-target-flat-object.mock.ts index c6e5d0363e2..353fc6ab1f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-target-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/task-target-flat-object.mock.ts @@ -24,6 +24,6 @@ export const TASK_TARGET_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1', - flatIndexMetadatas: [], + flatFieldMetadatas: Object.values(TASKTARGET_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/timeline-activity-flat-object.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/timeline-activity-flat-object.mock.ts index a8a0569c20d..ccfc6b5a428 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/timeline-activity-flat-object.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/__mocks__/timeline-activity-flat-object.mock.ts @@ -24,6 +24,5 @@ export const TIMELINE_ACTIVITY_FLAT_OBJECT_MOCK = getFlatObjectMetadataMock({ isLabelSyncedWithName: false, workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', universalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5', - flatIndexMetadatas: [], flatFieldMetadatas: Object.values(TIMELINEACTIVITY_FLAT_FIELDS_MOCK), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts index 953ff310bb4..1d54ed2fe22 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts @@ -1,5 +1,4 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type'; import { type MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type'; @@ -23,7 +22,6 @@ export type FlatObjectMetadata = Omit< ObjectMetadataRelationProperties | 'dataSourceId' > & { universalIdentifier: string; - flatIndexMetadatas: FlatIndexMetadata[]; flatFieldMetadatas: FlatFieldMetadata[]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts index 82ea0e121b1..1e42caa002c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts @@ -5,10 +5,12 @@ import { import { v4 } from 'uuid'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { buildDefaultFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util'; +import { buildDefaultIndexesForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util'; import { buildDefaultRelationFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util'; type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs = @@ -24,7 +26,8 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre existingFlatObjectMetadataMaps, }: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): { flatObjectMetadataToCreate: FlatObjectMetadata; - relationTargetFlatFieldMetadatas: FlatFieldMetadata[]; + relationTargetFlatFieldMetadataToCreate: FlatFieldMetadata[]; + flatIndexMetadataToCreate: FlatIndexMetadata[]; } => { const createObjectInput = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -41,9 +44,11 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre ); const objectMetadataId = v4(); - const baseCustomFlatFieldMetadatas = + const defaultFlatFieldForCustomObjectMaps = buildDefaultFlatFieldMetadatasForCustomObject({ - objectMetadataId, + flatObjectMetadata: { + id: objectMetadataId, + }, workspaceId, }); const createdAt = new Date(); @@ -53,7 +58,6 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre updatedAt: createdAt, duplicateCriteria: null, description: createObjectInput.description ?? null, - flatIndexMetadatas: [], icon: createObjectInput.icon ?? null, id: objectMetadataId, imageIdentifierFieldMetadataId: null, @@ -65,7 +69,8 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre isSearchable: true, isUIReadOnly: false, isSystem: false, - labelIdentifierFieldMetadataId: baseCustomFlatFieldMetadatas.nameField.id, + labelIdentifierFieldMetadataId: + defaultFlatFieldForCustomObjectMaps.fields.nameField.id, labelPlural: capitalize(createObjectInput.labelPlural), labelSingular: capitalize(createObjectInput.labelSingular), namePlural: createObjectInput.namePlural, @@ -77,6 +82,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre targetTableName: 'DEPRECATED', workspaceId, }; + const { standardSourceFlatFieldMetadatas, standardTargetFlatFieldMetadatas, @@ -87,12 +93,21 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre }); flatObjectMetadataToCreate.flatFieldMetadatas = [ - ...Object.values(baseCustomFlatFieldMetadatas), + ...Object.values(defaultFlatFieldForCustomObjectMaps.fields), ...standardSourceFlatFieldMetadatas, ]; + const defaultIndexesForCustomObject = buildDefaultIndexesForCustomObject({ + defaultFlatFieldForCustomObjectMaps, + flatObjectMetadata: flatObjectMetadataToCreate, + workspaceId, + }); + return { flatObjectMetadataToCreate, - relationTargetFlatFieldMetadatas: standardTargetFlatFieldMetadatas, + relationTargetFlatFieldMetadataToCreate: standardTargetFlatFieldMetadatas, + flatIndexMetadataToCreate: Object.values( + defaultIndexesForCustomObject.indexes, + ), }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts index 52b4e9b07cb..bbd6d61924c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util.ts @@ -3,9 +3,11 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { findRelationFlatFieldMetadataTargetFlatFieldMetadataOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-relation-flat-field-metadatas-target-flat-field-metadata-or-throw.util'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -15,15 +17,20 @@ import { ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +type FromDeleteObjectInputToFlatFieldMetadatasToDeleteArgs = { + deleteObjectInput: DeleteOneObjectInput; + existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; + existingFlatIndexMaps: FlatEntityMaps; +}; export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({ deleteObjectInput: rawDeleteObjectInput, existingFlatObjectMetadataMaps, -}: { - deleteObjectInput: DeleteOneObjectInput; - existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; -}): { + existingFlatIndexMaps, + // This should return an AllFlatEntityMaps +}: FromDeleteObjectInputToFlatFieldMetadatasToDeleteArgs): { flatFieldMetadatasToDelete: FlatFieldMetadata[]; flatObjectMetadataToDelete: FlatObjectMetadata; + flatIndexToDelete: FlatIndexMetadata[]; } => { const { id: objectMetadataToDeleteId } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -61,8 +68,18 @@ export const fromDeleteObjectInputToFlatFieldMetadatasToDelete = ({ }, ); + // We should maintain a idsByObjectMetadataId maps in the index + const flatIndexMetadataToDelete = Object.values( + existingFlatIndexMaps.byId, + ).filter( + (flatIndex): flatIndex is FlatIndexMetadata => + isDefined(flatIndex) && + flatIndex.objectMetadataId === flatObjectMetadataToDelete.id, + ); + return { flatFieldMetadatasToDelete, flatObjectMetadataToDelete, + flatIndexToDelete: flatIndexMetadataToDelete, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts index fb65b04a7f1..66d80666d4b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts @@ -17,7 +17,6 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ( return { ...objectMetadataEntityWithoutRelations, - flatIndexMetadatas: [], // TODO prastoin handle indexes universalIdentifier: objectMetadataEntityWithoutRelations.standardId ?? objectMetadataEntityWithoutRelations.id, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util.ts index 9cd39768deb..74af41c130e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util.ts @@ -35,6 +35,5 @@ export const fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata = ({ ...rest, flatFieldMetadatas, universalIdentifier: rest.standardId ?? rest.id, - flatIndexMetadatas: [], // prastoin TODO convert from indexMetadatas to flatIndexMetadatas }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts index 2f08e5dac5a..0f43e92f646 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata.util.ts @@ -4,12 +4,15 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; +import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; import { findFlatObjectMetadataInFlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps.util'; import { FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-properties-to-compare.constant'; import { type FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-properties-to-compare.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util'; import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util'; import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant'; import { type UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; @@ -23,7 +26,7 @@ import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standar type FromUpdateObjectInputToFlatObjectMetadataArgs = { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; updateObjectInput: UpdateOneObjectInput; -}; +} & Pick; const objectMetadataEditableProperties = FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE.filter( @@ -37,12 +40,14 @@ const objectMetadataEditableProperties = type UpdatedFlatObjectAndOtherObjectFieldMetadatas = { flatObjectMetadata: FlatObjectMetadata; - otherObjectFlatFieldMetadatas: FlatFieldMetadata[]; + otherObjectFlatFieldMetadataToUpdate: FlatFieldMetadata[]; + flatIndexMetadataToUpdate: FlatIndexMetadata[]; }; export const fromUpdateObjectInputToFlatObjectMetadata = ({ existingFlatObjectMetadataMaps, updateObjectInput: rawUpdateObjectInput, + flatIndexMaps, }: FromUpdateObjectInputToFlatObjectMetadataArgs): UpdatedFlatObjectAndOtherObjectFieldMetadatas => { const { id: objectMetadataIdToUpdate } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -102,17 +107,26 @@ export const fromUpdateObjectInputToFlatObjectMetadata = ({ return { flatObjectMetadata: updatedStandardFlatObjectdMetadata, - otherObjectFlatFieldMetadatas: [], + otherObjectFlatFieldMetadataToUpdate: [], + flatIndexMetadataToUpdate: [], }; } const initialAccumulator: UpdatedFlatObjectAndOtherObjectFieldMetadatas = { flatObjectMetadata: flatObjectMetadataToUpdate, - otherObjectFlatFieldMetadatas: [], + otherObjectFlatFieldMetadataToUpdate: [], + flatIndexMetadataToUpdate: [], }; return objectMetadataEditableProperties.reduce( - ({ flatObjectMetadata, otherObjectFlatFieldMetadatas }, property) => { + ( + { + flatObjectMetadata, + otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas, + flatIndexMetadataToUpdate, + }, + property, + ) => { const updatedPropertyValue = updatedEditableObjectProperties[property]; const isPropertyUpdated = updatedPropertyValue !== undefined && @@ -121,7 +135,8 @@ export const fromUpdateObjectInputToFlatObjectMetadata = ({ if (!isPropertyUpdated) { return { flatObjectMetadata, - otherObjectFlatFieldMetadatas, + otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas, + flatIndexMetadataToUpdate, }; } @@ -129,6 +144,7 @@ export const fromUpdateObjectInputToFlatObjectMetadata = ({ ...flatObjectMetadata, [property]: updatedPropertyValue, }; + const newUpdatedOtherObjectFlatFieldMetadatas = property === 'nameSingular' || property === 'namePlural' ? renameRelatedMorphFieldOnObjectNamesUpdate({ @@ -138,12 +154,25 @@ export const fromUpdateObjectInputToFlatObjectMetadata = ({ }) : []; + const newUpdatedFlatIndexMetadatas = + property === 'nameSingular' + ? recomputeIndexAfterFlatObjectMetadataSingularNameUpdate({ + existingFlatObjectMetadata: flatObjectMetadataToUpdate, + flatIndexMaps, + updatedSingularName: updatedFlatObjectMetadata.nameSingular, + }) + : []; + return { flatObjectMetadata: updatedFlatObjectMetadata, - otherObjectFlatFieldMetadatas: [ + otherObjectFlatFieldMetadataToUpdate: [ ...otherObjectFlatFieldMetadatas, ...newUpdatedOtherObjectFlatFieldMetadatas, ], + flatIndexMetadataToUpdate: [ + ...flatIndexMetadataToUpdate, + ...newUpdatedFlatIndexMetadatas, + ], }; }, initialAccumulator, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts new file mode 100644 index 00000000000..299410ef6fa --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util.ts @@ -0,0 +1,40 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; + +type RecomputeIndexAfterFlatObjectMetadataSingularNameUpdateArgs = { + existingFlatObjectMetadata: FlatObjectMetadata; + updatedSingularName: string; +} & Pick; +export const recomputeIndexAfterFlatObjectMetadataSingularNameUpdate = ({ + existingFlatObjectMetadata, + flatIndexMaps, + updatedSingularName, +}: RecomputeIndexAfterFlatObjectMetadataSingularNameUpdateArgs): FlatIndexMetadata[] => { + const allRelatedFlatIndexMetadata = Object.values(flatIndexMaps.byId).filter( + (flatIndexMetadata): flatIndexMetadata is FlatIndexMetadata => + isDefined(flatIndexMetadata) && + flatIndexMetadata.objectMetadataId === existingFlatObjectMetadata.id, + ); + + if (allRelatedFlatIndexMetadata.length === 0) { + return []; + } + + const optimisticFlatObjectMetadata: FlatObjectMetadata = { + ...existingFlatObjectMetadata, + nameSingular: updatedSingularName, + }; + + return allRelatedFlatIndexMetadata.map((flatIndex) => { + const newIndex = generateFlatIndexMetadataWithNameOrThrow({ + flatIndex, + flatObjectMetadata: optimisticFlatObjectMetadata, + }); + + return newIndex; + }); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts index f9a73a38466..ff570357544 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util.ts @@ -37,6 +37,7 @@ type RenameRelatedMorphFieldOnObjectNamesUpdateArgs = FromTo< > & { existingFlatObjectMetadataMaps: FlatObjectMetadataMaps; }; +// We should recompute each index here too ? YES TODO prastoin export const renameRelatedMorphFieldOnObjectNamesUpdate = ({ fromFlatObjectMetadata, existingFlatObjectMetadataMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2.ts new file mode 100644 index 00000000000..66f4fa689d1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2.ts @@ -0,0 +1,33 @@ +import { createHash } from 'crypto'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; + +type GenerateDeterministicIndexNameArgs = { + flatObjectMetadata: Pick; + isUnique?: boolean; + relatedFieldNames: Pick[]; +}; +export const generateDeterministicIndexNameV2 = ({ + relatedFieldNames, + flatObjectMetadata, + isUnique = false, +}: GenerateDeterministicIndexNameArgs): string => { + const hash = createHash('sha256'); + + const tableName = computeTableName( + flatObjectMetadata.nameSingular, + flatObjectMetadata.isCustom, + ); + + const columnsNames = relatedFieldNames.map( + (flatFieldMetadata) => flatFieldMetadata.name, + ); + + [tableName, ...columnsNames].forEach((column) => { + hash.update(column); + }); + + return `IDX_${isUnique ? 'UNIQUE_' : ''}${hash.digest('hex').slice(0, 27)}`; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts new file mode 100644 index 00000000000..bddbb4cc6d4 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts @@ -0,0 +1,56 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { generateDeterministicIndexNameV2 } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name-v2'; + +type GenerateFlatIndexArgs = { + flatObjectMetadata: FlatObjectMetadata; + flatIndex: Omit; +}; +export const generateFlatIndexMetadataWithNameOrThrow = ({ + flatObjectMetadata, + flatIndex, +}: GenerateFlatIndexArgs): FlatIndexMetadata => { + const orderedFlatFieldNames = flatIndex.flatIndexFieldMetadatas + .sort((a, b) => a.order - b.order) + .map((flatIndexField) => { + const relatedFlatFieldMetadata = + flatObjectMetadata.flatFieldMetadatas.find( + (flatFieldMetadata) => + flatFieldMetadata.id === flatIndexField.fieldMetadataId, + ); + + if (!isDefined(relatedFlatFieldMetadata)) { + throw new FlatEntityMapsException( + 'Could not find flat index field related field in cache', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const name = isMorphOrRelationFlatFieldMetadata(relatedFlatFieldMetadata) + ? (relatedFlatFieldMetadata.settings.joinColumnName ?? + relatedFlatFieldMetadata.name) + : relatedFlatFieldMetadata.name; + + return { + name, + }; + }); + + const name = generateDeterministicIndexNameV2({ + flatObjectMetadata, + isUnique: flatIndex.isUnique, + relatedFieldNames: orderedFlatFieldNames, + }); + + return { + ...flatIndex, + name, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index 123c1c1640f..133e76cb085 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -4,6 +4,8 @@ import { isDefined } from 'twenty-shared/utils'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.'; import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; @@ -53,20 +55,25 @@ export class ObjectMetadataServiceV2 { workspaceId: string; updateObjectInput: UpdateOneObjectInput; }): Promise { - const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + const { + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + flatIndexMaps: existingFlatIndexMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatEntities: ['flatObjectMetadataMaps'], + flatEntities: ['flatObjectMetadataMaps', 'flatIndexMaps'], }, ); const { flatObjectMetadata: optimisticallyUpdatedFlatObjectMetadata, - otherObjectFlatFieldMetadatas, + otherObjectFlatFieldMetadataToUpdate: otherObjectFlatFieldMetadatas, + flatIndexMetadataToUpdate, } = fromUpdateObjectInputToFlatObjectMetadata({ existingFlatObjectMetadataMaps, updateObjectInput, + flatIndexMaps: existingFlatIndexMaps, }); const impactedObjectMetadataIds = [ @@ -93,6 +100,15 @@ export class ObjectMetadataServiceV2 { }), ); + const toFlatIndexMaps = flatIndexMetadataToUpdate.reduce( + (flatIndexMaps, flatIndexMetadata) => + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: flatIndexMetadata, + flatEntityMaps: flatIndexMaps, + }), + existingFlatIndexMaps, + ); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -101,6 +117,10 @@ export class ObjectMetadataServiceV2 { from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, }, + flatIndexMaps: { + from: existingFlatIndexMaps, + to: toFlatIndexMaps, + }, }, buildOptions: { isSystemBuild: false, @@ -155,19 +175,26 @@ export class ObjectMetadataServiceV2 { deleteObjectInput: DeleteOneObjectInput; workspaceId: string; }): Promise { - const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = + const { + flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + flatIndexMaps: existingFlatIndexMaps, + } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatEntities: ['flatObjectMetadataMaps'], + flatEntities: ['flatObjectMetadataMaps', 'flatIndexMaps'], }, ); - const { flatFieldMetadatasToDelete, flatObjectMetadataToDelete } = - fromDeleteObjectInputToFlatFieldMetadatasToDelete({ - deleteObjectInput, - existingFlatObjectMetadataMaps, - }); + const { + flatFieldMetadatasToDelete, + flatObjectMetadataToDelete, + flatIndexToDelete, + } = fromDeleteObjectInputToFlatFieldMetadatasToDelete({ + existingFlatIndexMaps, + deleteObjectInput, + existingFlatObjectMetadataMaps, + }); const { id: objectMetadataToDeleteId } = flatObjectMetadataToDelete; const impactedObjectMetadataIds = Array.from( @@ -202,6 +229,15 @@ export class ObjectMetadataServiceV2 { }), ); + const toFlatIndexMaps = flatIndexToDelete.reduce( + (flatIndexMaps, flatIndex) => + deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: flatIndex.id, + flatEntityMaps: flatIndexMaps, + }), + existingFlatIndexMaps, + ); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -210,6 +246,10 @@ export class ObjectMetadataServiceV2 { from: fromFlatObjectMetadataMaps, to: toFlatObjectMetadataMaps, }, + flatIndexMaps: { + from: existingFlatIndexMaps, + to: toFlatIndexMaps, + }, }, buildOptions: { inferDeletionFromMissingEntities: true, @@ -242,6 +282,7 @@ export class ObjectMetadataServiceV2 { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatViewMaps: existingFlatViewMaps, flatViewFieldMaps: existingFlatViewFieldMaps, + flatIndexMaps: existingFlatIndexMaps, } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, @@ -249,19 +290,23 @@ export class ObjectMetadataServiceV2 { 'flatObjectMetadataMaps', 'flatViewMaps', 'flatViewFieldMaps', + 'flatIndexMaps', ], }, ); - const { flatObjectMetadataToCreate, relationTargetFlatFieldMetadatas } = - fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate({ - createObjectInput, - workspaceId, - existingFlatObjectMetadataMaps, - }); + const { + flatObjectMetadataToCreate, + relationTargetFlatFieldMetadataToCreate, + flatIndexMetadataToCreate, + } = fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate({ + createObjectInput, + workspaceId, + existingFlatObjectMetadataMaps, + }); const existingFlatObjectMetadataMapsWithTargetRelationFlatFieldMetadatas = - relationTargetFlatFieldMetadatas.reduce( + relationTargetFlatFieldMetadataToCreate.reduce( (flatObjectMetadataMaps, flatFieldMetadata) => addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow({ flatFieldMetadata, @@ -272,7 +317,7 @@ export class ObjectMetadataServiceV2 { const flatObjectMetadataMapsWithTargetRelationFlatFieldMetadatas = getSubFlatObjectMetadataMapsOutOfFlatFieldMetadatasOrThrow({ - flatFieldMetadatas: relationTargetFlatFieldMetadatas, + flatFieldMetadatas: relationTargetFlatFieldMetadataToCreate, flatObjectMetadataMaps: existingFlatObjectMetadataMapsWithTargetRelationFlatFieldMetadatas, }); @@ -286,7 +331,7 @@ export class ObjectMetadataServiceV2 { const impactedObjectMetadataIds = [ ...new Set( - relationTargetFlatFieldMetadatas.map( + relationTargetFlatFieldMetadataToCreate.map( ({ objectMetadataId }) => objectMetadataId, ), ), @@ -329,6 +374,15 @@ export class ObjectMetadataServiceV2 { existingFlatViewFieldMaps, ); + const toFlatIndexMaps = flatIndexMetadataToCreate.reduce( + (flatIndexMaps, flatIndexMetadata) => + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatIndexMetadata, + flatEntityMaps: flatIndexMaps, + }), + existingFlatIndexMaps, + ); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -345,6 +399,10 @@ export class ObjectMetadataServiceV2 { from: existingFlatViewFieldMaps, to: toFlatViewFieldMaps, }, + flatIndexMaps: { + from: existingFlatIndexMaps, + to: toFlatIndexMaps, + }, }, buildOptions: { isSystemBuild: false, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts index ec8e76ed6b8..90d1fdeee26 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts @@ -2,6 +2,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { v4 } from 'uuid'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { BASE_OBJECT_STANDARD_FIELD_IDS, CUSTOM_OBJECT_STANDARD_FIELD_IDS, @@ -10,12 +11,16 @@ import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-mana type BuildDefaultFlatFieldMetadataForCustomObjectArgs = { workspaceId: string; - objectMetadataId: string; + flatObjectMetadata: Pick; }; +export type DefaultFlatFieldForCustomObjectMaps = ReturnType< + typeof buildDefaultFlatFieldMetadatasForCustomObject +>; +// This could be replaced totally by an import schema + its transpilation when it's ready export const buildDefaultFlatFieldMetadatasForCustomObject = ({ workspaceId, - objectMetadataId, + flatObjectMetadata: { id: objectMetadataId }, }: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => { const createdAt = new Date(); const idField: FlatFieldMetadata = { @@ -278,13 +283,17 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ }; return { - idField, - nameField, - createdAtField, - updatedAtField, - deletedAtField, - createdByField, - positionField, - searchVectorField, - } as const; + fields: { + idField, + nameField, + createdAtField, + updatedAtField, + deletedAtField, + createdByField, + positionField, + searchVectorField, + }, + } as const satisfies { + fields: Record; + }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts new file mode 100644 index 00000000000..20f23278c83 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util.ts @@ -0,0 +1,52 @@ +import { v4 } from 'uuid'; + +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; +import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; +import { type DefaultFlatFieldForCustomObjectMaps } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util'; + +export const buildDefaultIndexesForCustomObject = ({ + workspaceId, + flatObjectMetadata, + defaultFlatFieldForCustomObjectMaps, +}: { + workspaceId: string; + flatObjectMetadata: FlatObjectMetadata; + defaultFlatFieldForCustomObjectMaps: DefaultFlatFieldForCustomObjectMaps; +}) => { + const tsFlatVectorIndexId = v4(); + const createdAt = new Date(); + const tsVectorFlatIndex = generateFlatIndexMetadataWithNameOrThrow({ + flatIndex: { + createdAt, + flatIndexFieldMetadatas: [ + { + createdAt, + fieldMetadataId: + defaultFlatFieldForCustomObjectMaps.fields.searchVectorField.id, + id: v4(), + indexMetadataId: tsFlatVectorIndexId, + order: 0, + updatedAt: createdAt, + }, + ], + id: tsFlatVectorIndexId, + indexType: IndexType.GIN, + indexWhereClause: null, + isCustom: false, + isUnique: false, + objectMetadataId: flatObjectMetadata.id, + universalIdentifier: tsFlatVectorIndexId, + updatedAt: createdAt, + workspaceId, + }, + flatObjectMetadata, + }); + + return { + indexes: { + tsVectorFlatIndex, + }, + } as const satisfies { indexes: Record }; +}; diff --git a/packages/twenty-server/src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module.ts b/packages/twenty-server/src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module.ts index cc8b721b5fa..438a616a78f 100644 --- a/packages/twenty-server/src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module.ts +++ b/packages/twenty-server/src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module.ts @@ -6,7 +6,9 @@ import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-fiel import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/core-modules/view/flat-view/services/workspace-flat-view-field-map-cache.service'; import { WorkspaceFlatViewMapCacheService } from 'src/engine/core-modules/view/flat-view/services/workspace-flat-view-map-cache.service'; +import { WorkspaceFlatIndexMapCacheService } from 'src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service'; import { WorkspaceFlatObjectMetadataMapCacheService } from 'src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @@ -18,19 +20,25 @@ import { WorkspaceFlatMapCacheRegistryService } from 'src/engine/workspace-flat- WorkspaceMetadataCacheModule, WorkspaceMetadataVersionModule, WorkspacePermissionsCacheModule, - TypeOrmModule.forFeature([ViewEntity, ViewFieldEntity]), + TypeOrmModule.forFeature([ + ViewEntity, + ViewFieldEntity, + IndexMetadataEntity, + ]), ], providers: [ WorkspaceFlatMapCacheRegistryService, WorkspaceFlatObjectMetadataMapCacheService, WorkspaceFlatViewMapCacheService, WorkspaceFlatViewFieldMapCacheService, + WorkspaceFlatIndexMapCacheService, ], exports: [ WorkspaceFlatMapCacheRegistryService, WorkspaceFlatObjectMetadataMapCacheService, WorkspaceFlatViewMapCacheService, WorkspaceFlatViewFieldMapCacheService, + WorkspaceFlatIndexMapCacheService, ], }) export class WorkspaceFlatMapCacheModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts index e3c3556905d..451dfd204d4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts @@ -208,14 +208,14 @@ export class WorkspaceMigrationBuildOrchestratorService { relatedFlatEntityMapsKeys, actions: [ // Object and fields - ...orchestratorActionsReport.fieldMetadata.deleted, ...orchestratorActionsReport.index.deleted, + ...orchestratorActionsReport.fieldMetadata.deleted, ...orchestratorActionsReport.objectMetadata.deleted, ...orchestratorActionsReport.objectMetadata.created, - ...orchestratorActionsReport.index.created, ...orchestratorActionsReport.objectMetadata.updated, ...orchestratorActionsReport.fieldMetadata.created, ...orchestratorActionsReport.fieldMetadata.updated, + ...orchestratorActionsReport.index.created, ...orchestratorActionsReport.index.updated, /// diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap index 4642ca08122..e6fe94f7ef6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap @@ -2408,653 +2408,6 @@ exports[`Workspace migration builder object actions test suite It should build a } `; -exports[`Workspace migration builder object actions test suite It should build a create_object and create_index actions for each of this fieldMetadata 1`] = ` -{ - "created": [], - "deleted": [], - "updated": [], -} -`; - -exports[`Workspace migration builder object actions test suite It should build a create_object and create_index actions for each of this fieldMetadata 2`] = ` -{ - "created": [ - { - "createFieldActions": [ - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": "uuid", - "description": "Id", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "Icon123", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": false, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Id", - "morphId": null, - "name": "id", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "UUID", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": "'Untitled'", - "description": "Name", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "IconAbc", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": false, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": false, - "label": "Name", - "morphId": null, - "name": "name", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "TEXT", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": "now", - "description": "Creation date", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "IconCalendar", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": false, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": false, - "label": "Creation date", - "morphId": null, - "name": "createdAt", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "DATE_TIME", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": "now", - "description": "Last time the record was changed", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "IconCalendarClock", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": false, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": false, - "label": "Last update", - "morphId": null, - "name": "updatedAt", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "DATE_TIME", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "Deletion date", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "IconCalendarClock", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": false, - "label": "Deleted at", - "morphId": null, - "name": "deletedAt", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "DATE_TIME", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": { - "name": "''", - "source": "'MANUAL'", - }, - "description": "The creator of the record", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "IconCreativeCommonsSa", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": false, - "isSystem": false, - "isUIReadOnly": false, - "isUnique": false, - "label": "Created by", - "morphId": null, - "name": "createdBy", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "ACTOR", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": 0, - "description": "Position", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": "IconHierarchy2", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": false, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Position", - "morphId": null, - "name": "position", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "POSITION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "TimelineActivities tied to the Rocket", - "flatRelationTargetFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "TimelineActivities Rocket", - "icon": "IconBuildingSkyscraper", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Rocket", - "morphId": null, - "name": "rocket", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "rocketId", - "onDelete": "CASCADE", - "relationType": "MANY_TO_ONE", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "flatRelationTargetObjectMetadata": null, - "icon": "IconTimelineEvent", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "TimelineActivities", - "morphId": null, - "name": "timelineActivities", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "Favorites tied to the Rocket", - "flatRelationTargetFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "Favorites Rocket", - "icon": "IconBuildingSkyscraper", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Rocket", - "morphId": null, - "name": "rocket", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "rocketId", - "onDelete": "CASCADE", - "relationType": "MANY_TO_ONE", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "flatRelationTargetObjectMetadata": null, - "icon": "IconHeart", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Favorites", - "morphId": null, - "name": "favorites", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "Attachments tied to the Rocket", - "flatRelationTargetFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "Attachments Rocket", - "icon": "IconBuildingSkyscraper", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Rocket", - "morphId": null, - "name": "rocket", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "rocketId", - "onDelete": "CASCADE", - "relationType": "MANY_TO_ONE", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "flatRelationTargetObjectMetadata": null, - "icon": "IconFileImport", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Attachments", - "morphId": null, - "name": "attachments", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "NoteTargets tied to the Rocket", - "flatRelationTargetFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "NoteTargets Rocket", - "icon": "IconBuildingSkyscraper", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Rocket", - "morphId": null, - "name": "rocket", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "rocketId", - "onDelete": "CASCADE", - "relationType": "MANY_TO_ONE", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "flatRelationTargetObjectMetadata": null, - "icon": "IconCheckbox", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "NoteTargets", - "morphId": null, - "name": "noteTargets", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "TaskTargets tied to the Rocket", - "flatRelationTargetFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "TaskTargets Rocket", - "icon": "IconBuildingSkyscraper", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Rocket", - "morphId": null, - "name": "rocket", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "joinColumnName": "rocketId", - "onDelete": "CASCADE", - "relationType": "MANY_TO_ONE", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "flatRelationTargetObjectMetadata": null, - "icon": "IconCheckbox", - "id": Any, - "isActive": true, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "TaskTargets", - "morphId": null, - "name": "taskTargets", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": Any, - "relationTargetObjectMetadataId": Any, - "settings": { - "relationType": "ONE_TO_MANY", - }, - "standardId": Any, - "standardOverrides": null, - "type": "RELATION", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - { - "flatFieldMetadata": { - "createdAt": Any, - "defaultValue": null, - "description": "Field used for full-text search", - "flatRelationTargetFieldMetadata": null, - "flatRelationTargetObjectMetadata": null, - "icon": null, - "id": Any, - "isActive": false, - "isCustom": false, - "isLabelSyncedWithName": false, - "isNullable": true, - "isSystem": true, - "isUIReadOnly": false, - "isUnique": false, - "label": "Search vector", - "morphId": null, - "name": "searchVector", - "objectMetadataId": Any, - "options": null, - "relationTargetFieldMetadataId": null, - "relationTargetObjectMetadataId": null, - "settings": null, - "standardId": Any, - "standardOverrides": null, - "type": "TS_VECTOR", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_field", - }, - ], - "flatObjectMetadataWithoutFields": { - "createdAt": Any, - "description": "A rocket", - "duplicateCriteria": null, - "icon": "IconRocket", - "id": Any, - "imageIdentifierFieldMetadataId": null, - "isActive": true, - "isAuditLogged": true, - "isCustom": true, - "isLabelSyncedWithName": false, - "isRemote": false, - "isSearchable": true, - "isSystem": false, - "isUIReadOnly": false, - "labelIdentifierFieldMetadataId": Any, - "labelPlural": "Rockets", - "labelSingular": "Rocket", - "namePlural": "rockets", - "nameSingular": "rocket", - "shortcut": null, - "standardId": null, - "standardOverrides": null, - "targetTableName": "DEPRECATED", - "universalIdentifier": Any, - "updatedAt": Any, - "workspaceId": Any, - }, - "type": "create_object", - }, - ], - "deleted": [], - "updated": [], -} -`; - exports[`Workspace migration builder object actions test suite It should build a delete_object action with custom deactivated object 1`] = ` { "created": [], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts index 0ffd13c0007..1e033d6b2c7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts @@ -1,4 +1,3 @@ -import { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock'; import { FLAT_OBJECT_METADATA_MAPS_MOCKS } from 'src/engine/metadata-modules/flat-object-metadata-maps/mocks/flat-object-metadata-maps.mock'; import { deleteObjectFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-object-from-flat-object-metadata-maps-or-throw.util'; import { replaceFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/replace-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; @@ -93,31 +92,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ }, }, }, - { - title: - 'It should build a create_object and create_index actions for each of this fieldMetadata', - context: { - input: { - fromFlatObjectMetadataMaps: - fromFlatObjectMetadatasToFlatObjectMetadataMaps([ - ...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS, - ]), - toFlatObjectMetadataMaps: - fromFlatObjectMetadatasToFlatObjectMetadataMaps([ - ...STANDARD_RELATION_TARGET_FLAT_OBJECT_METADATA_MOCKS, - { - ...ROCKET_FLAT_OBJECT_MOCK, - flatIndexMetadatas: [ - getFlatIndexMetadataMock({ - objectMetadataId: ROCKET_FLAT_OBJECT_MOCK.id, - universalIdentifier: 'field-metadata-unique-identifier-1', - }), - ], - }, - ]), - }, - }, - }, ]; const UPDATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts index 7d5d02f93f1..fe677a7e216 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { compareTwoFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/compare-two-flat-index-metadata.util'; import { @@ -139,7 +140,10 @@ export class WorkspaceMigrationV2IndexActionsBuilderService extends WorkspaceEnt this.flatIndexValidatorService.validateFlatIndexCreation({ dependencyOptimisticFlatEntityMaps, flatIndexToValidate: toFlatIndex, - optimisticFlatIndexMaps, + optimisticFlatIndexMaps: deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: fromFlatIndex.id, + flatEntityMaps: optimisticFlatIndexMaps, + }), }); if (creationValidationResult.errors.length > 0) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util.ts deleted file mode 100644 index 9aa857cb803..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type FromTo } from 'twenty-shared/types'; - -import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; -import { - type FlatObjectMetadata, - type FlatObjectMetadataWithoutFields, -} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { - type CustomDeletedCreatedUpdatedMatrix, - deletedCreatedUpdatedMatrixDispatcher, -} from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util'; - -export type UpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix = { - flatObjectMetadata: FlatObjectMetadataWithoutFields; -} & CustomDeletedCreatedUpdatedMatrix<'indexMetadata', FlatIndexMetadata>; - -export const computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix = ( - updatedObjectMetadata: FromTo[], -): UpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix[] => { - const matrixAccumulator: UpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix[] = - []; - - for (const { from, to } of updatedObjectMetadata) { - const indexMetadataMatrix = deletedCreatedUpdatedMatrixDispatcher({ - from: from.flatIndexMetadatas, - to: to.flatIndexMetadatas, - }); - - matrixAccumulator.push({ - flatObjectMetadata: to, - createdIndexMetadata: indexMetadataMatrix.created, - deletedIndexMetadata: indexMetadataMatrix.deleted, - updatedIndexMetadata: indexMetadataMatrix.updated, - }); - } - - return matrixAccumulator; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/from-flat-object-metadata-to-flat-object-metadata-without-fields.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/from-flat-object-metadata-to-flat-object-metadata-without-fields.util.ts index 017dcf0da93..47aae28fb33 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/from-flat-object-metadata-to-flat-object-metadata-without-fields.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/from-flat-object-metadata-to-flat-object-metadata-without-fields.util.ts @@ -5,6 +5,5 @@ import { export const fromFlatObjectMetadataToFlatObjectMetadataWithoutFields = ({ flatFieldMetadatas: _flatFieldMetadatas, - flatIndexMetadatas: _flatIndexMetadatas, ...rest }: FlatObjectMetadata): FlatObjectMetadataWithoutFields => rest; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts index b30ac218a85..70dc2804da1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service.ts @@ -94,7 +94,7 @@ export class FlatIndexValidatorService { optimisticFlatIndexMaps.byId, ).filter(isDefined); - const existingFlatIndexOnName = allExistingFlatIndex.some( + const existingFlatIndexOnName = allExistingFlatIndex.find( (flatIndexMetadata) => flatIndexMetadata.name.toLocaleUpperCase() === flatIndexToValidate.name.toLocaleUpperCase(), @@ -102,7 +102,7 @@ export class FlatIndexValidatorService { if (isDefined(existingFlatIndexOnName)) { validationResult.errors.push({ - code: IndexExceptionCode.INDEX_EMPTY_FIELDS, + code: IndexExceptionCode.INDEX_ALREADY_EXISTS, message: t`Index with same name already exists`, userFriendlyMessage: t`Index with same name already exists`, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts index 08717d90c29..fcc85a72e15 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/create-index-action-handler.service.ts @@ -1,13 +1,26 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'twenty-shared/utils'; + import { OptimisticallyApplyActionOnAllFlatEntityMapsArgs, WorkspaceMigrationRunnerActionHandler, } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; +import { + WorkspaceQueryRunnerException, + WorkspaceQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity-maps.exception'; import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; +import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; import { type CreateIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2'; @@ -44,15 +57,34 @@ export class CreateIndexActionHandlerService extends WorkspaceMigrationRunnerAct queryRunner.manager.getRepository( IndexMetadataEntity, ); + const indexFieldMetadataRepository = + queryRunner.manager.getRepository( + IndexFieldMetadataEntity, + ); const { - flatIndexMetadata: { flatIndexFieldMetadatas, ...rest }, + flatIndexMetadata: { flatIndexFieldMetadatas, ...flatIndexMetadata }, } = action; - await indexMetadataRepository.insert({ - indexFieldMetadatas: flatIndexFieldMetadatas, - ...rest, - }); + const indexInsertResult = + await indexMetadataRepository.insert(flatIndexMetadata); + + if (indexInsertResult.identifiers.length !== 1) { + throw new WorkspaceQueryRunnerException( + 'Failed to create index metadata', + WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + const indexMetadataId = indexInsertResult.identifiers[0].id; + + const indexFieldMetadataToInsert = flatIndexFieldMetadatas.map( + (flatIndexFieldMetadata) => ({ + ...flatIndexFieldMetadata, + indexMetadataId, + }), + ); + + await indexFieldMetadataRepository.insert(indexFieldMetadataToInsert); } async executeForWorkspaceSchema( @@ -76,7 +108,33 @@ export class CreateIndexActionHandlerService extends WorkspaceMigrationRunnerAct }); const quotedColumns = flatIndexMetadata.flatIndexFieldMetadatas.map( - (column) => `"${column}"`, + ({ fieldMetadataId }) => { + const flatFieldMetadata = + findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId({ + fieldMetadataId, + flatObjectMetadataMaps, + }); + + if (!isDefined(flatFieldMetadata)) { + throw new FlatEntityMapsException( + 'Index field related field metadata not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + if (isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { + if (!isDefined(flatFieldMetadata.settings?.joinColumnName)) { + throw new FlatEntityMapsException( + 'Join column name is not defined for relation field', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return `"${flatFieldMetadata.settings.joinColumnName}"`; + } + + return `"${flatFieldMetadata.name}"`; + }, ); await this.workspaceSchemaManagerService.indexManager.createIndex({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/delete-index-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/delete-index-action-handler.service.ts index 42edf67b287..44ecf68b5cb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/delete-index-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/index/services/delete-index-action-handler.service.ts @@ -8,16 +8,22 @@ import { import { AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type'; import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-object-metadata-in-flat-object-metadata-maps-or-throw.util'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; +import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { type DeleteIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2'; import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; -import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/get-workspace-schema-context-for-migration.util'; @Injectable() export class DeleteIndexActionHandlerService extends WorkspaceMigrationRunnerActionHandler( 'delete_index', ) { + constructor( + private readonly workspaceSchemaManagerService: WorkspaceSchemaManagerService, + ) { + super(); + } + optimisticallyApplyActionOnAllFlatEntityMaps({ action, allFlatEntityMaps: { flatIndexMaps }, @@ -51,7 +57,7 @@ export class DeleteIndexActionHandlerService extends WorkspaceMigrationRunnerAct ): Promise { const { action, - allFlatEntityMaps: { flatObjectMetadataMaps, flatIndexMaps }, + allFlatEntityMaps: { flatIndexMaps }, queryRunner, workspaceId, } = context; @@ -63,19 +69,12 @@ export class DeleteIndexActionHandlerService extends WorkspaceMigrationRunnerAct }, ); - const flatObjectMetadata = - findFlatObjectMetadataInFlatObjectMetadataMapsOrThrow({ - flatObjectMetadataMaps, - objectMetadataId: flatIndexMetadataToDelete.objectMetadataId, - }); - const { schemaName, tableName } = getWorkspaceSchemaContextForMigration({ - workspaceId, - flatObjectMetadata, - }); + const schemaName = getWorkspaceSchemaName(workspaceId); - await queryRunner.dropIndex( - `${schemaName}.${tableName}`, - flatIndexMetadataToDelete.name, - ); + await this.workspaceSchemaManagerService.indexManager.dropIndex({ + indexName: flatIndexMetadataToDelete.name, + queryRunner, + schemaName, + }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service.ts index d68a8a84b00..2aec8a50a1c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service.ts @@ -48,7 +48,6 @@ export class CreateObjectActionHandlerService extends WorkspaceMigrationRunnerAc addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow({ flatObjectMetadata: { ...flatObjectMetadataWithoutFields, - flatIndexMetadatas: [], flatFieldMetadatas, }, flatObjectMetadataMaps, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap index d075c206e57..dd37a07464e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-create-one-field-metadata-morph-relation-v2.integration-spec.ts.snap @@ -5,7 +5,38 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_4a59b92546a3dbf54abee51283a", + }, + "status": "fail", + "type": "create_index", + }, + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_247f630f4f88242063a98b01371", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -179,7 +210,23 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_c1add8eb2b51d67721916a3e5a9", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -227,7 +274,23 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_c84138bdf8bc3889ef56273281e", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -269,7 +332,23 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_e07d8240883043fcb1ad1ba0324", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -311,7 +390,23 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 it shou "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_c1add8eb2b51d67721916a3e5a9", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ diff --git a/packages/twenty-server/test/integration/metadata/suites/index/__snapshots__/successful-index-creation-on-object-creation-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/index/__snapshots__/successful-index-creation-on-object-creation-v2.integration-spec.ts.snap new file mode 100644 index 00000000000..4b9c7bcf189 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/index/__snapshots__/successful-index-creation-on-object-creation-v2.integration-spec.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Index metadata creation through object metadata creation v2 Should create an index on ts-search-vector standard field when creating a custom object 1`] = ` +{ + "indexType": "GIN", + "isCustom": false, + "isUnique": false, + "name": "IDX_be6fe08944beb9886fb83dfbbf1", +} +`; + +exports[`Index metadata creation through object metadata creation v2 Should update index on related object name singular update 1`] = ` +{ + "indexType": "GIN", + "isCustom": false, + "isUnique": false, + "name": "IDX_be6fe08944beb9886fb83dfbbf1", +} +`; + +exports[`Index metadata creation through object metadata creation v2 Should update index on related object name singular update 2`] = ` +{ + "indexType": "GIN", + "isCustom": false, + "isUnique": false, + "name": "IDX_e1506c1b3612d7f73c39fcaae6e", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/index/__snapshots__/successful-index-on-relation-field-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/index/__snapshots__/successful-index-on-relation-field-v2.integration-spec.ts.snap new file mode 100644 index 00000000000..48e21e72f5f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/index/__snapshots__/successful-index-on-relation-field-v2.integration-spec.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Index metadata creation on relation field creation v2 Should create and delete index on MANY_TO_ONE relation field creation and deletion 1`] = ` +{ + "indexFieldMetadataList": [ + { + "createdAt": Any, + "fieldMetadataId": Any, + "id": Any, + "order": 0, + "updatedAt": Any, + }, + ], + "indexType": "BTREE", + "isCustom": true, + "isUnique": false, + "name": "IDX_c7afa9b8a4109b84530eafdacdb", +} +`; + +exports[`Index metadata creation on relation field creation v2 Should create and delete index on ONE_TO_MANY relation field creation and deletion 1`] = ` +{ + "indexFieldMetadataList": [ + { + "createdAt": Any, + "fieldMetadataId": Any, + "id": Any, + "order": 0, + "updatedAt": Any, + }, + ], + "indexType": "BTREE", + "isCustom": true, + "isUnique": false, + "name": "IDX_9343d8f57e4835944dffb85d501", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/index/successful-index-creation-on-object-creation-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/index/successful-index-creation-on-object-creation-v2.integration-spec.ts new file mode 100644 index 00000000000..ba8770a9624 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/index/successful-index-creation-on-object-creation-v2.integration-spec.ts @@ -0,0 +1,151 @@ +import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; + +const findObjectWithIndex = async ({ + objectMetadataId, +}: { + objectMetadataId: string; +}) => { + const objects = await findManyObjectMetadataWithIndexes({ + expectToFail: false, + }); + + const foundObject = objects.find((object) => object.id === objectMetadataId); + + jestExpectToBeDefined(foundObject); + expect(foundObject.id).toBe(objectMetadataId); + + return foundObject; +}; + +describe('Index metadata creation through object metadata creation v2', () => { + let createdObjectId: string; + + beforeAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: true, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: false, + }); + }); + + beforeEach(async () => { + const { + labelPlural, + description, + labelSingular, + namePlural, + nameSingular, + } = CUSTOM_OBJECT_DISHES; + + const { data } = await createOneObjectMetadata({ + expectToFail: false, + input: { + labelPlural, + description, + labelSingular, + namePlural, + nameSingular, + icon: 'IconTest', + isLabelSyncedWithName: false, + }, + gqlFields: ` + id + `, + }); + + createdObjectId = data.createOneObject.id; + }); + + afterEach(async () => { + await updateOneObjectMetadata({ + input: { + idToUpdate: createdObjectId, + updatePayload: { + isActive: false, + }, + }, + expectToFail: false, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: createdObjectId }, + }); + }); + + it('Should create an index on ts-search-vector standard field when creating a custom object', async () => { + const foundObject = await findObjectWithIndex({ + objectMetadataId: createdObjectId, + }); + const tsVectorField = foundObject.fieldsList.find( + (field) => field.type === FieldMetadataType.TS_VECTOR, + ); + + jestExpectToBeDefined(tsVectorField); + + expect(foundObject.indexMetadataList.length).toBe(1); + const { indexFieldMetadataList, ...index } = + foundObject.indexMetadataList[0]; + + expect(index).toMatchSnapshot(); + expect(indexFieldMetadataList.length).toBe(1); + expect(indexFieldMetadataList).toMatchObject([ + { + id: expect.any(String), + fieldMetadataId: tsVectorField.id, + order: 0, + }, + ]); + }); + + it('Should update index on related object name singular update', async () => { + const foundObject = await findObjectWithIndex({ + objectMetadataId: createdObjectId, + }); + + expect(foundObject.indexMetadataList.length).toBe(1); + const { indexFieldMetadataList: _, ...index } = + foundObject.indexMetadataList[0]; + + expect(index).toMatchSnapshot(); + const fromIndexName = index.name; + + await updateOneObjectMetadata({ + input: { + idToUpdate: foundObject.id, + updatePayload: { + nameSingular: 'updatedName', + }, + }, + }); + + { + const foundObject = await findObjectWithIndex({ + objectMetadataId: createdObjectId, + }); + + expect(foundObject.indexMetadataList.length).toBe(1); + const { indexFieldMetadataList: _, ...index } = + foundObject.indexMetadataList[0]; + + expect(index).toMatchSnapshot(); + expect(fromIndexName).not.toBe(index.name); + } + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/index/successful-index-on-relation-field-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/index/successful-index-on-relation-field-v2.integration-spec.ts new file mode 100644 index 00000000000..4966ed6fba4 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/index/successful-index-on-relation-field-v2.integration-spec.ts @@ -0,0 +1,353 @@ +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants'; +import { CUSTOM_OBJECT_FOOD } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-food.constants'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { findManyObjectMetadataWithIndexes } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util'; +import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; + +describe('Index metadata creation on relation field creation v2', () => { + let createdObjectId: string; + let secondCreatedObjectId: string; + + beforeAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: true, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: false, + }); + }); + + beforeEach(async () => { + const { + labelPlural, + description, + labelSingular, + namePlural, + nameSingular, + } = CUSTOM_OBJECT_DISHES; + + const { data } = await createOneObjectMetadata({ + expectToFail: false, + input: { + labelPlural, + description, + labelSingular, + namePlural, + nameSingular, + icon: 'IconTest', + isLabelSyncedWithName: false, + }, + gqlFields: ` + id + `, + }); + + createdObjectId = data.createOneObject.id; + + { + const { + labelPlural, + description, + labelSingular, + namePlural, + nameSingular, + } = CUSTOM_OBJECT_FOOD; + const { data: secondData } = await createOneObjectMetadata({ + expectToFail: false, + input: { + labelPlural, + description, + labelSingular, + namePlural, + nameSingular, + icon: 'IconTest', + isLabelSyncedWithName: false, + }, + gqlFields: ` + id + `, + }); + + secondCreatedObjectId = secondData.createOneObject.id; + } + }); + + afterEach(async () => { + for (const objectMetadataId of [createdObjectId, secondCreatedObjectId]) { + await updateOneObjectMetadata({ + input: { + idToUpdate: objectMetadataId, + updatePayload: { + isActive: false, + }, + }, + expectToFail: false, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataId }, + }); + } + }); + + it('Should create and delete index on MANY_TO_ONE relation field creation and deletion', async () => { + await createOneFieldMetadata({ + expectToFail: false, + input: { + name: 'relationField', + type: FieldMetadataType.RELATION, + label: 'relation field', + objectMetadataId: createdObjectId, + relationCreationPayload: { + type: RelationType.MANY_TO_ONE, + targetFieldIcon: '123Icon', + targetFieldLabel: 'whatever', + targetObjectMetadataId: secondCreatedObjectId, + }, + }, + }); + + let objects = await findManyObjectMetadataWithIndexes({ + expectToFail: false, + }); + + const dishObject = objects.find((obj) => obj.id === createdObjectId); + const foodObject = objects.find((obj) => obj.id === secondCreatedObjectId); + + jestExpectToBeDefined(dishObject); + jestExpectToBeDefined(foodObject); + + const dishRelationField = dishObject.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.name === 'relationField', + ); + + jestExpectToBeDefined(dishRelationField?.relation); + expect(dishRelationField.relation.targetObjectMetadata.id).toBe( + secondCreatedObjectId, + ); + + const foodRelationField = foodObject.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.id === dishRelationField.relation?.targetFieldMetadata.id, + ); + + jestExpectToBeDefined(foodRelationField); + + const dishRelationFieldIndex = dishObject.indexMetadataList.find((index) => + index.indexFieldMetadataList.some( + (indexField) => indexField.fieldMetadataId === dishRelationField.id, + ), + ); + + jestExpectToBeDefined(dishRelationFieldIndex); + expect(dishRelationFieldIndex).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...dishRelationFieldIndex }), + ); + + const foodRelationFieldIndex = foodObject.indexMetadataList.find((index) => + index.indexFieldMetadataList.some( + (indexField) => indexField.fieldMetadataId === foodRelationField.id, + ), + ); + + expect(foodRelationFieldIndex).toBeUndefined(); + + await updateOneFieldMetadata({ + expectToFail: false, + input: { + idToUpdate: dishRelationField.id, + updatePayload: { + isActive: false, + }, + }, + }); + + await deleteOneFieldMetadata({ + expectToFail: false, + input: { idToDelete: dishRelationField.id }, + }); + + objects = await findManyObjectMetadataWithIndexes({ + expectToFail: false, + }); + + const dishObjectAfterDeletion = objects.find( + (obj) => obj.id === createdObjectId, + ); + const foodObjectAfterDeletion = objects.find( + (obj) => obj.id === secondCreatedObjectId, + ); + + jestExpectToBeDefined(dishObjectAfterDeletion); + jestExpectToBeDefined(foodObjectAfterDeletion); + + const dishRelationFieldAfterDeletion = + dishObjectAfterDeletion.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.name === 'relationField', + ); + + expect(dishRelationFieldAfterDeletion).toBeUndefined(); + + const foodRelationFieldAfterDeletion = + foodObjectAfterDeletion.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.id === dishRelationField?.relation?.targetFieldMetadata.id, + ); + + expect(foodRelationFieldAfterDeletion).toBeUndefined(); + + const dishRelationFieldIndexAfterDeletion = + dishObjectAfterDeletion.indexMetadataList.find((index) => + index.indexFieldMetadataList.some( + (indexField) => indexField.fieldMetadataId === dishRelationField.id, + ), + ); + + expect(dishRelationFieldIndexAfterDeletion).toBeUndefined(); + }); + + it('Should create and delete index on ONE_TO_MANY relation field creation and deletion', async () => { + await createOneFieldMetadata({ + expectToFail: false, + input: { + name: 'relationField', + type: FieldMetadataType.RELATION, + label: 'relation field', + objectMetadataId: createdObjectId, + relationCreationPayload: { + type: RelationType.ONE_TO_MANY, + targetFieldIcon: '123Icon', + targetFieldLabel: 'whatever', + targetObjectMetadataId: secondCreatedObjectId, + }, + }, + }); + + let objects = await findManyObjectMetadataWithIndexes({ + expectToFail: false, + }); + + const dishObject = objects.find((obj) => obj.id === createdObjectId); + const foodObject = objects.find((obj) => obj.id === secondCreatedObjectId); + + jestExpectToBeDefined(dishObject); + jestExpectToBeDefined(foodObject); + + const dishRelationField = dishObject.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.name === 'relationField', + ); + + jestExpectToBeDefined(dishRelationField?.relation); + expect(dishRelationField.relation.targetObjectMetadata.id).toBe( + secondCreatedObjectId, + ); + + const foodRelationField = foodObject.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.id === dishRelationField.relation?.targetFieldMetadata.id, + ); + + jestExpectToBeDefined(foodRelationField); + + const foodRelationFieldIndex = foodObject.indexMetadataList.find((index) => + index.indexFieldMetadataList.some( + (indexField) => indexField.fieldMetadataId === foodRelationField.id, + ), + ); + + jestExpectToBeDefined(foodRelationFieldIndex); + expect(foodRelationFieldIndex).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny({ ...foodRelationFieldIndex }), + ); + + const dishRelationFieldIndex = dishObject.indexMetadataList.find((index) => + index.indexFieldMetadataList.some( + (indexField) => indexField.fieldMetadataId === dishRelationField.id, + ), + ); + + expect(dishRelationFieldIndex).toBeUndefined(); + + await updateOneFieldMetadata({ + expectToFail: false, + input: { + idToUpdate: dishRelationField.id, + updatePayload: { + isActive: false, + }, + }, + }); + + await deleteOneFieldMetadata({ + expectToFail: false, + input: { idToDelete: dishRelationField.id }, + }); + + objects = await findManyObjectMetadataWithIndexes({ + expectToFail: false, + }); + + const dishObjectAfterDeletion = objects.find( + (obj) => obj.id === createdObjectId, + ); + const foodObjectAfterDeletion = objects.find( + (obj) => obj.id === secondCreatedObjectId, + ); + + jestExpectToBeDefined(dishObjectAfterDeletion); + jestExpectToBeDefined(foodObjectAfterDeletion); + + const dishRelationFieldAfterDeletion = + dishObjectAfterDeletion.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.name === 'relationField', + ); + + expect(dishRelationFieldAfterDeletion).toBeUndefined(); + + const foodRelationFieldAfterDeletion = + foodObjectAfterDeletion.fieldsList.find( + (field) => + field.type === FieldMetadataType.RELATION && + field.id === dishRelationField?.relation?.targetFieldMetadata.id, + ); + + expect(foodRelationFieldAfterDeletion).toBeUndefined(); + + const foodRelationFieldIndexAfterDeletion = + foodObjectAfterDeletion.indexMetadataList.find((index) => + index.indexFieldMetadataList.some( + (indexField) => indexField.fieldMetadataId === dishRelationField.id, + ), + ); + + expect(foodRelationFieldIndexAfterDeletion).toBeUndefined(); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap index 65e732a22db..0a33ced73be 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -5,7 +5,28 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -293,7 +314,28 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -581,7 +623,28 @@ exports[`Object metadata creation should fail v2 when labelPlural is empty 1`] = "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -869,7 +932,28 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -1157,7 +1241,28 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -1445,7 +1550,28 @@ exports[`Object metadata creation should fail v2 when labelSingular is empty 1`] "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -1733,7 +1859,28 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] = "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -2022,7 +2169,28 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -2311,7 +2479,28 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_da4ca4dc0c441477f29e4a384b0", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -2679,7 +2868,28 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -2967,7 +3177,28 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -3255,7 +3486,28 @@ exports[`Object metadata creation should fail v2 when namePlural is an empty str "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -3548,7 +3800,28 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_ffebb98ec981552dbecc71223c8", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -3841,7 +4114,28 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_2e70d840ef39341865172574229", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -4244,7 +4538,28 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_e0b8367f7f89bf39915991232b6", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -4647,7 +4962,28 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_e895f7d7a9fc30bad070509afab", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -5015,7 +5351,28 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_00aaf7dba3ae2d70218bdebd383", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -5383,7 +5740,28 @@ exports[`Object metadata creation should fail v2 when nameSingular is an empty s "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_e0b8367f7f89bf39915991232b6", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -5786,7 +6164,28 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_17a51d4440e492a8ec4d594c445", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -6189,7 +6588,28 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] = "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_acbec9630e5330ad7c000993538", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ @@ -6478,7 +6898,28 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res "extensions": { "code": "BAD_USER_INPUT", "errors": { - "index": [], + "index": [ + { + "errors": [ + { + "code": "INDEX_OBJECT_NOT_FOUND", + "message": "Could not find index related object metadata", + "userFriendlyMessage": "Index related object not found", + }, + { + "code": "INDEX_FIELD_NOT_FOUND", + "message": "Could not find index field related field metadata", + "userFriendlyMessage": "Field referenced in index does not exist", + }, + ], + "flatEntityMinimalInformation": { + "id": Any, + "name": "IDX_acbec9630e5330ad7c000993538", + }, + "status": "fail", + "type": "create_index", + }, + ], "objectMetadata": [ { "errors": [ diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/constants/custom-object-food.constants.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/constants/custom-object-food.constants.ts new file mode 100644 index 00000000000..705ab69c476 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/constants/custom-object-food.constants.ts @@ -0,0 +1,9 @@ +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const CUSTOM_OBJECT_FOOD = { + namePlural: 'foods', + nameSingular: 'food', + description: 'My favorite foods', + labelPlural: 'Foods I love', + labelSingular: 'food I love', +} as const satisfies Partial; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts index a4acc5789f7..1e1301c582d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util.ts @@ -8,14 +8,14 @@ import { type PerformMetadataQueryParams } from 'test/integration/metadata/types import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; export const createOneObjectMetadata = async ({ input, gqlFields, expectToFail, }: PerformMetadataQueryParams): CommonResponseBody<{ - createOneObject: ObjectMetadataEntity; // not accurate + createOneObject: ObjectMetadataDTO; }> => { const graphqlOperation = createOneObjectMetadataQueryFactory({ input, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts new file mode 100644 index 00000000000..cbd44534b40 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-with-indexes.util.ts @@ -0,0 +1,77 @@ +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; + +import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { type RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; +import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; +import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; +import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; + +export const findManyObjectMetadataWithIndexes = async ({ + expectToFail, +}: { + expectToFail: boolean; +}) => { + const { objects } = await findManyObjectMetadata({ + expectToFail, + input: { + filter: {}, + paging: { + first: 100, + }, + }, + gqlFields: ` + id + nameSingular + fieldsList { + id + type + name + relation { + type + sourceObjectMetadata { + id + nameSingular + namePlural + } + targetObjectMetadata { + id + nameSingular + namePlural + } + sourceFieldMetadata { + id + name + } + targetFieldMetadata { + id + name + } + } + } + indexMetadataList { + name + isUnique + isCustom + indexType + indexFieldMetadataList { + id + fieldMetadataId + createdAt + updatedAt + order + } + } + `, + }); + + return objects as Array< + ObjectMetadataDTO & { + fieldsList: (FieldMetadataDTO & { relation: RelationDTO | null })[]; + indexMetadataList: Array< + IndexMetadataDTO & { + indexFieldMetadataList: IndexFieldMetadataDTO[]; + } + >; + } + >; +};