algoliasearch.esm.browser.js 76 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221
  1. function createBrowserLocalStorageCache(options) {
  2. const namespaceKey = `algoliasearch-client-js-${options.key}`;
  3. // eslint-disable-next-line functional/no-let
  4. let storage;
  5. const getStorage = () => {
  6. if (storage === undefined) {
  7. storage = options.localStorage || window.localStorage;
  8. }
  9. return storage;
  10. };
  11. const getNamespace = () => {
  12. return JSON.parse(getStorage().getItem(namespaceKey) || '{}');
  13. };
  14. return {
  15. get(key, defaultValue, events = {
  16. miss: () => Promise.resolve(),
  17. }) {
  18. return Promise.resolve()
  19. .then(() => {
  20. const keyAsString = JSON.stringify(key);
  21. const value = getNamespace()[keyAsString];
  22. return Promise.all([value || defaultValue(), value !== undefined]);
  23. })
  24. .then(([value, exists]) => {
  25. return Promise.all([value, exists || events.miss(value)]);
  26. })
  27. .then(([value]) => value);
  28. },
  29. set(key, value) {
  30. return Promise.resolve().then(() => {
  31. const namespace = getNamespace();
  32. // eslint-disable-next-line functional/immutable-data
  33. namespace[JSON.stringify(key)] = value;
  34. getStorage().setItem(namespaceKey, JSON.stringify(namespace));
  35. return value;
  36. });
  37. },
  38. delete(key) {
  39. return Promise.resolve().then(() => {
  40. const namespace = getNamespace();
  41. // eslint-disable-next-line functional/immutable-data
  42. delete namespace[JSON.stringify(key)];
  43. getStorage().setItem(namespaceKey, JSON.stringify(namespace));
  44. });
  45. },
  46. clear() {
  47. return Promise.resolve().then(() => {
  48. getStorage().removeItem(namespaceKey);
  49. });
  50. },
  51. };
  52. }
  53. // @todo Add logger on options to debug when caches go wrong.
  54. function createFallbackableCache(options) {
  55. const caches = [...options.caches];
  56. const current = caches.shift(); // eslint-disable-line functional/immutable-data
  57. if (current === undefined) {
  58. return createNullCache();
  59. }
  60. return {
  61. get(key, defaultValue, events = {
  62. miss: () => Promise.resolve(),
  63. }) {
  64. return current.get(key, defaultValue, events).catch(() => {
  65. return createFallbackableCache({ caches }).get(key, defaultValue, events);
  66. });
  67. },
  68. set(key, value) {
  69. return current.set(key, value).catch(() => {
  70. return createFallbackableCache({ caches }).set(key, value);
  71. });
  72. },
  73. delete(key) {
  74. return current.delete(key).catch(() => {
  75. return createFallbackableCache({ caches }).delete(key);
  76. });
  77. },
  78. clear() {
  79. return current.clear().catch(() => {
  80. return createFallbackableCache({ caches }).clear();
  81. });
  82. },
  83. };
  84. }
  85. function createNullCache() {
  86. return {
  87. get(_key, defaultValue, events = {
  88. miss: () => Promise.resolve(),
  89. }) {
  90. const value = defaultValue();
  91. return value
  92. .then(result => Promise.all([result, events.miss(result)]))
  93. .then(([result]) => result);
  94. },
  95. set(_key, value) {
  96. return Promise.resolve(value);
  97. },
  98. delete(_key) {
  99. return Promise.resolve();
  100. },
  101. clear() {
  102. return Promise.resolve();
  103. },
  104. };
  105. }
  106. function createInMemoryCache(options = { serializable: true }) {
  107. // eslint-disable-next-line functional/no-let
  108. let cache = {};
  109. return {
  110. get(key, defaultValue, events = {
  111. miss: () => Promise.resolve(),
  112. }) {
  113. const keyAsString = JSON.stringify(key);
  114. if (keyAsString in cache) {
  115. return Promise.resolve(options.serializable ? JSON.parse(cache[keyAsString]) : cache[keyAsString]);
  116. }
  117. const promise = defaultValue();
  118. const miss = (events && events.miss) || (() => Promise.resolve());
  119. return promise.then((value) => miss(value)).then(() => promise);
  120. },
  121. set(key, value) {
  122. // eslint-disable-next-line functional/immutable-data
  123. cache[JSON.stringify(key)] = options.serializable ? JSON.stringify(value) : value;
  124. return Promise.resolve(value);
  125. },
  126. delete(key) {
  127. // eslint-disable-next-line functional/immutable-data
  128. delete cache[JSON.stringify(key)];
  129. return Promise.resolve();
  130. },
  131. clear() {
  132. cache = {};
  133. return Promise.resolve();
  134. },
  135. };
  136. }
  137. function createAuth(authMode, appId, apiKey) {
  138. const credentials = {
  139. 'x-algolia-api-key': apiKey,
  140. 'x-algolia-application-id': appId,
  141. };
  142. return {
  143. headers() {
  144. return authMode === AuthMode.WithinHeaders ? credentials : {};
  145. },
  146. queryParameters() {
  147. return authMode === AuthMode.WithinQueryParameters ? credentials : {};
  148. },
  149. };
  150. }
  151. function createRetryablePromise(callback) {
  152. let retriesCount = 0; // eslint-disable-line functional/no-let
  153. const retry = () => {
  154. retriesCount++;
  155. return new Promise((resolve) => {
  156. setTimeout(() => {
  157. resolve(callback(retry));
  158. }, Math.min(100 * retriesCount, 1000));
  159. });
  160. };
  161. return callback(retry);
  162. }
  163. function createWaitablePromise(promise, wait = (_response, _requestOptions) => {
  164. return Promise.resolve();
  165. }) {
  166. // eslint-disable-next-line functional/immutable-data
  167. return Object.assign(promise, {
  168. wait(requestOptions) {
  169. return createWaitablePromise(promise
  170. .then(response => Promise.all([wait(response, requestOptions), response]))
  171. .then(promiseResults => promiseResults[1]));
  172. },
  173. });
  174. }
  175. // eslint-disable-next-line functional/prefer-readonly-type
  176. function shuffle(array) {
  177. let c = array.length - 1; // eslint-disable-line functional/no-let
  178. // eslint-disable-next-line functional/no-loop-statement
  179. for (c; c > 0; c--) {
  180. const b = Math.floor(Math.random() * (c + 1));
  181. const a = array[c];
  182. array[c] = array[b]; // eslint-disable-line functional/immutable-data, no-param-reassign
  183. array[b] = a; // eslint-disable-line functional/immutable-data, no-param-reassign
  184. }
  185. return array;
  186. }
  187. function addMethods(base, methods) {
  188. if (!methods) {
  189. return base;
  190. }
  191. Object.keys(methods).forEach(key => {
  192. // eslint-disable-next-line functional/immutable-data, no-param-reassign
  193. base[key] = methods[key](base);
  194. });
  195. return base;
  196. }
  197. function encode(format, ...args) {
  198. // eslint-disable-next-line functional/no-let
  199. let i = 0;
  200. return format.replace(/%s/g, () => encodeURIComponent(args[i++]));
  201. }
  202. const version = '4.13.0';
  203. const AuthMode = {
  204. /**
  205. * If auth credentials should be in query parameters.
  206. */
  207. WithinQueryParameters: 0,
  208. /**
  209. * If auth credentials should be in headers.
  210. */
  211. WithinHeaders: 1,
  212. };
  213. function createMappedRequestOptions(requestOptions, timeout) {
  214. const options = requestOptions || {};
  215. const data = options.data || {};
  216. Object.keys(options).forEach(key => {
  217. if (['timeout', 'headers', 'queryParameters', 'data', 'cacheable'].indexOf(key) === -1) {
  218. data[key] = options[key]; // eslint-disable-line functional/immutable-data
  219. }
  220. });
  221. return {
  222. data: Object.entries(data).length > 0 ? data : undefined,
  223. timeout: options.timeout || timeout,
  224. headers: options.headers || {},
  225. queryParameters: options.queryParameters || {},
  226. cacheable: options.cacheable,
  227. };
  228. }
  229. const CallEnum = {
  230. /**
  231. * If the host is read only.
  232. */
  233. Read: 1,
  234. /**
  235. * If the host is write only.
  236. */
  237. Write: 2,
  238. /**
  239. * If the host is both read and write.
  240. */
  241. Any: 3,
  242. };
  243. const HostStatusEnum = {
  244. Up: 1,
  245. Down: 2,
  246. Timeouted: 3,
  247. };
  248. // By default, API Clients at Algolia have expiration delay
  249. // of 5 mins. In the JavaScript client, we have 2 mins.
  250. const EXPIRATION_DELAY = 2 * 60 * 1000;
  251. function createStatefulHost(host, status = HostStatusEnum.Up) {
  252. return {
  253. ...host,
  254. status,
  255. lastUpdate: Date.now(),
  256. };
  257. }
  258. function isStatefulHostUp(host) {
  259. return host.status === HostStatusEnum.Up || Date.now() - host.lastUpdate > EXPIRATION_DELAY;
  260. }
  261. function isStatefulHostTimeouted(host) {
  262. return (host.status === HostStatusEnum.Timeouted && Date.now() - host.lastUpdate <= EXPIRATION_DELAY);
  263. }
  264. function createStatelessHost(options) {
  265. if (typeof options === 'string') {
  266. return {
  267. protocol: 'https',
  268. url: options,
  269. accept: CallEnum.Any,
  270. };
  271. }
  272. return {
  273. protocol: options.protocol || 'https',
  274. url: options.url,
  275. accept: options.accept || CallEnum.Any,
  276. };
  277. }
  278. const MethodEnum = {
  279. Delete: 'DELETE',
  280. Get: 'GET',
  281. Post: 'POST',
  282. Put: 'PUT',
  283. };
  284. function createRetryableOptions(hostsCache, statelessHosts) {
  285. return Promise.all(statelessHosts.map(statelessHost => {
  286. return hostsCache.get(statelessHost, () => {
  287. return Promise.resolve(createStatefulHost(statelessHost));
  288. });
  289. })).then(statefulHosts => {
  290. const hostsUp = statefulHosts.filter(host => isStatefulHostUp(host));
  291. const hostsTimeouted = statefulHosts.filter(host => isStatefulHostTimeouted(host));
  292. /**
  293. * Note, we put the hosts that previously timeouted on the end of the list.
  294. */
  295. const hostsAvailable = [...hostsUp, ...hostsTimeouted];
  296. const statelessHostsAvailable = hostsAvailable.length > 0
  297. ? hostsAvailable.map(host => createStatelessHost(host))
  298. : statelessHosts;
  299. return {
  300. getTimeout(timeoutsCount, baseTimeout) {
  301. /**
  302. * Imagine that you have 4 hosts, if timeouts will increase
  303. * on the following way: 1 (timeouted) > 4 (timeouted) > 5 (200)
  304. *
  305. * Note that, the very next request, we start from the previous timeout
  306. *
  307. * 5 (timeouted) > 6 (timeouted) > 7 ...
  308. *
  309. * This strategy may need to be reviewed, but is the strategy on the our
  310. * current v3 version.
  311. */
  312. const timeoutMultiplier = hostsTimeouted.length === 0 && timeoutsCount === 0
  313. ? 1
  314. : hostsTimeouted.length + 3 + timeoutsCount;
  315. return timeoutMultiplier * baseTimeout;
  316. },
  317. statelessHosts: statelessHostsAvailable,
  318. };
  319. });
  320. }
  321. const isNetworkError = ({ isTimedOut, status }) => {
  322. return !isTimedOut && ~~status === 0;
  323. };
  324. const isRetryable = (response) => {
  325. const status = response.status;
  326. const isTimedOut = response.isTimedOut;
  327. return (isTimedOut || isNetworkError(response) || (~~(status / 100) !== 2 && ~~(status / 100) !== 4));
  328. };
  329. const isSuccess = ({ status }) => {
  330. return ~~(status / 100) === 2;
  331. };
  332. const retryDecision = (response, outcomes) => {
  333. if (isRetryable(response)) {
  334. return outcomes.onRetry(response);
  335. }
  336. if (isSuccess(response)) {
  337. return outcomes.onSuccess(response);
  338. }
  339. return outcomes.onFail(response);
  340. };
  341. function retryableRequest(transporter, statelessHosts, request, requestOptions) {
  342. const stackTrace = []; // eslint-disable-line functional/prefer-readonly-type
  343. /**
  344. * First we prepare the payload that do not depend from hosts.
  345. */
  346. const data = serializeData(request, requestOptions);
  347. const headers = serializeHeaders(transporter, requestOptions);
  348. const method = request.method;
  349. // On `GET`, the data is proxied to query parameters.
  350. const dataQueryParameters = request.method !== MethodEnum.Get
  351. ? {}
  352. : {
  353. ...request.data,
  354. ...requestOptions.data,
  355. };
  356. const queryParameters = {
  357. 'x-algolia-agent': transporter.userAgent.value,
  358. ...transporter.queryParameters,
  359. ...dataQueryParameters,
  360. ...requestOptions.queryParameters,
  361. };
  362. let timeoutsCount = 0; // eslint-disable-line functional/no-let
  363. const retry = (hosts, // eslint-disable-line functional/prefer-readonly-type
  364. getTimeout) => {
  365. /**
  366. * We iterate on each host, until there is no host left.
  367. */
  368. const host = hosts.pop(); // eslint-disable-line functional/immutable-data
  369. if (host === undefined) {
  370. throw createRetryError(stackTraceWithoutCredentials(stackTrace));
  371. }
  372. const payload = {
  373. data,
  374. headers,
  375. method,
  376. url: serializeUrl(host, request.path, queryParameters),
  377. connectTimeout: getTimeout(timeoutsCount, transporter.timeouts.connect),
  378. responseTimeout: getTimeout(timeoutsCount, requestOptions.timeout),
  379. };
  380. /**
  381. * The stackFrame is pushed to the stackTrace so we
  382. * can have information about onRetry and onFailure
  383. * decisions.
  384. */
  385. const pushToStackTrace = (response) => {
  386. const stackFrame = {
  387. request: payload,
  388. response,
  389. host,
  390. triesLeft: hosts.length,
  391. };
  392. // eslint-disable-next-line functional/immutable-data
  393. stackTrace.push(stackFrame);
  394. return stackFrame;
  395. };
  396. const decisions = {
  397. onSuccess: response => deserializeSuccess(response),
  398. onRetry(response) {
  399. const stackFrame = pushToStackTrace(response);
  400. /**
  401. * If response is a timeout, we increaset the number of
  402. * timeouts so we can increase the timeout later.
  403. */
  404. if (response.isTimedOut) {
  405. timeoutsCount++;
  406. }
  407. return Promise.all([
  408. /**
  409. * Failures are individually send the logger, allowing
  410. * the end user to debug / store stack frames even
  411. * when a retry error does not happen.
  412. */
  413. transporter.logger.info('Retryable failure', stackFrameWithoutCredentials(stackFrame)),
  414. /**
  415. * We also store the state of the host in failure cases. If the host, is
  416. * down it will remain down for the next 2 minutes. In a timeout situation,
  417. * this host will be added end of the list of hosts on the next request.
  418. */
  419. transporter.hostsCache.set(host, createStatefulHost(host, response.isTimedOut ? HostStatusEnum.Timeouted : HostStatusEnum.Down)),
  420. ]).then(() => retry(hosts, getTimeout));
  421. },
  422. onFail(response) {
  423. pushToStackTrace(response);
  424. throw deserializeFailure(response, stackTraceWithoutCredentials(stackTrace));
  425. },
  426. };
  427. return transporter.requester.send(payload).then(response => {
  428. return retryDecision(response, decisions);
  429. });
  430. };
  431. /**
  432. * Finally, for each retryable host perform request until we got a non
  433. * retryable response. Some notes here:
  434. *
  435. * 1. The reverse here is applied so we can apply a `pop` later on => more performant.
  436. * 2. We also get from the retryable options a timeout multiplier that is tailored
  437. * for the current context.
  438. */
  439. return createRetryableOptions(transporter.hostsCache, statelessHosts).then(options => {
  440. return retry([...options.statelessHosts].reverse(), options.getTimeout);
  441. });
  442. }
  443. function createTransporter(options) {
  444. const { hostsCache, logger, requester, requestsCache, responsesCache, timeouts, userAgent, hosts, queryParameters, headers, } = options;
  445. const transporter = {
  446. hostsCache,
  447. logger,
  448. requester,
  449. requestsCache,
  450. responsesCache,
  451. timeouts,
  452. userAgent,
  453. headers,
  454. queryParameters,
  455. hosts: hosts.map(host => createStatelessHost(host)),
  456. read(request, requestOptions) {
  457. /**
  458. * First, we compute the user request options. Now, keep in mind,
  459. * that using request options the user is able to modified the intire
  460. * payload of the request. Such as headers, query parameters, and others.
  461. */
  462. const mappedRequestOptions = createMappedRequestOptions(requestOptions, transporter.timeouts.read);
  463. const createRetryableRequest = () => {
  464. /**
  465. * Then, we prepare a function factory that contains the construction of
  466. * the retryable request. At this point, we may *not* perform the actual
  467. * request. But we want to have the function factory ready.
  468. */
  469. return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Read) !== 0), request, mappedRequestOptions);
  470. };
  471. /**
  472. * Once we have the function factory ready, we need to determine of the
  473. * request is "cacheable" - should be cached. Note that, once again,
  474. * the user can force this option.
  475. */
  476. const cacheable = mappedRequestOptions.cacheable !== undefined
  477. ? mappedRequestOptions.cacheable
  478. : request.cacheable;
  479. /**
  480. * If is not "cacheable", we immediatly trigger the retryable request, no
  481. * need to check cache implementations.
  482. */
  483. if (cacheable !== true) {
  484. return createRetryableRequest();
  485. }
  486. /**
  487. * If the request is "cacheable", we need to first compute the key to ask
  488. * the cache implementations if this request is on progress or if the
  489. * response already exists on the cache.
  490. */
  491. const key = {
  492. request,
  493. mappedRequestOptions,
  494. transporter: {
  495. queryParameters: transporter.queryParameters,
  496. headers: transporter.headers,
  497. },
  498. };
  499. /**
  500. * With the computed key, we first ask the responses cache
  501. * implemention if this request was been resolved before.
  502. */
  503. return transporter.responsesCache.get(key, () => {
  504. /**
  505. * If the request has never resolved before, we actually ask if there
  506. * is a current request with the same key on progress.
  507. */
  508. return transporter.requestsCache.get(key, () => {
  509. return (transporter.requestsCache
  510. /**
  511. * Finally, if there is no request in progress with the same key,
  512. * this `createRetryableRequest()` will actually trigger the
  513. * retryable request.
  514. */
  515. .set(key, createRetryableRequest())
  516. .then(response => Promise.all([transporter.requestsCache.delete(key), response]), err => Promise.all([transporter.requestsCache.delete(key), Promise.reject(err)]))
  517. .then(([_, response]) => response));
  518. });
  519. }, {
  520. /**
  521. * Of course, once we get this response back from the server, we
  522. * tell response cache to actually store the received response
  523. * to be used later.
  524. */
  525. miss: response => transporter.responsesCache.set(key, response),
  526. });
  527. },
  528. write(request, requestOptions) {
  529. /**
  530. * On write requests, no cache mechanisms are applied, and we
  531. * proxy the request immediately to the requester.
  532. */
  533. return retryableRequest(transporter, transporter.hosts.filter(host => (host.accept & CallEnum.Write) !== 0), request, createMappedRequestOptions(requestOptions, transporter.timeouts.write));
  534. },
  535. };
  536. return transporter;
  537. }
  538. function createUserAgent(version) {
  539. const userAgent = {
  540. value: `Algolia for JavaScript (${version})`,
  541. add(options) {
  542. const addedUserAgent = `; ${options.segment}${options.version !== undefined ? ` (${options.version})` : ''}`;
  543. if (userAgent.value.indexOf(addedUserAgent) === -1) {
  544. // eslint-disable-next-line functional/immutable-data
  545. userAgent.value = `${userAgent.value}${addedUserAgent}`;
  546. }
  547. return userAgent;
  548. },
  549. };
  550. return userAgent;
  551. }
  552. function deserializeSuccess(response) {
  553. // eslint-disable-next-line functional/no-try-statement
  554. try {
  555. return JSON.parse(response.content);
  556. }
  557. catch (e) {
  558. throw createDeserializationError(e.message, response);
  559. }
  560. }
  561. function deserializeFailure({ content, status }, stackFrame) {
  562. // eslint-disable-next-line functional/no-let
  563. let message = content;
  564. // eslint-disable-next-line functional/no-try-statement
  565. try {
  566. message = JSON.parse(content).message;
  567. }
  568. catch (e) {
  569. // ..
  570. }
  571. return createApiError(message, status, stackFrame);
  572. }
  573. function serializeUrl(host, path, queryParameters) {
  574. const queryParametersAsString = serializeQueryParameters(queryParameters);
  575. // eslint-disable-next-line functional/no-let
  576. let url = `${host.protocol}://${host.url}/${path.charAt(0) === '/' ? path.substr(1) : path}`;
  577. if (queryParametersAsString.length) {
  578. url += `?${queryParametersAsString}`;
  579. }
  580. return url;
  581. }
  582. function serializeQueryParameters(parameters) {
  583. const isObjectOrArray = (value) => Object.prototype.toString.call(value) === '[object Object]' ||
  584. Object.prototype.toString.call(value) === '[object Array]';
  585. return Object.keys(parameters)
  586. .map(key => encode('%s=%s', key, isObjectOrArray(parameters[key]) ? JSON.stringify(parameters[key]) : parameters[key]))
  587. .join('&');
  588. }
  589. function serializeData(request, requestOptions) {
  590. if (request.method === MethodEnum.Get ||
  591. (request.data === undefined && requestOptions.data === undefined)) {
  592. return undefined;
  593. }
  594. const data = Array.isArray(request.data)
  595. ? request.data
  596. : { ...request.data, ...requestOptions.data };
  597. return JSON.stringify(data);
  598. }
  599. function serializeHeaders(transporter, requestOptions) {
  600. const headers = {
  601. ...transporter.headers,
  602. ...requestOptions.headers,
  603. };
  604. const serializedHeaders = {};
  605. Object.keys(headers).forEach(header => {
  606. const value = headers[header];
  607. // @ts-ignore
  608. // eslint-disable-next-line functional/immutable-data
  609. serializedHeaders[header.toLowerCase()] = value;
  610. });
  611. return serializedHeaders;
  612. }
  613. function stackTraceWithoutCredentials(stackTrace) {
  614. return stackTrace.map(stackFrame => stackFrameWithoutCredentials(stackFrame));
  615. }
  616. function stackFrameWithoutCredentials(stackFrame) {
  617. const modifiedHeaders = stackFrame.request.headers['x-algolia-api-key']
  618. ? { 'x-algolia-api-key': '*****' }
  619. : {};
  620. return {
  621. ...stackFrame,
  622. request: {
  623. ...stackFrame.request,
  624. headers: {
  625. ...stackFrame.request.headers,
  626. ...modifiedHeaders,
  627. },
  628. },
  629. };
  630. }
  631. function createApiError(message, status, transporterStackTrace) {
  632. return {
  633. name: 'ApiError',
  634. message,
  635. status,
  636. transporterStackTrace,
  637. };
  638. }
  639. function createDeserializationError(message, response) {
  640. return {
  641. name: 'DeserializationError',
  642. message,
  643. response,
  644. };
  645. }
  646. function createRetryError(transporterStackTrace) {
  647. return {
  648. name: 'RetryError',
  649. message: 'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.',
  650. transporterStackTrace,
  651. };
  652. }
  653. const createAnalyticsClient = options => {
  654. const region = options.region || 'us';
  655. const auth = createAuth(AuthMode.WithinHeaders, options.appId, options.apiKey);
  656. const transporter = createTransporter({
  657. hosts: [{ url: `analytics.${region}.algolia.com` }],
  658. ...options,
  659. headers: {
  660. ...auth.headers(),
  661. ...{ 'content-type': 'application/json' },
  662. ...options.headers,
  663. },
  664. queryParameters: {
  665. ...auth.queryParameters(),
  666. ...options.queryParameters,
  667. },
  668. });
  669. const appId = options.appId;
  670. return addMethods({ appId, transporter }, options.methods);
  671. };
  672. const addABTest = (base) => {
  673. return (abTest, requestOptions) => {
  674. return base.transporter.write({
  675. method: MethodEnum.Post,
  676. path: '2/abtests',
  677. data: abTest,
  678. }, requestOptions);
  679. };
  680. };
  681. const deleteABTest = (base) => {
  682. return (abTestID, requestOptions) => {
  683. return base.transporter.write({
  684. method: MethodEnum.Delete,
  685. path: encode('2/abtests/%s', abTestID),
  686. }, requestOptions);
  687. };
  688. };
  689. const getABTest = (base) => {
  690. return (abTestID, requestOptions) => {
  691. return base.transporter.read({
  692. method: MethodEnum.Get,
  693. path: encode('2/abtests/%s', abTestID),
  694. }, requestOptions);
  695. };
  696. };
  697. const getABTests = (base) => {
  698. return (requestOptions) => {
  699. return base.transporter.read({
  700. method: MethodEnum.Get,
  701. path: '2/abtests',
  702. }, requestOptions);
  703. };
  704. };
  705. const stopABTest = (base) => {
  706. return (abTestID, requestOptions) => {
  707. return base.transporter.write({
  708. method: MethodEnum.Post,
  709. path: encode('2/abtests/%s/stop', abTestID),
  710. }, requestOptions);
  711. };
  712. };
  713. const createPersonalizationClient = options => {
  714. const region = options.region || 'us';
  715. const auth = createAuth(AuthMode.WithinHeaders, options.appId, options.apiKey);
  716. const transporter = createTransporter({
  717. hosts: [{ url: `personalization.${region}.algolia.com` }],
  718. ...options,
  719. headers: {
  720. ...auth.headers(),
  721. ...{ 'content-type': 'application/json' },
  722. ...options.headers,
  723. },
  724. queryParameters: {
  725. ...auth.queryParameters(),
  726. ...options.queryParameters,
  727. },
  728. });
  729. return addMethods({ appId: options.appId, transporter }, options.methods);
  730. };
  731. const getPersonalizationStrategy = (base) => {
  732. return (requestOptions) => {
  733. return base.transporter.read({
  734. method: MethodEnum.Get,
  735. path: '1/strategies/personalization',
  736. }, requestOptions);
  737. };
  738. };
  739. const setPersonalizationStrategy = (base) => {
  740. return (personalizationStrategy, requestOptions) => {
  741. return base.transporter.write({
  742. method: MethodEnum.Post,
  743. path: '1/strategies/personalization',
  744. data: personalizationStrategy,
  745. }, requestOptions);
  746. };
  747. };
  748. function createBrowsablePromise(options) {
  749. const browse = (data) => {
  750. return options.request(data).then(response => {
  751. /**
  752. * First we send to the developer the
  753. * batch retrieved from the API.
  754. */
  755. if (options.batch !== undefined) {
  756. options.batch(response.hits);
  757. }
  758. /**
  759. * Then, we ask to the browse concrete implementation
  760. * if we should stop browsing. As example, the `browseObjects`
  761. * method will stop if the cursor is not present on the response.
  762. */
  763. if (options.shouldStop(response)) {
  764. return undefined;
  765. }
  766. /**
  767. * Finally, if the response contains a cursor, we browse to the next
  768. * batch using that same cursor. Otherwise, we just use the traditional
  769. * browsing using the page element.
  770. */
  771. if (response.cursor) {
  772. return browse({
  773. cursor: response.cursor,
  774. });
  775. }
  776. return browse({
  777. page: (data.page || 0) + 1,
  778. });
  779. });
  780. };
  781. return browse({});
  782. }
  783. const createSearchClient = options => {
  784. const appId = options.appId;
  785. const auth = createAuth(options.authMode !== undefined ? options.authMode : AuthMode.WithinHeaders, appId, options.apiKey);
  786. const transporter = createTransporter({
  787. hosts: [
  788. { url: `${appId}-dsn.algolia.net`, accept: CallEnum.Read },
  789. { url: `${appId}.algolia.net`, accept: CallEnum.Write },
  790. ].concat(shuffle([
  791. { url: `${appId}-1.algolianet.com` },
  792. { url: `${appId}-2.algolianet.com` },
  793. { url: `${appId}-3.algolianet.com` },
  794. ])),
  795. ...options,
  796. headers: {
  797. ...auth.headers(),
  798. ...{ 'content-type': 'application/x-www-form-urlencoded' },
  799. ...options.headers,
  800. },
  801. queryParameters: {
  802. ...auth.queryParameters(),
  803. ...options.queryParameters,
  804. },
  805. });
  806. const base = {
  807. transporter,
  808. appId,
  809. addAlgoliaAgent(segment, version) {
  810. transporter.userAgent.add({ segment, version });
  811. },
  812. clearCache() {
  813. return Promise.all([
  814. transporter.requestsCache.clear(),
  815. transporter.responsesCache.clear(),
  816. ]).then(() => undefined);
  817. },
  818. };
  819. return addMethods(base, options.methods);
  820. };
  821. function createMissingObjectIDError() {
  822. return {
  823. name: 'MissingObjectIDError',
  824. message: 'All objects must have an unique objectID ' +
  825. '(like a primary key) to be valid. ' +
  826. 'Algolia is also able to generate objectIDs ' +
  827. "automatically but *it's not recommended*. " +
  828. "To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option.",
  829. };
  830. }
  831. function createObjectNotFoundError() {
  832. return {
  833. name: 'ObjectNotFoundError',
  834. message: 'Object not found.',
  835. };
  836. }
  837. const addApiKey = (base) => {
  838. return (acl, requestOptions) => {
  839. const { queryParameters, ...options } = requestOptions || {};
  840. const data = {
  841. acl,
  842. ...(queryParameters !== undefined ? { queryParameters } : {}),
  843. };
  844. const wait = (response, waitRequestOptions) => {
  845. return createRetryablePromise(retry => {
  846. return getApiKey(base)(response.key, waitRequestOptions).catch((apiError) => {
  847. if (apiError.status !== 404) {
  848. throw apiError;
  849. }
  850. return retry();
  851. });
  852. });
  853. };
  854. return createWaitablePromise(base.transporter.write({
  855. method: MethodEnum.Post,
  856. path: '1/keys',
  857. data,
  858. }, options), wait);
  859. };
  860. };
  861. const assignUserID = (base) => {
  862. return (userID, clusterName, requestOptions) => {
  863. const mappedRequestOptions = createMappedRequestOptions(requestOptions);
  864. // eslint-disable-next-line functional/immutable-data
  865. mappedRequestOptions.queryParameters['X-Algolia-User-ID'] = userID;
  866. return base.transporter.write({
  867. method: MethodEnum.Post,
  868. path: '1/clusters/mapping',
  869. data: { cluster: clusterName },
  870. }, mappedRequestOptions);
  871. };
  872. };
  873. const assignUserIDs = (base) => {
  874. return (userIDs, clusterName, requestOptions) => {
  875. return base.transporter.write({
  876. method: MethodEnum.Post,
  877. path: '1/clusters/mapping/batch',
  878. data: {
  879. users: userIDs,
  880. cluster: clusterName,
  881. },
  882. }, requestOptions);
  883. };
  884. };
  885. const clearDictionaryEntries = (base) => {
  886. return (dictionary, requestOptions) => {
  887. return createWaitablePromise(base.transporter.write({
  888. method: MethodEnum.Post,
  889. path: encode('/1/dictionaries/%s/batch', dictionary),
  890. data: {
  891. clearExistingDictionaryEntries: true,
  892. requests: { action: 'addEntry', body: [] },
  893. },
  894. }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions));
  895. };
  896. };
  897. const copyIndex = (base) => {
  898. return (from, to, requestOptions) => {
  899. const wait = (response, waitRequestOptions) => {
  900. return initIndex(base)(from, {
  901. methods: { waitTask },
  902. }).waitTask(response.taskID, waitRequestOptions);
  903. };
  904. return createWaitablePromise(base.transporter.write({
  905. method: MethodEnum.Post,
  906. path: encode('1/indexes/%s/operation', from),
  907. data: {
  908. operation: 'copy',
  909. destination: to,
  910. },
  911. }, requestOptions), wait);
  912. };
  913. };
  914. const copyRules = (base) => {
  915. return (from, to, requestOptions) => {
  916. return copyIndex(base)(from, to, {
  917. ...requestOptions,
  918. scope: [ScopeEnum.Rules],
  919. });
  920. };
  921. };
  922. const copySettings = (base) => {
  923. return (from, to, requestOptions) => {
  924. return copyIndex(base)(from, to, {
  925. ...requestOptions,
  926. scope: [ScopeEnum.Settings],
  927. });
  928. };
  929. };
  930. const copySynonyms = (base) => {
  931. return (from, to, requestOptions) => {
  932. return copyIndex(base)(from, to, {
  933. ...requestOptions,
  934. scope: [ScopeEnum.Synonyms],
  935. });
  936. };
  937. };
  938. const customRequest = (base) => {
  939. return (request, requestOptions) => {
  940. if (request.method === MethodEnum.Get) {
  941. return base.transporter.read(request, requestOptions);
  942. }
  943. return base.transporter.write(request, requestOptions);
  944. };
  945. };
  946. const deleteApiKey = (base) => {
  947. return (apiKey, requestOptions) => {
  948. const wait = (_, waitRequestOptions) => {
  949. return createRetryablePromise(retry => {
  950. return getApiKey(base)(apiKey, waitRequestOptions)
  951. .then(retry)
  952. .catch((apiError) => {
  953. if (apiError.status !== 404) {
  954. throw apiError;
  955. }
  956. });
  957. });
  958. };
  959. return createWaitablePromise(base.transporter.write({
  960. method: MethodEnum.Delete,
  961. path: encode('1/keys/%s', apiKey),
  962. }, requestOptions), wait);
  963. };
  964. };
  965. const deleteDictionaryEntries = (base) => {
  966. return (dictionary, objectIDs, requestOptions) => {
  967. const requests = objectIDs.map(objectID => ({
  968. action: 'deleteEntry',
  969. body: { objectID },
  970. }));
  971. return createWaitablePromise(base.transporter.write({
  972. method: MethodEnum.Post,
  973. path: encode('/1/dictionaries/%s/batch', dictionary),
  974. data: { clearExistingDictionaryEntries: false, requests },
  975. }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions));
  976. };
  977. };
  978. const getApiKey = (base) => {
  979. return (apiKey, requestOptions) => {
  980. return base.transporter.read({
  981. method: MethodEnum.Get,
  982. path: encode('1/keys/%s', apiKey),
  983. }, requestOptions);
  984. };
  985. };
  986. const getAppTask = (base) => {
  987. return (taskID, requestOptions) => {
  988. return base.transporter.read({
  989. method: MethodEnum.Get,
  990. path: encode('1/task/%s', taskID.toString()),
  991. }, requestOptions);
  992. };
  993. };
  994. const getDictionarySettings = (base) => {
  995. return (requestOptions) => {
  996. return base.transporter.read({
  997. method: MethodEnum.Get,
  998. path: '/1/dictionaries/*/settings',
  999. }, requestOptions);
  1000. };
  1001. };
  1002. const getLogs = (base) => {
  1003. return (requestOptions) => {
  1004. return base.transporter.read({
  1005. method: MethodEnum.Get,
  1006. path: '1/logs',
  1007. }, requestOptions);
  1008. };
  1009. };
  1010. const getTopUserIDs = (base) => {
  1011. return (requestOptions) => {
  1012. return base.transporter.read({
  1013. method: MethodEnum.Get,
  1014. path: '1/clusters/mapping/top',
  1015. }, requestOptions);
  1016. };
  1017. };
  1018. const getUserID = (base) => {
  1019. return (userID, requestOptions) => {
  1020. return base.transporter.read({
  1021. method: MethodEnum.Get,
  1022. path: encode('1/clusters/mapping/%s', userID),
  1023. }, requestOptions);
  1024. };
  1025. };
  1026. const hasPendingMappings = (base) => {
  1027. return (requestOptions) => {
  1028. const { retrieveMappings, ...options } = requestOptions || {};
  1029. if (retrieveMappings === true) {
  1030. // eslint-disable-next-line functional/immutable-data
  1031. options.getClusters = true;
  1032. }
  1033. return base.transporter.read({
  1034. method: MethodEnum.Get,
  1035. path: '1/clusters/mapping/pending',
  1036. }, options);
  1037. };
  1038. };
  1039. const initIndex = (base) => {
  1040. return (indexName, options = {}) => {
  1041. const searchIndex = {
  1042. transporter: base.transporter,
  1043. appId: base.appId,
  1044. indexName,
  1045. };
  1046. return addMethods(searchIndex, options.methods);
  1047. };
  1048. };
  1049. const listApiKeys = (base) => {
  1050. return (requestOptions) => {
  1051. return base.transporter.read({
  1052. method: MethodEnum.Get,
  1053. path: '1/keys',
  1054. }, requestOptions);
  1055. };
  1056. };
  1057. const listClusters = (base) => {
  1058. return (requestOptions) => {
  1059. return base.transporter.read({
  1060. method: MethodEnum.Get,
  1061. path: '1/clusters',
  1062. }, requestOptions);
  1063. };
  1064. };
  1065. const listIndices = (base) => {
  1066. return (requestOptions) => {
  1067. return base.transporter.read({
  1068. method: MethodEnum.Get,
  1069. path: '1/indexes',
  1070. }, requestOptions);
  1071. };
  1072. };
  1073. const listUserIDs = (base) => {
  1074. return (requestOptions) => {
  1075. return base.transporter.read({
  1076. method: MethodEnum.Get,
  1077. path: '1/clusters/mapping',
  1078. }, requestOptions);
  1079. };
  1080. };
  1081. const moveIndex = (base) => {
  1082. return (from, to, requestOptions) => {
  1083. const wait = (response, waitRequestOptions) => {
  1084. return initIndex(base)(from, {
  1085. methods: { waitTask },
  1086. }).waitTask(response.taskID, waitRequestOptions);
  1087. };
  1088. return createWaitablePromise(base.transporter.write({
  1089. method: MethodEnum.Post,
  1090. path: encode('1/indexes/%s/operation', from),
  1091. data: {
  1092. operation: 'move',
  1093. destination: to,
  1094. },
  1095. }, requestOptions), wait);
  1096. };
  1097. };
  1098. const multipleBatch = (base) => {
  1099. return (requests, requestOptions) => {
  1100. const wait = (response, waitRequestOptions) => {
  1101. return Promise.all(Object.keys(response.taskID).map(indexName => {
  1102. return initIndex(base)(indexName, {
  1103. methods: { waitTask },
  1104. }).waitTask(response.taskID[indexName], waitRequestOptions);
  1105. }));
  1106. };
  1107. return createWaitablePromise(base.transporter.write({
  1108. method: MethodEnum.Post,
  1109. path: '1/indexes/*/batch',
  1110. data: {
  1111. requests,
  1112. },
  1113. }, requestOptions), wait);
  1114. };
  1115. };
  1116. const multipleGetObjects = (base) => {
  1117. return (requests, requestOptions) => {
  1118. return base.transporter.read({
  1119. method: MethodEnum.Post,
  1120. path: '1/indexes/*/objects',
  1121. data: {
  1122. requests,
  1123. },
  1124. }, requestOptions);
  1125. };
  1126. };
  1127. const multipleQueries = (base) => {
  1128. return (queries, requestOptions) => {
  1129. const requests = queries.map(query => {
  1130. return {
  1131. ...query,
  1132. params: serializeQueryParameters(query.params || {}),
  1133. };
  1134. });
  1135. return base.transporter.read({
  1136. method: MethodEnum.Post,
  1137. path: '1/indexes/*/queries',
  1138. data: {
  1139. requests,
  1140. },
  1141. cacheable: true,
  1142. }, requestOptions);
  1143. };
  1144. };
  1145. const multipleSearchForFacetValues = (base) => {
  1146. return (queries, requestOptions) => {
  1147. return Promise.all(queries.map(query => {
  1148. const { facetName, facetQuery, ...params } = query.params;
  1149. return initIndex(base)(query.indexName, {
  1150. methods: { searchForFacetValues },
  1151. }).searchForFacetValues(facetName, facetQuery, {
  1152. ...requestOptions,
  1153. ...params,
  1154. });
  1155. }));
  1156. };
  1157. };
  1158. const removeUserID = (base) => {
  1159. return (userID, requestOptions) => {
  1160. const mappedRequestOptions = createMappedRequestOptions(requestOptions);
  1161. // eslint-disable-next-line functional/immutable-data
  1162. mappedRequestOptions.queryParameters['X-Algolia-User-ID'] = userID;
  1163. return base.transporter.write({
  1164. method: MethodEnum.Delete,
  1165. path: '1/clusters/mapping',
  1166. }, mappedRequestOptions);
  1167. };
  1168. };
  1169. const replaceDictionaryEntries = (base) => {
  1170. return (dictionary, entries, requestOptions) => {
  1171. const requests = entries.map(entry => ({
  1172. action: 'addEntry',
  1173. body: entry,
  1174. }));
  1175. return createWaitablePromise(base.transporter.write({
  1176. method: MethodEnum.Post,
  1177. path: encode('/1/dictionaries/%s/batch', dictionary),
  1178. data: { clearExistingDictionaryEntries: true, requests },
  1179. }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions));
  1180. };
  1181. };
  1182. const restoreApiKey = (base) => {
  1183. return (apiKey, requestOptions) => {
  1184. const wait = (_, waitRequestOptions) => {
  1185. return createRetryablePromise(retry => {
  1186. return getApiKey(base)(apiKey, waitRequestOptions).catch((apiError) => {
  1187. if (apiError.status !== 404) {
  1188. throw apiError;
  1189. }
  1190. return retry();
  1191. });
  1192. });
  1193. };
  1194. return createWaitablePromise(base.transporter.write({
  1195. method: MethodEnum.Post,
  1196. path: encode('1/keys/%s/restore', apiKey),
  1197. }, requestOptions), wait);
  1198. };
  1199. };
  1200. const saveDictionaryEntries = (base) => {
  1201. return (dictionary, entries, requestOptions) => {
  1202. const requests = entries.map(entry => ({
  1203. action: 'addEntry',
  1204. body: entry,
  1205. }));
  1206. return createWaitablePromise(base.transporter.write({
  1207. method: MethodEnum.Post,
  1208. path: encode('/1/dictionaries/%s/batch', dictionary),
  1209. data: { clearExistingDictionaryEntries: false, requests },
  1210. }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions));
  1211. };
  1212. };
  1213. const searchDictionaryEntries = (base) => {
  1214. return (dictionary, query, requestOptions) => {
  1215. return base.transporter.read({
  1216. method: MethodEnum.Post,
  1217. path: encode('/1/dictionaries/%s/search', dictionary),
  1218. data: {
  1219. query,
  1220. },
  1221. cacheable: true,
  1222. }, requestOptions);
  1223. };
  1224. };
  1225. const searchUserIDs = (base) => {
  1226. return (query, requestOptions) => {
  1227. return base.transporter.read({
  1228. method: MethodEnum.Post,
  1229. path: '1/clusters/mapping/search',
  1230. data: {
  1231. query,
  1232. },
  1233. }, requestOptions);
  1234. };
  1235. };
  1236. const setDictionarySettings = (base) => {
  1237. return (settings, requestOptions) => {
  1238. return createWaitablePromise(base.transporter.write({
  1239. method: MethodEnum.Put,
  1240. path: '/1/dictionaries/*/settings',
  1241. data: settings,
  1242. }, requestOptions), (response, waitRequestOptions) => waitAppTask(base)(response.taskID, waitRequestOptions));
  1243. };
  1244. };
  1245. const updateApiKey = (base) => {
  1246. return (apiKey, requestOptions) => {
  1247. const updatedFields = Object.assign({}, requestOptions);
  1248. const { queryParameters, ...options } = requestOptions || {};
  1249. const data = queryParameters ? { queryParameters } : {};
  1250. const apiKeyFields = [
  1251. 'acl',
  1252. 'indexes',
  1253. 'referers',
  1254. 'restrictSources',
  1255. 'queryParameters',
  1256. 'description',
  1257. 'maxQueriesPerIPPerHour',
  1258. 'maxHitsPerQuery',
  1259. ];
  1260. const hasChanged = (getApiKeyResponse) => {
  1261. return Object.keys(updatedFields)
  1262. .filter((updatedField) => apiKeyFields.indexOf(updatedField) !== -1)
  1263. .every(updatedField => {
  1264. return getApiKeyResponse[updatedField] === updatedFields[updatedField];
  1265. });
  1266. };
  1267. const wait = (_, waitRequestOptions) => createRetryablePromise(retry => {
  1268. return getApiKey(base)(apiKey, waitRequestOptions).then(getApiKeyResponse => {
  1269. return hasChanged(getApiKeyResponse) ? Promise.resolve() : retry();
  1270. });
  1271. });
  1272. return createWaitablePromise(base.transporter.write({
  1273. method: MethodEnum.Put,
  1274. path: encode('1/keys/%s', apiKey),
  1275. data,
  1276. }, options), wait);
  1277. };
  1278. };
  1279. const waitAppTask = (base) => {
  1280. return (taskID, requestOptions) => {
  1281. return createRetryablePromise(retry => {
  1282. return getAppTask(base)(taskID, requestOptions).then(response => {
  1283. return response.status !== 'published' ? retry() : undefined;
  1284. });
  1285. });
  1286. };
  1287. };
  1288. const batch = (base) => {
  1289. return (requests, requestOptions) => {
  1290. const wait = (response, waitRequestOptions) => {
  1291. return waitTask(base)(response.taskID, waitRequestOptions);
  1292. };
  1293. return createWaitablePromise(base.transporter.write({
  1294. method: MethodEnum.Post,
  1295. path: encode('1/indexes/%s/batch', base.indexName),
  1296. data: {
  1297. requests,
  1298. },
  1299. }, requestOptions), wait);
  1300. };
  1301. };
  1302. const browseObjects = (base) => {
  1303. return (requestOptions) => {
  1304. return createBrowsablePromise({
  1305. shouldStop: response => response.cursor === undefined,
  1306. ...requestOptions,
  1307. request: (data) => base.transporter.read({
  1308. method: MethodEnum.Post,
  1309. path: encode('1/indexes/%s/browse', base.indexName),
  1310. data,
  1311. }, requestOptions),
  1312. });
  1313. };
  1314. };
  1315. const browseRules = (base) => {
  1316. return (requestOptions) => {
  1317. const options = {
  1318. hitsPerPage: 1000,
  1319. ...requestOptions,
  1320. };
  1321. return createBrowsablePromise({
  1322. shouldStop: response => response.hits.length < options.hitsPerPage,
  1323. ...options,
  1324. request(data) {
  1325. return searchRules(base)('', { ...options, ...data }).then((response) => {
  1326. return {
  1327. ...response,
  1328. hits: response.hits.map(rule => {
  1329. // eslint-disable-next-line functional/immutable-data,no-param-reassign
  1330. delete rule._highlightResult;
  1331. return rule;
  1332. }),
  1333. };
  1334. });
  1335. },
  1336. });
  1337. };
  1338. };
  1339. const browseSynonyms = (base) => {
  1340. return (requestOptions) => {
  1341. const options = {
  1342. hitsPerPage: 1000,
  1343. ...requestOptions,
  1344. };
  1345. return createBrowsablePromise({
  1346. shouldStop: response => response.hits.length < options.hitsPerPage,
  1347. ...options,
  1348. request(data) {
  1349. return searchSynonyms(base)('', { ...options, ...data }).then((response) => {
  1350. return {
  1351. ...response,
  1352. hits: response.hits.map(synonym => {
  1353. // eslint-disable-next-line functional/immutable-data,no-param-reassign
  1354. delete synonym._highlightResult;
  1355. return synonym;
  1356. }),
  1357. };
  1358. });
  1359. },
  1360. });
  1361. };
  1362. };
  1363. const chunkedBatch = (base) => {
  1364. return (bodies, action, requestOptions) => {
  1365. const { batchSize, ...options } = requestOptions || {};
  1366. const response = {
  1367. taskIDs: [],
  1368. objectIDs: [],
  1369. };
  1370. const forEachBatch = (lastIndex = 0) => {
  1371. // eslint-disable-next-line functional/prefer-readonly-type
  1372. const bodiesChunk = [];
  1373. // eslint-disable-next-line functional/no-let
  1374. let index;
  1375. /* eslint-disable-next-line functional/no-loop-statement */
  1376. for (index = lastIndex; index < bodies.length; index++) {
  1377. // eslint-disable-next-line functional/immutable-data
  1378. bodiesChunk.push(bodies[index]);
  1379. if (bodiesChunk.length === (batchSize || 1000)) {
  1380. break;
  1381. }
  1382. }
  1383. if (bodiesChunk.length === 0) {
  1384. return Promise.resolve(response);
  1385. }
  1386. return batch(base)(bodiesChunk.map(body => {
  1387. return {
  1388. action,
  1389. body,
  1390. };
  1391. }), options).then(res => {
  1392. response.objectIDs = response.objectIDs.concat(res.objectIDs); // eslint-disable-line functional/immutable-data
  1393. response.taskIDs.push(res.taskID); // eslint-disable-line functional/immutable-data
  1394. index++;
  1395. return forEachBatch(index);
  1396. });
  1397. };
  1398. return createWaitablePromise(forEachBatch(), (chunkedBatchResponse, waitRequestOptions) => {
  1399. return Promise.all(chunkedBatchResponse.taskIDs.map(taskID => {
  1400. return waitTask(base)(taskID, waitRequestOptions);
  1401. }));
  1402. });
  1403. };
  1404. };
  1405. const clearObjects = (base) => {
  1406. return (requestOptions) => {
  1407. return createWaitablePromise(base.transporter.write({
  1408. method: MethodEnum.Post,
  1409. path: encode('1/indexes/%s/clear', base.indexName),
  1410. }, requestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1411. };
  1412. };
  1413. const clearRules = (base) => {
  1414. return (requestOptions) => {
  1415. const { forwardToReplicas, ...options } = requestOptions || {};
  1416. const mappedRequestOptions = createMappedRequestOptions(options);
  1417. if (forwardToReplicas) {
  1418. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1419. }
  1420. return createWaitablePromise(base.transporter.write({
  1421. method: MethodEnum.Post,
  1422. path: encode('1/indexes/%s/rules/clear', base.indexName),
  1423. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1424. };
  1425. };
  1426. const clearSynonyms = (base) => {
  1427. return (requestOptions) => {
  1428. const { forwardToReplicas, ...options } = requestOptions || {};
  1429. const mappedRequestOptions = createMappedRequestOptions(options);
  1430. if (forwardToReplicas) {
  1431. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1432. }
  1433. return createWaitablePromise(base.transporter.write({
  1434. method: MethodEnum.Post,
  1435. path: encode('1/indexes/%s/synonyms/clear', base.indexName),
  1436. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1437. };
  1438. };
  1439. const deleteBy = (base) => {
  1440. return (filters, requestOptions) => {
  1441. return createWaitablePromise(base.transporter.write({
  1442. method: MethodEnum.Post,
  1443. path: encode('1/indexes/%s/deleteByQuery', base.indexName),
  1444. data: filters,
  1445. }, requestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1446. };
  1447. };
  1448. const deleteIndex = (base) => {
  1449. return (requestOptions) => {
  1450. return createWaitablePromise(base.transporter.write({
  1451. method: MethodEnum.Delete,
  1452. path: encode('1/indexes/%s', base.indexName),
  1453. }, requestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1454. };
  1455. };
  1456. const deleteObject = (base) => {
  1457. return (objectID, requestOptions) => {
  1458. return createWaitablePromise(deleteObjects(base)([objectID], requestOptions).then(response => {
  1459. return { taskID: response.taskIDs[0] };
  1460. }), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1461. };
  1462. };
  1463. const deleteObjects = (base) => {
  1464. return (objectIDs, requestOptions) => {
  1465. const objects = objectIDs.map(objectID => {
  1466. return { objectID };
  1467. });
  1468. return chunkedBatch(base)(objects, BatchActionEnum.DeleteObject, requestOptions);
  1469. };
  1470. };
  1471. const deleteRule = (base) => {
  1472. return (objectID, requestOptions) => {
  1473. const { forwardToReplicas, ...options } = requestOptions || {};
  1474. const mappedRequestOptions = createMappedRequestOptions(options);
  1475. if (forwardToReplicas) {
  1476. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1477. }
  1478. return createWaitablePromise(base.transporter.write({
  1479. method: MethodEnum.Delete,
  1480. path: encode('1/indexes/%s/rules/%s', base.indexName, objectID),
  1481. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1482. };
  1483. };
  1484. const deleteSynonym = (base) => {
  1485. return (objectID, requestOptions) => {
  1486. const { forwardToReplicas, ...options } = requestOptions || {};
  1487. const mappedRequestOptions = createMappedRequestOptions(options);
  1488. if (forwardToReplicas) {
  1489. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1490. }
  1491. return createWaitablePromise(base.transporter.write({
  1492. method: MethodEnum.Delete,
  1493. path: encode('1/indexes/%s/synonyms/%s', base.indexName, objectID),
  1494. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1495. };
  1496. };
  1497. const exists = (base) => {
  1498. return (requestOptions) => {
  1499. return getSettings(base)(requestOptions)
  1500. .then(() => true)
  1501. .catch(error => {
  1502. if (error.status !== 404) {
  1503. throw error;
  1504. }
  1505. return false;
  1506. });
  1507. };
  1508. };
  1509. const findAnswers = (base) => {
  1510. return (query, queryLanguages, requestOptions) => {
  1511. return base.transporter.read({
  1512. method: MethodEnum.Post,
  1513. path: encode('1/answers/%s/prediction', base.indexName),
  1514. data: {
  1515. query,
  1516. queryLanguages,
  1517. },
  1518. cacheable: true,
  1519. }, requestOptions);
  1520. };
  1521. };
  1522. const findObject = (base) => {
  1523. return (callback, requestOptions) => {
  1524. const { query, paginate, ...options } = requestOptions || {};
  1525. // eslint-disable-next-line functional/no-let
  1526. let page = 0;
  1527. const forEachPage = () => {
  1528. return search(base)(query || '', { ...options, page }).then(result => {
  1529. // eslint-disable-next-line functional/no-loop-statement
  1530. for (const [position, hit] of Object.entries(result.hits)) {
  1531. // eslint-disable-next-line promise/no-callback-in-promise
  1532. if (callback(hit)) {
  1533. return {
  1534. object: hit,
  1535. position: parseInt(position, 10),
  1536. page,
  1537. };
  1538. }
  1539. }
  1540. page++;
  1541. // paginate if option was set and has next page
  1542. if (paginate === false || page >= result.nbPages) {
  1543. throw createObjectNotFoundError();
  1544. }
  1545. return forEachPage();
  1546. });
  1547. };
  1548. return forEachPage();
  1549. };
  1550. };
  1551. const getObject = (base) => {
  1552. return (objectID, requestOptions) => {
  1553. return base.transporter.read({
  1554. method: MethodEnum.Get,
  1555. path: encode('1/indexes/%s/%s', base.indexName, objectID),
  1556. }, requestOptions);
  1557. };
  1558. };
  1559. const getObjectPosition = () => {
  1560. return (searchResponse, objectID) => {
  1561. // eslint-disable-next-line functional/no-loop-statement
  1562. for (const [position, hit] of Object.entries(searchResponse.hits)) {
  1563. if (hit.objectID === objectID) {
  1564. return parseInt(position, 10);
  1565. }
  1566. }
  1567. return -1;
  1568. };
  1569. };
  1570. const getObjects = (base) => {
  1571. return (objectIDs, requestOptions) => {
  1572. const { attributesToRetrieve, ...options } = requestOptions || {};
  1573. const requests = objectIDs.map(objectID => {
  1574. return {
  1575. indexName: base.indexName,
  1576. objectID,
  1577. ...(attributesToRetrieve ? { attributesToRetrieve } : {}),
  1578. };
  1579. });
  1580. return base.transporter.read({
  1581. method: MethodEnum.Post,
  1582. path: '1/indexes/*/objects',
  1583. data: {
  1584. requests,
  1585. },
  1586. }, options);
  1587. };
  1588. };
  1589. const getRule = (base) => {
  1590. return (objectID, requestOptions) => {
  1591. return base.transporter.read({
  1592. method: MethodEnum.Get,
  1593. path: encode('1/indexes/%s/rules/%s', base.indexName, objectID),
  1594. }, requestOptions);
  1595. };
  1596. };
  1597. const getSettings = (base) => {
  1598. return (requestOptions) => {
  1599. return base.transporter.read({
  1600. method: MethodEnum.Get,
  1601. path: encode('1/indexes/%s/settings', base.indexName),
  1602. data: {
  1603. getVersion: 2,
  1604. },
  1605. }, requestOptions);
  1606. };
  1607. };
  1608. const getSynonym = (base) => {
  1609. return (objectID, requestOptions) => {
  1610. return base.transporter.read({
  1611. method: MethodEnum.Get,
  1612. path: encode(`1/indexes/%s/synonyms/%s`, base.indexName, objectID),
  1613. }, requestOptions);
  1614. };
  1615. };
  1616. const getTask = (base) => {
  1617. return (taskID, requestOptions) => {
  1618. return base.transporter.read({
  1619. method: MethodEnum.Get,
  1620. path: encode('1/indexes/%s/task/%s', base.indexName, taskID.toString()),
  1621. }, requestOptions);
  1622. };
  1623. };
  1624. const partialUpdateObject = (base) => {
  1625. return (object, requestOptions) => {
  1626. return createWaitablePromise(partialUpdateObjects(base)([object], requestOptions).then(response => {
  1627. return {
  1628. objectID: response.objectIDs[0],
  1629. taskID: response.taskIDs[0],
  1630. };
  1631. }), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1632. };
  1633. };
  1634. const partialUpdateObjects = (base) => {
  1635. return (objects, requestOptions) => {
  1636. const { createIfNotExists, ...options } = requestOptions || {};
  1637. const action = createIfNotExists
  1638. ? BatchActionEnum.PartialUpdateObject
  1639. : BatchActionEnum.PartialUpdateObjectNoCreate;
  1640. return chunkedBatch(base)(objects, action, options);
  1641. };
  1642. };
  1643. const replaceAllObjects = (base) => {
  1644. return (objects, requestOptions) => {
  1645. const { safe, autoGenerateObjectIDIfNotExist, batchSize, ...options } = requestOptions || {};
  1646. const operation = (from, to, type, operationRequestOptions) => {
  1647. return createWaitablePromise(base.transporter.write({
  1648. method: MethodEnum.Post,
  1649. path: encode('1/indexes/%s/operation', from),
  1650. data: {
  1651. operation: type,
  1652. destination: to,
  1653. },
  1654. }, operationRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1655. };
  1656. const randomSuffix = Math.random()
  1657. .toString(36)
  1658. .substring(7);
  1659. const temporaryIndexName = `${base.indexName}_tmp_${randomSuffix}`;
  1660. const saveObjectsInTemporary = saveObjects({
  1661. appId: base.appId,
  1662. transporter: base.transporter,
  1663. indexName: temporaryIndexName,
  1664. });
  1665. // @ts-ignore
  1666. // eslint-disable-next-line prefer-const, functional/no-let, functional/prefer-readonly-type
  1667. let responses = [];
  1668. const copyWaitablePromise = operation(base.indexName, temporaryIndexName, 'copy', {
  1669. ...options,
  1670. scope: ['settings', 'synonyms', 'rules'],
  1671. });
  1672. // eslint-disable-next-line functional/immutable-data
  1673. responses.push(copyWaitablePromise);
  1674. const result = (safe
  1675. ? copyWaitablePromise.wait(options)
  1676. : copyWaitablePromise)
  1677. .then(() => {
  1678. const saveObjectsWaitablePromise = saveObjectsInTemporary(objects, {
  1679. ...options,
  1680. autoGenerateObjectIDIfNotExist,
  1681. batchSize,
  1682. });
  1683. // eslint-disable-next-line functional/immutable-data
  1684. responses.push(saveObjectsWaitablePromise);
  1685. return safe ? saveObjectsWaitablePromise.wait(options) : saveObjectsWaitablePromise;
  1686. })
  1687. .then(() => {
  1688. const moveWaitablePromise = operation(temporaryIndexName, base.indexName, 'move', options);
  1689. // eslint-disable-next-line functional/immutable-data
  1690. responses.push(moveWaitablePromise);
  1691. return safe ? moveWaitablePromise.wait(options) : moveWaitablePromise;
  1692. })
  1693. .then(() => Promise.all(responses))
  1694. .then(([copyResponse, saveObjectsResponse, moveResponse]) => {
  1695. return {
  1696. objectIDs: saveObjectsResponse.objectIDs,
  1697. taskIDs: [copyResponse.taskID, ...saveObjectsResponse.taskIDs, moveResponse.taskID],
  1698. };
  1699. });
  1700. return createWaitablePromise(result, (_, waitRequestOptions) => {
  1701. return Promise.all(responses.map(response => response.wait(waitRequestOptions)));
  1702. });
  1703. };
  1704. };
  1705. const replaceAllRules = (base) => {
  1706. return (rules, requestOptions) => {
  1707. return saveRules(base)(rules, {
  1708. ...requestOptions,
  1709. clearExistingRules: true,
  1710. });
  1711. };
  1712. };
  1713. const replaceAllSynonyms = (base) => {
  1714. return (synonyms, requestOptions) => {
  1715. return saveSynonyms(base)(synonyms, {
  1716. ...requestOptions,
  1717. clearExistingSynonyms: true,
  1718. });
  1719. };
  1720. };
  1721. const saveObject = (base) => {
  1722. return (object, requestOptions) => {
  1723. return createWaitablePromise(saveObjects(base)([object], requestOptions).then(response => {
  1724. return {
  1725. objectID: response.objectIDs[0],
  1726. taskID: response.taskIDs[0],
  1727. };
  1728. }), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1729. };
  1730. };
  1731. const saveObjects = (base) => {
  1732. return (objects, requestOptions) => {
  1733. const { autoGenerateObjectIDIfNotExist, ...options } = requestOptions || {};
  1734. const action = autoGenerateObjectIDIfNotExist
  1735. ? BatchActionEnum.AddObject
  1736. : BatchActionEnum.UpdateObject;
  1737. if (action === BatchActionEnum.UpdateObject) {
  1738. // eslint-disable-next-line functional/no-loop-statement
  1739. for (const object of objects) {
  1740. if (object.objectID === undefined) {
  1741. return createWaitablePromise(Promise.reject(createMissingObjectIDError()));
  1742. }
  1743. }
  1744. }
  1745. return chunkedBatch(base)(objects, action, options);
  1746. };
  1747. };
  1748. const saveRule = (base) => {
  1749. return (rule, requestOptions) => {
  1750. return saveRules(base)([rule], requestOptions);
  1751. };
  1752. };
  1753. const saveRules = (base) => {
  1754. return (rules, requestOptions) => {
  1755. const { forwardToReplicas, clearExistingRules, ...options } = requestOptions || {};
  1756. const mappedRequestOptions = createMappedRequestOptions(options);
  1757. if (forwardToReplicas) {
  1758. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1759. }
  1760. if (clearExistingRules) {
  1761. mappedRequestOptions.queryParameters.clearExistingRules = 1; // eslint-disable-line functional/immutable-data
  1762. }
  1763. return createWaitablePromise(base.transporter.write({
  1764. method: MethodEnum.Post,
  1765. path: encode('1/indexes/%s/rules/batch', base.indexName),
  1766. data: rules,
  1767. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1768. };
  1769. };
  1770. const saveSynonym = (base) => {
  1771. return (synonym, requestOptions) => {
  1772. return saveSynonyms(base)([synonym], requestOptions);
  1773. };
  1774. };
  1775. const saveSynonyms = (base) => {
  1776. return (synonyms, requestOptions) => {
  1777. const { forwardToReplicas, clearExistingSynonyms, replaceExistingSynonyms, ...options } = requestOptions || {};
  1778. const mappedRequestOptions = createMappedRequestOptions(options);
  1779. if (forwardToReplicas) {
  1780. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1781. }
  1782. if (replaceExistingSynonyms || clearExistingSynonyms) {
  1783. mappedRequestOptions.queryParameters.replaceExistingSynonyms = 1; // eslint-disable-line functional/immutable-data
  1784. }
  1785. return createWaitablePromise(base.transporter.write({
  1786. method: MethodEnum.Post,
  1787. path: encode('1/indexes/%s/synonyms/batch', base.indexName),
  1788. data: synonyms,
  1789. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1790. };
  1791. };
  1792. const search = (base) => {
  1793. return (query, requestOptions) => {
  1794. return base.transporter.read({
  1795. method: MethodEnum.Post,
  1796. path: encode('1/indexes/%s/query', base.indexName),
  1797. data: {
  1798. query,
  1799. },
  1800. cacheable: true,
  1801. }, requestOptions);
  1802. };
  1803. };
  1804. const searchForFacetValues = (base) => {
  1805. return (facetName, facetQuery, requestOptions) => {
  1806. return base.transporter.read({
  1807. method: MethodEnum.Post,
  1808. path: encode('1/indexes/%s/facets/%s/query', base.indexName, facetName),
  1809. data: {
  1810. facetQuery,
  1811. },
  1812. cacheable: true,
  1813. }, requestOptions);
  1814. };
  1815. };
  1816. const searchRules = (base) => {
  1817. return (query, requestOptions) => {
  1818. return base.transporter.read({
  1819. method: MethodEnum.Post,
  1820. path: encode('1/indexes/%s/rules/search', base.indexName),
  1821. data: {
  1822. query,
  1823. },
  1824. }, requestOptions);
  1825. };
  1826. };
  1827. const searchSynonyms = (base) => {
  1828. return (query, requestOptions) => {
  1829. return base.transporter.read({
  1830. method: MethodEnum.Post,
  1831. path: encode('1/indexes/%s/synonyms/search', base.indexName),
  1832. data: {
  1833. query,
  1834. },
  1835. }, requestOptions);
  1836. };
  1837. };
  1838. const setSettings = (base) => {
  1839. return (settings, requestOptions) => {
  1840. const { forwardToReplicas, ...options } = requestOptions || {};
  1841. const mappedRequestOptions = createMappedRequestOptions(options);
  1842. if (forwardToReplicas) {
  1843. mappedRequestOptions.queryParameters.forwardToReplicas = 1; // eslint-disable-line functional/immutable-data
  1844. }
  1845. return createWaitablePromise(base.transporter.write({
  1846. method: MethodEnum.Put,
  1847. path: encode('1/indexes/%s/settings', base.indexName),
  1848. data: settings,
  1849. }, mappedRequestOptions), (response, waitRequestOptions) => waitTask(base)(response.taskID, waitRequestOptions));
  1850. };
  1851. };
  1852. const waitTask = (base) => {
  1853. return (taskID, requestOptions) => {
  1854. return createRetryablePromise(retry => {
  1855. return getTask(base)(taskID, requestOptions).then(response => {
  1856. return response.status !== 'published' ? retry() : undefined;
  1857. });
  1858. });
  1859. };
  1860. };
  1861. const BatchActionEnum = {
  1862. AddObject: 'addObject',
  1863. UpdateObject: 'updateObject',
  1864. PartialUpdateObject: 'partialUpdateObject',
  1865. PartialUpdateObjectNoCreate: 'partialUpdateObjectNoCreate',
  1866. DeleteObject: 'deleteObject',
  1867. DeleteIndex: 'delete',
  1868. ClearIndex: 'clear',
  1869. };
  1870. const ScopeEnum = {
  1871. Settings: 'settings',
  1872. Synonyms: 'synonyms',
  1873. Rules: 'rules',
  1874. };
  1875. const LogLevelEnum = {
  1876. Debug: 1,
  1877. Info: 2,
  1878. Error: 3,
  1879. };
  1880. /* eslint no-console: 0 */
  1881. function createConsoleLogger(logLevel) {
  1882. return {
  1883. debug(message, args) {
  1884. if (LogLevelEnum.Debug >= logLevel) {
  1885. console.debug(message, args);
  1886. }
  1887. return Promise.resolve();
  1888. },
  1889. info(message, args) {
  1890. if (LogLevelEnum.Info >= logLevel) {
  1891. console.info(message, args);
  1892. }
  1893. return Promise.resolve();
  1894. },
  1895. error(message, args) {
  1896. console.error(message, args);
  1897. return Promise.resolve();
  1898. },
  1899. };
  1900. }
  1901. function createBrowserXhrRequester() {
  1902. return {
  1903. send(request) {
  1904. return new Promise((resolve) => {
  1905. const baseRequester = new XMLHttpRequest();
  1906. baseRequester.open(request.method, request.url, true);
  1907. Object.keys(request.headers).forEach(key => baseRequester.setRequestHeader(key, request.headers[key]));
  1908. const createTimeout = (timeout, content) => {
  1909. return setTimeout(() => {
  1910. baseRequester.abort();
  1911. resolve({
  1912. status: 0,
  1913. content,
  1914. isTimedOut: true,
  1915. });
  1916. }, timeout * 1000);
  1917. };
  1918. const connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout');
  1919. // eslint-disable-next-line functional/no-let
  1920. let responseTimeout;
  1921. // eslint-disable-next-line functional/immutable-data
  1922. baseRequester.onreadystatechange = () => {
  1923. if (baseRequester.readyState > baseRequester.OPENED && responseTimeout === undefined) {
  1924. clearTimeout(connectTimeout);
  1925. responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout');
  1926. }
  1927. };
  1928. // eslint-disable-next-line functional/immutable-data
  1929. baseRequester.onerror = () => {
  1930. // istanbul ignore next
  1931. if (baseRequester.status === 0) {
  1932. clearTimeout(connectTimeout);
  1933. clearTimeout(responseTimeout);
  1934. resolve({
  1935. content: baseRequester.responseText || 'Network request failed',
  1936. status: baseRequester.status,
  1937. isTimedOut: false,
  1938. });
  1939. }
  1940. };
  1941. // eslint-disable-next-line functional/immutable-data
  1942. baseRequester.onload = () => {
  1943. clearTimeout(connectTimeout);
  1944. clearTimeout(responseTimeout);
  1945. resolve({
  1946. content: baseRequester.responseText,
  1947. status: baseRequester.status,
  1948. isTimedOut: false,
  1949. });
  1950. };
  1951. baseRequester.send(request.data);
  1952. });
  1953. },
  1954. };
  1955. }
  1956. function algoliasearch(appId, apiKey, options) {
  1957. const commonOptions = {
  1958. appId,
  1959. apiKey,
  1960. timeouts: {
  1961. connect: 1,
  1962. read: 2,
  1963. write: 30,
  1964. },
  1965. requester: createBrowserXhrRequester(),
  1966. logger: createConsoleLogger(LogLevelEnum.Error),
  1967. responsesCache: createInMemoryCache(),
  1968. requestsCache: createInMemoryCache({ serializable: false }),
  1969. hostsCache: createFallbackableCache({
  1970. caches: [
  1971. createBrowserLocalStorageCache({ key: `${version}-${appId}` }),
  1972. createInMemoryCache(),
  1973. ],
  1974. }),
  1975. userAgent: createUserAgent(version).add({ segment: 'Browser' }),
  1976. };
  1977. const searchClientOptions = { ...commonOptions, ...options };
  1978. const initPersonalization = () => (clientOptions) => {
  1979. return createPersonalizationClient({
  1980. ...commonOptions,
  1981. ...clientOptions,
  1982. methods: {
  1983. getPersonalizationStrategy,
  1984. setPersonalizationStrategy,
  1985. },
  1986. });
  1987. };
  1988. return createSearchClient({
  1989. ...searchClientOptions,
  1990. methods: {
  1991. search: multipleQueries,
  1992. searchForFacetValues: multipleSearchForFacetValues,
  1993. multipleBatch,
  1994. multipleGetObjects,
  1995. multipleQueries,
  1996. copyIndex,
  1997. copySettings,
  1998. copySynonyms,
  1999. copyRules,
  2000. moveIndex,
  2001. listIndices,
  2002. getLogs,
  2003. listClusters,
  2004. multipleSearchForFacetValues,
  2005. getApiKey,
  2006. addApiKey,
  2007. listApiKeys,
  2008. updateApiKey,
  2009. deleteApiKey,
  2010. restoreApiKey,
  2011. assignUserID,
  2012. assignUserIDs,
  2013. getUserID,
  2014. searchUserIDs,
  2015. listUserIDs,
  2016. getTopUserIDs,
  2017. removeUserID,
  2018. hasPendingMappings,
  2019. clearDictionaryEntries,
  2020. deleteDictionaryEntries,
  2021. getDictionarySettings,
  2022. getAppTask,
  2023. replaceDictionaryEntries,
  2024. saveDictionaryEntries,
  2025. searchDictionaryEntries,
  2026. setDictionarySettings,
  2027. waitAppTask,
  2028. customRequest,
  2029. initIndex: base => (indexName) => {
  2030. return initIndex(base)(indexName, {
  2031. methods: {
  2032. batch,
  2033. delete: deleteIndex,
  2034. findAnswers,
  2035. getObject,
  2036. getObjects,
  2037. saveObject,
  2038. saveObjects,
  2039. search,
  2040. searchForFacetValues,
  2041. waitTask,
  2042. setSettings,
  2043. getSettings,
  2044. partialUpdateObject,
  2045. partialUpdateObjects,
  2046. deleteObject,
  2047. deleteObjects,
  2048. deleteBy,
  2049. clearObjects,
  2050. browseObjects,
  2051. getObjectPosition,
  2052. findObject,
  2053. exists,
  2054. saveSynonym,
  2055. saveSynonyms,
  2056. getSynonym,
  2057. searchSynonyms,
  2058. browseSynonyms,
  2059. deleteSynonym,
  2060. clearSynonyms,
  2061. replaceAllObjects,
  2062. replaceAllSynonyms,
  2063. searchRules,
  2064. getRule,
  2065. deleteRule,
  2066. saveRule,
  2067. saveRules,
  2068. replaceAllRules,
  2069. browseRules,
  2070. clearRules,
  2071. },
  2072. });
  2073. },
  2074. initAnalytics: () => (clientOptions) => {
  2075. return createAnalyticsClient({
  2076. ...commonOptions,
  2077. ...clientOptions,
  2078. methods: {
  2079. addABTest,
  2080. getABTest,
  2081. getABTests,
  2082. stopABTest,
  2083. deleteABTest,
  2084. },
  2085. });
  2086. },
  2087. initPersonalization,
  2088. initRecommendation: () => (clientOptions) => {
  2089. searchClientOptions.logger.info('The `initRecommendation` method is deprecated. Use `initPersonalization` instead.');
  2090. return initPersonalization()(clientOptions);
  2091. },
  2092. },
  2093. });
  2094. }
  2095. // eslint-disable-next-line functional/immutable-data
  2096. algoliasearch.version = version;
  2097. export default algoliasearch;