modules/quizzes.js

/* eslint-disable object-curly-newline, no-underscore-dangle */
/* eslint-disable max-len */
/* eslint-disable complexity */
const EventDispatcher = require('../utils/event-dispatcher');
const helpers = require('../utils/helpers');

// Create URL from supplied quizId and parameters
function createQuizUrl(quizId, parameters, options, path) {
  const {
    apiKey,
    clientId,
    sessionId,
    segments,
    userId,
    version,
    quizzesServiceUrl,
  } = options;
  let queryParams = { c: version };
  let answersParamString = '';

  queryParams.key = apiKey;
  queryParams.i = clientId;
  queryParams.s = sessionId;

  // Pull user segments from options
  if (segments && segments.length) {
    queryParams.us = segments;
  }

  // Pull user id from options and ensure string
  if (userId) {
    queryParams.ui = String(userId);
  }

  // Validate quiz id is provided
  if (!quizId || typeof quizId !== 'string') {
    throw new Error('quizId is a required parameter of type string');
  }

  if (path === 'results' && (typeof parameters.answers !== 'object' || !Array.isArray(parameters.answers) || parameters.answers.length === 0)) {
    throw new Error('answers is a required parameter of type array');
  }

  if (parameters) {
    const { section, answers, quizSessionId, quizVersionId, page, resultsPerPage, filters, fmtOptions, hiddenFields } = parameters;

    // Pull section from parameters
    if (section) {
      queryParams.section = section;
    }

    // Pull quiz_version_id from parameters
    if (quizVersionId) {
      queryParams.quiz_version_id = quizVersionId;
    }

    // Pull quiz_session_id from parameters
    if (quizSessionId) {
      queryParams.quiz_session_id = quizSessionId;
    }

    // Pull a (answers) from parameters and transform
    if (answers && answers.length) {
      answersParamString = `&${helpers.stringify({ a: answers.map((ans) => [...ans].join(',')) })}`;
    }

    // Pull page from parameters
    if (!helpers.isNil(page)) {
      queryParams.page = page;
    }

    // Pull results per page from parameters
    if (!helpers.isNil(resultsPerPage)) {
      queryParams.num_results_per_page = resultsPerPage;
    }

    if (filters) {
      queryParams.filters = filters;
    }

    if (fmtOptions) {
      queryParams.fmt_options = fmtOptions;
    }

    if (hiddenFields) {
      if (queryParams.fmt_options) {
        queryParams.fmt_options.hidden_fields = hiddenFields;
      } else {
        queryParams.fmt_options = { hidden_fields: hiddenFields };
      }
    }
  }

  queryParams._dt = Date.now();
  queryParams = helpers.cleanParams(queryParams);

  const queryString = helpers.stringify(queryParams);

  return `${quizzesServiceUrl}/v1/quizzes/${encodeURIComponent(quizId)}/${encodeURIComponent(path)}/?${queryString}${answersParamString}`;
}

/**
 * Interface to quiz related API calls
 *
 * @module quizzes
 * @inner
 * @returns {object}
 */
class Quizzes {
  constructor(options) {
    this.options = options || {};
    this.eventDispatcher = new EventDispatcher(options.eventDispatcher);
  }

  /**
   * Retrieve next question from API
   *
   * @function getQuizNextQuestion
   * @description Retrieve next question from Constructor.io API
   * @param {string} quizId - The identifier of the quiz
   * @param {string} [parameters] - Additional parameters to refine result set
   * @param {string} [parameters.section] - Product catalog section
   * @param {array} [parameters.answers] - An array of answers in the format [[1,2], [1], ["true"], ["seen"], [""]]. Based on the question type, answers should either be an integer, "true"/"false", "seen" or an empty string ("") if skipped
   * @param {string} [parameters.quizVersionId] - Version identifier for the quiz. Version ID will be returned with the first request and it should be passed with subsequent requests. More information can be found: https://docs.constructor.com/reference/configuration-quizzes
   * @param {string} [parameters.quizSessionId] - Session identifier for the quiz. Session ID will be returned with the first request and it should be passed with subsequent requests. More information can be found: https://docs.constructor.com/reference/configuration-quizzes
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {Promise}
   * @see https://docs.constructor.com/reference/v1-quizzes-get-quiz-results
   * @example
   * constructorio.quizzes.getQuizNextQuestion('quizId', {
   *    answers: [[1,2],[1]],
   *    section: '123',
   *    quizVersionId: '123',
   *    quizSessionId: '1234',
   * });
   */
  getQuizNextQuestion(quizId, parameters, networkParameters = {}) {
    let requestUrl;
    const { fetch } = this.options;
    const controller = new AbortController();
    const { signal } = controller;

    try {
      requestUrl = createQuizUrl(quizId, parameters, this.options, 'next');
    } catch (e) {
      return Promise.reject(e);
    }

    // Handle network timeout if specified
    helpers.applyNetworkTimeout(this.options, networkParameters, controller);

    return fetch(requestUrl, { signal })
      .then(helpers.convertResponseToJson)
      .then((json) => {
        if (json.quiz_version_id) {
          this.eventDispatcher.queue('quizzes.getQuizNextQuestion.completed', json);

          return json;
        }

        throw new Error('getQuizNextQuestion response data is malformed');
      });
  }

