sfmc-sdk 0.1.1 → 0.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/.eslintrc.json CHANGED
@@ -4,44 +4,22 @@
4
4
  "node": true,
5
5
  "mocha": true
6
6
  },
7
- "extends": ["eslint:recommended", "prettier", "ssjs"],
8
- "plugins": ["mocha", "prettier"],
7
+ "extends": [
8
+ "eslint:recommended",
9
+ "plugin:mocha/recommended",
10
+ "plugin:jsdoc/recommended",
11
+ "plugin:prettier/recommended"
12
+ ],
13
+ "plugins": [
14
+ "mocha",
15
+ "jsdoc"
16
+ ],
9
17
  "globals": {
10
18
  "Atomics": "readonly",
11
19
  "SharedArrayBuffer": "readonly"
12
20
  },
13
21
  "parserOptions": {
14
- "ecmaVersion": 2018,
22
+ "ecmaVersion": 2020,
15
23
  "sourceType": "module"
16
- },
17
- "rules": {
18
- "arrow-body-style": ["error", "as-needed"],
19
- "curly": "error",
20
- "mocha/no-exclusive-tests": "error",
21
- "no-console": "off",
22
- "require-jsdoc": [
23
- "warn",
24
- {
25
- "require": {
26
- "FunctionDeclaration": true,
27
- "MethodDefinition": true,
28
- "ClassDeclaration": true,
29
- "ArrowFunctionExpression": false,
30
- "FunctionExpression": true
31
- }
32
- }
33
- ],
34
- "valid-jsdoc": "error",
35
- "spaced-comment": ["warn", "always", { "block": { "exceptions": ["*"], "balanced": true } }]
36
- },
37
- "overrides": [
38
- {
39
- "files": ["*.js"],
40
- "rules": {
41
- "no-var": "error",
42
- "prefer-const": "error",
43
- "prettier/prettier": "warn"
44
- }
45
- }
46
- ]
47
- }
24
+ }
25
+ }
@@ -21,4 +21,6 @@ jobs:
21
21
  with:
22
22
  node-version: 16
23
23
  registry-url: https://registry.npmjs.org/
24
+ - run: npm install
25
+ - run: npm run lint
24
26
  - run: npm run test
@@ -34,7 +34,7 @@ jobs:
34
34
  git add package.json
35
35
  git add package-lock.json
36
36
  git commit -m "Release ${{ steps.create_release.outputs.tag_name }}"
37
- git push origin master
37
+ git push origin main
38
38
  - uses: eregon/publish-release@v1
39
39
  env:
40
40
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,3 @@
1
+ {
2
+ "editor.formatOnSave": true
3
+ }
package/README.md CHANGED
@@ -36,7 +36,12 @@ const sfmc = new SDK(
36
36
  auth_url: 'https://ZZZZZZZ.auth.marketingcloudapis.com/',
37
37
  account_id: 7281698,
38
38
  },
39
- true
39
+ {
40
+ onLoop: (type, accumulator) => console.log("Looping", type, accumlator.length),
41
+ onRefresh: (options) => console.log("RefreshingToken.", Options),
42
+ logRequest: (req) => console.log(req),
43
+ logResponse: (res) => console.log(res)
44
+ }
40
45
  );
41
46
  ```
42
47
 
@@ -46,19 +51,25 @@ SOAP currently only supports all the standard SOAP action types. Some examples b
46
51
 
47
52
  ```javascript
48
53
  const soapRetrieve = await sfmc.soap.retrieve('DataExtension', ['ObjectID'], {});
