function createBrowserLocalStorageCache(options) { const namespaceKey = `algoliasearch-client-js-${options.key}`; // eslint-disable-next-line functional/no-let let storage; const getStorage = () => { if (storage === undefined) { storage = options.localStorage || window.localStorage; } return storage; }; const getNamespace = () => { return JSON.parse(getStorage().getItem(namespaceKey) || '{}'); }; const setNamespace = (namespace) => { getStorage().setItem(namespaceKey, JSON.stringify(namespace)); }; const removeOutdatedCacheItems = () => { const timeToLive = options.timeToLive ? options.timeToLive * 1000 : null; const namespace = getNamespace(); const filteredNamespaceWithoutOldFormattedCacheItems = Object.fromEntries(Object.entries(namespace).filter(([, cacheItem]) => { return cacheItem.timestamp !== undefined; })); setNamespace(filteredNamespaceWithoutOldFormattedCacheItems); if (!timeToLive) return; const filteredNamespaceWithoutExpiredItems = Object.fromEntries(Object.entries(filteredNamespaceWithoutOldFormattedCacheItems).filter(([, cacheItem]) => { const currentTimestamp = new Date().getTime(); const isExpired = cacheItem.timestamp + timeToLive < currentTimestamp; return !isExpired; })); setNamespace(filteredNamespaceWithoutExpiredItems); }; return { get(key, defaultValue, events = { miss: () => Promise.resolve(), }) { return Promise.resolve() .then(() => { removeOutdatedCacheItems(); const keyAsString = JSON.stringify(key); return getNamespace()[keyAsString]; }) .then(value => { return Promise.all([value ? value.value : defaultValue(), value !== undefined]); }) .then(([value, exists]) => { return Promise.all([value, exists || events.miss(value)]); }) .then(([value]) => value); }, set(key, value) { return Promise.resolve().then(() => { const namespace = getNamespace(); // eslint-disable-next-line functional/immutable-data namespace[JSON.stringify(key)] = { timestamp: new Date().getTime(), value, }; getStorage().setItem(namespaceKey, JSON.stringify(namespace)); return value; }); }, delete(key) { return Promise.resolve().then(() => { const namespace = getNamespace(); // eslint-disable-next-line functional/immutable-data delete namespace[JSON.stringify(key)]; getStorage().setItem(namespaceKey, JSON.stringify(namespace)); }); }, clear() { return Promise.resolve().then(() => { getStorage().removeItem(namespaceKey); }); }, }; } // @todo Add logger on options to debug when caches go wrong. function createFallbackableCache(options) { const caches = [...options.caches]; const current = caches.shift(); // eslint-disable-line functional/immutable-data if (current === undefined) { return createNullCache(); } return { get(key, defaultValue, events = { miss: () => Promise.resolve(), }) { return current.get(key, defaultValue, events).catch(() => { return createFallbackableCache({ caches }).get(key, defaultValue, events); }); }, set(key, value) { return current.set(key, value).catch(() => { return createFallbackableCache({ caches }).set(key, value); }); }, delete(key) { return current.delete(key).catch(() => { return createFallbackableCache({ caches }).delete(key); }); }, clear() { return current.clear().catch(() => { return createFallbackableCache({ caches }).clear(); }); }, }; } function createNullCache() { return { get(_key, defaultValue, events = { miss: () => Promise.resolve(), }) { const value = defaultValue(); return value .then(result => Promise.all([result, events.miss(result)])) .then(([result]) => result); }, set(_key, value) { return Promise.resolve(value); }, delete(_key) { return Promise.resolve(); }, clear() { return Promise.resolve(); }, }; } function createInMemoryCache(options = { serializable: true }) { // eslint-disable-next-line functional/no-let let cache = {}; return { get(key, defaultValue, events = { miss: () => Promise.resolve(), }) { const keyAsString = JSON.stringify(key); if (keyAsString in cache) { return Promise.resolve(options.serializable ? JSON.parse(cache[keyAsString]) : cache[keyAsString]); } const promise = defaultValue(); const miss = (events && events.miss) || (() => Promise.resolve()); return promise.then((value) => miss(value)).then(() => promise); }, set(key, value) { // eslint-disable-next-line functional/immutable-data cache[JSON.stringify(key)] = options.serializable ? JSON.stringify(value) : value; return Promise.resolve(value); }, delete(key) { // eslint-disable-next-line functional/immutable-data delete cache[JSON.stringify(key)]; return Promise.resolve(); }, clear() { cache = {}; return Promise.resolve(); }, }; } function createAuth(authMode, appId, apiKey) { const credentials = { 'x-algolia-api-key': apiKey, 'x-algolia-application-id': appId, }; return { headers() { return authMode === AuthMode.WithinHeaders ? credentials : {}; }, queryParameters() { return authMode === AuthMode.WithinQueryParameters ? credentials : {}; }, }; } function createRetryablePromise(callback) { let retriesCount = 0; // eslint-disable-line functional/no-let const retry = () => { retriesCount++; return new Promise((resolve) => { setTimeout(() => { resolve(callback(retry)); }, Math.min(100 * retriesCount, 1000)); }); }; return callback(retry); } function createWaitablePromise(promise, wait = (_response, _requestOptions) => { return Promise.resolve(); }) { // eslint-disable-next-line functional/immutable-data return Object.assign(promise, { wait(requestOptions) { return createWaitablePromise(promise .then(response => Promise.all([wait(response, requestOptions), response])) .then(promiseResults => promiseResults[1])); }, }); } // eslint-disable-next-line functional/prefer-readonly-type function shuffle(array) { let c = array.length - 1; // eslint-disable-line functional/no-let // eslint-disable-next-line functional/no-loop-statement for (c; c > 0; c--) { const b = Math.floor(Math.random() * (c + 1)); const a = array[c]; array[c] = array[b]; // eslint-disable-line functional/immutable-data, no-param-reassign array[b] = a; // eslint-disable-line functional/immutable-data, no-param-reassign } return array; } function addMethods(base, methods) { if (!methods) { return base; } Object.keys(methods).forEach(key => { // eslint-disable-next-line functional/immutable-data, no-param-reassign base[key] = methods[key](base); }); return base; } function encode(format, ...args) { // eslint-disable-next-line functional/no-let let i = 0; return format.replace(/%s/g, () => encodeURIComponent(args[i++])); } const version = '4.22.1'; const AuthMode = { /** * If auth credentials should be in query parameters. */ WithinQueryParameters: 0, /** * If auth credentials should be in headers. */ WithinHeaders: 1, }; function createMappedRequestOptions(requestOptions, timeout) { const options = requestOptions || {}; const data = options.data || {}; Object.keys(options).forEach(key => { if (['timeout', 'headers', 'queryParameters', 'data', 'cacheable'].indexOf(key) === -1) { data[key] = options[key]; // eslint-disable-line functional/immutable-data } }); return { data: Object.entries(data).length > 0 ? data : undefined, timeout: options.timeout || timeout, headers: options.headers || {}, queryParameters: options.queryParameters || {}, cacheable: options.cacheable, }; } const CallEnum = { /** * If the host is read only. */ Read: 1, /** * If the host is write only. */ Write: 2, /** * If the host is both read and write. */ Any: 3, }; const HostStatusEnum = { Up: 1, Down: 2, Timeouted: 3, }; // By default, API Clients at Algolia have expiration delay // of 5 mins. In the JavaScript client, we have 2 mins. const EXPIRATION_DELAY = 2 * 60 * 1000; function createStatefulHost(host, status = HostStatusEnum.Up) { return { ...host, status, lastUpdate: Date.now(), }; } function isStatefulHostUp(host) { return host.status === HostStatusEnum.Up || Date.now() - host.lastUpdate > EXPIRATION_DELAY; } function isStatefulHostTimeouted(host) { return (host.status === HostStatusEnum.Timeouted && Date.now() - host.lastUpdate <= EXPIRATION_DELAY); } function createStatelessHost(options) { if (typeof options === 'string') { return { protocol: 'https', url: options, accept: CallEnum.Any, }; } return { protocol: options.protocol || 'https', url: options.url, accept: options.accept || CallEnum.Any, }; } const MethodEnum = { Delete: 'DELETE', Get: 'GET', Post: 'POST', Put: 'PUT', }; function createRetryableOptions(hostsCache, statelessHosts) { return Promise.all(statelessHosts.map(statelessHost => { return hostsCache.get(statelessHost, () => { return Promise.resolve(createStatefulHost(statelessHost)); }); })).then(statefulHosts => { const hostsUp = statefulHosts.filter(host => isStatefulHostUp(host)); const hostsTimeouted = statefulHosts.filter(host => isStatefulHostTimeouted(host)); /** * Note, we put the hosts that previously timeouted on the end of the list. */ const hostsAvailable = [...hostsUp, ...hostsTimeouted]; const statelessHostsAvailable = hostsAvailable.length > 0 ? hostsAvailable.map(host => createStatelessHost(host)) : statelessHosts; return { getTimeout(timeoutsCount, baseTimeout) { /** * Imagine that you have 4 hosts, if timeouts will increase * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200) * * Note that, the very next request, we start from the previous timeout * * 5 (timeouted) > 6 (timeouted) > 7 ... * * This strategy may need to be reviewed, but is the strategy on the our * current v3 version. */ const timeoutMultiplier = hostsTimeouted.length === 0 && timeoutsCount === 0 ? 1 : hostsTimeouted.length + 3 + timeoutsCount; return timeoutMultiplier * baseTimeout; }, statelessHosts: statelessHostsAvailable, }; }); } const isNetworkError = ({ isTimedOut, status }) => { return !isTimedOut && ~~status === 0; }; const isRetryable = (response) => { const status = response.status; const isTimedOut = response.isTimedOut; return (isTimedOut || isNetworkError(response) || (~~(status / 100) !== 2 && ~~(status / 100) !== 4)); }; const isSuccess = ({ status }) => { return ~~(status / 100) === 2; }; const retryDecision = (response, outcomes) => { if (isRetryable(response)) { return outcomes.onRetry(response); } if (isSuccess(response)) { return outcomes.onSuccess(response); } return outcomes.onFail(response); }; function retryableRequest(transporter, statelessHosts, request, requestOptions) { const stackTrace = []; // eslint-disable-line functional/prefer-readonly-type /** * First we prepare the payload that do not depend from hosts. */ const data = serializeData(request, requestOptions); const headers = serializeHeaders(transporter, requestOptions); const method = request.method; // On `GET`, the data is proxied to query parameters. const dataQueryParameters = request.method !== MethodEnum.Get ? {} : { ...request.data, ...requestOptions.data, }; const queryParameters = { 'x-algolia-agent': transporter.userAgent.value, ...transporter.queryParameters, ...dataQueryParameters, ...requestOptions.queryParameters, }; let timeoutsCount = 0; // eslint-disable-line functional/no-let const retry = (hosts, // eslint-disable-line functional/prefer-readonly-type getTimeout) => { /** * We iterate on each host, until there is no host left. */ const host = hosts.pop(); // eslint-disable-line functional/immutable-data if (host === undefined) { throw createRetryError(stackTraceWithoutCredentials(stackTrace)); } const payload = { data, headers, method, url: serializeUrl(host, request.path, queryParameters), connectTimeout: getTimeout(timeoutsCount, transporter.timeouts.connect), responseTimeout: getTimeout(timeoutsCount, requestOptions.timeout), }; /** * The stackFrame is pushed to the stackTrace so we * can have information about onRetry and onFailure * decisions. */ const pushToStackTrace = (response) => { const stackFrame = { request: payload, response, host, triesLeft: hosts.length, }; // eslint-disable-next-line functional/immutable-data stackTrace.push(stackFrame); return stackFrame; }; const decisions = { onSuccess: response => deserializeSuccess(response), onRetry(response) { const stackFrame = pushToStackTrace(response); /** * If response is a timeout, we increaset the number of * timeouts so we can increase the timeout later. */ if (response.isTimedOut) { timeoutsCount++; } return Promise.all([ /** * Failures are individually send the logger, allowing * the end user to debug / store stack frames even * when a retry error does not happen. */ transporter.logger.info('Retryable failure', stackFrameWithoutCredentials(stackFrame)), /** * We also store the state of the host in failure cases. If the host, is * down it will remain down for the next 2 minutes. In a timeout situation, * this host will be added end of the list of hosts on the next request. */ transporter.hostsCache.set(host, createStatefulHost(host, response.isTimedOut ? HostStatusEnum.Timeouted : HostStatusEnum.Down)), ]).then(() => retry(hosts, getTimeout)); }, onFail(response) { pushToStackTrace(response); throw deserializeFailure(response, stackTraceWithoutCredentials(stackTrace)); }, }; return transporter.requester.send(payload).then(response => { return retryDecision(response, decisions); }); }; /** * Finally, for each retryable host perform request until we got a non * retryable response. Some notes here: * * 1. The reverse here is applied so we can apply a `pop` later on => more performant. * 2. We also get from the retryable options a timeout multiplier that is tailored * for the current context. */ return createRetryableOptions(transporter.hostsCache, statelessHosts).then(options => { return retry([...options.statelessHosts].reverse(), options.getTimeout); }); } function createTransporter(options) { const { hostsCache, logger, requester, requestsCache, responsesCache, timeouts, userAgent, hosts, queryParameters, headers, } = options; const transporter = { hostsCache, logger, requester, requestsCache, responsesCache, timeouts, userAgent, headers, queryParameters, hosts: hosts.map(host => createStatelessHost(host)), read(request, requestOptions) { /** * First, we compute the user request options. Now, keep in mind, * that using request options the user is able to modified the intire * payload of the request. Such as headers, query parameters, and others. */ const mappedRequestOptions = createMappedRequestOptions(requestOptions, transporter.timeouts.read); const createRetryableRequest = () => { /** * Then, we prepare a function factory that contains the construction of * the retryable request. At this point, we may *not* perform the actual * request. But we want to have the function factory ready. */ return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Read) !== 0), request, mappedRequestOptions); }; /** * Once we have the function factory ready, we need to determine of the * request is "cacheable" - should be cached. Note that, once again, * the user can force this option. */ const cacheable = mappedRequestOptions.cacheable !== undefined ? mappedRequestOptions.cacheable : request.cacheable; /** * If is not "cacheable", we immediatly trigger the retryable request, no * need to check cache implementations. */ if (cacheable !== true) { return createRetryableRequest(); } /** * If the request is "cacheable", we need to first compute the key to ask * the cache implementations if this request is on progress or if the * response already exists on the cache. */ const key = { request, mappedRequestOptions, transporter: { queryParameters: transporter.queryParameters, headers: transporter.headers, }, }; /** * With the computed key, we first ask the responses cache * implemention if this request was been resolved before. */ return transporter.responsesCache.get(key, () => { /** * If the request has never resolved before, we actually ask if there * is a current request with the same key on progress. */ return transporter.requestsCache.get(key, () => { return (transporter.requestsCache /** * Finally, if there is no request in progress with the same key, * this `createRetryableRequest()` will actually trigger the * retryable request. */ .set(key, createRetryableRequest()) .then(response => Promise.all([transporter.requestsCache.delete(key), response]), err => Promise.all([transporter.requestsCache.delete(key), Promise.reject(err)])) .then(([_, response]) => response)); }); }, { /** * Of course, once we get this response back from the server, we * tell response cache to actually store the received response * to be used later. */ miss: response => transporter.responsesCache.set(key, response), }); }, write(request, requestOptions) { /** * On write requests, no cache mechanisms are applied, and we * proxy the request immediately to the requester. */ return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Write) !== 0), request, createMappedRequestOptions(requestOptions, transporter.timeouts.write)); }, }; return transporter; } function createUserAgent(version) { const userAgent = { value: `Algolia for JavaScript (${version})`, add(options) { const addedUserAgent = `; ${options.segment}${options.version !== undefined ? ` (${options.version})` : ''}`; if (userAgent.value.indexOf(addedUserAgent) === -1) { // eslint-disable-next-line functional/immutable-data userAgent.value = `${userAgent.value}${addedUserAgent}`; } return userAgent; }, }; return userAgent; } function deserializeSuccess(response) { // eslint-disable-next-line functional/no-try-statement try { return JSON.parse(response.content); } catch (e) { throw createDeserializationError(e.message, response); } } function deserializeFailure({ content, status }, stackFrame) { // eslint-disable-next-line functional/no-let let message = content; // eslint-disable-next-line functional/no-try-statement try { message = JSON.parse(content).message; } catch (e) { // .. } return createApiError(message, status, stackFrame); } function serializeUrl(host, path, queryParameters) { const queryParametersAsString = serializeQueryParameters(queryParameters); // eslint-disable-next-line functional/no-let let url = `${host.protocol}://${host.url}/${path.charAt(0) === '/' ? path.substr(1) : path}`; if (queryParametersAsString.length) { url += `?${queryParametersAsString}`; } return url; } function serializeQueryParameters(parameters) { const isObjectOrArray = (value) => Object.prototype.toString.call(value) === '[object Object]' || Object.prototype.toString.call(value) === '[object Array]'; return Object.keys(parameters) .map(key => encode('%s=%s', key, isObjectOrArray(parameters[key]) ? JSON.stringify(parameters[key]) : parameters[key])) .join('&'); } function serializeData(request, requestOptions) { if (request.method === MethodEnum.Get || (request.data === undefined && requestOptions.data === undefined)) { return undefined; } const data = Array.isArray(request.data) ? request.data : { ...request.data, ...requestOptions.data }; return JSON.stringify(data); } function serializeHeaders(transporter, requestOptions) { const headers = { ...transporter.headers, ...requestOptions.headers, }; const serializedHeaders = {}; Object.keys(headers).forEach(header => { const value = headers[header]; // @ts-ignore // eslint-disable-next-line functional/immutable-data serializedHeaders[header.toLowerCase()] = value; }); return serializedHeaders; } function stackTraceWithoutCredentials(stackTrace) { return stackTrace.map(stackFrame => stackFrameWithoutCredentials(stackFrame)); } function stackFrameWithoutCredentials(stackFrame) { const modifiedHeaders = stackFrame.request.headers['x-algolia-api-key'] ? { 'x-algolia-api-key': '*****' } : {}; return { ...stackFrame, request: { ...stackFrame.request, headers: { ...stackFrame.request.headers, ...modifiedHeaders, }, }, }; } function createApiError(message, status, transporterStackTrace) { return { name: 'ApiError', message, status, transporterStackTrace, }; } function createDeserializationError(message, response) { return { name: 'DeserializationError', message, response, }; } function createRetryError(transporterStackTrace) { return { name: 'RetryError', message: 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.', transporterStackTrace, }; } const createAnalyticsClient = options => { const region = options.region || 'us'; const auth = createAuth(AuthMode.WithinHeaders, options.appId, options.apiKey); const transporter = createTransporter({ hosts: [{ url: `analytics.${region}.algolia.com` }], ...options, headers: { ...auth.headers(), ...{ 'content-type': 'application/json' }, ...options.headers, }, queryParameters: { ...auth.queryParameters(), ...options.queryParameters, }, }); const appId = options.appId; return addMethods({ appId, transporter }, options.methods); }; const addABTest = (base) => { return (abTest, requestOptions) => { return base.transporter.write({ method: MethodEnum.Post, path: '2/abtests', data: abTest, }, requestOptions); }; }; const deleteABTest = (base) => { return (abTestID, requestOptions) => { return base.transporter.write({ method: MethodEnum.Delete, path: encode('2/abtests/%s', abTestID), }, requestOptions); }; }; const getABTest = (base) => { return (abTestID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('2/abtests/%s', abTestID), }, requestOptions); }; }; const getABTests = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '2/abtests', }, requestOptions); }; }; const stopABTest = (base) => { return (abTestID, requestOptions) => { return base.transporter.write({ method: MethodEnum.Post, path: encode('2/abtests/%s/stop', abTestID), }, requestOptions); }; }; const createPersonalizationClient = options => { const region = options.region || 'us'; const auth = createAuth(AuthMode.WithinHeaders, options.appId, options.apiKey); const transporter = createTransporter({ hosts: [{ url: `personalization.${region}.algolia.com` }], ...options, headers: { ...auth.headers(), ...{ 'content-type': 'application/json' }, ...options.headers, }, queryParameters: { ...auth.queryParameters(), ...options.queryParameters, }, }); return addMethods({ appId: options.appId, transporter }, options.methods); }; const getPersonalizationStrategy = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/strategies/personalization', }, requestOptions); }; }; const setPersonalizationStrategy = (base) => { return (personalizationStrategy, requestOptions) => { return base.transporter.write({ method: MethodEnum.Post, path: '1/strategies/personalization', data: personalizationStrategy, }, requestOptions); }; }; function createBrowsablePromise(options) { const browse = (data) => { return options.request(data).then(response => { /** * First we send to the developer the * batch retrieved from the API. */ if (options.batch !== undefined) { options.batch(response.hits); } /** * Then, we ask to the browse concrete implementation * if we should stop browsing. As example, the `browseObjects` * method will stop if the cursor is not present on the response. */ if (options.shouldStop(response)) { return undefined; } /** * Finally, if the response contains a cursor, we browse to the next * batch using that same cursor. Otherwise, we just use the traditional * browsing using the page element. */ if (response.cursor) { return browse({ cursor: response.cursor, }); } return browse({ page: (data.page || 0) + 1, }); }); }; return browse({}); } const createSearchClient = options => { const appId = options.appId; const auth = createAuth(options.authMode !== undefined ? options.authMode : AuthMode.WithinHeaders, appId, options.apiKey); const transporter = createTransporter({ hosts: [ { url: `${appId}-dsn.algolia.net`, accept: CallEnum.Read }, { url: `${appId}.algolia.net`, accept: CallEnum.Write }, ].concat(shuffle([ { url: `${appId}-1.algolianet.com` }, { url: `${appId}-2.algolianet.com` }, { url: `${appId}-3.algolianet.com` }, ])), ...options, headers: { ...auth.headers(), ...{ 'content-type': 'application/x-www-form-urlencoded' }, ...options.headers, }, queryParameters: { ...auth.queryParameters(), ...options.queryParameters, }, }); const base = { transporter, appId, addAlgoliaAgent(segment, version) { transporter.userAgent.add({ segment, version }); }, clearCache() { return Promise.all([ transporter.requestsCache.clear(), transporter.responsesCache.clear(), ]).then(() => undefined); }, }; return addMethods(base, options.methods); }; function createMissingObjectIDError() { return { name: 'MissingObjectIDError', message: 'All objects must have an unique objectID ' + '(like a primary key) to be valid. ' + 'Algolia is also able to generate objectIDs ' + "automatically but *it's not recommended*. " + "To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option.", }; } function createObjectNotFoundError() { return { name: 'ObjectNotFoundError', message: 'Object not found.', }; } const addApiKey = (base) => { return (acl, requestOptions) => { const { queryParameters, ...options } = requestOptions || {}; const data = { acl, ...(queryParameters !== undefined ? { queryParameters } : {}), }; const wait = (response, waitRequestOptions) => { return createRetryablePromise(retry => { return getApiKey(base)(response.key, waitRequestOptions).catch((apiError) => { if (apiError.status !== 404) { throw apiError; } return retry(); }); }); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: '1/keys', data, }, options), wait); }; }; const assignUserID = (base) => { return (userID, clusterName, requestOptions) => { const mappedRequestOptions = createMappedRequestOptions(requestOptions); // eslint-disable-next-line functional/immutable-data mappedRequestOptions.queryParameters['X-Algolia-User-ID'] = userID; return base.transporter.write({ method: MethodEnum.Post, path: '1/clusters/mapping', data: { cluster: clusterName }, }, mappedRequestOptions); }; }; const assignUserIDs = (base) => { return (userIDs, clusterName, requestOptions) => { return base.transporter.write({ method: MethodEnum.Post, path: '1/clusters/mapping/batch', data: { users: userIDs, cluster: clusterName, }, }, requestOptions); }; }; const clearDictionaryEntries = (base) => { return (dictionary, requestOptions) => { return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('/1/dictionaries/%s/batch', dictionary), data: { clearExistingDictionaryEntries: true, requests: { action: 'addEntry', body: [] }, }, }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions)); }; }; const copyIndex = (base) => { return (from, to, requestOptions) => { const wait = (response, waitRequestOptions) => { return initIndex(base)(from, { methods: { waitTask }, }).waitTask(response.taskID, waitRequestOptions); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/operation', from), data: { operation: 'copy', destination: to, }, }, requestOptions), wait); }; }; const copyRules = (base) => { return (from, to, requestOptions) => { return copyIndex(base)(from, to, { ...requestOptions, scope: [ScopeEnum.Rules], }); }; }; const copySettings = (base) => { return (from, to, requestOptions) => { return copyIndex(base)(from, to, { ...requestOptions, scope: [ScopeEnum.Settings], }); }; }; const copySynonyms = (base) => { return (from, to, requestOptions) => { return copyIndex(base)(from, to, { ...requestOptions, scope: [ScopeEnum.Synonyms], }); }; }; const customRequest = (base) => { return (request, requestOptions) => { if (request.method === MethodEnum.Get) { return base.transporter.read(request, requestOptions); } return base.transporter.write(request, requestOptions); }; }; const deleteApiKey = (base) => { return (apiKey, requestOptions) => { const wait = (_, waitRequestOptions) => { return createRetryablePromise(retry => { return getApiKey(base)(apiKey, waitRequestOptions) .then(retry) .catch((apiError) => { if (apiError.status !== 404) { throw apiError; } }); }); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Delete, path: encode('1/keys/%s', apiKey), }, requestOptions), wait); }; }; const deleteDictionaryEntries = (base) => { return (dictionary, objectIDs, requestOptions) => { const requests = objectIDs.map(objectID => ({ action: 'deleteEntry', body: { objectID }, })); return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('/1/dictionaries/%s/batch', dictionary), data: { clearExistingDictionaryEntries: false, requests }, }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions)); }; }; const getApiKey = (base) => { return (apiKey, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/keys/%s', apiKey), }, requestOptions); }; }; const getAppTask = (base) => { return (taskID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/task/%s', taskID.toString()), }, requestOptions); }; }; const getDictionarySettings = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '/1/dictionaries/*/settings', }, requestOptions); }; }; const getLogs = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/logs', }, requestOptions); }; }; const getTopUserIDs = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/clusters/mapping/top', }, requestOptions); }; }; const getUserID = (base) => { return (userID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/clusters/mapping/%s', userID), }, requestOptions); }; }; const hasPendingMappings = (base) => { return (requestOptions) => { const { retrieveMappings, ...options } = requestOptions || {}; if (retrieveMappings === true) { // eslint-disable-next-line functional/immutable-data options.getClusters = true; } return base.transporter.read({ method: MethodEnum.Get, path: '1/clusters/mapping/pending', }, options); }; }; const initIndex = (base) => { return (indexName, options = {}) => { const searchIndex = { transporter: base.transporter, appId: base.appId, indexName, }; return addMethods(searchIndex, options.methods); }; }; const listApiKeys = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/keys', }, requestOptions); }; }; const listClusters = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/clusters', }, requestOptions); }; }; const listIndices = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/indexes', }, requestOptions); }; }; const listUserIDs = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: '1/clusters/mapping', }, requestOptions); }; }; const moveIndex = (base) => { return (from, to, requestOptions) => { const wait = (response, waitRequestOptions) => { return initIndex(base)(from, { methods: { waitTask }, }).waitTask(response.taskID, waitRequestOptions); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/operation', from), data: { operation: 'move', destination: to, }, }, requestOptions), wait); }; }; const multipleBatch = (base) => { return (requests, requestOptions) => { const wait = (response, waitRequestOptions) => { return Promise.all(Object.keys(response.taskID).map(indexName => { return initIndex(base)(indexName, { methods: { waitTask }, }).waitTask(response.taskID[indexName], waitRequestOptions); })); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: '1/indexes/*/batch', data: { requests, }, }, requestOptions), wait); }; }; const multipleGetObjects = (base) => { return (requests, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: '1/indexes/*/objects', data: { requests, }, }, requestOptions); }; }; const multipleQueries = (base) => { return (queries, requestOptions) => { const requests = queries.map(query => { return { ...query, params: serializeQueryParameters(query.params || {}), }; }); return base.transporter.read({ method: MethodEnum.Post, path: '1/indexes/*/queries', data: { requests, }, cacheable: true, }, requestOptions); }; }; const multipleSearchForFacetValues = (base) => { return (queries, requestOptions) => { return Promise.all(queries.map(query => { const { facetName, facetQuery, ...params } = query.params; return initIndex(base)(query.indexName, { methods: { searchForFacetValues }, }).searchForFacetValues(facetName, facetQuery, { ...requestOptions, ...params, }); })); }; }; const removeUserID = (base) => { return (userID, requestOptions) => { const mappedRequestOptions = createMappedRequestOptions(requestOptions); // eslint-disable-next-line functional/immutable-data mappedRequestOptions.queryParameters['X-Algolia-User-ID'] = userID; return base.transporter.write({ method: MethodEnum.Delete, path: '1/clusters/mapping', }, mappedRequestOptions); }; }; const replaceDictionaryEntries = (base) => { return (dictionary, entries, requestOptions) => { const requests = entries.map(entry => ({ action: 'addEntry', body: entry, })); return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('/1/dictionaries/%s/batch', dictionary), data: { clearExistingDictionaryEntries: true, requests }, }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions)); }; }; const restoreApiKey = (base) => { return (apiKey, requestOptions) => { const wait = (_, waitRequestOptions) => { return createRetryablePromise(retry => { return getApiKey(base)(apiKey, waitRequestOptions).catch((apiError) => { if (apiError.status !== 404) { throw apiError; } return retry(); }); }); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/keys/%s/restore', apiKey), }, requestOptions), wait); }; }; const saveDictionaryEntries = (base) => { return (dictionary, entries, requestOptions) => { const requests = entries.map(entry => ({ action: 'addEntry', body: entry, })); return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('/1/dictionaries/%s/batch', dictionary), data: { clearExistingDictionaryEntries: false, requests }, }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions)); }; }; const searchDictionaryEntries = (base) => { return (dictionary, query, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: encode('/1/dictionaries/%s/search', dictionary), data: { query, }, cacheable: true, }, requestOptions); }; }; const searchUserIDs = (base) => { return (query, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: '1/clusters/mapping/search', data: { query, }, }, requestOptions); }; }; const setDictionarySettings = (base) => { return (settings, requestOptions) => { return createWaitablePromise(base.transporter.write({ method: MethodEnum.Put, path: '/1/dictionaries/*/settings', data: settings, }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions)); }; }; const updateApiKey = (base) => { return (apiKey, requestOptions) => { const updatedFields = Object.assign({}, requestOptions); const { queryParameters, ...options } = requestOptions || {}; const data = queryParameters ? { queryParameters } : {}; const apiKeyFields = [ 'acl', 'indexes', 'referers', 'restrictSources', 'queryParameters', 'description', 'maxQueriesPerIPPerHour', 'maxHitsPerQuery', ]; // Check that all the fields retrieved through getApiKey are the same as the ones we wanted to update const hasChanged = (getApiKeyResponse) => { return Object.keys(updatedFields) .filter((updatedField) => apiKeyFields.indexOf(updatedField) !== -1) .every(updatedField => { // If the field is an array, we need to check that they are the same length and that all the values are the same if (Array.isArray(getApiKeyResponse[updatedField]) && Array.isArray(updatedFields[updatedField])) { const getApiKeyResponseArray = getApiKeyResponse[updatedField]; return (getApiKeyResponseArray.length === updatedFields[updatedField].length && getApiKeyResponseArray.every((value, index) => value === updatedFields[updatedField][index])); } else { return getApiKeyResponse[updatedField] === updatedFields[updatedField]; } }); }; const wait = (_, waitRequestOptions) => createRetryablePromise(retry => { return getApiKey(base)(apiKey, waitRequestOptions).then(getApiKeyResponse => { return hasChanged(getApiKeyResponse) ? Promise.resolve() : retry(); }); }); return createWaitablePromise(base.transporter.write({ method: MethodEnum.Put, path: encode('1/keys/%s', apiKey), data, }, options), wait); }; }; const waitAppTask = (base) => { return (taskID, requestOptions) => { return createRetryablePromise(retry => { return getAppTask(base)(taskID, requestOptions).then(response => { return response.status !== 'published' ? retry() : undefined; }); }); }; }; const batch = (base) => { return (requests, requestOptions) => { const wait = (response, waitRequestOptions) => { return waitTask(base)(response.taskID, waitRequestOptions); }; return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/batch', base.indexName), data: { requests, }, }, requestOptions), wait); }; }; const browseObjects = (base) => { return (requestOptions) => { return createBrowsablePromise({ shouldStop: response => response.cursor === undefined, ...requestOptions, request: (data) => base.transporter.read({ method: MethodEnum.Post, path: encode('1/indexes/%s/browse', base.indexName), data, }, requestOptions), }); }; }; const browseRules = (base) => { return (requestOptions) => { const options = { hitsPerPage: 1000, ...requestOptions, }; return createBrowsablePromise({ shouldStop: response => response.hits.length < options.hitsPerPage, ...options, request(data) { return searchRules(base)('', { ...options, ...data }).then((response) => { return { ...response, hits: response.hits.map(rule => { // eslint-disable-next-line functional/immutable-data,no-param-reassign delete rule._highlightResult; return rule; }), }; }); }, }); }; }; const browseSynonyms = (base) => { return (requestOptions) => { const options = { hitsPerPage: 1000, ...requestOptions, }; return createBrowsablePromise({ shouldStop: response => response.hits.length < options.hitsPerPage, ...options, request(data) { return searchSynonyms(base)('', { ...options, ...data }).then((response) => { return { ...response, hits: response.hits.map(synonym => { // eslint-disable-next-line functional/immutable-data,no-param-reassign delete synonym._highlightResult; return synonym; }), }; }); }, }); }; }; const chunkedBatch = (base) => { return (bodies, action, requestOptions) => { const { batchSize, ...options } = requestOptions || {}; const response = { taskIDs: [], objectIDs: [], }; const forEachBatch = (lastIndex = 0) => { // eslint-disable-next-line functional/prefer-readonly-type const bodiesChunk = []; // eslint-disable-next-line functional/no-let let index; /* eslint-disable-next-line functional/no-loop-statement */ for (index = lastIndex; index < bodies.length; index++) { // eslint-disable-next-line functional/immutable-data bodiesChunk.push(bodies[index]); if (bodiesChunk.length === (batchSize || 1000)) { break; } } if (bodiesChunk.length === 0) { return Promise.resolve(response); } return batch(base)(bodiesChunk.map(body => { return { action, body, }; }), options).then(res => { response.objectIDs = response.objectIDs.concat(res.objectIDs); // eslint-disable-line functional/immutable-data response.taskIDs.push(res.taskID); // eslint-disable-line functional/immutable-data index++; return forEachBatch(index); }); }; return createWaitablePromise(forEachBatch(), (chunkedBatchResponse, waitRequestOptions) => { return Promise.all(chunkedBatchResponse.taskIDs.map(taskID => { return waitTask(base)(taskID, waitRequestOptions); })); }); }; }; const clearObjects = (base) => { return (requestOptions) => { return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/clear', base.indexName), }, requestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const clearRules = (base) => { return (requestOptions) => { const { forwardToReplicas, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/rules/clear', base.indexName), }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const clearSynonyms = (base) => { return (requestOptions) => { const { forwardToReplicas, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/synonyms/clear', base.indexName), }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const deleteBy = (base) => { return (filters, requestOptions) => { return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/deleteByQuery', base.indexName), data: filters, }, requestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const deleteIndex = (base) => { return (requestOptions) => { return createWaitablePromise(base.transporter.write({ method: MethodEnum.Delete, path: encode('1/indexes/%s', base.indexName), }, requestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const deleteObject = (base) => { return (objectID, requestOptions) => { return createWaitablePromise(deleteObjects(base)([objectID], requestOptions).then(response => { return { taskID: response.taskIDs[0] }; }), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const deleteObjects = (base) => { return (objectIDs, requestOptions) => { const objects = objectIDs.map(objectID => { return { objectID }; }); return chunkedBatch(base)(objects, BatchActionEnum.DeleteObject, requestOptions); }; }; const deleteRule = (base) => { return (objectID, requestOptions) => { const { forwardToReplicas, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Delete, path: encode('1/indexes/%s/rules/%s', base.indexName, objectID), }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const deleteSynonym = (base) => { return (objectID, requestOptions) => { const { forwardToReplicas, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Delete, path: encode('1/indexes/%s/synonyms/%s', base.indexName, objectID), }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const exists = (base) => { return (requestOptions) => { return getSettings(base)(requestOptions) .then(() => true) .catch(error => { if (error.status !== 404) { throw error; } return false; }); }; }; const findAnswers = (base) => { return (query, queryLanguages, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: encode('1/answers/%s/prediction', base.indexName), data: { query, queryLanguages, }, cacheable: true, }, requestOptions); }; }; const findObject = (base) => { return (callback, requestOptions) => { const { query, paginate, ...options } = requestOptions || {}; // eslint-disable-next-line functional/no-let let page = 0; const forEachPage = () => { return search(base)(query || '', { ...options, page }).then(result => { // eslint-disable-next-line functional/no-loop-statement for (const [position, hit] of Object.entries(result.hits)) { // eslint-disable-next-line promise/no-callback-in-promise if (callback(hit)) { return { object: hit, position: parseInt(position, 10), page, }; } } page++; // paginate if option was set and has next page if (paginate === false || page >= result.nbPages) { throw createObjectNotFoundError(); } return forEachPage(); }); }; return forEachPage(); }; }; const getObject = (base) => { return (objectID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/indexes/%s/%s', base.indexName, objectID), }, requestOptions); }; }; const getObjectPosition = () => { return (searchResponse, objectID) => { // eslint-disable-next-line functional/no-loop-statement for (const [position, hit] of Object.entries(searchResponse.hits)) { if (hit.objectID === objectID) { return parseInt(position, 10); } } return -1; }; }; const getObjects = (base) => { return (objectIDs, requestOptions) => { const { attributesToRetrieve, ...options } = requestOptions || {}; const requests = objectIDs.map(objectID => { return { indexName: base.indexName, objectID, ...(attributesToRetrieve ? { attributesToRetrieve } : {}), }; }); return base.transporter.read({ method: MethodEnum.Post, path: '1/indexes/*/objects', data: { requests, }, }, options); }; }; const getRule = (base) => { return (objectID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/indexes/%s/rules/%s', base.indexName, objectID), }, requestOptions); }; }; const getSettings = (base) => { return (requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/indexes/%s/settings', base.indexName), data: { getVersion: 2, }, }, requestOptions); }; }; const getSynonym = (base) => { return (objectID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode(`1/indexes/%s/synonyms/%s`, base.indexName, objectID), }, requestOptions); }; }; const getTask = (base) => { return (taskID, requestOptions) => { return base.transporter.read({ method: MethodEnum.Get, path: encode('1/indexes/%s/task/%s', base.indexName, taskID.toString()), }, requestOptions); }; }; const partialUpdateObject = (base) => { return (object, requestOptions) => { return createWaitablePromise(partialUpdateObjects(base)([object], requestOptions).then(response => { return { objectID: response.objectIDs[0], taskID: response.taskIDs[0], }; }), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const partialUpdateObjects = (base) => { return (objects, requestOptions) => { const { createIfNotExists, ...options } = requestOptions || {}; const action = createIfNotExists ? BatchActionEnum.PartialUpdateObject : BatchActionEnum.PartialUpdateObjectNoCreate; return chunkedBatch(base)(objects, action, options); }; }; const replaceAllObjects = (base) => { return (objects, requestOptions) => { const { safe, autoGenerateObjectIDIfNotExist, batchSize, ...options } = requestOptions || {}; const operation = (from, to, type, operationRequestOptions) => { return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/operation', from), data: { operation: type, destination: to, }, }, operationRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; const randomSuffix = Math.random() .toString(36) .substring(7); const temporaryIndexName = `${base.indexName}_tmp_${randomSuffix}`; const saveObjectsInTemporary = saveObjects({ appId: base.appId, transporter: base.transporter, indexName: temporaryIndexName, }); // @ts-ignore // eslint-disable-next-line prefer-const, functional/no-let, functional/prefer-readonly-type let responses = []; const copyWaitablePromise = operation(base.indexName, temporaryIndexName, 'copy', { ...options, scope: ['settings', 'synonyms', 'rules'], }); // eslint-disable-next-line functional/immutable-data responses.push(copyWaitablePromise); const result = (safe ? copyWaitablePromise.wait(options) : copyWaitablePromise) .then(() => { const saveObjectsWaitablePromise = saveObjectsInTemporary(objects, { ...options, autoGenerateObjectIDIfNotExist, batchSize, }); // eslint-disable-next-line functional/immutable-data responses.push(saveObjectsWaitablePromise); return safe ? saveObjectsWaitablePromise.wait(options) : saveObjectsWaitablePromise; }) .then(() => { const moveWaitablePromise = operation(temporaryIndexName, base.indexName, 'move', options); // eslint-disable-next-line functional/immutable-data responses.push(moveWaitablePromise); return safe ? moveWaitablePromise.wait(options) : moveWaitablePromise; }) .then(() => Promise.all(responses)) .then(([copyResponse, saveObjectsResponse, moveResponse]) => { return { objectIDs: saveObjectsResponse.objectIDs, taskIDs: [copyResponse.taskID, ...saveObjectsResponse.taskIDs, moveResponse.taskID], }; }); return createWaitablePromise(result, (_, waitRequestOptions) => { return Promise.all(responses.map(response => response.wait(waitRequestOptions))); }); }; }; const replaceAllRules = (base) => { return (rules, requestOptions) => { return saveRules(base)(rules, { ...requestOptions, clearExistingRules: true, }); }; }; const replaceAllSynonyms = (base) => { return (synonyms, requestOptions) => { return saveSynonyms(base)(synonyms, { ...requestOptions, clearExistingSynonyms: true, }); }; }; const saveObject = (base) => { return (object, requestOptions) => { return createWaitablePromise(saveObjects(base)([object], requestOptions).then(response => { return { objectID: response.objectIDs[0], taskID: response.taskIDs[0], }; }), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const saveObjects = (base) => { return (objects, requestOptions) => { const { autoGenerateObjectIDIfNotExist, ...options } = requestOptions || {}; const action = autoGenerateObjectIDIfNotExist ? BatchActionEnum.AddObject : BatchActionEnum.UpdateObject; if (action === BatchActionEnum.UpdateObject) { // eslint-disable-next-line functional/no-loop-statement for (const object of objects) { if (object.objectID === undefined) { return createWaitablePromise(Promise.reject(createMissingObjectIDError())); } } } return chunkedBatch(base)(objects, action, options); }; }; const saveRule = (base) => { return (rule, requestOptions) => { return saveRules(base)([rule], requestOptions); }; }; const saveRules = (base) => { return (rules, requestOptions) => { const { forwardToReplicas, clearExistingRules, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } if (clearExistingRules) { mappedRequestOptions.queryParameters.clearExistingRules = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/rules/batch', base.indexName), data: rules, }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const saveSynonym = (base) => { return (synonym, requestOptions) => { return saveSynonyms(base)([synonym], requestOptions); }; }; const saveSynonyms = (base) => { return (synonyms, requestOptions) => { const { forwardToReplicas, clearExistingSynonyms, replaceExistingSynonyms, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } if (replaceExistingSynonyms || clearExistingSynonyms) { mappedRequestOptions.queryParameters.replaceExistingSynonyms = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Post, path: encode('1/indexes/%s/synonyms/batch', base.indexName), data: synonyms, }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const search = (base) => { return (query, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: encode('1/indexes/%s/query', base.indexName), data: { query, }, cacheable: true, }, requestOptions); }; }; const searchForFacetValues = (base) => { return (facetName, facetQuery, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: encode('1/indexes/%s/facets/%s/query', base.indexName, facetName), data: { facetQuery, }, cacheable: true, }, requestOptions); }; }; const searchRules = (base) => { return (query, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: encode('1/indexes/%s/rules/search', base.indexName), data: { query, }, }, requestOptions); }; }; const searchSynonyms = (base) => { return (query, requestOptions) => { return base.transporter.read({ method: MethodEnum.Post, path: encode('1/indexes/%s/synonyms/search', base.indexName), data: { query, }, }, requestOptions); }; }; const setSettings = (base) => { return (settings, requestOptions) => { const { forwardToReplicas, ...options } = requestOptions || {}; const mappedRequestOptions = createMappedRequestOptions(options); if (forwardToReplicas) { mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data } return createWaitablePromise(base.transporter.write({ method: MethodEnum.Put, path: encode('1/indexes/%s/settings', base.indexName), data: settings, }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions)); }; }; const waitTask = (base) => { return (taskID, requestOptions) => { return createRetryablePromise(retry => { return getTask(base)(taskID, requestOptions).then(response => { return response.status !== 'published' ? retry() : undefined; }); }); }; }; const BatchActionEnum = { AddObject: 'addObject', UpdateObject: 'updateObject', PartialUpdateObject: 'partialUpdateObject', PartialUpdateObjectNoCreate: 'partialUpdateObjectNoCreate', DeleteObject: 'deleteObject', DeleteIndex: 'delete', ClearIndex: 'clear', }; const ScopeEnum = { Settings: 'settings', Synonyms: 'synonyms', Rules: 'rules', }; const LogLevelEnum = { Debug: 1, Info: 2, Error: 3, }; /* eslint no-console: 0 */ function createConsoleLogger(logLevel) { return { debug(message, args) { if (LogLevelEnum.Debug >= logLevel) { console.debug(message, args); } return Promise.resolve(); }, info(message, args) { if (LogLevelEnum.Info >= logLevel) { console.info(message, args); } return Promise.resolve(); }, error(message, args) { console.error(message, args); return Promise.resolve(); }, }; } function createBrowserXhrRequester() { return { send(request) { return new Promise((resolve) => { const baseRequester = new XMLHttpRequest(); baseRequester.open(request.method, request.url, true); Object.keys(request.headers).forEach(key => baseRequester.setRequestHeader(key, request.headers[key])); const createTimeout = (timeout, content) => { return setTimeout(() => { baseRequester.abort(); resolve({ status: 0, content, isTimedOut: true, }); }, timeout * 1000); }; const connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout'); // eslint-disable-next-line functional/no-let let responseTimeout; // eslint-disable-next-line functional/immutable-data baseRequester.onreadystatechange = () => { if (baseRequester.readyState > baseRequester.OPENED && responseTimeout === undefined) { clearTimeout(connectTimeout); responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout'); } }; // eslint-disable-next-line functional/immutable-data baseRequester.onerror = () => { // istanbul ignore next if (baseRequester.status === 0) { clearTimeout(connectTimeout); clearTimeout(responseTimeout); resolve({ content: baseRequester.responseText || 'Network request failed', status: baseRequester.status, isTimedOut: false, }); } }; // eslint-disable-next-line functional/immutable-data baseRequester.onload = () => { clearTimeout(connectTimeout); clearTimeout(responseTimeout); resolve({ content: baseRequester.responseText, status: baseRequester.status, isTimedOut: false, }); }; baseRequester.send(request.data); }); }, }; } function algoliasearch(appId, apiKey, options) { const commonOptions = { appId, apiKey, timeouts: { connect: 1, read: 2, write: 30, }, requester: createBrowserXhrRequester(), logger: createConsoleLogger(LogLevelEnum.Error), responsesCache: createInMemoryCache(), requestsCache: createInMemoryCache({ serializable: false }), hostsCache: createFallbackableCache({ caches: [ createBrowserLocalStorageCache({ key: `${version}-${appId}` }), createInMemoryCache(), ], }), userAgent: createUserAgent(version).add({ segment: 'Browser' }), }; const searchClientOptions = { ...commonOptions, ...options }; const initPersonalization = () => (clientOptions) => { return createPersonalizationClient({ ...commonOptions, ...clientOptions, methods: { getPersonalizationStrategy, setPersonalizationStrategy, }, }); }; return createSearchClient({ ...searchClientOptions, methods: { search: multipleQueries, searchForFacetValues: multipleSearchForFacetValues, multipleBatch, multipleGetObjects, multipleQueries, copyIndex, copySettings, copySynonyms, copyRules, moveIndex, listIndices, getLogs, listClusters, multipleSearchForFacetValues, getApiKey, addApiKey, listApiKeys, updateApiKey, deleteApiKey, restoreApiKey, assignUserID, assignUserIDs, getUserID, searchUserIDs, listUserIDs, getTopUserIDs, removeUserID, hasPendingMappings, clearDictionaryEntries, deleteDictionaryEntries, getDictionarySettings, getAppTask, replaceDictionaryEntries, saveDictionaryEntries, searchDictionaryEntries, setDictionarySettings, waitAppTask, customRequest, initIndex: base => (indexName) => { return initIndex(base)(indexName, { methods: { batch, delete: deleteIndex, findAnswers, getObject, getObjects, saveObject, saveObjects, search, searchForFacetValues, waitTask, setSettings, getSettings, partialUpdateObject, partialUpdateObjects, deleteObject, deleteObjects, deleteBy, clearObjects, browseObjects, getObjectPosition, findObject, exists, saveSynonym, saveSynonyms, getSynonym, searchSynonyms, browseSynonyms, deleteSynonym, clearSynonyms, replaceAllObjects, replaceAllSynonyms, searchRules, getRule, deleteRule, saveRule, saveRules, replaceAllRules, browseRules, clearRules, }, }); }, initAnalytics: () => (clientOptions) => { return createAnalyticsClient({ ...commonOptions, ...clientOptions, methods: { addABTest, getABTest, getABTests, stopABTest, deleteABTest, }, }); }, initPersonalization, initRecommendation: () => (clientOptions) => { searchClientOptions.logger.info('The `initRecommendation` method is deprecated. Use `initPersonalization` instead.'); return initPersonalization()(clientOptions); }, }, }); } // eslint-disable-next-line functional/immutable-data algoliasearch.version = version; export default algoliasearch;