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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions src/controllers/searchResult/base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
let _kuzzle;

class SearchResultBase {

/**
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to instantiate a new SearchResult object here ?

Copy link
Contributor Author

@Aschen Aschen Apr 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next method does not return a new SearchResult but instead it modify his properties.
I instantiate a new SearchResult to avoid mutating the original one when calling forEachHit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the next method should return a new SearchResult instance, as it is done in other SDK, instead of mutating the current object.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so it's next method that should return a new instance.
Instead of a complex recursive code with firstCall, I'd rather fix the next method first.

Once done, you will only need to loop into this.hits and then call this.next().forEachHit(actions)

} 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;
Expand Down
76 changes: 76 additions & 0 deletions test/controllers/searchResult/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});