diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 14eaba88364..98122895864 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -215,6 +215,7 @@ export enum AllMetadataName { skill = 'skill', view = 'view', viewField = 'viewField', + viewFieldGroup = 'viewFieldGroup', viewFilter = 'viewFilter', viewFilterGroup = 'viewFilterGroup', viewGroup = 'viewGroup', @@ -827,6 +828,7 @@ export type CoreView = { shouldHideEmptyGroups: Scalars['Boolean']; type: ViewType; updatedAt: Scalars['DateTime']; + viewFieldGroups: Array; viewFields: Array; viewFilterGroups: Array; viewFilters: Array; @@ -851,6 +853,20 @@ export type CoreViewField = { workspaceId: Scalars['UUID']; }; +export type CoreViewFieldGroup = { + __typename?: 'CoreViewFieldGroup'; + createdAt: Scalars['DateTime']; + deletedAt?: Maybe; + id: Scalars['UUID']; + isVisible: Scalars['Boolean']; + name: Scalars['String']; + position: Scalars['Float']; + updatedAt: Scalars['DateTime']; + viewFields: Array; + viewId: Scalars['UUID']; + workspaceId: Scalars['UUID']; +}; + export type CoreViewFilter = { __typename?: 'CoreViewFilter'; createdAt: Scalars['DateTime']; @@ -1080,6 +1096,14 @@ export type CreateSkillInput = { name: Scalars['String']; }; +export type CreateViewFieldGroupInput = { + id?: InputMaybe; + isVisible?: InputMaybe; + name: Scalars['String']; + position?: InputMaybe; + viewId: Scalars['UUID']; +}; + export type CreateViewFieldInput = { aggregateOperation?: InputMaybe; fieldMetadataId: Scalars['UUID']; @@ -1087,6 +1111,7 @@ export type CreateViewFieldInput = { isVisible?: InputMaybe; position?: InputMaybe; size?: InputMaybe; + viewFieldGroupId?: InputMaybe; viewId: Scalars['UUID']; }; @@ -1208,6 +1233,11 @@ export type DeleteTwoFactorAuthenticationMethodOutput = { success: Scalars['Boolean']; }; +export type DeleteViewFieldGroupInput = { + /** The id of the view field group to delete. */ + id: Scalars['UUID']; +}; + export type DeleteViewFieldInput = { /** The id of the view field to delete. */ id: Scalars['UUID']; @@ -1232,6 +1262,11 @@ export type DeletedWorkspaceMember = { userWorkspaceId?: Maybe; }; +export type DestroyViewFieldGroupInput = { + /** The id of the view field group to destroy. */ + id: Scalars['UUID']; +}; + export type DestroyViewFieldInput = { /** The id of the view field to destroy. */ id: Scalars['UUID']; @@ -2029,6 +2064,7 @@ export type Mutation = { createCommandMenuItem: CommandMenuItem; createCoreView: CoreView; createCoreViewField: CoreViewField; + createCoreViewFieldGroup: CoreViewFieldGroup; createCoreViewFilter: CoreViewFilter; createCoreViewFilterGroup: CoreViewFilterGroup; createCoreViewGroup: CoreViewGroup; @@ -2038,6 +2074,7 @@ export type Mutation = { createEmailingDomain: EmailingDomain; createFile: File; createFrontComponent: FrontComponent; + createManyCoreViewFieldGroups: Array; createManyCoreViewFields: Array; createManyCoreViewGroups: Array; createNavigationMenuItem: NavigationMenuItem; @@ -2061,6 +2098,7 @@ export type Mutation = { deleteCommandMenuItem: CommandMenuItem; deleteCoreView: Scalars['Boolean']; deleteCoreViewField: CoreViewField; + deleteCoreViewFieldGroup: CoreViewFieldGroup; deleteCoreViewFilter: CoreViewFilter; deleteCoreViewFilterGroup: Scalars['Boolean']; deleteCoreViewGroup: CoreViewGroup; @@ -2087,6 +2125,7 @@ export type Mutation = { deleteWorkspaceInvitation: Scalars['String']; destroyCoreView: Scalars['Boolean']; destroyCoreViewField: CoreViewField; + destroyCoreViewFieldGroup: CoreViewFieldGroup; destroyCoreViewFilter: CoreViewFilter; destroyCoreViewFilterGroup: Scalars['Boolean']; destroyCoreViewGroup: CoreViewGroup; @@ -2141,6 +2180,7 @@ export type Mutation = { updateCommandMenuItem: CommandMenuItem; updateCoreView: CoreView; updateCoreViewField: CoreViewField; + updateCoreViewFieldGroup: CoreViewFieldGroup; updateCoreViewFilter: CoreViewFilter; updateCoreViewFilterGroup: CoreViewFilterGroup; updateCoreViewGroup: CoreViewGroup; @@ -2258,6 +2298,11 @@ export type MutationCreateCoreViewFieldArgs = { }; +export type MutationCreateCoreViewFieldGroupArgs = { + input: CreateViewFieldGroupInput; +}; + + export type MutationCreateCoreViewFilterArgs = { input: CreateViewFilterInput; }; @@ -2305,6 +2350,11 @@ export type MutationCreateFrontComponentArgs = { }; +export type MutationCreateManyCoreViewFieldGroupsArgs = { + inputs: Array; +}; + + export type MutationCreateManyCoreViewFieldsArgs = { inputs: Array; }; @@ -2423,6 +2473,11 @@ export type MutationDeleteCoreViewFieldArgs = { }; +export type MutationDeleteCoreViewFieldGroupArgs = { + input: DeleteViewFieldGroupInput; +}; + + export type MutationDeleteCoreViewFilterArgs = { input: DeleteViewFilterInput; }; @@ -2544,6 +2599,11 @@ export type MutationDestroyCoreViewFieldArgs = { }; +export type MutationDestroyCoreViewFieldGroupArgs = { + input: DestroyViewFieldGroupInput; +}; + + export type MutationDestroyCoreViewFilterArgs = { input: DestroyViewFilterInput; }; @@ -2797,6 +2857,11 @@ export type MutationUpdateCoreViewFieldArgs = { }; +export type MutationUpdateCoreViewFieldGroupArgs = { + input: UpdateViewFieldGroupInput; +}; + + export type MutationUpdateCoreViewFilterArgs = { input: UpdateViewFilterInput; }; @@ -3487,6 +3552,8 @@ export type Query = { getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount; getCoreView?: Maybe; getCoreViewField?: Maybe; + getCoreViewFieldGroup?: Maybe; + getCoreViewFieldGroups: Array; getCoreViewFields: Array; getCoreViewFilter?: Maybe; getCoreViewFilterGroup?: Maybe; @@ -3662,6 +3729,16 @@ export type QueryGetCoreViewFieldArgs = { }; +export type QueryGetCoreViewFieldGroupArgs = { + id: Scalars['String']; +}; + + +export type QueryGetCoreViewFieldGroupsArgs = { + viewId: Scalars['String']; +}; + + export type QueryGetCoreViewFieldsArgs = { viewId: Scalars['String']; }; @@ -4517,6 +4594,20 @@ export type UpdateSkillInput = { name?: InputMaybe; }; +export type UpdateViewFieldGroupInput = { + /** The id of the view field group to update */ + id: Scalars['UUID']; + /** The view field group to update */ + update: UpdateViewFieldGroupInputUpdates; +}; + +export type UpdateViewFieldGroupInputUpdates = { + deletedAt?: InputMaybe; + isVisible?: InputMaybe; + name?: InputMaybe; + position?: InputMaybe; +}; + export type UpdateViewFieldInput = { /** The id of the view field to update */ id: Scalars['UUID']; @@ -4529,6 +4620,7 @@ export type UpdateViewFieldInputUpdates = { isVisible?: InputMaybe; position?: InputMaybe; size?: InputMaybe; + viewFieldGroupId?: InputMaybe; }; export type UpdateViewFilterGroupInput = { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 10de6c93883..8003f733f50 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -775,6 +775,7 @@ export type CoreView = { shouldHideEmptyGroups: Scalars['Boolean']; type: ViewType; updatedAt: Scalars['DateTime']; + viewFieldGroups: Array; viewFields: Array; viewFilterGroups: Array; viewFilters: Array; @@ -799,6 +800,20 @@ export type CoreViewField = { workspaceId: Scalars['UUID']; }; +export type CoreViewFieldGroup = { + __typename?: 'CoreViewFieldGroup'; + createdAt: Scalars['DateTime']; + deletedAt?: Maybe; + id: Scalars['UUID']; + isVisible: Scalars['Boolean']; + name: Scalars['String']; + position: Scalars['Float']; + updatedAt: Scalars['DateTime']; + viewFields: Array; + viewId: Scalars['UUID']; + workspaceId: Scalars['UUID']; +}; + export type CoreViewFilter = { __typename?: 'CoreViewFilter'; createdAt: Scalars['DateTime']; diff --git a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts index 938dfa949f3..eaf6d7aaafd 100644 --- a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts +++ b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts @@ -25,6 +25,7 @@ export const useMetadataErrorHandler = () => { fieldMetadata: t`field`, view: t`view`, viewField: t`view field`, + viewFieldGroup: t`view field group`, viewGroup: t`view group`, viewFilter: t`view filter`, index: t`index`, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1770818941843-add-view-field-group.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1770818941843-add-view-field-group.ts new file mode 100644 index 00000000000..2479d96a298 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1770818941843-add-view-field-group.ts @@ -0,0 +1,61 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddViewFieldGroup1770818941843 implements MigrationInterface { + name = 'AddViewFieldGroup1770818941843'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."viewFieldGroup" ("workspaceId" uuid NOT NULL, "universalIdentifier" uuid NOT NULL, "applicationId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" text NOT NULL, "position" double precision NOT NULL DEFAULT '0', "isVisible" boolean NOT NULL DEFAULT true, "viewId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_006f1cb78ab9eeef56c3e305009" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_e88d35604c4445b16e682edb30" ON "core"."viewFieldGroup" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FIELD_GROUP_VIEW_ID" ON "core"."viewFieldGroup" ("viewId") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FIELD_GROUP_WORKSPACE_ID_VIEW_ID" ON "core"."viewFieldGroup" ("workspaceId", "viewId") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" ADD "viewFieldGroupId" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" ADD CONSTRAINT "FK_118208b32ebf53be5aaede9c9cf" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" ADD CONSTRAINT "FK_38ec9201914a42386e5cdaa6521" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" ADD CONSTRAINT "FK_15c7197294c08e6e780d9734c99" FOREIGN KEY ("viewId") REFERENCES "core"."view"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" ADD CONSTRAINT "FK_d6f7c88260b1d4eaa8ad0f13c26" FOREIGN KEY ("viewFieldGroupId") REFERENCES "core"."viewFieldGroup"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."viewField" DROP CONSTRAINT "FK_d6f7c88260b1d4eaa8ad0f13c26"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" DROP CONSTRAINT "FK_15c7197294c08e6e780d9734c99"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" DROP CONSTRAINT "FK_38ec9201914a42386e5cdaa6521"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewFieldGroup" DROP CONSTRAINT "FK_118208b32ebf53be5aaede9c9cf"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."viewField" DROP COLUMN "viewFieldGroupId"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_FIELD_GROUP_WORKSPACE_ID_VIEW_ID"`, + ); + await queryRunner.query(`DROP INDEX "core"."IDX_VIEW_FIELD_GROUP_VIEW_ID"`); + await queryRunner.query( + `DROP INDEX "core"."IDX_e88d35604c4445b16e682edb30"`, + ); + await queryRunner.query(`DROP TABLE "core"."viewFieldGroup"`); + } +} diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts index 2fb1ff98f2c..2e56b63b1b4 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.interface.ts @@ -7,12 +7,16 @@ import { type MorphRelationLoaderPayload, type ObjectMetadataLoaderPayload, type RelationLoaderPayload, + type ViewFieldGroupsByViewIdLoaderPayload, + type ViewFieldsByViewFieldGroupIdLoaderPayload, } from 'src/engine/dataloaders/dataloader.service'; 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'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; +import { type ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; export interface IDataloaders { relationLoader: DataLoader; @@ -41,4 +45,14 @@ export interface IDataloaders { ObjectMetadataLoaderPayload, ObjectMetadataDTO | null >; + + viewFieldGroupsByViewIdLoader: DataLoader< + ViewFieldGroupsByViewIdLoaderPayload, + ViewFieldGroupDTO[] + >; + + viewFieldsByViewFieldGroupIdLoader: DataLoader< + ViewFieldsByViewFieldGroupIdLoaderPayload, + ViewFieldDTO[] + >; } diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 76b83fefeab..695a0df26e3 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -18,6 +18,7 @@ import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-module import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util'; import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; @@ -26,6 +27,8 @@ import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-fiel import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util'; +import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util'; +import { fromFlatViewFieldToViewFieldDto } from 'src/engine/metadata-modules/view-field/utils/from-flat-view-field-to-view-field-dto.util'; 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 { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; @@ -70,6 +73,16 @@ export type ObjectMetadataLoaderPayload = { objectMetadataId: string; }; +export type ViewFieldGroupsByViewIdLoaderPayload = { + workspaceId: string; + viewId: string; +}; + +export type ViewFieldsByViewFieldGroupIdLoaderPayload = { + workspaceId: string; + viewFieldGroupId: string; +}; + @Injectable() export class DataloaderService { constructor( @@ -84,6 +97,10 @@ export class DataloaderService { const indexMetadataLoader = this.createIndexMetadataLoader(); const indexFieldMetadataLoader = this.createIndexFieldMetadataLoader(); const objectMetadataLoader = this.createObjectMetadataLoader(); + const viewFieldGroupsByViewIdLoader = + this.createViewFieldGroupsByViewIdLoader(); + const viewFieldsByViewFieldGroupIdLoader = + this.createViewFieldsByViewFieldGroupIdLoader(); return { relationLoader, @@ -92,6 +109,8 @@ export class DataloaderService { indexMetadataLoader, indexFieldMetadataLoader, objectMetadataLoader, + viewFieldGroupsByViewIdLoader, + viewFieldsByViewFieldGroupIdLoader, }; } @@ -515,4 +534,74 @@ export class DataloaderService { }); }); } + + private createViewFieldGroupsByViewIdLoader() { + return new DataLoader< + ViewFieldGroupsByViewIdLoaderPayload, + ReturnType[] + >(async (dataLoaderParams: ViewFieldGroupsByViewIdLoaderPayload[]) => { + const workspaceId = dataLoaderParams[0].workspaceId; + + const { flatViewMaps, flatViewFieldGroupMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewMaps', 'flatViewFieldGroupMaps'], + }, + ); + + return dataLoaderParams.map(({ viewId }) => { + const flatView = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewId, + flatEntityMaps: flatViewMaps, + }); + + if (!isDefined(flatView)) { + return []; + } + + return findManyFlatEntityByIdInFlatEntityMaps({ + flatEntityIds: flatView.viewFieldGroupIds, + flatEntityMaps: flatViewFieldGroupMaps, + }) + .filter((flatViewFieldGroup) => flatViewFieldGroup.deletedAt === null) + .map(fromFlatViewFieldGroupToViewFieldGroupDto); + }); + }); + } + + private createViewFieldsByViewFieldGroupIdLoader() { + return new DataLoader< + ViewFieldsByViewFieldGroupIdLoaderPayload, + ReturnType[] + >(async (dataLoaderParams: ViewFieldsByViewFieldGroupIdLoaderPayload[]) => { + const workspaceId = dataLoaderParams[0].workspaceId; + + const { flatViewFieldGroupMaps, flatViewFieldMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps', 'flatViewFieldMaps'], + }, + ); + + return dataLoaderParams.map(({ viewFieldGroupId }) => { + const flatViewFieldGroup = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewFieldGroupId, + flatEntityMaps: flatViewFieldGroupMaps, + }); + + if (!isDefined(flatViewFieldGroup)) { + return []; + } + + return findManyFlatEntityByIdInFlatEntityMaps({ + flatEntityIds: flatViewFieldGroup.viewFieldIds, + flatEntityMaps: flatViewFieldMaps, + }) + .filter((flatViewField) => flatViewField.deletedAt === null) + .map(fromFlatViewFieldToViewFieldDto); + }); + }); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap index 4c168ad231b..f95b103a644 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.constant.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should match snapshot 1`] = ` { @@ -246,6 +246,16 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "size", "position", "aggregateOperation", + "viewFieldGroupUniversalIdentifier", + "deletedAt", + ], + "propertiesToStringify": [], + }, + "viewFieldGroup": { + "propertiesToCompare": [ + "name", + "position", + "isVisible", "deletedAt", ], "propertiesToStringify": [], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index 4a65381b45a..21a37e1982a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -112,6 +112,16 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { size: { toStringify: false, universalProperty: undefined }, position: { toStringify: false, universalProperty: undefined }, aggregateOperation: { toStringify: false, universalProperty: undefined }, + viewFieldGroupId: { + toStringify: false, + universalProperty: 'viewFieldGroupUniversalIdentifier', + }, + deletedAt: { toStringify: false, universalProperty: undefined }, + }, + viewFieldGroup: { + name: { toStringify: false, universalProperty: undefined }, + position: { toStringify: false, universalProperty: undefined }, + isVisible: { toStringify: false, universalProperty: undefined }, deletedAt: { toStringify: false, universalProperty: undefined }, }, viewGroup: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts index 24554950e24..026d2192fbf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant.ts @@ -18,6 +18,7 @@ import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modu import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; @@ -26,6 +27,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit export const ALL_METADATA_ENTITY_BY_METADATA_NAME = { viewField: ViewFieldEntity, + viewFieldGroup: ViewFieldGroupEntity, viewFilter: ViewFilterEntity, viewGroup: ViewGroupEntity, viewFilterGroup: ViewFilterGroupEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts index cf0810e0d46..fd78e4367a1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts @@ -221,6 +221,7 @@ export const ALL_METADATA_RELATIONS = { metadataName: 'viewFilterGroup', }, viewGroups: { metadataName: 'viewGroup' }, + viewFieldGroups: { metadataName: 'viewFieldGroup' }, // @ts-expect-error TODO migrate viewSort to v2 viewSorts: null, }, @@ -239,11 +240,32 @@ export const ALL_METADATA_RELATIONS = { foreignKey: 'viewId', isNullable: false, }, + viewFieldGroup: { + metadataName: 'viewFieldGroup', + flatEntityForeignKeyAggregator: 'viewFieldIds', + foreignKey: 'viewFieldGroupId', + isNullable: true, + }, workspace: null, application: null, }, oneToMany: {}, }, + viewFieldGroup: { + manyToOne: { + view: { + metadataName: 'view', + flatEntityForeignKeyAggregator: 'viewFieldGroupIds', + foreignKey: 'viewId', + isNullable: false, + }, + workspace: null, + application: null, + }, + oneToMany: { + viewFields: { metadataName: 'viewField' }, + }, + }, viewFilter: { manyToOne: { fieldMetadata: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts index 2c765614c15..a47821be503 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-required-metadata-for-validation.constant.ts @@ -27,6 +27,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = { view: true, fieldMetadata: true, objectMetadata: true, + viewFieldGroup: true, + }, + viewFieldGroup: { + view: true, }, index: { objectMetadata: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts index 52e4120ea6f..e44f7925a10 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts @@ -12,6 +12,7 @@ import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadat import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service'; import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service'; import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service'; +import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service'; import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service'; import { WorkspaceFlatViewFilterGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service'; import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service'; @@ -26,6 +27,7 @@ import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entiti import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; @@ -39,6 +41,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache TypeOrmModule.forFeature([ ViewEntity, ViewFieldEntity, + ViewFieldGroupEntity, ViewFilterEntity, ViewFilterGroupEntity, ViewGroupEntity, @@ -60,6 +63,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceFlatObjectMetadataMapCacheService, WorkspaceFlatViewMapCacheService, WorkspaceFlatViewFieldMapCacheService, + WorkspaceFlatViewFieldGroupMapCacheService, WorkspaceFlatViewFilterMapCacheService, WorkspaceFlatViewFilterGroupMapCacheService, WorkspaceFlatIndexMapCacheService, @@ -76,6 +80,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache WorkspaceFlatObjectMetadataMapCacheService, WorkspaceFlatViewMapCacheService, WorkspaceFlatViewFieldMapCacheService, + WorkspaceFlatViewFieldGroupMapCacheService, WorkspaceFlatViewFilterMapCacheService, WorkspaceFlatViewFilterGroupMapCacheService, WorkspaceFlatIndexMapCacheService, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts index a4d35f6b9f9..5ce8ceb08b8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts @@ -23,6 +23,8 @@ import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-targe import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; import { type FlatSkillMaps } from 'src/engine/metadata-modules/flat-skill/types/flat-skill-maps.type'; import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type'; +import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type'; import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type'; import { type FlatViewFilterGroupMaps } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group-maps.type'; @@ -57,6 +59,7 @@ import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/works import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type'; import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type'; import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type'; import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter-group.type'; import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type'; @@ -199,6 +202,14 @@ import { type UniversalDeleteViewFieldAction, type UniversalUpdateViewFieldAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type'; +import { + type FlatCreateViewFieldGroupAction, + type FlatDeleteViewFieldGroupAction, + type FlatUpdateViewFieldGroupAction, + type UniversalCreateViewFieldGroupAction, + type UniversalDeleteViewFieldGroupAction, + type UniversalUpdateViewFieldGroupAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type'; import { type FlatCreateViewFilterGroupAction, type FlatDeleteViewFilterGroupAction, @@ -308,6 +319,22 @@ export type AllFlatEntityTypesByMetadataName = { universalFlatEntity: UniversalFlatViewField; entity: MetadataEntity<'viewField'>; }; + viewFieldGroup: { + flatEntityMaps: FlatViewFieldGroupMaps; + universalActions: { + create: UniversalCreateViewFieldGroupAction; + update: UniversalUpdateViewFieldGroupAction; + delete: UniversalDeleteViewFieldGroupAction; + }; + flatActions: { + create: FlatCreateViewFieldGroupAction; + update: FlatUpdateViewFieldGroupAction; + delete: FlatDeleteViewFieldGroupAction; + }; + flatEntity: FlatViewFieldGroup; + universalFlatEntity: UniversalFlatViewFieldGroup; + entity: MetadataEntity<'viewFieldGroup'>; + }; viewGroup: { flatEntityMaps: FlatViewGroupMaps; universalActions: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap index b1b86365935..fed3a70aaa8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/get-metadata-related-metadata-names.util.spec.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`; @@ -108,6 +108,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo "viewFilter", "viewFilterGroup", "viewGroup", + "viewFieldGroup", ] `; @@ -115,6 +116,14 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo [ "fieldMetadata", "view", + "viewFieldGroup", +] +`; + +exports[`getMetadataRelatedMetadataNames should return related metadata names for viewFieldGroup 1`] = ` +[ + "view", + "viewField", ] `; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap index d763efc6a1a..ba7d88b8a9c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/__tests__/__snapshots__/sort-metadata-names-children-first.util.spec.ts.snap @@ -4,10 +4,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with [ "rowLevelPermissionPredicate", "navigationMenuItem", + "viewField", "viewFilter", "commandMenuItem", "pageLayoutWidget", - "viewField", "index", "pageLayout", "roleTarget", @@ -18,9 +18,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with "logicFunction", "pageLayoutTab", "skill", - "view", + "viewFieldGroup", "viewFilterGroup", "webhook", + "view", "fieldMetadata", "role", "objectMetadata", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts index bbc454d7e9a..fc9b3293b55 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util.ts @@ -80,6 +80,8 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({ deletedAt: null, universalIdentifier: viewFieldId, aggregateOperation: null, + viewFieldGroupId: null, + viewFieldGroupUniversalIdentifier: null, applicationId: existingFlatObjectMetadata.applicationId, applicationUniversalIdentifier: existingFlatObjectMetadata.applicationUniversalIdentifier, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/constants/flat-view-field-group-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/constants/flat-view-field-group-editable-properties.constant.ts new file mode 100644 index 00000000000..0f25d04b72c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/constants/flat-view-field-group-editable-properties.constant.ts @@ -0,0 +1,8 @@ +import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant'; + +export const FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES = [ + 'name', + 'position', + 'isVisible', + 'deletedAt', +] as const satisfies MetadataEntityPropertyName<'viewFieldGroup'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service.ts new file mode 100644 index 00000000000..d8be2c79bc6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { fromViewFieldGroupEntityToFlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-view-field-group-entity-to-flat-view-field-group.util'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; +import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; +import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; + +@Injectable() +@WorkspaceCache('flatViewFieldGroupMaps') +export class WorkspaceFlatViewFieldGroupMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ViewFieldGroupEntity) + private readonly viewFieldGroupRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, + @InjectRepository(ViewFieldEntity) + private readonly viewFieldRepository: Repository, + ) { + super(); + } + + async computeForCache(workspaceId: string): Promise { + const [viewFieldGroups, applications, views, viewFields] = + await Promise.all([ + this.viewFieldGroupRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewFieldRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'viewFieldGroupId'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); + + const viewFieldsByViewFieldGroupId = + regroupEntitiesByRelatedEntityId<'viewField'>({ + entities: viewFields, + foreignKey: 'viewFieldGroupId', + }); + + const flatViewFieldGroupMaps = createEmptyFlatEntityMaps(); + + for (const viewFieldGroupEntity of viewFieldGroups) { + const flatViewFieldGroup = fromViewFieldGroupEntityToFlatViewFieldGroup({ + entity: { + ...viewFieldGroupEntity, + viewFields: + viewFieldsByViewFieldGroupId.get(viewFieldGroupEntity.id) || [], + }, + applicationIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + }); + + addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ + flatEntity: flatViewFieldGroup, + flatEntityMapsToMutate: flatViewFieldGroupMaps, + }); + } + + return flatViewFieldGroupMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type.ts new file mode 100644 index 00000000000..8ad1cd54c53 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type'; + +export type FlatViewFieldGroupMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type.ts new file mode 100644 index 00000000000..a2b2dfded79 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; +import { type ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; + +export type FlatViewFieldGroup = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts new file mode 100644 index 00000000000..fb356e7f37c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util.ts @@ -0,0 +1,52 @@ +import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; + +export type FromCreateViewFieldGroupInputToFlatViewFieldGroupToCreateArgs = { + createViewFieldGroupInput: CreateViewFieldGroupInput; + flatApplication: FlatApplication; +} & Pick; + +export const fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate = ({ + createViewFieldGroupInput: rawCreateViewFieldGroupInput, + flatApplication, + flatViewMaps, +}: FromCreateViewFieldGroupInputToFlatViewFieldGroupToCreateArgs): UniversalFlatViewFieldGroup & { + id: string; +} => { + const { viewId, ...createViewFieldGroupInput } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawCreateViewFieldGroupInput, + ['id', 'name', 'viewId'], + ); + + const createdAt = new Date().toISOString(); + const viewFieldGroupId = createViewFieldGroupInput.id ?? v4(); + + const { viewUniversalIdentifier } = resolveEntityRelationUniversalIdentifiers( + { + metadataName: 'viewFieldGroup', + foreignKeyValues: { viewId }, + flatEntityMaps: { flatViewMaps }, + }, + ); + + return { + id: viewFieldGroupId, + name: createViewFieldGroupInput.name, + viewUniversalIdentifier, + createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: createViewFieldGroupInput.universalIdentifier ?? v4(), + position: createViewFieldGroupInput.position ?? 0, + isVisible: createViewFieldGroupInput.isVisible ?? true, + viewFieldUniversalIdentifiers: [], + applicationUniversalIdentifier: flatApplication.universalIdentifier, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-delete-view-field-group-input-to-flat-view-field-group-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-delete-view-field-group-input-to-flat-view-field-group-or-throw.util.ts new file mode 100644 index 00000000000..4891b413867 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-delete-view-field-group-input-to-flat-view-field-group-or-throw.util.ts @@ -0,0 +1,46 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input'; +import { + ViewFieldGroupException, + ViewFieldGroupExceptionCode, +} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; + +export const fromDeleteViewFieldGroupInputToFlatViewFieldGroupOrThrow = ({ + deleteViewFieldGroupInput: rawDeleteViewFieldGroupInput, + flatViewFieldGroupMaps, +}: { + deleteViewFieldGroupInput: DeleteViewFieldGroupInput; + flatViewFieldGroupMaps: FlatViewFieldGroupMaps; +}): UniversalFlatViewFieldGroup => { + const { id: viewFieldGroupId } = extractAndSanitizeObjectStringFields( + rawDeleteViewFieldGroupInput, + ['id'], + ); + + const existingFlatViewFieldGroupToDelete = findFlatEntityByIdInFlatEntityMaps( + { + flatEntityId: viewFieldGroupId, + flatEntityMaps: flatViewFieldGroupMaps, + }, + ); + + if (!isDefined(existingFlatViewFieldGroupToDelete)) { + throw new ViewFieldGroupException( + t`View field group to delete not found`, + ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND, + ); + } + + return { + ...existingFlatViewFieldGroupToDelete, + deletedAt: new Date().toISOString(), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-destroy-view-field-group-input-to-flat-view-field-group-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-destroy-view-field-group-input-to-flat-view-field-group-or-throw.util.ts new file mode 100644 index 00000000000..ac9ebf172ac --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-destroy-view-field-group-input-to-flat-view-field-group-or-throw.util.ts @@ -0,0 +1,42 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, +} from 'twenty-shared/utils'; + +import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input'; +import { + ViewFieldGroupException, + ViewFieldGroupExceptionCode, +} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; + +export const fromDestroyViewFieldGroupInputToFlatViewFieldGroupOrThrow = ({ + destroyViewFieldGroupInput, + flatViewFieldGroupMaps, +}: { + destroyViewFieldGroupInput: DestroyViewFieldGroupInput; + flatViewFieldGroupMaps: FlatViewFieldGroupMaps; +}): UniversalFlatViewFieldGroup => { + const { id: viewFieldGroupId } = extractAndSanitizeObjectStringFields( + destroyViewFieldGroupInput, + ['id'], + ); + + const existingFlatViewFieldGroupToDestroy = + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewFieldGroupId, + flatEntityMaps: flatViewFieldGroupMaps, + }); + + if (!isDefined(existingFlatViewFieldGroupToDestroy)) { + throw new ViewFieldGroupException( + t`View field group to destroy not found`, + ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND, + ); + } + + return existingFlatViewFieldGroupToDestroy; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-update-view-field-group-input-to-flat-view-field-group-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-update-view-field-group-input-to-flat-view-field-group-to-update-or-throw.util.ts new file mode 100644 index 00000000000..3f511e983fe --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-update-view-field-group-input-to-flat-view-field-group-to-update-or-throw.util.ts @@ -0,0 +1,56 @@ +import { t } from '@lingui/core/macro'; +import { + extractAndSanitizeObjectStringFields, + isDefined, + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, +} from 'twenty-shared/utils'; + +import { FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field-group/constants/flat-view-field-group-editable-properties.constant'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type'; +import { type UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input'; +import { + ViewFieldGroupException, + ViewFieldGroupExceptionCode, +} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception'; +import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type'; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; + +export const fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow = + ({ + updateViewFieldGroupInput: rawUpdateViewFieldGroupInput, + flatViewFieldGroupMaps, + }: { + updateViewFieldGroupInput: UpdateViewFieldGroupInput; + flatViewFieldGroupMaps: FlatViewFieldGroupMaps; + }): UniversalFlatViewFieldGroup => { + const { id: viewFieldGroupToUpdateId } = + trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( + rawUpdateViewFieldGroupInput, + ['id'], + ); + + const existingFlatViewFieldGroupToUpdate = + findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: viewFieldGroupToUpdateId, + flatEntityMaps: flatViewFieldGroupMaps, + }); + + if (!isDefined(existingFlatViewFieldGroupToUpdate)) { + throw new ViewFieldGroupException( + t`View field group to update not found`, + ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND, + ); + } + + const updatedEditableProperties = extractAndSanitizeObjectStringFields( + rawUpdateViewFieldGroupInput.update, + FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES, + ); + + return mergeUpdateInExistingRecord({ + existing: existingFlatViewFieldGroupToUpdate, + properties: FLAT_VIEW_FIELD_GROUP_EDITABLE_PROPERTIES, + update: updatedEditableProperties, + }); + }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-view-field-group-entity-to-flat-view-field-group.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-view-field-group-entity-to-flat-view-field-group.util.ts new file mode 100644 index 00000000000..01fc5c8644b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field-group/utils/from-view-field-group-entity-to-flat-view-field-group.util.ts @@ -0,0 +1,60 @@ +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util'; +import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromViewFieldGroupEntityToFlatViewFieldGroup = ({ + entity: viewFieldGroupEntity, + applicationIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'viewFieldGroup'>): FlatViewFieldGroup => { + const viewFieldGroupEntityWithoutRelations = removePropertiesFromRecord( + viewFieldGroupEntity, + getMetadataEntityRelationProperties('viewFieldGroup'), + ); + + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + viewFieldGroupEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewFieldGroupEntity.applicationId} not found for viewFieldGroup ${viewFieldGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get( + viewFieldGroupEntity.viewId, + ); + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${viewFieldGroupEntity.viewId} not found for viewFieldGroup ${viewFieldGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + return { + ...viewFieldGroupEntityWithoutRelations, + createdAt: viewFieldGroupEntity.createdAt.toISOString(), + updatedAt: viewFieldGroupEntity.updatedAt.toISOString(), + deletedAt: viewFieldGroupEntity.deletedAt?.toISOString() ?? null, + universalIdentifier: + viewFieldGroupEntityWithoutRelations.universalIdentifier, + applicationUniversalIdentifier, + viewUniversalIdentifier, + viewFieldIds: + viewFieldGroupEntity.viewFields?.map((viewField) => viewField.id) ?? [], + viewFieldUniversalIdentifiers: + viewFieldGroupEntity.viewFields?.map( + (viewField) => viewField.universalIdentifier, + ) ?? [], + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant.ts index c44b72b715f..396cc806f89 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant.ts @@ -5,4 +5,5 @@ export const FLAT_VIEW_FIELD_EDITABLE_PROPERTIES = [ 'size', 'position', 'aggregateOperation', + 'viewFieldGroupId', ] as const satisfies MetadataEntityPropertyName<'viewField'>[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts index 9358081f706..abf036f5742 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts @@ -10,6 +10,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; import { fromViewFieldEntityToFlatViewField } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; @@ -28,13 +29,15 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide private readonly fieldMetadataRepository: Repository, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, + @InjectRepository(ViewFieldGroupEntity) + private readonly viewFieldGroupRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const [viewFields, applications, fieldMetadatas, views] = await Promise.all( - [ + const [viewFields, applications, fieldMetadatas, views, viewFieldGroups] = + await Promise.all([ this.viewFieldRepository.find({ where: { workspaceId }, withDeleted: true, @@ -54,8 +57,12 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide select: ['id', 'universalIdentifier'], withDeleted: true, }), - ], - ); + this.viewFieldGroupRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); const applicationIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(applications); @@ -63,6 +70,8 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide createIdToUniversalIdentifierMap(fieldMetadatas); const viewIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(views); + const viewFieldGroupIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(viewFieldGroups); const flatViewFieldMaps = createEmptyFlatEntityMaps(); @@ -72,6 +81,7 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide applicationIdToUniversalIdentifierMap, fieldMetadataIdToUniversalIdentifierMap, viewIdToUniversalIdentifierMap, + viewFieldGroupIdToUniversalIdentifierMap, }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts index fb19a1b9aac..5485b70f903 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util.ts @@ -11,31 +11,48 @@ import { type UniversalFlatViewField } from 'src/engine/workspace-manager/worksp export type FromCreateViewFieldInputToFlatViewFieldToCreateArgs = { createViewFieldInput: CreateViewFieldInput; flatApplication: FlatApplication; -} & Pick; +} & Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' | 'flatViewMaps' | 'flatViewFieldGroupMaps' +>; export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({ createViewFieldInput: rawCreateViewFieldInput, flatApplication, flatFieldMetadataMaps, flatViewMaps, + flatViewFieldGroupMaps, }: FromCreateViewFieldInputToFlatViewFieldToCreateArgs): UniversalFlatViewField & { id: string; } => { - const { fieldMetadataId, viewId, ...createViewFieldInput } = + const { fieldMetadataId, viewId, viewFieldGroupId, ...createViewFieldInput } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawCreateViewFieldInput, - ['aggregateOperation', 'fieldMetadataId', 'id', 'viewId'], + [ + 'aggregateOperation', + 'fieldMetadataId', + 'id', + 'viewId', + 'viewFieldGroupId', + ], ); const createdAt = new Date().toISOString(); const viewFieldId = createViewFieldInput.id ?? v4(); - const { fieldMetadataUniversalIdentifier, viewUniversalIdentifier } = - resolveEntityRelationUniversalIdentifiers({ - metadataName: 'viewField', - foreignKeyValues: { fieldMetadataId, viewId }, - flatEntityMaps: { flatFieldMetadataMaps, flatViewMaps }, - }); + const { + fieldMetadataUniversalIdentifier, + viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier, + } = resolveEntityRelationUniversalIdentifiers({ + metadataName: 'viewField', + foreignKeyValues: { fieldMetadataId, viewId, viewFieldGroupId }, + flatEntityMaps: { + flatFieldMetadataMaps, + flatViewMaps, + flatViewFieldGroupMaps, + }, + }); return { id: viewFieldId, @@ -49,6 +66,7 @@ export const fromCreateViewFieldInputToFlatViewFieldToCreate = ({ size: createViewFieldInput.size ?? DEFAULT_VIEW_FIELD_SIZE, position: createViewFieldInput.position ?? 0, aggregateOperation: createViewFieldInput.aggregateOperation ?? null, + viewFieldGroupUniversalIdentifier, applicationUniversalIdentifier: flatApplication.universalIdentifier, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts index fe5ada5e7de..bbe9e06d05d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-update-view-field-input-to-flat-view-field-to-update-or-throw.util.ts @@ -5,8 +5,10 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; -import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util'; +import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant'; import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; import { type UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input'; import { @@ -19,10 +21,14 @@ import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing- export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({ updateViewFieldInput: rawUpdateViewFieldInput, flatViewFieldMaps, + flatViewFieldGroupMaps, }: { updateViewFieldInput: UpdateViewFieldInput; flatViewFieldMaps: FlatViewFieldMaps; -}): UniversalFlatViewField => { +} & Pick< + AllFlatEntityMaps, + 'flatViewFieldGroupMaps' +>): UniversalFlatViewField => { const { id: viewFieldToUpdateId } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( rawUpdateViewFieldInput, @@ -40,14 +46,31 @@ export const fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow = ({ ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, ); } + const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( rawUpdateViewFieldInput.update, FLAT_VIEW_FIELD_EDITABLE_PROPERTIES, ); - return mergeUpdateInExistingRecord({ + const flatViewFieldToUpdate = mergeUpdateInExistingRecord({ existing: existingFlatViewFieldToUpdate, properties: FLAT_VIEW_FIELD_EDITABLE_PROPERTIES, update: updatedEditableFieldProperties, }); + + if (updatedEditableFieldProperties.viewFieldGroupId !== undefined) { + const { viewFieldGroupUniversalIdentifier } = + resolveEntityRelationUniversalIdentifiers({ + metadataName: 'viewField', + foreignKeyValues: { + viewFieldGroupId: flatViewFieldToUpdate.viewFieldGroupId, + }, + flatEntityMaps: { flatViewFieldGroupMaps }, + }); + + flatViewFieldToUpdate.viewFieldGroupUniversalIdentifier = + viewFieldGroupUniversalIdentifier; + } + + return flatViewFieldToUpdate; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts index 3bfbf211d02..110387811bb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts @@ -13,6 +13,7 @@ export const fromViewFieldEntityToFlatViewField = ({ applicationIdToUniversalIdentifierMap, fieldMetadataIdToUniversalIdentifierMap, viewIdToUniversalIdentifierMap, + viewFieldGroupIdToUniversalIdentifierMap, }: FromEntityToFlatEntityArgs<'viewField'>): FlatViewField => { const viewFieldEntityWithoutRelations = removePropertiesFromRecord( viewFieldEntity, @@ -52,6 +53,22 @@ export const fromViewFieldEntityToFlatViewField = ({ ); } + let viewFieldGroupUniversalIdentifier: string | null = null; + + if (isDefined(viewFieldEntity.viewFieldGroupId)) { + viewFieldGroupUniversalIdentifier = + viewFieldGroupIdToUniversalIdentifierMap.get( + viewFieldEntity.viewFieldGroupId, + ) ?? null; + + if (!isDefined(viewFieldGroupUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ViewFieldGroup with id ${viewFieldEntity.viewFieldGroupId} not found for viewField ${viewFieldEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + return { ...viewFieldEntityWithoutRelations, createdAt: viewFieldEntity.createdAt.toISOString(), @@ -61,5 +78,6 @@ export const fromViewFieldEntityToFlatViewField = ({ applicationUniversalIdentifier, fieldMetadataUniversalIdentifier, viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts index 231b3ddb767..4ce354fa92b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util.ts @@ -87,6 +87,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({ viewFieldUniversalIdentifiers: [], viewFilterUniversalIdentifiers: [], viewGroupUniversalIdentifiers: [], + viewFieldGroupUniversalIdentifiers: [], viewFilterGroupUniversalIdentifiers: [], applicationUniversalIdentifier: flatApplication.universalIdentifier, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts index 221b19559b3..47413776b93 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util.ts @@ -95,6 +95,7 @@ export const fromViewEntityToFlatView = ({ deletedAt: viewEntity.deletedAt?.toISOString() ?? null, universalIdentifier: viewEntityWithoutRelations.universalIdentifier, viewFieldIds: viewEntity.viewFields.map(({ id }) => id), + viewFieldGroupIds: viewEntity.viewFieldGroups?.map(({ id }) => id) ?? [], viewFilterIds: viewEntity.viewFilters.map(({ id }) => id), viewGroupIds: viewEntity.viewGroups.map(({ id }) => id), viewFilterGroupIds: viewEntity.viewFilterGroups?.map(({ id }) => id) ?? [], @@ -106,6 +107,10 @@ export const fromViewEntityToFlatView = ({ viewFieldUniversalIdentifiers: viewEntity.viewFields.map( ({ universalIdentifier }) => universalIdentifier, ), + viewFieldGroupUniversalIdentifiers: + viewEntity.viewFieldGroups?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], viewFilterUniversalIdentifiers: viewEntity.viewFilters.map( ({ universalIdentifier }) => universalIdentifier, ), diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index c80968d6cc0..908d8370943 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -549,6 +549,7 @@ export class ObjectMetadataService extends TypeOrmQueryService((field, index) => ({ fieldMetadataUniversalIdentifier: field.universalIdentifier, viewUniversalIdentifier, + viewFieldGroupUniversalIdentifier: null, createdAt, updatedAt: createdAt, deletedAt: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input.ts new file mode 100644 index 00000000000..8452885a02e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input.ts @@ -0,0 +1,45 @@ +import { Field, HideField, InputType } from '@nestjs/graphql'; + +import { + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class CreateViewFieldGroupInput { + @IsOptional() + @IsUUID() + @Field(() => UUIDScalarType, { nullable: true }) + id?: string; + + @IsNotEmpty() + @IsString() + @Field({ nullable: false }) + name: string; + + @IsUUID() + @Field(() => UUIDScalarType, { nullable: false }) + viewId: string; + + @IsOptional() + @IsNumber() + @Field({ nullable: true, defaultValue: 0 }) + position?: number; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true, defaultValue: true }) + isVisible?: boolean; + + @HideField() + universalIdentifier?: string; + + @HideField() + applicationId?: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input.ts new file mode 100644 index 00000000000..8f614808f82 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input.ts @@ -0,0 +1,15 @@ +import { InputType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class DeleteViewFieldGroupInput { + @IDField(() => UUIDScalarType, { + description: 'The id of the view field group to delete.', + }) + @IsUUID() + id: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input.ts new file mode 100644 index 00000000000..788d76bb83e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input.ts @@ -0,0 +1,15 @@ +import { InputType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IsUUID } from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class DestroyViewFieldGroupInput { + @IDField(() => UUIDScalarType, { + description: 'The id of the view field group to destroy.', + }) + @IsUUID() + id: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input.ts new file mode 100644 index 00000000000..a503cdab68a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input.ts @@ -0,0 +1,54 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +class UpdateViewFieldGroupInputUpdates { + @IsOptional() + @IsString() + @Field({ nullable: true }) + name?: string; + + @IsOptional() + @IsNumber() + @Field({ nullable: true }) + position?: number; + + @IsOptional() + @IsBoolean() + @Field({ nullable: true }) + isVisible?: boolean; + + @IsOptional() + @IsString() + @Field({ nullable: true }) + deletedAt?: string; +} + +@InputType() +export class UpdateViewFieldGroupInput { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType, { + description: 'The id of the view field group to update', + }) + id: string; + + @Type(() => UpdateViewFieldGroupInputUpdates) + @ValidateNested() + @Field(() => UpdateViewFieldGroupInputUpdates, { + description: 'The view field group to update', + }) + update: UpdateViewFieldGroupInputUpdates; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto.ts new file mode 100644 index 00000000000..2318ef9b9ff --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto.ts @@ -0,0 +1,39 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; + +@ObjectType('CoreViewFieldGroup') +export class ViewFieldGroupDTO { + @IDField(() => UUIDScalarType) + id: string; + + @Field({ nullable: false }) + name: string; + + @Field({ nullable: false, defaultValue: 0 }) + position: number; + + @Field({ nullable: false, defaultValue: true }) + isVisible: boolean; + + @Field(() => UUIDScalarType, { nullable: false }) + viewId: string; + + @Field(() => UUIDScalarType, { nullable: false }) + workspaceId: string; + + @Field() + createdAt: Date; + + @Field() + updatedAt: Date; + + @Field(() => Date, { nullable: true }) + deletedAt?: Date | null; + + @Field(() => [ViewFieldDTO]) + viewFields?: ViewFieldDTO[]; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/entities/view-field-group.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/entities/view-field-group.entity.ts new file mode 100644 index 00000000000..2aa9167d44b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/entities/view-field-group.entity.ts @@ -0,0 +1,58 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + type Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; + +@Entity({ name: 'viewFieldGroup', schema: 'core' }) +@Index('IDX_VIEW_FIELD_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_FIELD_GROUP_VIEW_ID', ['viewId']) +export class ViewFieldGroupEntity + extends SyncableEntity + implements Required +{ + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: false, type: 'text' }) + name: string; + + @Column({ nullable: false, type: 'double precision', default: 0 }) + position: number; + + @Column({ nullable: false, default: true }) + isVisible: boolean; + + @Column({ nullable: false, type: 'uuid' }) + viewId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date | null; + + @ManyToOne(() => ViewEntity, (view) => view.viewFieldGroups, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'viewId' }) + view: Relation; + + @OneToMany(() => ViewFieldEntity, (viewField) => viewField.viewFieldGroup) + viewFields: Relation; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception.ts new file mode 100644 index 00000000000..d49eb40120b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception.ts @@ -0,0 +1,23 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; + +import { CustomException } from 'src/utils/custom-exception'; + +export class ViewFieldGroupException extends CustomException { + constructor( + message: string, + code: ViewFieldGroupExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, + ) { + super(message, code, { + userFriendlyMessage: + userFriendlyMessage ?? msg`A view field group error occurred.`, + }); + } +} + +export enum ViewFieldGroupExceptionCode { + VIEW_FIELD_GROUP_NOT_FOUND = 'VIEW_FIELD_GROUP_NOT_FOUND', + VIEW_NOT_FOUND = 'VIEW_NOT_FOUND', + INVALID_VIEW_FIELD_GROUP_DATA = 'INVALID_VIEW_FIELD_GROUP_DATA', +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/resolvers/view-field-group.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/resolvers/view-field-group.resolver.ts new file mode 100644 index 00000000000..7050ddd700b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/resolvers/view-field-group.resolver.ts @@ -0,0 +1,131 @@ +import { UseFilters, UseGuards } from '@nestjs/common'; +import { + Args, + Context, + Mutation, + Parent, + Query, + ResolveField, +} from '@nestjs/graphql'; + +import { isArray } from '@sniptt/guards'; + +import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; +import { DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input'; +import { DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input'; +import { UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input'; +import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { ViewFieldGroupService } from 'src/engine/metadata-modules/view-field-group/services/view-field-group.service'; +import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; +import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter'; + +@MetadataResolver(() => ViewFieldGroupDTO) +@UseFilters(ViewGraphqlApiExceptionFilter) +@UseGuards(WorkspaceAuthGuard) +export class ViewFieldGroupResolver { + constructor(private readonly viewFieldGroupService: ViewFieldGroupService) {} + + @Query(() => [ViewFieldGroupDTO]) + @UseGuards(NoPermissionGuard) + async getCoreViewFieldGroups( + @Args('viewId', { type: () => String }) viewId: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.viewFieldGroupService.findByViewId(workspace.id, viewId); + } + + @Query(() => ViewFieldGroupDTO, { nullable: true }) + @UseGuards(NoPermissionGuard) + async getCoreViewFieldGroup( + @Args('id', { type: () => String }) id: string, + @AuthWorkspace() workspace: WorkspaceEntity, + ): Promise { + return this.viewFieldGroupService.findById(id, workspace.id); + } + + @Mutation(() => ViewFieldGroupDTO) + @UseGuards(NoPermissionGuard) + async updateCoreViewFieldGroup( + @Args('input') updateViewFieldGroupInput: UpdateViewFieldGroupInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + ): Promise { + return await this.viewFieldGroupService.updateOne({ + updateViewFieldGroupInput, + workspaceId, + }); + } + + @Mutation(() => ViewFieldGroupDTO) + @UseGuards(NoPermissionGuard) + async createCoreViewFieldGroup( + @Args('input') + createViewFieldGroupInput: CreateViewFieldGroupInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + ): Promise { + return await this.viewFieldGroupService.createOne({ + createViewFieldGroupInput, + workspaceId, + }); + } + + @Mutation(() => [ViewFieldGroupDTO]) + @UseGuards(NoPermissionGuard) + async createManyCoreViewFieldGroups( + @Args('inputs', { type: () => [CreateViewFieldGroupInput] }) + createViewFieldGroupInputs: CreateViewFieldGroupInput[], + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + ): Promise { + return await this.viewFieldGroupService.createMany({ + createViewFieldGroupInputs, + workspaceId, + }); + } + + @Mutation(() => ViewFieldGroupDTO) + @UseGuards(NoPermissionGuard) + async deleteCoreViewFieldGroup( + @Args('input') deleteViewFieldGroupInput: DeleteViewFieldGroupInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + ): Promise { + return await this.viewFieldGroupService.deleteOne({ + deleteViewFieldGroupInput, + workspaceId, + }); + } + + @Mutation(() => ViewFieldGroupDTO) + @UseGuards(NoPermissionGuard) + async destroyCoreViewFieldGroup( + @Args('input') + destroyViewFieldGroupInput: DestroyViewFieldGroupInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + ): Promise { + return await this.viewFieldGroupService.destroyOne({ + destroyViewFieldGroupInput, + workspaceId, + }); + } + + @ResolveField(() => [ViewFieldDTO]) + async viewFields( + @Parent() viewFieldGroup: ViewFieldGroupDTO, + @Context() context: { loaders: IDataloaders }, + @AuthWorkspace() workspace: WorkspaceEntity, + ) { + if (isArray(viewFieldGroup.viewFields)) { + return viewFieldGroup.viewFields; + } + + return context.loaders.viewFieldsByViewFieldGroupIdLoader.load({ + workspaceId: workspace.id, + viewFieldGroupId: viewFieldGroup.id, + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts new file mode 100644 index 00000000000..11f2a134909 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/services/view-field-group.service.ts @@ -0,0 +1,366 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined } from 'twenty-shared/utils'; +import { IsNull, Repository } from 'typeorm'; + +import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-create-view-field-group-input-to-flat-view-field-group-to-create.util'; +import { fromDeleteViewFieldGroupInputToFlatViewFieldGroupOrThrow } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-delete-view-field-group-input-to-flat-view-field-group-or-throw.util'; +import { fromDestroyViewFieldGroupInputToFlatViewFieldGroupOrThrow } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-destroy-view-field-group-input-to-flat-view-field-group-or-throw.util'; +import { fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view-field-group/utils/from-update-view-field-group-input-to-flat-view-field-group-to-update-or-throw.util'; +import { CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; +import { DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input'; +import { DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input'; +import { UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input'; +import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { + ViewFieldGroupException, + ViewFieldGroupExceptionCode, +} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception'; +import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util'; +import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; + +@Injectable() +export class ViewFieldGroupService { + constructor( + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + @InjectRepository(ViewFieldGroupEntity) + private readonly viewFieldGroupRepository: Repository, + private readonly applicationService: ApplicationService, + ) {} + + async createOne({ + createViewFieldGroupInput, + workspaceId, + }: { + createViewFieldGroupInput: CreateViewFieldGroupInput; + workspaceId: string; + }): Promise { + const [createdViewFieldGroup] = await this.createMany({ + workspaceId, + createViewFieldGroupInputs: [createViewFieldGroupInput], + }); + + if (!isDefined(createdViewFieldGroup)) { + throw new ViewFieldGroupException( + 'Failed to create view field group', + ViewFieldGroupExceptionCode.INVALID_VIEW_FIELD_GROUP_DATA, + ); + } + + return createdViewFieldGroup; + } + + async createMany({ + createViewFieldGroupInputs, + workspaceId, + }: { + createViewFieldGroupInputs: CreateViewFieldGroupInput[]; + workspaceId: string; + }): Promise { + if (createViewFieldGroupInputs.length === 0) { + return []; + } + + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewMaps'], + }, + ); + + const flatViewFieldGroupsToCreate = createViewFieldGroupInputs.map( + (createViewFieldGroupInput) => + fromCreateViewFieldGroupInputToFlatViewFieldGroupToCreate({ + createViewFieldGroupInput, + flatApplication: workspaceCustomFlatApplication, + flatViewMaps, + }), + ); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewFieldGroup: { + flatEntityToCreate: flatViewFieldGroupsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while creating view field groups', + ); + } + + const { flatViewFieldGroupMaps: recomputedExistingFlatViewFieldGroupMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps'], + }, + ); + + return findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: flatViewFieldGroupsToCreate.map((entity) => entity.id), + flatEntityMaps: recomputedExistingFlatViewFieldGroupMaps, + }).map(fromFlatViewFieldGroupToViewFieldGroupDto); + } + + async updateOne({ + updateViewFieldGroupInput, + workspaceId, + }: { + workspaceId: string; + updateViewFieldGroupInput: UpdateViewFieldGroupInput; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps'], + }, + ); + + const optimisticallyUpdatedFlatViewFieldGroup = + fromUpdateViewFieldGroupInputToFlatViewFieldGroupToUpdateOrThrow({ + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + updateViewFieldGroupInput, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewFieldGroup: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatViewFieldGroup], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while updating view field group', + ); + } + + const { flatViewFieldGroupMaps: recomputedExistingFlatViewFieldGroupMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps'], + }, + ); + + return fromFlatViewFieldGroupToViewFieldGroupDto( + findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: + optimisticallyUpdatedFlatViewFieldGroup.universalIdentifier, + flatEntityMaps: recomputedExistingFlatViewFieldGroupMaps, + }), + ); + } + + async deleteOne({ + deleteViewFieldGroupInput, + workspaceId, + }: { + deleteViewFieldGroupInput: DeleteViewFieldGroupInput; + workspaceId: string; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps'], + }, + ); + + const optimisticallyUpdatedFlatViewFieldGroupWithDeletedAt = + fromDeleteViewFieldGroupInputToFlatViewFieldGroupOrThrow({ + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + deleteViewFieldGroupInput, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewFieldGroup: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + optimisticallyUpdatedFlatViewFieldGroupWithDeletedAt, + ], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting view field group', + ); + } + + const { flatViewFieldGroupMaps: recomputedExistingFlatViewFieldGroupMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps'], + }, + ); + + return fromFlatViewFieldGroupToViewFieldGroupDto( + findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: + optimisticallyUpdatedFlatViewFieldGroupWithDeletedAt.universalIdentifier, + flatEntityMaps: recomputedExistingFlatViewFieldGroupMaps, + }), + ); + } + + async destroyOne({ + destroyViewFieldGroupInput, + workspaceId, + }: { + destroyViewFieldGroupInput: DestroyViewFieldGroupInput; + workspaceId: string; + }): Promise { + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const { flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatViewFieldGroupMaps'], + }, + ); + + const existingViewFieldGroupToDelete = + fromDestroyViewFieldGroupInputToFlatViewFieldGroupOrThrow({ + destroyViewFieldGroupInput, + flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps, + }); + + const existingFlatViewFieldGroup = + findFlatEntityByUniversalIdentifierOrThrow({ + universalIdentifier: existingViewFieldGroupToDelete.universalIdentifier, + flatEntityMaps: existingFlatViewFieldGroupMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + viewFieldGroup: { + flatEntityToCreate: [], + flatEntityToDelete: [existingViewFieldGroupToDelete], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while destroying view field group', + ); + } + + return fromFlatViewFieldGroupToViewFieldGroupDto({ + ...existingFlatViewFieldGroup, + deletedAt: new Date().toISOString(), + }); + } + + async findByViewId( + workspaceId: string, + viewId: string, + ): Promise { + return this.viewFieldGroupRepository.find({ + where: { + workspaceId, + viewId, + deletedAt: IsNull(), + }, + order: { position: 'ASC' }, + }); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + const viewFieldGroup = await this.viewFieldGroupRepository.findOne({ + where: { + id, + workspaceId, + deletedAt: IsNull(), + }, + }); + + return viewFieldGroup || null; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util.ts new file mode 100644 index 00000000000..1dad8888158 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util.ts @@ -0,0 +1,15 @@ +import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const fromFlatViewFieldGroupToViewFieldGroupDto = ( + flatViewFieldGroup: FlatViewFieldGroup, +): ViewFieldGroupDTO => { + const { createdAt, updatedAt, deletedAt, ...rest } = flatViewFieldGroup; + + return { + ...rest, + createdAt: new Date(createdAt), + updatedAt: new Date(updatedAt), + deletedAt: deletedAt ? new Date(deletedAt) : null, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field-group/view-field-group.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-field-group/view-field-group.module.ts new file mode 100644 index 00000000000..efe6c3b46dc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/view-field-group/view-field-group.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { ViewFieldGroupResolver } from 'src/engine/metadata-modules/view-field-group/resolvers/view-field-group.resolver'; +import { ViewFieldGroupService } from 'src/engine/metadata-modules/view-field-group/services/view-field-group.service'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ViewFieldGroupEntity, ViewEntity]), + WorkspaceCacheStorageModule, + ApplicationModule, + PermissionsModule, + WorkspaceMigrationModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], + providers: [ViewFieldGroupResolver, ViewFieldGroupService], + exports: [ViewFieldGroupService], +}) +export class ViewFieldGroupModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts index ada53554e9f..f580ba7ab62 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts @@ -29,7 +29,7 @@ import { ViewFieldExceptionMessageKey, } from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception'; import { ViewFieldRestApiExceptionFilter } from 'src/engine/metadata-modules/view-field/filters/view-field-rest-api-exception.filter'; -import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service'; +import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard'; import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard'; import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-field-permission.guard'; @@ -38,7 +38,7 @@ import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view @UseGuards(WorkspaceAuthGuard) @UseFilters(ViewFieldRestApiExceptionFilter) export class ViewFieldController { - constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {} + constructor(private readonly viewFieldService: ViewFieldService) {} @Get() @UseGuards(NoPermissionGuard) @@ -47,10 +47,10 @@ export class ViewFieldController { @Query('viewId') viewId?: string, ): Promise { if (viewId) { - return this.viewFieldV2Service.findByViewId(workspace.id, viewId); + return this.viewFieldService.findByViewId(workspace.id, viewId); } - return this.viewFieldV2Service.findByWorkspaceId(workspace.id); + return this.viewFieldService.findByWorkspaceId(workspace.id); } @Get(':id') @@ -59,7 +59,7 @@ export class ViewFieldController { @Param('id') id: string, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - const viewField = await this.viewFieldV2Service.findById(id, workspace.id); + const viewField = await this.viewFieldService.findById(id, workspace.id); if (!isDefined(viewField)) { throw new ViewFieldException( @@ -86,7 +86,7 @@ export class ViewFieldController { @Body() input: UpdateViewFieldInput['update'], @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.updateOne({ + return await this.viewFieldService.updateOne({ updateViewFieldInput: { id, update: input }, workspaceId: workspace.id, }); @@ -98,7 +98,7 @@ export class ViewFieldController { @Body() input: CreateViewFieldInput, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.createOne({ + return await this.viewFieldService.createOne({ createViewFieldInput: input, workspaceId: workspace.id, }); @@ -110,7 +110,7 @@ export class ViewFieldController { @Param('id') id: string, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise<{ success: boolean }> { - const deletedViewField = await this.viewFieldV2Service.deleteOne({ + const deletedViewField = await this.viewFieldService.deleteOne({ deleteViewFieldInput: { id }, workspaceId: workspace.id, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input.ts index cb6fca53796..3c09d0e9697 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input.ts @@ -46,6 +46,11 @@ export class CreateViewFieldInput { @Field(() => AggregateOperations, { nullable: true }) aggregateOperation?: AggregateOperations; + @IsOptional() + @IsUUID() + @Field(() => UUIDScalarType, { nullable: true }) + viewFieldGroupId?: string; + @HideField() universalIdentifier?: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input.ts index 65bb439d9fd..24efa258289 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input.ts @@ -35,6 +35,11 @@ class UpdateViewFieldInputUpdates { @IsEnum(AggregateOperations) @Field(() => AggregateOperations, { nullable: true }) aggregateOperation?: AggregateOperations; + + @IsOptional() + @IsUUID() + @Field(() => UUIDScalarType, { nullable: true }) + viewFieldGroupId?: string | null; } @InputType() diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/entities/view-field.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/entities/view-field.entity.ts index bd20b340ea4..ec1d61d4d68 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/entities/view-field.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/entities/view-field.entity.ts @@ -13,6 +13,7 @@ import { import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @@ -64,6 +65,9 @@ export class ViewFieldEntity @Column({ nullable: false, type: 'uuid' }) viewId: string; + @Column({ nullable: true, type: 'uuid' }) + viewFieldGroupId: string | null; + @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; @@ -78,4 +82,15 @@ export class ViewFieldEntity }) @JoinColumn({ name: 'viewId' }) view: Relation; + + @ManyToOne( + () => ViewFieldGroupEntity, + (viewFieldGroup) => viewFieldGroup.viewFields, + { + onDelete: 'SET NULL', + nullable: true, + }, + ) + @JoinColumn({ name: 'viewFieldGroupId' }) + viewFieldGroup: Relation | null; } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts index a54f5244ee7..a62670255a0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts @@ -1,9 +1,9 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query } from '@nestjs/graphql'; +import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input'; @@ -12,7 +12,7 @@ import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dt import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input'; import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; -import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service'; +import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard'; import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard'; import { DestroyViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-field-permission.guard'; @@ -23,7 +23,7 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/ @UseFilters(ViewGraphqlApiExceptionFilter) @UseGuards(WorkspaceAuthGuard) export class ViewFieldResolver { - constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {} + constructor(private readonly viewFieldService: ViewFieldService) {} @Query(() => [ViewFieldDTO]) @UseGuards(NoPermissionGuard) @@ -31,7 +31,7 @@ export class ViewFieldResolver { @Args('viewId', { type: () => String }) viewId: string, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - return this.viewFieldV2Service.findByViewId(workspace.id, viewId); + return this.viewFieldService.findByViewId(workspace.id, viewId); } @Query(() => ViewFieldDTO, { nullable: true }) @@ -40,7 +40,7 @@ export class ViewFieldResolver { @Args('id', { type: () => String }) id: string, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - return this.viewFieldV2Service.findById(id, workspace.id); + return this.viewFieldService.findById(id, workspace.id); } @Mutation(() => ViewFieldDTO) @@ -49,7 +49,7 @@ export class ViewFieldResolver { @Args('input') updateViewFieldInput: UpdateViewFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.updateOne({ + return await this.viewFieldService.updateOne({ updateViewFieldInput, workspaceId, }); @@ -61,7 +61,7 @@ export class ViewFieldResolver { @Args('input') createViewFieldInput: CreateViewFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.createOne({ + return await this.viewFieldService.createOne({ createViewFieldInput, workspaceId, }); @@ -74,7 +74,7 @@ export class ViewFieldResolver { createViewFieldInputs: CreateViewFieldInput[], @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.createMany({ + return await this.viewFieldService.createMany({ createViewFieldInputs, workspaceId, }); @@ -86,7 +86,7 @@ export class ViewFieldResolver { @Args('input') deleteViewFieldInput: DeleteViewFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.deleteOne({ + return await this.viewFieldService.deleteOne({ deleteViewFieldInput, workspaceId, }); @@ -98,7 +98,7 @@ export class ViewFieldResolver { @Args('input') destroyViewFieldInput: DestroyViewFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { - return await this.viewFieldV2Service.destroyOne({ + return await this.viewFieldService.destroyOne({ destroyViewFieldInput, workspaceId, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts similarity index 96% rename from packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts rename to packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts index f27befe6b83..fa0dd8103de 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts @@ -27,7 +27,7 @@ import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @Injectable() -export class ViewFieldV2Service { +export class ViewFieldService { constructor( private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, @@ -76,11 +76,15 @@ export class ViewFieldV2Service { }, ); - const { flatFieldMetadataMaps, flatViewMaps } = + const { flatFieldMetadataMaps, flatViewMaps, flatViewFieldGroupMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatMapsKeys: ['flatFieldMetadataMaps', 'flatViewMaps'], + flatMapsKeys: [ + 'flatFieldMetadataMaps', + 'flatViewMaps', + 'flatViewFieldGroupMaps', + ], }, ); @@ -91,6 +95,7 @@ export class ViewFieldV2Service { flatApplication: workspaceCustomFlatApplication, flatFieldMetadataMaps, flatViewMaps, + flatViewFieldGroupMaps, }), ); @@ -146,17 +151,21 @@ export class ViewFieldV2Service { }, ); - const { flatViewFieldMaps: existingFlatViewFieldMaps } = + const { + flatViewFieldMaps: existingFlatViewFieldMaps, + flatViewFieldGroupMaps, + } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatMapsKeys: ['flatViewFieldMaps'], + flatMapsKeys: ['flatViewFieldMaps', 'flatViewFieldGroupMaps'], }, ); const optimisticallyUpdatedFlatView = fromUpdateViewFieldInputToFlatViewFieldToUpdateOrThrow({ flatViewFieldMaps: existingFlatViewFieldMaps, + flatViewFieldGroupMaps, updateViewFieldInput, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts index f39ffaffafe..636ebd94448 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts @@ -7,7 +7,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { ViewFieldController } from 'src/engine/metadata-modules/view-field/controllers/view-field.controller'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFieldResolver } from 'src/engine/metadata-modules/view-field/resolvers/view-field.resolver'; -import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service'; +import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -24,7 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace ViewPermissionsModule, ], controllers: [ViewFieldController], - providers: [ViewFieldResolver, ViewFieldV2Service], - exports: [ViewFieldV2Service], + providers: [ViewFieldResolver, ViewFieldService], + exports: [ViewFieldService], }) export class ViewFieldModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/dtos/view.dto.ts b/packages/twenty-server/src/engine/metadata-modules/view/dtos/view.dto.ts index 8faf334d312..6c3595ef5d8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/dtos/view.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/dtos/view.dto.ts @@ -4,6 +4,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto'; import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto'; @@ -104,6 +105,9 @@ export class ViewDTO { @Field(() => [ViewGroupDTO]) viewGroups?: ViewGroupDTO[]; + @Field(() => [ViewFieldGroupDTO]) + viewFieldGroups?: ViewFieldGroupDTO[]; + @Field(() => ViewVisibility, { nullable: false, }) diff --git a/packages/twenty-server/src/engine/metadata-modules/view/entities/view.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/entities/view.entity.ts index 284a2535dd7..fd0c035329b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/entities/view.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/entities/view.entity.ts @@ -17,6 +17,7 @@ import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; @@ -191,6 +192,12 @@ export class ViewEntity extends SyncableEntity implements Required { @OneToMany(() => ViewFieldEntity, (viewField) => viewField.view) viewFields: Relation; + @OneToMany( + () => ViewFieldGroupEntity, + (viewFieldGroup) => viewFieldGroup.view, + ) + viewFieldGroups: Relation; + @OneToMany(() => ViewFilterEntity, (viewFilter) => viewFilter.view) viewFilters: Relation; diff --git a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts index 0083dae79ab..a9cf03f3845 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts @@ -11,19 +11,20 @@ import { import { isArray } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; +import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; +import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto'; -import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service'; +import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto'; import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service'; import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto'; @@ -56,7 +57,7 @@ export class ViewResolver { private readonly viewGroupService: ViewGroupService, private readonly i18nService: I18nService, - private readonly viewFieldV2Service: ViewFieldV2Service, + private readonly viewFieldService: ViewFieldService, ) {} @ResolveField(() => String) @@ -209,7 +210,7 @@ export class ViewResolver { return view.viewFields; } - return this.viewFieldV2Service.findByViewId(workspace.id, view.id); + return this.viewFieldService.findByViewId(workspace.id, view.id); } @ResolveField(() => [ViewFilterDTO]) @@ -259,4 +260,20 @@ export class ViewResolver { return this.viewGroupService.findByViewId(workspace.id, view.id); } + + @ResolveField(() => [ViewFieldGroupDTO]) + async viewFieldGroups( + @Parent() view: ViewDTO, + @Context() context: { loaders: IDataloaders }, + @AuthWorkspace() workspace: WorkspaceEntity, + ) { + if (isArray(view.viewFieldGroups)) { + return view.viewFieldGroups; + } + + return context.loaders.viewFieldGroupsByViewIdLoader.load({ + workspaceId: workspace.id, + viewId: view.id, + }); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts index 8d30a7a4045..f5c740fd485 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception-handler.util.ts @@ -7,6 +7,10 @@ import { NotFoundError, UserInputError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + ViewFieldGroupException, + ViewFieldGroupExceptionCode, +} from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception'; import { ViewFieldException, ViewFieldExceptionCode, @@ -77,6 +81,22 @@ export const viewGraphqlApiExceptionHandler = (error: Error, i18n: I18n) => { } } + if (error instanceof ViewFieldGroupException) { + switch (error.code) { + case ViewFieldGroupExceptionCode.VIEW_FIELD_GROUP_NOT_FOUND: + throw new NotFoundError(error.message); + case ViewFieldGroupExceptionCode.VIEW_NOT_FOUND: + throw new NotFoundError(error.message); + case ViewFieldGroupExceptionCode.INVALID_VIEW_FIELD_GROUP_DATA: + throw new UserInputError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); + default: { + return assertUnreachable(error.code); + } + } + } + if (error instanceof ViewFilterException) { switch (error.code) { case ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter.ts index 78d4620fca6..1a2c9c98db3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter.ts @@ -9,6 +9,7 @@ import { GqlExecutionContext } from '@nestjs/graphql'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; +import { ViewFieldGroupException } from 'src/engine/metadata-modules/view-field-group/exceptions/view-field-group.exception'; import { ViewFieldException } from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception'; import { ViewFilterGroupException } from 'src/engine/metadata-modules/view-filter-group/exceptions/view-filter-group.exception'; import { ViewFilterException } from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception'; @@ -35,6 +36,7 @@ export class ViewGraphqlApiExceptionFilter implements ExceptionFilter { exception: | ViewException | ViewFieldException + | ViewFieldGroupException | ViewFilterException | ViewFilterGroupException | ViewGroupException diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts b/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts index b5f2571f04b..03579f9b099 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts @@ -7,6 +7,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata import { FlatViewModule } from 'src/engine/metadata-modules/flat-view/flat-view.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; +import { ViewFieldGroupModule } from 'src/engine/metadata-modules/view-field-group/view-field-group.module'; import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module'; import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module'; import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module'; @@ -27,6 +28,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace TypeOrmModule.forFeature([ViewEntity]), ViewPermissionsModule, ViewFieldModule, + ViewFieldGroupModule, ViewFilterModule, ViewFilterGroupModule, ViewGroupModule, diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index b20bc96e51a..0e27db6c0d0 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -17,6 +17,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { flatIndexMaps: 'flat-maps:index', flatViewMaps: 'flat-maps:view', flatViewFieldMaps: 'flat-maps:view-field', + flatViewFieldGroupMaps: 'flat-maps:view-field-group', flatViewGroupMaps: 'flat-maps:view-group', flatViewFilterMaps: 'flat-maps:view-filter', flatViewFilterGroupMaps: 'flat-maps:view-filter-group', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-all-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-all-metadata-name.constant.ts index 57ecb2067be..90e21f1b4b7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-all-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-all-metadata-name.constant.ts @@ -5,6 +5,7 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [ 'objectMetadata', 'fieldMetadata', 'viewField', + 'viewFieldGroup', 'viewFilter', 'viewGroup', 'view', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts index 29fef22e71b..1a99d9d1653 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant.ts @@ -97,6 +97,8 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({ const flatViewFilterGroupMaps = createEmptyFlatEntityMaps(); + const flatViewFieldGroupMaps = createEmptyFlatEntityMaps(); + const flatViewFilterMaps = buildStandardFlatViewFilterMetadataMaps({ dependencyFlatEntityMaps: { flatFieldMetadataMaps, @@ -114,6 +116,7 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({ flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewMaps, + flatViewFieldGroupMaps, }, now, standardObjectMetadataRelatedEntityIds, @@ -188,6 +191,7 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({ const allFlatEntityMaps: TwentyStandardAllFlatEntityMaps = { flatViewFieldMaps, + flatViewFieldGroupMaps, flatViewFilterMaps, flatViewGroupMaps, flatViewMaps, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts index 43bc511a3aa..6b79e83c0f9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util.ts @@ -88,6 +88,8 @@ export const createStandardViewFieldFlatMetadata = < fieldMetadataId: standardObjectMetadataRelatedEntityIds[objectName].fields[fieldName].id, fieldMetadataUniversalIdentifier: fieldDefinition.universalIdentifier, + viewFieldGroupId: null, + viewFieldGroupUniversalIdentifier: null, position, isVisible, size, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts index ffbfbd600d8..d8238899524 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util.ts @@ -142,6 +142,8 @@ export const createStandardViewFlatMetadata = < createdByUserWorkspaceId: null, viewFieldIds: [], viewFieldUniversalIdentifiers: [], + viewFieldGroupIds: [], + viewFieldGroupUniversalIdentifiers: [], viewFilterIds: [], viewFilterUniversalIdentifiers: [], viewGroupIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts index 8cbd5a102ce..656c3063cc9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/utils/enrich-create-workspace-migration-action-with-ids.util.ts @@ -143,6 +143,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({ case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index eca8f05505a..e04b283c18f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -31,6 +31,7 @@ import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service'; import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service'; import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; +import { WorkspaceMigrationViewFieldGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service'; import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service'; import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service'; import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service'; @@ -47,6 +48,7 @@ export class WorkspaceMigrationBuildOrchestratorService { private readonly workspaceMigrationViewFilterActionsBuilderService: WorkspaceMigrationViewFilterActionsBuilderService, private readonly workspaceMigrationViewFilterGroupActionsBuilderService: WorkspaceMigrationViewFilterGroupActionsBuilderService, private readonly workspaceMigrationViewGroupActionsBuilderService: WorkspaceMigrationViewGroupActionsBuilderService, + private readonly workspaceMigrationViewFieldGroupActionsBuilderService: WorkspaceMigrationViewFieldGroupActionsBuilderService, private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService, private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService, private readonly workspaceMigrationFieldActionsBuilderService: WorkspaceMigrationFieldActionsBuilderService, @@ -150,6 +152,7 @@ export class WorkspaceMigrationBuildOrchestratorService { flatViewFilterMaps, flatViewFilterGroupMaps, flatViewGroupMaps, + flatViewFieldGroupMaps, flatRowLevelPermissionPredicateMaps, flatRowLevelPermissionPredicateGroupMaps, flatRoleMaps, @@ -324,6 +327,8 @@ export class WorkspaceMigrationBuildOrchestratorService { flatFieldMetadataMaps: optimisticAllFlatEntityMaps.flatFieldMetadataMaps, flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, + flatViewFieldGroupMaps: + optimisticAllFlatEntityMaps.flatViewFieldGroupMaps, }, workspaceId, }, @@ -452,6 +457,41 @@ export class WorkspaceMigrationBuildOrchestratorService { } } + if (isDefined(flatViewFieldGroupMaps)) { + const { from: fromFlatViewFieldGroupMaps, to: toFlatViewFieldGroupMaps } = + flatViewFieldGroupMaps; + + const viewFieldGroupResult = + await this.workspaceMigrationViewFieldGroupActionsBuilderService.validateAndBuild( + { + additionalCacheDataMaps, + from: fromFlatViewFieldGroupMaps, + to: toFlatViewFieldGroupMaps, + buildOptions, + dependencyOptimisticFlatEntityMaps: { + flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps, + }, + workspaceId, + }, + ); + + this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation( + { + allFlatEntityMaps: optimisticAllFlatEntityMaps, + flatEntityMapsAndRelatedFlatEntityMaps: + viewFieldGroupResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps, + }, + ); + + if (viewFieldGroupResult.status === 'fail') { + orchestratorFailureReport.viewFieldGroup.push( + ...viewFieldGroupResult.errors, + ); + } else { + orchestratorActionsReport.viewFieldGroup = viewFieldGroupResult.actions; + } + } + if (isDefined(flatRowLevelPermissionPredicateGroupMaps)) { const { from: fromFlatRowLevelPermissionPredicateGroupMaps, @@ -1003,6 +1043,9 @@ export class WorkspaceMigrationBuildOrchestratorService { ...aggregatedOrchestratorActionsReport.viewGroup.delete, ...aggregatedOrchestratorActionsReport.viewGroup.create, ...aggregatedOrchestratorActionsReport.viewGroup.update, + ...aggregatedOrchestratorActionsReport.viewFieldGroup.delete, + ...aggregatedOrchestratorActionsReport.viewFieldGroup.create, + ...aggregatedOrchestratorActionsReport.viewFieldGroup.update, /// // Logic functions diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts index d987efddd75..37415db4a84 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant.ts @@ -15,6 +15,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = { view: {}, viewField: {}, viewGroup: {}, + viewFieldGroup: {}, viewFilter: {}, viewFilterGroup: {}, index: {}, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant.ts index 2e19679a733..ae6581c9fe7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant.ts @@ -202,6 +202,7 @@ export const ALL_UNIVERSAL_METADATA_RELATIONS = { viewFilters: { metadataName: 'viewFilter' }, viewFilterGroups: { metadataName: 'viewFilterGroup' }, viewGroups: { metadataName: 'viewGroup' }, + viewFieldGroups: { metadataName: 'viewFieldGroup' }, // TODO migrate viewSort to v2 viewSorts: null, }, @@ -224,11 +225,36 @@ export const ALL_UNIVERSAL_METADATA_RELATIONS = { universalForeignKey: 'viewUniversalIdentifier', isNullable: false, }, + viewFieldGroup: { + metadataName: 'viewFieldGroup', + foreignKey: 'viewFieldGroupId', + universalFlatEntityForeignKeyAggregator: + 'viewFieldUniversalIdentifiers', + universalForeignKey: 'viewFieldGroupUniversalIdentifier', + isNullable: true, + }, workspace: null, application: null, }, oneToMany: {}, }, + viewFieldGroup: { + manyToOne: { + view: { + metadataName: 'view', + foreignKey: 'viewId', + universalFlatEntityForeignKeyAggregator: + 'viewFieldGroupUniversalIdentifiers', + universalForeignKey: 'viewUniversalIdentifier', + isNullable: false, + }, + workspace: null, + application: null, + }, + oneToMany: { + viewFields: { metadataName: 'viewField' }, + }, + }, viewFilter: { manyToOne: { fieldMetadata: { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type.ts new file mode 100644 index 00000000000..f47649dfb3f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type.ts @@ -0,0 +1,7 @@ +import { type ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type UniversalFlatViewFieldGroup = UniversalFlatEntityFrom< + ViewFieldGroupEntity, + 'viewFieldGroup' +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type.ts new file mode 100644 index 00000000000..d0737ddd17b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type.ts @@ -0,0 +1,24 @@ +import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type'; +import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type'; +import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type'; +import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type'; +import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type'; +import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type'; + +export type UniversalCreateViewFieldGroupAction = + BaseUniversalCreateWorkspaceMigrationAction<'viewFieldGroup'>; + +export type FlatCreateViewFieldGroupAction = + BaseFlatCreateWorkspaceMigrationAction<'viewFieldGroup'>; + +export type FlatUpdateViewFieldGroupAction = + BaseFlatUpdateWorkspaceMigrationAction<'viewFieldGroup'>; + +export type UniversalUpdateViewFieldGroupAction = + BaseUniversalUpdateWorkspaceMigrationAction<'viewFieldGroup'>; + +export type UniversalDeleteViewFieldGroupAction = + BaseUniversalDeleteWorkspaceMigrationAction<'viewFieldGroup'>; + +export type FlatDeleteViewFieldGroupAction = + BaseFlatDeleteWorkspaceMigrationAction<'viewFieldGroup'>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service.ts new file mode 100644 index 00000000000..65a97d75260 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@nestjs/common'; + +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; + +import { UniversalUpdateViewFieldGroupAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type'; +import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; +import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type'; +import { FlatViewFieldGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-group-validator.service'; + +@Injectable() +export class WorkspaceMigrationViewFieldGroupActionsBuilderService extends WorkspaceEntityMigrationBuilderService< + typeof ALL_METADATA_NAME.viewFieldGroup +> { + constructor( + private readonly flatViewFieldGroupValidatorService: FlatViewFieldGroupValidatorService, + ) { + super(ALL_METADATA_NAME.viewFieldGroup); + } + + protected validateFlatEntityCreation( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.viewFieldGroup + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.viewFieldGroup, + 'create' + > { + const validationResult = + this.flatViewFieldGroupValidatorService.validateFlatViewFieldGroupCreation( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + return { + status: 'success', + action: { + type: 'create', + metadataName: 'viewFieldGroup', + flatEntity: args.flatEntityToValidate, + }, + }; + } + + protected validateFlatEntityDeletion( + args: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.viewFieldGroup + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.viewFieldGroup, + 'delete' + > { + const validationResult = + this.flatViewFieldGroupValidatorService.validateFlatViewFieldGroupDeletion( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + return { + status: 'success', + action: { + type: 'delete', + metadataName: 'viewFieldGroup', + universalIdentifier: args.flatEntityToValidate.universalIdentifier, + }, + }; + } + + protected validateFlatEntityUpdate( + args: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.viewFieldGroup + >, + ): UniversalFlatEntityValidationReturnType< + typeof ALL_METADATA_NAME.viewFieldGroup, + 'update' + > { + const validationResult = + this.flatViewFieldGroupValidatorService.validateFlatViewFieldGroupUpdate( + args, + ); + + if (validationResult.errors.length > 0) { + return { + status: 'fail', + ...validationResult, + }; + } + + const { universalIdentifier, flatEntityUpdate } = args; + + const updateAction: UniversalUpdateViewFieldGroupAction = { + type: 'update', + metadataName: 'viewFieldGroup', + universalIdentifier, + update: flatEntityUpdate, + }; + + return { + status: 'success', + action: updateAction, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-group-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-group-validator.service.ts new file mode 100644 index 00000000000..79ba56f4e53 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-group-validator.service.ts @@ -0,0 +1,143 @@ +import { Injectable } from '@nestjs/common'; + +import { msg, t } from '@lingui/core/macro'; +import { ALL_METADATA_NAME } from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +import { ViewExceptionCode } from 'src/engine/metadata-modules/view/exceptions/view.exception'; +import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; +import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util'; +import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; +import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type'; + +@Injectable() +export class FlatViewFieldGroupValidatorService { + public validateFlatViewFieldGroupCreation({ + flatEntityToValidate: flatViewFieldGroupToValidate, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatViewFieldGroupMaps: optimisticFlatViewFieldGroupMaps, + flatViewMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.viewFieldGroup + >): FailedFlatEntityValidation<'viewFieldGroup', 'create'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier: flatViewFieldGroupToValidate.universalIdentifier, + viewUniversalIdentifier: + flatViewFieldGroupToValidate.viewUniversalIdentifier, + }, + metadataName: 'viewFieldGroup', + type: 'create', + }); + + const existingFlatViewFieldGroup = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatViewFieldGroupToValidate.universalIdentifier, + flatEntityMaps: optimisticFlatViewFieldGroupMaps, + }); + + if (isDefined(existingFlatViewFieldGroup)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View field group with this universal identifier already exists`, + userFriendlyMessage: msg`View field group already exists`, + }); + } + + const flatView = findFlatEntityByUniversalIdentifier({ + universalIdentifier: flatViewFieldGroupToValidate.viewUniversalIdentifier, + flatEntityMaps: flatViewMaps, + }); + + if (!isDefined(flatView)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View not found`, + userFriendlyMessage: msg`View not found`, + }); + } + + return validationResult; + } + + public validateFlatViewFieldGroupDeletion({ + flatEntityToValidate: { universalIdentifier }, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatViewFieldGroupMaps: optimisticFlatViewFieldGroupMaps, + }, + }: UniversalFlatEntityValidationArgs< + typeof ALL_METADATA_NAME.viewFieldGroup + >): FailedFlatEntityValidation<'viewFieldGroup', 'delete'> { + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'viewFieldGroup', + type: 'delete', + }); + + const existingFlatViewFieldGroup = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatViewFieldGroupMaps, + }); + + if (!isDefined(existingFlatViewFieldGroup)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View field group to delete not found`, + userFriendlyMessage: msg`View field group to delete not found`, + }); + } + + return validationResult; + } + + public validateFlatViewFieldGroupUpdate({ + universalIdentifier, + optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { + flatViewFieldGroupMaps: optimisticFlatViewFieldGroupMaps, + flatViewMaps, + }, + }: FlatEntityUpdateValidationArgs< + typeof ALL_METADATA_NAME.viewFieldGroup + >): FailedFlatEntityValidation<'viewFieldGroup', 'update'> { + const existingFlatViewFieldGroup = findFlatEntityByUniversalIdentifier({ + universalIdentifier, + flatEntityMaps: optimisticFlatViewFieldGroupMaps, + }); + + const validationResult = getEmptyFlatEntityValidationError({ + flatEntityMinimalInformation: { + universalIdentifier, + }, + metadataName: 'viewFieldGroup', + type: 'update', + }); + + if (!isDefined(existingFlatViewFieldGroup)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View field group to update not found`, + userFriendlyMessage: msg`View field group to update not found`, + }); + + return validationResult; + } + + const flatView = findFlatEntityByUniversalIdentifier({ + universalIdentifier: existingFlatViewFieldGroup.viewUniversalIdentifier, + flatEntityMaps: flatViewMaps, + }); + + if (!isDefined(flatView)) { + validationResult.errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View field group parent view not found`, + userFriendlyMessage: msg`View field group parent view not found`, + }); + } + + return validationResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts index 2af3517380e..fa90309ad35 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts @@ -19,6 +19,7 @@ import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service'; import { FlatLogicFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-logic-function-validator.service'; import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service'; +import { FlatViewFieldGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-group-validator.service'; import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service'; import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-group-validator.service'; import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service'; @@ -34,6 +35,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp FlatViewFilterValidatorService, FlatViewFilterGroupValidatorService, FlatViewGroupValidatorService, + FlatViewFieldGroupValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, FlatObjectMetadataValidatorService, @@ -60,6 +62,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp FlatViewFilterValidatorService, FlatViewFilterGroupValidatorService, FlatViewGroupValidatorService, + FlatViewFieldGroupValidatorService, FlatIndexValidatorService, FlatFieldMetadataValidatorService, FlatObjectMetadataValidatorService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts index 429adfc615a..30d1a3e04c3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts @@ -19,6 +19,7 @@ import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service'; import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service'; import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service'; +import { WorkspaceMigrationViewFieldGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service'; import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service'; import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service'; import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service'; @@ -36,6 +37,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationViewFilterActionsBuilderService, WorkspaceMigrationViewFilterGroupActionsBuilderService, WorkspaceMigrationViewGroupActionsBuilderService, + WorkspaceMigrationViewFieldGroupActionsBuilderService, WorkspaceMigrationIndexActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, @@ -61,6 +63,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace- WorkspaceMigrationViewFilterActionsBuilderService, WorkspaceMigrationViewFilterGroupActionsBuilderService, WorkspaceMigrationViewGroupActionsBuilderService, + WorkspaceMigrationViewFieldGroupActionsBuilderService, WorkspaceMigrationFieldActionsBuilderService, WorkspaceMigrationLogicFunctionActionsBuilderService, WorkspaceMigrationRoleActionsBuilderService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts new file mode 100644 index 00000000000..f5997416bcc --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; + +import { v4 } from 'uuid'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util'; +import { + FlatCreateViewFieldGroupAction, + UniversalCreateViewFieldGroupAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreateViewFieldGroupActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create', + 'viewFieldGroup', +) { + override async transpileUniversalActionToFlatAction({ + action, + allFlatEntityMaps, + flatApplication, + workspaceId, + }: WorkspaceMigrationActionRunnerArgs): Promise { + const { viewId } = resolveUniversalRelationIdentifiersToIds({ + flatEntityMaps: allFlatEntityMaps, + metadataName: action.metadataName, + universalForeignKeyValues: action.flatEntity, + }); + + return { + ...action, + flatEntity: { + ...action.flatEntity, + viewId, + viewFieldIds: [], + viewFieldUniversalIdentifiers: [], + id: action.id ?? v4(), + applicationId: flatApplication.id, + workspaceId, + }, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + const { flatEntity } = flatAction; + + const repository = + queryRunner.manager.getRepository( + ViewFieldGroupEntity, + ); + + await repository.insert({ + ...flatEntity, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/delete-view-field-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/delete-view-field-group-action-handler.service.ts new file mode 100644 index 00000000000..155a7ff8099 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/delete-view-field-group-action-handler.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { + FlatDeleteViewFieldGroupAction, + UniversalDeleteViewFieldGroupAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteViewFieldGroupActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete', + 'viewFieldGroup', +) { + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return this.transpileUniversalDeleteActionToFlatDeleteAction(context); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + + const repository = + queryRunner.manager.getRepository( + ViewFieldGroupEntity, + ); + + await repository.delete({ + id: flatAction.entityId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/update-view-field-group-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/update-view-field-group-action-handler.service.ts new file mode 100644 index 00000000000..a0245ab63e0 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/update-view-field-group-action-handler.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; +import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity'; +import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util'; +import { + FlatUpdateViewFieldGroupAction, + UniversalUpdateViewFieldGroupAction, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type'; +import { + WorkspaceMigrationActionRunnerArgs, + WorkspaceMigrationActionRunnerContext, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class UpdateViewFieldGroupActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update', + 'viewFieldGroup', +) { + override async transpileUniversalActionToFlatAction( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, allFlatEntityMaps } = context; + + const flatViewFieldGroup = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: allFlatEntityMaps.flatViewFieldGroupMaps, + universalIdentifier: action.universalIdentifier, + }); + + const update = resolveUniversalUpdateRelationIdentifiersToIds({ + metadataName: 'viewFieldGroup', + universalUpdate: action.update, + allFlatEntityMaps, + }); + + return { + type: 'update', + metadataName: 'viewFieldGroup', + entityId: flatViewFieldGroup.id, + update, + }; + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerContext, + ): Promise { + const { flatAction, queryRunner, workspaceId } = context; + const { entityId, update } = flatAction; + + const repository = + queryRunner.manager.getRepository( + ViewFieldGroupEntity, + ); + + await repository.update({ id: entityId, workspaceId }, update); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerContext, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts index aa0554763a4..1952f4321be 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service.ts @@ -30,7 +30,7 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne flatApplication, workspaceId, }: WorkspaceMigrationActionRunnerArgs): Promise { - const { fieldMetadataId, viewId } = + const { fieldMetadataId, viewId, viewFieldGroupId } = resolveUniversalRelationIdentifiersToIds({ flatEntityMaps: allFlatEntityMaps, metadataName: action.metadataName, @@ -43,6 +43,7 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne ...action.flatEntity, fieldMetadataId, viewId, + viewFieldGroupId, id: action.id ?? v4(), applicationId: flatApplication.id, workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts index 6c824eb388b..7369d9ba8fa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service.ts @@ -53,6 +53,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi applicationId: flatApplication.id, workspaceId, viewFieldIds: [], + viewFieldGroupIds: [], viewGroupIds: [], viewFilterIds: [], viewFilterGroupIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index 51967cd6f51..b87d4b0f58f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -54,6 +54,9 @@ import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/wo import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service'; import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service'; import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service'; +import { CreateViewFieldGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/create-view-field-group-action-handler.service'; +import { DeleteViewFieldGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/delete-view-field-group-action-handler.service'; +import { UpdateViewFieldGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field-group/services/update-view-field-group-action-handler.service'; import { CreateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service'; import { DeleteViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/delete-view-field-action-handler.service'; import { UpdateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/update-view-field-action-handler.service'; @@ -108,6 +111,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor UpdateViewGroupActionHandlerService, DeleteViewGroupActionHandlerService, + CreateViewFieldGroupActionHandlerService, + UpdateViewFieldGroupActionHandlerService, + DeleteViewFieldGroupActionHandlerService, + CreateLogicFunctionActionHandlerService, DeleteLogicFunctionActionHandlerService, UpdateLogicFunctionActionHandlerService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts index da2345fd9d1..7af7c4b47d2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-create-action.util.ts @@ -45,6 +45,7 @@ export const deriveMetadataEventsFromCreateAction = ( case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts index 7aa4e9c7b40..ca46100eb9a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-delete-action.util.ts @@ -22,6 +22,7 @@ export const deriveMetadataEventsFromDeleteAction = ({ case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts index be5072c5598..6e51a8a6bb3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/derive-metadata-events-from-update-action.util.ts @@ -89,6 +89,7 @@ export const deriveMetadataEventsFromUpdateAction = ({ case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts index 0de61907173..ebcb4ac0211 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util.ts @@ -45,6 +45,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({ case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts index 14749b889e1..a270d0c1c55 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util.ts @@ -22,6 +22,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({ case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts index 606761bd713..e143edab4ea 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-update-action-on-all-flat-entity-maps.util.ts @@ -44,6 +44,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({ case 'view': case 'viewField': case 'viewGroup': + case 'viewFieldGroup': case 'rowLevelPermissionPredicate': case 'rowLevelPermissionPredicateGroup': case 'viewFilterGroup': diff --git a/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts b/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts index d2021a785a0..01778ace569 100644 --- a/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts +++ b/packages/twenty-server/test/integration/constants/view-gql-fields.constants.ts @@ -70,3 +70,14 @@ export const VIEW_FILTER_GROUP_GQL_FIELDS = ` updatedAt deletedAt `; + +export const VIEW_FIELD_GROUP_GQL_FIELDS = ` + id + name + position + isVisible + viewId + createdAt + updatedAt + deletedAt +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-group-test.util.ts b/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-group-test.util.ts new file mode 100644 index 00000000000..c7cd33f342c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-group-test.util.ts @@ -0,0 +1,62 @@ +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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; + +export type ViewFieldGroupTestSetup = { + testViewId: string; + testObjectMetadataId: string; +}; + +export const setupViewFieldGroupTest = + async (): Promise => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'myFieldGroupTestObject', + namePlural: 'myFieldGroupTestObjects', + labelSingular: 'My Field Group Test Object', + labelPlural: 'My Field Group Test Objects', + icon: 'Icon123', + }, + }); + + const { + data: { + createCoreView: { id: testViewId }, + }, + } = await createOneCoreView({ + input: { + icon: 'icon123', + objectMetadataId, + name: 'TestViewForFieldGroups', + }, + expectToFail: false, + }); + + return { + testViewId, + testObjectMetadataId: objectMetadataId, + }; + }; + +export const cleanupViewFieldGroupTest = async ( + objectMetadataId: string, +): Promise => { + await updateOneObjectMetadata({ + input: { + idToUpdate: objectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataId }, + }); +}; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util.ts b/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-test.util.ts similarity index 87% rename from packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util.ts rename to packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-test.util.ts index 0fa0bf368bd..48ada563c01 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/utils/setup-view-field-test.util.ts @@ -11,7 +11,7 @@ export type ViewFieldTestSetup = { testFieldMetadataId: string; }; -export const setupViewFieldTestV2 = async (): Promise => { +export const setupViewFieldTest = async (): Promise => { const { data: { createOneObject: { id: objectMetadataId }, @@ -19,10 +19,10 @@ export const setupViewFieldTestV2 = async (): Promise => { } = await createOneObjectMetadata({ expectToFail: false, input: { - nameSingular: 'myFieldTestObjectV2', - namePlural: 'myFieldTestObjectsV2', - labelSingular: 'My Field Test Object v2', - labelPlural: 'My Field Test Objects v2', + nameSingular: 'myFieldTestObject', + namePlural: 'myFieldTestObjects', + labelSingular: 'My Field Test Object', + labelPlural: 'My Field Test Objects', icon: 'Icon123', }, }); @@ -68,7 +68,7 @@ export const setupViewFieldTestV2 = async (): Promise => { }; }; -export const cleanupViewFieldTestV2 = async ( +export const cleanupViewFieldTest = async ( objectMetadataId: string, ): Promise => { await updateOneObjectMetadata({ diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-create-many-view-field-groups.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-create-many-view-field-groups.integration-spec.ts.snap new file mode 100644 index 00000000000..a0cb1d4a0d9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-create-many-view-field-groups.integration-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Field Group Resolver - Failing Create Many Operations should fail when some inputs reference non-existent views 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Could not find view for given viewId", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-create-view-field-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-create-view-field-group.integration-spec.ts.snap new file mode 100644 index 00000000000..004ad7a974b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-create-view-field-group.integration-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Field Group Resolver - Failing Create Operation should fail to create view field group when non-existent view 1`] = ` +{ + "extensions": { + "code": "NOT_FOUND", + "subCode": "RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Could not find view for given viewId", + "name": "NotFoundError", +} +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-delete-view-field-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-delete-view-field-group.integration-spec.ts.snap new file mode 100644 index 00000000000..8234ce2bafb --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-delete-view-field-group.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Field Group Resolver - Failing Delete Operation should fail to delete view field group when non-existent view field group 1`] = ` +{ + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": Any, + "userFriendlyMessage": "A view field group error occurred.", + }, + "message": "View field group to delete not found", +} +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-destroy-view-field-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-destroy-view-field-group.integration-spec.ts.snap new file mode 100644 index 00000000000..c898432b1bc --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-destroy-view-field-group.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Field Group Resolver - Failing Destroy Operation should fail to destroy view field group when non-existent view field group 1`] = ` +{ + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": Any, + "userFriendlyMessage": "A view field group error occurred.", + }, + "message": "View field group to destroy not found", +} +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-update-view-field-group.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-update-view-field-group.integration-spec.ts.snap new file mode 100644 index 00000000000..c4a0ec0483b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/__snapshots__/failing-update-view-field-group.integration-spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`View Field Group Resolver - Failing Update Operation should fail to update view field group when non-existent view field group 1`] = ` +{ + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": Any, + "userFriendlyMessage": "A view field group error occurred.", + }, + "message": "View field group to update not found", +} +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-create-many-view-field-groups.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-create-many-view-field-groups.integration-spec.ts new file mode 100644 index 00000000000..56365aa1d61 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-create-many-view-field-groups.integration-spec.ts @@ -0,0 +1,94 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createManyCoreViewFieldGroups } from 'test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { v4 as uuidv4 } from 'uuid'; + +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; + +describe('View Field Group Resolver - Failing Create Many Operations', () => { + let testSetup: { + testViewId: string; + testObjectMetadataId: string; + }; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'myFieldGroupTestObject', + namePlural: 'myFieldGroupTestObjects', + labelSingular: 'My Field Group Test Object', + labelPlural: 'My Field Group Test Objects', + icon: 'Icon123', + }, + }); + + const { + data: { + createCoreView: { id: testViewId }, + }, + } = await createOneCoreView({ + input: { + icon: 'icon123', + objectMetadataId, + name: 'TestViewForFieldGroups', + }, + expectToFail: false, + }); + + testSetup = { + testViewId, + testObjectMetadataId: objectMetadataId, + }; + }); + + afterAll(async () => { + await updateOneObjectMetadata({ + input: { + idToUpdate: testSetup.testObjectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: testSetup.testObjectMetadataId }, + }); + }); + + it('should fail when some inputs reference non-existent views', async () => { + const invalidViewId = uuidv4(); + + const inputs: CreateViewFieldGroupInput[] = [ + { + name: 'Valid Group', + viewId: testSetup.testViewId, + position: 0, + isVisible: true, + }, + { + name: 'Invalid Group', + viewId: invalidViewId, + position: 1, + isVisible: true, + }, + ]; + + const { errors } = await createManyCoreViewFieldGroups({ + inputs, + expectToFail: true, + }); + + expectOneNotInternalServerErrorSnapshot({ + errors, + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-create-view-field-group.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-create-view-field-group.integration-spec.ts new file mode 100644 index 00000000000..4190c5df2f3 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-create-view-field-group.integration-spec.ts @@ -0,0 +1,97 @@ +import { faker } from '@faker-js/faker'; +import { + cleanupViewFieldGroupTest, + setupViewFieldGroupTest, + type ViewFieldGroupTestSetup, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-group-test.util'; +import { createOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/create-one-core-view-field-group.util'; +import { deleteOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util'; +import { destroyOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; + +const normalizeErrorMessage = (error: any) => { + const UUID_REGEX = + /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; + + return { + ...error, + message: error.message?.replace(UUID_REGEX, '{{UUID}}') || error.message, + }; +}; + +describe('View Field Group Resolver - Failing Create Operation', () => { + let testSetup: ViewFieldGroupTestSetup; + let createdViewFieldGroupIds: string[] = []; + + beforeAll(async () => { + testSetup = await setupViewFieldGroupTest(); + }); + + afterEach(async () => { + for (const viewFieldGroupId of createdViewFieldGroupIds) { + await deleteOneCoreViewFieldGroup({ + input: { + id: viewFieldGroupId, + }, + expectToFail: false, + }); + + await destroyOneCoreViewFieldGroup({ + input: { + id: viewFieldGroupId, + }, + expectToFail: false, + }); + } + }); + + afterAll(async () => { + await cleanupViewFieldGroupTest(testSetup.testObjectMetadataId); + }); + + type CreateViewFieldGroupTestCase = (testSetup: ViewFieldGroupTestSetup) => { + input: CreateViewFieldGroupInput; + }; + + const createViewFieldGroupTestCases: EachTestingContext[] = + [ + { + title: 'non-existent view', + context: () => ({ + input: { + name: 'Test Group', + viewId: faker.string.uuid(), + }, + }), + }, + ]; + + it.each(eachTestingContextFilter(createViewFieldGroupTestCases))( + 'should fail to create view field group when $title', + async ({ context }) => { + const { input } = context(testSetup); + const response = await createOneCoreViewFieldGroup({ + input, + expectToFail: true, + }); + + expect(response.errors).toBeDefined(); + expect(response.errors.length).toBe(1); + const [firstError] = response.errors; + + expect(firstError.extensions.code).not.toBe('INTERNAL_SERVER_ERROR'); + + const normalizedError = normalizeErrorMessage(firstError); + + expect(normalizedError).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(normalizedError), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-delete-view-field-group.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-delete-view-field-group.integration-spec.ts new file mode 100644 index 00000000000..48e02c27b00 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-delete-view-field-group.integration-spec.ts @@ -0,0 +1,62 @@ +import { + cleanupViewFieldGroupTest, + setupViewFieldGroupTest, + type ViewFieldGroupTestSetup, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-group-test.util'; +import { deleteOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input'; + +const TEST_NOT_EXISTING_VIEW_FIELD_GROUP_ID = + '20202020-0000-4000-8000-000000000002'; + +describe('View Field Group Resolver - Failing Delete Operation', () => { + let testSetup: ViewFieldGroupTestSetup; + + beforeAll(async () => { + testSetup = await setupViewFieldGroupTest(); + }); + + afterAll(async () => { + await cleanupViewFieldGroupTest(testSetup.testObjectMetadataId); + }); + + type DeleteViewFieldGroupTestCase = { + input: DeleteViewFieldGroupInput; + }; + + const deleteViewFieldGroupTestCases: EachTestingContext[] = + [ + { + title: 'non-existent view field group', + context: { + input: { + id: TEST_NOT_EXISTING_VIEW_FIELD_GROUP_ID, + }, + }, + }, + ]; + + it.each(eachTestingContextFilter(deleteViewFieldGroupTestCases))( + 'should fail to delete view field group when $title', + async ({ context }) => { + const response = await deleteOneCoreViewFieldGroup({ + input: context.input, + expectToFail: true, + }); + + expect(response.errors).toBeDefined(); + expect(response.errors.length).toBe(1); + const [firstError] = response.errors; + + expect(firstError).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(firstError), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-destroy-view-field-group.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-destroy-view-field-group.integration-spec.ts new file mode 100644 index 00000000000..bca86bd70a0 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-destroy-view-field-group.integration-spec.ts @@ -0,0 +1,62 @@ +import { + cleanupViewFieldGroupTest, + setupViewFieldGroupTest, + type ViewFieldGroupTestSetup, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-group-test.util'; +import { destroyOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input'; + +const TEST_NOT_EXISTING_VIEW_FIELD_GROUP_ID = + '20202020-0000-4000-8000-000000000002'; + +describe('View Field Group Resolver - Failing Destroy Operation', () => { + let testSetup: ViewFieldGroupTestSetup; + + beforeAll(async () => { + testSetup = await setupViewFieldGroupTest(); + }); + + afterAll(async () => { + await cleanupViewFieldGroupTest(testSetup.testObjectMetadataId); + }); + + type DestroyViewFieldGroupTestCase = { + input: DestroyViewFieldGroupInput; + }; + + const destroyViewFieldGroupTestCases: EachTestingContext[] = + [ + { + title: 'non-existent view field group', + context: { + input: { + id: TEST_NOT_EXISTING_VIEW_FIELD_GROUP_ID, + }, + }, + }, + ]; + + it.each(eachTestingContextFilter(destroyViewFieldGroupTestCases))( + 'should fail to destroy view field group when $title', + async ({ context }) => { + const response = await destroyOneCoreViewFieldGroup({ + input: context.input, + expectToFail: true, + }); + + expect(response.errors).toBeDefined(); + expect(response.errors.length).toBe(1); + const [firstError] = response.errors; + + expect(firstError).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(firstError), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-update-view-field-group.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-update-view-field-group.integration-spec.ts new file mode 100644 index 00000000000..ad23481dac9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/failing-update-view-field-group.integration-spec.ts @@ -0,0 +1,65 @@ +import { + cleanupViewFieldGroupTest, + setupViewFieldGroupTest, + type ViewFieldGroupTestSetup, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-group-test.util'; +import { updateOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/update-one-core-view-field-group.util'; +import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; + +import { type UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input'; + +const TEST_NOT_EXISTING_VIEW_FIELD_GROUP_ID = + '20202020-0000-4000-8000-000000000002'; + +describe('View Field Group Resolver - Failing Update Operation', () => { + let testSetup: ViewFieldGroupTestSetup; + + beforeAll(async () => { + testSetup = await setupViewFieldGroupTest(); + }); + + afterAll(async () => { + await cleanupViewFieldGroupTest(testSetup.testObjectMetadataId); + }); + + type UpdateViewFieldGroupTestCase = { + input: UpdateViewFieldGroupInput; + }; + + const updateViewFieldGroupTestCases: EachTestingContext[] = + [ + { + title: 'non-existent view field group', + context: { + input: { + id: TEST_NOT_EXISTING_VIEW_FIELD_GROUP_ID, + update: { + name: 'Updated Name', + }, + }, + }, + }, + ]; + + it.each(eachTestingContextFilter(updateViewFieldGroupTestCases))( + 'should fail to update view field group when $title', + async ({ context }) => { + const response = await updateOneCoreViewFieldGroup({ + input: context.input, + expectToFail: true, + }); + + expect(response.errors).toBeDefined(); + expect(response.errors.length).toBe(1); + const [firstError] = response.errors; + + expect(firstError).toMatchSnapshot( + extractRecordIdsAndDatesAsExpectAny(firstError), + ); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/successful-create-many-view-field-groups.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/successful-create-many-view-field-groups.integration-spec.ts new file mode 100644 index 00000000000..8b38820103e --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/successful-create-many-view-field-groups.integration-spec.ts @@ -0,0 +1,188 @@ +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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createManyCoreViewFieldGroups } from 'test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups.util'; +import { deleteOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util'; +import { destroyOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { isDefined } from 'twenty-shared/utils'; + +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; + +describe('View Field Group Resolver - Successful Create Many Operations', () => { + let testSetup: { + testViewId: string; + testObjectMetadataId: string; + }; + let createdViewFieldGroupIds: string[] = []; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: 'myFieldGroupTestObject', + namePlural: 'myFieldGroupTestObjects', + labelSingular: 'My Field Group Test Object', + labelPlural: 'My Field Group Test Objects', + icon: 'Icon123', + }, + }); + + const { + data: { + createCoreView: { id: testViewId }, + }, + } = await createOneCoreView({ + input: { + icon: 'icon123', + objectMetadataId, + name: 'TestViewForFieldGroups', + }, + expectToFail: false, + }); + + testSetup = { + testViewId, + testObjectMetadataId: objectMetadataId, + }; + }); + + afterAll(async () => { + await updateOneObjectMetadata({ + input: { + idToUpdate: testSetup.testObjectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: testSetup.testObjectMetadataId }, + }); + }); + + afterEach(async () => { + for (const viewFieldGroupId of createdViewFieldGroupIds) { + if (isDefined(viewFieldGroupId)) { + const { + data: { deleteCoreViewFieldGroup }, + } = await deleteOneCoreViewFieldGroup({ + expectToFail: false, + input: { + id: viewFieldGroupId, + }, + }); + + expect(deleteCoreViewFieldGroup.deletedAt).not.toBeNull(); + await destroyOneCoreViewFieldGroup({ + expectToFail: false, + input: { + id: viewFieldGroupId, + }, + }); + } + } + createdViewFieldGroupIds = []; + }); + + it('should successfully create multiple view field groups in batch', async () => { + const inputs: CreateViewFieldGroupInput[] = [ + { + name: 'First Group', + viewId: testSetup.testViewId, + position: 0, + isVisible: true, + }, + { + name: 'Second Group', + viewId: testSetup.testViewId, + position: 1, + isVisible: false, + }, + { + name: 'Third Group', + viewId: testSetup.testViewId, + position: 2, + isVisible: true, + }, + ]; + + const { + data: { createManyCoreViewFieldGroups: createdViewFieldGroups }, + errors, + } = await createManyCoreViewFieldGroups({ + inputs, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + expect(createdViewFieldGroups).toBeDefined(); + expect(createdViewFieldGroups).toHaveLength(3); + + createdViewFieldGroups.forEach((viewFieldGroup, index) => { + expect(viewFieldGroup).toMatchObject({ + name: inputs[index].name, + viewId: testSetup.testViewId, + position: inputs[index].position, + isVisible: inputs[index].isVisible, + }); + + createdViewFieldGroupIds.push(viewFieldGroup.id); + }); + }); + + it('should successfully create single view field group using batch endpoint', async () => { + const inputs: CreateViewFieldGroupInput[] = [ + { + name: 'Single Batch Group', + viewId: testSetup.testViewId, + position: 5, + isVisible: true, + }, + ]; + + const { + data: { createManyCoreViewFieldGroups: createdViewFieldGroups }, + errors, + } = await createManyCoreViewFieldGroups({ + inputs, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + expect(createdViewFieldGroups).toBeDefined(); + expect(createdViewFieldGroups).toHaveLength(1); + + const viewFieldGroup = createdViewFieldGroups[0]; + + expect(viewFieldGroup).toMatchObject({ + name: 'Single Batch Group', + viewId: testSetup.testViewId, + position: 5, + isVisible: true, + }); + + createdViewFieldGroupIds.push(viewFieldGroup.id); + }); + + it('should return empty array when creating zero view field groups', async () => { + const inputs: CreateViewFieldGroupInput[] = []; + + const { + data: { createManyCoreViewFieldGroups: createdViewFieldGroups }, + errors, + } = await createManyCoreViewFieldGroups({ + inputs, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + expect(createdViewFieldGroups).toBeDefined(); + expect(createdViewFieldGroups).toHaveLength(0); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/successful-create-view-field-group.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/successful-create-view-field-group.integration-spec.ts new file mode 100644 index 00000000000..aefcb1708d6 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-group/successful-create-view-field-group.integration-spec.ts @@ -0,0 +1,127 @@ +import { + type ViewFieldGroupTestSetup, + cleanupViewFieldGroupTest, + setupViewFieldGroupTest, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-group-test.util'; +import { createOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/create-one-core-view-field-group.util'; +import { deleteOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util'; +import { destroyOneCoreViewFieldGroup } from 'test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util'; +import { assertViewFieldGroupStructure } from 'test/integration/utils/view-test.util'; +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; +import { isDefined } from 'twenty-shared/utils'; + +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +type TestContext = { + viewFieldGroupInput: ( + testSetup: ViewFieldGroupTestSetup, + ) => CreateViewFieldGroupInput; + expected: Partial; +}; + +describe('View Field Group Resolver - Successful Create Operations', () => { + let testSetup: ViewFieldGroupTestSetup; + let createdViewFieldGroupId: string | undefined; + + beforeAll(async () => { + testSetup = await setupViewFieldGroupTest(); + }); + + afterAll(async () => { + await cleanupViewFieldGroupTest(testSetup.testObjectMetadataId); + }); + + afterEach(async () => { + if (isDefined(createdViewFieldGroupId)) { + const { + data: { deleteCoreViewFieldGroup }, + } = await deleteOneCoreViewFieldGroup({ + expectToFail: false, + input: { + id: createdViewFieldGroupId, + }, + }); + + expect(deleteCoreViewFieldGroup.deletedAt).not.toBeNull(); + await destroyOneCoreViewFieldGroup({ + expectToFail: false, + input: { + id: createdViewFieldGroupId, + }, + }); + createdViewFieldGroupId = undefined; + } + }); + + const successfulTestCases: EachTestingContext[] = [ + { + title: 'visible group with position', + context: { + viewFieldGroupInput: (testSetup) => ({ + name: 'Test Group', + viewId: testSetup.testViewId, + position: 1, + isVisible: true, + }), + expected: { + name: 'Test Group', + position: 1, + isVisible: true, + }, + }, + }, + { + title: 'hidden group with position', + context: { + viewFieldGroupInput: (testSetup) => ({ + name: 'Hidden Group', + viewId: testSetup.testViewId, + position: 2, + isVisible: false, + }), + expected: { + name: 'Hidden Group', + position: 2, + isVisible: false, + }, + }, + }, + { + title: 'group with minimum required properties', + context: { + viewFieldGroupInput: (testSetup) => ({ + name: 'Minimal Group', + viewId: testSetup.testViewId, + }), + expected: { + name: 'Minimal Group', + position: 0, + isVisible: true, + }, + }, + }, + ]; + + test.each(eachTestingContextFilter(successfulTestCases))( + 'Create $title', + async ({ context: { viewFieldGroupInput, expected } }) => { + const response = await createOneCoreViewFieldGroup({ + input: viewFieldGroupInput(testSetup), + expectToFail: false, + }); + + expect(response.errors).toBeUndefined(); + expect(response.data.createCoreViewFieldGroup).toBeDefined(); + createdViewFieldGroupId = response.data.createCoreViewFieldGroup.id; + + assertViewFieldGroupStructure(response.data.createCoreViewFieldGroup, { + viewId: testSetup.testViewId, + ...expected, + }); + }, + ); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields.integration-spec.ts.snap similarity index 68% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields.integration-spec.ts.snap index 3cadbffdcbc..18580c6d3bb 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-many-view-fields.integration-spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`View Field Resolver - Failing Create Many Operations - v2 should accumulate multiple validation errors when some inputs are invalid 1`] = ` +exports[`View Field Resolver - Failing Create Many Operations should accumulate multiple validation errors when some inputs are invalid 1`] = ` { "extensions": { "code": "NOT_FOUND", diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field.integration-spec.ts.snap similarity index 79% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field.integration-spec.ts.snap index 1c4e7a31277..999b7e4e6bf 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-create-view-field.integration-spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`View Field Resolver - Failing Create Operation - v2 Should fail to create a conflicting view field on view and field metadata 1`] = ` +exports[`View Field Resolver - Failing Create Operation Should fail to create a conflicting view field on view and field metadata 1`] = ` { "extensions": { "code": "METADATA_VALIDATION_FAILED", @@ -37,7 +37,7 @@ exports[`View Field Resolver - Failing Create Operation - v2 Should fail to crea } `; -exports[`View Field Resolver - Failing Create Operation - v2 should fail to create view field when non-existent field metadata 1`] = ` +exports[`View Field Resolver - Failing Create Operation should fail to create view field when non-existent field metadata 1`] = ` { "extensions": { "code": "NOT_FOUND", @@ -49,7 +49,7 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea } `; -exports[`View Field Resolver - Failing Create Operation - v2 should fail to create view field when non-existent view metadata 1`] = ` +exports[`View Field Resolver - Failing Create Operation should fail to create view field when non-existent view metadata 1`] = ` { "extensions": { "code": "NOT_FOUND", diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-delete-view-field-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-delete-view-field.integration-spec.ts.snap similarity index 64% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-delete-view-field-v2.integration-spec.ts.snap rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-delete-view-field.integration-spec.ts.snap index 664b0b1b6c8..9575b17b6be 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-delete-view-field-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-delete-view-field.integration-spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`View Field Resolver - Failing Delete Operation - v2 should fail to delete view field when non-existent view field 1`] = ` +exports[`View Field Resolver - Failing Delete Operation should fail to delete view field when non-existent view field 1`] = ` { "extensions": { "code": "NOT_FOUND", diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-destroy-view-field-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-destroy-view-field.integration-spec.ts.snap similarity index 64% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-destroy-view-field-v2.integration-spec.ts.snap rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-destroy-view-field.integration-spec.ts.snap index 96d45bf50a4..fbb8acece30 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-destroy-view-field-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-destroy-view-field.integration-spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`View Field Resolver - Failing Destroy Operation - v2 should fail to destroy view field when non-existent view field 1`] = ` +exports[`View Field Resolver - Failing Destroy Operation should fail to destroy view field when non-existent view field 1`] = ` { "extensions": { "code": "NOT_FOUND", diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-update-view-field-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-update-view-field.integration-spec.ts.snap similarity index 64% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-update-view-field-v2.integration-spec.ts.snap rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-update-view-field.integration-spec.ts.snap index 8a0c4b765c1..1b21f2ac83d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-update-view-field-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/failing-update-view-field.integration-spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`View Field Resolver - Failing Update Operation - v2 should fail to update view field when non-existent view field 1`] = ` +exports[`View Field Resolver - Failing Update Operation should fail to update view field when non-existent view field 1`] = ` { "extensions": { "code": "NOT_FOUND", diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-many-view-fields-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-many-view-fields.integration-spec.ts similarity index 93% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-many-view-fields-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-many-view-fields.integration-spec.ts index 382ecf4eeab..59884083b35 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-many-view-fields-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-many-view-fields.integration-spec.ts @@ -1,16 +1,16 @@ -import { createManyCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/create-many-core-view-fields.util'; -import { v4 as uuidv4 } from 'uuid'; import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; 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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createManyCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/create-many-core-view-fields.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; import { FieldMetadataType } from 'twenty-shared/types'; +import { v4 as uuidv4 } from 'uuid'; import { type CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input'; -describe('View Field Resolver - Failing Create Many Operations - v2', () => { +describe('View Field Resolver - Failing Create Many Operations', () => { let testSetup: { testViewId: string; testObjectMetadataId: string; @@ -26,10 +26,10 @@ describe('View Field Resolver - Failing Create Many Operations - v2', () => { } = await createOneObjectMetadata({ expectToFail: false, input: { - nameSingular: 'myFieldTestObjectV2', - namePlural: 'myFieldTestObjectsV2', - labelSingular: 'My Field Test Object v2', - labelPlural: 'My Field Test Objects v2', + nameSingular: 'myFieldTestObject', + namePlural: 'myFieldTestObjects', + labelSingular: 'My Field Test Object', + labelPlural: 'My Field Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-view-field-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-view-field.integration-spec.ts similarity index 94% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-view-field-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-view-field.integration-spec.ts index ed5f42eec16..52188cd7ea0 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-view-field-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-create-view-field.integration-spec.ts @@ -1,9 +1,9 @@ import { faker } from '@faker-js/faker'; import { - cleanupViewFieldTestV2, - setupViewFieldTestV2, + cleanupViewFieldTest, + setupViewFieldTest, type ViewFieldTestSetup, -} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util'; +} from 'test/integration/graphql/suites/view/utils/setup-view-field-test.util'; import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util'; import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util'; import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util'; @@ -26,12 +26,12 @@ const normalizeErrorMessage = (error: any) => { }; }; -describe('View Field Resolver - Failing Create Operation - v2', () => { +describe('View Field Resolver - Failing Create Operation', () => { let testSetup: ViewFieldTestSetup; let createdFlatViewFieldIds: string[] = []; beforeAll(async () => { - testSetup = await setupViewFieldTestV2(); + testSetup = await setupViewFieldTest(); }); afterEach(async () => { @@ -53,7 +53,7 @@ describe('View Field Resolver - Failing Create Operation - v2', () => { }); afterAll(async () => { - await cleanupViewFieldTestV2(testSetup.testObjectMetadataId); + await cleanupViewFieldTest(testSetup.testObjectMetadataId); }); type CreateViewFieldTestCase = (testSetup: ViewFieldTestSetup) => { diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-delete-view-field-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-delete-view-field.integration-spec.ts similarity index 86% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-delete-view-field-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-delete-view-field.integration-spec.ts index 13313161df5..acfc8174f5e 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-delete-view-field-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-delete-view-field.integration-spec.ts @@ -1,8 +1,8 @@ import { - cleanupViewFieldTestV2, - setupViewFieldTestV2, + cleanupViewFieldTest, + setupViewFieldTest, type ViewFieldTestSetup, -} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util'; +} from 'test/integration/graphql/suites/view/utils/setup-view-field-test.util'; import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; import { @@ -14,15 +14,15 @@ import { type DeleteViewFieldInput } from 'src/engine/metadata-modules/view-fiel const TEST_NOT_EXISTING_VIEW_FIELD_ID = '20202020-0000-4000-8000-000000000001'; -describe('View Field Resolver - Failing Delete Operation - v2', () => { +describe('View Field Resolver - Failing Delete Operation', () => { let testSetup: ViewFieldTestSetup; beforeAll(async () => { - testSetup = await setupViewFieldTestV2(); + testSetup = await setupViewFieldTest(); }); afterAll(async () => { - await cleanupViewFieldTestV2(testSetup.testObjectMetadataId); + await cleanupViewFieldTest(testSetup.testObjectMetadataId); }); type DeleteViewFieldTestCase = { diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-destroy-view-field-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-destroy-view-field.integration-spec.ts similarity index 86% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-destroy-view-field-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-destroy-view-field.integration-spec.ts index bf65d7500cb..35b39df5276 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-destroy-view-field-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-destroy-view-field.integration-spec.ts @@ -1,8 +1,8 @@ import { - cleanupViewFieldTestV2, - setupViewFieldTestV2, + cleanupViewFieldTest, + setupViewFieldTest, type ViewFieldTestSetup, -} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util'; +} from 'test/integration/graphql/suites/view/utils/setup-view-field-test.util'; import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; import { @@ -14,15 +14,15 @@ import { type DestroyViewFieldInput } from 'src/engine/metadata-modules/view-fie const TEST_NOT_EXISTING_VIEW_FIELD_ID = '20202020-0000-4000-8000-000000000001'; -describe('View Field Resolver - Failing Destroy Operation - v2', () => { +describe('View Field Resolver - Failing Destroy Operation', () => { let testSetup: ViewFieldTestSetup; beforeAll(async () => { - testSetup = await setupViewFieldTestV2(); + testSetup = await setupViewFieldTest(); }); afterAll(async () => { - await cleanupViewFieldTestV2(testSetup.testObjectMetadataId); + await cleanupViewFieldTest(testSetup.testObjectMetadataId); }); type DestroyViewFieldTestCase = { diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-update-view-field-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-update-view-field.integration-spec.ts similarity index 86% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-update-view-field-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-update-view-field.integration-spec.ts index 7814c7ccf0a..4336ec1186b 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-update-view-field-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/failing-update-view-field.integration-spec.ts @@ -1,8 +1,8 @@ import { - cleanupViewFieldTestV2, - setupViewFieldTestV2, + cleanupViewFieldTest, + setupViewFieldTest, type ViewFieldTestSetup, -} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util'; +} from 'test/integration/graphql/suites/view/utils/setup-view-field-test.util'; import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util'; import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any'; import { @@ -14,15 +14,15 @@ import { type UpdateViewFieldInput } from 'src/engine/metadata-modules/view-fiel const TEST_NOT_EXISTING_VIEW_FIELD_ID = '20202020-0000-4000-8000-000000000001'; -describe('View Field Resolver - Failing Update Operation - v2', () => { +describe('View Field Resolver - Failing Update Operation', () => { let testSetup: ViewFieldTestSetup; beforeAll(async () => { - testSetup = await setupViewFieldTestV2(); + testSetup = await setupViewFieldTest(); }); afterAll(async () => { - await cleanupViewFieldTestV2(testSetup.testObjectMetadataId); + await cleanupViewFieldTest(testSetup.testObjectMetadataId); }); type UpdateViewFieldTestCase = { diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/object-identifier-update-side-effect-on-view-field.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/object-identifier-update-side-effect-on-view-field.integration-spec.ts index 23b0c4a8ec4..78a962d92b2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/object-identifier-update-side-effect-on-view-field.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/object-identifier-update-side-effect-on-view-field.integration-spec.ts @@ -1,8 +1,8 @@ import { type ViewFieldTestSetup, - cleanupViewFieldTestV2, - setupViewFieldTestV2, -} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util'; + cleanupViewFieldTest, + setupViewFieldTest, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-test.util'; import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; @@ -22,7 +22,7 @@ describe('View Field Resolver - Successful object metadata identifier update sid beforeAll(async () => { const { testFieldMetadataId, testObjectMetadataId, testViewId } = - await setupViewFieldTestV2(); + await setupViewFieldTest(); await updateOneObjectMetadata({ input: { @@ -56,7 +56,7 @@ describe('View Field Resolver - Successful object metadata identifier update sid }); afterAll(async () => { - await cleanupViewFieldTestV2(testSetup.testObjectMetadataId); + await cleanupViewFieldTest(testSetup.testObjectMetadataId); }); it('should create a view field on label identifier object metadata update if it does not exist on view', async () => { diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-many-view-fields-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-many-view-fields.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-many-view-fields-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-many-view-fields.integration-spec.ts index 68d5051a0c6..7e5804d2baf 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-many-view-fields-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-many-view-fields.integration-spec.ts @@ -1,17 +1,17 @@ -import { createManyCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/create-many-core-view-fields.util'; -import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util'; -import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util'; -import { isDefined } from 'twenty-shared/utils'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; 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 { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createManyCoreViewFields } from 'test/integration/metadata/suites/view-field/utils/create-many-core-view-fields.util'; +import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util'; +import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input'; -describe('View Field Resolver - Successful Create Many Operations - v2', () => { +describe('View Field Resolver - Successful Create Many Operations', () => { let testSetup: { testViewId: string; testObjectMetadataId: string; @@ -29,10 +29,10 @@ describe('View Field Resolver - Successful Create Many Operations - v2', () => { } = await createOneObjectMetadata({ expectToFail: false, input: { - nameSingular: 'myFieldTestObjectV2', - namePlural: 'myFieldTestObjectsV2', - labelSingular: 'My Field Test Object v2', - labelPlural: 'My Field Test Objects v2', + nameSingular: 'myFieldTestObject', + namePlural: 'myFieldTestObjects', + labelSingular: 'My Field Test Object', + labelPlural: 'My Field Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-view-field-v2.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-view-field.integration-spec.ts similarity index 94% rename from packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-view-field-v2.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-view-field.integration-spec.ts index 71cbadcd04a..3d411452cbd 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-view-field-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/successful-create-view-field.integration-spec.ts @@ -1,8 +1,8 @@ import { type ViewFieldTestSetup, - cleanupViewFieldTestV2, - setupViewFieldTestV2, -} from 'test/integration/graphql/suites/view/utils/setup-view-field-test-v2.util'; + cleanupViewFieldTest, + setupViewFieldTest, +} from 'test/integration/graphql/suites/view/utils/setup-view-field-test.util'; import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util'; import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util'; import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util'; @@ -21,16 +21,16 @@ type TestContext = { expected: Partial; }; -describe('View Field Resolver - Successful Create Operations - v2', () => { +describe('View Field Resolver - Successful Create Operations', () => { let testSetup: ViewFieldTestSetup; let createdViewFieldId: string | undefined; beforeAll(async () => { - testSetup = await setupViewFieldTestV2(); + testSetup = await setupViewFieldTest(); }); afterAll(async () => { - await cleanupViewFieldTestV2(testSetup.testObjectMetadataId); + await cleanupViewFieldTest(testSetup.testObjectMetadataId); }); afterEach(async () => { diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-core-view-field-group-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-core-view-field-group-query-factory.util.ts new file mode 100644 index 00000000000..88995fa945a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-core-view-field-group-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_FIELD_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; + +export const createCoreViewFieldGroupQueryFactory = ({ + gqlFields = VIEW_FIELD_GROUP_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation CreateCoreViewFieldGroup($input: CreateViewFieldGroupInput!) { + createCoreViewFieldGroup(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups-query-factory.util.ts new file mode 100644 index 00000000000..32ebf5861bc --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups-query-factory.util.ts @@ -0,0 +1,23 @@ +import gql from 'graphql-tag'; +import { VIEW_FIELD_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; + +import { type CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; + +export const createManyCoreViewFieldGroupsQueryFactory = ({ + gqlFields = VIEW_FIELD_GROUP_GQL_FIELDS, + inputs, +}: { + gqlFields?: string; + inputs: CreateViewFieldGroupInput[]; +}) => ({ + query: gql` + mutation CreateManyCoreViewFieldGroups($inputs: [CreateViewFieldGroupInput!]!) { + createManyCoreViewFieldGroups(inputs: $inputs) { + ${gqlFields} + } + } + `, + variables: { + inputs, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups.util.ts new file mode 100644 index 00000000000..4d8ab7aed78 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups.util.ts @@ -0,0 +1,45 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { createManyCoreViewFieldGroupsQueryFactory } from 'test/integration/metadata/suites/view-field-group/utils/create-many-core-view-field-groups-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +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 CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const createManyCoreViewFieldGroups = async ({ + inputs, + gqlFields, + expectToFail, +}: { + inputs: CreateViewFieldGroupInput[]; + gqlFields?: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + createManyCoreViewFieldGroups: ViewFieldGroupDTO[]; +}> => { + const graphqlOperation = createManyCoreViewFieldGroupsQueryFactory({ + inputs, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'View Field Groups batch creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: + 'View Field Groups batch creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-one-core-view-field-group.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-one-core-view-field-group.util.ts new file mode 100644 index 00000000000..bfcad92adbd --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/create-one-core-view-field-group.util.ts @@ -0,0 +1,40 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { createCoreViewFieldGroupQueryFactory } from 'test/integration/metadata/suites/view-field-group/utils/create-core-view-field-group-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +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 CreateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/create-view-field-group.input'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const createOneCoreViewFieldGroup = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + createCoreViewFieldGroup: ViewFieldGroupDTO; +}> => { + const graphqlOperation = createCoreViewFieldGroupQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Field Group creation should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Field Group creation has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/delete-core-view-field-group-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/delete-core-view-field-group-query-factory.util.ts new file mode 100644 index 00000000000..5d46c2ee381 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/delete-core-view-field-group-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_FIELD_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input'; + +export const deleteCoreViewFieldGroupQueryFactory = ({ + gqlFields = VIEW_FIELD_GROUP_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DeleteCoreViewFieldGroup($input: DeleteViewFieldGroupInput!) { + deleteCoreViewFieldGroup(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util.ts new file mode 100644 index 00000000000..614b6e81b7f --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/delete-one-core-view-field-group.util.ts @@ -0,0 +1,40 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { deleteCoreViewFieldGroupQueryFactory } from 'test/integration/metadata/suites/view-field-group/utils/delete-core-view-field-group-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +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 DeleteViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/delete-view-field-group.input'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const deleteOneCoreViewFieldGroup = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + deleteCoreViewFieldGroup: ViewFieldGroupDTO; +}> => { + const graphqlOperation = deleteCoreViewFieldGroupQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Field Group deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Field Group deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/destroy-core-view-field-group-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/destroy-core-view-field-group-query-factory.util.ts new file mode 100644 index 00000000000..ca6f1ed539a --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/destroy-core-view-field-group-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_FIELD_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input'; + +export const destroyCoreViewFieldGroupQueryFactory = ({ + gqlFields = VIEW_FIELD_GROUP_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation DestroyCoreViewFieldGroup($input: DestroyViewFieldGroupInput!) { + destroyCoreViewFieldGroup(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util.ts new file mode 100644 index 00000000000..4cc94fbf1bc --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/destroy-one-core-view-field-group.util.ts @@ -0,0 +1,41 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { destroyCoreViewFieldGroupQueryFactory } from 'test/integration/metadata/suites/view-field-group/utils/destroy-core-view-field-group-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +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 DestroyViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/destroy-view-field-group.input'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const destroyOneCoreViewFieldGroup = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + destroyCoreViewFieldGroup: ViewFieldGroupDTO; +}> => { + const graphqlOperation = destroyCoreViewFieldGroupQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: + 'View Field Group destruction should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Field Group destruction has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups-query-factory.util.ts new file mode 100644 index 00000000000..1ad234846d7 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_FIELD_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; + +export const findCoreViewFieldGroupsQueryFactory = ({ + gqlFields = VIEW_FIELD_GROUP_GQL_FIELDS, + viewId, +}: { + gqlFields?: string; + viewId: string; +}) => ({ + query: gql` + query GetCoreViewFieldGroups($viewId: String!) { + getCoreViewFieldGroups(viewId: $viewId) { + ${gqlFields} + } + } + `, + variables: { + viewId, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups.util.ts new file mode 100644 index 00000000000..959aecb9af0 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups.util.ts @@ -0,0 +1,42 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { findCoreViewFieldGroupsQueryFactory } from 'test/integration/metadata/suites/view-field-group/utils/find-core-view-field-groups-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +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 ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const findCoreViewFieldGroups = async ({ + viewId, + gqlFields, + expectToFail, +}: { + viewId: string; + gqlFields?: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + getCoreViewFieldGroups: ViewFieldGroupDTO[]; +}> => { + const graphqlOperation = findCoreViewFieldGroupsQueryFactory({ + viewId, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Field Group search should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Field Group search has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/update-core-view-field-group-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/update-core-view-field-group-query-factory.util.ts new file mode 100644 index 00000000000..bbb595c0086 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/update-core-view-field-group-query-factory.util.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; +import { VIEW_FIELD_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; + +import { type UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input'; + +export const updateCoreViewFieldGroupQueryFactory = ({ + gqlFields = VIEW_FIELD_GROUP_GQL_FIELDS, + input, +}: PerformMetadataQueryParams) => ({ + query: gql` + mutation UpdateCoreViewFieldGroup($input: UpdateViewFieldGroupInput!) { + updateCoreViewFieldGroup(input: $input) { + ${gqlFields} + } + } + `, + variables: { + input, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/update-one-core-view-field-group.util.ts b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/update-one-core-view-field-group.util.ts new file mode 100644 index 00000000000..1a51db50518 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view-field-group/utils/update-one-core-view-field-group.util.ts @@ -0,0 +1,40 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateCoreViewFieldGroupQueryFactory } from 'test/integration/metadata/suites/view-field-group/utils/update-core-view-field-group-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; +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 UpdateViewFieldGroupInput } from 'src/engine/metadata-modules/view-field-group/dtos/inputs/update-view-field-group.input'; +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; + +export const updateOneCoreViewFieldGroup = async ({ + input, + gqlFields, + expectToFail, +}: PerformMetadataQueryParams): CommonResponseBody<{ + updateCoreViewFieldGroup: ViewFieldGroupDTO; +}> => { + const graphqlOperation = updateCoreViewFieldGroupQueryFactory({ + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View Field Group update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View Field Group update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/utils/view-test.util.ts b/packages/twenty-server/test/integration/utils/view-test.util.ts index e6fa0caff96..1f89abe93d3 100644 --- a/packages/twenty-server/test/integration/utils/view-test.util.ts +++ b/packages/twenty-server/test/integration/utils/view-test.util.ts @@ -1,3 +1,4 @@ +import { type ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto'; import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator'; @@ -23,6 +24,22 @@ export const assertViewStructure = ( } }; +export const assertViewFieldGroupStructure = ( + viewFieldGroup: ViewFieldGroupDTO, + expectedFields?: Partial, +) => { + expect(viewFieldGroup).toBeDefined(); + expect(viewFieldGroup.id).toBeDefined(); + expect(viewFieldGroup.name).toBeDefined(); + expect(viewFieldGroup.viewId).toBeDefined(); + expect(typeof viewFieldGroup.position).toBe('number'); + expect(typeof viewFieldGroup.isVisible).toBe('boolean'); + + if (expectedFields) { + expect(viewFieldGroup).toMatchObject(expectedFields); + } +}; + export const assertViewFieldStructure = ( viewField: ViewFieldEntity, expectedFields?: Partial, diff --git a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts b/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts index 2240d850c8c..9e0cafafcf6 100644 --- a/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts +++ b/packages/twenty-shared/src/metadata/all-metadata-name.constant.ts @@ -3,6 +3,7 @@ export const ALL_METADATA_NAME = { objectMetadata: 'objectMetadata', view: 'view', viewField: 'viewField', + viewFieldGroup: 'viewFieldGroup', viewGroup: 'viewGroup', rowLevelPermissionPredicate: 'rowLevelPermissionPredicate', rowLevelPermissionPredicateGroup: 'rowLevelPermissionPredicateGroup',