From 99a0bf25e53d7666917aefc7df39848ea87a2f96 Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Sat, 31 Mar 2018 16:56:35 +0200 Subject: [PATCH 1/6] Implement simple page size --- lib/adapter/send-test.js | 43 ++++++++++++++++++++++++++++++++++++++-- lib/adapter/send.js | 6 +++++- tests/get-test.js | 31 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/adapter/send-test.js b/lib/adapter/send-test.js index 5e473ce..52482ab 100644 --- a/lib/adapter/send-test.js +++ b/lib/adapter/send-test.js @@ -102,7 +102,46 @@ test('should get with query', async (t) => { t.deepEqual(arg, expected) }) -test('should return empty array when not one item', async (t) => { +test('should get one page of items', async (t) => { + const items = [ + {id: 'ent1', type: 'entry'}, + {id: 'ent2', type: 'entry'}, + {id: 'ent3', type: 'entry'} + ] + const find = sinon.stub().returns({ + // toArray returns all three + toArray: async () => items, + // Mimick limit method + limit: (size) => ({toArray: async () => items.slice(0, size)}) + }) + const connection = createConnection({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + 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.true(find.calledWith({type: 'entry'})) +}) + +test('should return empty array when collection query comes back empty', async (t) => { const find = sinon.stub().returns({ toArray: () => [] }) @@ -128,7 +167,7 @@ test('should return empty array when not one item', async (t) => { t.deepEqual(response, expected) }) -test('should return notfound when one item not found', async (t) => { +test('should return notfound when member query comes back empty', async (t) => { const find = sinon.stub().returns({ toArray: () => [] }) diff --git a/lib/adapter/send.js b/lib/adapter/send.js index 126e19f..71a2ff6 100644 --- a/lib/adapter/send.js +++ b/lib/adapter/send.js @@ -62,7 +62,11 @@ const getData = async (getCollection, {endpoint, params}) => { const collection = getCollection() const filter = prepareFilter(params, endpoint, params) - const data = await collection.find(filter).toArray() + let cursor = await collection.find(filter) + if (params.pageSize) { + cursor = cursor.limit(params.pageSize) + } + const data = await cursor.toArray() if (data.length === 0 && params.id) { return { diff --git a/tests/get-test.js b/tests/get-test.js index adfc7d8..03057a4 100644 --- a/tests/get-test.js +++ b/tests/get-test.js @@ -115,3 +115,34 @@ test('get a document with endpoint query', async (t) => { t.is(data.length, 1) t.is(data[0].id, 'ent2') }) + +test('get one 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: 'ent2', type: 'entry'} + ]) + const request = { + action: 'GET', + params: { + type: 'entry', + pageSize: 2 + }, + endpoint: { + collection: collectionName, + db: 'test' + } + } + + 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') +}) From 9633ac38b16635bb799f16eb3ed45fdc96d4103d Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Sun, 1 Apr 2018 17:03:46 +0200 Subject: [PATCH 2/6] Support paging through result sets --- lib/adapter/getDocs-test.js | 7 ++ lib/adapter/getDocs.js | 63 +++++++++++++++ lib/adapter/prepareFilter.js | 27 +++++++ lib/adapter/send-test.js | 153 ++++++++++++++++++++++++----------- lib/adapter/send.js | 36 +-------- tests/get-test.js | 52 +++++++++++- 6 files changed, 255 insertions(+), 83 deletions(-) create mode 100644 lib/adapter/getDocs-test.js create mode 100644 lib/adapter/getDocs.js create mode 100644 lib/adapter/prepareFilter.js diff --git a/lib/adapter/getDocs-test.js b/lib/adapter/getDocs-test.js new file mode 100644 index 0000000..53861bc --- /dev/null +++ b/lib/adapter/getDocs-test.js @@ -0,0 +1,7 @@ +import test from 'ava' + +import getDocs from './getDocs' + +test('should exist', (t) => { + t.is(typeof getDocs, 'function') +}) diff --git a/lib/adapter/getDocs.js b/lib/adapter/getDocs.js new file mode 100644 index 0000000..03748ec --- /dev/null +++ b/lib/adapter/getDocs.js @@ -0,0 +1,63 @@ +const prepareFilter = require('./prepareFilter') + +const createPaging = (data, params) => { + if (data.length === 0) { + return {} + } + const lastId = data[data.length - 1]._id + return { + next: { + pageSize: params.pageSize, + pageAfter: lastId, + query: {_id: {$gte: lastId}} + } + } +} + +const getPage = async (cursor, {pageSize = Infinity, pageAfter}) => { + 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) { + break + } + data.push(doc) + } + + return data +} + +async function getDocs (getCollection, {endpoint, params}) { + const collection = getCollection() + + const filter = prepareFilter(params, endpoint, params) + let cursor = await collection.find(filter) + 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) + } + + return response +} + +module.exports = getDocs diff --git a/lib/adapter/prepareFilter.js b/lib/adapter/prepareFilter.js new file mode 100644 index 0000000..225bcb5 --- /dev/null +++ b/lib/adapter/prepareFilter.js @@ -0,0 +1,27 @@ +const dotprop = require('dot-prop') + +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 + if (Object.keys(query).length === 0) { + if (id) { + query._id = `${type}:${id}` + } else { + query.type = type + } + } + + // 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 52482ab..89d9bc9 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().returns({ + // 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,24 +41,18 @@ 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 find = createFind([{id: 'ent1', type: 'entry'}]) const connection = createConnection({find}) const request = { action: 'GET', @@ -60,21 +66,17 @@ test('should get one item', async (t) => { db: 'database' } } - const expected = { - status: 'ok', - data: [ - {id: 'ent1', type: 'entry'} - ] - } const response = await send(request, connection) - t.deepEqual(response, expected) + 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 = sinon.stub().returns({toArray: () => []}) + const find = createFind([]) const connection = createConnection({find}) const request = { action: 'GET', @@ -103,17 +105,11 @@ test('should get with query', async (t) => { }) test('should get one page of items', async (t) => { - const items = [ + const find = createFind([ {id: 'ent1', type: 'entry'}, {id: 'ent2', type: 'entry'}, {id: 'ent3', type: 'entry'} - ] - const find = sinon.stub().returns({ - // toArray returns all three - toArray: async () => items, - // Mimick limit method - limit: (size) => ({toArray: async () => items.slice(0, size)}) - }) + ]) const connection = createConnection({find}) const request = { action: 'GET', @@ -127,24 +123,88 @@ test('should get one page of 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 return params for next page', async (t) => { + const find = createFind([ + {id: 'ent1', type: 'entry'}, + {id: 'ent2', type: 'entry'} + ]) + const connection = createConnection({find}) + const request = { + action: 'GET', + params: { + type: 'entry', + typePlural: 'entries', + pageSize: 2 + }, + endpoint: { + collection: 'documents', + db: 'database' + } + } + const expectedPaging = { + next: { + query: {_id: {$gte: 'entry:ent2'}}, + pageAfter: 'entry:ent2', + pageSize: 2 + } + } + + const response = await send(request, connection) + + 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 connection = createConnection({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: { + query: {_id: {$gte: 'entry:ent4'}}, + pageAfter: 'entry:ent4', + pageSize: 2 + } + } + + const response = await send(request, connection) + + 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 return empty array when collection query comes back empty', async (t) => { - const find = sinon.stub().returns({ - toArray: () => [] - }) + const find = createFind([]) const connection = createConnection({find}) const request = { action: 'GET', @@ -157,20 +217,15 @@ test('should return empty array when collection query comes back empty', async ( db: 'database' } } - const expected = { - status: 'ok', - data: [] - } const response = await send(request, connection) - t.deepEqual(response, expected) + 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 = sinon.stub().returns({ - toArray: () => [] - }) + const find = createFind([]) const connection = createConnection({find}) const request = { action: 'GET', diff --git a/lib/adapter/send.js b/lib/adapter/send.js index 71a2ff6..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,26 +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) - let cursor = await collection.find(filter) - if (params.pageSize) { - cursor = cursor.limit(params.pageSize) - } - const data = await cursor.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 @@ -87,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/tests/get-test.js b/tests/get-test.js index 03057a4..913f635 100644 --- a/tests/get-test.js +++ b/tests/get-test.js @@ -116,7 +116,7 @@ test('get a document with endpoint query', async (t) => { t.is(data[0].id, 'ent2') }) -test('get one page of documents', async (t) => { +test('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'}, @@ -134,6 +134,13 @@ test('get one page of documents', async (t) => { db: 'test' } } + const expectedPaging = { + next: { + query: {_id: {$gte: 'entry:ent2'}}, + pageAfter: 'entry:ent2', + pageSize: 2 + } + } const connection = await adapter.connect({sourceOptions}) const response = await adapter.send(request, connection) @@ -145,4 +152,47 @@ test('get one page of documents', async (t) => { t.is(data.length, 2) t.is(data[0].id, 'ent1') t.is(data[1].id, 'ent2') + t.deepEqual(response.paging, expectedPaging) +}) + +test('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: { + 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) }) From 00c035bc3912813bb4f496755b0df33afcbcde6f Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Sun, 1 Apr 2018 19:14:19 +0200 Subject: [PATCH 3/6] Include type in next params --- lib/adapter/getDocs.js | 4 ++++ lib/adapter/send-test.js | 2 ++ tests/get-test.js | 2 ++ 3 files changed, 8 insertions(+) diff --git a/lib/adapter/getDocs.js b/lib/adapter/getDocs.js index 03748ec..0b824e7 100644 --- a/lib/adapter/getDocs.js +++ b/lib/adapter/getDocs.js @@ -5,8 +5,12 @@ const createPaging = (data, params) => { return {} } const lastId = data[data.length - 1]._id + + const {typePlural, ...nextParams} = params + return { next: { + ...nextParams, pageSize: params.pageSize, pageAfter: lastId, query: {_id: {$gte: lastId}} diff --git a/lib/adapter/send-test.js b/lib/adapter/send-test.js index 89d9bc9..a0920e3 100644 --- a/lib/adapter/send-test.js +++ b/lib/adapter/send-test.js @@ -153,6 +153,7 @@ test('should return params for next page', async (t) => { } const expectedPaging = { next: { + type: 'entry', query: {_id: {$gte: 'entry:ent2'}}, pageAfter: 'entry:ent2', pageSize: 2 @@ -187,6 +188,7 @@ test('should get second page of items', async (t) => { } const expectedPaging = { next: { + type: 'entry', query: {_id: {$gte: 'entry:ent4'}}, pageAfter: 'entry:ent4', pageSize: 2 diff --git a/tests/get-test.js b/tests/get-test.js index 913f635..de89521 100644 --- a/tests/get-test.js +++ b/tests/get-test.js @@ -136,6 +136,7 @@ test('get one page of documents with params for next page', async (t) => { } const expectedPaging = { next: { + type: 'entry', query: {_id: {$gte: 'entry:ent2'}}, pageAfter: 'entry:ent2', pageSize: 2 @@ -178,6 +179,7 @@ test('get second page of documents', async (t) => { } const expectedPaging = { next: { + type: 'entry', query: {_id: {$gte: 'entry:ent4'}}, pageAfter: 'entry:ent4', pageSize: 2 From 9909464a14ad66e698c03b752520036ff084f560 Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Sun, 1 Apr 2018 19:20:05 +0200 Subject: [PATCH 4/6] Sort documents --- lib/adapter/getDocs.js | 3 +++ tests/get-test.js | 42 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/adapter/getDocs.js b/lib/adapter/getDocs.js index 0b824e7..f91036f 100644 --- a/lib/adapter/getDocs.js +++ b/lib/adapter/getDocs.js @@ -46,6 +46,9 @@ async function getDocs (getCollection, {endpoint, params}) { 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) { diff --git a/tests/get-test.js b/tests/get-test.js index de89521..624eaa1 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'}}, @@ -116,7 +116,7 @@ test('get a document with endpoint query', async (t) => { t.is(data[0].id, 'ent2') }) -test('get one page of documents with params for next page', async (t) => { +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'}, @@ -156,7 +156,7 @@ test('get one page of documents with params for next page', async (t) => { t.deepEqual(response.paging, expectedPaging) }) -test('get second page of documents', async (t) => { +test('should get second page of documents', async (t) => { const {collection, collectionName} = t.context await insertDocuments(collection, [ {_id: 'entry:ent1', id: 'ent1', type: 'entry'}, @@ -198,3 +198,35 @@ test('get second page of documents', async (t) => { 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, [ + {_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') +}) From 5f2d5711f3c5fc7d718bc514d2120d2b71bdeb4a Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Sun, 1 Apr 2018 20:23:47 +0200 Subject: [PATCH 5/6] Support paging for sorted queries --- lib/adapter/createPaging-test.js | 143 +++++++++++++ lib/adapter/createPaging.js | 35 +++ lib/adapter/getDocs.js | 51 ++--- lib/adapter/prepareFilter.js | 18 +- tests/get-test.js | 83 -------- tests/paging-test.js | 353 +++++++++++++++++++++++++++++++ 6 files changed, 569 insertions(+), 114 deletions(-) create mode 100644 lib/adapter/createPaging-test.js create mode 100644 lib/adapter/createPaging.js create mode 100644 tests/paging-test.js 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.js b/lib/adapter/getDocs.js index f91036f..298bee7 100644 --- a/lib/adapter/getDocs.js +++ b/lib/adapter/getDocs.js @@ -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) { @@ -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() @@ -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 diff --git a/lib/adapter/prepareFilter.js b/lib/adapter/prepareFilter.js index 225bcb5..00d1b11 100644 --- a/lib/adapter/prepareFilter.js +++ b/lib/adapter/prepareFilter.js @@ -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) => { @@ -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) { diff --git a/tests/get-test.js b/tests/get-test.js index 624eaa1..4c4d6f0 100644 --- a/tests/get-test.js +++ b/tests/get-test.js @@ -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, [ 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) +}) From 28c352b712f51a579148add7ae28709d792fc806 Mon Sep 17 00:00:00 2001 From: Kjell-Morten Date: Thu, 5 Apr 2018 12:00:17 +0200 Subject: [PATCH 6/6] Document and update tests for paging --- README.md | 5 + lib/adapter/getDocs-test.js | 350 +++++++++++++++++++++++++++++++++++- lib/adapter/getDocs.js | 2 +- lib/adapter/send-test.js | 202 +-------------------- package.json | 2 +- 5 files changed, 356 insertions(+), 205 deletions(-) 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/getDocs-test.js b/lib/adapter/getDocs-test.js index 53861bc..d815e36 100644 --- a/lib/adapter/getDocs-test.js +++ b/lib/adapter/getDocs-test.js @@ -1,7 +1,353 @@ import test from 'ava' +import sinon from 'sinon' import getDocs from './getDocs' -test('should exist', (t) => { - t.is(typeof getDocs, 'function') +// 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 index 298bee7..e66ba43 100644 --- a/lib/adapter/getDocs.js +++ b/lib/adapter/getDocs.js @@ -34,7 +34,7 @@ const getData = async (cursor, pageSize) => { 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) + const foundFirst = await moveToData(cursor, pageAfter) // Get the number of docs specified with pageSize - or the rest of the docs if (foundFirst) { diff --git a/lib/adapter/send-test.js b/lib/adapter/send-test.js index a0920e3..bffba69 100644 --- a/lib/adapter/send-test.js +++ b/lib/adapter/send-test.js @@ -15,7 +15,7 @@ const createFind = (items) => { const docs = items.map((item) => ({...item, _id: `${item.type}:${item.id}`})) const it = docs[Symbol.iterator]() - return sinon.stub().returns({ + return sinon.stub().resolves({ // toArray returns all docs toArray: async () => docs, // Mimick limit method @@ -51,206 +51,6 @@ test('should get items', async (t) => { t.true(find.calledWith({type: 'entry'})) }) -test('should get one item', async (t) => { - const find = createFind([{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 response = await send(request, connection) - - 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 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 get one page of items', async (t) => { - const find = createFind([ - {id: 'ent1', type: 'entry'}, - {id: 'ent2', type: 'entry'}, - {id: 'ent3', type: 'entry'} - ]) - const connection = createConnection({find}) - const request = { - action: 'GET', - params: { - type: 'entry', - typePlural: 'entries', - pageSize: 2 - }, - endpoint: { - collection: 'documents', - db: 'database' - } - } - - const response = await send(request, connection) - - 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 connection = createConnection({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 send(request, connection) - - 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 connection = createConnection({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 send(request, connection) - - 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 return empty array when collection query comes back empty', async (t) => { - const find = createFind([]) - const connection = createConnection({find}) - const request = { - action: 'GET', - params: { - type: 'entry', - typePlural: 'entries' - }, - endpoint: { - collection: 'documents', - db: 'database' - } - } - - const response = await send(request, connection) - - 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 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/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 ",