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:
+3
@@ -78,10 +78,13 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const flatViewFilterGroupMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
const flatViewFilterMaps = buildStandardFlatViewFilterMetadataMaps({
|
||||
dependencyFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatViewMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
},
|
||||
now,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
|
||||
+1
@@ -116,6 +116,7 @@ export const createStandardViewFlatMetadata = <
|
||||
viewFieldIds: [],
|
||||
viewFilterIds: [],
|
||||
viewGroupIds: [],
|
||||
viewFilterGroupIds: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE
|
||||
summary: {
|
||||
invalidAgent: 0,
|
||||
invalidViewFilter: 0,
|
||||
invalidViewFilterGroup: 0,
|
||||
invalidObjectMetadata: 0,
|
||||
invalidView: 0,
|
||||
invalidViewField: 0,
|
||||
|
||||
+54
-6
@@ -1,17 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type MetadataFlatEntityAndRelatedFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
|
||||
import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/constant/empty-orchestrator-actions-report.constant';
|
||||
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration-v2/constant/empty-orchestrator-failure-report.constant';
|
||||
import {
|
||||
WorkspaceMigrationOrchestratorBuildArgs,
|
||||
WorkspaceMigrationOrchestratorFailedResult,
|
||||
WorkspaceMigrationOrchestratorSuccessfulResult,
|
||||
type WorkspaceMigrationOrchestratorBuildArgs,
|
||||
type WorkspaceMigrationOrchestratorFailedResult,
|
||||
type WorkspaceMigrationOrchestratorSuccessfulResult,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
|
||||
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util';
|
||||
import { WorkspaceMigrationV2AgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/agent/workspace-migration-v2-agent-actions-builder.service';
|
||||
@@ -30,6 +30,7 @@ import { WorkspaceMigrationV2RowLevelPermissionPredicateGroupActionsBuilderServi
|
||||
import { WorkspaceMigrationV2RowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/row-level-permission-predicate/workspace-migration-v2-row-level-permission-predicate-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/workspace-migration-v2-view-filter-group-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-group/workspace-migration-v2-view-group-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
|
||||
@@ -42,6 +43,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
private readonly workspaceMigrationV2ViewActionsBuilderService: WorkspaceMigrationV2ViewActionsBuilderService,
|
||||
private readonly workspaceMigrationV2ViewFieldActionsBuilderService: WorkspaceMigrationV2ViewFieldActionsBuilderService,
|
||||
private readonly workspaceMigrationV2ViewFilterActionsBuilderService: WorkspaceMigrationV2ViewFilterActionsBuilderService,
|
||||
private readonly workspaceMigrationV2ViewFilterGroupActionsBuilderService: WorkspaceMigrationV2ViewFilterGroupActionsBuilderService,
|
||||
private readonly workspaceMigrationV2ViewGroupActionsBuilderService: WorkspaceMigrationV2ViewGroupActionsBuilderService,
|
||||
private readonly workspaceMigrationV2ServerlessFunctionActionsBuilderService: WorkspaceMigrationV2ServerlessFunctionActionsBuilderService,
|
||||
private readonly workspaceMigrationV2DatabaseEventTriggerActionsBuilderService: WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
|
||||
@@ -144,6 +146,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatRouteTriggerMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFilterMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
flatViewGroupMaps,
|
||||
flatRowLevelPermissionPredicateMaps,
|
||||
flatRowLevelPermissionPredicateGroupMaps,
|
||||
@@ -347,6 +350,46 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatViewFilterGroupMaps)) {
|
||||
const {
|
||||
from: fromFlatViewFilterGroupMaps,
|
||||
to: toFlatViewFilterGroupMaps,
|
||||
} = flatViewFilterGroupMaps;
|
||||
|
||||
const viewFilterGroupResult =
|
||||
await this.workspaceMigrationV2ViewFilterGroupActionsBuilderService.validateAndBuild(
|
||||
{
|
||||
additionalCacheDataMaps,
|
||||
from: fromFlatViewFilterGroupMaps,
|
||||
to: toFlatViewFilterGroupMaps,
|
||||
buildOptions,
|
||||
dependencyOptimisticFlatEntityMaps: {
|
||||
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
|
||||
flatViewFilterGroupMaps:
|
||||
optimisticAllFlatEntityMaps.flatViewFilterGroupMaps,
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
|
||||
{
|
||||
allFlatEntityMaps: optimisticAllFlatEntityMaps,
|
||||
flatEntityMapsAndRelatedFlatEntityMaps:
|
||||
viewFilterGroupResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (viewFilterGroupResult.status === 'fail') {
|
||||
orchestratorFailureReport.viewFilterGroup.push(
|
||||
...viewFilterGroupResult.errors,
|
||||
);
|
||||
} else {
|
||||
orchestratorActionsReport.viewFilterGroup =
|
||||
viewFilterGroupResult.actions;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatViewFilterMaps)) {
|
||||
const { from: fromFlatViewFilterMaps, to: toFlatViewFilterMaps } =
|
||||
flatViewFilterMaps;
|
||||
@@ -361,6 +404,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatFieldMetadataMaps:
|
||||
optimisticAllFlatEntityMaps.flatFieldMetadataMaps,
|
||||
flatViewMaps: optimisticAllFlatEntityMaps.flatViewMaps,
|
||||
flatViewFilterGroupMaps:
|
||||
optimisticAllFlatEntityMaps.flatViewFilterGroupMaps,
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
@@ -890,6 +935,9 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
...aggregatedOrchestratorActionsReport.viewField.deleted,
|
||||
...aggregatedOrchestratorActionsReport.viewField.created,
|
||||
...aggregatedOrchestratorActionsReport.viewField.updated,
|
||||
...aggregatedOrchestratorActionsReport.viewFilterGroup.deleted,
|
||||
...aggregatedOrchestratorActionsReport.viewFilterGroup.created,
|
||||
...aggregatedOrchestratorActionsReport.viewFilterGroup.updated,
|
||||
...aggregatedOrchestratorActionsReport.viewFilter.deleted,
|
||||
...aggregatedOrchestratorActionsReport.viewFilter.created,
|
||||
...aggregatedOrchestratorActionsReport.viewFilter.updated,
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
import {
|
||||
type CircularDependencyValidationResult,
|
||||
validateFlatEntityCircularDependency,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/utils/validate-flat-entity-circular-dependency.util';
|
||||
|
||||
type TestFlatEntity = SyncableFlatEntity & {
|
||||
parentId: string | null;
|
||||
};
|
||||
|
||||
const createFlatEntityMaps = (
|
||||
entities: TestFlatEntity[],
|
||||
): FlatEntityMaps<TestFlatEntity> => ({
|
||||
byId: Object.fromEntries(entities.map((entity) => [entity.id, entity])),
|
||||
idByUniversalIdentifier: Object.fromEntries(
|
||||
entities.map((entity) => [entity.universalIdentifier, entity.id]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const createTestEntity = (
|
||||
id: string,
|
||||
parentId: string | null = null,
|
||||
): TestFlatEntity => ({
|
||||
id,
|
||||
parentId,
|
||||
universalIdentifier: id,
|
||||
applicationId: 'test-app',
|
||||
workspaceId: 'test-workspace',
|
||||
});
|
||||
|
||||
type TestContext = {
|
||||
flatEntityId: string;
|
||||
flatEntityParentId: string;
|
||||
maxDepth?: number;
|
||||
entities: TestFlatEntity[];
|
||||
expected: CircularDependencyValidationResult;
|
||||
};
|
||||
|
||||
type ValidateCircularDependencyTestCase = EachTestingContext<TestContext>;
|
||||
|
||||
const testCases: ValidateCircularDependencyTestCase[] = [
|
||||
{
|
||||
title: 'should fail with self_reference when entity is its own parent',
|
||||
context: {
|
||||
flatEntityId: 'entity-1',
|
||||
flatEntityParentId: 'entity-1',
|
||||
entities: [],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'self_reference',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed with depth 2 when parent has no parent',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
entities: [createTestEntity('entity-parent', null)],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed with depth 3 when parent has a grandparent',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
entities: [
|
||||
createTestEntity('entity-grandparent', null),
|
||||
createTestEntity('entity-parent', 'entity-grandparent'),
|
||||
],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fail with circular_dependency when parent points back to entity',
|
||||
context: {
|
||||
flatEntityId: 'entity-a',
|
||||
flatEntityParentId: 'entity-b',
|
||||
entities: [createTestEntity('entity-b', 'entity-a')],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fail with circular_dependency when chain creates a cycle (A -> B -> C -> A)',
|
||||
context: {
|
||||
flatEntityId: 'entity-a',
|
||||
flatEntityParentId: 'entity-b',
|
||||
entities: [
|
||||
createTestEntity('entity-b', 'entity-c'),
|
||||
createTestEntity('entity-c', 'entity-a'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fail with circular_dependency when ancestors form a cycle among themselves',
|
||||
context: {
|
||||
flatEntityId: 'entity-new',
|
||||
flatEntityParentId: 'entity-a',
|
||||
entities: [
|
||||
createTestEntity('entity-a', 'entity-b'),
|
||||
createTestEntity('entity-b', 'entity-a'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed when parent does not exist in maps',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'non-existent-parent',
|
||||
entities: [],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should fail with max_depth_exceeded when depth exceeds maxDepth',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
maxDepth: 2,
|
||||
entities: [
|
||||
createTestEntity('entity-grandparent', null),
|
||||
createTestEntity('entity-parent', 'entity-grandparent'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed when depth equals maxDepth',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
maxDepth: 3,
|
||||
entities: [
|
||||
createTestEntity('entity-grandparent', null),
|
||||
createTestEntity('entity-parent', 'entity-grandparent'),
|
||||
],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should succeed with depth 2 when maxDepth is 2 and parent is root',
|
||||
context: {
|
||||
flatEntityId: 'entity-child',
|
||||
flatEntityParentId: 'entity-parent',
|
||||
maxDepth: 2,
|
||||
entities: [createTestEntity('entity-parent', null)],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should check max depth before circular dependency to prevent infinite loop',
|
||||
context: {
|
||||
flatEntityId: 'entity-new',
|
||||
flatEntityParentId: 'entity-a',
|
||||
maxDepth: 2,
|
||||
entities: [
|
||||
createTestEntity('entity-a', 'entity-b'),
|
||||
createTestEntity('entity-b', 'entity-c'),
|
||||
createTestEntity('entity-c', 'entity-d'),
|
||||
createTestEntity('entity-d', 'entity-a'),
|
||||
],
|
||||
expected: {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should handle deep valid hierarchy without maxDepth',
|
||||
context: {
|
||||
flatEntityId: 'entity-new',
|
||||
flatEntityParentId: 'entity-1',
|
||||
entities: [
|
||||
createTestEntity('entity-5', null),
|
||||
createTestEntity('entity-4', 'entity-5'),
|
||||
createTestEntity('entity-3', 'entity-4'),
|
||||
createTestEntity('entity-2', 'entity-3'),
|
||||
createTestEntity('entity-1', 'entity-2'),
|
||||
],
|
||||
expected: {
|
||||
status: 'success',
|
||||
depth: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('validateFlatEntityCircularDependency', () => {
|
||||
test.each(testCases)('$title', ({ context }) => {
|
||||
const flatEntityMaps = createFlatEntityMaps(context.entities);
|
||||
|
||||
const result = validateFlatEntityCircularDependency({
|
||||
flatEntityId: context.flatEntityId,
|
||||
flatEntityParentId: context.flatEntityParentId,
|
||||
maxDepth: context.maxDepth,
|
||||
parentIdKey: 'parentId',
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
expect(result).toEqual(context.expected);
|
||||
});
|
||||
});
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
|
||||
|
||||
export type CircularDependencyValidationSuccess = {
|
||||
status: 'success';
|
||||
depth: number;
|
||||
};
|
||||
|
||||
export type CircularDependencyValidationFailure = {
|
||||
status: 'fail';
|
||||
reason: 'self_reference' | 'circular_dependency' | 'max_depth_exceeded';
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
export type CircularDependencyValidationResult =
|
||||
| CircularDependencyValidationSuccess
|
||||
| CircularDependencyValidationFailure;
|
||||
|
||||
export const validateFlatEntityCircularDependency = <
|
||||
T extends SyncableFlatEntity,
|
||||
K extends keyof T,
|
||||
>({
|
||||
flatEntityId,
|
||||
flatEntityParentId,
|
||||
maxDepth,
|
||||
parentIdKey,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
flatEntityId: string;
|
||||
flatEntityParentId: string;
|
||||
maxDepth?: number;
|
||||
parentIdKey: K;
|
||||
flatEntityMaps: FlatEntityMaps<T>;
|
||||
}): CircularDependencyValidationResult => {
|
||||
// Direct self-reference check
|
||||
if (flatEntityId === flatEntityParentId) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'self_reference',
|
||||
};
|
||||
}
|
||||
|
||||
// Traverse ancestor chain to detect cycles and measure depth
|
||||
const visited = new Set<string>();
|
||||
let currentParentId: string | null | undefined = flatEntityParentId;
|
||||
let depth = 1;
|
||||
|
||||
while (isDefined(currentParentId)) {
|
||||
if (isDefined(maxDepth) && depth > maxDepth) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for circular dependency
|
||||
if (currentParentId === flatEntityId) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for cycle in ancestors (already visited node)
|
||||
if (visited.has(currentParentId)) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'circular_dependency',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
visited.add(currentParentId);
|
||||
|
||||
const parentEntity: T | undefined = flatEntityMaps.byId[currentParentId];
|
||||
|
||||
if (!isDefined(parentEntity)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextParentId: T[K] = parentEntity[parentIdKey];
|
||||
|
||||
currentParentId =
|
||||
isDefined(nextParentId) && typeof nextParentId === 'string'
|
||||
? nextParentId
|
||||
: undefined;
|
||||
depth++;
|
||||
}
|
||||
|
||||
// Check max depth for the final depth value
|
||||
if (isDefined(maxDepth) && depth > maxDepth) {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: 'max_depth_exceeded',
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
depth,
|
||||
};
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group.type';
|
||||
|
||||
export type CreateViewFilterGroupAction = {
|
||||
type: 'create_view_filter_group';
|
||||
viewFilterGroup: FlatViewFilterGroup;
|
||||
};
|
||||
|
||||
export type UpdateViewFilterGroupAction = {
|
||||
type: 'update_view_filter_group';
|
||||
viewFilterGroupId: string;
|
||||
updates: FlatEntityPropertiesUpdates<'viewFilterGroup'>;
|
||||
};
|
||||
|
||||
export type DeleteViewFilterGroupAction = {
|
||||
type: 'delete_view_filter_group';
|
||||
viewFilterGroupId: string;
|
||||
};
|
||||
|
||||
export type WorkspaceMigrationViewFilterGroupActionV2 =
|
||||
| CreateViewFilterGroupAction
|
||||
| UpdateViewFilterGroupAction
|
||||
| DeleteViewFilterGroupAction;
|
||||
|
||||
export type WorkspaceMigrationViewFilterGroupActionTypeV2 =
|
||||
WorkspaceMigrationViewFilterGroupActionV2['type'];
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
|
||||
import { type UpdateViewFilterGroupAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/types/workspace-migration-view-filter-group-action-v2.type';
|
||||
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
import { type FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
|
||||
import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-group-validator.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationV2ViewFilterGroupActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatViewFilterGroupValidatorService: FlatViewFilterGroupValidatorService,
|
||||
) {
|
||||
super(ALL_METADATA_NAME.viewFilterGroup);
|
||||
}
|
||||
|
||||
protected validateFlatEntityCreation(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.viewFilterGroup>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup,
|
||||
'created'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatViewFilterGroupValidatorService.validateFlatViewFilterGroupCreation(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatViewFilterGroupToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create_view_filter_group',
|
||||
viewFilterGroup: flatViewFilterGroupToValidate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityDeletion(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.viewFilterGroup>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup,
|
||||
'deleted'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatViewFilterGroupValidatorService.validateFlatViewFilterGroupDeletion(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatViewFilterGroupToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete_view_filter_group',
|
||||
viewFilterGroupId: flatViewFilterGroupToValidate.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityUpdate(
|
||||
args: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup
|
||||
>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup,
|
||||
'updated'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatViewFilterGroupValidatorService.validateFlatViewFilterGroupUpdate(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityId, flatEntityUpdates } = args;
|
||||
|
||||
const updateViewFilterGroupAction: UpdateViewFilterGroupAction = {
|
||||
type: 'update_view_filter_group',
|
||||
viewFilterGroupId: flatEntityId,
|
||||
updates: flatEntityUpdates,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: updateViewFilterGroupAction,
|
||||
};
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatViewFilterGroupMaps } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group-maps.type';
|
||||
import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group.type';
|
||||
import { ViewFilterGroupExceptionCode } from 'src/engine/metadata-modules/view-filter-group/exceptions/view-filter-group.exception';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util';
|
||||
import { validateFlatEntityCircularDependency } from 'src/engine/workspace-manager/workspace-migration-v2/utils/validate-flat-entity-circular-dependency.util';
|
||||
import {
|
||||
type FailedFlatEntityValidation,
|
||||
type FlatEntityValidationError,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
|
||||
// View filter groups can have at most 2 levels of nesting (root and one child level)
|
||||
const VIEW_FILTER_GROUP_MAX_DEPTH = 2;
|
||||
|
||||
@Injectable()
|
||||
export class FlatViewFilterGroupValidatorService {
|
||||
constructor() {}
|
||||
|
||||
private getCircularDependencyValidationErrors({
|
||||
viewFilterGroupId,
|
||||
parentViewFilterGroupId,
|
||||
flatViewFilterGroupMaps,
|
||||
}: {
|
||||
viewFilterGroupId: string;
|
||||
parentViewFilterGroupId: string;
|
||||
flatViewFilterGroupMaps: FlatViewFilterGroupMaps;
|
||||
}): FlatEntityValidationError<ViewFilterGroupExceptionCode>[] {
|
||||
const circularDependencyResult = validateFlatEntityCircularDependency({
|
||||
flatEntityId: viewFilterGroupId,
|
||||
flatEntityParentId: parentViewFilterGroupId,
|
||||
maxDepth: VIEW_FILTER_GROUP_MAX_DEPTH,
|
||||
parentIdKey: 'parentViewFilterGroupId',
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (circularDependencyResult.status === 'success') {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (circularDependencyResult.reason) {
|
||||
case 'self_reference':
|
||||
return [
|
||||
{
|
||||
code: ViewFilterGroupExceptionCode.CIRCULAR_DEPENDENCY,
|
||||
message: t`View filter group cannot be its own parent`,
|
||||
userFriendlyMessage: msg`View filter group cannot be its own parent`,
|
||||
},
|
||||
];
|
||||
case 'circular_dependency':
|
||||
return [
|
||||
{
|
||||
code: ViewFilterGroupExceptionCode.CIRCULAR_DEPENDENCY,
|
||||
message: t`Circular dependency detected in view filter group hierarchy`,
|
||||
userFriendlyMessage: msg`Circular dependency detected in view filter group hierarchy`,
|
||||
},
|
||||
];
|
||||
case 'max_depth_exceeded':
|
||||
return [
|
||||
{
|
||||
code: ViewFilterGroupExceptionCode.MAX_DEPTH_EXCEEDED,
|
||||
message: t`View filter group hierarchy exceeds maximum depth of ${VIEW_FILTER_GROUP_MAX_DEPTH}`,
|
||||
userFriendlyMessage: msg`View filter group hierarchy exceeds maximum depth of ${VIEW_FILTER_GROUP_MAX_DEPTH}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
validateFlatViewFilterGroupCreation({
|
||||
flatEntityToValidate: flatViewFilterGroupToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatViewFilterGroupMaps: optimisticFlatViewFilterGroupMaps,
|
||||
flatViewMaps,
|
||||
},
|
||||
remainingFlatEntityMapsToValidate,
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup
|
||||
>): FailedFlatEntityValidation<FlatViewFilterGroup> {
|
||||
const validationResult: FailedFlatEntityValidation<FlatViewFilterGroup> = {
|
||||
type: 'create_view_filter_group',
|
||||
errors: [],
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatViewFilterGroupToValidate.id,
|
||||
},
|
||||
};
|
||||
|
||||
const existingViewFilterGroup = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatViewFilterGroupToValidate.id,
|
||||
flatEntityMaps: optimisticFlatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (isDefined(existingViewFilterGroup)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterGroupExceptionCode.INVALID_VIEW_FILTER_GROUP_DATA,
|
||||
message: t`View filter group with this id already exists`,
|
||||
userFriendlyMessage: msg`View filter group with this id already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
const referencedView = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatViewFilterGroupToValidate.viewId,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(referencedView)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterGroupExceptionCode.VIEW_NOT_FOUND,
|
||||
message: t`View not found`,
|
||||
userFriendlyMessage: msg`View not found`,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(flatViewFilterGroupToValidate.parentViewFilterGroupId)) {
|
||||
const circularDependencyErrors =
|
||||
this.getCircularDependencyValidationErrors({
|
||||
viewFilterGroupId: flatViewFilterGroupToValidate.id,
|
||||
parentViewFilterGroupId:
|
||||
flatViewFilterGroupToValidate.parentViewFilterGroupId,
|
||||
flatViewFilterGroupMaps: optimisticFlatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (circularDependencyErrors.length > 0) {
|
||||
validationResult.errors.push(...circularDependencyErrors);
|
||||
}
|
||||
|
||||
const referencedParentInOptimistic = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatViewFilterGroupToValidate.parentViewFilterGroupId,
|
||||
flatEntityMaps: optimisticFlatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
const referencedParentInRemaining = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatViewFilterGroupToValidate.parentViewFilterGroupId,
|
||||
flatEntityMaps: remainingFlatEntityMapsToValidate,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(referencedParentInOptimistic) &&
|
||||
!isDefined(referencedParentInRemaining)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
message: t`Parent view filter group not found`,
|
||||
userFriendlyMessage: msg`Parent view filter group not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
validateFlatViewFilterGroupDeletion({
|
||||
flatEntityToValidate: flatViewFilterGroupToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatViewFilterGroupMaps: optimisticFlatViewFilterGroupMaps,
|
||||
},
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup
|
||||
>): FailedFlatEntityValidation<FlatViewFilterGroup> {
|
||||
const validationResult: FailedFlatEntityValidation<FlatViewFilterGroup> = {
|
||||
type: 'delete_view_filter_group',
|
||||
errors: [],
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatViewFilterGroupToValidate.id,
|
||||
},
|
||||
};
|
||||
|
||||
const existingViewFilterGroup = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatViewFilterGroupToValidate.id,
|
||||
flatEntityMaps: optimisticFlatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingViewFilterGroup)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
message: t`View filter group not found`,
|
||||
userFriendlyMessage: msg`View filter group not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
validateFlatViewFilterGroupUpdate({
|
||||
flatEntityId,
|
||||
flatEntityUpdates,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatViewFilterGroupMaps: optimisticFlatViewFilterGroupMaps,
|
||||
},
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewFilterGroup
|
||||
>): FailedFlatEntityValidation<FlatViewFilterGroup> {
|
||||
const validationResult: FailedFlatEntityValidation<FlatViewFilterGroup> = {
|
||||
type: 'update_view_filter_group',
|
||||
errors: [],
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityId,
|
||||
},
|
||||
};
|
||||
|
||||
const existingViewFilterGroup =
|
||||
optimisticFlatViewFilterGroupMaps.byId[flatEntityId];
|
||||
|
||||
if (!isDefined(existingViewFilterGroup)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
message: t`View filter group not found`,
|
||||
userFriendlyMessage: msg`View filter group not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const parentViewFilterGroupIdUpdate = findFlatEntityPropertyUpdate({
|
||||
property: 'parentViewFilterGroupId',
|
||||
flatEntityUpdates,
|
||||
});
|
||||
|
||||
if (!isDefined(parentViewFilterGroupIdUpdate)) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const newParentViewFilterGroupId = parentViewFilterGroupIdUpdate.to;
|
||||
|
||||
if (!isDefined(newParentViewFilterGroupId)) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const circularDependencyErrors = this.getCircularDependencyValidationErrors(
|
||||
{
|
||||
viewFilterGroupId: flatEntityId,
|
||||
parentViewFilterGroupId: newParentViewFilterGroupId,
|
||||
flatViewFilterGroupMaps: optimisticFlatViewFilterGroupMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (circularDependencyErrors.length > 0) {
|
||||
validationResult.errors.push(...circularDependencyErrors);
|
||||
}
|
||||
|
||||
const referencedParentViewFilterGroup = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: newParentViewFilterGroupId,
|
||||
flatEntityMaps: optimisticFlatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(referencedParentViewFilterGroup)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
message: t`Parent view filter group not found`,
|
||||
userFriendlyMessage: msg`Parent view filter group not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
+36
-4
@@ -1,15 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
|
||||
import { ViewFilterExceptionCode } from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
|
||||
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
|
||||
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -22,6 +22,7 @@ export class FlatViewFilterValidatorService {
|
||||
flatViewFilterMaps: optimisticFlatViewFilterMaps,
|
||||
flatViewMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
},
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewFilter
|
||||
@@ -73,6 +74,21 @@ export class FlatViewFilterValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(flatViewFilterToValidate.viewFilterGroupId)) {
|
||||
const referencedViewFilterGroup = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatViewFilterToValidate.viewFilterGroupId,
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(referencedViewFilterGroup)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
|
||||
message: t`View filter group not found`,
|
||||
userFriendlyMessage: msg`View filter group not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
@@ -116,6 +132,7 @@ export class FlatViewFilterValidatorService {
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatViewFilterMaps: optimisticFlatViewFilterMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFilterGroupMaps,
|
||||
},
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewFilter
|
||||
@@ -160,6 +177,21 @@ export class FlatViewFilterValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(updatedFlatViewFilter.viewFilterGroupId)) {
|
||||
const referencedViewFilterGroup = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: updatedFlatViewFilter.viewFilterGroupId,
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(referencedViewFilterGroup)) {
|
||||
validationResult.errors.push({
|
||||
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
|
||||
message: t`View filter group not found`,
|
||||
userFriendlyMessage: msg`View filter group not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine
|
||||
import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-row-level-permission-predicate-validator.service';
|
||||
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-serverless-function-validator.service';
|
||||
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service';
|
||||
import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-group-validator.service';
|
||||
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service';
|
||||
import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service';
|
||||
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service';
|
||||
@@ -28,6 +29,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatViewValidatorService,
|
||||
FlatViewFieldValidatorService,
|
||||
FlatViewFilterValidatorService,
|
||||
FlatViewFilterGroupValidatorService,
|
||||
FlatViewGroupValidatorService,
|
||||
FlatIndexValidatorService,
|
||||
FlatFieldMetadataValidatorService,
|
||||
@@ -50,6 +52,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatViewValidatorService,
|
||||
FlatViewFieldValidatorService,
|
||||
FlatViewFilterValidatorService,
|
||||
FlatViewFilterGroupValidatorService,
|
||||
FlatViewGroupValidatorService,
|
||||
FlatIndexValidatorService,
|
||||
FlatFieldMetadataValidatorService,
|
||||
|
||||
+3
@@ -19,6 +19,7 @@ import { WorkspaceMigrationV2RowLevelPermissionPredicateActionsBuilderService }
|
||||
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/workspace-migration-v2-view-filter-group-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-group/workspace-migration-v2-view-group-actions-builder.service';
|
||||
import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service';
|
||||
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module';
|
||||
@@ -31,6 +32,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationV2ViewActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewFieldActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewFilterActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewFilterGroupActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewGroupActionsBuilderService,
|
||||
WorkspaceMigrationV2IndexActionsBuilderService,
|
||||
WorkspaceMigrationV2ServerlessFunctionActionsBuilderService,
|
||||
@@ -53,6 +55,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationV2ObjectActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewFieldActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewFilterActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewFilterGroupActionsBuilderService,
|
||||
WorkspaceMigrationV2ViewGroupActionsBuilderService,
|
||||
WorkspaceMigrationV2FieldActionsBuilderService,
|
||||
WorkspaceMigrationV2ServerlessFunctionActionsBuilderService,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type CreateViewFilterGroupAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/types/workspace-migration-view-filter-group-action-v2.type';
|
||||
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateViewFilterGroupActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'create_view_filter_group',
|
||||
) {
|
||||
optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateViewFilterGroupAction>): Partial<AllFlatEntityMaps> {
|
||||
const { flatViewFilterGroupMaps } = allFlatEntityMaps;
|
||||
const { viewFilterGroup } = action;
|
||||
|
||||
const updatedFlatViewFilterGroupMaps = addFlatEntityToFlatEntityMapsOrThrow(
|
||||
{
|
||||
flatEntity: viewFilterGroup,
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
flatViewFilterGroupMaps: updatedFlatViewFilterGroupMaps,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<CreateViewFilterGroupAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { viewFilterGroup } = action;
|
||||
|
||||
const viewFilterGroupRepository =
|
||||
queryRunner.manager.getRepository<ViewFilterGroupEntity>(
|
||||
ViewFilterGroupEntity,
|
||||
);
|
||||
|
||||
await viewFilterGroupRepository.insert({
|
||||
...viewFilterGroup,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<CreateViewFilterGroupAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type DeleteViewFilterGroupAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/types/workspace-migration-view-filter-group-action-v2.type';
|
||||
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteViewFilterGroupActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'delete_view_filter_group',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteViewFilterGroupAction>): Partial<AllFlatEntityMaps> {
|
||||
const { flatViewFilterGroupMaps } = allFlatEntityMaps;
|
||||
const { viewFilterGroupId } = action;
|
||||
|
||||
const updatedFlatViewFilterGroupMaps =
|
||||
deleteFlatEntityFromFlatEntityMapsOrThrow({
|
||||
entityToDeleteId: viewFilterGroupId,
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatViewFilterGroupMaps: updatedFlatViewFilterGroupMaps,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<DeleteViewFilterGroupAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { viewFilterGroupId } = action;
|
||||
|
||||
const viewFilterGroupRepository =
|
||||
queryRunner.manager.getRepository<ViewFilterGroupEntity>(
|
||||
ViewFilterGroupEntity,
|
||||
);
|
||||
|
||||
await viewFilterGroupRepository.delete({
|
||||
id: viewFilterGroupId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<DeleteViewFilterGroupAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
|
||||
WorkspaceMigrationRunnerActionHandler,
|
||||
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { type UpdateViewFilterGroupAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter-group/types/workspace-migration-view-filter-group-action-v2.type';
|
||||
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateViewFilterGroupActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update_view_filter_group',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
optimisticallyApplyActionOnAllFlatEntityMaps({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateViewFilterGroupAction>): Partial<AllFlatEntityMaps> {
|
||||
const { flatViewFilterGroupMaps } = allFlatEntityMaps;
|
||||
const { viewFilterGroupId } = action;
|
||||
|
||||
const existingViewFilterGroup = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: viewFilterGroupId,
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
const updatedViewFilterGroup = {
|
||||
...existingViewFilterGroup,
|
||||
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
|
||||
};
|
||||
|
||||
const updatedFlatViewFilterGroupMaps =
|
||||
replaceFlatEntityInFlatEntityMapsOrThrow({
|
||||
flatEntity: updatedViewFilterGroup,
|
||||
flatEntityMaps: flatViewFilterGroupMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatViewFilterGroupMaps: updatedFlatViewFilterGroupMaps,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UpdateViewFilterGroupAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner } = context;
|
||||
const { viewFilterGroupId } = action;
|
||||
|
||||
const viewFilterGroupRepository =
|
||||
queryRunner.manager.getRepository<ViewFilterGroupEntity>(
|
||||
ViewFilterGroupEntity,
|
||||
);
|
||||
|
||||
const update = fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action);
|
||||
|
||||
await viewFilterGroupRepository.update(viewFilterGroupId, update);
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<UpdateViewFilterGroupAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+7
@@ -51,6 +51,9 @@ import { UpdateViewFieldActionHandlerService } from 'src/engine/workspace-manage
|
||||
import { CreateViewFilterActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter/services/create-view-filter-action-handler.service';
|
||||
import { DeleteViewFilterActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter/services/delete-view-filter-action-handler.service';
|
||||
import { UpdateViewFilterActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter/services/update-view-filter-action-handler.service';
|
||||
import { CreateViewFilterGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter-group/services/create-view-filter-group-action-handler.service';
|
||||
import { DeleteViewFilterGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter-group/services/delete-view-filter-group-action-handler.service';
|
||||
import { UpdateViewFilterGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-filter-group/services/update-view-filter-group-action-handler.service';
|
||||
import { CreateViewGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-group/services/create-view-group-action-handler.service';
|
||||
import { DeleteViewGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-group/services/delete-view-group-action-handler.service';
|
||||
import { UpdateViewGroupActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-group/services/update-view-group-action-handler.service';
|
||||
@@ -84,6 +87,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
|
||||
UpdateViewFilterActionHandlerService,
|
||||
DeleteViewFilterActionHandlerService,
|
||||
|
||||
CreateViewFilterGroupActionHandlerService,
|
||||
UpdateViewFilterGroupActionHandlerService,
|
||||
DeleteViewFilterGroupActionHandlerService,
|
||||
|
||||
CreateViewGroupActionHandlerService,
|
||||
UpdateViewGroupActionHandlerService,
|
||||
DeleteViewGroupActionHandlerService,
|
||||
|
||||
Reference in New Issue
Block a user