[GroupBy] Add views filters to groupBy query (#14762)
Closes https://github.com/twentyhq/core-team-issues/issues/1560 If viewId is defined in a groupBy query, we want to apply all filters of the view to the query. This required to move a lot of code from twenty-front to twenty-shared to convert the filters as stored in the db into graphql filters, applying the right combinations between filters etc., which was previously only done in the FE. This PR does not handle any field filters, it will be done in a later pr
This commit is contained in:
+2
@@ -22,6 +22,7 @@ import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/gra
|
||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
import { CoreViewModule } from 'src/engine/core-modules/view/view.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
@@ -52,6 +53,7 @@ const graphqlQueryResolvers = [
|
||||
TypeOrmModule.forFeature([RoleTargetsEntity]),
|
||||
UserRoleModule,
|
||||
ApiKeyModule,
|
||||
CoreViewModule,
|
||||
],
|
||||
providers: [
|
||||
ProcessNestedRelationsHelper,
|
||||
|
||||
+102
@@ -1,5 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CompositeFieldSubFieldName,
|
||||
RecordFilterGroupLogicalOperator,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
assertIsDefinedOrThrow,
|
||||
combineFilters,
|
||||
computeRecordGqlOperationFilter,
|
||||
convertViewFilterValueToString,
|
||||
getFilterTypeFromFieldType,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
GraphqlQueryBaseResolverService,
|
||||
GraphqlQueryResolverExecutionArgs,
|
||||
@@ -18,7 +30,11 @@ import {
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { convertViewFilterOperand } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-view-filter-operands';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
|
||||
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||
@@ -28,6 +44,13 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
|
||||
GroupByResolverArgs,
|
||||
IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[]
|
||||
> {
|
||||
constructor(
|
||||
private readonly viewFilterService: ViewFilterService,
|
||||
private readonly viewFilterGroupService: ViewFilterGroupService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<GroupByResolverArgs>,
|
||||
_featureFlagsMap: Record<FeatureFlagKey, boolean>,
|
||||
@@ -44,6 +67,14 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
|
||||
let appliedFilters =
|
||||
executionArgs.args.filter ?? ({} as ObjectRecordFilter);
|
||||
|
||||
if (executionArgs.args.viewId) {
|
||||
appliedFilters = await this.addFiltersFromView({
|
||||
executionArgs,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
appliedFilters,
|
||||
});
|
||||
}
|
||||
|
||||
executionArgs.graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
@@ -182,6 +213,77 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
|
||||
return groupByFields;
|
||||
}
|
||||
|
||||
private async addFiltersFromView({
|
||||
executionArgs,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
appliedFilters,
|
||||
}: {
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<GroupByResolverArgs>;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
appliedFilters: ObjectRecordFilter;
|
||||
}): Promise<ObjectRecordFilter> {
|
||||
assertIsDefinedOrThrow(executionArgs.args.viewId);
|
||||
|
||||
const workspaceId = executionArgs.options.authContext.workspace?.id;
|
||||
|
||||
assertIsDefinedOrThrow(workspaceId, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const viewFilters = await this.viewFilterService.findByViewId(
|
||||
workspaceId,
|
||||
executionArgs.args.viewId,
|
||||
);
|
||||
|
||||
const viewFilterGroups = await this.viewFilterGroupService.findByViewId(
|
||||
workspaceId,
|
||||
executionArgs.args.viewId,
|
||||
);
|
||||
|
||||
const recordFilters = viewFilters.map((viewFilter) => {
|
||||
const fieldMetadataItem =
|
||||
objectMetadataItemWithFieldMaps.fieldsById[viewFilter.fieldMetadataId];
|
||||
|
||||
return {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
value: convertViewFilterValueToString(viewFilter.value),
|
||||
type: getFilterTypeFromFieldType(fieldMetadataItem.type),
|
||||
operand: convertViewFilterOperand(viewFilter.operand),
|
||||
recordFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
|
||||
};
|
||||
});
|
||||
|
||||
const recordFilterGroups = viewFilterGroups.map((viewFilterGroup) => {
|
||||
return {
|
||||
id: viewFilterGroup.id,
|
||||
logicalOperator:
|
||||
viewFilterGroup.logicalOperator as unknown as RecordFilterGroupLogicalOperator, // TODO - https://github.com/twentyhq/twenty/issues/14746
|
||||
parentRecordFilterGroupId: viewFilterGroup.parentViewFilterGroupId,
|
||||
};
|
||||
});
|
||||
|
||||
const fields = Object.values(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
).map((field) => ({
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
label: field.label,
|
||||
}));
|
||||
|
||||
const filtersFromView = computeRecordGqlOperationFilter({
|
||||
recordFilters,
|
||||
recordFilterGroups: recordFilterGroups,
|
||||
fields,
|
||||
filterValueDependencies: {},
|
||||
});
|
||||
|
||||
appliedFilters = combineFilters([appliedFilters, filtersFromView]);
|
||||
|
||||
return appliedFilters;
|
||||
}
|
||||
|
||||
async validate(
|
||||
_args: GroupByResolverArgs<ObjectRecordFilter>,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { ViewFilterOperand as ViewFilterOperandShared } from 'twenty-shared/types';
|
||||
|
||||
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
|
||||
|
||||
export const convertViewFilterOperand = (
|
||||
operand: ViewFilterOperand,
|
||||
): ViewFilterOperandShared => {
|
||||
switch (operand) {
|
||||
case ViewFilterOperand.IS:
|
||||
return ViewFilterOperandShared.Is;
|
||||
case ViewFilterOperand.IS_NOT_NULL:
|
||||
return ViewFilterOperandShared.IsNotNull;
|
||||
case ViewFilterOperand.IS_NOT:
|
||||
return ViewFilterOperandShared.IsNot;
|
||||
case ViewFilterOperand.LESS_THAN_OR_EQUAL:
|
||||
return ViewFilterOperandShared.LessThanOrEqual;
|
||||
case ViewFilterOperand.GREATER_THAN_OR_EQUAL:
|
||||
return ViewFilterOperandShared.GreaterThanOrEqual;
|
||||
case ViewFilterOperand.IS_BEFORE:
|
||||
return ViewFilterOperandShared.IsBefore;
|
||||
case ViewFilterOperand.IS_AFTER:
|
||||
return ViewFilterOperandShared.IsAfter;
|
||||
case ViewFilterOperand.CONTAINS:
|
||||
return ViewFilterOperandShared.Contains;
|
||||
case ViewFilterOperand.DOES_NOT_CONTAIN:
|
||||
return ViewFilterOperandShared.DoesNotContain;
|
||||
case ViewFilterOperand.IS_EMPTY:
|
||||
return ViewFilterOperandShared.IsEmpty;
|
||||
case ViewFilterOperand.IS_NOT_EMPTY:
|
||||
return ViewFilterOperandShared.IsNotEmpty;
|
||||
case ViewFilterOperand.IS_RELATIVE:
|
||||
return ViewFilterOperandShared.IsRelative;
|
||||
case ViewFilterOperand.IS_IN_PAST:
|
||||
return ViewFilterOperandShared.IsInPast;
|
||||
case ViewFilterOperand.IS_IN_FUTURE:
|
||||
return ViewFilterOperandShared.IsInFuture;
|
||||
case ViewFilterOperand.IS_TODAY:
|
||||
return ViewFilterOperandShared.IsToday;
|
||||
case ViewFilterOperand.VECTOR_SEARCH:
|
||||
return ViewFilterOperandShared.VectorSearch;
|
||||
default:
|
||||
throw new Error(`Invalid view filter operand: ${operand}`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user