[REQUIRES_CACHE_FLUSH][GQL_VIEW_GROUP_API_BREAKING_CHANGE] ViewGroup in v2 (#15052)

# Introduction
Adding view-group to core engine v2
Following https://github.com/twentyhq/twenty/pull/15010 ( same pattern )

## What's done
- Created flat-view-group
- flat view group runner
- flat view group builder
- create view group service v2 and input transpilers
- refactor the existing view group resolver to fix standard (
BREAKING_CHANGE on graphql api update especially ) REST stays the same
- refactored the front to consume the mutations autogenerated

close https://github.com/twentyhq/core-team-issues/issues/1665
This commit is contained in:
Paul Rastoin
2025-10-14 15:55:50 +02:00
committed by GitHub
parent 5fec6a9afc
commit 7d747c9876
88 changed files with 2640 additions and 497 deletions
@@ -0,0 +1,8 @@
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
export const FLAT_VIEW_GROUP_EDITABLE_PROPERTIES = [
'isVisible',
'fieldValue',
'position',
'fieldMetadataId',
] as const satisfies (keyof FlatViewGroup)[];
@@ -0,0 +1,7 @@
import { type ViewGroupEntityRelationProperties } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
export const VIEW_GROUP_ENTITY_RELATION_PROPERTIES = [
'fieldMetadata',
'view',
'workspace',
] as const satisfies ViewGroupEntityRelationProperties[];
@@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type';
import { fromViewGroupEntityToFlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
@WorkspaceFlatMapCache('flatViewGroupMaps')
export class WorkspaceFlatViewGroupMapCacheService extends WorkspaceFlatMapCacheService<FlatViewGroupMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewGroupMaps> {
const existingViewGroups = await this.viewGroupRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
return existingViewGroups.reduce((flatViewGroupMaps, viewGroupEntity) => {
const flatViewGroup = fromViewGroupEntityToFlatViewGroup(viewGroupEntity);
return addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatViewGroup,
flatEntityMaps: flatViewGroupMaps,
});
}, EMPTY_FLAT_ENTITY_MAPS);
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
export type FlatViewGroupMaps = FlatEntityMaps<FlatViewGroup>;
@@ -0,0 +1,18 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type ViewGroupEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ViewGroupEntity,
FieldMetadataEntity | ViewEntity | Workspace
>;
export type FlatViewGroup = Omit<
ViewGroupEntity,
ViewGroupEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,36 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
export const fromCreateViewGroupInputToFlatViewGroupToCreate = ({
createViewGroupInput: rawCreateViewGroupInput,
workspaceId,
}: {
createViewGroupInput: CreateViewGroupInput;
workspaceId: string;
}): FlatViewGroup => {
const { fieldMetadataId, viewId, ...createViewGroupInput } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateViewGroupInput,
['fieldMetadataId', 'fieldValue', 'id', 'viewId'],
);
const createdAt = new Date();
const viewGroupId = createViewGroupInput.id ?? v4();
return {
id: viewGroupId,
fieldMetadataId,
viewId,
workspaceId,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: viewGroupId,
isVisible: createViewGroupInput.isVisible ?? true,
fieldValue: createViewGroupInput.fieldValue,
position: createViewGroupInput.position ?? 0,
};
};
@@ -0,0 +1,40 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type DeleteViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/delete-view-group.input';
import {
ViewGroupException,
ViewGroupExceptionCode,
} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
export const fromDeleteViewGroupInputToFlatViewGroupOrThrow = ({
deleteViewGroupInput: rawDeleteViewGroupInput,
flatViewGroupMaps,
}: {
deleteViewGroupInput: DeleteViewGroupInput;
flatViewGroupMaps: FlatViewGroupMaps;
}): FlatViewGroup => {
const { id: viewGroupId } = extractAndSanitizeObjectStringFields(
rawDeleteViewGroupInput,
['id'],
);
const existingFlatViewGroupToDelete = flatViewGroupMaps.byId[viewGroupId];
if (!isDefined(existingFlatViewGroupToDelete)) {
throw new ViewGroupException(
t`View group to delete not found`,
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
);
}
return {
...existingFlatViewGroupToDelete,
deletedAt: new Date(),
};
};
@@ -0,0 +1,37 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
} from 'twenty-shared/utils';
import { type FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type DestroyViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/destroy-view-group.input';
import {
ViewGroupException,
ViewGroupExceptionCode,
} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
export const fromDestroyViewGroupInputToFlatViewGroupOrThrow = ({
destroyViewGroupInput,
flatViewGroupMaps,
}: {
destroyViewGroupInput: DestroyViewGroupInput;
flatViewGroupMaps: FlatViewGroupMaps;
}): FlatViewGroup => {
const { id: viewGroupId } = extractAndSanitizeObjectStringFields(
destroyViewGroupInput,
['id'],
);
const existingFlatViewGroupToDestroy = flatViewGroupMaps.byId[viewGroupId];
if (!isDefined(existingFlatViewGroupToDestroy)) {
throw new ViewGroupException(
t`View group to destroy not found`,
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
);
}
return existingFlatViewGroupToDestroy;
};
@@ -0,0 +1,50 @@
import { t } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
} from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FLAT_VIEW_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/flat-view-group-editable-properties.constant';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
import {
ViewGroupException,
ViewGroupExceptionCode,
} from 'src/engine/metadata-modules/view-group/exceptions/view-group.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow = ({
updateViewGroupInput: rawUpdateViewGroupInput,
flatViewGroupMaps,
}: {
updateViewGroupInput: UpdateViewGroupInput;
flatViewGroupMaps: FlatEntityMaps<FlatViewGroup>;
}): FlatViewGroup => {
const { id: viewGroupToUpdateId, update } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateViewGroupInput,
['id'],
);
const existingFlatViewGroupToUpdate =
flatViewGroupMaps.byId[viewGroupToUpdateId];
if (!isDefined(existingFlatViewGroupToUpdate)) {
throw new ViewGroupException(
t`View group to update not found`,
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
);
}
const updatedEditableGroupProperties = extractAndSanitizeObjectStringFields(
update,
FLAT_VIEW_GROUP_EDITABLE_PROPERTIES,
);
return mergeUpdateInExistingRecord({
existing: existingFlatViewGroupToUpdate,
properties: FLAT_VIEW_GROUP_EDITABLE_PROPERTIES,
update: updatedEditableGroupProperties,
});
};
@@ -0,0 +1,21 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { VIEW_GROUP_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/view-group-entity-relation-properties.constant';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
export const fromViewGroupEntityToFlatViewGroup = (
viewGroupEntity: ViewGroupEntity,
): FlatViewGroup => {
const viewGroupEntityWithoutRelations = removePropertiesFromRecord(
viewGroupEntity,
VIEW_GROUP_ENTITY_RELATION_PROPERTIES,
);
return {
...viewGroupEntityWithoutRelations,
universalIdentifier:
viewGroupEntityWithoutRelations.universalIdentifier ??
viewGroupEntityWithoutRelations.id,
};
};