Skip to content

Commit 31bfc55

Browse files
authored
feat(mcm): add methods for multi cluster management (algolia#643)
* feat(mcm): add methods for multi cluster management Also have added the option to add `headers` to `_jsonRequest` and changed it into an object-accepting function for "future-proofing" it * fix(mcm): mark as correct as read query * chore(mcm): don't have unused parameters when only callback
1 parent 9f2460c commit 31bfc55

File tree

10 files changed

+331
-7
lines changed

10 files changed

+331
-7
lines changed

src/AlgoliaSearch.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,159 @@ AlgoliaSearch.prototype.batch = function(operations, callback) {
487487
});
488488
};
489489

490+
/**
491+
* Assign or Move a userID to a cluster
492+
*
493+
* @param {string} data.userID The userID to assign to a new cluster
494+
* @param {string} data.cluster The cluster to assign the user to
495+
* @return {Promise|undefined} Returns a promise if no callback given
496+
* @example
497+
* client.assignUserID({ cluster: 'c1-test', userID: 'some-user' });
498+
*/
499+
AlgoliaSearch.prototype.assignUserID = function(data, callback) {
500+
if (!data.userID || !data.cluster) {
501+
throw new errors.AlgoliaSearchError('You have to provide both a userID and cluster', data);
502+
}
503+
return this._jsonRequest({
504+
method: 'POST',
505+
url: '/1/clusters/mapping',
506+
hostType: 'write',
507+
body: {cluster: data.cluster},
508+
callback: callback,
509+
headers: {
510+
'X-Algolia-User-ID': data.userID
511+
}
512+
});
513+
};
514+
515+
/**
516+
* Get the top userIDs
517+
*
518+
* (the callback is the second argument)
519+
*
520+
* @return {Promise|undefined} Returns a promise if no callback given
521+
* @example
522+
* client.getTopUserID();
523+
*/
524+
AlgoliaSearch.prototype.getTopUserID = function(callback) {
525+
return this._jsonRequest({
526+
method: 'GET',
527+
url: '/1/clusters/mapping/top',
528+
hostType: 'read',
529+
callback: callback
530+
});
531+
};
532+
533+
/**
534+
* Get userID
535+
*
536+
* @param {string} data.userID The userID to get info about
537+
* @return {Promise|undefined} Returns a promise if no callback given
538+
* @example
539+
* client.getUserID({ userID: 'some-user' });
540+
*/
541+
AlgoliaSearch.prototype.getUserID = function(data, callback) {
542+
if (!data.userID) {
543+
throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data});
544+
}
545+
return this._jsonRequest({
546+
method: 'GET',
547+
url: '/1/clusters/mapping/' + data.userID,
548+
hostType: 'read',
549+
callback: callback
550+
});
551+
};
552+
553+
/**
554+
* List all the clusters
555+
*
556+
* (the callback is the second argument)
557+
*
558+
* @return {Promise|undefined} Returns a promise if no callback given
559+
* @example
560+
* client.listClusters();
561+
*/
562+
AlgoliaSearch.prototype.listClusters = function(callback) {
563+
return this._jsonRequest({
564+
method: 'GET',
565+
url: '/1/clusters',
566+
hostType: 'read',
567+
callback: callback
568+
});
569+
};
570+
571+
/**
572+
* List the userIDs
573+
*
574+
* (the callback is the second argument)
575+
*
576+
* @param {string} data.hitsPerPage How many hits on every page
577+
* @param {string} data.page The page to retrieve
578+
* @return {Promise|undefined} Returns a promise if no callback given
579+
* @example
580+
* client.listClusters();
581+
* client.listClusters({ page: 3, hitsPerPage: 30});
582+
*/
583+
AlgoliaSearch.prototype.listUserIDs = function(data, callback) {
584+
return this._jsonRequest({
585+
method: 'GET',
586+
url: '/1/clusters/mapping',
587+
body: data,
588+
hostType: 'read',
589+
callback: callback
590+
});
591+
};
592+
593+
/**
594+
* Remove an userID
595+
*
596+
* @param {string} data.userID The userID to assign to a new cluster
597+
* @return {Promise|undefined} Returns a promise if no callback given
598+
* @example
599+
* client.removeUserID({ userID: 'some-user' });
600+
*/
601+
AlgoliaSearch.prototype.removeUserID = function(data, callback) {
602+
if (!data.userID) {
603+
throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data});
604+
}
605+
return this._jsonRequest({
606+
method: 'DELETE',
607+
url: '/1/clusters/mapping',
608+
hostType: 'write',
609+
callback: callback,
610+
headers: {
611+
'X-Algolia-User-ID': data.userID
612+
}
613+
});
614+
};
615+
616+
/**
617+
* Search for userIDs
618+
*
619+
* @param {string} data.cluster The cluster to target
620+
* @param {string} data.query The query to execute
621+
* @param {string} data.hitsPerPage How many hits on every page
622+
* @param {string} data.page The page to retrieve
623+
* @return {Promise|undefined} Returns a promise if no callback given
624+
* @example
625+
* client.searchUserIDs({ cluster: 'c1-test', query: 'some-user' });
626+
* client.searchUserIDs({
627+
* cluster: "c1-test",
628+
* query: "some-user",
629+
* page: 3,
630+
* hitsPerPage: 2
631+
* });
632+
*/
633+
AlgoliaSearch.prototype.searchUserIDs = function(data, callback) {
634+
return this._jsonRequest({
635+
method: 'POST',
636+
url: '/1/clusters/mapping/search',
637+
body: data,
638+
hostType: 'read',
639+
callback: callback
640+
});
641+
};
642+
490643
// environment specific methods
491644
AlgoliaSearch.prototype.destroy = notImplemented;
492645
AlgoliaSearch.prototype.enableRateLimitForward = notImplemented;

