Migrate view filter group to v2 (#16876)

## Introduction
On the side hanlded PR with a // agents on another repo
Made several iterations to fix behavior and direction
Find below auto-generated PR description

closes https://github.com/twentyhq/core-team-issues/issues/2037

Created generic tooling for entity circular dep checking, will be useful
for permissions validation too @Weiko


## Migrate `viewFilterGroup` entity to v2 flat architecture

### Summary
Migrates the `viewFilterGroup` entity from v1 to the v2 flat entity
architecture, following the established patterns for other v2 entities
like `viewFilter`, `view`, and `viewField`.

### Changes

**Types & Constants**
- Added `FlatViewFilterGroup` and `FlatViewFilterGroupMaps` types
- Added editable properties constant for `viewFilterGroup`
- Registered `viewFilterGroup` in `ALL_METADATA_NAME`,
`ALL_METADATA_RELATION_PROPERTIES`,
`ALL_METADATA_MANY_TO_ONE_RELATIONS`, and related constants

**Cache Service**
- Created `WorkspaceFlatViewFilterGroupMapCacheService` with proper
relation loading for `viewFilters` and `childViewFilterGroups`
- Updated `WorkspaceFlatViewMapCacheService` to load `viewFilterGroups`
relation

**Builder & Validator**
- Created `WorkspaceMigrationV2ViewFilterGroupActionsBuilderService`
- Created `FlatViewFilterGroupValidatorService` with creation, update,
and deletion validation
- Integrated validation into the orchestrator service (runs before
`viewFilter` validation)

**Action Handlers**
- Created create, update, and delete action handlers for
`viewFilterGroup`

**Service Migration**
- Rewrote `ViewFilterGroupService` to use v2 migration pattern with
`WorkspaceMigrationValidateBuildAndRunService`
- Created utility functions for transforming DTOs to flat entities

**Database Migration**
- Added migration to make `parentViewFilterGroupId` foreign key
deferrable (handles self-referential parent/child insertions)

**ViewFilter Integration**
- Added `viewFilterGroupId` validation in
`FlatViewFilterValidatorService`
- Updated `viewFilter` many-to-one relations to include
`viewFilterGroup`

**Tests**
- Added integration tests for successful creation, update, deletion, and
destruction
- Added failing test cases for non-existent entities and invalid
references
- Added failing test for `viewFilter` creation with non-existent
`viewFilterGroupId`

### Breaking Changes
None - existing API contracts are preserved.
This commit is contained in:
Paul Rastoin
2026-01-02 14:16:01 +01:00
committed by GitHub
parent 0adbe439d5
commit 98a9ae2a0e
106 changed files with 4648 additions and 439 deletions
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
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';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
@@ -14,6 +15,7 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit
ViewFieldEntity,
ViewFilterEntity,
ViewGroupEntity,
ViewFilterGroupEntity,
]),
],
providers: [WorkspaceFlatViewMapCacheService],
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { type Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
@@ -9,6 +9,7 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-enti
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
import { fromViewEntityToFlatView } from 'src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util';
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';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
@@ -28,34 +29,47 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<Fla
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
@InjectRepository(ViewFilterGroupEntity)
private readonly viewFilterGroupRepository: Repository<ViewFilterGroupEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatViewMaps> {
const [views, viewFields, viewFilters, viewGroups] = await Promise.all([
this.viewRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.viewFieldRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewGroupRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
]);
const [views, viewFields, viewFilters, viewGroups, viewFilterGroups] =
await Promise.all([
this.viewRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.viewFieldRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewFilterRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewGroupRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
this.viewFilterGroupRepository.find({
where: { workspaceId },
select: ['id', 'viewId'],
withDeleted: true,
}),
]);
const [viewFieldsByViewId, viewFiltersByViewId, viewGroupsByViewId] = (
const [
viewFieldsByViewId,
viewFiltersByViewId,
viewGroupsByViewId,
viewFilterGroupsByViewId,
] = (
[
{
entities: viewFields,
@@ -69,6 +83,10 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<Fla
entities: viewGroups,
foreignKey: 'viewId',
},
{
entities: viewFilterGroups,
foreignKey: 'viewId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
@@ -80,6 +98,7 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<Fla
viewFields: viewFieldsByViewId.get(viewEntity.id) || [],
viewFilters: viewFiltersByViewId.get(viewEntity.id) || [],
viewGroups: viewGroupsByViewId.get(viewEntity.id) || [],
viewFilterGroups: viewFilterGroupsByViewId.get(viewEntity.id) || [],
} as ViewEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
@@ -2,6 +2,6 @@ import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/typ
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
export type FlatView = FlatEntityFrom<
// TODO remove once viewSorts and viewFilterGroups have been migrated to v2
Omit<ViewEntity, 'viewSorts' | 'viewFilterGroups'>
// TODO remove once viewSorts has been migrated to v2
Omit<ViewEntity, 'viewSorts'>
>;
@@ -68,6 +68,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({
viewFieldIds: [],
viewFilterIds: [],
viewGroupIds: [],
viewFilterGroupIds: [],
applicationId: workspaceCustomApplicationId,
};
@@ -23,5 +23,9 @@ export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => {
viewFieldIds: viewEntity.viewFields.map((viewField) => viewField.id),
viewFilterIds: viewEntity.viewFilters.map((viewFilter) => viewFilter.id),
viewGroupIds: viewEntity.viewGroups.map((viewGroup) => viewGroup.id),
viewFilterGroupIds:
viewEntity.viewFilterGroups?.map(
(viewFilterGroup) => viewFilterGroup.id,
) ?? [],
};
};