@constructor-io/constructorio-node 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@constructor-io/constructorio-node",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Constructor.io Node.js client",
5
5
  "main": "src/constructorio.js",
6
6
  "scripts": {
7
7
  "version": "chmod +x ./scripts/verify-node-version.sh && ./scripts/verify-node-version.sh && npm run docs && git add ./docs/*",
8
8
  "check-lisc": "license-checker --production --onlyAllow 'Apache-2.0;BSD-3-Clause;MIT'",
9
9
  "lint": "eslint 'src/**/*.js' 'spec/**/*.js'",
10
+ "test:parallel": "mkdir -p test && cp -rf src/* test && mocha --parallel ./spec/*",
10
11
  "test": "mkdir -p test && cp -rf src/* test && mocha ./spec/*",
11
12
  "precoverage": "rm -rf ./coverage && rm -rf ./.nyc_output",
12
- "coverage": "nyc --all --reporter=html npm run test",
13
+ "coverage": "nyc --all --reporter=html npm run test:parallel",
13
14
  "postcoverage": "open coverage/index.html && rm -rf test",
14
15
  "docs": "jsdoc --configure ./.jsdoc.json ./README.md --recurse ./src --destination ./docs",
15
16
  "prepare": "husky install"
@@ -8,6 +8,7 @@ const Recommendations = require('./modules/recommendations');
8
8
  const Tracker = require('./modules/tracker');
9
9
  const Catalog = require('./modules/catalog');
10
10
  const Tasks = require('./modules/tasks');
11
+ const Quizzes = require('./modules/quizzes');
11
12
  const { version: packageVersion } = require('../package.json');
12
13
 
13
14
  /**
@@ -30,6 +31,7 @@ class ConstructorIO {
30
31
  * @property {object} tracker - Interface to {@link module:tracker}
31
32
  * @property {object} catalog - Interface to {@link module:catalog}
32
33
  * @property {object} tasks - Interface to {@link module:tasks}
34
+ * @property {object} quizzes - Interface to {@link module:quizzes}
33
35
  * @returns {class}
34
36
  */
35
37
  constructor(options = {}) {
@@ -65,6 +67,7 @@ class ConstructorIO {
65
67
  this.tracker = new Tracker(this.options);
66
68
  this.catalog = new Catalog(this.options);
67
69
  this.tasks = new Tasks(this.options);
70
+ this.quizzes = new Quizzes(this.options);
68
71
  }
69
72
  }
70
73
 
@@ -153,6 +153,8 @@ class Autocomplete {
153
153
  return Promise.reject(e);
154
154
  }
155
155
 
156
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
157
+
156
158
  // Append security token as 'x-cnstrc-token' if available
157
159
  if (this.options.securityToken && typeof this.options.securityToken === 'string') {
158
160
  headers['x-cnstrc-token'] = this.options.securityToken;
@@ -193,9 +193,11 @@ function createBrowseUrlForFacetOptions(facetName, parameters, userParameters, o
193
193
  }
194
194
 
195
195
  // Create request headers using supplied options and user parameters
196
- function createHeaders(options, userParameters) {
196
+ function createHeaders(options, userParameters, networkParameters = {}) {
197
197
  const headers = {};
198
198
 
199
+ Object.assign(headers, helpers.combineCustomHeaders(options, networkParameters));
200
+
199
201
  // Append security token as 'x-cnstrc-token' if available
200
202
  if (options.securityToken && typeof options.securityToken === 'string') {
201
203
  headers['x-cnstrc-token'] = options.securityToken;
@@ -273,7 +275,7 @@ class Browse {
273
275
  const fetch = (this.options && this.options.fetch) || nodeFetch;
274
276
  const controller = new AbortController();
275
277
  const { signal } = controller;
276
- const headers = createHeaders(this.options, userParameters);
278
+ const headers = createHeaders(this.options, userParameters, networkParameters);
277
279
 
278
280
  try {
279
281
  requestUrl = createBrowseUrlFromFilter(filterName, filterValue, parameters, userParameters, this.options);
@@ -357,7 +359,7 @@ class Browse {
357
359
  const fetch = (this.options && this.options.fetch) || nodeFetch;
358
360
  const controller = new AbortController();
359
361
  const { signal } = controller;
360
- const headers = createHeaders(this.options, userParameters);
362
+ const headers = createHeaders(this.options, userParameters, networkParameters);
361
363
 
362
364
  try {
363
365
  requestUrl = createBrowseUrlFromIDs(itemIds, parameters, userParameters, this.options);
@@ -428,7 +430,7 @@ class Browse {
428
430
  const fetch = (this.options && this.options.fetch) || nodeFetch;
429
431
  const controller = new AbortController();
430
432
  const { signal } = controller;
431
- const headers = createHeaders(this.options, userParameters);
433
+ const headers = createHeaders(this.options, userParameters, networkParameters);
432
434
  const { serviceUrl } = this.options;
433
435
  const queryParams = createQueryParams(parameters, userParameters, this.options);
434
436
 
@@ -488,6 +490,7 @@ class Browse {
488
490
  const fetch = (this.options && this.options.fetch) || nodeFetch;
489
491
  const controller = new AbortController();
490
492
  const { signal } = controller;
493
+ const headers = createHeaders(this.options, userParameters, networkParameters);
491
494
 
492
495
  try {
493
496
  requestUrl = createBrowseUrlForFacets(parameters, userParameters, this.options);
@@ -499,7 +502,7 @@ class Browse {
499
502
  helpers.applyNetworkTimeout(this.options, networkParameters, controller);
500
503
 
501
504
  return fetch(requestUrl, {
502
- headers: helpers.createAuthHeader(this.options),
505
+ headers: { ...headers, ...helpers.createAuthHeader(this.options) },
503
506
  signal,
504
507
  }).then((response) => {
505
508
  if (response.ok) {
@@ -542,6 +545,7 @@ class Browse {
542
545
  const fetch = (this.options && this.options.fetch) || nodeFetch;
543
546
  const controller = new AbortController();
544
547
  const { signal } = controller;
548
+ const headers = createHeaders(this.options, userParameters, networkParameters);
545
549
 
546
550
  try {
547
551
  requestUrl = createBrowseUrlForFacetOptions(facetName, parameters, userParameters, this.options);
@@ -553,7 +557,7 @@ class Browse {
553
557
  helpers.applyNetworkTimeout(this.options, networkParameters, controller);
554
558
 
555
559
  return fetch(requestUrl, {
556
- headers: helpers.createAuthHeader(this.options),
560
+ headers: { ...headers, ...helpers.createAuthHeader(this.options) },
557
561
  signal,
558
562
  }).then((response) => {
559
563
  if (response.ok) {
@@ -0,0 +1,240 @@
1
+ /* eslint-disable object-curly-newline, no-underscore-dangle */
2
+ const qs = require('qs');
3
+ const nodeFetch = require('node-fetch').default;
4
+ const helpers = require('../utils/helpers');
5
+
6
+ // Create URL from supplied quizId and parameters
7
+ // eslint-disable-next-line max-params
8
+ function createQuizUrl(quizId, parameters, userParameters, options, path) {
9
+ const {
10
+ apiKey,
11
+ version,
12
+ } = options;
13
+ const {
14
+ sessionId,
15
+ clientId,
16
+ userId,
17
+ segments,
18
+ } = userParameters;
19
+ const serviceUrl = 'https://quizzes.cnstrc.com';
20
+ let queryParams = { c: version };
21
+ let answersParamString = '';
22
+
23
+ queryParams.key = apiKey;
24
+ queryParams.i = clientId;
25
+ queryParams.s = sessionId;
26
+
27
+ // Pull user segments from options
28
+ if (segments && segments.length) {
29
+ queryParams.us = segments;
30
+ }
31
+
32
+ // Pull user id from options
33
+ if (userId) {
34
+ queryParams.ui = userId;
35
+ }
36
+
37
+ // Validate quiz id is provided
38
+ if (!quizId || typeof quizId !== 'string') {
39
+ throw new Error('quizId is a required parameter of type string');
40
+ }
41
+
42
+ if (path === 'finalize' && (typeof parameters.answers !== 'object' || !Array.isArray(parameters.answers) || parameters.answers.length === 0)) {
43
+ throw new Error('answers is a required parameter of type array');
44
+ }
45
+
46
+ if (parameters) {
47
+ const { section, answers, versionId } = parameters;
48
+
49
+ // Pull section from parameters
50
+ if (section) {
51
+ queryParams.section = section;
52
+ }
53
+
54
+ // Pull version_id from parameters
55
+ if (versionId) {
56
+ queryParams.version_id = versionId;
57
+ }
58
+
59
+ // Pull answers from parameters and transform
60
+ if (answers) {
61
+ answers.forEach((ans) => {
62
+ answersParamString += `&${qs.stringify({ a: ans }, { arrayFormat: 'comma' })}`;
63
+ });
64
+ }
65
+ }
66
+
67
+ queryParams._dt = Date.now();
68
+ queryParams = helpers.cleanParams(queryParams);
69
+
70
+ const queryString = qs.stringify(queryParams, { indices: false });
71
+
72
+ return `${serviceUrl}/v1/quizzes/${encodeURIComponent(quizId)}/${encodeURIComponent(path)}/?${queryString}${answersParamString}`;
73
+ }
74
+
75
+ /**
76
+ * Interface to quiz related API calls
77
+ *
78
+ * @module quizzes
79
+ * @inner
80
+ * @returns {object}
81
+ */
82
+ class Quizzes {
83
+ constructor(options) {
84
+ this.options = options || {};
85
+ }
86
+
87
+ /**
88
+ * Retrieve quiz question from API
89
+ *
90
+ * @function getQuizNextQuestion
91
+ * @description Retrieve quiz question from Constructor.io API
92
+ * @param {string} id - The identifier of the quiz
93
+ * @param {string} [parameters] - Additional parameters to refine result set
94
+ * @param {string} [parameters.section] - Product catalog section
95
+ * @param {array} [parameters.answers] - An array for answers in the format [[1,2],[1]]
96
+ * @param {string} [parameters.versionId] - Version identifier for the quiz.
97
+ * @param {object} [userParameters] - Parameters relevant to the user request
98
+ * @param {number} [userParameters.sessionId] - Session ID, utilized to personalize results
99
+ * @param {number} [userParameters.clientId] - Client ID, utilized to personalize results
100
+ * @param {string} [userParameters.userId] - User ID, utilized to personalize results
101
+ * @param {string} [userParameters.segments] - User segments
102
+ * @param {string} [userParameters.userIp] - Origin user IP, from client
103
+ * @param {string} [userParameters.userAgent] - Origin user agent, from client
104
+ * @param {object} [networkParameters] - Parameters relevant to the network request
105
+ * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
106
+ * @returns {Promise}
107
+ * @see https://docs.constructor.io/rest_api/quiz/using_quizzes/#answering-a-quiz
108
+ * @example
109
+ * constructorio.quizzes.getQuizNextQuestion('quizId', {
110
+ * answers: [[1,2],[1]],
111
+ * section: '123',
112
+ * versionId: '123'
113
+ * });
114
+ */
115
+ getQuizNextQuestion(quizId, parameters, userParameters = {}, networkParameters = {}) {
116
+ const headers = {};
117
+ let requestUrl;
118
+ const fetch = (this.options && this.options.fetch) || nodeFetch;
119
+ const controller = new AbortController();
120
+ const { signal } = controller;
121
+
122
+ try {
123
+ requestUrl = createQuizUrl(quizId, parameters, userParameters, this.options, 'next');
124
+ } catch (e) {
125
+ return Promise.reject(e);
126
+ }
127
+
128
+ // Append security token as 'x-cnstrc-token' if available
129
+ if (this.options.securityToken && typeof this.options.securityToken === 'string') {
130
+ headers['x-cnstrc-token'] = this.options.securityToken;
131
+ }
132
+
133
+ // Append user IP as 'X-Forwarded-For' if available
134
+ if (userParameters.userIp && typeof userParameters.userIp === 'string') {
135
+ headers['X-Forwarded-For'] = userParameters.userIp;
136
+ }
137
+
138
+ // Append user agent as 'User-Agent' if available
139
+ if (userParameters.userAgent && typeof userParameters.userAgent === 'string') {
140
+ headers['User-Agent'] = userParameters.userAgent;
141
+ }
142
+
143
+ // Handle network timeout if specified
144
+ helpers.applyNetworkTimeout(this.options, networkParameters, controller);
145
+
146
+ return fetch(requestUrl, { headers, signal })
147
+ .then((response) => {
148
+ if (response.ok) {
149
+ return response.json();
150
+ }
151
+
152
+ return helpers.throwHttpErrorFromResponse(new Error(), response);
153
+ })
154
+ .then((json) => {
155
+ if (json.version_id) {
156
+ return json;
157
+ }
158
+
159
+ throw new Error('getQuizNextQuestion response data is malformed');
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Retrieves filter expression and recommendation URL from given answers
165
+ *
166
+ * @function getQuizResults
167
+ * @description Retrieve quiz recommendation and filter expression from Constructor.io API
168
+ * @param {string} id - The identifier of the quiz
169
+ * @param {string} [parameters] - Additional parameters to refine result set
170
+ * @param {string} [parameters.section] - Product catalog section
171
+ * @param {array} [parameters.answers] - An array of answers in the format [[1,2],[1]]
172
+ * @param {string} [parameters.versionId] - Specific version identifier for the quiz
173
+ * @param {object} [userParameters] - Parameters relevant to the user request
174
+ * @param {number} [userParameters.sessionId] - Session ID, utilized to personalize results
175
+ * @param {number} [userParameters.clientId] - Client ID, utilized to personalize results
176
+ * @param {string} [userParameters.userId] - User ID, utilized to personalize results
177
+ * @param {string} [userParameters.segments] - User segments
178
+ * @param {string} [userParameters.userIp] - Origin user IP, from client
179
+ * @param {string} [userParameters.userAgent] - Origin user agent, from client
180
+ * @param {object} [networkParameters] - Parameters relevant to the network request
181
+ * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
182
+ * @returns {Promise}
183
+ * @see https://docs.constructor.io/rest_api/quiz/using_quizzes/#completing-the-quiz
184
+ * @example
185
+ * constructorio.quizzes.getQuizResults('quizId', {
186
+ * answers: [[1,2],[1]],
187
+ * section: '123',
188
+ * versionId: '123'
189
+ * });
190
+ */
191
+ getQuizResults(quizId, parameters, userParameters = {}, networkParameters = {}) {
192
+ let requestUrl;
193
+ const headers = {};
194
+ const fetch = (this.options && this.options.fetch) || nodeFetch;
195
+ const controller = new AbortController();
196
+ const { signal } = controller;
197
+
198
+ try {
199
+ requestUrl = createQuizUrl(quizId, parameters, userParameters, this.options, 'finalize');
200
+ } catch (e) {
201
+ return Promise.reject(e);
202
+ }
203
+
204
+ // Append security token as 'x-cnstrc-token' if available
205
+ if (this.options.securityToken && typeof this.options.securityToken === 'string') {
206
+ headers['x-cnstrc-token'] = this.options.securityToken;
207
+ }
208
+
209
+ // Append user IP as 'X-Forwarded-For' if available
210
+ if (userParameters.userIp && typeof userParameters.userIp === 'string') {
211
+ headers['X-Forwarded-For'] = userParameters.userIp;
212
+ }
213
+
214
+ // Append user agent as 'User-Agent' if available
215
+ if (userParameters.userAgent && typeof userParameters.userAgent === 'string') {
216
+ headers['User-Agent'] = userParameters.userAgent;
217
+ }
218
+
219
+ // Handle network timeout if specified
220
+ helpers.applyNetworkTimeout(this.options, networkParameters, controller);
221
+
222
+ return fetch(requestUrl, { headers, signal })
223
+ .then((response) => {
224
+ if (response.ok) {
225
+ return response.json();
226
+ }
227
+
228
+ return helpers.throwHttpErrorFromResponse(new Error(), response);
229
+ })
230
+ .then((json) => {
231
+ if (json.version_id) {
232
+ return json;
233
+ }
234
+
235
+ throw new Error('getQuizResults response data is malformed');
236
+ });
237
+ }
238
+ }
239
+
240
+ module.exports = Quizzes;
@@ -142,6 +142,8 @@ class Recommendations {
142
142
  return Promise.reject(e);
143
143
  }
144
144
 
145
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
146
+
145
147
  // Append security token as 'x-cnstrc-token' if available
146
148
  if (this.options.securityToken && typeof this.options.securityToken === 'string') {
147
149
  headers['x-cnstrc-token'] = this.options.securityToken;
@@ -209,6 +211,8 @@ class Recommendations {
209
211
  const headers = {};
210
212
  const requestUrl = `${serviceUrl}/v1/recommendation_pods?key=${apiKey}`;
211
213
 
214
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
215
+
212
216
  // Append security token as 'x-cnstrc-token' if available
213
217
  if (this.options.securityToken && typeof this.options.securityToken === 'string') {
214
218
  headers['x-cnstrc-token'] = this.options.securityToken;
@@ -202,6 +202,8 @@ class Search {
202
202
  return Promise.reject(e);
203
203
  }
204
204
 
205
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
206
+
205
207
  // Append security token as 'x-cnstrc-token' if available
206
208
  if (this.options.securityToken && typeof this.options.securityToken === 'string') {
207
209
  headers['x-cnstrc-token'] = this.options.securityToken;
@@ -61,6 +61,9 @@ class Tasks {
61
61
  const fetch = (this.options && this.options.fetch) || nodeFetch;
62
62
  const controller = new AbortController();
63
63
  const { signal } = controller;
64
+ const headers = {
65
+ 'Content-Type': 'application/json',
66
+ };
64
67
 
65
68
  if (parameters) {
66
69
  const { num_results_per_page: numResultsPerPageOld, numResultsPerPage, page, startDate, endDate, status, type } = parameters;
@@ -98,12 +101,14 @@ class Tasks {
98
101
  return Promise.reject(e);
99
102
  }
100
103
 
104
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
105
+
101
106
  helpers.applyNetworkTimeout(this.options, networkParameters, controller);
102
107
 
103
108
  return fetch(requestUrl, {
104
109
  method: 'GET',
105
110
  headers: {
106
- 'Content-Type': 'application/json',
111
+ ...headers,
107
112
  ...helpers.createAuthHeader(this.options),
108
113
  },
109
114
  signal,
@@ -132,6 +137,9 @@ class Tasks {
132
137
  const fetch = (this.options && this.options.fetch) || nodeFetch;
133
138
  const controller = new AbortController();
134
139
  const { signal } = controller;
140
+ const headers = {
141
+ 'Content-Type': 'application/json',
142
+ };
135
143
 
136
144
  try {
137
145
  requestUrl = createTaskUrl(`tasks/${parameters.id}`, this.options);
@@ -139,12 +147,14 @@ class Tasks {
139
147
  return Promise.reject(e);
140
148
  }
141
149
 
150
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
151
+
142
152
  helpers.applyNetworkTimeout(this.options, networkParameters, controller);
143
153
 
144
154
  return fetch(requestUrl, {
145
155
  method: 'GET',
146
156
  headers: {
147
- 'Content-Type': 'application/json',
157
+ ...headers,
148
158
  ...helpers.createAuthHeader(this.options),
149
159
  },
150
160
  signal,
@@ -84,6 +84,8 @@ function send(url, userParameters, networkParameters, method = 'GET', body = {})
84
84
  const { signal } = controller;
85
85
  const headers = {};
86
86
 
87
+ Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters));
88
+
87
89
  // Append security token as 'x-cnstrc-token' if available
88
90
  if (this.options.securityToken && typeof this.options.securityToken === 'string') {
89
91
  headers['x-cnstrc-token'] = this.options.securityToken;
@@ -43,16 +43,25 @@ const utils = {
43
43
  },
44
44
 
45
45
  // Abort network request based on supplied timeout interval (in milliseconds)
46
- // - method call parameter takes precedence over global options parameter
46
+ // - Method call parameter takes precedence over global options parameter
47
47
  applyNetworkTimeout: (options = {}, networkParameters = {}, controller = undefined) => {
48
48
  const optionsTimeout = options && options.networkParameters && options.networkParameters.timeout;
49
49
  const networkParametersTimeout = networkParameters && networkParameters.timeout;
50
- const timeout = optionsTimeout || networkParametersTimeout;
50
+ const timeout = networkParametersTimeout || optionsTimeout;
51
51
 
52
52
  if (typeof timeout === 'number') {
53
53
  setTimeout(() => controller.abort(), timeout);
54
54
  }
55
55
  },
56
+
57
+ // Combine headers from options and networkParameters
58
+ // - Method call parameter takes precedence over global options parameter
59
+ combineCustomHeaders: (options = {}, networkParameters = {}) => {
60
+ const optionsHeaders = options && options.networkParameters && options.networkParameters.headers;
61
+ const networkParametersHeaders = networkParameters && networkParameters.headers;
62
+
63
+ return { ...optionsHeaders, ...networkParametersHeaders };
64
+ },
56
65
  };
57
66
 
58
67
  module.exports = utils;