Skip to content

Commit

Permalink
Support paging for sorted queries
Browse files Browse the repository at this point in the history
  • Loading branch information
kjellmorten committed Apr 1, 2018
1 parent 9909464 commit 5f2d571
Show file tree
Hide file tree
Showing 6 changed files with 569 additions and 114 deletions.
143 changes: 143 additions & 0 deletions lib/adapter/createPaging-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import test from 'ava'

import createPaging from './createPaging'

// Helpers

const prepareData = (data) => data.map((item) => ({...item, _id: `${item.type}:${item.id}`}))

// Tests

test('should return next: null when no data', (t) => {
const data = []
const params = {type: 'entry', pageSize: 2}
const expected = {next: null}

const ret = createPaging(data, params)

t.deepEqual(ret, expected)
})

test('should return paging for first page', (t) => {
const data = prepareData([{id: 'ent1', type: 'entry'}, {id: 'ent2', type: 'entry'}])
const params = {
type: 'entry',
pageSize: 2
}
const expected = {
next: {
type: 'entry',
query: {_id: {$gte: 'entry:ent2'}},
pageAfter: 'entry:ent2',
pageSize: 2
}
}

const ret = createPaging(data, params)

t.deepEqual(ret, expected)
})

test('should return paging for second page', (t) => {
const data = prepareData([{id: 'ent3', type: 'entry'}, {id: 'ent4', type: 'entry'}])
const params = {
type: 'entry',
query: {_id: {$gte: 'entry:ent2'}},
pageAfter: 'entry:ent2',
pageSize: 2
}
const expected = {
next: {
type: 'entry',
query: {_id: {$gte: 'entry:ent4'}},
pageAfter: 'entry:ent4',
pageSize: 2
}
}

const ret = createPaging(data, params)

t.deepEqual(ret, expected)
})

test('should return paging when sorting', (t) => {
const data = prepareData([
{id: 'ent2', type: 'entry', attributes: {index: 1}},
{id: 'ent3', type: 'entry', attributes: {index: 2}}
])
const params = {
type: 'entry',
pageSize: 2
}
const sort = {
'attributes.index': 1
}
const expected = {
next: {
type: 'entry',
query: {'attributes.index': {$gte: 2}},
pageAfter: 'entry:ent3',
pageSize: 2
}
}

const ret = createPaging(data, params, sort)

t.deepEqual(ret, expected)
})

test('should return paging when sorting descending', (t) => {
const data = prepareData([
{id: 'ent3', type: 'entry', attributes: {index: 2}},
{id: 'ent2', type: 'entry', attributes: {index: 1}}
])
const params = {
type: 'entry',
pageSize: 2
}
const sort = {
'attributes.index': -1
}
const expected = {
next: {
type: 'entry',
query: {'attributes.index': {$lte: 1}},
pageAfter: 'entry:ent2',
pageSize: 2
}
}

const ret = createPaging(data, params, sort)

t.deepEqual(ret, expected)
})

test('should return paging when sorting ascending and descending', (t) => {
const data = prepareData([
{id: 'ent3', type: 'entry', attributes: {index: 2}},
{id: 'ent2', type: 'entry', attributes: {index: 1}}
])
const params = {
type: 'entry',
pageSize: 2
}
const sort = {
'attributes.index': -1,
id: 1
}
const expected = {
next: {
type: 'entry',
query: {
'attributes.index': {$lte: 1},
id: {$gte: 'ent2'}
},
pageAfter: 'entry:ent2',
pageSize: 2
}
}

const ret = createPaging(data, params, sort)

t.deepEqual(ret, expected)
})
35 changes: 35 additions & 0 deletions lib/adapter/createPaging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const dotprop = require('dot-prop')

const createQuery = (lastItem, sort) => {
if (sort) {
return Object.keys(sort).reduce((query, key) => {
const value = dotprop.get(lastItem, key)
const operator = (sort[key] > 0) ? '$gte' : '$lte'
return {...query, [key]: {[operator]: value}}
}, {})
} else {
return {_id: {$gte: lastItem._id}}
}
}

const createPaging = (data, params, sort) => {
if (data.length === 0) {
return {next: null}
}
const lastItem = data[data.length - 1]

const {typePlural, ...nextParams} = params

const query = createQuery(lastItem, sort)

return {
next: {
...nextParams,
pageSize: params.pageSize,
pageAfter: lastItem._id,
query
}
}
}

module.exports = createPaging
51 changes: 27 additions & 24 deletions lib/adapter/getDocs.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
const prepareFilter = require('./prepareFilter')
const createPaging = require('./createPaging')

