fix(filters): unify combinedFilter for queries and bulk delete (#13952)

Issue: https://github.com/twentyhq/twenty/issues/13913

Motivation/Problem:
Bulk delete and query paths were composing filters differently, causing
mismatches. In some cases this led to invalid or empty GraphQL filters
(e.g. {"and":[{}]}), breaking delete operations.

Fix:
Unify filter composition (combinedFilter) across queries and bulk
delete, ensuring consistent handling of base filters + soft-deleted
clause. Also adjusted record filter grouping logic to avoid dropping
filters when no groups exist.

Result:
Filtered queries and bulk deletes now behave consistently and reliably
without producing broken filters.

---------

Co-authored-by: root <root@DESKTOP-E2VOJGE>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Puranjay Mishra
2025-08-18 06:01:47 -04:00
committed by GitHub
parent 1598e5590a
commit 483c1e2214
16 changed files with 185 additions and 53 deletions
@@ -15,6 +15,8 @@ import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { type OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
import { type RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
RecordGqlOperationVariables & {
onError?: (error?: Error) => void;
@@ -53,9 +55,19 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
handleError: onError,
});
const softDeleteFilter: RecordGqlOperationFilter = {
or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }],
};
const withSoftDeleteFilter = withSoftDeleted
? {
and: [...(filter ? [filter] : []), softDeleteFilter],
}
: filter;
const queryIdentifier = getQueryIdentifier({
objectNameSingular,
filter,
filter: withSoftDeleteFilter,
orderBy,
limit,
});
@@ -66,10 +78,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onCompleted,
});
const withSoftDeleterFilter = {
or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }],
};
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
@@ -80,14 +88,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem || !hasReadPermission,
variables: {
filter: withSoftDeleted
? {
and: [
...(filter ? [filter] : []),
...(withSoftDeleted ? [withSoftDeleterFilter] : []),
],
}
: filter,
filter: withSoftDeleteFilter,
orderBy,
lastCursor: cursorFilter?.cursor ?? undefined,
limit,
@@ -101,7 +102,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const { fetchMoreRecords, records, hasNextPage } =
useFetchMoreRecordsWithPagination<T>({
objectNameSingular,
filter,
filter: withSoftDeleteFilter,
orderBy,
limit,
fetchMore,