  /**
   * Retrieves filter expression and recommendation URL from given answers
   *
   * @function getQuizResults
   * @description Retrieve quiz recommendation and filter expression from Constructor.io API
   * @param {string} quizId - The identifier of the quiz
   * @param {string} parameters - Additional parameters to refine result set
   * @param {array} parameters.answers - An array of answers in the format [[1,2], [1], ["true"], ["seen"], [""]]. Based on the question type, answers should either be an integer, "true"/"false", "seen" or an empty string ("") if skipped
   * @param {string} [parameters.section] - Product catalog section
   * @param {string} [parameters.quizVersionId] - Version identifier for the quiz. Version ID will be returned with the first request and it should be passed with subsequent requests. More information can be found: https://docs.constructor.com/reference/configuration-quizzes
   * @param {string} [parameters.quizSessionId] - Session identifier for the quiz. Session ID will be returned with the first request and it should be passed with subsequent requests. More information can be found: https://docs.constructor.com/reference/configuration-quizzes
   * @param {number} [parameters.page] - The page number of the results
   * @param {number} [parameters.resultsPerPage] - The number of results per page to return
   * @param {object} [parameters.filters] - Key / value mapping (dictionary) of filters used to refine results
   * @param {object} [parameters.fmtOptions] - Key / value mapping (dictionary) of options used for result formatting
   * @param {string[]} [parameters.hiddenFields] - Hidden metadata fields to return
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {Promise}
   * @see https://docs.constructor.com/reference/v1-quizzes-get-quiz-results
   * @example
   * constructorio.quizzes.getQuizResults('quizId', {
   *    answers: [[1,2],[1]],
   *    section: '123',
   *    quizVersionId: '123',
   *    quizSessionId: '234'
   * });
   */
  getQuizResults(quizId, parameters, networkParameters = {}) {
    let requestUrl;
    const { fetch } = this.options;
    const controller = new AbortController();
    const { signal } = controller;

    try {
      requestUrl = createQuizUrl(quizId, parameters, this.options, 'results');
    } catch (e) {
      return Promise.reject(e);
    }

    // Handle network timeout if specified
    helpers.applyNetworkTimeout(this.options, networkParameters, controller);

    return fetch(requestUrl, { signal })
      .then(helpers.convertResponseToJson)
      .then((json) => {
        if (json.quiz_version_id) {
          this.eventDispatcher.queue('quizzes.getQuizResults.completed', json);

          return json;
        }

        throw new Error('getQuizResults response data is malformed');
      });
  }

  /**
   * Retrieves configuration for the results page of a particular quiz
   *
   * @function getQuizResultsConfig
   * @description Retrieve quiz results page configuration from Constructor.io API
   * @param {string} quizId - The identifier of the quiz
   * @param {string} parameters - Additional parameters
   * @param {string} [parameters.section] - Product catalog section
   * @param {string} [parameters.quizVersionId] - Version identifier for the quiz. Version ID will be returned with the first request and it should be passed with subsequent requests. More information can be found: https://docs.constructor.com/reference/configuration-quizzes
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {Promise}
   * @example
   * constructorio.quizzes.getQuizResultsConfig('quizId', {
   *    quizVersionId: '123',
   * });
   */
  getQuizResultsConfig(quizId, parameters, networkParameters = {}) {
    let requestUrl;
    const { fetch } = this.options;
    const controller = new AbortController();
    const { signal } = controller;

    try {
      requestUrl = createQuizUrl(quizId, parameters, this.options, 'results_config');
    } catch (e) {
      return Promise.reject(e);
    }

    // Handle network timeout if specified
    helpers.applyNetworkTimeout(this.options, networkParameters, controller);

    return fetch(requestUrl, { signal })
      .then(helpers.convertResponseToJson)
      .then((json) => {
        if (json.quiz_version_id) {
          this.eventDispatcher.queue('quizzes.getQuizResultsConfig.completed', json);

          return json;
        }

        throw new Error('getQuizResultsConfig response data is malformed');
      });
  }
}

module.exports = Quizzes;