Skip to content

Conversation

@abdulrahmancodes
Copy link
Contributor

@abdulrahmancodes abdulrahmancodes commented Dec 15, 2025

Closes #6344

Issues Fixed

  1. Cursor encoding was broken: The frontend only requested the id field in recordGqlFields, but cursor encoding requires all orderBy field values.

  2. Cursor encoding was broken. The frontend only requested the id field in recordGqlFields for pagination queries, but cursor encoding requires all orderBy field values to generate valid cursors.

  3. Navigation buttons were not disabled when navigation was unavailable. The next/previous record navigation buttons remained enabled even when recordAfter/recordBefore was not available, leading to a poor user experience where clicking the buttons would have no effect.

Changes

  • Updated buildColumnsToSelect to automatically include orderBy fields in the SELECT statement:

    • Extract column names from orderBy (including composite field sub-columns)
    • Add these columns to the select clause so cursor encoding has access to all required fields, even if not explicitly requested by the frontend
  • Updated buildCursorCompositeFieldWhereCondition to properly handle null composite field values:

    • Use is: 'NULL' instead of eq: null for equality conditions
    • Use is: 'NOT NULL' or is: 'NULL' for comparison conditions when all sub-fields are null
    • Include NULL values in comparison filters based on sort order (NullsFirst/NullsLast) and pagination direction
    • Filter out null values from cursor entries to avoid invalid SQL comparisons
  • Added disabled state support to Action component system:

    • Updated Action, ActionDisplay, ActionButton, ActionListItem, and ActionDropdownItem to accept and handle disabled prop
    • Updated NavigateToNextRecordSingleRecordAction and NavigateToPreviousRecordSingleRecordAction to use canNavigateToNextRecord and canNavigateToPreviousRecord flags to disable buttons when navigation is unavailable

Known Limitations

Navigation with Null Composite Field Values:

Navigation backward when records have null composite field values (e.g., amount) may skip records in certain scenarios. This occurs when:

  • Records are sorted by a composite field with AscNullsLast ordering
  • Navigating backward from a record with a value to a record with a null value
  • The filter logic for handling nulls in cursor-based pagination needs further refinement

Multiple approaches were attempted to fix this issue (including explicit IS NOT NULL checks, restructuring OR conditions, and separate null handling), but none fully resolved the skipping behavior without introducing other issues. This is a known limitation that requires further investigation. The current implementation handles most cases but may skip intermediate records in specific scenarios involving null composite field values.

…on logic

