Introducing view field group syncable entity (#17867)

## Context
Introduces a new viewFieldGroup entity that allows grouping view fields
into sections (e.g. "General", "Additional", "Other") within a view.

The page layout fields widget needs a way to organize fields into
sections. Today, views have no concept of field grouping. This PR
introduces the viewFieldGroup entity which sits between a view and its
viewFields, enabling section-based organization.

<img width="401" height="724" alt="Layout - V2 (customize visibility)"
src="https://github.com/user-attachments/assets/6376e2ab-44db-42bf-9d2c-758f56f6b548"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2026-02-11 22:02:58 +01:00
committed by GitHub
parent ed66fbd71b
commit 6aca1dd013
126 changed files with 3593 additions and 115 deletions
@@ -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<CoreViewFieldGroup>;
viewFields: Array<CoreViewField>;
viewFilterGroups: Array<CoreViewFilterGroup>;
viewFilters: Array<CoreViewFilter>;
@@ -851,6 +853,20 @@ export type CoreViewField = {
workspaceId: Scalars['UUID'];
};
export type CoreViewFieldGroup = {
__typename?: 'CoreViewFieldGroup';
createdAt: Scalars['DateTime'];
deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID'];
isVisible: Scalars['Boolean'];
name: Scalars['String'];
position: Scalars['Float'];
updatedAt: Scalars['DateTime'];
viewFields: Array<CoreViewField>;
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<Scalars['UUID']>;
isVisible?: InputMaybe<Scalars['Boolean']>;
name: Scalars['String'];
position?: InputMaybe<Scalars['Float']>;
viewId: Scalars['UUID'];
};
export type CreateViewFieldInput = {
aggregateOperation?: InputMaybe<AggregateOperations>;
fieldMetadataId: Scalars['UUID'];
@@ -1087,6 +1111,7 @@ export type CreateViewFieldInput = {
isVisible?: InputMaybe<Scalars['Boolean']>;
position?: InputMaybe<Scalars['Float']>;
size?: InputMaybe<Scalars['Float']>;
viewFieldGroupId?: InputMaybe<Scalars['UUID']>;
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<Scalars['UUID']>;
};
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<CoreViewFieldGroup>;
createManyCoreViewFields: Array<CoreViewField>;
createManyCoreViewGroups: Array<CoreViewGroup>;
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<CreateViewFieldGroupInput>;
};
export type MutationCreateManyCoreViewFieldsArgs = {
inputs: Array<CreateViewFieldInput>;
};
@@ -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<CoreView>;
getCoreViewField?: Maybe<CoreViewField>;
getCoreViewFieldGroup?: Maybe<CoreViewFieldGroup>;
getCoreViewFieldGroups: Array<CoreViewFieldGroup>;
getCoreViewFields: Array<CoreViewField>;
getCoreViewFilter?: Maybe<CoreViewFilter>;
getCoreViewFilterGroup?: Maybe<CoreViewFilterGroup>;
@@ -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<Scalars['String']>;
};
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<Scalars['String']>;
isVisible?: InputMaybe<Scalars['Boolean']>;
name?: InputMaybe<Scalars['String']>;
position?: InputMaybe<Scalars['Float']>;
};
export type UpdateViewFieldInput = {
/** The id of the view field to update */
id: Scalars['UUID'];
@@ -4529,6 +4620,7 @@ export type UpdateViewFieldInputUpdates = {
isVisible?: InputMaybe<Scalars['Boolean']>;
position?: InputMaybe<Scalars['Float']>;
size?: InputMaybe<Scalars['Float']>;
viewFieldGroupId?: InputMaybe<Scalars['UUID']>;
};
export type UpdateViewFilterGroupInput = {
@@ -775,6 +775,7 @@ export type CoreView = {
shouldHideEmptyGroups: Scalars['Boolean'];
type: ViewType;
updatedAt: Scalars['DateTime'];
viewFieldGroups: Array<CoreViewFieldGroup>;
viewFields: Array<CoreViewField>;
viewFilterGroups: Array<CoreViewFilterGroup>;
viewFilters: Array<CoreViewFilter>;
@@ -799,6 +800,20 @@ export type CoreViewField = {
workspaceId: Scalars['UUID'];
};
export type CoreViewFieldGroup = {
__typename?: 'CoreViewFieldGroup';
createdAt: Scalars['DateTime'];
deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID'];
isVisible: Scalars['Boolean'];
name: Scalars['String'];
position: Scalars['Float'];
updatedAt: Scalars['DateTime'];
viewFields: Array<CoreViewField>;
viewId: Scalars['UUID'];
workspaceId: Scalars['UUID'];
};
export type CoreViewFilter = {
__typename?: 'CoreViewFilter';
createdAt: Scalars['DateTime'];
@@ -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`,
@@ -0,0 +1,61 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddViewFieldGroup1770818941843 implements MigrationInterface {
name = 'AddViewFieldGroup1770818941843';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`);
}
}
@@ -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<RelationLoaderPayload, RelationDTO | null>;
@@ -41,4 +45,14 @@ export interface IDataloaders {
ObjectMetadataLoaderPayload,
ObjectMetadataDTO | null
>;
viewFieldGroupsByViewIdLoader: DataLoader<
ViewFieldGroupsByViewIdLoaderPayload,
ViewFieldGroupDTO[]
>;
viewFieldsByViewFieldGroupIdLoader: DataLoader<
ViewFieldsByViewFieldGroupIdLoaderPayload,
ViewFieldDTO[]
>;
}
@@ -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<typeof fromFlatViewFieldGroupToViewFieldGroupDto>[]
>(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<typeof fromFlatViewFieldToViewFieldDto>[]
>(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);
});
});
}
}
@@ -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": [],
@@ -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: {
@@ -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,
@@ -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: {
@@ -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,
@@ -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,
@@ -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: {
@@ -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",
]
`;
@@ -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",
@@ -80,6 +80,8 @@ export const recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate = ({
deletedAt: null,
universalIdentifier: viewFieldId,
aggregateOperation: null,
viewFieldGroupId: null,
viewFieldGroupUniversalIdentifier: null,
applicationId: existingFlatObjectMetadata.applicationId,
applicationUniversalIdentifier:
existingFlatObjectMetadata.applicationUniversalIdentifier,
@@ -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'>[];
@@ -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<FlatViewFieldGroupMaps> {
constructor(
@InjectRepository(ViewFieldGroupEntity)
private readonly viewFieldGroupRepository: Repository<ViewFieldGroupEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewFieldGroupMaps> {
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;
}
}
@@ -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<FlatViewFieldGroup>;
@@ -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<ViewFieldGroupEntity>;
@@ -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<AllFlatEntityMaps, 'flatViewMaps'>;
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,
};
};
@@ -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(),
};
};
@@ -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;
};
@@ -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,
});
};
@@ -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,
) ?? [],
};
};
@@ -5,4 +5,5 @@ export const FLAT_VIEW_FIELD_EDITABLE_PROPERTIES = [
'size',
'position',
'aggregateOperation',
'viewFieldGroupId',
] as const satisfies MetadataEntityPropertyName<'viewField'>[];
@@ -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<FieldMetadataEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ViewFieldGroupEntity)
private readonly viewFieldGroupRepository: Repository<ViewFieldGroupEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewFieldMaps> {
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({
@@ -11,31 +11,48 @@ import { type UniversalFlatViewField } from 'src/engine/workspace-manager/worksp
export type FromCreateViewFieldInputToFlatViewFieldToCreateArgs = {
createViewFieldInput: CreateViewFieldInput;
flatApplication: FlatApplication;
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps' | 'flatViewMaps'>;
} & 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,
};
};
@@ -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;
};
@@ -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,
};
};
@@ -87,6 +87,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
applicationUniversalIdentifier: flatApplication.universalIdentifier,
};
@@ -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,
),
@@ -549,6 +549,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
viewFieldUniversalIdentifiers: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
viewGroupUniversalIdentifiers: [],
viewFilterGroupUniversalIdentifiers: [],
@@ -580,6 +581,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
.map<UniversalFlatViewField>((field, index) => ({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
viewUniversalIdentifier,
viewFieldGroupUniversalIdentifier: null,
createdAt,
updatedAt: createdAt,
deletedAt: null,
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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[];
}
@@ -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<ViewFieldGroupEntity>
{
@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<ViewEntity>;
@OneToMany(() => ViewFieldEntity, (viewField) => viewField.viewFieldGroup)
viewFields: Relation<ViewFieldEntity[]>;
}
@@ -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<ViewFieldGroupExceptionCode> {
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',
}
@@ -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<ViewFieldGroupEntity[]> {
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<ViewFieldGroupEntity | null> {
return this.viewFieldGroupService.findById(id, workspace.id);
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async updateCoreViewFieldGroup(
@Args('input') updateViewFieldGroupInput: UpdateViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.updateOne({
updateViewFieldGroupInput,
workspaceId,
});
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async createCoreViewFieldGroup(
@Args('input')
createViewFieldGroupInput: CreateViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.createOne({
createViewFieldGroupInput,
workspaceId,
});
}
@Mutation(() => [ViewFieldGroupDTO])
@UseGuards(NoPermissionGuard)
async createManyCoreViewFieldGroups(
@Args('inputs', { type: () => [CreateViewFieldGroupInput] })
createViewFieldGroupInputs: CreateViewFieldGroupInput[],
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO[]> {
return await this.viewFieldGroupService.createMany({
createViewFieldGroupInputs,
workspaceId,
});
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async deleteCoreViewFieldGroup(
@Args('input') deleteViewFieldGroupInput: DeleteViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
return await this.viewFieldGroupService.deleteOne({
deleteViewFieldGroupInput,
workspaceId,
});
}
@Mutation(() => ViewFieldGroupDTO)
@UseGuards(NoPermissionGuard)
async destroyCoreViewFieldGroup(
@Args('input')
destroyViewFieldGroupInput: DestroyViewFieldGroupInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldGroupDTO> {
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,
});
}
}
@@ -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<ViewFieldGroupEntity>,
private readonly applicationService: ApplicationService,
) {}
async createOne({
createViewFieldGroupInput,
workspaceId,
}: {
createViewFieldGroupInput: CreateViewFieldGroupInput;
workspaceId: string;
}): Promise<ViewFieldGroupDTO> {
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<ViewFieldGroupDTO[]> {
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<ViewFieldGroupDTO> {
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<ViewFieldGroupDTO> {
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<ViewFieldGroupDTO> {
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<ViewFieldGroupEntity[]> {
return this.viewFieldGroupRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
});
}
async findById(
id: string,
workspaceId: string,
): Promise<ViewFieldGroupEntity | null> {
const viewFieldGroup = await this.viewFieldGroupRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
});
return viewFieldGroup || null;
}
}
@@ -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,
};
};
@@ -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 {}
@@ -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<ViewFieldEntity[]> {
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<ViewFieldEntity> {
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<ViewFieldDTO> {
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<ViewFieldDTO> {
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,
});
@@ -46,6 +46,11 @@ export class CreateViewFieldInput {
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
viewFieldGroupId?: string;
@HideField()
universalIdentifier?: string;
@@ -35,6 +35,11 @@ class UpdateViewFieldInputUpdates {
@IsEnum(AggregateOperations)
@Field(() => AggregateOperations, { nullable: true })
aggregateOperation?: AggregateOperations;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
viewFieldGroupId?: string | null;
}
@InputType()
@@ -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<ViewEntity>;
@ManyToOne(
() => ViewFieldGroupEntity,
(viewFieldGroup) => viewFieldGroup.viewFields,
{
onDelete: 'SET NULL',
nullable: true,
},
)
@JoinColumn({ name: 'viewFieldGroupId' })
viewFieldGroup: Relation<ViewFieldGroupEntity> | null;
}
@@ -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<ViewFieldEntity[]> {
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<ViewFieldEntity | null> {
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<ViewFieldDTO> {
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<ViewFieldDTO> {
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<ViewFieldDTO[]> {
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<ViewFieldDTO> {
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<ViewFieldDTO> {
return await this.viewFieldV2Service.destroyOne({
return await this.viewFieldService.destroyOne({
destroyViewFieldInput,
workspaceId,
});
@@ -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,
});
@@ -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 {}
@@ -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,
})
@@ -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<ViewEntity> {
@OneToMany(() => ViewFieldEntity, (viewField) => viewField.view)
viewFields: Relation<ViewFieldEntity[]>;
@OneToMany(
() => ViewFieldGroupEntity,
(viewFieldGroup) => viewFieldGroup.view,
)
viewFieldGroups: Relation<ViewFieldGroupEntity[]>;
@OneToMany(() => ViewFilterEntity, (viewFilter) => viewFilter.view)
viewFilters: Relation<ViewFilterEntity[]>;
@@ -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,
});
}
}
@@ -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:
@@ -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
@@ -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,
@@ -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',
@@ -5,6 +5,7 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
'objectMetadata',
'fieldMetadata',
'viewField',
'viewFieldGroup',
'viewFilter',
'viewGroup',
'view',
@@ -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,
@@ -88,6 +88,8 @@ export const createStandardViewFieldFlatMetadata = <
fieldMetadataId:
standardObjectMetadataRelatedEntityIds[objectName].fields[fieldName].id,
fieldMetadataUniversalIdentifier: fieldDefinition.universalIdentifier,
viewFieldGroupId: null,
viewFieldGroupUniversalIdentifier: null,
position,
isVisible,
size,
@@ -142,6 +142,8 @@ export const createStandardViewFlatMetadata = <
createdByUserWorkspaceId: null,
viewFieldIds: [],
viewFieldUniversalIdentifiers: [],
viewFieldGroupIds: [],
viewFieldGroupUniversalIdentifiers: [],
viewFilterIds: [],
viewFilterUniversalIdentifiers: [],
viewGroupIds: [],
@@ -143,6 +143,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -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
@@ -15,6 +15,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
view: {},
viewField: {},
viewGroup: {},
viewFieldGroup: {},
viewFilter: {},
viewFilterGroup: {},
index: {},
@@ -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: {
@@ -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'
>;
@@ -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'>;
@@ -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,
};
}
}
@@ -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;
}
}
@@ -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,
@@ -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,
@@ -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<UniversalCreateViewFieldGroupAction>): Promise<FlatCreateViewFieldGroupAction> {
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<FlatCreateViewFieldGroupAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { flatEntity } = flatAction;
const repository =
queryRunner.manager.getRepository<ViewFieldGroupEntity>(
ViewFieldGroupEntity,
);
await repository.insert({
...flatEntity,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatCreateViewFieldGroupAction>,
): Promise<void> {
return;
}
}
@@ -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<UniversalDeleteViewFieldGroupAction>,
): Promise<FlatDeleteViewFieldGroupAction> {
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatDeleteViewFieldGroupAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const repository =
queryRunner.manager.getRepository<ViewFieldGroupEntity>(
ViewFieldGroupEntity,
);
await repository.delete({
id: flatAction.entityId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteViewFieldGroupAction>,
): Promise<void> {
return;
}
}
@@ -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<UniversalUpdateViewFieldGroupAction>,
): Promise<FlatUpdateViewFieldGroupAction> {
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<FlatUpdateViewFieldGroupAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, update } = flatAction;
const repository =
queryRunner.manager.getRepository<ViewFieldGroupEntity>(
ViewFieldGroupEntity,
);
await repository.update({ id: entityId, workspaceId }, update);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateViewFieldGroupAction>,
): Promise<void> {
return;
}
}
@@ -30,7 +30,7 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateViewFieldAction>): Promise<FlatCreateViewFieldAction> {
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,
@@ -53,6 +53,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
applicationId: flatApplication.id,
workspaceId,
viewFieldIds: [],
viewFieldGroupIds: [],
viewGroupIds: [],
viewFilterIds: [],
viewFilterGroupIds: [],
@@ -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,
@@ -45,6 +45,7 @@ export const deriveMetadataEventsFromCreateAction = (
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -22,6 +22,7 @@ export const deriveMetadataEventsFromDeleteAction = ({
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -89,6 +89,7 @@ export const deriveMetadataEventsFromUpdateAction = ({
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -45,6 +45,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -22,6 +22,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -44,6 +44,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
case 'view':
case 'viewField':
case 'viewGroup':
case 'viewFieldGroup':
case 'rowLevelPermissionPredicate':
case 'rowLevelPermissionPredicateGroup':
case 'viewFilterGroup':
@@ -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
`;
@@ -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<ViewFieldGroupTestSetup> => {
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<void> => {
await updateOneObjectMetadata({
input: {
idToUpdate: objectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: objectMetadataId },
});
};
@@ -11,7 +11,7 @@ export type ViewFieldTestSetup = {
testFieldMetadataId: string;
};
export const setupViewFieldTestV2 = async (): Promise<ViewFieldTestSetup> => {
export const setupViewFieldTest = async (): Promise<ViewFieldTestSetup> => {
const {
data: {
createOneObject: { id: objectMetadataId },
@@ -19,10 +19,10 @@ export const setupViewFieldTestV2 = async (): Promise<ViewFieldTestSetup> => {
} = 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<ViewFieldTestSetup> => {
};
};
export const cleanupViewFieldTestV2 = async (
export const cleanupViewFieldTest = async (
objectMetadataId: string,
): Promise<void> => {
await updateOneObjectMetadata({
@@ -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",
}
`;
@@ -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",
}
`;
@@ -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<String>,
"userFriendlyMessage": "A view field group error occurred.",
},
"message": "View field group to delete not found",
}
`;
@@ -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<String>,
"userFriendlyMessage": "A view field group error occurred.",
},
"message": "View field group to destroy not found",
}
`;
@@ -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<String>,
"userFriendlyMessage": "A view field group error occurred.",
},
"message": "View field group to update not found",
}
`;
@@ -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,
});
});
});
@@ -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<CreateViewFieldGroupTestCase>[] =
[
{
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),
);
},
);
});
@@ -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<DeleteViewFieldGroupTestCase>[] =
[
{
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),
);
},
);
});
@@ -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<DestroyViewFieldGroupTestCase>[] =
[
{
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),
);
},
);
});
@@ -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<UpdateViewFieldGroupTestCase>[] =
[
{
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),
);
},
);
});
@@ -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);
});
});
@@ -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<ViewFieldGroupDTO>;
};
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<TestContext>[] = [
{
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,
});
},
);
});
@@ -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",

Some files were not shown because too many files have changed in this diff Show More