forked from twentyhq/twenty
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix sort with pagination and composite fields (twentyhq#9150)
Fixes twentyhq#8863 ## Description This PR fixes an issue with cursor-based pagination when dealing with composite fields (like `fullName`). Previously, the pagination direction was incorrectly determined for composite fields because the code wasn't properly handling nested object structures in the `orderBy` parameter. Refactored the code accordingly.
- Loading branch information
1 parent
9db9b87
commit 30fe169
Showing
4 changed files
with
228 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
...engine/api/graphql/graphql-query-runner/utils/__tests__/compute-cursor-arg-filter.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; | ||
|
||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; | ||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||
|
||
describe('computeCursorArgFilter', () => { | ||
const mockFieldMetadataMap = { | ||
name: { | ||
type: FieldMetadataType.TEXT, | ||
id: 'name-id', | ||
name: 'name', | ||
label: 'Name', | ||
objectMetadataId: 'object-id', | ||
}, | ||
age: { | ||
type: FieldMetadataType.NUMBER, | ||
id: 'age-id', | ||
name: 'age', | ||
label: 'Age', | ||
objectMetadataId: 'object-id', | ||
}, | ||
fullName: { | ||
type: FieldMetadataType.FULL_NAME, | ||
id: 'fullname-id', | ||
name: 'fullName', | ||
label: 'Full Name', | ||
objectMetadataId: 'object-id', | ||
}, | ||
}; | ||
|
||
describe('basic cursor filtering', () => { | ||
it('should return empty array when cursor is empty', () => { | ||
const result = computeCursorArgFilter({}, [], mockFieldMetadataMap, true); | ||
|
||
expect(result).toEqual([]); | ||
}); | ||
|
||
it('should compute forward pagination filter for single field', () => { | ||
const cursor = { name: 'John' }; | ||
const orderBy = [{ name: OrderByDirection.AscNullsLast }]; | ||
|
||
const result = computeCursorArgFilter( | ||
cursor, | ||
orderBy, | ||
mockFieldMetadataMap, | ||
true, | ||
); | ||
|
||
expect(result).toEqual([{ name: { gt: 'John' } }]); | ||
}); | ||
|
||
it('should compute backward pagination filter for single field', () => { | ||
const cursor = { name: 'John' }; | ||
const orderBy = [{ name: OrderByDirection.AscNullsLast }]; | ||
|
||
const result = computeCursorArgFilter( | ||
cursor, | ||
orderBy, | ||
mockFieldMetadataMap, | ||
false, | ||
); | ||
|
||
expect(result).toEqual([{ name: { lt: 'John' } }]); | ||
}); | ||
}); | ||
|
||
describe('multiple fields cursor filtering', () => { | ||
it('should handle multiple cursor fields with forward pagination', () => { | ||
const cursor = { name: 'John', age: 30 }; | ||
const orderBy = [ | ||
{ name: OrderByDirection.AscNullsLast }, | ||
{ age: OrderByDirection.DescNullsLast }, | ||
]; | ||
|
||
const result = computeCursorArgFilter( | ||
cursor, | ||
orderBy, | ||
mockFieldMetadataMap, | ||
true, | ||
); | ||
|
||
expect(result).toEqual([ | ||
{ name: { gt: 'John' } }, | ||
{ name: { eq: 'John' }, age: { lt: 30 } }, | ||
]); | ||
}); | ||
}); | ||
|
||
describe('composite field handling', () => { | ||
it('should handle fullName composite field', () => { | ||
const cursor = { | ||
fullName: { firstName: 'John', lastName: 'Doe' }, | ||
}; | ||
const orderBy = [ | ||
{ | ||
fullName: { | ||
firstName: OrderByDirection.AscNullsLast, | ||
lastName: OrderByDirection.AscNullsLast, | ||
}, | ||
}, | ||
]; | ||
|
||
const result = computeCursorArgFilter( | ||
cursor, | ||
orderBy, | ||
mockFieldMetadataMap, | ||
true, | ||
); | ||
|
||
expect(result).toEqual([ | ||
{ | ||
fullName: { | ||
firstName: { gt: 'John' }, | ||
lastName: { gt: 'Doe' }, | ||
}, | ||
}, | ||
]); | ||
}); | ||
}); | ||
|
||
describe('error handling', () => { | ||
it('should throw error for invalid field metadata', () => { | ||
const cursor = { invalidField: 'value' }; | ||
const orderBy = [{ invalidField: OrderByDirection.AscNullsLast }]; | ||
|
||
expect(() => | ||
computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true), | ||
).toThrow(GraphqlQueryRunnerException); | ||
}); | ||
|
||
it('should throw error for missing orderBy entry', () => { | ||
const cursor = { name: 'John' }; | ||
const orderBy = [{ age: OrderByDirection.AscNullsLast }]; | ||
|
||
expect(() => | ||
computeCursorArgFilter(cursor, orderBy, mockFieldMetadataMap, true), | ||
).toThrow(GraphqlQueryRunnerException); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters