sfmc-sdk 0.6.3 → 0.8.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/.eslintrc.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "mocha": true
6
6
  },
7
7
  "extends": [
8
+ "plugin:unicorn/recommended",
8
9
  "eslint:recommended",
9
10
  "plugin:mocha/recommended",
10
11
  "plugin:jsdoc/recommended",
@@ -18,5 +19,42 @@
18
19
  "parserOptions": {
19
20
  "ecmaVersion": 2020,
20
21
  "sourceType": "module"
22
+ },
23
+ "settings": {
24
+ "jsdoc": {
25
+ "mode": "jsdoc",
26
+ "preferredTypes": {
27
+ "array": "Array",
28
+ "array.<>": "[]",
29
+ "Array.<>": "[]",
30
+ "array<>": "[]",
31
+ "Array<>": "[]",
32
+ "Object": "object",
33
+ "object.<>": "Object.<>",
34
+ "object<>": "Object.<>",
35
+ "Object<>": "Object.<>",
36
+ "promise": "Promise",
37
+ "promise.<>": "Promise.<>",
38
+ "promise<>": "Promise.<>",
39
+ "Promise<>": "Promise.<>"
40
+ }
41
+ }
42
+ },
43
+ "rules": {
44
+ "unicorn/prefer-module": "off",
45
+ "unicorn/numeric-separators-style": "off",
46
+ "jsdoc/check-line-alignment": 2,
47
+ "jsdoc/require-jsdoc": [
48
+ "warn",
49
+ {
50
+ "require": {
51
+ "FunctionDeclaration": true,
52
+ "MethodDefinition": true,
53
+ "ClassDeclaration": true,
54
+ "ArrowFunctionExpression": false,
55
+ "FunctionExpression": true
56
+ }
57
+ }
58
+ ]
21
59
  }
22
60
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "useTabs": false,
3
+ "tabWidth": 2,
4
+ "printWidth": 40,
5
+ "trailingComma": "none"
6
+ }
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "name": "Create PR to develop branch",
4
+ "target": "ref",
5
+ "refTargets": [
6
+ "localbranch",
7
+ "remotebranch"
8
+ ],
9
+ "action": {
10
+ "type": "url",
11
+ "url": "https://github.com/DougMidgley/SFMC-SDK/compare/develop...$shortname?expand=1"
12
+ }
13
+ },
14
+ {
15
+ "name": "Create PR to hotfix branch",
16
+ "target": "ref",
17
+ "refTargets": [
18
+ "localbranch",
19
+ "remotebranch"
20
+ ],
21
+ "action": {
22
+ "type": "url",
23
+ "url": "https://github.com/DougMidgley/SFMC-SDK/compare/hotfix...$shortname?expand=1"
24
+ }
25
+ }
26
+ ]
package/.gitattributes CHANGED
@@ -1,5 +1,2 @@
1
1
  # Set the default behavior, in case people don't have core.autocrlf set.
2
- * text=auto eol=lf
3
-
4
- # Declare files that will always have LF line endings on checkout.
5
- *.ssjs text eol=lf
2
+ * text=auto eol=lf
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ . "$(dirname "$0")/_/husky.sh"
3
+ INPUT_FILE=$1
4
+ START_LINE=`head -n1 $INPUT_FILE`
5
+ PATTERN="^(#[[:digit:]]|Merge)"
6
+
7
+ if ! [[ "$START_LINE" =~ $PATTERN ]] ; then
8
+ echo "Bad commit message, see example: \"#431 commit message\", you provided: \"$START_LINE\""
9
+ exit 1
10
+ fi
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ git config commit.template .git/templatemessage
3
+ TICKETID=`git rev-parse --abbrev-ref HEAD | LC_ALL=en_US.utf8 grep -oP '((feature|bug|bugfix|fix|hotfix|task|chore)\/)\K\d{1,7}'`
4
+ echo "[POST_CHECKOUT] Setting template commit to $TICKETID"
5
+ echo "#$TICKETID: " > ".git/templatemessage"
package/lib/auth.js CHANGED
@@ -1,5 +1,4 @@
1
1
  'use strict';
2
-
3
2
  const axios = require('axios');
4
3
  const { isConnectionError, RestError } = require('./util');
