Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/chatty-pens-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'contexture-elasticsearch': minor
'contexture-mongo': minor
'contexture-client': minor
'contexture': minor
---

Support sorting on multiple fields
1 change: 1 addition & 0 deletions packages/client/src/exampleTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export default F.stampKey('type', {
reactors: {
page: 'self',
pageSize: 'self',
sort: 'self',
sortField: 'self',
sortDir: 'self',
include: 'self',
Expand Down
10 changes: 4 additions & 6 deletions packages/export/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,10 @@ await csv(
- `include`: An array with the list of fields that will
be included on each retrieved record. This is relevant to the
`results` type. It's undefined by default (which is valid).
- `sortField`: Specifies what field will be used to sort the data.
This is relevant to the `results` type. It's undefined by default
(which is valid).
- `sortDir`: Specifies in which direction the data will be sorted
(`asc` or `desc`). This is relevant to the `results` type. It's
undefined by default (which is valid).
- `sort`: Specifies which fields will be used to sort the data.
This is relevant to the `results` type. It's undefined by default.
- `sortField`: Deprecated, use `sort`.
- `sortDir`: Deprecated, use `sort`.
- `pageSize`: It allows you to specify how many records per page
(per call of `getNext`) are returned. It defaults to 100.
- `page`: Indicates the starting page of the specified search.
Expand Down
19 changes: 17 additions & 2 deletions packages/provider-elasticsearch/src/example-types/results/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import F from 'futil'
import _ from 'lodash/fp.js'
import { getField } from '../../utils/fields.js'
import { searchWithHighlights } from './highlighting/search.js'

export let getSortParameter = ({ sort, sortField, sortDir }, schema) => {
if (!_.isEmpty(sort)) {
return Object.fromEntries(
sort.map(({ field, desc }) => [
getField(schema, field),
desc ? 'desc' : 'asc',
])
)
}
if (sortField) {
return { [getField(schema, sortField)]: sortDir || 'asc' }
}
return { _score: 'desc' }
}

export default {
validContext: () => true,
async result(node, search, schema) {
let page = (node.page || 1) - 1
let pageSize = node.pageSize || 10
let startRecord = page * pageSize
let sortField = node.sortField ? getField(schema, node.sortField) : '_score'

search = node.highlight?.disable
? search
Expand All @@ -18,7 +33,7 @@ export default {
F.omitBlank({
from: startRecord,
size: pageSize,
sort: { [sortField]: node.sortDir || 'desc' },
sort: getSortParameter(node, schema),
explain: node.explain,
// Without this, ES7+ stops counting at 10k instead of returning the actual count
track_total_hits: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest'
import { getSortParameter } from './index.js'

describe(getSortParameter, () => {
it('defaults to sorting on score', () => {
const actual = getSortParameter({})
const expected = { _score: 'desc' }
expect(actual).toEqual(expected)
})

it('sort on multiple fields', () => {
const actual = getSortParameter({
sort: [{ field: 'name' }, { field: 'age', desc: true }],
sortField: 'city',
sortDir: 'asc',
})
const expected = { name: 'asc', age: 'desc' }
expect(actual).toEqual(expected)
})

it('sort on multiple subfields', () => {
const actual = getSortParameter(
{
sort: [{ field: 'name' }, { field: 'age', desc: true }],
sortField: 'city',
sortDir: 'asc',
},
{
fields: {
name: { elasticsearch: { notAnalyzedField: 'keyword' } },
age: { elasticsearch: { notAnalyzedField: 'keyword' } },
},
}
)
const expected = { 'name.keyword': 'asc', 'age.keyword': 'desc' }
expect(actual).toEqual(expected)
})

it('legacy sort on single field', () => {
const actual = getSortParameter({
sortField: 'name',
sortDir: 'asc',
})
const expected = { name: 'asc' }
expect(actual).toEqual(expected)
})

it('legacy sort on single subfield', () => {
const actual = getSortParameter(
{
sortField: 'name',
sortDir: 'asc',
},
{
fields: {
name: { elasticsearch: { notAnalyzedField: 'keyword' } },
},
}
)
const expected = { 'name.keyword': 'asc' }
expect(actual).toEqual(expected)
})
})
43 changes: 29 additions & 14 deletions packages/provider-mongo/src/example-types/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,26 +112,40 @@ let projectFromInclude = (include) =>
_.countBy(_.identity)
)(include)

export let getSortStage = ({ sort, sortField, sortDir }) => {
if (!_.isEmpty(sort)) {
return [
{
$sort: Object.fromEntries(
sort.map(({ field, desc }) => [field, desc ? -1 : 1])
),
},
]
}
if (sortField) {
return [{ $sort: { [sortField]: sortDir === 'asc' ? 1 : -1 } }]
}
return []
}

let getResultsQuery = (node, getSchema, startRecord) => {
let { pageSize, sortField, sortDir, populate, include, skipCount } = node
let { pageSize, sortField, sort, populate, include, skipCount } = node

// $sort, $skip, $limit
let $sort = {
$sort: {
[sortField]: sortDir === 'asc' ? 1 : -1,
},
}
let $sort = getSortStage(node)

let $limit = { $limit: F.when(skipCount, _.add(1), pageSize) }
let sort = _.compact([sortField && $sort])
let skipLimit = _.compact([{ $skip: startRecord }, pageSize > 0 && $limit])
let sortSkipLimit = _.compact([...sort, ...skipLimit])
// If sort field is a join field move $sort, $skip, and $limit to after $lookup.
// Otherwise, place those stages first to take advantage of any indexes on that field.
let sortSkipLimit = _.compact([...$sort, ...skipLimit])
// If any sort fields is a join field move $sort, $skip, and $limit to after
// $lookup. Otherwise, place those stages first to take advantage of any
// indexes on the sort fields.
let sortOnJoinField = _.some((x) => {
let lookupField = _.getOr(x, `${x}.as`, populate)
return (
_.startsWith(`${lookupField}.`, sortField) || sortField === lookupField
return _.some(
({ field: sortField }) =>
_.startsWith(`${lookupField}.`, sortField) || sortField === lookupField,
sort ?? [{ field: sortField }]
)
}, _.keys(populate))
// check if any of the "populate" fields are indicating they can have more than one record
Expand All @@ -143,8 +157,9 @@ let getResultsQuery = (node, getSchema, startRecord) => {

return [
...(!sortOnJoinField && !hasMany ? sortSkipLimit : []),
// if "hasMany" is set on a "populate" field but we are not sorting on a "populate" field, sort as early as possible
...(hasMany && !sortOnJoinField ? sort : []),
// if "hasMany" is set on a "populate" field but we are not sorting on a
// "populate" field, sort as early as possible
...(hasMany && !sortOnJoinField ? $sort : []),
...convertPopulate(getSchema)(populate),
...(sortOnJoinField ? sortSkipLimit : []),
...(hasMany && !sortOnJoinField ? skipLimit : []),
Expand Down
23 changes: 22 additions & 1 deletion packages/provider-mongo/src/example-types/results.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import F from 'futil'
import result from './results.js'
import result, { getSortStage } from './results.js'
import { describe, expect, it } from 'vitest'

let {
Expand Down Expand Up @@ -549,3 +549,24 @@ describe('results', () => {
})
})
})

describe(getSortStage, () => {
it('sort on multiple fields', () => {
const actual = getSortStage({
sort: [{ field: 'name' }, { field: 'age', desc: true }],
sortField: 'city',
sortDir: 'asc',
})
const expected = [{ $sort: { name: 1, age: -1 } }]
expect(actual).toEqual(expected)
})

it('legacy sort on single field', () => {
const actual = getSortStage({
sortField: 'name',
sortDir: 'asc',
})
const expected = [{ $sort: { name: 1 } }]
expect(actual).toEqual(expected)
})
})
9 changes: 7 additions & 2 deletions packages/server/src/provider-memory/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import _ from 'lodash/fp.js'

export default {
result: (
{ pageSize = 10, page = 1, sortField, sortDir = 'desc' },
{ pageSize = 10, page = 1, sort, sortField, sortDir = 'desc' },
search
) => ({
totalRecords: search(_.size),
results: search(
_.flow(
_.orderBy(sortField, sortDir),
sort
? _.orderBy(
sort.map('field'),
sort.map(({ desc }) => (desc ? 'desc' : 'asc'))
)
: _.orderBy(sortField, sortDir),
pageSize > 0
? _.slice((page - 1) * pageSize, page * pageSize)
: _.identity
Expand Down
Loading