src/AlgoliaSearchCore.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,16 @@ AlgoliaSearchCore.prototype._jsonRequest = function(initialOpts) {
199199
initialOpts.body.requests !== undefined) // client.search()
200200
) {
201201
initialOpts.body.apiKey = this.apiKey;
202-
headers = this._computeRequestHeaders(additionalUA, false);
202+
headers = this._computeRequestHeaders({
203+
additionalUA: additionalUA,
204+
withApiKey: false,
205+
headers: initialOpts.headers
206+
});
203207
} else {
204-
headers = this._computeRequestHeaders(additionalUA);
208+
headers = this._computeRequestHeaders({
209+
additionalUA: additionalUA,
210+
headers: initialOpts.headers
211+
});
205212
}
206213

207214
if (initialOpts.body !== undefined) {
@@ -258,7 +265,10 @@ AlgoliaSearchCore.prototype._jsonRequest = function(initialOpts) {
258265
reqOpts.body = safeJSONStringify(reqOpts.jsonBody);
259266
}
260267
// re-compute headers, they could be omitting the API KEY
261-
headers = client._computeRequestHeaders(additionalUA);
268+
headers = client._computeRequestHeaders({
269+
additionalUA: additionalUA,
270+
headers: initialOpts.headers
271+
});
262272

263273
reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType);
264274
client._setHostIndexByType(0, initialOpts.hostType);
@@ -474,11 +484,18 @@ AlgoliaSearchCore.prototype._getSearchParams = function(args, params) {
474484
return params;
475485
};
476486

