From 26105034da4fcce7ac883c899d781f016559310d Mon Sep 17 00:00:00 2001 From: ruki Date: Thu, 8 Nov 2018 00:38:48 +0800 Subject: switch to vuepress --- node_modules/algoliasearch/src/AlgoliaSearch.js | 669 +++++++++++ .../algoliasearch/src/AlgoliaSearchCore.js | 966 +++++++++++++++ node_modules/algoliasearch/src/Index.js | 1249 ++++++++++++++++++++ node_modules/algoliasearch/src/IndexBrowser.js | 39 + node_modules/algoliasearch/src/IndexCore.js | 383 ++++++ .../src/browser/builds/algoliasearch.angular.js | 201 ++++ .../src/browser/builds/algoliasearch.jquery.js | 147 +++ .../src/browser/builds/algoliasearch.js | 6 + .../src/browser/builds/algoliasearchLite.js | 6 + .../src/browser/createAlgoliasearch.js | 233 ++++ .../algoliasearch/src/browser/inline-headers.js | 15 + .../algoliasearch/src/browser/jsonp-request.js | 126 ++ .../src/browser/migration-layer/is-using-latest.js | 23 + .../src/browser/migration-layer/load-v2.js | 50 + .../src/browser/migration-layer/old-globals.js | 26 + .../src/browser/migration-layer/script.js | 25 + .../algoliasearch/src/buildSearchMethod.js | 67 ++ node_modules/algoliasearch/src/clone.js | 3 + .../algoliasearch/src/createAnalyticsClient.js | 85 ++ node_modules/algoliasearch/src/deprecate.js | 15 + .../algoliasearch/src/deprecatedMessage.js | 7 + node_modules/algoliasearch/src/errors.js | 78 ++ node_modules/algoliasearch/src/exitPromise.js | 7 + node_modules/algoliasearch/src/map.js | 9 + node_modules/algoliasearch/src/merge.js | 19 + node_modules/algoliasearch/src/omit.js | 14 + node_modules/algoliasearch/src/places.js | 37 + .../builds/algoliasearch.reactnative.js | 175 +++ .../src/server/builds/AlgoliaSearchServer.js | 78 ++ .../algoliasearch/src/server/builds/get-agent.js | 107 ++ .../algoliasearch/src/server/builds/node.js | 318 +++++ node_modules/algoliasearch/src/store.js | 86 ++ node_modules/algoliasearch/src/version.js | 3 + 33 files changed, 5272 insertions(+) create mode 100644 node_modules/algoliasearch/src/AlgoliaSearch.js create mode 100644 node_modules/algoliasearch/src/AlgoliaSearchCore.js create mode 100644 node_modules/algoliasearch/src/Index.js create mode 100644 node_modules/algoliasearch/src/IndexBrowser.js create mode 100644 node_modules/algoliasearch/src/IndexCore.js create mode 100644 node_modules/algoliasearch/src/browser/builds/algoliasearch.angular.js create mode 100644 node_modules/algoliasearch/src/browser/builds/algoliasearch.jquery.js create mode 100644 node_modules/algoliasearch/src/browser/builds/algoliasearch.js create mode 100644 node_modules/algoliasearch/src/browser/builds/algoliasearchLite.js create mode 100644 node_modules/algoliasearch/src/browser/createAlgoliasearch.js create mode 100644 node_modules/algoliasearch/src/browser/inline-headers.js create mode 100644 node_modules/algoliasearch/src/browser/jsonp-request.js create mode 100644 node_modules/algoliasearch/src/browser/migration-layer/is-using-latest.js create mode 100644 node_modules/algoliasearch/src/browser/migration-layer/load-v2.js create mode 100644 node_modules/algoliasearch/src/browser/migration-layer/old-globals.js create mode 100644 node_modules/algoliasearch/src/browser/migration-layer/script.js create mode 100644 node_modules/algoliasearch/src/buildSearchMethod.js create mode 100644 node_modules/algoliasearch/src/clone.js create mode 100644 node_modules/algoliasearch/src/createAnalyticsClient.js create mode 100644 node_modules/algoliasearch/src/deprecate.js create mode 100644 node_modules/algoliasearch/src/deprecatedMessage.js create mode 100644 node_modules/algoliasearch/src/errors.js create mode 100644 node_modules/algoliasearch/src/exitPromise.js create mode 100644 node_modules/algoliasearch/src/map.js create mode 100644 node_modules/algoliasearch/src/merge.js create mode 100644 node_modules/algoliasearch/src/omit.js create mode 100644 node_modules/algoliasearch/src/places.js create mode 100644 node_modules/algoliasearch/src/reactnative/builds/algoliasearch.reactnative.js create mode 100644 node_modules/algoliasearch/src/server/builds/AlgoliaSearchServer.js create mode 100644 node_modules/algoliasearch/src/server/builds/get-agent.js create mode 100644 node_modules/algoliasearch/src/server/builds/node.js create mode 100644 node_modules/algoliasearch/src/store.js create mode 100644 node_modules/algoliasearch/src/version.js (limited to 'node_modules/algoliasearch/src') diff --git a/node_modules/algoliasearch/src/AlgoliaSearch.js b/node_modules/algoliasearch/src/AlgoliaSearch.js new file mode 100644 index 00000000..6dfb4dcc --- /dev/null +++ b/node_modules/algoliasearch/src/AlgoliaSearch.js @@ -0,0 +1,669 @@ +module.exports = AlgoliaSearch; + +var Index = require('./Index.js'); +var deprecate = require('./deprecate.js'); +var deprecatedMessage = require('./deprecatedMessage.js'); +var AlgoliaSearchCore = require('./AlgoliaSearchCore.js'); +var inherits = require('inherits'); +var errors = require('./errors'); + +function AlgoliaSearch() { + AlgoliaSearchCore.apply(this, arguments); +} + +inherits(AlgoliaSearch, AlgoliaSearchCore); + +/* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.deleteIndex = function(indexName, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + hostType: 'write', + callback: callback + }); +}; + +/** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of + * srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.moveIndex = function(srcIndexName, dstIndexName, callback) { + var postObj = { + operation: 'move', destination: dstIndexName + }; + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy + * of srcIndexName (destination will be overriten if it already exist). + * @param scope an array of scopes to copy: ['settings', 'synonyms', 'rules'] + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.copyIndex = function(srcIndexName, dstIndexName, scopeOrCallback, _callback) { + var postObj = { + operation: 'copy', + destination: dstIndexName + }; + var callback = _callback; + if (typeof scopeOrCallback === 'function') { + // oops, old behaviour of third argument being a function + callback = scopeOrCallback; + } else if (Array.isArray(scopeOrCallback) && scopeOrCallback.length > 0) { + postObj.scope = scopeOrCallback; + } else if (typeof scopeOrCallback !== 'undefined') { + throw new Error('the scope given to `copyIndex` was not an array with settings, synonyms or rules'); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param type Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.getLogs = function(offset, length, callback) { + var clone = require('./clone.js'); + var params = {}; + if (typeof offset === 'object') { + // getLogs(params) + params = clone(offset); + callback = length; + } else if (arguments.length === 0 || typeof offset === 'function') { + // getLogs([cb]) + callback = offset; + } else if (arguments.length === 1 || typeof length === 'function') { + // getLogs(1, [cb)] + callback = length; + params.offset = offset; + } else { + // getLogs(1, 2, [cb]) + params.offset = offset; + params.length = length; + } + + if (params.offset === undefined) params.offset = 0; + if (params.length === undefined) params.length = 10; + + return this._jsonRequest({ + method: 'GET', + url: '/1/logs?' + this._getSearchParams(params, ''), + hostType: 'read', + callback: callback + }); +}; + +/* + * List all existing indexes (paginated) + * + * @param page The page to retrieve, starting at 0. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with index list + */ +AlgoliaSearch.prototype.listIndexes = function(page, callback) { + var params = ''; + + if (page === undefined || typeof page === 'function') { + callback = page; + } else { + params = '?page=' + page; + } + + return this._jsonRequest({ + method: 'GET', + url: '/1/indexes' + params, + hostType: 'read', + callback: callback + }); +}; + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearch.prototype.initIndex = function(indexName) { + return new Index(this, indexName); +}; + +AlgoliaSearch.prototype.initAnalytics = function(opts) { + // the actual require must be inside the function, when put outside then you have a cyclic dependency + // not well resolved that ends up making the main "./index.js" (main module, the agloliasearch function) + // export an object instead of a function + // Other workarounds: + // - rewrite the lib in ES6, cyclic dependencies may be better supported + // - move initAnalytics to a property on the main module (algoliasearch.initAnalytics), + // same as places. + // The current API was made mostly to mimic the one made in PHP + var createAnalyticsClient = require('./createAnalyticsClient.js'); + return createAnalyticsClient(this.applicationID, this.apiKey, opts); +}; + +/* + * @deprecated use client.listApiKeys + */ +AlgoliaSearch.prototype.listUserKeys = deprecate(function(callback) { + return this.listApiKeys(callback); +}, deprecatedMessage('client.listUserKeys()', 'client.listApiKeys()')); + +/* + * List all existing api keys with their associated ACLs + * + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with api keys list + */ +AlgoliaSearch.prototype.listApiKeys = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys', + hostType: 'read', + callback: callback + }); +}; + +/* + * @deprecated see client.getApiKey + */ +AlgoliaSearch.prototype.getUserKeyACL = deprecate(function(key, callback) { + return this.getApiKey(key, callback); +}, deprecatedMessage('client.getUserKeyACL()', 'client.getApiKey()')); + +/* + * Get an API key + * + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the right API key + */ +AlgoliaSearch.prototype.getApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys/' + key, + hostType: 'read', + callback: callback + }); +}; + +/* + * @deprecated see client.deleteApiKey + */ +AlgoliaSearch.prototype.deleteUserKey = deprecate(function(key, callback) { + return this.deleteApiKey(key, callback); +}, deprecatedMessage('client.deleteUserKey()', 'client.deleteApiKey()')); + +/* + * Delete an existing API key + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the date of deletion + */ +AlgoliaSearch.prototype.deleteApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/keys/' + key, + hostType: 'write', + callback: callback + }); +}; + +/* + @deprecated see client.addApiKey + */ +AlgoliaSearch.prototype.addUserKey = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('client.addUserKey()', 'client.addApiKey()')); + +/* + * Add a new global API key + * + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the added API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.addApiKey(['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#AddKey|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.addApiKey = function(acls, params, callback) { + var isArray = require('isarray'); + var usage = 'Usage: client.addApiKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.indexes = params.indexes; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/keys', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * @deprecated Please use client.addApiKey() + */ +AlgoliaSearch.prototype.addUserKeyWithValidity = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('client.addUserKeyWithValidity()', 'client.addApiKey()')); + +/** + * @deprecated Please use client.updateApiKey() + */ +AlgoliaSearch.prototype.updateUserKey = deprecate(function(key, acls, params, callback) { + return this.updateApiKey(key, acls, params, callback); +}, deprecatedMessage('client.updateUserKey()', 'client.updateApiKey()')); + +/** + * Update an existing API key + * @param {string} key - The key to update + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the modified API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.updateApiKey('APIKEY', ['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.updateApiKey = function(key, acls, params, callback) { + var isArray = require('isarray'); + var usage = 'Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.indexes = params.indexes; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'PUT', + url: '/1/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Initialize a new batch of search queries + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.startQueriesBatch = deprecate(function startQueriesBatchDeprecated() { + this._batch = []; +}, deprecatedMessage('client.startQueriesBatch()', 'client.search()')); + +/** + * Add a search query in the batch + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.addQueryInBatch = deprecate(function addQueryInBatchDeprecated(indexName, query, args) { + this._batch.push({ + indexName: indexName, + query: query, + params: args + }); +}, deprecatedMessage('client.addQueryInBatch()', 'client.search()')); + +/** + * Launch the batch of queries using XMLHttpRequest. + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.sendQueriesBatch = deprecate(function sendQueriesBatchDeprecated(callback) { + return this.search(this._batch, callback); +}, deprecatedMessage('client.sendQueriesBatch()', 'client.search()')); + +/** + * Perform write operations accross multiple indexes. + * + * To reduce the amount of time spent on network round trips, + * you can create, update, or delete several objects in one call, + * using the batch endpoint (all operations are done in the given order). + * + * Available actions: + * - addObject + * - updateObject + * - partialUpdateObject + * - partialUpdateObjectNoCreate + * - deleteObject + * + * https://www.algolia.com/doc/rest_api#Indexes + * @param {Object[]} operations An array of operations to perform + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.batch([{ + * action: 'addObject', + * indexName: 'clients', + * body: { + * name: 'Bill' + * } + * }, { + * action: 'udpateObject', + * indexName: 'fruits', + * body: { + * objectID: '29138', + * name: 'banana' + * } + * }], cb) + */ +AlgoliaSearch.prototype.batch = function(operations, callback) { + var isArray = require('isarray'); + var usage = 'Usage: client.batch(operations[, callback])'; + + if (!isArray(operations)) { + throw new Error(usage); + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/batch', + body: { + requests: operations + }, + hostType: 'write', + callback: callback + }); +}; + +/** + * Assign or Move a userID to a cluster + * + * @param {string} data.userID The userID to assign to a new cluster + * @param {string} data.cluster The cluster to assign the user to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.assignUserID({ cluster: 'c1-test', userID: 'some-user' }); + */ +AlgoliaSearch.prototype.assignUserID = function(data, callback) { + if (!data.userID || !data.cluster) { + throw new errors.AlgoliaSearchError('You have to provide both a userID and cluster', data); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping', + hostType: 'write', + body: {cluster: data.cluster}, + callback: callback, + headers: { + 'x-algolia-user-id': data.userID + } + }); +}; + +/** + * Get the top userIDs + * + * (the callback is the second argument) + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getTopUserID(); + */ +AlgoliaSearch.prototype.getTopUserID = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping/top', + hostType: 'read', + callback: callback + }); +}; + +/** + * Get userID + * + * @param {string} data.userID The userID to get info about + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getUserID({ userID: 'some-user' }); + */ +AlgoliaSearch.prototype.getUserID = function(data, callback) { + if (!data.userID) { + throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data}); + } + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping/' + data.userID, + hostType: 'read', + callback: callback + }); +}; + +/** + * List all the clusters + * + * (the callback is the second argument) + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.listClusters(); + */ +AlgoliaSearch.prototype.listClusters = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters', + hostType: 'read', + callback: callback + }); +}; + +/** + * List the userIDs + * + * (the callback is the second argument) + * + * @param {string} data.hitsPerPage How many hits on every page + * @param {string} data.page The page to retrieve + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.listClusters(); + * client.listClusters({ page: 3, hitsPerPage: 30}); + */ +AlgoliaSearch.prototype.listUserIDs = function(data, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping', + body: data, + hostType: 'read', + callback: callback + }); +}; + +/** + * Remove an userID + * + * @param {string} data.userID The userID to assign to a new cluster + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.removeUserID({ userID: 'some-user' }); + */ +AlgoliaSearch.prototype.removeUserID = function(data, callback) { + if (!data.userID) { + throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data}); + } + return this._jsonRequest({ + method: 'DELETE', + url: '/1/clusters/mapping', + hostType: 'write', + callback: callback, + headers: { + 'x-algolia-user-id': data.userID + } + }); +}; + +/** + * Search for userIDs + * + * @param {string} data.cluster The cluster to target + * @param {string} data.query The query to execute + * @param {string} data.hitsPerPage How many hits on every page + * @param {string} data.page The page to retrieve + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.searchUserIDs({ cluster: 'c1-test', query: 'some-user' }); + * client.searchUserIDs({ + * cluster: "c1-test", + * query: "some-user", + * page: 3, + * hitsPerPage: 2 + * }); + */ +AlgoliaSearch.prototype.searchUserIDs = function(data, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping/search', + body: data, + hostType: 'read', + callback: callback + }); +}; + +// environment specific methods +AlgoliaSearch.prototype.destroy = notImplemented; +AlgoliaSearch.prototype.enableRateLimitForward = notImplemented; +AlgoliaSearch.prototype.disableRateLimitForward = notImplemented; +AlgoliaSearch.prototype.useSecuredAPIKey = notImplemented; +AlgoliaSearch.prototype.disableSecuredAPIKey = notImplemented; +AlgoliaSearch.prototype.generateSecuredApiKey = notImplemented; + +function notImplemented() { + var message = 'Not implemented in this environment.\n' + + 'If you feel this is a mistake, write to support@algolia.com'; + + throw new errors.AlgoliaSearchError(message); +} diff --git a/node_modules/algoliasearch/src/AlgoliaSearchCore.js b/node_modules/algoliasearch/src/AlgoliaSearchCore.js new file mode 100644 index 00000000..a94e99ca --- /dev/null +++ b/node_modules/algoliasearch/src/AlgoliaSearchCore.js @@ -0,0 +1,966 @@ +module.exports = AlgoliaSearchCore; + +var errors = require('./errors'); +var exitPromise = require('./exitPromise.js'); +var IndexCore = require('./IndexCore.js'); +var store = require('./store.js'); + +// We will always put the API KEY in the JSON body in case of too long API KEY, +// to avoid query string being too long and failing in various conditions (our server limit, browser limit, +// proxies limit) +var MAX_API_KEY_LENGTH = 500; +var RESET_APP_DATA_TIMER = + process.env.RESET_APP_DATA_TIMER && parseInt(process.env.RESET_APP_DATA_TIMER, 10) || + 60 * 2 * 1000; // after 2 minutes reset to first host + +/* + * Algolia Search library initialization + * https://www.algolia.com/ + * + * @param {string} applicationID - Your applicationID, found in your dashboard + * @param {string} apiKey - Your API key, found in your dashboard + * @param {Object} [opts] + * @param {number} [opts.timeout=2000] - The request timeout set in milliseconds, + * another request will be issued after this timeout + * @param {string} [opts.protocol='https:'] - The protocol used to query Algolia Search API. + * Set to 'http:' to force using http. + * @param {Object|Array} [opts.hosts={ + * read: [this.applicationID + '-dsn.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]), + * write: [this.applicationID + '.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]) - The hosts to use for Algolia Search API. + * If you provide them, you will less benefit from our HA implementation + */ +function AlgoliaSearchCore(applicationID, apiKey, opts) { + var debug = require('debug')('algoliasearch'); + + var clone = require('./clone.js'); + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: algoliasearch(applicationID, apiKey, opts)'; + + if (opts._allowEmptyCredentials !== true && !applicationID) { + throw new errors.AlgoliaSearchError('Please provide an application ID. ' + usage); + } + + if (opts._allowEmptyCredentials !== true && !apiKey) { + throw new errors.AlgoliaSearchError('Please provide an API key. ' + usage); + } + + this.applicationID = applicationID; + this.apiKey = apiKey; + + this.hosts = { + read: [], + write: [] + }; + + opts = opts || {}; + + this._timeouts = opts.timeouts || { + connect: 1 * 1000, // 500ms connect is GPRS latency + read: 2 * 1000, + write: 30 * 1000 + }; + + // backward compat, if opts.timeout is passed, we use it to configure all timeouts like before + if (opts.timeout) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = opts.timeout; + } + + var protocol = opts.protocol || 'https:'; + // while we advocate for colon-at-the-end values: 'http:' for `opts.protocol` + // we also accept `http` and `https`. It's a common error. + if (!/:$/.test(protocol)) { + protocol = protocol + ':'; + } + + if (protocol !== 'http:' && protocol !== 'https:') { + throw new errors.AlgoliaSearchError('protocol must be `http:` or `https:` (was `' + opts.protocol + '`)'); + } + + this._checkAppIdData(); + + if (!opts.hosts) { + var defaultHosts = map(this._shuffleResult, function(hostNumber) { + return applicationID + '-' + hostNumber + '.algolianet.com'; + }); + + // no hosts given, compute defaults + var mainSuffix = (opts.dsn === false ? '' : '-dsn') + '.algolia.net'; + this.hosts.read = [this.applicationID + mainSuffix].concat(defaultHosts); + this.hosts.write = [this.applicationID + '.algolia.net'].concat(defaultHosts); + } else if (isArray(opts.hosts)) { + // when passing custom hosts, we need to have a different host index if the number + // of write/read hosts are different. + this.hosts.read = clone(opts.hosts); + this.hosts.write = clone(opts.hosts); + } else { + this.hosts.read = clone(opts.hosts.read); + this.hosts.write = clone(opts.hosts.write); + } + + // add protocol and lowercase hosts + this.hosts.read = map(this.hosts.read, prepareHost(protocol)); + this.hosts.write = map(this.hosts.write, prepareHost(protocol)); + + this.extraHeaders = {}; + + // In some situations you might want to warm the cache + this.cache = opts._cache || {}; + + this._ua = opts._ua; + this._useCache = opts._useCache === undefined || opts._cache ? true : opts._useCache; + this._useRequestCache = this._useCache && opts._useRequestCache; + this._useFallback = opts.useFallback === undefined ? true : opts.useFallback; + + this._setTimeout = opts._setTimeout; + + debug('init done, %j', this); +} + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearchCore.prototype.initIndex = function(indexName) { + return new IndexCore(this, indexName); +}; + +/** +* Add an extra field to the HTTP request +* +* @param name the header field name +* @param value the header field value +*/ +AlgoliaSearchCore.prototype.setExtraHeader = function(name, value) { + this.extraHeaders[name.toLowerCase()] = value; +}; + +/** +* Get the value of an extra HTTP header +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.getExtraHeader = function(name) { + return this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Remove an extra field from the HTTP request +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.unsetExtraHeader = function(name) { + delete this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Augment sent x-algolia-agent with more data, each agent part +* is automatically separated from the others by a semicolon; +* +* @param algoliaAgent the agent to add +*/ +AlgoliaSearchCore.prototype.addAlgoliaAgent = function(algoliaAgent) { + if (this._ua.indexOf(';' + algoliaAgent) === -1) { + this._ua += ';' + algoliaAgent; + } +}; + +/* + * Wrapper that try all hosts to maximize the quality of service + */ +AlgoliaSearchCore.prototype._jsonRequest = function(initialOpts) { + this._checkAppIdData(); + + var requestDebug = require('debug')('algoliasearch:' + initialOpts.url); + + + var body; + var cacheID; + var additionalUA = initialOpts.additionalUA || ''; + var cache = initialOpts.cache; + var client = this; + var tries = 0; + var usingFallback = false; + var hasFallback = client._useFallback && client._request.fallback && initialOpts.fallback; + var headers; + + if ( + this.apiKey.length > MAX_API_KEY_LENGTH && + initialOpts.body !== undefined && + (initialOpts.body.params !== undefined || // index.search() + initialOpts.body.requests !== undefined) // client.search() + ) { + initialOpts.body.apiKey = this.apiKey; + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + withApiKey: false, + headers: initialOpts.headers + }); + } else { + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + } + + if (initialOpts.body !== undefined) { + body = safeJSONStringify(initialOpts.body); + } + + requestDebug('request start'); + var debugData = []; + + + function doRequest(requester, reqOpts) { + client._checkAppIdData(); + + var startTime = new Date(); + + if (client._useCache && !client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && !client._useRequestCache && body) { + cacheID += '_body_' + reqOpts.body; + } + + // handle cache existence + if (isCacheValidWithCurrentID(!client._useRequestCache, cache, cacheID)) { + requestDebug('serving response from cache'); + + var responseText = cache[cacheID]; + + // Cache response must match the type of the original one + return client._promise.resolve({ + body: JSON.parse(responseText), + responseText: responseText + }); + } + + // if we reached max tries + if (tries >= client.hosts[initialOpts.hostType].length) { + if (!hasFallback || usingFallback) { + requestDebug('could not get any response'); + // then stop + return client._promise.reject(new errors.AlgoliaSearchError( + 'Cannot connect to the AlgoliaSearch API.' + + ' Send an email to support@algolia.com to report and resolve the issue.' + + ' Application id was: ' + client.applicationID, {debugData: debugData} + )); + } + + requestDebug('switching to fallback'); + + // let's try the fallback starting from here + tries = 0; + + // method, url and body are fallback dependent + reqOpts.method = initialOpts.fallback.method; + reqOpts.url = initialOpts.fallback.url; + reqOpts.jsonBody = initialOpts.fallback.body; + if (reqOpts.jsonBody) { + reqOpts.body = safeJSONStringify(reqOpts.jsonBody); + } + // re-compute headers, they could be omitting the API KEY + headers = client._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + client._setHostIndexByType(0, initialOpts.hostType); + usingFallback = true; // the current request is now using fallback + return doRequest(client._request.fallback, reqOpts); + } + + var currentHost = client._getHostByType(initialOpts.hostType); + + var url = currentHost + reqOpts.url; + var options = { + body: reqOpts.body, + jsonBody: reqOpts.jsonBody, + method: reqOpts.method, + headers: headers, + timeouts: reqOpts.timeouts, + debug: requestDebug, + forceAuthHeaders: reqOpts.forceAuthHeaders + }; + + requestDebug('method: %s, url: %s, headers: %j, timeouts: %d', + options.method, url, options.headers, options.timeouts); + + if (requester === client._request.fallback) { + requestDebug('using fallback'); + } + + // `requester` is any of this._request or this._request.fallback + // thus it needs to be called using the client as context + return requester.call(client, url, options).then(success, tryFallback); + + function success(httpResponse) { + // compute the status of the response, + // + // When in browser mode, using XDR or JSONP, we have no statusCode available + // So we rely on our API response `status` property. + // But `waitTask` can set a `status` property which is not the statusCode (it's the task status) + // So we check if there's a `message` along `status` and it means it's an error + // + // That's the only case where we have a response.status that's not the http statusCode + var status = httpResponse && httpResponse.body && httpResponse.body.message && httpResponse.body.status || + + // this is important to check the request statusCode AFTER the body eventual + // statusCode because some implementations (jQuery XDomainRequest transport) may + // send statusCode 200 while we had an error + httpResponse.statusCode || + + // When in browser mode, using XDR or JSONP + // we default to success when no error (no response.status && response.message) + // If there was a JSON.parse() error then body is null and it fails + httpResponse && httpResponse.body && 200; + + requestDebug('received response: statusCode: %s, computed statusCode: %d, headers: %j', + httpResponse.statusCode, status, httpResponse.headers); + + var httpResponseOk = Math.floor(status / 100) === 2; + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime, + statusCode: status + }); + + if (httpResponseOk) { + if (client._useCache && !client._useRequestCache && cache) { + cache[cacheID] = httpResponse.responseText; + } + + return { + responseText: httpResponse.responseText, + body: httpResponse.body + }; + } + + var shouldRetry = Math.floor(status / 100) !== 4; + + if (shouldRetry) { + tries += 1; + return retryRequest(); + } + + requestDebug('unrecoverable error'); + + // no success and no retry => fail + var unrecoverableError = new errors.AlgoliaSearchError( + httpResponse.body && httpResponse.body.message, {debugData: debugData, statusCode: status} + ); + + return client._promise.reject(unrecoverableError); + } + + function tryFallback(err) { + // error cases: + // While not in fallback mode: + // - CORS not supported + // - network error + // While in fallback mode: + // - timeout + // - network error + // - badly formatted JSONP (script loaded, did not call our callback) + // In both cases: + // - uncaught exception occurs (TypeError) + requestDebug('error: %s, stack: %s', err.message, err.stack); + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime + }); + + if (!(err instanceof errors.AlgoliaSearchError)) { + err = new errors.Unknown(err && err.message, err); + } + + tries += 1; + + // stop the request implementation when: + if ( + // we did not generate this error, + // it comes from a throw in some other piece of code + err instanceof errors.Unknown || + + // server sent unparsable JSON + err instanceof errors.UnparsableJSON || + + // max tries and already using fallback or no fallback + tries >= client.hosts[initialOpts.hostType].length && + (usingFallback || !hasFallback)) { + // stop request implementation for this command + err.debugData = debugData; + return client._promise.reject(err); + } + + // When a timeout occured, retry by raising timeout + if (err instanceof errors.RequestTimeout) { + return retryRequestWithHigherTimeout(); + } + + return retryRequest(); + } + + function retryRequest() { + requestDebug('retrying request'); + client._incrementHostIndex(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + + function retryRequestWithHigherTimeout() { + requestDebug('retrying request with higher timeout'); + client._incrementHostIndex(initialOpts.hostType); + client._incrementTimeoutMultipler(); + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + } + + function isCacheValidWithCurrentID( + useRequestCache, + currentCache, + currentCacheID + ) { + return ( + client._useCache && + useRequestCache && + currentCache && + currentCache[currentCacheID] !== undefined + ); + } + + + function interopCallbackReturn(request, callback) { + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + request.catch(function() { + // Release the cache on error + delete cache[cacheID]; + }); + } + + if (typeof initialOpts.callback === 'function') { + // either we have a callback + request.then(function okCb(content) { + exitPromise(function() { + initialOpts.callback(null, callback(content)); + }, client._setTimeout || setTimeout); + }, function nookCb(err) { + exitPromise(function() { + initialOpts.callback(err); + }, client._setTimeout || setTimeout); + }); + } else { + // either we are using promises + return request.then(callback); + } + } + + if (client._useCache && client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && client._useRequestCache && body) { + cacheID += '_body_' + body; + } + + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + requestDebug('serving request from cache'); + + var maybePromiseForCache = cache[cacheID]; + + // In case the cache is warmup with value that is not a promise + var promiseForCache = typeof maybePromiseForCache.then !== 'function' + ? client._promise.resolve({responseText: maybePromiseForCache}) + : maybePromiseForCache; + + return interopCallbackReturn(promiseForCache, function(content) { + // In case of the cache request, return the original value + return JSON.parse(content.responseText); + }); + } + + var request = doRequest( + client._request, { + url: initialOpts.url, + method: initialOpts.method, + body: body, + jsonBody: initialOpts.body, + timeouts: client._getTimeoutsForRequest(initialOpts.hostType), + forceAuthHeaders: initialOpts.forceAuthHeaders + } + ); + + if (client._useCache && client._useRequestCache && cache) { + cache[cacheID] = request; + } + + return interopCallbackReturn(request, function(content) { + // In case of the first request, return the JSON value + return content.body; + }); +}; + +/* +* Transform search param object in query string +* @param {object} args arguments to add to the current query string +* @param {string} params current query string +* @return {string} the final query string +*/ +AlgoliaSearchCore.prototype._getSearchParams = function(args, params) { + if (args === undefined || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args[key] !== undefined && args.hasOwnProperty(key)) { + params += params === '' ? '' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? safeJSONStringify(args[key]) : args[key]); + } + } + return params; +}; + +/** + * Compute the headers for a request + * + * @param [string] options.additionalUA semi-colon separated string with other user agents to add + * @param [boolean=true] options.withApiKey Send the api key as a header + * @param [Object] options.headers Extra headers to send + */ +AlgoliaSearchCore.prototype._computeRequestHeaders = function(options) { + var forEach = require('foreach'); + + var ua = options.additionalUA ? + this._ua + ';' + options.additionalUA : + this._ua; + + var requestHeaders = { + 'x-algolia-agent': ua, + 'x-algolia-application-id': this.applicationID + }; + + // browser will inline headers in the url, node.js will use http headers + // but in some situations, the API KEY will be too long (big secured API keys) + // so if the request is a POST and the KEY is very long, we will be asked to not put + // it into headers but in the JSON body + if (options.withApiKey !== false) { + requestHeaders['x-algolia-api-key'] = this.apiKey; + } + + if (this.userToken) { + requestHeaders['x-algolia-usertoken'] = this.userToken; + } + + if (this.securityTags) { + requestHeaders['x-algolia-tagfilters'] = this.securityTags; + } + + forEach(this.extraHeaders, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + + if (options.headers) { + forEach(options.headers, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + } + + return requestHeaders; +}; + +/** + * Search through multiple indices at the same time + * @param {Object[]} queries An array of queries you want to run. + * @param {string} queries[].indexName The index name you want to target + * @param {string} [queries[].query] The query to issue on this index. Can also be passed into `params` + * @param {Object} queries[].params Any search param like hitsPerPage, .. + * @param {Function} callback Callback to be called + * @return {Promise|undefined} Returns a promise if no callback given + */ +AlgoliaSearchCore.prototype.search = function(queries, opts, callback) { + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: client.search(arrayOfQueries[, callback])'; + + if (!isArray(queries)) { + throw new Error(usage); + } + + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var client = this; + + var postObj = { + requests: map(queries, function prepareRequest(query) { + var params = ''; + + // allow query.query + // so we are mimicing the index.search(query, params) method + // {indexName:, query:, params:} + if (query.query !== undefined) { + params += 'query=' + encodeURIComponent(query.query); + } + + return { + indexName: query.indexName, + params: client._getSearchParams(query.params, params) + }; + }) + }; + + var JSONPParams = map(postObj.requests, function prepareJSONPParams(request, requestId) { + return requestId + '=' + + encodeURIComponent( + '/1/indexes/' + encodeURIComponent(request.indexName) + '?' + + request.params + ); + }).join('&'); + + var url = '/1/indexes/*/queries'; + + if (opts.strategy !== undefined) { + postObj.strategy = opts.strategy; + } + + return this._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url, + body: postObj, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/*', + body: { + params: JSONPParams + } + }, + callback: callback + }); +}; + +/** +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* This is the top-level API for SFFV. +* +* @param {object[]} queries An array of queries to run. +* @param {string} queries[].indexName Index name, name of the index to search. +* @param {object} queries[].params Query parameters. +* @param {string} queries[].params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} queries[].params.facetQuery Query for the facet search +* @param {string} [queries[].params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +*/ +AlgoliaSearchCore.prototype.searchForFacetValues = function(queries) { + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])'; // eslint-disable-line max-len + + if (!isArray(queries)) { + throw new Error(usage); + } + + var client = this; + + return client._promise.all(map(queries, function performQuery(query) { + if ( + !query || + query.indexName === undefined || + query.params.facetName === undefined || + query.params.facetQuery === undefined + ) { + throw new Error(usage); + } + + var clone = require('./clone.js'); + var omit = require('./omit.js'); + + var indexName = query.indexName; + var params = query.params; + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = client._getSearchParams(filteredParams, ''); + + return client._jsonRequest({ + cache: client.cache, + method: 'POST', + url: + '/1/indexes/' + + encodeURIComponent(indexName) + + '/facets/' + + encodeURIComponent(facetName) + + '/query', + hostType: 'read', + body: {params: searchParameters} + }); + })); +}; + +/** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ +AlgoliaSearchCore.prototype.setSecurityTags = function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + + this.securityTags = tags; +}; + +/** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ +AlgoliaSearchCore.prototype.setUserToken = function(userToken) { + this.userToken = userToken; +}; + +/** + * Clear all queries in client's cache + * @return undefined + */ +AlgoliaSearchCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/** +* Set the number of milliseconds a request can take before automatically being terminated. +* @deprecated +* @param {Number} milliseconds +*/ +AlgoliaSearchCore.prototype.setRequestTimeout = function(milliseconds) { + if (milliseconds) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = milliseconds; + } +}; + +/** +* Set the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.setTimeouts = function(timeouts) { + this._timeouts = timeouts; +}; + +/** +* Get the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.getTimeouts = function() { + return this._timeouts; +}; + +AlgoliaSearchCore.prototype._getAppIdData = function() { + var data = store.get(this.applicationID); + if (data !== null) this._cacheAppIdData(data); + return data; +}; + +AlgoliaSearchCore.prototype._setAppIdData = function(data) { + data.lastChange = (new Date()).getTime(); + this._cacheAppIdData(data); + return store.set(this.applicationID, data); +}; + +AlgoliaSearchCore.prototype._checkAppIdData = function() { + var data = this._getAppIdData(); + var now = (new Date()).getTime(); + if (data === null || now - data.lastChange > RESET_APP_DATA_TIMER) { + return this._resetInitialAppIdData(data); + } + + return data; +}; + +AlgoliaSearchCore.prototype._resetInitialAppIdData = function(data) { + var newData = data || {}; + newData.hostIndexes = {read: 0, write: 0}; + newData.timeoutMultiplier = 1; + newData.shuffleResult = newData.shuffleResult || shuffle([1, 2, 3]); + return this._setAppIdData(newData); +}; + +AlgoliaSearchCore.prototype._cacheAppIdData = function(data) { + this._hostIndexes = data.hostIndexes; + this._timeoutMultiplier = data.timeoutMultiplier; + this._shuffleResult = data.shuffleResult; +}; + +AlgoliaSearchCore.prototype._partialAppIdDataUpdate = function(newData) { + var foreach = require('foreach'); + var currentData = this._getAppIdData(); + foreach(newData, function(value, key) { + currentData[key] = value; + }); + + return this._setAppIdData(currentData); +}; + +AlgoliaSearchCore.prototype._getHostByType = function(hostType) { + return this.hosts[hostType][this._getHostIndexByType(hostType)]; +}; + +AlgoliaSearchCore.prototype._getTimeoutMultiplier = function() { + return this._timeoutMultiplier; +}; + +AlgoliaSearchCore.prototype._getHostIndexByType = function(hostType) { + return this._hostIndexes[hostType]; +}; + +AlgoliaSearchCore.prototype._setHostIndexByType = function(hostIndex, hostType) { + var clone = require('./clone'); + var newHostIndexes = clone(this._hostIndexes); + newHostIndexes[hostType] = hostIndex; + this._partialAppIdDataUpdate({hostIndexes: newHostIndexes}); + return hostIndex; +}; + +AlgoliaSearchCore.prototype._incrementHostIndex = function(hostType) { + return this._setHostIndexByType( + (this._getHostIndexByType(hostType) + 1) % this.hosts[hostType].length, hostType + ); +}; + +AlgoliaSearchCore.prototype._incrementTimeoutMultipler = function() { + var timeoutMultiplier = Math.max(this._timeoutMultiplier + 1, 4); + return this._partialAppIdDataUpdate({timeoutMultiplier: timeoutMultiplier}); +}; + +AlgoliaSearchCore.prototype._getTimeoutsForRequest = function(hostType) { + return { + connect: this._timeouts.connect * this._timeoutMultiplier, + complete: this._timeouts[hostType] * this._timeoutMultiplier + }; +}; + +function prepareHost(protocol) { + return function prepare(host) { + return protocol + '//' + host.toLowerCase(); + }; +} + +// Prototype.js < 1.7, a widely used library, defines a weird +// Array.prototype.toJSON function that will fail to stringify our content +// appropriately +// refs: +// - https://groups.google.com/forum/#!topic/prototype-core/E-SAVvV_V9Q +// - https://github.com/sstephenson/prototype/commit/038a2985a70593c1a86c230fadbdfe2e4898a48c +// - http://stackoverflow.com/a/3148441/147079 +function safeJSONStringify(obj) { + /* eslint no-extend-native:0 */ + + if (Array.prototype.toJSON === undefined) { + return JSON.stringify(obj); + } + + var toJSON = Array.prototype.toJSON; + delete Array.prototype.toJSON; + var out = JSON.stringify(obj); + Array.prototype.toJSON = toJSON; + + return out; +} + +function shuffle(array) { + var currentIndex = array.length; + var temporaryValue; + var randomIndex; + + // While there remain elements to shuffle... + while (currentIndex !== 0) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + +function removeCredentials(headers) { + var newHeaders = {}; + + for (var headerName in headers) { + if (Object.prototype.hasOwnProperty.call(headers, headerName)) { + var value; + + if (headerName === 'x-algolia-api-key' || headerName === 'x-algolia-application-id') { + value = '**hidden for security purposes**'; + } else { + value = headers[headerName]; + } + + newHeaders[headerName] = value; + } + } + + return newHeaders; +} diff --git a/node_modules/algoliasearch/src/Index.js b/node_modules/algoliasearch/src/Index.js new file mode 100644 index 00000000..99e5a196 --- /dev/null +++ b/node_modules/algoliasearch/src/Index.js @@ -0,0 +1,1249 @@ +var inherits = require('inherits'); +var IndexCore = require('./IndexCore.js'); +var deprecate = require('./deprecate.js'); +var deprecatedMessage = require('./deprecatedMessage.js'); +var exitPromise = require('./exitPromise.js'); +var errors = require('./errors'); + +var deprecateForwardToSlaves = deprecate( + function() {}, + deprecatedMessage('forwardToSlaves', 'forwardToReplicas') +); + +module.exports = Index; + +function Index() { + IndexCore.apply(this, arguments); +} + +inherits(Index, IndexCore); + +/* +* Add an object in this index +* +* @param content contains the javascript object to add inside the index +* @param objectID (optional) an objectID you want to attribute to this object +* (if the attribute already exist the old object will be overwrite) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.addObject = function(content, objectID, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof objectID === 'function') { + callback = objectID; + objectID = undefined; + } + + return this.as._jsonRequest({ + method: objectID !== undefined ? + 'PUT' : // update or create + 'POST', // create (API generates an objectID) + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + // create + (objectID !== undefined ? '/' + encodeURIComponent(objectID) : ''), // update or create + body: content, + hostType: 'write', + callback: callback + }); +}; + +/* +* Add several objects +* +* @param objects contains an array of objects to add +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.addObjects = function(objects, callback) { + var isArray = require('isarray'); + var usage = 'Usage: index.addObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'addObject', + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Update partially an object (only update attributes passed in argument) +* +* @param partialObject contains the javascript attributes to override, the +* object must contains an objectID attribute +* @param createIfNotExists (optional) if false, avoid an automatic creation of the object +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.partialUpdateObject = function(partialObject, createIfNotExists, callback) { + if (arguments.length === 1 || typeof createIfNotExists === 'function') { + callback = createIfNotExists; + createIfNotExists = undefined; + } + + var indexObj = this; + var url = '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial'; + if (createIfNotExists === false) { + url += '?createIfNotExists=false'; + } + + return this.as._jsonRequest({ + method: 'POST', + url: url, + body: partialObject, + hostType: 'write', + callback: callback + }); +}; + +/* +* Partially Override the content of several objects +* +* @param objects contains an array of objects to update (each object must contains a objectID attribute) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.partialUpdateObjects = function(objects, createIfNotExists, callback) { + if (arguments.length === 1 || typeof createIfNotExists === 'function') { + callback = createIfNotExists; + createIfNotExists = true; + } + + var isArray = require('isarray'); + var usage = 'Usage: index.partialUpdateObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: createIfNotExists === true ? 'partialUpdateObject' : 'partialUpdateObjectNoCreate', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Override the content of object +* +* @param object contains the javascript object to save, the object must contains an objectID attribute +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.saveObject = function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + hostType: 'write', + callback: callback + }); +}; + +/* +* Override the content of several objects +* +* @param objects contains an array of objects to update (each object must contains a objectID attribute) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.saveObjects = function(objects, callback) { + var isArray = require('isarray'); + var usage = 'Usage: index.saveObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete an object from the index +* +* @param objectID the unique identifier of object to delete +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.deleteObject = function(objectID, callback) { + if (typeof objectID === 'function' || typeof objectID !== 'string' && typeof objectID !== 'number') { + var err = new errors.AlgoliaSearchError( + objectID && typeof objectID !== 'function' + ? 'ObjectID must be a string' + : 'Cannot delete an object without an objectID' + ); + callback = objectID; + if (typeof callback === 'function') { + return callback(err); + } + + return this.as._promise.reject(err); + } + + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete several objects from an index +* +* @param objectIDs contains an array of objectID to delete +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.deleteObjects = function(objectIDs, callback) { + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: index.deleteObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: map(objectIDs, function prepareRequest(objectID) { + return { + action: 'deleteObject', + objectID: objectID, + body: { + objectID: objectID + } + }; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete all objects matching a query +* +* @param query the query string +* @param params the optional query parameters +* @param callback (optional) the result callback called with one argument +* error: null or Error('message') +* @deprecated see index.deleteBy +*/ +Index.prototype.deleteByQuery = deprecate(function(query, params, callback) { + var clone = require('./clone.js'); + var map = require('./map.js'); + + var indexObj = this; + var client = indexObj.as; + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = {}; + } else { + params = clone(params); + } + + params.attributesToRetrieve = 'objectID'; + params.hitsPerPage = 1000; + params.distinct = false; + + // when deleting, we should never use cache to get the + // search results + this.clearCache(); + + // there's a problem in how we use the promise chain, + // see how waitTask is done + var promise = this + .search(query, params) + .then(stopOrDelete); + + function stopOrDelete(searchContent) { + // stop here + if (searchContent.nbHits === 0) { + // return indexObj.as._request.resolve(); + return searchContent; + } + + // continue and do a recursive call + var objectIDs = map(searchContent.hits, function getObjectID(object) { + return object.objectID; + }); + + return indexObj + .deleteObjects(objectIDs) + .then(waitTask) + .then(doDeleteByQuery); + } + + function waitTask(deleteObjectsContent) { + return indexObj.waitTask(deleteObjectsContent.taskID); + } + + function doDeleteByQuery() { + return indexObj.deleteByQuery(query, params); + } + + if (!callback) { + return promise; + } + + promise.then(success, failure); + + function success() { + exitPromise(function exit() { + callback(null); + }, client._setTimeout || setTimeout); + } + + function failure(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } +}, deprecatedMessage('index.deleteByQuery()', 'index.deleteBy()')); + +/** +* Delete all objects matching a query +* +* the query parameters that can be used are: +* - filters (numeric, facet, tag) +* - geo +* +* you can not send an empty query or filters +* +* @param params the optional query parameters +* @param callback (optional) the result callback called with one argument +* error: null or Error('message') +*/ +Index.prototype.deleteBy = function(params, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/deleteByQuery', + body: {params: indexObj.as._getSearchParams(params, '')}, + hostType: 'write', + callback: callback + }); +}; + +/* +* Browse all content from an index using events. Basically this will do +* .browse() -> .browseFrom -> .browseFrom -> .. until all the results are returned +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @return {EventEmitter} +* @example +* var browser = index.browseAll('cool songs', { +* tagFilters: 'public,comments', +* hitsPerPage: 500 +* }); +* +* browser.on('result', function resultCallback(content) { +* console.log(content.hits); +* }); +* +* // if any error occurs, you get it +* browser.on('error', function(err) { +* throw err; +* }); +* +* // when you have browsed the whole index, you get this event +* browser.on('end', function() { +* console.log('finished'); +* }); +* +* // at any point if you want to stop the browsing process, you can stop it manually +* // otherwise it will go on and on +* browser.stop(); +* +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +Index.prototype.browseAll = function(query, queryParameters) { + if (typeof query === 'object') { + queryParameters = query; + query = undefined; + } + + var merge = require('./merge.js'); + + var IndexBrowser = require('./IndexBrowser'); + + var browser = new IndexBrowser(); + var client = this.as; + var index = this; + var params = client._getSearchParams( + merge({}, queryParameters || {}, { + query: query + }), '' + ); + + // start browsing + browseLoop(); + + function browseLoop(cursor) { + if (browser._stopped) { + return; + } + + var body; + + if (cursor !== undefined) { + body = { + cursor: cursor + }; + } else { + body = { + params: params + }; + } + + client._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(index.indexName) + '/browse', + hostType: 'read', + body: body, + callback: browseCallback + }); + } + + function browseCallback(err, content) { + if (browser._stopped) { + return; + } + + if (err) { + browser._error(err); + return; + } + + browser._result(content); + + // no cursor means we are finished browsing + if (content.cursor === undefined) { + browser._end(); + return; + } + + browseLoop(content.cursor); + } + + return browser; +}; + +/* +* Get a Typeahead.js adapter +* @param searchParams contains an object with query parameters (see search for details) +*/ +Index.prototype.ttAdapter = deprecate(function(params) { + var self = this; + return function ttAdapter(query, syncCb, asyncCb) { + var cb; + + if (typeof asyncCb === 'function') { + // typeahead 0.11 + cb = asyncCb; + } else { + // pre typeahead 0.11 + cb = syncCb; + } + + self.search(query, params, function searchDone(err, content) { + if (err) { + cb(err); + return; + } + + cb(content.hits); + }); + }; +}, +'ttAdapter is not necessary anymore and will be removed in the next version,\n' + +'have a look at autocomplete.js (https://github.com/algolia/autocomplete.js)'); + +/* +* Wait the publication of a task on the server. +* All server task are asynchronous and you can check with this method that the task is published. +* +* @param taskID the id of the task returned by server +* @param callback the result callback with with two arguments: +* error: null or Error('message') +* content: the server answer that contains the list of results +*/ +Index.prototype.waitTask = function(taskID, callback) { + // wait minimum 100ms before retrying + var baseDelay = 100; + // wait maximum 5s before retrying + var maxDelay = 5000; + var loop = 0; + + // waitTask() must be handled differently from other methods, + // it's a recursive method using a timeout + var indexObj = this; + var client = indexObj.as; + + var promise = retryLoop(); + + function retryLoop() { + return client._jsonRequest({ + method: 'GET', + hostType: 'read', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID + }).then(function success(content) { + loop++; + var delay = baseDelay * loop * loop; + if (delay > maxDelay) { + delay = maxDelay; + } + + if (content.status !== 'published') { + return client._promise.delay(delay).then(retryLoop); + } + + return content; + }); + } + + if (!callback) { + return promise; + } + + promise.then(successCb, failureCb); + + function successCb(content) { + exitPromise(function exit() { + callback(null, content); + }, client._setTimeout || setTimeout); + } + + function failureCb(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } +}; + +/* +* This function deletes the index content. Settings and index specific API keys are kept untouched. +* +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the settings object or the error message if a failure occured +*/ +Index.prototype.clearIndex = function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + hostType: 'write', + callback: callback + }); +}; + +/* +* Get settings of this index +* +* @param opts an object of options to add +* @param opts.advanced get more settings like nbShards (useful for Enterprise) +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the settings object or the error message if a failure occurred +*/ +Index.prototype.getSettings = function(opts, callback) { + if (arguments.length === 1 && typeof opts === 'function') { + callback = opts; + opts = {}; + } + opts = opts || {}; + + var indexName = encodeURIComponent(this.indexName); + return this.as._jsonRequest({ + method: 'GET', + url: + '/1/indexes/' + + indexName + + '/settings?getVersion=2' + + (opts.advanced ? '&advanced=' + opts.advanced : ''), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.searchSynonyms = function(params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } else if (params === undefined) { + params = {}; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/search', + body: params, + hostType: 'read', + callback: callback + }); +}; + +function exportData(method, _hitsPerPage, callback) { + function search(page, _previous) { + var options = { + page: page || 0, + hitsPerPage: _hitsPerPage || 100 + }; + var previous = _previous || []; + + return method(options).then(function(result) { + var hits = result.hits; + var nbHits = result.nbHits; + var current = hits.map(function(s) { + delete s._highlightResult; + return s; + }); + var synonyms = previous.concat(current); + if (synonyms.length < nbHits) { + return search(options.page + 1, synonyms); + } + return synonyms; + }); + } + return search().then(function(data) { + if (typeof callback === 'function') { + callback(data); + return undefined; + } + return data; + }); +} + +/** + * Retrieve all the synonyms in an index + * @param [number=100] hitsPerPage The amount of synonyms to retrieve per batch + * @param [function] callback will be called after all synonyms are retrieved + */ +Index.prototype.exportSynonyms = function(hitsPerPage, callback) { + return exportData(this.searchSynonyms.bind(this), hitsPerPage, callback); +}; + +Index.prototype.saveSynonym = function(synonym, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(synonym.objectID) + + '?forwardToReplicas=' + forwardToReplicas, + body: synonym, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.getSynonym = function(objectID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(objectID), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.deleteSynonym = function(objectID, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(objectID) + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.clearSynonyms = function(opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/clear' + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.batchSynonyms = function(synonyms, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/batch' + + '?forwardToReplicas=' + forwardToReplicas + + '&replaceExistingSynonyms=' + (opts.replaceExistingSynonyms ? 'true' : 'false'), + hostType: 'write', + body: synonyms, + callback: callback + }); +}; + +Index.prototype.searchRules = function(params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } else if (params === undefined) { + params = {}; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/search', + body: params, + hostType: 'read', + callback: callback + }); +}; +/** + * Retrieve all the query rules in an index + * @param [number=100] hitsPerPage The amount of query rules to retrieve per batch + * @param [function] callback will be called after all query rules are retrieved + * error: null or Error('message') + */ +Index.prototype.exportRules = function(hitsPerPage, callback) { + return exportData(this.searchRules.bind(this), hitsPerPage, callback); +}; + +Index.prototype.saveRule = function(rule, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (!rule.objectID) { + throw new errors.AlgoliaSearchError('Missing or empty objectID field for rule'); + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(rule.objectID) + + '?forwardToReplicas=' + forwardToReplicas, + body: rule, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.getRule = function(objectID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(objectID), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.deleteRule = function(objectID, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(objectID) + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.clearRules = function(opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/clear' + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.batchRules = function(rules, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/batch' + + '?forwardToReplicas=' + forwardToReplicas + + '&clearExistingRules=' + (opts.clearExistingRules === true ? 'true' : 'false'), + hostType: 'write', + body: rules, + callback: callback + }); +}; + +/* +* Set settings for this index +* +* @param settigns the settings object that can contains : +* - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). +* - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). +* - hitsPerPage: (integer) the number of hits per page (default = 10). +* - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. +* If set to null, all attributes are retrieved. +* - attributesToHighlight: (array of strings) default list of attributes to highlight. +* If set to null, all indexed attributes are highlighted. +* - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number +* of words to return (syntax is attributeName:nbWords). +* By default no snippet is computed. If set to null, no snippet is computed. +* - attributesToIndex: (array of strings) the list of fields you want to index. +* If set to null, all textual and numerical attributes of your objects are indexed, +* but you should update it to get optimal results. +* This parameter has two important uses: +* - Limit the attributes to index: For example if you store a binary image in base64, +* you want to store it and be able to +* retrieve it but you don't want to search in the base64 string. +* - Control part of the ranking*: (see the ranking parameter for full explanation) +* Matches in attributes at the beginning of +* the list will be considered more important than matches in attributes further down the list. +* In one attribute, matching text at the beginning of the attribute will be +* considered more important than text after, you can disable +* this behavior if you add your attribute inside `unordered(AttributeName)`, +* for example attributesToIndex: ["title", "unordered(text)"]. +* - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. +* All strings in the attribute selected for faceting are extracted and added as a facet. +* If set to null, no attribute is used for faceting. +* - attributeForDistinct: (string) The attribute name used for the Distinct feature. +* This feature is similar to the SQL "distinct" keyword: when enabled +* in query with the distinct=1 parameter, all hits containing a duplicate +* value for this attribute are removed from results. +* For example, if the chosen attribute is show_name and several hits have +* the same value for show_name, then only the best one is kept and others are removed. +* - ranking: (array of strings) controls the way results are sorted. +* We have six available criteria: +* - typo: sort according to number of typos, +* - geo: sort according to decreassing distance when performing a geo-location based search, +* - proximity: sort according to the proximity of query words in hits, +* - attribute: sort according to the order of attributes defined by attributesToIndex, +* - exact: +* - if the user query contains one word: sort objects having an attribute +* that is exactly the query word before others. +* For example if you search for the "V" TV show, you want to find it +* with the "V" query and avoid to have all popular TV +* show starting by the v letter before it. +* - if the user query contains multiple words: sort according to the +* number of words that matched exactly (and not as a prefix). +* - custom: sort according to a user defined formula set in **customRanking** attribute. +* The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] +* - customRanking: (array of strings) lets you specify part of the ranking. +* The syntax of this condition is an array of strings containing attributes +* prefixed by asc (ascending order) or desc (descending order) operator. +* For example `"customRanking" => ["desc(population)", "asc(name)"]` +* - queryType: Select how the query words are interpreted, it can be one of the following value: +* - prefixAll: all query words are interpreted as prefixes, +* - prefixLast: only the last word is interpreted as a prefix (default behavior), +* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. +* - highlightPreTag: (string) Specify the string that is inserted before +* the highlighted parts in the query result (default to ""). +* - highlightPostTag: (string) Specify the string that is inserted after +* the highlighted parts in the query result (default to ""). +* - optionalWords: (array of strings) Specify a list of words that should +* be considered as optional when found in the query. +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the server answer or the error message if a failure occured +*/ +Index.prototype.setSettings = function(settings, opts, callback) { + if (arguments.length === 1 || typeof opts === 'function') { + callback = opts; + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings?forwardToReplicas=' + + forwardToReplicas, + hostType: 'write', + body: settings, + callback: callback + }); +}; + +/* +* @deprecated see client.listApiKeys() +*/ +Index.prototype.listUserKeys = deprecate(function(callback) { + return this.listApiKeys(callback); +}, deprecatedMessage('index.listUserKeys()', 'client.listApiKeys()')); + +/* +* List all existing API keys to this index +* +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with API keys belonging to the index +* +* @deprecated see client.listApiKeys() +*/ +Index.prototype.listApiKeys = deprecate(function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + hostType: 'read', + callback: callback + }); +}, deprecatedMessage('index.listApiKeys()', 'client.listApiKeys()')); + +/* +* @deprecated see client.getApiKey() +*/ +Index.prototype.getUserKeyACL = deprecate(function(key, callback) { + return this.getApiKey(key, callback); +}, deprecatedMessage('index.getUserKeyACL()', 'client.getApiKey()')); + + +/* +* Get an API key from this index +* +* @param key +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the right API key +* +* @deprecated see client.getApiKey() +*/ +Index.prototype.getApiKey = deprecate(function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'read', + callback: callback + }); +}, deprecatedMessage('index.getApiKey()', 'client.getApiKey()')); + +/* +* @deprecated see client.deleteApiKey() +*/ +Index.prototype.deleteUserKey = deprecate(function(key, callback) { + return this.deleteApiKey(key, callback); +}, deprecatedMessage('index.deleteUserKey()', 'client.deleteApiKey()')); + +/* +* Delete an existing API key associated to this index +* +* @param key +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the deletion date +* +* @deprecated see client.deleteApiKey() +*/ +Index.prototype.deleteApiKey = deprecate(function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.deleteApiKey()', 'client.deleteApiKey()')); + +/* +* @deprecated see client.addApiKey() +*/ +Index.prototype.addUserKey = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('index.addUserKey()', 'client.addApiKey()')); + +/* +* Add a new API key to this index +* +* @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that +* can contains the following values: +* - search: allow to search (https and http) +* - addObject: allows to add/update an object in the index (https only) +* - deleteObject : allows to delete an existing object (https only) +* - deleteIndex : allows to delete index content (https only) +* - settings : allows to get index settings (https only) +* - editSettings : allows to change index settings (https only) +* @param {Object} [params] - Optionnal parameters to set for the key +* @param {number} params.validity - Number of seconds after which the key will +* be automatically removed (0 means no time limit for this key) +* @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour +* @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call +* @param {string} params.description - A description for your key +* @param {string[]} params.referers - A list of authorized referers +* @param {Object} params.queryParameters - Force the key to use specific query parameters +* @param {Function} callback - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the added API key +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.addUserKey(['search'], { +* validity: 300, +* maxQueriesPerIPPerHour: 2000, +* maxHitsPerQuery: 3, +* description: 'Eat three fruits', +* referers: ['*.algolia.com'], +* queryParameters: { +* tagFilters: ['public'], +* } +* }) +* @see {@link https://www.algolia.com/doc/rest_api#AddIndexKey|Algolia REST API Documentation} +* +* @deprecated see client.addApiKey() +*/ +Index.prototype.addApiKey = deprecate(function(acls, params, callback) { + var isArray = require('isarray'); + var usage = 'Usage: index.addApiKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys', + body: postObj, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.addApiKey()', 'client.addApiKey()')); + +/** +* @deprecated use client.addApiKey() +*/ +Index.prototype.addUserKeyWithValidity = deprecate(function deprecatedAddUserKeyWithValidity(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('index.addUserKeyWithValidity()', 'client.addApiKey()')); + +/* +* @deprecated see client.updateApiKey() +*/ +Index.prototype.updateUserKey = deprecate(function(key, acls, params, callback) { + return this.updateApiKey(key, acls, params, callback); +}, deprecatedMessage('index.updateUserKey()', 'client.updateApiKey()')); + +/** +* Update an existing API key of this index +* @param {string} key - The key to update +* @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that +* can contains the following values: +* - search: allow to search (https and http) +* - addObject: allows to add/update an object in the index (https only) +* - deleteObject : allows to delete an existing object (https only) +* - deleteIndex : allows to delete index content (https only) +* - settings : allows to get index settings (https only) +* - editSettings : allows to change index settings (https only) +* @param {Object} [params] - Optionnal parameters to set for the key +* @param {number} params.validity - Number of seconds after which the key will +* be automatically removed (0 means no time limit for this key) +* @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour +* @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call +* @param {string} params.description - A description for your key +* @param {string[]} params.referers - A list of authorized referers +* @param {Object} params.queryParameters - Force the key to use specific query parameters +* @param {Function} callback - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with user keys list +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.updateApiKey('APIKEY', ['search'], { +* validity: 300, +* maxQueriesPerIPPerHour: 2000, +* maxHitsPerQuery: 3, +* description: 'Eat three fruits', +* referers: ['*.algolia.com'], +* queryParameters: { +* tagFilters: ['public'], +* } +* }) +* @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} +* +* @deprecated see client.updateApiKey() +*/ +Index.prototype.updateApiKey = deprecate(function(key, acls, params, callback) { + var isArray = require('isarray'); + var usage = 'Usage: index.updateApiKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.updateApiKey()', 'client.updateApiKey()')); diff --git a/node_modules/algoliasearch/src/IndexBrowser.js b/node_modules/algoliasearch/src/IndexBrowser.js new file mode 100644 index 00000000..b035a200 --- /dev/null +++ b/node_modules/algoliasearch/src/IndexBrowser.js @@ -0,0 +1,39 @@ +'use strict'; + +// This is the object returned by the `index.browseAll()` method + +module.exports = IndexBrowser; + +var inherits = require('inherits'); +var EventEmitter = require('events').EventEmitter; + +function IndexBrowser() { +} + +inherits(IndexBrowser, EventEmitter); + +IndexBrowser.prototype.stop = function() { + this._stopped = true; + this._clean(); +}; + +IndexBrowser.prototype._end = function() { + this.emit('end'); + this._clean(); +}; + +IndexBrowser.prototype._error = function(err) { + this.emit('error', err); + this._clean(); +}; + +IndexBrowser.prototype._result = function(content) { + this.emit('result', content); +}; + +IndexBrowser.prototype._clean = function() { + this.removeAllListeners('stop'); + this.removeAllListeners('end'); + this.removeAllListeners('error'); + this.removeAllListeners('result'); +}; diff --git a/node_modules/algoliasearch/src/IndexCore.js b/node_modules/algoliasearch/src/IndexCore.js new file mode 100644 index 00000000..f06b783b --- /dev/null +++ b/node_modules/algoliasearch/src/IndexCore.js @@ -0,0 +1,383 @@ +var buildSearchMethod = require('./buildSearchMethod.js'); +var deprecate = require('./deprecate.js'); +var deprecatedMessage = require('./deprecatedMessage.js'); + +module.exports = IndexCore; + +/* +* Index class constructor. +* You should not use this method directly but use initIndex() function +*/ +function IndexCore(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; +} + +/* +* Clear all queries in cache +*/ +IndexCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/* +* Search inside the index using XMLHttpRequest request (Using a POST query to +* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). +* +* @param {string} [query] the full text query +* @param {object} [args] (optional) if set, contains an object with query parameters: +* - page: (integer) Pagination parameter used to select the page to retrieve. +* Page is zero-based and defaults to 0. Thus, +* to retrieve the 10th page you need to set page=9 +* - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. +* - attributesToRetrieve: a string that contains the list of object attributes +* you want to retrieve (let you minimize the answer size). +* Attributes are separated with a comma (for example "name,address"). +* You can also use an array (for example ["name","address"]). +* By default, all attributes are retrieved. You can also use '*' to retrieve all +* values when an attributesToRetrieve setting is specified for your index. +* - attributesToHighlight: a string that contains the list of attributes you +* want to highlight according to the query. +* Attributes are separated by a comma. You can also use an array (for example ["name","address"]). +* If an attribute has no match for the query, the raw value is returned. +* By default all indexed text attributes are highlighted. +* You can use `*` if you want to highlight all textual attributes. +* Numerical attributes are not highlighted. +* A matchLevel is returned for each highlighted attribute and can contain: +* - full: if all the query terms were found in the attribute, +* - partial: if only some of the query terms were found, +* - none: if none of the query terms were found. +* - attributesToSnippet: a string that contains the list of attributes to snippet alongside +* the number of words to return (syntax is `attributeName:nbWords`). +* Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). +* You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). +* By default no snippet is computed. +* - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. +* Defaults to 3. +* - minWordSizefor2Typos: the minimum number of characters in a query word +* to accept two typos in this word. Defaults to 7. +* - getRankingInfo: if set to 1, the result hits will contain ranking +* information in _rankingInfo attribute. +* - aroundLatLng: search for entries around a given +* latitude/longitude (specified as two floats separated by a comma). +* For example aroundLatLng=47.316669,5.016670). +* You can specify the maximum distance in meters with the aroundRadius parameter (in meters) +* and the precision for ranking with aroundPrecision +* (for example if you set aroundPrecision=100, two objects that are distant of +* less than 100m will be considered as identical for "geo" ranking parameter). +* At indexing, you should specify geoloc of an object with the _geoloc attribute +* (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) +* - insideBoundingBox: search entries inside a given area defined by the two extreme points +* of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). +* For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). +* At indexing, you should specify geoloc of an object with the _geoloc attribute +* (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) +* - numericFilters: a string that contains the list of numeric filters you want to +* apply separated by a comma. +* The syntax of one filter is `attributeName` followed by `operand` followed by `value`. +* Supported operands are `<`, `<=`, `=`, `>` and `>=`. +* You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. +* You can also use an array (for example numericFilters: ["price>100","price<1000"]). +* - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. +* To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). +* You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] +* means tag1 AND (tag2 OR tag3). +* At indexing, tags should be added in the _tags** attribute +* of objects (for example {"_tags":["tag1","tag2"]}). +* - facetFilters: filter the query by a list of facets. +* Facets are separated by commas and each facet is encoded as `attributeName:value`. +* For example: `facetFilters=category:Book,author:John%20Doe`. +* You can also use an array (for example `["category:Book","author:John%20Doe"]`). +* - facets: List of object attributes that you want to use for faceting. +* Comma separated list: `"category,author"` or array `['category','author']` +* Only attributes that have been added in **attributesForFaceting** index setting +* can be used in this parameter. +* You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. +* - queryType: select how the query words are interpreted, it can be one of the following value: +* - prefixAll: all query words are interpreted as prefixes, +* - prefixLast: only the last word is interpreted as a prefix (default behavior), +* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. +* - optionalWords: a string that contains the list of words that should +* be considered as optional when found in the query. +* Comma separated and array are accepted. +* - distinct: If set to 1, enable the distinct feature (disabled by default) +* if the attributeForDistinct index setting is set. +* This feature is similar to the SQL "distinct" keyword: when enabled +* in a query with the distinct=1 parameter, +* all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. +* For example, if the chosen attribute is show_name and several hits have +* the same value for show_name, then only the best +* one is kept and others are removed. +* - restrictSearchableAttributes: List of attributes you want to use for +* textual search (must be a subset of the attributesToIndex index setting) +* either comma separated or as an array +* @param {function} [callback] the result callback called with two arguments: +* error: null or Error('message'). If false, the content contains the error. +* content: the server answer that contains the list of results. +*/ +IndexCore.prototype.search = buildSearchMethod('query'); + +/* +* -- BETA -- +* Search a record similar to the query inside the index using XMLHttpRequest request (Using a POST query to +* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). +* +* @param {string} [query] the similar query +* @param {object} [args] (optional) if set, contains an object with query parameters. +* All search parameters are supported (see search function), restrictSearchableAttributes and facetFilters +* are the two most useful to restrict the similar results and get more relevant content +*/ +IndexCore.prototype.similarSearch = buildSearchMethod('similarQuery'); + +/* +* Browse index content. The response content will have a `cursor` property that you can use +* to browse subsequent pages for this query. Use `index.browseFrom(cursor)` when you want. +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @param {Function} [callback] - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the browse result +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.browse('cool songs', { +* tagFilters: 'public,comments', +* hitsPerPage: 500 +* }, callback); +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +IndexCore.prototype.browse = function(query, queryParameters, callback) { + var merge = require('./merge.js'); + + var indexObj = this; + + var page; + var hitsPerPage; + + // we check variadic calls that are not the one defined + // .browse()/.browse(fn) + // => page = 0 + if (arguments.length === 0 || arguments.length === 1 && typeof arguments[0] === 'function') { + page = 0; + callback = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'number') { + // .browse(2)/.browse(2, 10)/.browse(2, fn)/.browse(2, 10, fn) + page = arguments[0]; + if (typeof arguments[1] === 'number') { + hitsPerPage = arguments[1]; + } else if (typeof arguments[1] === 'function') { + callback = arguments[1]; + hitsPerPage = undefined; + } + query = undefined; + queryParameters = undefined; + } else if (typeof arguments[0] === 'object') { + // .browse(queryParameters)/.browse(queryParameters, cb) + if (typeof arguments[1] === 'function') { + callback = arguments[1]; + } + queryParameters = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { + // .browse(query, cb) + callback = arguments[1]; + queryParameters = undefined; + } + + // otherwise it's a .browse(query)/.browse(query, queryParameters)/.browse(query, queryParameters, cb) + + // get search query parameters combining various possible calls + // to .browse(); + queryParameters = merge({}, queryParameters || {}, { + page: page, + hitsPerPage: hitsPerPage, + query: query + }); + + var params = this.as._getSearchParams(queryParameters, ''); + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse', + body: {params: params}, + hostType: 'read', + callback: callback + }); +}; + +/* +* Continue browsing from a previous position (cursor), obtained via a call to `.browse()`. +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @param {Function} [callback] - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the browse result +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.browseFrom('14lkfsakl32', callback); +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +IndexCore.prototype.browseFrom = function(cursor, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/browse', + body: {cursor: cursor}, + hostType: 'read', + callback: callback + }); +}; + +/* +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* +* @param {string} params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} params.facetQuery Query for the facet search +* @param {string} [params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +* @param callback (optional) +*/ +IndexCore.prototype.searchForFacetValues = function(params, callback) { + var clone = require('./clone.js'); + var omit = require('./omit.js'); + var usage = 'Usage: index.searchForFacetValues({facetName, facetQuery, ...params}[, callback])'; + + if (params.facetName === undefined || params.facetQuery === undefined) { + throw new Error(usage); + } + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = this.as._getSearchParams(filteredParams, ''); + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + + encodeURIComponent(this.indexName) + '/facets/' + encodeURIComponent(facetName) + '/query', + hostType: 'read', + body: {params: searchParameters}, + callback: callback + }); +}; + +IndexCore.prototype.searchFacet = deprecate(function(params, callback) { + return this.searchForFacetValues(params, callback); +}, deprecatedMessage( + 'index.searchFacet(params[, callback])', + 'index.searchForFacetValues(params[, callback])' +)); + +IndexCore.prototype._search = function(params, url, callback, additionalUA) { + return this.as._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url || '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: {params: params}, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: {params: params} + }, + callback: callback, + additionalUA: additionalUA + }); +}; + +/* +* Get an object from this index +* +* @param objectID the unique identifier of the object to retrieve +* @param attrs (optional) if set, contains the array of attribute names to retrieve +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the object to retrieve or the error message if a failure occured +*/ +IndexCore.prototype.getObject = function(objectID, attrs, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof attrs === 'function') { + callback = attrs; + attrs = undefined; + } + + var params = ''; + if (attrs !== undefined) { + params = '?attributes='; + for (var i = 0; i < attrs.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attrs[i]; + } + } + + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + hostType: 'read', + callback: callback + }); +}; + +/* +* Get several objects from this index +* +* @param objectIDs the array of unique identifier of objects to retrieve +*/ +IndexCore.prototype.getObjects = function(objectIDs, attributesToRetrieve, callback) { + var isArray = require('isarray'); + var map = require('./map.js'); + + var usage = 'Usage: index.getObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + + if (arguments.length === 1 || typeof attributesToRetrieve === 'function') { + callback = attributesToRetrieve; + attributesToRetrieve = undefined; + } + + var body = { + requests: map(objectIDs, function prepareRequest(objectID) { + var request = { + indexName: indexObj.indexName, + objectID: objectID + }; + + if (attributesToRetrieve) { + request.attributesToRetrieve = attributesToRetrieve.join(','); + } + + return request; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/objects', + hostType: 'read', + body: body, + callback: callback + }); +}; + +IndexCore.prototype.as = null; +IndexCore.prototype.indexName = null; +IndexCore.prototype.typeAheadArgs = null; +IndexCore.prototype.typeAheadValueOption = null; diff --git a/node_modules/algoliasearch/src/browser/builds/algoliasearch.angular.js b/node_modules/algoliasearch/src/browser/builds/algoliasearch.angular.js new file mode 100644 index 00000000..c1db3343 --- /dev/null +++ b/node_modules/algoliasearch/src/browser/builds/algoliasearch.angular.js @@ -0,0 +1,201 @@ +'use strict'; + +// This is the AngularJS Algolia Search module +// It's using $http to do requests with a JSONP fallback +// $q promises are returned + +var inherits = require('inherits'); + +var forEach = require('foreach'); + +var AlgoliaSearch = require('../../AlgoliaSearch'); +var errors = require('../../errors'); +var inlineHeaders = require('../inline-headers'); +var jsonpRequest = require('../jsonp-request'); +var places = require('../../places.js'); + +// expose original algoliasearch fn in window +window.algoliasearch = require('./algoliasearch'); + +if (process.env.NODE_ENV === 'debug') { + require('debug').enable('algoliasearch*'); +} + +window.angular.module('algoliasearch', []) + .service('algolia', ['$http', '$q', '$timeout', function algoliaSearchService($http, $q, $timeout) { + function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = require('../../clone.js'); + + opts = cloneDeep(opts || {}); + + opts._ua = opts._ua || algoliasearch.ua; + + return new AlgoliaSearchAngular(applicationID, apiKey, opts); + } + + algoliasearch.version = require('../../version.js'); + algoliasearch.ua = 'Algolia for AngularJS ' + algoliasearch.version; + algoliasearch.initPlaces = places(algoliasearch); + + // we expose into window no matter how we are used, this will allow + // us to easily debug any website running algolia + window.__algolia = { + debug: require('debug'), + algoliasearch: algoliasearch + }; + + function AlgoliaSearchAngular() { + // call AlgoliaSearch constructor + AlgoliaSearch.apply(this, arguments); + } + + inherits(AlgoliaSearchAngular, AlgoliaSearch); + + AlgoliaSearchAngular.prototype._request = function request(url, opts) { + // Support most Angular.js versions by using $q.defer() instead + // of the new $q() constructor everywhere we need a promise + var deferred = $q.defer(); + var resolve = deferred.resolve; + var reject = deferred.reject; + + var timedOut; + var body = opts.body; + + url = inlineHeaders(url, opts.headers); + + var timeoutDeferred = $q.defer(); + var timeoutPromise = timeoutDeferred.promise; + + $timeout(function timedout() { + timedOut = true; + // will cancel the xhr + timeoutDeferred.resolve('test'); + reject(new errors.RequestTimeout()); + }, opts.timeouts.complete); + + var requestHeaders = {}; + + // "remove" (set to undefined) possible globally set headers + // in $httpProvider.defaults.headers.common + // otherwise we might fail sometimes + // ref: https://github.com/algolia/algoliasearch-client-js/issues/135 + forEach( + $http.defaults.headers.common, + function removeIt(headerValue, headerName) { + requestHeaders[headerName] = undefined; + } + ); + + requestHeaders.accept = 'application/json'; + + if (body) { + if (opts.method === 'POST') { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests + requestHeaders['content-type'] = 'application/x-www-form-urlencoded'; + } else { + requestHeaders['content-type'] = 'application/json'; + } + } + + $http({ + url: url, + method: opts.method, + data: body, + cache: false, + timeout: timeoutPromise, + headers: requestHeaders, + transformResponse: transformResponse, + // if client uses $httpProvider.defaults.withCredentials = true, + // we revert it to false to avoid CORS failure + withCredentials: false + }).then(success, error); + + function success(response) { + resolve({ + statusCode: response.status, + headers: response.headers, + body: JSON.parse(response.data), + responseText: response.data + }); + } + + // we force getting the raw data because we need it so + // for cache keys + function transformResponse(data) { + return data; + } + + function error(response) { + if (timedOut) { + return; + } + + // network error + if (response.status === 0) { + reject( + new errors.Network({ + more: response + }) + ); + return; + } + + resolve({ + body: JSON.parse(response.data), + statusCode: response.status + }); + } + + return deferred.promise; + }; + + // using IE8 or IE9 we will always end up here + // AngularJS does not fallback to XDomainRequest + AlgoliaSearchAngular.prototype._request.fallback = function requestFallback(url, opts) { + url = inlineHeaders(url, opts.headers); + + var deferred = $q.defer(); + var resolve = deferred.resolve; + var reject = deferred.reject; + + jsonpRequest(url, opts, function jsonpRequestDone(err, content) { + if (err) { + reject(err); + return; + } + + resolve(content); + }); + + return deferred.promise; + }; + + AlgoliaSearchAngular.prototype._promise = { + reject: function(val) { + return $q.reject(val); + }, + resolve: function(val) { + // http://www.bennadel.com/blog/2735-q-when-is-the-missing-q-resolve-method-in-angularjs.htm + return $q.when(val); + }, + delay: function(ms) { + var deferred = $q.defer(); + var resolve = deferred.resolve; + + $timeout(resolve, ms); + + return deferred.promise; + }, + all: function(promises) { + return $q.all(promises); + } + }; + + return { + Client: function(applicationID, apiKey, options) { + return algoliasearch(applicationID, apiKey, options); + }, + ua: algoliasearch.ua, + version: algoliasearch.version + }; + }]); diff --git a/node_modules/algoliasearch/src/browser/builds/algoliasearch.jquery.js b/node_modules/algoliasearch/src/browser/builds/algoliasearch.jquery.js new file mode 100644 index 00000000..3b8bebb9 --- /dev/null +++ b/node_modules/algoliasearch/src/browser/builds/algoliasearch.jquery.js @@ -0,0 +1,147 @@ +'use strict'; + +// This is the jQuery Algolia Search module +// It's using $.ajax to do requests with a JSONP fallback +// jQuery promises are returned + +var inherits = require('inherits'); + +var AlgoliaSearch = require('../../AlgoliaSearch'); +var errors = require('../../errors'); +var inlineHeaders = require('../inline-headers'); +var jsonpRequest = require('../jsonp-request'); +var places = require('../../places.js'); + +// expose original algoliasearch fn in window +window.algoliasearch = require('./algoliasearch'); + +if (process.env.NODE_ENV === 'debug') { + require('debug').enable('algoliasearch*'); +} + +function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = require('../../clone.js'); + + opts = cloneDeep(opts || {}); + + opts._ua = opts._ua || algoliasearch.ua; + + return new AlgoliaSearchJQuery(applicationID, apiKey, opts); +} + +algoliasearch.version = require('../../version.js'); +algoliasearch.ua = 'Algolia for jQuery ' + algoliasearch.version; +algoliasearch.initPlaces = places(algoliasearch); + +// we expose into window no matter how we are used, this will allow +// us to easily debug any website running algolia +window.__algolia = { + debug: require('debug'), + algoliasearch: algoliasearch +}; + +var $ = window.jQuery; + +$.algolia = { + Client: algoliasearch, + ua: algoliasearch.ua, + version: algoliasearch.version +}; + +function AlgoliaSearchJQuery() { + // call AlgoliaSearch constructor + AlgoliaSearch.apply(this, arguments); +} + +inherits(AlgoliaSearchJQuery, AlgoliaSearch); + +AlgoliaSearchJQuery.prototype._request = function request(url, opts) { + return new $.Deferred(function(deferred) { + var body = opts.body; + + url = inlineHeaders(url, opts.headers); + + var requestHeaders = { + accept: 'application/json' + }; + + if (body) { + if (opts.method === 'POST') { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests + requestHeaders['content-type'] = 'application/x-www-form-urlencoded'; + } else { + requestHeaders['content-type'] = 'application/json'; + } + } + + $.ajax(url, { + type: opts.method, + timeout: opts.timeouts.complete, + dataType: 'json', + data: body, + headers: requestHeaders, + complete: function onComplete(jqXHR, textStatus/* , error*/) { + if (textStatus === 'timeout') { + deferred.reject(new errors.RequestTimeout()); + return; + } + + if (jqXHR.status === 0) { + deferred.reject( + new errors.Network({ + more: jqXHR + }) + ); + return; + } + + deferred.resolve({ + statusCode: jqXHR.status, + body: jqXHR.responseJSON, + responseText: jqXHR.responseText, + headers: jqXHR.getAllResponseHeaders() + }); + } + }); + }).promise(); +}; + +// using IE8 or IE9 we will always end up here +// jQuery does not not fallback to XDomainRequest +AlgoliaSearchJQuery.prototype._request.fallback = function requestFallback(url, opts) { + url = inlineHeaders(url, opts.headers); + + return new $.Deferred(function wrapJsonpRequest(deferred) { + jsonpRequest(url, opts, function jsonpRequestDone(err, content) { + if (err) { + deferred.reject(err); + return; + } + + deferred.resolve(content); + }); + }).promise(); +}; + +AlgoliaSearchJQuery.prototype._promise = { + reject: function reject(val) { + return new $.Deferred(function rejectDeferred(deferred) { + deferred.reject(val); + }).promise(); + }, + resolve: function resolve(val) { + return new $.Deferred(function resolveDeferred(deferred) { + deferred.resolve(val); + }).promise(); + }, + delay: function delay(ms) { + return new $.Deferred(function delayResolve(deferred) { + setTimeout(function resolveDeferred() { + deferred.resolve(); + }, ms); + }).promise(); + }, + all: function all(promises) { + return $.when.apply(null, promises); + } +}; diff --git a/node_modules/algoliasearch/src/browser/builds/algoliasearch.js b/node_modules/algoliasearch/src/browser/builds/algoliasearch.js new file mode 100644 index 00000000..e8b820db --- /dev/null +++ b/node_modules/algoliasearch/src/browser/builds/algoliasearch.js @@ -0,0 +1,6 @@ +'use strict'; + +var AlgoliaSearch = require('../../AlgoliaSearch.js'); +var createAlgoliasearch = require('../createAlgoliasearch.js'); + +module.exports = createAlgoliasearch(AlgoliaSearch); diff --git a/node_modules/algoliasearch/src/browser/builds/algoliasearchLite.js b/node_modules/algoliasearch/src/browser/builds/algoliasearchLite.js new file mode 100644 index 00000000..ca0b4b13 --- /dev/null +++ b/node_modules/algoliasearch/src/browser/builds/algoliasearchLite.js @@ -0,0 +1,6 @@ +'use strict'; + +var AlgoliaSearchCore = require('../../AlgoliaSearchCore.js'); +var createAlgoliasearch = require('../createAlgoliasearch.js'); + +module.exports = createAlgoliasearch(AlgoliaSearchCore, '(lite) '); diff --git a/node_modules/algoliasearch/src/browser/createAlgoliasearch.js b/node_modules/algoliasearch/src/browser/createAlgoliasearch.js new file mode 100644 index 00000000..a2a488b8 --- /dev/null +++ b/node_modules/algoliasearch/src/browser/createAlgoliasearch.js @@ -0,0 +1,233 @@ +'use strict'; + +var global = require('global'); +var Promise = global.Promise || require('es6-promise').Promise; + +// This is the standalone browser build entry point +// Browser implementation of the Algolia Search JavaScript client, +// using XMLHttpRequest, XDomainRequest and JSONP as fallback +module.exports = function createAlgoliasearch(AlgoliaSearch, uaSuffix) { + var inherits = require('inherits'); + var errors = require('../errors'); + var inlineHeaders = require('./inline-headers'); + var jsonpRequest = require('./jsonp-request'); + var places = require('../places.js'); + uaSuffix = uaSuffix || ''; + + if (process.env.NODE_ENV === 'debug') { + require('debug').enable('algoliasearch*'); + } + + function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = require('../clone.js'); + + opts = cloneDeep(opts || {}); + + opts._ua = opts._ua || algoliasearch.ua; + + return new AlgoliaSearchBrowser(applicationID, apiKey, opts); + } + + algoliasearch.version = require('../version.js'); + algoliasearch.ua = 'Algolia for vanilla JavaScript ' + uaSuffix + algoliasearch.version; + algoliasearch.initPlaces = places(algoliasearch); + + // we expose into window no matter how we are used, this will allow + // us to easily debug any website running algolia + global.__algolia = { + debug: require('debug'), + algoliasearch: algoliasearch + }; + + var support = { + hasXMLHttpRequest: 'XMLHttpRequest' in global, + hasXDomainRequest: 'XDomainRequest' in global + }; + + if (support.hasXMLHttpRequest) { + support.cors = 'withCredentials' in new XMLHttpRequest(); + } + + function AlgoliaSearchBrowser() { + // call AlgoliaSearch constructor + AlgoliaSearch.apply(this, arguments); + } + + inherits(AlgoliaSearchBrowser, AlgoliaSearch); + + AlgoliaSearchBrowser.prototype._request = function request(url, opts) { + return new Promise(function wrapRequest(resolve, reject) { + // no cors or XDomainRequest, no request + if (!support.cors && !support.hasXDomainRequest) { + // very old browser, not supported + reject(new errors.Network('CORS not supported')); + return; + } + + url = inlineHeaders(url, opts.headers); + + var body = opts.body; + var req = support.cors ? new XMLHttpRequest() : new XDomainRequest(); + var reqTimeout; + var timedOut; + var connected = false; + + reqTimeout = setTimeout(onTimeout, opts.timeouts.connect); + // we set an empty onprogress listener + // so that XDomainRequest on IE9 is not aborted + // refs: + // - https://github.com/algolia/algoliasearch-client-js/issues/76 + // - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + req.onprogress = onProgress; + if ('onreadystatechange' in req) req.onreadystatechange = onReadyStateChange; + req.onload = onLoad; + req.onerror = onError; + + // do not rely on default XHR async flag, as some analytics code like hotjar + // breaks it and set it to false by default + if (req instanceof XMLHttpRequest) { + req.open(opts.method, url, true); + + // The Analytics API never accepts Auth headers as query string + // this option exists specifically for them. + if (opts.forceAuthHeaders) { + req.setRequestHeader( + 'x-algolia-application-id', + opts.headers['x-algolia-application-id'] + ); + req.setRequestHeader( + 'x-algolia-api-key', + opts.headers['x-algolia-api-key'] + ); + } + } else { + req.open(opts.method, url); + } + + // headers are meant to be sent after open + if (support.cors) { + if (body) { + if (opts.method === 'POST') { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests + req.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); + } else { + req.setRequestHeader('content-type', 'application/json'); + } + } + req.setRequestHeader('accept', 'application/json'); + } + + if (body) { + req.send(body); + } else { + req.send(); + } + + // event object not received in IE8, at least + // but we do not use it, still important to note + function onLoad(/* event */) { + // When browser does not supports req.timeout, we can + // have both a load and timeout event, since handled by a dumb setTimeout + if (timedOut) { + return; + } + + clearTimeout(reqTimeout); + + var out; + + try { + out = { + body: JSON.parse(req.responseText), + responseText: req.responseText, + statusCode: req.status, + // XDomainRequest does not have any response headers + headers: req.getAllResponseHeaders && req.getAllResponseHeaders() || {} + }; + } catch (e) { + out = new errors.UnparsableJSON({ + more: req.responseText + }); + } + + if (out instanceof errors.UnparsableJSON) { + reject(out); + } else { + resolve(out); + } + } + + function onError(event) { + if (timedOut) { + return; + } + + clearTimeout(reqTimeout); + + // error event is trigerred both with XDR/XHR on: + // - DNS error + // - unallowed cross domain request + reject( + new errors.Network({ + more: event + }) + ); + } + + function onTimeout() { + timedOut = true; + req.abort(); + + reject(new errors.RequestTimeout()); + } + + function onConnect() { + connected = true; + clearTimeout(reqTimeout); + reqTimeout = setTimeout(onTimeout, opts.timeouts.complete); + } + + function onProgress() { + if (!connected) onConnect(); + } + + function onReadyStateChange() { + if (!connected && req.readyState > 1) onConnect(); + } + }); + }; + + AlgoliaSearchBrowser.prototype._request.fallback = function requestFallback(url, opts) { + url = inlineHeaders(url, opts.headers); + + return new Promise(function wrapJsonpRequest(resolve, reject) { + jsonpRequest(url, opts, function jsonpRequestDone(err, content) { + if (err) { + reject(err); + return; + } + + resolve(content); + }); + }); + }; + + AlgoliaSearchBrowser.prototype._promise = { + reject: function rejectPromise(val) { + return Promise.reject(val); + }, + resolve: function resolvePromise(val) { + return Promise.resolve(val); + }, + delay: function delayPromise(ms) { + return new Promise(function resolveOnTimeout(resolve/* , reject*/) { + setTimeout(resolve, ms); + }); + }, + all: function all(promises) { + return Promise.all(promises); + } + }; + + return algoliasearch; +}; diff --git a/node_modules/algoliasearch/src/browser/inline-headers.js b/node_modules/algoliasearch/src/browser/inline-headers.js new file mode 100644 index 00000000..7339d38a --- /dev/null +++ b/node_modules/algoliasearch/src/browser/inline-headers.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = inlineHeaders; + +var encode = require('querystring-es3/encode'); + +function inlineHeaders(url, headers) { + if (/\?/.test(url)) { + url += '&'; + } else { + url += '?'; + } + + return url + encode(headers); +} diff --git a/node_modules/algoliasearch/src/browser/jsonp-request.js b/node_modules/algoliasearch/src/browser/jsonp-request.js new file mode 100644 index 00000000..1aed7631 --- /dev/null +++ b/node_modules/algoliasearch/src/browser/jsonp-request.js @@ -0,0 +1,126 @@ +'use strict'; + +module.exports = jsonpRequest; + +var errors = require('../errors'); + +var JSONPCounter = 0; + +function jsonpRequest(url, opts, cb) { + if (opts.method !== 'GET') { + cb(new Error('Method ' + opts.method + ' ' + url + ' is not supported by JSONP.')); + return; + } + + opts.debug('JSONP: start'); + + var cbCalled = false; + var timedOut = false; + + JSONPCounter += 1; + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + var cbName = 'algoliaJSONP_' + JSONPCounter; + var done = false; + + window[cbName] = function(data) { + removeGlobals(); + + if (timedOut) { + opts.debug('JSONP: Late answer, ignoring'); + return; + } + + cbCalled = true; + + clean(); + + cb(null, { + body: data, + responseText: JSON.stringify(data)/* , + // We do not send the statusCode, there's no statusCode in JSONP, it will be + // computed using data.status && data.message like with XDR + statusCode*/ + }); + }; + + // add callback by hand + url += '&callback=' + cbName; + + // add body params manually + if (opts.jsonBody && opts.jsonBody.params) { + url += '&' + opts.jsonBody.params; + } + + var ontimeout = setTimeout(timeout, opts.timeouts.complete); + + // script onreadystatechange needed only for + // <= IE8 + // https://github.com/angular/angular.js/issues/4523 + script.onreadystatechange = readystatechange; + script.onload = success; + script.onerror = error; + + script.async = true; + script.defer = true; + script.src = url; + head.appendChild(script); + + function success() { + opts.debug('JSONP: success'); + + if (done || timedOut) { + return; + } + + done = true; + + // script loaded but did not call the fn => script loading error + if (!cbCalled) { + opts.debug('JSONP: Fail. Script loaded but did not call the callback'); + clean(); + cb(new errors.JSONPScriptFail()); + } + } + + function readystatechange() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + success(); + } + } + + function clean() { + clearTimeout(ontimeout); + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + head.removeChild(script); + } + + function removeGlobals() { + try { + delete window[cbName]; + delete window[cbName + '_loaded']; + } catch (e) { + window[cbName] = window[cbName + '_loaded'] = undefined; + } + } + + function timeout() { + opts.debug('JSONP: Script timeout'); + timedOut = true; + clean(); + cb(new errors.RequestTimeout()); + } + + function error() { + opts.debug('JSONP: Script error'); + + if (done || timedOut) { + return; + } + + clean(); + cb(new errors.JSONPScriptError()); + } +} diff --git a/node_modules/algoliasearch/src/browser/migration-layer/is-using-latest.js b/node_modules/algoliasearch/src/browser/migration-layer/is-using-latest.js new file mode 100644 index 00000000..f8e29d4e --- /dev/null +++ b/node_modules/algoliasearch/src/browser/migration-layer/is-using-latest.js @@ -0,0 +1,23 @@ +'use strict'; + +// this module helps finding if the current page is using +// the cdn.jsdelivr.net/algoliasearch/latest/$BUILDNAME.min.js version + +module.exports = isUsingLatest; + +function isUsingLatest(buildName) { + var toFind = new RegExp('cdn\\.jsdelivr\\.net/algoliasearch/latest/' + + buildName.replace('.', '\\.') + // algoliasearch, algoliasearch.angular + '(?:\\.min)?\\.js$'); // [.min].js + + var scripts = document.getElementsByTagName('script'); + var found = false; + for (var currentScript = 0, nbScripts = scripts.length; currentScript < nbScripts; currentScript++) { + if (scripts[currentScript].src && toFind.test(scripts[currentScript].src)) { + found = true; + break; + } + } + + return found; +} diff --git a/node_modules/algoliasearch/src/browser/migration-layer/load-v2.js b/node_modules/algoliasearch/src/browser/migration-layer/load-v2.js new file mode 100644 index 00000000..b3470baa --- /dev/null +++ b/node_modules/algoliasearch/src/browser/migration-layer/load-v2.js @@ -0,0 +1,50 @@ +'use strict'; + +module.exports = loadV2; + +function loadV2(buildName) { + var loadScript = require('load-script'); + var v2ScriptUrl = '//cdn.jsdelivr.net/algoliasearch/2/' + buildName + '.min.js'; + + var message = '-- AlgoliaSearch `latest` warning --\n' + + 'Warning, you are using the `latest` version string from jsDelivr to load the AlgoliaSearch library.\n' + + 'Using `latest` is no more recommended, you should load //cdn.jsdelivr.net/algoliasearch/2/algoliasearch.min.js\n\n' + + 'Also, we updated the AlgoliaSearch JavaScript client to V3. If you want to upgrade,\n' + + 'please read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n' + + '-- /AlgoliaSearch `latest` warning --'; + + if (window.console) { + if (window.console.warn) { + window.console.warn(message); + } else if (window.console.log) { + window.console.log(message); + } + } + + // If current script loaded asynchronously, + // it will load the script with DOMElement + // otherwise, it will load the script with document.write + try { + // why \x3c? http://stackoverflow.com/a/236106/147079 + document.write('\x3Cscript>window.ALGOLIA_SUPPORTS_DOCWRITE = true\x3C/script>'); + + if (window.ALGOLIA_SUPPORTS_DOCWRITE === true) { + document.write('\x3Cscript src="' + v2ScriptUrl + '">\x3C/script>'); + scriptLoaded('document.write')(); + } else { + loadScript(v2ScriptUrl, scriptLoaded('DOMElement')); + } + } catch (e) { + loadScript(v2ScriptUrl, scriptLoaded('DOMElement')); + } +} + +function scriptLoaded(method) { + return function log() { + var message = 'AlgoliaSearch: loaded V2 script using ' + method; + + if (window.console && window.console.log) { + window.console.log(message); + } + }; +} diff --git a/node_modules/algoliasearch/src/browser/migration-layer/old-globals.js b/node_modules/algoliasearch/src/browser/migration-layer/old-globals.js new file mode 100644 index 00000000..1f40c0d2 --- /dev/null +++ b/node_modules/algoliasearch/src/browser/migration-layer/old-globals.js @@ -0,0 +1,26 @@ +'use strict'; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +module.exports = oldGlobals; + +// put old window.AlgoliaSearch.. into window. again so that +// users upgrading to V3 without changing their code, will be warned +function oldGlobals() { + var message = '-- AlgoliaSearch V2 => V3 error --\n' + + 'You are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\n' + + 'Please read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n' + + '-- /AlgoliaSearch V2 => V3 error --'; + + window.AlgoliaSearch = function() { + throw new Error(message); + }; + + window.AlgoliaSearchHelper = function() { + throw new Error(message); + }; + + window.AlgoliaExplainResults = function() { + throw new Error(message); + }; +} diff --git a/node_modules/algoliasearch/src/browser/migration-layer/script.js b/node_modules/algoliasearch/src/browser/migration-layer/script.js new file mode 100644 index 00000000..9d12315f --- /dev/null +++ b/node_modules/algoliasearch/src/browser/migration-layer/script.js @@ -0,0 +1,25 @@ +'use strict'; + +// This script will be browserified and prepended to the normal build +// directly in window, not wrapped in any module definition +// To avoid cases where we are loaded with /latest/ along with +migrationLayer(process.env.ALGOLIA_BUILDNAME); + +// Now onto the V2 related code: +// If the client is using /latest/$BUILDNAME.min.js, load V2 of the library +// +// Otherwise, setup a migration layer that will throw on old constructors like +// new AlgoliaSearch(). +// So that users upgrading from v2 to v3 will have a clear information +// message on what to do if they did not read the migration guide +function migrationLayer(buildName) { + var isUsingLatest = require('./is-using-latest'); + var loadV2 = require('./load-v2'); + var oldGlobals = require('./old-globals'); + + if (isUsingLatest(buildName)) { + loadV2(buildName); + } else { + oldGlobals(); + } +} diff --git a/node_modules/algoliasearch/src/buildSearchMethod.js b/node_modules/algoliasearch/src/buildSearchMethod.js new file mode 100644 index 00000000..33b1d52b --- /dev/null +++ b/node_modules/algoliasearch/src/buildSearchMethod.js @@ -0,0 +1,67 @@ +module.exports = buildSearchMethod; + +var errors = require('./errors.js'); + +/** + * Creates a search method to be used in clients + * @param {string} queryParam the name of the attribute used for the query + * @param {string} url the url + * @return {function} the search method + */ +function buildSearchMethod(queryParam, url) { + /** + * The search method. Prepares the data and send the query to Algolia. + * @param {string} query the string used for query search + * @param {object} args additional parameters to send with the search + * @param {function} [callback] the callback to be called with the client gets the answer + * @return {undefined|Promise} If the callback is not provided then this methods returns a Promise + */ + return function search(query, args, callback) { + // warn V2 users on how to search + if (typeof query === 'function' && typeof args === 'object' || + typeof callback === 'object') { + // .search(query, params, cb) + // .search(cb, params) + throw new errors.AlgoliaSearchError('index.search usage is index.search(query, params, cb)'); + } + + // Normalizing the function signature + if (arguments.length === 0 || typeof query === 'function') { + // Usage : .search(), .search(cb) + callback = query; + query = ''; + } else if (arguments.length === 1 || typeof args === 'function') { + // Usage : .search(query/args), .search(query, cb) + callback = args; + args = undefined; + } + // At this point we have 3 arguments with values + + // Usage : .search(args) // careful: typeof null === 'object' + if (typeof query === 'object' && query !== null) { + args = query; + query = undefined; + } else if (query === undefined || query === null) { // .search(undefined/null) + query = ''; + } + + var params = ''; + + if (query !== undefined) { + params += queryParam + '=' + encodeURIComponent(query); + } + + var additionalUA; + if (args !== undefined) { + if (args.additionalUA) { + additionalUA = args.additionalUA; + delete args.additionalUA; + } + // `_getSearchParams` will augment params, do not be fooled by the = versus += from previous if + params = this.as._getSearchParams(args, params); + } + + + return this._search(params, url, callback, additionalUA); + }; +} diff --git a/node_modules/algoliasearch/src/clone.js b/node_modules/algoliasearch/src/clone.js new file mode 100644 index 00000000..5e505560 --- /dev/null +++ b/node_modules/algoliasearch/src/clone.js @@ -0,0 +1,3 @@ +module.exports = function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +}; diff --git a/node_modules/algoliasearch/src/createAnalyticsClient.js b/node_modules/algoliasearch/src/createAnalyticsClient.js new file mode 100644 index 00000000..d2bf8005 --- /dev/null +++ b/node_modules/algoliasearch/src/createAnalyticsClient.js @@ -0,0 +1,85 @@ +module.exports = createAnalyticsClient; + +var algoliasearch = require('../index.js'); + +function createAnalyticsClient(appId, apiKey, opts) { + var analytics = {}; + + opts = opts || {}; + // there need to be 4 hosts, like on the client, since if requests fail, + // the counter goes up by 1, so we need to have the same amount of hosts + // 4 because: -dsn, -1, -2, -3 + // This is done because the APPID used for search will be the same for the analytics client created, + // and since the state of available hosts is shared by APPID globally for the module, we had issues + // where the hostIndex would be 1 while the array was only one entry (you got an empty host) + opts.hosts = opts.hosts || [ + 'analytics.algolia.com', + 'analytics.algolia.com', + 'analytics.algolia.com', + 'analytics.algolia.com' + ]; + opts.protocol = opts.protocol || 'https:'; + + analytics.as = algoliasearch(appId, apiKey, opts); + + analytics.getABTests = function(_params, callback) { + var params = params || {}; + var offset = params.offset || 0; + var limit = params.limit || 10; + + return this.as._jsonRequest({ + method: 'GET', + url: '/2/abtests?offset=' + encodeURIComponent(offset) + '&limit=' + encodeURIComponent(limit), + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.getABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/2/abtests/' + encodeURIComponent(abTestID), + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.addABTest = function(abTest, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/2/abtests', + body: abTest, + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.stopABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/2/abtests/' + encodeURIComponent(abTestID) + '/stop', + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.deleteABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'DELETE', + url: '/2/abtests/' + encodeURIComponent(abTestID), + hostType: 'write', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.waitTask = function(indexName, taskID, callback) { + return this.as.initIndex(indexName).waitTask(taskID, callback); + }; + + return analytics; +} diff --git a/node_modules/algoliasearch/src/deprecate.js b/node_modules/algoliasearch/src/deprecate.js new file mode 100644 index 00000000..ff51a54b --- /dev/null +++ b/node_modules/algoliasearch/src/deprecate.js @@ -0,0 +1,15 @@ +module.exports = function deprecate(fn, message) { + var warned = false; + + function deprecated() { + if (!warned) { + /* eslint no-console:0 */ + console.warn(message); + warned = true; + } + + return fn.apply(this, arguments); + } + + return deprecated; +}; diff --git a/node_modules/algoliasearch/src/deprecatedMessage.js b/node_modules/algoliasearch/src/deprecatedMessage.js new file mode 100644 index 00000000..3d809428 --- /dev/null +++ b/node_modules/algoliasearch/src/deprecatedMessage.js @@ -0,0 +1,7 @@ +module.exports = function deprecatedMessage(previousUsage, newUsage) { + var githubAnchorLink = previousUsage.toLowerCase() + .replace(/[\.\(\)]/g, ''); + + return 'algoliasearch: `' + previousUsage + '` was replaced by `' + newUsage + + '`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#' + githubAnchorLink; +}; diff --git a/node_modules/algoliasearch/src/errors.js b/node_modules/algoliasearch/src/errors.js new file mode 100644 index 00000000..31154503 --- /dev/null +++ b/node_modules/algoliasearch/src/errors.js @@ -0,0 +1,78 @@ +'use strict'; + +// This file hosts our error definitions +// We use custom error "types" so that we can act on them when we need it +// e.g.: if error instanceof errors.UnparsableJSON then.. + +var inherits = require('inherits'); + +function AlgoliaSearchError(message, extraProperties) { + var forEach = require('foreach'); + + var error = this; + + // try to get a stacktrace + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + error.stack = (new Error()).stack || 'Cannot get a stacktrace, browser is too old'; + } + + this.name = 'AlgoliaSearchError'; + this.message = message || 'Unknown error'; + + if (extraProperties) { + forEach(extraProperties, function addToErrorObject(value, key) { + error[key] = value; + }); + } +} + +inherits(AlgoliaSearchError, Error); + +function createCustomError(name, message) { + function AlgoliaSearchCustomError() { + var args = Array.prototype.slice.call(arguments, 0); + + // custom message not set, use default + if (typeof args[0] !== 'string') { + args.unshift(message); + } + + AlgoliaSearchError.apply(this, args); + this.name = 'AlgoliaSearch' + name + 'Error'; + } + + inherits(AlgoliaSearchCustomError, AlgoliaSearchError); + + return AlgoliaSearchCustomError; +} + +// late exports to let various fn defs and inherits take place +module.exports = { + AlgoliaSearchError: AlgoliaSearchError, + UnparsableJSON: createCustomError( + 'UnparsableJSON', + 'Could not parse the incoming response as JSON, see err.more for details' + ), + RequestTimeout: createCustomError( + 'RequestTimeout', + 'Request timedout before getting a response' + ), + Network: createCustomError( + 'Network', + 'Network issue, see err.more for details' + ), + JSONPScriptFail: createCustomError( + 'JSONPScriptFail', + '