const createPaging = (data, params) => {
if (data.length === 0) {
return {}
// Move the cursor to the first doc after the `pageAfter`
// When no `pageAfter`, just start from the beginning
const moveToData = async (cursor, pageAfter) => {
if (!pageAfter) {
// Start from the beginning
return true
}
const lastId = data[data.length - 1]._id

const {typePlural, ...nextParams} = params
let doc
do {
doc = await cursor.next()
} while (doc && doc._id !== pageAfter)

return {
next: {
...nextParams,
pageSize: params.pageSize,
pageAfter: lastId,
query: {_id: {$gte: lastId}}
}
}
return !!doc // false if the doc to start after is not found
}

const getPage = async (cursor, {pageSize = Infinity, pageAfter}) => {
// Get one page of docs from where the cursor is
const getData = async (cursor, pageSize) => {
const data = []

// When pageAfter is set – loop until we find the doc with that _id
if (pageAfter) {
let doc
do {
doc = await cursor.next()
} while (doc && doc._id !== pageAfter)
}

// Get the number of docs specified with pageSize - or the rest of the docs
while (data.length < pageSize) {
const doc = await cursor.next()
if (!doc) {
Expand All @@ -41,6 +32,18 @@ const getPage = async (cursor, {pageSize = Infinity, pageAfter}) => {
return data
}

const getPage = async (cursor, {pageSize = Infinity, pageAfter}) => {
// When pageAfter is set – loop until we find the doc with that _id
const foundFirst = moveToData(cursor, pageAfter)

// Get the number of docs specified with pageSize - or the rest of the docs
if (foundFirst) {
return getData(cursor, pageSize)
}

return []
}

async function getDocs (getCollection, {endpoint, params}) {
const collection = getCollection()

Expand All @@ -61,7 +64,7 @@ async function getDocs (getCollection, {endpoint, params}) {
const response = {status: 'ok', data}

if (params.pageSize) {
response.paging = createPaging(data, params)
response.paging = createPaging(data, params, endpoint.sort)
}

return response
Expand Down
18 changes: 11 additions & 7 deletions lib/adapter/prepareFilter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
const dotprop = require('dot-prop')

const setTypeOrId = (query, type, id) => {
if (Object.keys(query).length === 0) {
if (id) {
query._id = `${type}:${id}`
} else {
query.type = type
}
}
}

const prepareFilter = ({type, id}, {query: queryProps = []}, params) => {
// Create query object from array of props
const query = queryProps.reduce((filter, prop) => {
Expand All @@ -8,13 +18,7 @@ const prepareFilter = ({type, id}, {query: queryProps = []}, params) => {
}, {})

// Set query props from id and type if no query was provided
if (Object.keys(query).length === 0) {
if (id) {
query._id = `${type}:${id}`
} else {
query.type = type
}
}
setTypeOrId(query, type, id)

// Add query from payload params
if (params && params.query) {
Expand Down
83 changes: 0 additions & 83 deletions tests/get-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,89 +116,6 @@ test('should get a document with endpoint query', async (t) => {
t.is(data[0].id, 'ent2')
})

test('should get one page of documents with params for next page', async (t) => {
const {collection, collectionName} = t.context
await insertDocuments(collection, [
{_id: 'entry:ent1', id: 'ent1', type: 'entry'},
{_id: 'entry:ent2', id: 'ent2', type: 'entry'},
{_id: 'entry:ent3', id: 'ent2', type: 'entry'}
])
const request = {
action: 'GET',
params: {
type: 'entry',
pageSize: 2
},
endpoint: {
collection: collectionName,
db: 'test'
}
}
const expectedPaging = {
next: {
type: 'entry',
query: {_id: {$gte: 'entry:ent2'}},
pageAfter: 'entry:ent2',
pageSize: 2
}
}

const connection = await adapter.connect({sourceOptions})
const response = await adapter.send(request, connection)
await adapter.disconnect(connection)

t.truthy(response)
t.is(response.status, 'ok')
const {data} = response
t.is(data.length, 2)
t.is(data[0].id, 'ent1')
t.is(data[1].id, 'ent2')
t.deepEqual(response.paging, expectedPaging)
})

test('should get second page of documents', async (t) => {
const {collection, collectionName} = t.context
await insertDocuments(collection, [
{_id: 'entry:ent1', id: 'ent1', type: 'entry'},
{_id: 'entry:ent2', id: 'ent2', type: 'entry'},
{_id: 'entry:ent3', id: 'ent3', type: 'entry'},
{_id: 'entry:ent4', id: 'ent4', type: 'entry'}
])
const request = {
action: 'GET',
params: {
type: 'entry',
query: {_id: {$gte: 'entry:ent2'}},
pageAfter: 'entry:ent2',
pageSize: 2
},
endpoint: {
collection: collectionName,
db: 'test'
}
}
const expectedPaging = {
next: {
type: 'entry',
query: {_id: {$gte: 'entry:ent4'}},
pageAfter: 'entry:ent4',
pageSize: 2
}
}

const connection = await adapter.connect({sourceOptions})
const response = await adapter.send(request, connection)
await adapter.disconnect(connection)

t.truthy(response)
t.is(response.status, 'ok')
const {data} = response
t.is(data.length, 2)
t.is(data[0].id, 'ent3')
t.is(data[1].id, 'ent4')
t.deepEqual(response.paging, expectedPaging)
})

test('should sort documents', async (t) => {
const {collection, collectionName} = t.context
await insertDocuments(collection, [
Expand Down
Loading

0 comments on commit 5f2d571

Please sign in to comment.