477-
AlgoliaSearchCore.prototype._computeRequestHeaders = function(additionalUA, withAPIKey) {
487+
/**
488+
* Compute the headers for a request
489+
*
490+
* @param [string] options.additionalUA semi-colon separated string with other user agents to add
491+
* @param [boolean=true] options.withAPIKey Send the api key as a header
492+
* @param [Object] options.headers Extra headers to send
493+
*/
494+
AlgoliaSearchCore.prototype._computeRequestHeaders = function(options) {
478495
var forEach = require('foreach');
479496

480-
var ua = additionalUA ?
481-
this._ua + ';' + additionalUA :
497+
var ua = options.additionalUA ?
498+
this._ua + ';' + options.additionalUA :
482499
this._ua;
483500

484501
var requestHeaders = {
@@ -490,7 +507,7 @@ AlgoliaSearchCore.prototype._computeRequestHeaders = function(additionalUA, with
490507
// but in some situations, the API KEY will be too long (big secured API keys)
491508
// so if the request is a POST and the KEY is very long, we will be asked to not put
492509
// it into headers but in the JSON body
493-
if (withAPIKey !== false) {
510+
if (options.withAPIKey !== false) {
494511
requestHeaders['x-algolia-api-key'] = this.apiKey;
495512
}
496513

@@ -506,6 +523,12 @@ AlgoliaSearchCore.prototype._computeRequestHeaders = function(additionalUA, with
506523
requestHeaders[key] = value;
507524
});
508525

526+
if (options.headers) {
527+
forEach(options.headers, function addToRequestHeaders(value, key) {
528+
requestHeaders[key] = value;
529+
});
530+
}
531+
509532
return requestHeaders;
510533
};
511534

test/spec/common/client/interface.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test('AlgoliaSearch client API spec', function(t) {
2424
'addQueryInBatch',
2525
'addUserKey',
2626
'addUserKeyWithValidity',
27+
'assignUserID',
2728
'batch',
2829
'clearCache',
2930
'copyIndex',
@@ -34,13 +35,19 @@ test('AlgoliaSearch client API spec', function(t) {
3435
'getExtraHeader',
3536
'getLogs',
3637
'getTimeouts',
38+
'getTopUserID',
39+
'getUserID',
3740
'getUserKeyACL',
3841
'initIndex',
3942
'listApiKeys',
43+
'listClusters',
4044
'listIndexes',
45+
'listUserIDs',
4146
'listUserKeys',
4247
'moveIndex',
48+
'removeUserID',
4349
'search',
50+
'searchUserIDs',
4451
'sendQueriesBatch',
4552
'setExtraHeader',
4653
'setRequestTimeout',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = [{
4+
testName: 'client.assignUserID({userID, cluster}, cb)',
5+
object: 'client',
6+
methodName: 'assignUserID',
7+
callArguments: [{userID: 'hi', cluster: 'the big one'}],
8+
action: 'write',
9+
expectedRequest: {
10+
method: 'POST',
11+
body: {cluster: 'the big one'},
12+
URL: {pathname: '/1/clusters/mapping'},
13+
headers: {'x-algolia-user-id': 'hi'}
14+
}
15+
}];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
module.exports = [{
4+
testName: 'client.getTopUserID(cb)',
5+
object: 'client',
6+
methodName: 'getTopUserID',
7+
callArguments: [],
8+
action: 'read',
9+
expectedRequest: {
10+
method: 'GET',
11+
URL: {pathname: '/1/clusters/mapping/top'}
12+
}
13+
}];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
module.exports = [{
4+
testName: 'client.getUserID({ userID }, cb)',
5+
object: 'client',
6+
methodName: 'getUserID',
7+
callArguments: [{userID: 'cool-user'}],
8+
action: 'read',
9+
expectedRequest: {
10+
method: 'GET',
11+
URL: {pathname: '/1/clusters/mapping/cool-user'}
12+
}
13+
}];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
module.exports = [{
4+
testName: 'client.listClusters(cb)',
5+
object: 'client',
6+
methodName: 'listClusters',
7+
callArguments: [],
8+
action: 'read',
9+
expectedRequest: {
10+
method: 'GET',
11+
URL: {pathname: '/1/clusters'}
12+
}
13+
}];
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
module.exports = [{
4+
testName: 'client.listUserIDs(undefined, cb)',
5+
object: 'client',
6+
methodName: 'listUserIDs',
7+
callArguments: [undefined],
8+
action: 'read',
9+
expectedRequest: {
10+
method: 'GET',
11+
body: undefined,
12+
URL: {pathname: '/1/clusters/mapping'}
13+
}
14+
}, {
15+
testName: 'client.listUserIDs({page}, cb)',
16+
object: 'client',
17+
methodName: 'listUserIDs',
18+
callArguments: [{page: 5}],
19+
action: 'read',
20+
expectedRequest: {
21+
method: 'GET',
22+
body: {page: 5},
23+
URL: {pathname: '/1/clusters/mapping'}
24+
}
25+
}, {
26+
testName: 'client.listUserIDs({page, hitsPerPage}, cb)',
27+
object: 'client',
28+
methodName: 'listUserIDs',
29+
callArguments: [{page: 5, hitsPerPage: 3}],
30+
action: 'read',
31+
expectedRequest: {
32+
method: 'GET',
33+
body: {page: 5, hitsPerPage: 3},
34+
URL: {pathname: '/1/clusters/mapping'}
35+
}
36+
}];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = [{
4+
testName: 'client.removeUserID({userID, cluster}, cb)',
5+
object: 'client',
6+
methodName: 'removeUserID',
7+
callArguments: [{userID: 'hi'}],
8+
action: 'write',
9+
expectedRequest: {
10+
method: 'DELETE',
11+
body: undefined,
12+
URL: {pathname: '/1/clusters/mapping'},
13+
headers: {'x-algolia-user-id': 'hi'}
14+
}
15+
}];

0 commit comments

Comments
 (0)