- Updated CommonFindManyQueryRunnerService to include orderBy argument.
- Enhanced buildColumnsToSelect function to process orderBy input and extract relevant column names.
- Introduced extractColumnNamesFromOrderBy utility to handle orderBy logic for composite fields.
- Introduced functions to build filters for null values in composite fields.
- Enhanced the main filter function to handle cases where all subfields are null.
- Added logic to include nulls in comparison based on order direction and computed operator.
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@abdulrahmancodes abdulrahmancodes marked this pull request as draft December 15, 2025 09:39
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Prompt for AI agents (all 1 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts">

<violation number="1" location="packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts:64">
P1: The `buildAllNullComparisonFilter` function returns `{ is: &#39;NULL&#39; }` for the `lt` operator, which is incorrect. When the cursor is at a null value and pagination goes &#39;less than&#39;, the correct filter depends on NullsFirst/NullsLast ordering:
- NullsFirst: nothing comes before nulls → return empty filter or no matches
- NullsLast: non-nulls come before nulls → return `{ is: &#39;NOT NULL&#39; }`

The function needs `orderByDirection` passed to it (not just `computedOperator`) to handle this correctly.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

…elds

- Updated test cases for computeCursorArgFilter to include null handling for firstName and lastName.
- Modified filter logic to allow comparisons with null values alongside existing conditions.
- Added a `disabled` prop to Action, ActionButton, ActionDisplay, ActionListItem, and ActionDropdownItem components to prevent interaction when set to true.
- Updated NavigateToNextRecordSingleRecordAction and NavigateToPreviousRecordSingleRecordAction to utilize the new disabled state based on navigation availability.
- Reformatted ActionButton and ActionListItem components for better code clarity by breaking props into multiple lines.
- No functional changes were made; this is purely a formatting update.
…include null checks

- Modified test snapshots to incorporate 'or' conditions for various address and name fields, allowing for comparisons with null values.
- Ensured that all relevant properties now handle both greater than and null conditions in the test cases.
…ll handling

- Updated test cases to include comprehensive checks for null values in orderBy conditions.
- Added validation for orderBy structure and direction, ensuring robust error handling.
- Enhanced assertions to accommodate both 'or' and 'and' conditions in the test logic.
@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2025

📊 API Changes Report

GraphQL Schema Changes

GraphQL Schema Changes

[error] Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-schema-introspection.json: Not valid JSON content
at JsonFileLoader.handleFileContent (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:147:19)
at /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:95:43
at async Promise.all (index 0)
at async JsonFileLoader.load (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:88:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:15:39
at async Promise.all (index 4)
at async loadFile (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:13:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/collect-sources.js:200:25
Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-schema-introspection.json: Not valid JSON content
at JsonFileLoader.handleFileContent (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:147:19)
at /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:95:43
at async Promise.all (index 0)
at async JsonFileLoader.load (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:88:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:15:39
at async Promise.all (index 4)
at async loadFile (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:13:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/collect-sources.js:200:25
⚠️ Breaking changes or errors detected in GraphQL schema

[error] Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-schema-introspection.json: Not valid JSON content
    at JsonFileLoader.handleFileContent (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:147:19)
    at /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:95:43
    at async Promise.all (index 0)
    at async JsonFileLoader.load (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:88:9)
    at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:15:39
    at async Promise.all (index 4)
    at async loadFile (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:13:9)
    at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/collect-sources.js:200:25
Error generating diff

GraphQL Metadata Schema Changes

GraphQL Metadata Schema Changes

[error] Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-metadata-schema-introspection.json: Not valid JSON content
at JsonFileLoader.handleFileContent (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:147:19)
at /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:95:43
at async Promise.all (index 0)
at async JsonFileLoader.load (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:88:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:15:39
at async Promise.all (index 4)
at async loadFile (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:13:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/collect-sources.js:200:25
Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-metadata-schema-introspection.json: Not valid JSON content
at JsonFileLoader.handleFileContent (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:147:19)
at /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:95:43
at async Promise.all (index 0)
at async JsonFileLoader.load (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:88:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:15:39
at async Promise.all (index 4)
at async loadFile (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:13:9)
at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/collect-sources.js:200:25
⚠️ Breaking changes or errors detected in GraphQL metadata schema

[error] Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-metadata-schema-introspection.json: Not valid JSON content
    at JsonFileLoader.handleFileContent (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:147:19)
    at /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:95:43
    at async Promise.all (index 0)
    at async JsonFileLoader.load (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/json-file-loader/cjs/index.js:88:9)
    at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:15:39
    at async Promise.all (index 4)
    at async loadFile (/opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/load-file.js:13:9)
    at async /opt/hostedtoolcache/node/24.12.0/x64/lib/node_modules/@graphql-inspector/cli/node_modules/@graphql-tools/load/cjs/load-typedefs/collect-sources.js:200:25
Error generating diff

REST API Analysis Error

⚠️ Error occurred while analyzing REST API changes

Error Output

REST Metadata API Analysis Error

⚠️ Error occurred while analyzing REST Metadata API changes

Error Output

⚠️ Please review these API changes carefully before merging.

⚠️ Breaking Change Protocol

Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.

🔄 Options:

  1. If this IS a breaking change: Add "breaking" to your PR title and add BREAKING CHANGE: to your commit message
  2. If this is NOT a breaking change: The API diff tool may have false positives - please review carefully

For breaking changes, add to commit message:

feat: add new API endpoint

BREAKING CHANGE: removed deprecated field from User schema

- Added orderByDirection parameter to improve handling of null values in comparisons.
- Updated logic to return an empty object when nulls should be prioritized based on order direction.
- Adjusted conditions for 'lt' operator to ensure correct handling of NOT NULL comparisons.
…ition logic

- Introduced isEmptyFilter function to check for empty filters.
- Updated buildCursorCumulativeWhereCondition to utilize isEmptyFilter for cleaner condition handling.
- Enhanced filtering of null conditions in the final output, improving overall performance and readability.
@abdulrahmancodes abdulrahmancodes marked this pull request as ready for review December 17, 2025 19:10
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 14 files

Prompt for AI agents (all 4 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts">

<violation number="1" location="packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts:57">
P1: The `gt` operator case doesn&#39;t check null ordering and always returns `NOT NULL`. For NULLS LAST ordering with forward pagination from a null cursor, there are no records after null, so it should return `{}` (empty filter). Currently it returns `NOT NULL`, which would incorrectly fetch records that appear before the null cursor.</violation>

<violation number="2" location="packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts:192">
P2: Filtering null values from `cursorEntries` causes equality conditions to miss `IS NULL` checks for partially null composite fields. For a cursor like `{firstName: &#39;John&#39;, lastName: null}`, the equality filter won&#39;t include a check for `lastName IS NULL`, potentially matching incorrect records.</violation>
</file>

<file name="packages/twenty-server/src/engine/api/utils/build-cursor-cumulative-where-conditions.utils.ts">

<violation number="1" location="packages/twenty-server/src/engine/api/utils/build-cursor-cumulative-where-conditions.utils.ts:77">
P3: Unreachable code: `andConditions.length === 0` can never be true at this point because `mainCondition` was just pushed to `andConditions` on line 74 (we only reach this code if `mainCondition` is not empty). This check should be removed.</violation>
</file>

<file name="packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts">

<violation number="1" location="packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts:256">
P1: The condition `isDefined(direction)` is too broad and will match string enum values like `AscNullsLast` or `DescNullsLast`. When `Object.entries()` is called on a string, it iterates over characters, producing incorrect results. Use `typeof direction === &#39;object&#39; &amp;&amp; direction !== null` to properly detect composite fields.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

return null;
}

if (isNull(cursorValue[property.name])) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Filtering null values from cursorEntries causes equality conditions to miss IS NULL checks for partially null composite fields. For a cursor like {firstName: 'John', lastName: null}, the equality filter won't include a check for lastName IS NULL, potentially matching incorrect records.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts, line 192:

<comment>Filtering null values from `cursorEntries` causes equality conditions to miss `IS NULL` checks for partially null composite fields. For a cursor like `{firstName: &#39;John&#39;, lastName: null}`, the equality filter won&#39;t include a check for `lastName IS NULL`, potentially matching incorrect records.</comment>

<file context>
@@ -64,36 +156,62 @@ export const buildCursorCompositeFieldWhereCondition = ({
         return null;
       }
 
+      if (isNull(cursorValue[property.name])) {
+        return null;
+      }
</file context>
Fix with Cubic

Comment on lines +57 to +63
if (computedOperator === 'gt') {
return {
[fieldKey]: {
[firstProperty.name]: { is: 'NOT NULL' },
},
};
}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The gt operator case doesn't check null ordering and always returns NOT NULL. For NULLS LAST ordering with forward pagination from a null cursor, there are no records after null, so it should return {} (empty filter). Currently it returns NOT NULL, which would incorrectly fetch records that appear before the null cursor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/api/utils/build-cursor-composite-field-where-condition.utils.ts, line 57:

<comment>The `gt` operator case doesn&#39;t check null ordering and always returns `NOT NULL`. For NULLS LAST ordering with forward pagination from a null cursor, there are no records after null, so it should return `{}` (empty filter). Currently it returns `NOT NULL`, which would incorrectly fetch records that appear before the null cursor.</comment>

<file context>
@@ -29,6 +31,96 @@ type BuildCursorCompositeFieldWhereConditionParams = {
+  computedOperator: string,
+  orderByDirection: OrderByDirection,
+): ObjectRecordFilter | Record&lt;string, never&gt; =&gt; {
+  if (computedOperator === &#39;gt&#39;) {
+    return {
+      [fieldKey]: {
</file context>
Suggested change
if (computedOperator === 'gt') {
return {
[fieldKey]: {
[firstProperty.name]: { is: 'NOT NULL' },
},
};
}
if (computedOperator === 'gt') {
const isNullsLast =
orderByDirection === OrderByDirection.AscNullsLast ||
orderByDirection === OrderByDirection.DescNullsLast;
if (isNullsLast) {
return {};
}
return {
[fieldKey]: {
[firstProperty.name]: { is: 'NOT NULL' },
},
};
}
Fix with Cubic


andConditions.push(mainCondition);

if (andConditions.length === 0) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Unreachable code: andConditions.length === 0 can never be true at this point because mainCondition was just pushed to andConditions on line 74 (we only reach this code if mainCondition is not empty). This check should be removed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/api/utils/build-cursor-cumulative-where-conditions.utils.ts, line 77:

<comment>Unreachable code: `andConditions.length === 0` can never be true at this point because `mainCondition` was just pushed to `andConditions` on line 74 (we only reach this code if `mainCondition` is not empty). This check should be removed.</comment>

<file context>
@@ -34,41 +38,53 @@ export const buildCursorCumulativeWhereCondition = &lt;
+
+      andConditions.push(mainCondition);
+
+      if (andConditions.length === 0) {
+        return null;
+      }
</file context>
Fix with Cubic

- Added support for composite field metadata types in the column selection process.
- Introduced a utility function to compute composite column names based on subfield properties.
- Updated the logic to handle cases where composite types are not defined, ensuring robustness in column name extraction.
@abdulrahmancodes abdulrahmancodes force-pushed the fix/cursor-pagination-order branch from 54f83a8 to 006fa43 Compare December 18, 2025 02:54
@lucasbordeau
Copy link
Contributor

lucasbordeau commented Jan 14, 2026

Hi @abdulrahmancodes sorry for the delay.

My main concern while reviewing this PR is that the filter built from the cursor looks odd.

For example, if I navigate from a simple view to a show page, I should have indeed a request to find the next and previous record id, by using cursor after and before with limit 1, respectively.

But if I look at the generated filters for this I end up with something like that :

[
  {
    "position": {
      "lt": 22
    }
  },
  {
    "and": [
      {
        "position": {
          "eq": 22
        }
      },
      {
        "id": {
          "lt": "50505050-0016-4e7c-8001-123456789abc"
        }
      }
    ]
  }
]

The problem is that a eq operator on the position field inside an and makes the other filters in this and useless. Moreover in this precise example "50505050-0016-4e7c-8001-123456789abc" is the record that has position = 22 so the second filter cannot return any data from an SQL request.

It works because the first filter is ok, but the second being an impossible filter, it will create technical debt and lead to bugs.

I don't know if this comes from your PR originally but since we're working on this part of the code it is the best PR to fix this problem.

Copy link
Contributor

@lucasbordeau lucasbordeau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Order by currency bug with cursor

4 participants