diff --git a/src/controllers/searchResult/base.js b/src/controllers/searchResult/base.js index cacb447a1..b259224ef 100644 --- a/src/controllers/searchResult/base.js +++ b/src/controllers/searchResult/base.js @@ -1,5 +1,3 @@ -let _kuzzle; - class SearchResultBase { /** @@ -10,7 +8,7 @@ class SearchResultBase { * @param {object} response */ constructor (kuzzle, request = {}, options = {}, response = {}) { - _kuzzle = kuzzle; + this._kuzzle = kuzzle; this._request = request; this._response = response; this._options = options; @@ -31,7 +29,7 @@ class SearchResultBase { } if (this._request.scroll) { - return _kuzzle.query({ + return this._kuzzle.query({ controller: this._request.controller, action: this._scrollAction, scrollId: this._response.scrollId @@ -65,7 +63,7 @@ class SearchResultBase { request.body.search_after.push(value); } - return _kuzzle.query(request, this._options) + return this._kuzzle.query(request, this._options) .then(response => { const result = response.result; this.fetched += result.hits.length; @@ -80,7 +78,7 @@ class SearchResultBase { return Promise.resolve(null); } - return _kuzzle.query(Object.assign({}, this._request, { + return this._kuzzle.query(Object.assign({}, this._request, { action: this._searchAction, from: this.fetched }), this._options) @@ -97,6 +95,46 @@ class SearchResultBase { throw new Error('Unable to retrieve next results from search: missing scrollId, from/sort, or from/size params'); } + /** + * Automatically fetch each page of results and execute the provided action + * on each hit. + * + * If the action return a promise, this function will wait for all promise to + * be resolved. + * + * @param {Function} action - Action to execute for each hit + * @returns {Promise} + */ + async forEachHit(action, firstCall = true) { + let results; + + if (firstCall) { + results = new this.constructor(this._kuzzle, this._request, this._options, this._response); + } else { + results = this; + } + + const promises = []; + + for (const hit of results.hits) { + const ret = action(hit); + + if (ret && typeof ret.then === 'function') { + promises.push(ret); + } + } + + return Promise.all(promises) + .then(() => results.next()) + .then(nextResults => { + if (nextResults === null) { + return null; + } + + return nextResults.forEachHit(action, false); + }); + } + _get (object, path) { if (!object) { return object; diff --git a/test/controllers/searchResult/document.test.js b/test/controllers/searchResult/document.test.js index b758ed1d4..55ac8ea42 100644 --- a/test/controllers/searchResult/document.test.js +++ b/test/controllers/searchResult/document.test.js @@ -285,4 +285,80 @@ describe('DocumentSearchResult', () => { }); }); }); + + describe('forEachHit', () => { + let documentSearchResultNext; + + beforeEach(() => { + documentSearchResultNext = DocumentSearchResult.prototype.next; + }); + + afterEach(() => { + DocumentSearchResult.prototype.next = documentSearchResultNext; + }); + + it('call the callback for each hit of the SearchResult', async () => { + response = { + hits: [ + {_id: 'document1', _score: 0.9876, _source: {foo: 'bar'}}, + {_id: 'document2', _score: 0.6789, _source: {foo: 'barbar'}} + ], + total: 2 + }; + searchResult = new DocumentSearchResult(kuzzle, request, options, response); + const spy = sinon.spy(); + + await searchResult.forEachHit(spy); + + should(spy).be.calledTwice(); + should(spy.getCall(0).args).be.eql([{_id: 'document1', _score: 0.9876, _source: {foo: 'bar'}}]); + should(spy.getCall(1).args).be.eql([{_id: 'document2', _score: 0.6789, _source: {foo: 'barbar'}}]); + }); + + it('should fetch the next page of results', async () => { + response = { + scrollId: 'scroll-id', + hits: [ + {_id: 'document1', _score: 0.9876, _source: {foo: 'bar'}}, + {_id: 'document2', _score: 0.6789, _source: {foo: 'barbar'}} + ], + total: 3 + }; + searchResult = new DocumentSearchResult(kuzzle, request, options, response); + DocumentSearchResult.prototype.next = sinon.stub() + .onFirstCall().resolves(searchResult) + .onSecondCall().resolves(null); + const spy = sinon.spy(); + + await searchResult.forEachHit(spy); + + should(searchResult.next).be.calledTwice(); + should(spy.callCount).be.eql(4); + }); + + it('should wait for promises resolution if the action returns a promise', async () => { + response = { + hits: [ + {_id: 'document1', _score: 0.9876, _source: {foo: 'bar'}}, + {_id: 'document2', _score: 0.6789, _source: {foo: 'barbar'}} + ], + total: 2 + }; + searchResult = new DocumentSearchResult(kuzzle, request, options, response); + const + spy = sinon.spy(), + asyncAction = () => { + return new Promise(resolve => { + setTimeout(() => { + spy(); + resolve(); + }, 100); + }); + }; + + await searchResult.forEachHit(asyncAction); + + should(spy.callCount).be.eql(2); + }); + }); });