diff --git a/README.md b/README.md index c58a248..6d7b1c8 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,11 @@ The query object will look like this, for a request for items of type `entry`: } ``` +When the `pageSize` param is set in a request, it is taken as the max number of +documents to return in the response. When nothing else is specified, the first +page of documents is returned, and the `paging.next` prop on the response will +hold a params object that may be used to get the next page. + **Note 1:** This adapter is currently updating and deleting arrays of documents by calling `updateOne` and `deleteOne` for every item in the array. This is not the best method of doing it, so stay tuned for improvements. diff --git a/lib/adapter/createPaging-test.js b/lib/adapter/createPaging-test.js new file mode 100644 index 0000000..f9607b8 --- /dev/null +++ b/lib/adapter/createPaging-test.js @@ -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) +}) diff --git a/lib/adapter/createPaging.js b/lib/adapter/createPaging.js new file mode 100644 index 0000000..9436ff2 --- /dev/null +++ b/lib/adapter/createPaging.js @@ -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 diff --git a/lib/adapter/getDocs-test.js b/lib/adapter/getDocs-test.js new file mode 100644 index 0000000..d815e36 --- /dev/null +++ b/lib/adapter/getDocs-test.js @@ -0,0 +1,353 @@ +import test from 'ava' +import sinon from 'sinon' + +import getDocs from './getDocs' + +// Helpers + +const createFind = (items) => { + const docs = items.map((item) => ({...item, _id: `${item.type}:${item.id}`})) + const it = docs[Symbol.iterator]() + + const cursor = { + // toArray returns all docs + toArray: async () => docs, + // Mimick limit method + limit: (size) => ({toArray: async () => docs.slice(0, size)}), + // Mimick next() + next: async () => it.next().value, + sort: () => cursor + } + + return sinon.stub().resolves(cursor) +} + +// Tests + +test('should get items', async (t) => { + const find = createFind([{id: 'ent1', type: 'entry'}, {id: 'ent2', type: 'entry'}]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries' + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + + const response = await getDocs(getCollection, request) + + t.is(response.status, 'ok') + t.is(response.data.length, 2) + t.is(response.data[0].id, 'ent1') + t.is(response.data[1].id, 'ent2') + t.true(find.calledWith({type: 'entry'})) +}) + +test('should get one item', async (t) => { + const find = createFind([{id: 'ent1', type: 'entry'}]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + id: 'ent1', + type: 'entry', + typePlural: 'entries' + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + + const response = await getDocs(getCollection, request) + + t.is(response.status, 'ok') + t.is(response.data.length, 1) + t.is(response.data[0].id, 'ent1') + t.true(find.calledWith({_id: 'entry:ent1'})) +}) + +test('should get with query', async (t) => { + const find = createFind([]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries' + }, + endpoint: { + collection: 'documents', + db: 'database', + query: [ + {path: 'type', param: 'type'}, + {path: 'attributes\\.age.$gt', value: 18} + ] + } + } + const expected = { + type: 'entry', + 'attributes.age': {$gt: 18} + } + + await getDocs(getCollection, request) + + const arg = find.args[0][0] + t.deepEqual(arg, expected) +}) + +test('should get one page of items', async (t) => { + const find = createFind([ + {id: 'ent1', type: 'entry'}, + {id: 'ent2', type: 'entry'}, + {id: 'ent3', type: 'entry'} + ]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + + const response = await getDocs(getCollection, request) + + t.is(response.status, 'ok') + t.is(response.data.length, 2) + t.is(response.data[0].id, 'ent1') + t.is(response.data[1].id, 'ent2') + t.true(find.calledWith({type: 'entry'})) +}) + +test('should return params for next page', async (t) => { + const find = createFind([ + {id: 'ent1', type: 'entry'}, + {id: 'ent2', type: 'entry'} + ]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + const expectedPaging = { + next: { + type: 'entry', + query: {_id: {$gte: 'entry:ent2'}}, + pageAfter: 'entry:ent2', + pageSize: 2 + } + } + + const response = await getDocs(getCollection, request) + + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get second page of items', async (t) => { + const find = createFind([ + {id: 'ent2', type: 'entry'}, + {id: 'ent3', type: 'entry'}, + {id: 'ent4', type: 'entry'} + ]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + query: {_id: {$gte: 'entry:ent2'}}, + pageAfter: 'entry:ent2', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + const expectedPaging = { + next: { + type: 'entry', + query: {_id: {$gte: 'entry:ent4'}}, + pageAfter: 'entry:ent4', + pageSize: 2 + } + } + + const response = await getDocs(getCollection, request) + + t.deepEqual(find.args[0][0], {type: 'entry', _id: {$gte: 'entry:ent2'}}) + t.is(response.status, 'ok') + t.is(response.data.length, 2) + t.is(response.data[0].id, 'ent3') + t.is(response.data[1].id, 'ent4') + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get empty result when we have passed the last page', async (t) => { + const find = createFind([ + {id: 'ent4', type: 'entry'} + ]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + query: {_id: {$gte: 'entry:ent4'}}, + pageAfter: 'entry:ent4', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + const expectedPaging = { + next: null + } + + const response = await getDocs(getCollection, request) + + t.deepEqual(find.args[0][0], {type: 'entry', _id: {$gte: 'entry:ent4'}}) + t.is(response.status, 'ok') + t.is(response.data.length, 0) + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get empty result when the pageAfter doc is not found', async (t) => { + const find = createFind([ + {id: 'ent5', type: 'entry'} + ]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + query: {_id: {$gte: 'entry:ent4'}}, + pageAfter: 'entry:ent4', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + const expectedPaging = { + next: null + } + + const response = await getDocs(getCollection, request) + + t.deepEqual(find.args[0][0], {type: 'entry', _id: {$gte: 'entry:ent4'}}) + t.is(response.status, 'ok') + t.is(response.data.length, 0) + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get second page of items when there is documents before the pageAfter', async (t) => { + const find = createFind([ + {id: 'ent1', type: 'entry', attributes: {index: 1}}, + {id: 'ent2', type: 'entry', attributes: {index: 1}}, + {id: 'ent3', type: 'entry', attributes: {index: 2}}, + {id: 'ent4', type: 'entry', attributes: {index: 3}} + ]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + query: {'attributes.index': {$gte: 1}}, + pageAfter: 'entry:ent2', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database', + sort: {'attributes.index': 1} + } + } + const expectedPaging = { + next: { + type: 'entry', + query: {'attributes.index': {$gte: 3}}, + pageAfter: 'entry:ent4', + pageSize: 2 + } + } + + const response = await getDocs(getCollection, request) + + t.deepEqual(find.args[0][0], {type: 'entry', 'attributes.index': {$gte: 1}}) + t.is(response.status, 'ok') + t.is(response.data.length, 2) + t.is(response.data[0].id, 'ent3') + t.is(response.data[1].id, 'ent4') + t.deepEqual(response.paging, expectedPaging) +}) + +test('should return empty array when collection query comes back empty', async (t) => { + const find = createFind([]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries' + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + + const response = await getDocs(getCollection, request) + + t.is(response.status, 'ok') + t.is(response.data.length, 0) +}) + +test('should return notfound when member query comes back empty', async (t) => { + const find = createFind([]) + const getCollection = () => ({find}) + const request = { + action: 'GET', + params: { + id: 'ent1', + type: 'entry', + typePlural: 'entries' + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + const expected = { + status: 'notfound', + error: 'Could not find \'ent1\' of type \'entry\'' + } + + const response = await getDocs(getCollection, request) + + t.deepEqual(response, expected) +}) diff --git a/lib/adapter/getDocs.js b/lib/adapter/getDocs.js new file mode 100644 index 0000000..e66ba43 --- /dev/null +++ b/lib/adapter/getDocs.js @@ -0,0 +1,73 @@ +const prepareFilter = require('./prepareFilter') +const createPaging = require('./createPaging') + +// 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 + } + + let doc + do { + doc = await cursor.next() + } while (doc && doc._id !== pageAfter) + + return !!doc // false if the doc to start after is not found +} + +// Get one page of docs from where the cursor is +const getData = async (cursor, pageSize) => { + const data = [] + + while (data.length < pageSize) { + const doc = await cursor.next() + if (!doc) { + break + } + data.push(doc) + } + + return data +} + +const getPage = async (cursor, {pageSize = Infinity, pageAfter}) => { + // When pageAfter is set – loop until we find the doc with that _id + const foundFirst = await 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() + + const filter = prepareFilter(params, endpoint, params) + let cursor = await collection.find(filter) + if (endpoint.sort) { + cursor = cursor.sort(endpoint.sort) + } + const data = await getPage(cursor, params) + + if (data.length === 0 && params.id) { + return { + status: 'notfound', + error: `Could not find '${params.id}' of type '${params.type}'` + } + } + + const response = {status: 'ok', data} + + if (params.pageSize) { + response.paging = createPaging(data, params, endpoint.sort) + } + + return response +} + +module.exports = getDocs diff --git a/lib/adapter/prepareFilter.js b/lib/adapter/prepareFilter.js new file mode 100644 index 0000000..00d1b11 --- /dev/null +++ b/lib/adapter/prepareFilter.js @@ -0,0 +1,31 @@ +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) => { + const value = (prop.param) ? params[prop.param] : prop.value + return dotprop.set(filter, prop.path, value) + }, {}) + + // Set query props from id and type if no query was provided + setTypeOrId(query, type, id) + + // Add query from payload params + if (params && params.query) { + Object.assign(query, params.query) + } + + return query +} + +module.exports = prepareFilter diff --git a/lib/adapter/send-test.js b/lib/adapter/send-test.js index 5e473ce..bffba69 100644 --- a/lib/adapter/send-test.js +++ b/lib/adapter/send-test.js @@ -11,12 +11,24 @@ const createConnection = (collection) => ({ } : null }) +const createFind = (items) => { + const docs = items.map((item) => ({...item, _id: `${item.type}:${item.id}`})) + const it = docs[Symbol.iterator]() + + return sinon.stub().resolves({ + // toArray returns all docs + toArray: async () => docs, + // Mimick limit method + limit: (size) => ({toArray: async () => docs.slice(0, size)}), + // Mimick next() + next: async () => it.next().value + }) +} + // Tests test('should get items', async (t) => { - const find = sinon.stub().returns({ - toArray: () => [{id: 'ent1', type: 'entry'}, {id: 'ent2', type: 'entry'}] - }) + const find = createFind([{id: 'ent1', type: 'entry'}, {id: 'ent2', type: 'entry'}]) const connection = createConnection({find}) const request = { action: 'GET', @@ -29,132 +41,16 @@ test('should get items', async (t) => { db: 'database' } } - const expected = { - status: 'ok', - data: [ - {id: 'ent1', type: 'entry'}, - {id: 'ent2', type: 'entry'} - ] - } const response = await send(request, connection) - t.deepEqual(response, expected) + t.is(response.status, 'ok') + t.is(response.data.length, 2) + t.is(response.data[0].id, 'ent1') + t.is(response.data[1].id, 'ent2') t.true(find.calledWith({type: 'entry'})) }) -test('should get one item', async (t) => { - const find = sinon.stub().returns({ - toArray: () => [{id: 'ent1', type: 'entry'}] - }) - const connection = createConnection({find}) - const request = { - action: 'GET', - params: { - id: 'ent1', - type: 'entry', - typePlural: 'entries' - }, - endpoint: { - collection: 'documents', - db: 'database' - } - } - const expected = { - status: 'ok', - data: [ - {id: 'ent1', type: 'entry'} - ] - } - - const response = await send(request, connection) - - t.deepEqual(response, expected) - t.true(find.calledWith({_id: 'entry:ent1'})) -}) - -test('should get with query', async (t) => { - const find = sinon.stub().returns({toArray: () => []}) - const connection = createConnection({find}) - const request = { - action: 'GET', - params: { - type: 'entry', - typePlural: 'entries' - }, - endpoint: { - collection: 'documents', - db: 'database', - query: [ - {path: 'type', param: 'type'}, - {path: 'attributes\\.age.$gt', value: 18} - ] - } - } - const expected = { - type: 'entry', - 'attributes.age': {$gt: 18} - } - - await send(request, connection) - - const arg = find.args[0][0] - t.deepEqual(arg, expected) -}) - -test('should return empty array when not one item', async (t) => { - const find = sinon.stub().returns({ - toArray: () => [] - }) - const connection = createConnection({find}) - const request = { - action: 'GET', - params: { - type: 'entry', - typePlural: 'entries' - }, - endpoint: { - collection: 'documents', - db: 'database' - } - } - const expected = { - status: 'ok', - data: [] - } - - const response = await send(request, connection) - - t.deepEqual(response, expected) -}) - -test('should return notfound when one item not found', async (t) => { - const find = sinon.stub().returns({ - toArray: () => [] - }) - const connection = createConnection({find}) - const request = { - action: 'GET', - params: { - id: 'ent1', - type: 'entry', - typePlural: 'entries' - }, - endpoint: { - collection: 'documents', - db: 'database' - } - } - const expected = { - status: 'notfound', - error: 'Could not find \'ent1\' of type \'entry\'' - } - - const response = await send(request, connection) - - t.deepEqual(response, expected) -}) - test('should update one item', async (t) => { const updateOne = sinon.stub().returns({ matchedCount: 1, modifiedCount: 1, upsertedCount: 0 diff --git a/lib/adapter/send.js b/lib/adapter/send.js index 126e19f..659c8c1 100644 --- a/lib/adapter/send.js +++ b/lib/adapter/send.js @@ -1,15 +1,5 @@ -const dotprop = require('dot-prop') - -const prepareFilter = ({type, id}, {query}, params) => { - if (query) { - return query.reduce((filter, prop) => { - const value = (prop.param) ? params[prop.param] : prop.value - return dotprop.set(filter, prop.path, value) - }, {}) - } else { - return (id) ? {_id: `${type}:${id}`} : {type} - } -} +const prepareFilter = require('./prepareFilter') +const getDocs = require('./getDocs') const createItemResponse = ({id, type}, status = 'ok', error = null) => { const response = {type, id, status} @@ -58,22 +48,6 @@ const setOrDeleteData = async (getCollection, {endpoint, data, params}, action) return performOnObjectOrArray(data, performOne, action) } -const getData = async (getCollection, {endpoint, params}) => { - const collection = getCollection() - - const filter = prepareFilter(params, endpoint, params) - const data = await collection.find(filter).toArray() - - if (data.length === 0 && params.id) { - return { - status: 'notfound', - error: `Could not find '${params.id}' of type '${params.type}'` - } - } - - return {status: 'ok', data} -} - async function send (request, connection) { const getCollection = () => { const {endpoint} = request @@ -83,7 +57,7 @@ async function send (request, connection) { switch (request.action) { case 'GET': - return getData(getCollection, request) + return getDocs(getCollection, request) case 'SET': case 'DELETE': return setOrDeleteData(getCollection, request, request.action) diff --git a/package.json b/package.json index 62debb8..a08dd96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "integreat-adapter-mongodb", - "version": "0.1.3", + "version": "0.1.4", "description": "Integreat adapter for mongodb", "main": "index.js", "author": "Kjell-Morten Bratsberg Thorsen ", diff --git a/tests/get-test.js b/tests/get-test.js index adfc7d8..4c4d6f0 100644 --- a/tests/get-test.js +++ b/tests/get-test.js @@ -26,7 +26,7 @@ test.afterEach.always(async (t) => { // Tests -test('get a document by type and id', async (t) => { +test('should get a document by type and id', async (t) => { const {collection, collectionName} = t.context await insertDocuments(collection, [ {_id: 'entry:ent1', id: 'ent1', type: 'entry'}, @@ -55,7 +55,7 @@ test('get a document by type and id', async (t) => { t.is(data[0].id, 'ent1') }) -test('get documents by type', async (t) => { +test('should get documents by type', async (t) => { const {collection, collectionName} = t.context await insertDocuments(collection, [ {_id: 'entry:ent1', id: 'ent1', type: 'entry'}, @@ -84,7 +84,7 @@ test('get documents by type', async (t) => { t.is(data[1].id, 'ent2') }) -test('get a document with endpoint query', async (t) => { +test('should get a document with endpoint query', async (t) => { const {collection, collectionName} = t.context await insertDocuments(collection, [ {_id: 'entry:ent1', id: 'ent1', type: 'entry', attributes: {title: 'Entry 1'}}, @@ -115,3 +115,35 @@ test('get a document with endpoint query', async (t) => { t.is(data.length, 1) t.is(data[0].id, 'ent2') }) + +test('should sort documents', async (t) => { + const {collection, collectionName} = t.context + await insertDocuments(collection, [ + {_id: 'entry:ent1', id: 'ent1', type: 'entry', attributes: {index: 2}}, + {_id: 'entry:ent2', id: 'ent2', type: 'entry', attriubtes: {index: 1}} + ]) + const request = { + action: 'GET', + params: { + type: 'entry' + }, + endpoint: { + collection: collectionName, + db: 'test', + sort: { + 'attributes.index': 1 + } + } + } + + 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, 'ent2') + t.is(data[1].id, 'ent1') +}) diff --git a/tests/paging-test.js b/tests/paging-test.js new file mode 100644 index 0000000..0865597 --- /dev/null +++ b/tests/paging-test.js @@ -0,0 +1,353 @@ +import test from 'ava' +import { + baseUri, + openMongoWithCollection, + closeMongo, + insertDocuments, + deleteDocuments +} from './helpers/mongo' + +import mongodb from '..' +const {adapter} = mongodb + +// Helpers + +const sourceOptions = {baseUri} + +test.beforeEach(async (t) => { + t.context = await openMongoWithCollection('test') +}) + +test.afterEach.always(async (t) => { + const {client, collection} = t.context + deleteDocuments(collection, {type: 'entry'}) + closeMongo(client) +}) + +// Tests + +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 return less than a full page at the end', 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'} + ]) + 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:ent3'}}, + pageAfter: 'entry:ent3', + 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, 1) + t.is(data[0].id, 'ent3') + t.deepEqual(response.paging, expectedPaging) +}) + +test('should return empty array when past last 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: 'ent3', type: 'entry'}, + {_id: 'entry:ent4', id: 'ent4', type: 'entry'} + ]) + const request = { + action: 'GET', + params: { + type: 'entry', + query: {_id: {$gte: 'entry:ent4'}}, + pageAfter: 'entry:ent4', + pageSize: 2 + }, + endpoint: { + collection: collectionName, + db: 'test' + } + } + const expectedPaging = { + next: null + } + + 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, 0) + t.deepEqual(response.paging, expectedPaging) +}) + +test('should not throw when pageAfter does not exist', async (t) => { + const {collection, collectionName} = t.context + await insertDocuments(collection, [ + {_id: 'entry:ent1', id: 'ent1', type: 'entry'}, + {_id: 'entry:ent2', id: 'ent2', type: 'entry'} + ]) + const request = { + action: 'GET', + params: { + type: 'entry', + query: {_id: {$gte: 'entry:ent3'}}, + pageAfter: 'entry:ent3', + pageSize: 2 + }, + endpoint: { + collection: collectionName, + db: 'test' + } + } + const expectedPaging = { + next: null + } + + 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, 0) + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get second page of documents when sorting', async (t) => { + const {collection, collectionName} = t.context + await insertDocuments(collection, [ + {_id: 'entry:ent1', id: 'ent1', type: 'entry', attributes: {index: 3}}, + {_id: 'entry:ent2', id: 'ent2', type: 'entry', attributes: {index: 1}}, + {_id: 'entry:ent3', id: 'ent3', type: 'entry', attributes: {index: 2}}, + {_id: 'entry:ent4', id: 'ent4', type: 'entry', attributes: {index: 4}} + ]) + const request = { + action: 'GET', + params: { + type: 'entry', + query: {'attributes.index': {$gte: 2}}, + pageAfter: 'entry:ent3', + pageSize: 2 + }, + endpoint: { + collection: collectionName, + db: 'test', + sort: {'attributes.index': 1} + } + } + const expectedPaging = { + next: { + type: 'entry', + query: {'attributes.index': {$gte: 4}}, + 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, 'ent1') + t.is(data[1].id, 'ent4') + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get second page of documents when sorting descending', async (t) => { + const {collection, collectionName} = t.context + await insertDocuments(collection, [ + {_id: 'entry:ent1', id: 'ent1', type: 'entry', attributes: {index: 3}}, + {_id: 'entry:ent2', id: 'ent2', type: 'entry', attributes: {index: 1}}, + {_id: 'entry:ent3', id: 'ent3', type: 'entry', attributes: {index: 2}}, + {_id: 'entry:ent4', id: 'ent4', type: 'entry', attributes: {index: 4}} + ]) + const request = { + action: 'GET', + params: { + type: 'entry', + query: {'attributes.index': {$lte: 3}}, + pageAfter: 'entry:ent1', + pageSize: 2 + }, + endpoint: { + collection: collectionName, + db: 'test', + sort: {'attributes.index': -1} + } + } + const expectedPaging = { + next: { + type: 'entry', + query: {'attributes.index': {$lte: 1}}, + 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, 'ent3') + t.is(data[1].id, 'ent2') + t.deepEqual(response.paging, expectedPaging) +}) + +test('should get second page of documents when sorting key is not unique', async (t) => { + const {collection, collectionName} = t.context + await insertDocuments(collection, [ + {_id: 'entry:ent1', id: 'ent1', type: 'entry', attributes: {index: 2}}, + {_id: 'entry:ent2', id: 'ent2', type: 'entry', attributes: {index: 1}}, + {_id: 'entry:ent3', id: 'ent3', type: 'entry', attributes: {index: 1}}, + {_id: 'entry:ent4', id: 'ent4', type: 'entry', attributes: {index: 3}} + ]) + const request = { + action: 'GET', + params: { + type: 'entry', + query: {'attributes.index': {$gte: 1}}, + pageAfter: 'entry:ent3', + pageSize: 2 + }, + endpoint: { + collection: collectionName, + db: 'test', + sort: {'attributes.index': 1} + } + } + const expectedPaging = { + next: { + type: 'entry', + query: {'attributes.index': {$gte: 3}}, + 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, 'ent1') + t.is(data[1].id, 'ent4') + t.deepEqual(response.paging, expectedPaging) +})