Fix cursor-based pagination with lexicographic ordering for composite fields (#12467)
# Fix cursor-based pagination with lexicographic ordering for composite
fields
## Bug
The existing cursor-based pagination implementation had a bug when
handling composite fields.
When paginating through results sorted by composite fields (like
`fullName` with sub-properties `firstName` and`lastName`), the WHERE
conditions generated for cursor positioning were incorrect, leading to
records being skipped.
The previous implementation was generating wrong WHERE conditions:
For example, when paginating with a cursor like `{ firstName: 'John',
lastName: 'Doe' }`, it would generate:
```sql
WHERE firstName > 'John' AND lastName > 'Doe'
```
This is incorrect because it would miss records like `{ firstName:
'John', lastName: 'Smith' }` which should be included in forward
pagination.
## Fix
Create a new util to use proper lexicographic order when sorting a
composite field.
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ObjectRecord,
|
||||
ObjectRecordCursorLeafCompositeValue,
|
||||
ObjectRecordCursorLeafScalarValue,
|
||||
ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { computeOperator } from 'src/engine/api/utils/compute-operator.utils';
|
||||
import { isAscendingOrder } from 'src/engine/api/utils/is-ascending-order.utils';
|
||||
import { validateAndGetOrderByForScalarField } from 'src/engine/api/utils/validate-and-get-order-by.utils';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import { buildCursorCompositeFieldWhereCondition } from 'src/engine/api/utils/build-cursor-composite-field-where-condition.utils';
|
||||
|
||||
type BuildCursorWhereConditionParams = {
|
||||
cursorKey: keyof ObjectRecord;
|
||||
cursorValue:
|
||||
| ObjectRecordCursorLeafScalarValue
|
||||
| ObjectRecordCursorLeafCompositeValue;
|
||||
fieldMetadataMapByName: FieldMetadataMap;
|
||||
orderBy: ObjectRecordOrderBy;
|
||||
isForwardPagination: boolean;
|
||||
isEqualityCondition?: boolean;
|
||||
};
|
||||
|
||||
export const buildCursorWhereCondition = ({
|
||||
cursorKey,
|
||||
cursorValue,
|
||||
fieldMetadataMapByName,
|
||||
orderBy,
|
||||
isForwardPagination,
|
||||
isEqualityCondition = false,
|
||||
}: BuildCursorWhereConditionParams): Record<string, unknown> => {
|
||||
const fieldMetadata = fieldMetadataMapByName[cursorKey];
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Field metadata not found for key: ${cursorKey}`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return buildCursorCompositeFieldWhereCondition({
|
||||
fieldType: fieldMetadata.type,
|
||||
fieldKey: cursorKey,
|
||||
orderBy,
|
||||
cursorValue: cursorValue as ObjectRecordCursorLeafCompositeValue,
|
||||
isForwardPagination,
|
||||
isEqualityCondition,
|
||||
});
|
||||
}
|
||||
|
||||
if (isEqualityCondition) {
|
||||
return { [cursorKey]: { eq: cursorValue } };
|
||||
}
|
||||
|
||||
const keyOrderBy = validateAndGetOrderByForScalarField(cursorKey, orderBy);
|
||||
const orderByDirection = keyOrderBy[cursorKey];
|
||||
|
||||
if (!isDefined(orderByDirection)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Invalid cursor',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
|
||||
const isAscending = isAscendingOrder(orderByDirection);
|
||||
const computedOperator = computeOperator(isAscending, isForwardPagination);
|
||||
|
||||
return { [cursorKey]: { [computedOperator]: cursorValue } };
|
||||
};
|
||||
Reference in New Issue
Block a user