49
- const soapRetrieveBulk = await sfmc.soap.retrieveBulk('DataExtension', ['ObjectID'], filter: {
50
- leftOperand: 'ExternalKey',
51
- operator: 'equals',
52
- rightOperand: 'SOMEKEYHERE',
53
- }); // when you want to auto paginate
54
- const soapCreate = await sfmc.soap.create('Subscriber', {
55
- "SubscriberKey": "12345123",
56
- "EmailAddress": "example@example.com"
57
- }, {
58
- "options": {
59
- "SaveOptions": { "SaveAction" : "UpdateAdd" }
60
- }
61
- }});
54
+ const soapRetrieveBulk = await sfmc.soap.retrieveBulk('DataExtension', ['ObjectID'], {
55
+ filter: {
56
+ leftOperand: 'CustomerKey',
57
+ operator: 'equals',
58
+ rightOperand: 'SOMEKEYHERE',
59
+ },
60
+ }); // when you want to auto paginate
61
+ const soapCreate = await sfmc.soap.create(
62
+ 'Subscriber',
63
+ {
64
+ SubscriberKey: '12345123',
65
+ EmailAddress: 'example@example.com',
66
+ },
67
+ {
68
+ options: {
69
+ SaveOptions: { SaveAction: 'UpdateAdd' },
70
+ },
71
+ }
72
+ );
62
73
  const soapUpdate = await sfmc.soap.update('Role', {
63
74
  "CustomerKey": "12345123",
64
75
  "Name": "UpdatedName"
@@ -90,7 +101,6 @@ Please make sure to update tests as appropriate.
90
101
 
91
102
  ## To Do
92
103
 
93
- - No tests are in place
94
104
  - Look at persisting access tokens across sessions as an option
95
105
  - Validation improvement
96
106
  - Support Scopes in API Requests
package/lib/auth.js CHANGED
@@ -4,60 +4,52 @@ const axios = require('axios');
4
4
  module.exports = class Auth {
5
5
  /**
6
6
  * Creates an instance of Auth.
7
- * @param {Object} options Auth Payload
7
+ *
8
+ * @param {object} options Auth Payload
8
9
  * @param {string} options.client_id Client Id from SFMC config
9
10
  * @param {string} options.client_secret Client Secret from SFMC config
10
11
  * @param {number} options.account_id MID of Business Unit used for API Calls
11
12
  * @param {string} options.auth_url Auth URL from SFMC config
12
13
  * @param {string} [options.scope] Scope used for requests
13
- * @param {Object} eventHandlers collection of handler functions (for examplef or logging)
14
+ * @param {object} eventHandlers collection of handler functions (for examplef or logging)
14
15
  */
15
16
  constructor(options, eventHandlers) {
16
- if (options) {
17
- if (!options.client_id) {
18
- throw new Error('clientId or clientSecret is missing or invalid');
19
- }
20
- if (typeof options.client_id !== 'string') {
21
- throw new Error('client_id or client_secret must be strings');
22
- }
23
- if (!options.client_secret) {
24
- throw new Error('client_id or client_secret is missing or invalid');
25
- }
26
- if (typeof options.client_secret !== 'string') {
27
- throw new Error('client_id or client_secret must be strings');
28
- }
29
- if (!options.account_id) {
30
- throw new Error('account_id is missing or invalid');
31
- }
32
- if (!Number.isInteger(options.account_id)) {
33
- throw new Error('account_id must be an integer');
34
- }
35
- if (!options.auth_url) {
36
- throw new Error('auth_url is missing or invalid');
37
- }
38
- if (typeof options.auth_url !== 'string') {
39
- throw new Error('auth_url must be a string');
40
- }
41
- if (
42
- !/https:\/\/[a-z0-9\-]{28}\.auth\.marketingcloudapis\.com\//gm.test(options.auth_url)
43
- ) {
44
- throw new Error(
45
- 'auth_url must be in format https://mcXXXXXXXXXXXXXXXXXXXXXXXXXX.auth.marketingcloudapis.com/'
46
- );
47
- }
48
- // TODO add check for scope
49
- } else {
17
+ if (!options) {
50
18
  throw new Error('options are required. see readme.');
19
+ } else if (!options.client_id) {
20
+ throw new Error('client_id or client_secret is missing or invalid');
21
+ } else if (typeof options.client_id !== 'string') {
22
+ throw new Error('client_id or client_secret must be strings');
23
+ } else if (!options.client_secret) {
24
+ throw new Error('client_id or client_secret is missing or invalid');
25
+ } else if (typeof options.client_secret !== 'string') {
26
+ throw new Error('client_id or client_secret must be strings');
27
+ } else if (!options.account_id) {
28
+ throw new Error('account_id is missing or invalid');
29
+ } else if (!Number.isInteger(Number.parseInt(options.account_id))) {
30
+ throw new Error(
31
+ 'account_id must be an Integer (Integers in String format are accepted)'
32
+ );
33
+ } else if (!options.auth_url) {
34
+ throw new Error('auth_url is missing or invalid');
35
+ } else if (typeof options.auth_url !== 'string') {
36
+ throw new Error('auth_url must be a string');
37
+ } else if (
38
+ !/https:\/\/[a-z0-9-]{28}\.auth\.marketingcloudapis\.com\//gm.test(options.auth_url)
39
+ ) {
40
+ throw new Error(
41
+ 'auth_url must be in format https://mcXXXXXXXXXXXXXXXXXXXXXXXXXX.auth.marketingcloudapis.com/'
42
+ );
51
43
  }
52
-
44
+ // TODO add check for scope
53
45
  this.options = Object.assign(this.options || {}, options);
54
46
  this.eventHandlers = eventHandlers;
55
47
  }
56
48
  /**
57
49
  *
58
50
  *
59
- * @param {Boolean} forceRefresh used to enforce a refresh of token
60
- * @return {Promise<Object>} current session information
51
+ * @param {boolean} forceRefresh used to enforce a refresh of token
52
+ * @returns {Promise<object>} current session information
61
53
  */
62
54
  async getAccessToken(forceRefresh) {
63
55
  if (Boolean(forceRefresh) || _isExpired(this.options)) {
@@ -70,8 +62,8 @@ module.exports = class Auth {
70
62
  }
71
63
  };
72
64
  /**
73
- * @param {Object} options Auth object
74
- * @return {Boolean} true if token is expired
65
+ * @param {object} options Auth object
66
+ * @returns {boolean} true if token is expired
75
67
  */
76
68
  function _isExpired(options) {
77
69
  let expired = false;
@@ -88,8 +80,8 @@ function _isExpired(options) {
88
80
  /**
89
81
  *
90
82
  *
91
- * @param {Object} options Auth Object for api calls
92
- * @return {Promise<Object>} updated Auth Object
83
+ * @param {object} options Auth Object for api calls
84
+ * @returns {Promise<object>} updated Auth Object
93
85
  */
94
86
  async function _requestToken(options) {
95
87
  // TODO retry logic
package/lib/index.js CHANGED
@@ -5,8 +5,9 @@ const Soap = require('./soap');
5
5
  module.exports = class SDK {
6
6
  /**
7
7
  * Creates an instance of SDK.
8
- * @param {Object} options Auth Object for making requests
9
- * @param {Object} eventHandlers collection of handler functions (for examplef or logging)
8
+ *
9
+ * @param {object} options Auth Object for making requests
10
+ * @param {object} eventHandlers collection of handler functions (for examplef or logging)
10
11
  */
11
12
  constructor(options, eventHandlers) {
12
13
  this.auth = new Auth(options, eventHandlers);
package/lib/rest.js CHANGED
@@ -7,9 +7,10 @@ const pLimit = require('p-limit');
7
7
  module.exports = class Rest {
8
8
  /**
9
9
  * Constuctor of Rest object
10
- * @constructor
11
- * @param {Object} auth Auth object used for initializing
12
- * @param {Object} eventHandlers collection of handler functions (for examplef or logging)
10
+ *
11
+ * @function Object() { [native code] }
12
+ * @param {object} auth Auth object used for initializing
13
+ * @param {object} eventHandlers collection of handler functions (for examplef or logging)
13
14
  */
14
15
  constructor(auth, eventHandlers) {
15
16
  this.auth = auth;
@@ -18,8 +19,9 @@ module.exports = class Rest {
18
19
 
19
20
  /**
20
21
  * Method that makes the GET API request
22
+ *
21
23
  * @param {string} url of the resource to retrieve
22
- * @returns {Promise<Object>} API response
24
+ * @returns {Promise<object>} API response
23
25
  */
24
26
  get(url) {
25
27
  return _apiRequest(
@@ -33,19 +35,20 @@ module.exports = class Rest {
33
35
  }
34
36
  /**
35
37
  * Method that makes paginated GET API Requests using $pageSize and $page parameters
38
+ *
36
39
  * @param {string} url of the resource to retrieve
37
- * @returns {Promise<Object>} API response combined items
40
+ * @param {number} pageSize of the response, defaults to 50
41
+ * @returns {Promise<object>} API response combined items
38
42
  */
39
- async getBulk(url) {
43
+ async getBulk(url, pageSize) {
40
44
  let page = 1;
41
45
  const baseUrl = url.split('?')[0];
42
46
  const queryParams = new URLSearchParams(url.split('?')[1]);
43
47
  let collector;
44
- let shouldContinue = false;
45
- queryParams.set('$pageSize', Number(2).toString());
48
+ let shouldPaginate = false;
49
+ queryParams.set('$pageSize', Number(pageSize || 50).toString());
46
50
  do {
47
51
  queryParams.set('$page', Number(page).toString());
48
-
49
52
  const temp = await _apiRequest(
50
53
  this.auth,
51
54
  {
@@ -60,22 +63,25 @@ module.exports = class Rest {
60
63
  collector = temp;
61
64
  }
62
65
  if (Array.isArray(collector.items) && collector.items.length >= temp.count) {
63
- shouldContinue = false;
66
+ shouldPaginate = false;
64
67
  } else {
65
68
  page++;
66
- shouldContinue = true;
69
+ shouldPaginate = true;
67
70
  }
68
- } while (shouldContinue);
71
+ } while (shouldPaginate);
69
72
  return collector;
70
73
  }
71
74
  /**
72
75
  * Method that makes a GET API request for each URL (including rate limiting)
73
- * @param {Array<String>} urlArray of the resource to retrieve
76
+ *
77
+ * @param {Array<string>} urlArray of the resource to retrieve
74
78
  * @param {number} [concurrentLimit=5] number of requests to execute at once
75
79
  * @returns {Promise<Array>} API response
76
80
  */
77
- getCollection(urlArray, concurrentLimit) {
81
+ async getCollection(urlArray, concurrentLimit) {
78
82
  const limit = pLimit(concurrentLimit || 5);
83
+ // run auth before to avoid parallel requests
84
+ await this.auth.getAccessToken();
79
85
  return Promise.all(
80
86
  urlArray.map((url) =>
81
87
  limit(() =>
@@ -93,9 +99,10 @@ module.exports = class Rest {
93
99
  }
94
100
  /**
95
101
  * Method that makes the POST api request
102
+ *
96
103
  * @param {string} url of the resource to create
97
- * @param {Object} payload for the POST request body
98
- * @returns {Promise<Object>} API response
104
+ * @param {object} payload for the POST request body
105
+ * @returns {Promise<object>} API response
99
106
  */
100
107
  post(url, payload) {
101
108
  const options = {
@@ -108,9 +115,10 @@ module.exports = class Rest {
108
115
  }
109
116
  /**
110
117
  * Method that makes the PUT api request
118
+ *
111
119
  * @param {string} url of the resource to replace
112
- * @param {Object} payload for the PUT request body
113
- * @returns {Promise<Object>} API response
120
+ * @param {object} payload for the PUT request body
121
+ * @returns {Promise<object>} API response
114
122
  */
115
123
  put(url, payload) {
116
124
  const options = {
@@ -123,9 +131,10 @@ module.exports = class Rest {
123
131
  }
124
132
  /**
125
133
  * Method that makes the PATCH api request
134
+ *
126
135
  * @param {string} url of the resource to update
127
- * @param {Object} payload for the PATCH request body
128
- * @returns {Promise<Object>} API response
136
+ * @param {object} payload for the PATCH request body
137
+ * @returns {Promise<object>} API response
129
138
  */
130
139
  patch(url, payload) {
131
140
  const options = {
@@ -138,8 +147,9 @@ module.exports = class Rest {
138
147
  }
139
148
  /**
140
149
  * Method that makes the DELETE api request
150
+ *
141
151
  * @param {string} url of the resource to delete
142
- * @returns {Promise<Object>} API response
152
+ * @returns {Promise<object>} API response
143
153
  */
144
154
  delete(url) {
145
155
  return _apiRequest(
@@ -155,24 +165,25 @@ module.exports = class Rest {
155
165
  };
156
166
  /**
157
167
  * method to check if the payload is plausible and throw error if not
158
- * @param {Object} options API request opptions
159
- * @returns {Void} throws error if issue
168
+ *
169
+ * @param {object} options API request opptions
160
170
  */
161
171
  function _checkPayload(options) {
162
172
  if (!isObject(options.data)) {
163
- throw new TypeError(`${options.method} requests require a payload in options.data`);
173
+ throw new Error(`${options.method} requests require a payload in options.data`);
164
174
  }
165
175
  }
166
176
  /**
167
177
  * Method that makes the api request
168
- * @param {Object} auth - Auth Object used to make request
169
- * @param {Object} options configuration for the request including body
178
+ *
179
+ * @param {object} auth - Auth Object used to make request
180
+ * @param {object} options configuration for the request including body
170
181
  * @param {number} remainingAttempts number of times this request should be reattempted in case of error
171
- * @returns {Promise<Object>} Results from the Rest request in Object format
182
+ * @returns {Promise<object>} Results from the Rest request in Object format
172
183
  */
173
184
  async function _apiRequest(auth, options, remainingAttempts) {
174
185
  if (!isObject(options)) {
175
- throw new TypeError('options argument is required');
186
+ throw new Error('options argument is required');
176
187
  }
177
188
  try {
178
189
  await auth.getAccessToken();
@@ -185,14 +196,14 @@ async function _apiRequest(auth, options, remainingAttempts) {
185
196
  if (options.method.includes('POST', 'PATCH', 'PUT')) {
186
197
  requestOptions.data = options.data;
187
198
  }
188
-
189
199
  const res = await axios(requestOptions);
190
200
  return res.data;
191
201
  } catch (ex) {
192
- if (ex.response && ex.response.status === 404 && remainingAttempts) {
202
+ if (ex.response && ex.response.status === 401 && remainingAttempts) {
193
203
  // force refresh due to url related issue
194
204
  await auth.getAccessToken(true);
195
- return _apiRequest(auth, options, remainingAttempts--);
205
+ remainingAttempts--;
206
+ return _apiRequest(auth, options, remainingAttempts);
196
207
  } else {
197
208
  throw ex;
198
209
  }