5
4
  const AVAIALABLE_SCOPES = [
@@ -99,7 +98,7 @@ module.exports = class Auth {
99
98
  * @param {string} authObject.client_secret Client Secret from SFMC config
100
99
  * @param {number} authObject.account_id MID of Business Unit used for API Calls
101
100
  * @param {string} authObject.auth_url Auth URL from SFMC config
102
- * @param {Array<string>} [authObject.scope] Array of scopes used for requests
101
+ * @param {string[]} [authObject.scope] Array of scopes used for requests
103
102
  * @param {object} options options for the SDK as a whole, for example collection of handler functions, or retry settings
104
103
  */
105
104
  constructor(authObject, options) {
@@ -108,23 +107,23 @@ module.exports = class Auth {
108
107
  } else if (!authObject.client_id) {
109
108
  throw new Error('client_id or client_secret is missing or invalid');
110
109
  } else if (typeof authObject.client_id !== 'string') {
111
- throw new Error('client_id or client_secret must be strings');
110
+ throw new TypeError('client_id or client_secret must be strings');
112
111
  } else if (!authObject.client_secret) {
113
112
  throw new Error('client_id or client_secret is missing or invalid');
114
113
  } else if (typeof authObject.client_secret !== 'string') {
115
- throw new Error('client_id or client_secret must be strings');
114
+ throw new TypeError('client_id or client_secret must be strings');
116
115
  } else if (!authObject.account_id) {
117
116
  throw new Error('account_id is missing or invalid');
118
117
  } else if (!Number.isInteger(Number.parseInt(authObject.account_id))) {
119
- throw new Error(
118
+ throw new TypeError(
120
119
  'account_id must be an Integer (Integers in String format are accepted)'
121
120
  );
122
121
  } else if (!authObject.auth_url) {
123
122
  throw new Error('auth_url is missing or invalid');
124
123
  } else if (typeof authObject.auth_url !== 'string') {
125
- throw new Error('auth_url must be a string');
124
+ throw new TypeError('auth_url must be a string');
126
125
  } else if (
127
- !/https:\/\/[a-z0-9-]{28}\.auth\.marketingcloudapis\.com\//gm.test(authObject.auth_url)
126
+ !/https:\/\/[\da-z-]{28}\.auth\.marketingcloudapis\.com\//gm.test(authObject.auth_url)
128
127
  ) {
129
128
  throw new Error(
130
129
  'auth_url must be in format https://mcXXXXXXXXXXXXXXXXXXXXXXXXXX.auth.marketingcloudapis.com/'
@@ -134,7 +133,7 @@ module.exports = class Auth {
134
133
  } else if (authObject.scope && getInvalidScopes(authObject.scope).length > 0) {
135
134
  throw new Error(
136
135
  getInvalidScopes(authObject.scope)
137
- .map((val) => '"' + val + '"')
136
+ .map((value) => '"' + value + '"')
138
137
  .join(',') + ' is/are invalid scope(s)'
139
138
  );
140
139
  }
@@ -146,7 +145,7 @@ module.exports = class Auth {
146
145
  *
147
146
  * @param {boolean} forceRefresh used to enforce a refresh of token
148
147
  * @param {number} remainingAttempts number of retries in case of issues
149
- * @returns {Promise<object>} current session information
148
+ * @returns {Promise.<object>} current session information
150
149
  */
151
150
  async getAccessToken(forceRefresh, remainingAttempts) {
152
151
  if (remainingAttempts === undefined) {
@@ -156,19 +155,19 @@ module.exports = class Auth {
156
155
  try {
157
156
  remainingAttempts--;
158
157
  this.authObject = await _requestToken(this.authObject);
159
- } catch (ex) {
158
+ } catch (error) {
160
159
  if (
161
160
  this.options.retryOnConnectionError &&
162
161
  remainingAttempts > 0 &&
163
- isConnectionError(ex.code)
162
+ isConnectionError(error.code)
164
163
  ) {
165
164
  if (this.options?.eventHandlers?.onConnectionError) {
166
- this.options.eventHandlers.onConnectionError(ex, remainingAttempts);
165
+ this.options.eventHandlers.onConnectionError(error, remainingAttempts);
167
166
  }
168
167
  return this.getAccessToken(forceRefresh, remainingAttempts);
169
168
  }
170
169
 
171
- throw new RestError(ex);
170
+ throw new RestError(error);
172
171
  }
173
172
 
174
173
  if (this.options?.eventHandlers?.onRefresh) {
@@ -205,7 +204,7 @@ function _isExpired(authObject) {
205
204
  }
206
205
  /**
207
206
  * @param {object} authObject Auth Object for api calls
208
- * @returns {Promise<object>} updated Auth Object
207
+ * @returns {Promise.<object>} updated Auth Object
209
208
  */
210
209
  async function _requestToken(authObject) {
211
210
  // TODO retry logic
@@ -218,19 +217,19 @@ async function _requestToken(authObject) {
218
217
  if (authObject.scope && Array.isArray(authObject.scope)) {
219
218
  payload.scope = authObject.scope.join(' ');
220
219
  }
221
- const res = await axios({
220
+ const result = await axios({
222
221
  method: 'post',
223
222
  baseURL: authObject.auth_url,
224
223
  url: '/v2/token',
225
224
  data: payload,
226
225
  });
227
- return Object.assign(authObject, res.data, {
228
- expiration: process.hrtime()[0] + res.data.expires_in,
226
+ return Object.assign(authObject, result.data, {
227
+ expiration: process.hrtime()[0] + result.data.expires_in,
229
228
  });
230
229
  }
231
230
  /**
232
- * @param {Array<string>} scopes list of scopes requested for the auth
233
- * @returns {Array<string>} list of invalid scopes
231
+ * @param {string[]} scopes list of scopes requested for the auth
232
+ * @returns {string[]} list of invalid scopes
234
233
  */
235
234
  function getInvalidScopes(scopes) {
236
235
  return scopes.filter((scope) => !AVAIALABLE_SCOPES.includes(scope));
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  const Auth = require('./auth');
2
3
  const Rest = require('./rest');
3
4
  const Soap = require('./soap');
package/lib/rest.js CHANGED
@@ -1,5 +1,4 @@
1
1
  'use strict';
2
-
3
2
  const axios = require('axios');
4
3
  const { isObject, isPayload, isConnectionError, RestError } = require('./util');
5
4
  const pLimit = require('p-limit');
@@ -26,7 +25,7 @@ module.exports = class Rest {
26
25
  * Method that makes the GET API request
27
26
  *
28
27
  * @param {string} url of the resource to retrieve
29
- * @returns {Promise<object>} API response
28
+ * @returns {Promise.<object>} API response
30
29
  */
31
30
  get(url) {
32
31
  return this._apiRequest(
@@ -37,51 +36,87 @@ module.exports = class Rest {
37
36
  this.options.requestAttempts
38
37
  );
39
38
  }
40
- isTransactionalMessageApi(url) {
39
+ /**
40
+ * helper for {@link getBulk} to determine if the url is a transactional message API
41
+ *
42
+ * @private
43
+ * @param {string} url url without query params
44
+ * @returns {boolean} true if the url is a transactional message API
45
+ */
46
+ _isTransactionalMessageApi(url) {
41
47
  return url && this.transactionalApis.some((api) => url.includes(api));
42
48
  }
49
+ /**
50
+ * helper for {@link getBulk} to determine if the url is a legacy API
51
+ *
52
+ * @private
53
+ * @param {string} url url without query params
54
+ * @returns {boolean} true if the url is a legacy API
55
+ */
56
+ _isLegacyApi(url) {
57
+ return url && url.startsWith('/legacy/v1/');
58
+ }
43
59
  /**
44
60
  * Method that makes paginated GET API Requests using $pageSize and $page parameters
45
61
  *
46
62
  * @param {string} url of the resource to retrieve
47
63
  * @param {number} [pageSize] of the response, defaults to 50
48
- * @returns {Promise<object>} API response combined items
64
+ * @param {string} [iteratorField] attribute of the response to iterate over (only required if it's not 'items'|'definitions'|'entry')
65
+ * @returns {Promise.<object>} API response combined items
49
66
  */
50
- async getBulk(url, pageSize) {
51
- let iteratorField;
67
+ async getBulk(url, pageSize, iteratorField) {
52
68
  let page = 1;
53
69
  const baseUrl = url.split('?')[0];
54
- const isTransactionalMessageApi = this.isTransactionalMessageApi(baseUrl);
55
- const queryParams = new URLSearchParams(url.split('?')[1]);
70
+ const isTransactionalMessageApi = this._isTransactionalMessageApi(baseUrl);
71
+ const isLegacyApi = this._isLegacyApi(baseUrl);
72
+ const queryParameters = new URLSearchParams(url.split('?')[1]);
56
73
  let collector;
57
74
  let shouldPaginate = false;
58
- queryParams.set('$pageSize', Number(pageSize || 50).toString());
75
+ let pageSizeKey = '$pageSize';
76
+ let pageKey = '$page';
77
+ let countKey = 'count';
78
+ if (isLegacyApi) {
79
+ pageSizeKey = '$top';
80
+ pageKey = '$skip';
81
+ countKey = 'totalResults';
82
+ page = 0; // legacy index starts with 0
83
+ if (pageSize != 50) {
84
+ // values other than 50 are ignored by at least some of the sub-endpoints; while others have 50 as the maximum.
85
+ pageSize = 50;
86
+ }
87
+ }
88
+ queryParameters.set(pageSizeKey, Number(pageSize || 50).toString());
59
89
  do {
60
- queryParams.set('$page', Number(page).toString());
61
- const temp = await this._apiRequest(
90
+ queryParameters.set(pageKey, Number(page).toString());
91
+ const responseBatch = await this._apiRequest(
62
92
  {
63
93
  method: 'GET',
64
- url: baseUrl + '?' + decodeURIComponent(queryParams.toString()),
94
+ url: baseUrl + '?' + decodeURIComponent(queryParameters.toString()),
65
95
  },
66
96
  this.options.requestAttempts
67
97
  );
68
- if (Array.isArray(temp.items)) {
98
+ if (iteratorField && Array.isArray(responseBatch[iteratorField])) {
99
+ // if the iteratorField is set, use it
100
+ } else if (Array.isArray(responseBatch.items)) {
69
101
  iteratorField = 'items';
70
- } else if (Array.isArray(temp.definitions)) {
102
+ } else if (Array.isArray(responseBatch.definitions)) {
71
103
  iteratorField = 'definitions';
104
+ } else if (Array.isArray(responseBatch.entry)) {
105
+ iteratorField = 'entry';
72
106
  } else {
73
- throw new Error('Could not find an array to iterate over');
107
+ throw new TypeError('Could not find an array to iterate over');
74
108
  }
75
- if (collector && Array.isArray(temp[iteratorField])) {
76
- collector[iteratorField].push(...temp[iteratorField]);
77
- } else if (collector == null) {
78
- collector = temp;
109
+ if (collector && Array.isArray(responseBatch[iteratorField])) {
110
+ collector[iteratorField].push(...responseBatch[iteratorField]);
111
+ } else if (!collector) {
112
+ collector = responseBatch;
79
113
  }
80
114
  if (
81
115
  Array.isArray(collector[iteratorField]) &&
82
- collector[iteratorField].length >= temp.count &&
116
+ collector[iteratorField].length >= responseBatch[countKey] &&
83
117
  (!isTransactionalMessageApi ||
84
- (isTransactionalMessageApi && temp.count != temp.pageSize))
118
+ (isTransactionalMessageApi &&
119
+ responseBatch[countKey] != responseBatch[pageSizeKey]))
85
120
  ) {
86
121
  // ! the transactional message API returns a value for "count" that represents the currently returned number of records, instead of the total amount. checking for count != pageSize is a workaround for this
87
122
  // * opened Support Case #43988240 for this issue
@@ -90,6 +125,8 @@ module.exports = class Rest {
90
125
  page++;
91
126
  shouldPaginate = true;
92
127
  if (this.options?.eventHandlers?.onLoop) {
128
+ // TODO in v1 change to undefined to ensure breaking changes considered
129
+ // eslint-disable-next-line unicorn/no-null
93
130
  this.options.eventHandlers.onLoop(null, collector?.[iteratorField]);
94
131
  }
95
132
  }
@@ -99,9 +136,9 @@ module.exports = class Rest {
99
136
  /**
100
137
  * Method that makes a GET API request for each URL (including rate limiting)
101
138
  *
102
- * @param {Array<string>} urlArray of the resource to retrieve
139
+ * @param {string[]} urlArray of the resource to retrieve
103
140
  * @param {number} [concurrentLimit=5] number of requests to execute at once
104
- * @returns {Promise<Array>} API response
141
+ * @returns {Promise.<Array>} API response
105
142
  */
106
143
  async getCollection(urlArray, concurrentLimit) {
107
144
  const limit = pLimit(concurrentLimit || 5);
@@ -126,7 +163,7 @@ module.exports = class Rest {
126
163
  *
127
164
  * @param {string} url of the resource to create
128
165
  * @param {object} payload for the POST request body
129
- * @returns {Promise<object>} API response
166
+ * @returns {Promise.<object>} API response
130
167
  */
131
168
  post(url, payload) {
132
169
  const requestOptions = {
@@ -142,7 +179,7 @@ module.exports = class Rest {
142
179
  *
143
180
  * @param {string} url of the resource to replace
144
181
  * @param {object} payload for the PUT request body
145
- * @returns {Promise<object>} API response
182
+ * @returns {Promise.<object>} API response
146
183
  */
147
184
  put(url, payload) {
148
185
  const requestOptions = {
@@ -158,7 +195,7 @@ module.exports = class Rest {
158
195
  *
159
196
  * @param {string} url of the resource to update
160
197
  * @param {object} payload for the PATCH request body
161
- * @returns {Promise<object>} API response
198
+ * @returns {Promise.<object>} API response
162
199
  */
163
200
  patch(url, payload) {
164
201
  const requestOptions = {
@@ -173,7 +210,7 @@ module.exports = class Rest {
173
210
  * Method that makes the DELETE api request
174
211
  *
175
212
  * @param {string} url of the resource to delete
176
- * @returns {Promise<object>} API response
213
+ * @returns {Promise.<object>} API response
177
214
  */
178
215
  delete(url) {
179
216
  return this._apiRequest(
@@ -190,7 +227,7 @@ module.exports = class Rest {
190
227
  *
191
228
  * @param {object} requestOptions configuration for the request including body
192
229
  * @param {number} remainingAttempts number of times this request should be reattempted in case of error
193
- * @returns {Promise<object>} Results from the Rest request in Object format
230
+ * @returns {Promise.<object>} Results from the Rest request in Object format
194
231
  */
195
232
  async _apiRequest(requestOptions, remainingAttempts) {
196
233
  if (!isObject(requestOptions)) {
@@ -214,9 +251,9 @@ module.exports = class Rest {
214
251
  });
215
252
  }
216
253
  return response.data;
217
- } catch (ex) {
254
+ } catch (error) {
218
255
  if (requestOptions.url) {
219
- ex.endpoint =
256
+ error.endpoint =
220
257
  requestOptions.baseURL +
221
258
  (requestOptions.url.startsWith('/')
222
259
  ? requestOptions.url.slice(1)
@@ -225,19 +262,19 @@ module.exports = class Rest {
225
262
  if (
226
263
  this.options.retryOnConnectionError &&
227
264
  remainingAttempts > 0 &&
228
- isConnectionError(ex.code)
265
+ isConnectionError(error.code)
229
266
  ) {
230
267
  if (this.options?.eventHandlers?.onConnectionError) {
231
- this.options.eventHandlers.onConnectionError(ex, remainingAttempts);
268
+ this.options.eventHandlers.onConnectionError(error, remainingAttempts);
232
269
  }
233
270
  return this._apiRequest(requestOptions, remainingAttempts);
234
- } else if (ex.response && ex.response.status === 401 && remainingAttempts) {
271
+ } else if (error.response && error.response.status === 401 && remainingAttempts) {
235
272
  // force refresh due to url related issue
236
273
  await this.auth.getAccessToken(true);
237
274
  //only retry once on refresh since there should be no reason for this token to be invalid
238
275
  return this._apiRequest(requestOptions, 1);
239
276
  } else {
240
- throw new RestError(ex);
277
+ throw new RestError(error);
241
278
  }
242
279
  }
243
280
  }