modules/autocomplete.js

  1. /* eslint-disable object-curly-newline, no-underscore-dangle */
  2. const EventDispatcher = require('../utils/event-dispatcher');
  3. const { convertResponseToJson, cleanParams, applyNetworkTimeout, trimNonBreakingSpaces, encodeURIComponentRFC3986, stringify } = require('../utils/helpers');
  4. // Create URL from supplied query (term) and parameters
  5. function createAutocompleteUrl(query, parameters, options) {
  6. const {
  7. apiKey,
  8. version,
  9. serviceUrl,
  10. sessionId,
  11. clientId,
  12. userId,
  13. segments,
  14. testCells,
  15. } = options;
  16. let queryParams = { c: version };
  17. queryParams.key = apiKey;
  18. queryParams.i = clientId;
  19. queryParams.s = sessionId;
  20. // Validate query (term) is provided
  21. if (!query || typeof query !== 'string') {
  22. throw new Error('query is a required parameter of type string');
  23. }
  24. // Pull test cells from options
  25. if (testCells) {
  26. Object.keys(testCells).forEach((testCellKey) => {
  27. queryParams[`ef-${testCellKey}`] = testCells[testCellKey];
  28. });
  29. }
  30. // Pull user segments from options
  31. if (segments && segments.length) {
  32. queryParams.us = segments;
  33. }
  34. // Pull user id from options and ensure string
  35. if (userId) {
  36. queryParams.ui = String(userId);
  37. }
  38. if (parameters) {
  39. const {
  40. numResults,
  41. resultsPerSection,
  42. filters,
  43. filtersPerSection,
  44. hiddenFields,
  45. variationsMap,
  46. preFilterExpression,
  47. qsParam,
  48. fmtOptions,
  49. } = parameters;
  50. // Pull results number from parameters
  51. if (numResults) {
  52. queryParams.num_results = numResults;
  53. }
  54. // Pull results number per section from parameters
  55. if (resultsPerSection) {
  56. Object.keys(resultsPerSection).forEach((section) => {
  57. queryParams[`num_results_${section}`] = resultsPerSection[section];
  58. });
  59. }
  60. // Pull filters from parameters
  61. if (filters) {
  62. queryParams.filters = filters;
  63. }
  64. // Pull filtersPerSection from parameters
  65. if (filtersPerSection) {
  66. Object.keys(filtersPerSection).forEach((section) => {
  67. queryParams[`filters[${section}]`] = filtersPerSection[section];
  68. });
  69. }
  70. // Pull filter expression from parameters
  71. if (preFilterExpression) {
  72. queryParams.pre_filter_expression = JSON.stringify(preFilterExpression);
  73. }
  74. // Pull format options from parameters
  75. if (fmtOptions) {
  76. queryParams.fmt_options = fmtOptions;
  77. }
  78. // Pull hidden fields from parameters
  79. if (hiddenFields) {
  80. if (queryParams.fmt_options) {
  81. queryParams.fmt_options.hidden_fields = hiddenFields;
  82. } else {
  83. queryParams.fmt_options = { hidden_fields: hiddenFields };
  84. }
  85. }
  86. // Pull variations map from parameters
  87. if (variationsMap) {
  88. queryParams.variations_map = JSON.stringify(variationsMap);
  89. }
  90. // pull qs param from parameters
  91. if (qsParam) {
  92. queryParams.qs = JSON.stringify(qsParam);
  93. }
  94. }
  95. queryParams._dt = Date.now();
  96. queryParams = cleanParams(queryParams);
  97. const queryString = stringify(queryParams);
  98. const cleanedQuery = query.replace(/^\//, '|'); // For compatibility with backend API
  99. return `${serviceUrl}/autocomplete/${encodeURIComponentRFC3986(trimNonBreakingSpaces(cleanedQuery))}?${queryString}`;
  100. }
  101. /**
  102. * Interface to autocomplete related API calls.
  103. *
  104. * @module autocomplete
  105. * @inner
  106. * @returns {object}
  107. */
  108. class Autocomplete {
  109. constructor(options) {
  110. this.options = options || {};
  111. this.eventDispatcher = new EventDispatcher(options.eventDispatcher);
  112. }
  113. /**
  114. * Retrieve autocomplete results from API
  115. *
  116. * @function getAutocompleteResults
  117. * @description Retrieve autocomplete results from Constructor.io API
  118. * @param {string} query - Term to use to perform an autocomplete search
  119. * @param {object} [parameters] - Additional parameters to refine result set
  120. * @param {number} [parameters.numResults] - The total number of results to return
  121. * @param {object} [parameters.filters] - Key / value mapping (dictionary) of filters used to refine results
  122. * @param {object} [parameters.filtersPerSection] - Filters used to refine results per section
  123. * @param {object} [parameters.resultsPerSection] - Number of results to return (value) per section (key)
  124. * @param {object} [parameters.fmtOptions] - An object containing options to format different aspects of the response. Please refer to https://docs.constructor.com/reference/v1-autocomplete-get-autocomplete-results for details
  125. * @param {object} [parameters.preFilterExpression] - Faceting expression to scope autocomplete results. Please refer to https://docs.constructor.com/reference/configuration-collections for details
  126. * @param {string[]} [parameters.hiddenFields] - Hidden metadata fields to return
  127. * @param {object} [parameters.variationsMap] - The variations map object to aggregate variations. Please refer to https://docs.constructor.com/reference/shared-variations-mapping for details
  128. * @param {object} [parameters.qsParam] - object of additional query parameters to be appended to requests for results
  129. * @param {object} [networkParameters] - Parameters relevant to the network request
  130. * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
  131. * @returns {Promise}
  132. * @see https://docs.constructor.com/reference/autocomplete-autocomplete-results
  133. * @example
  134. * constructorio.autocomplete.getAutocompleteResults('t-shirt', {
  135. * resultsPerSection: {
  136. * Products: 5,
  137. * 'Search Suggestions': 10,
  138. * },
  139. * filters: {
  140. * size: 'medium'
  141. * },
  142. * });
  143. */
  144. getAutocompleteResults(query, parameters, networkParameters = {}) {
  145. let requestUrl;
  146. const { fetch } = this.options;
  147. let signal;
  148. if (typeof AbortController === 'function') {
  149. const controller = new AbortController();
  150. signal = controller && controller.signal;
  151. // Handle network timeout if specified
  152. applyNetworkTimeout(this.options, networkParameters, controller);
  153. }
  154. try {
  155. requestUrl = createAutocompleteUrl(query, parameters, this.options);
  156. } catch (e) {
  157. return Promise.reject(e);
  158. }
  159. return fetch(requestUrl, { signal })
  160. .then(convertResponseToJson)
  161. .then((json) => {
  162. if (json.sections) {
  163. if (json.result_id) {
  164. const sectionKeys = Object.keys(json.sections);
  165. sectionKeys.forEach((section) => {
  166. const sectionItems = json.sections[section];
  167. if (sectionItems.length) {
  168. // Append `result_id` to each section item
  169. sectionItems.forEach((item) => {
  170. // eslint-disable-next-line no-param-reassign
  171. item.result_id = json.result_id;
  172. });
  173. }
  174. });
  175. }
  176. this.eventDispatcher.queue('autocomplete.getAutocompleteResults.completed', json);
  177. return json;
  178. }
  179. throw new Error('getAutocompleteResults response data is malformed');
  180. });
  181. }
  182. }
  183. module.exports = Autocomplete;