From ad8830ecbf2f5e512f843f5b7162c8cb8916692a Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 3 Aug 2026 10:24:44 +0200 Subject: [PATCH] fix(server): stop FIND_RECORDS from silently ignoring its filter (#23640) ## Problem A FIND_RECORDS workflow step with a filter configured could silently return every record (and thus the first row of the table) instead of applying the filter. Ways to hit it: - `recordFilters` set but `recordFilterGroups` omitted (e.g. an API/agent caller, or any non-UI config). - A grouped filter (carrying `recordFilterGroupId`) whose `recordFilterGroups` is missing. - A filter referencing an unknown `fieldMetadataId` or an unresolvable relation (`turnRecordFilterIntoRecordGqlOperationFilter` returns `undefined`, silently dropped). - `gqlOperationFilter` set, which passed validation but was never read. In every case the computed filter collapses to `{}`, `FindRecordsService` returns all records ordered by `id ASC`, and `records[0]` is the first row. This is fail-open: the step reports success and returns wrong records rather than erroring. ## Fix - Compute the filter whenever `recordFilters` is non-empty, defaulting `recordFilterGroups` to `[]`. `computeRecordGqlOperationFilter` handles ungrouped filters independently of groups. - **Fail closed**: if `recordFilters` is non-empty but the computed `gqlOperationFilter` is empty, throw `INVALID_STEP_INPUT` instead of running an unfiltered query. This covers grouped-without-groups and unknown-field/unresolvable-relation cases raised in review. An absent or empty `recordFilters` still legitimately means "find all". - Remove the unused `gqlOperationFilter` field from the find-records input type and settings schema so it is no longer advertised as a filter option (it has been dead since #16147, when the action moved to computing the filter at runtime from `recordFilters`). --- .../types/record-crud-input.type.ts | 6 +-- .../find-records.workflow-action.ts | 38 ++++++++++++------- .../find-records-action-settings-schema.ts | 1 - 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts index 91ba7af519..496ec7a3e4 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts @@ -1,7 +1,4 @@ -import { - type ObjectRecordFilter, - type ObjectRecordOrderBy, -} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { type ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { type ObjectRecordProperties } from './object-record-properties.type'; @@ -30,7 +27,6 @@ export type FindRecordsInput = { recordFilterGroups?: any; // oxlint-disable-next-line typescript/no-explicit-any recordFilters?: any; - gqlOperationFilter?: Partial[]; }; orderBy?: { // oxlint-disable-next-line typescript/no-explicit-any diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 04bf853525..5c7580b665 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common'; import { computeRecordGqlOperationFilter, isDefined, + isEmptyObject, + isNonEmptyArray, isRecordFilterValueValid, resolveInput, } from 'twenty-shared/utils'; @@ -75,20 +77,28 @@ export class FindRecordsWorkflowAction implements WorkflowAction { } } - const gqlOperationFilter = - workflowActionInput.filter?.recordFilters && - workflowActionInput.filter?.recordFilterGroups - ? computeRecordGqlOperationFilter({ - fieldMetadataItems: Object.values( - flatFieldMetadataMaps.byUniversalIdentifier, - ).filter(isDefined), - recordFilters: workflowActionInput.filter.recordFilters, - recordFilterGroups: workflowActionInput.filter.recordFilterGroups, - filterValueDependencies: { - timeZone: 'UTC', - }, - }) - : {}; + const recordFilters = workflowActionInput.filter?.recordFilters; + + const gqlOperationFilter = isDefined(recordFilters) + ? computeRecordGqlOperationFilter({ + fieldMetadataItems: Object.values( + flatFieldMetadataMaps.byUniversalIdentifier, + ).filter(isDefined), + recordFilters, + recordFilterGroups: + workflowActionInput.filter?.recordFilterGroups ?? [], + filterValueDependencies: { + timeZone: 'UTC', + }, + }) + : {}; + + if (isNonEmptyArray(recordFilters) && isEmptyObject(gqlOperationFilter)) { + throw new WorkflowStepExecutorException( + 'Filter could not be resolved to a valid query. Check that filtered fields exist and that grouped filters include their recordFilterGroups.', + WorkflowStepExecutorExceptionCode.INVALID_STEP_INPUT, + ); + } const toolOutput = await this.findRecordsService.execute({ objectName: workflowActionInput.objectName, diff --git a/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts b/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts index 0dd390e5bb..1145daccf4 100644 --- a/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts +++ b/packages/twenty-shared/src/workflow/schemas/find-records-action-settings-schema.ts @@ -11,7 +11,6 @@ export const workflowFindRecordsActionSettingsSchema = .object({ recordFilterGroups: z.array(z.any()).optional(), recordFilters: z.array(z.any()).optional(), - gqlOperationFilter: z.any().optional().nullable(), }) .optional(), orderBy: z