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,88 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ObjectRecord,
|
||||
ObjectRecordOrderBy,
|
||||
ObjectRecordOrderByForCompositeField,
|
||||
ObjectRecordOrderByForScalarField,
|
||||
OrderByDirection,
|
||||
} 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';
|
||||
|
||||
const isOrderByDirection = (value: unknown): value is OrderByDirection => {
|
||||
return Object.values(OrderByDirection).includes(value as OrderByDirection);
|
||||
};
|
||||
|
||||
const isOrderByForScalarField = (
|
||||
orderByLeaf: Record<string, unknown>,
|
||||
key: keyof ObjectRecord,
|
||||
): orderByLeaf is ObjectRecordOrderByForScalarField => {
|
||||
const value = orderByLeaf[key as string];
|
||||
|
||||
return isDefined(value) && isOrderByDirection(value);
|
||||
};
|
||||
|
||||
const isOrderByForCompositeField = (
|
||||
orderByLeaf: Record<string, unknown>,
|
||||
key: keyof ObjectRecord,
|
||||
): orderByLeaf is ObjectRecordOrderByForCompositeField => {
|
||||
const value = orderByLeaf[key as string];
|
||||
|
||||
return (
|
||||
isDefined(value) &&
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!isOrderByDirection(value) &&
|
||||
Object.values(value as Record<string, unknown>).every(isOrderByDirection)
|
||||
);
|
||||
};
|
||||
|
||||
export const validateAndGetOrderByForScalarField = (
|
||||
key: keyof ObjectRecord,
|
||||
orderBy: ObjectRecordOrderBy,
|
||||
): ObjectRecordOrderByForScalarField => {
|
||||
const keyOrderBy = orderBy.find((order) => key in order);
|
||||
|
||||
if (!isDefined(keyOrderBy)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Invalid cursor',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOrderByForScalarField(keyOrderBy, key)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Expected non-composite field order by',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
|
||||
return keyOrderBy;
|
||||
};
|
||||
|
||||
export const validateAndGetOrderByForCompositeField = (
|
||||
key: keyof ObjectRecord,
|
||||
orderBy: ObjectRecordOrderBy,
|
||||
): ObjectRecordOrderByForCompositeField => {
|
||||
const keyOrderBy = orderBy.find((order) => key in order);
|
||||
|
||||
if (!isDefined(keyOrderBy)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Invalid cursor',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOrderByForCompositeField(keyOrderBy, key)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Expected composite field order by',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
|
||||
);
|
||||
}
|
||||
|
||||
return keyOrderBy;
|
||||
};
|
||||
Reference in New Issue
Block a user