sfmc-sdk 0.0.2 → 0.0.6

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.
@@ -20,16 +20,3 @@ jobs:
20
20
  - run: npm publish
21
21
  env:
22
22
  NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
23
-
24
- publish-gpr:
25
- runs-on: ubuntu-latest
26
- steps:
27
- - uses: actions/checkout@v2
28
- - uses: actions/setup-node@v2
29
- with:
30
- node-version: 12
31
- registry-url: https://npm.pkg.github.com/
32
- - run: npm ci
33
- - run: npm publish
34
- env:
35
- NODE_AUTH_TOKEN: ${{secrets.GH_PACKAGEREGISTRY}}
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## SFMC SDK follows [semantic versioning](https://semver.org/).
4
4
 
5
+ ## 0.0.6 - 2021-12-23
6
+
7
+ - Bump dependency versions
8
+ - Extended SOAP action support to other types
9
+ - Added SOAP Retreive Bulk
10
+ - Added REST Get Collection & GetBulk features
11
+
5
12
  ## 0.0.2 - 2021-04-10
6
13
 
7
14
  NPM Publishing
@@ -4,10 +4,7 @@
4
4
 
5
5
  - [ ] Documentation update
6
6
  - [ ] Bug fix
7
- - [ ] New metadata support
8
- - [ ] Enhanced metadata
9
- - [ ] Add a CLI option
10
- - [ ] Add something to the core
7
+ - [ ] Extend features
11
8
  - [ ] Other, please explain:
12
9
 
13
10
  ## What changes did you make? (Give an overview)
package/README.md CHANGED
@@ -16,30 +16,57 @@ This library attempts to overcomes some of the complexity/shortcomings of the or
16
16
  - Is opinionated about how Auth should be managed (only accepts a standard Auth method)
17
17
  - Only uses Promises/Async-Await, no callbacks
18
18
  - Maintainers of the semi-official lib from Salesforce are not responsive
19
+ - Allows for using a persisting credentials in an external app, then passing
20
+ - We expect parsing of SOAP to
19
21
 
20
22
  ## Usage
21
23
 
22
24
  ### Initialization
23
25
 
24
26
  Initializes the Auth Object in the SDK.
25
- The SDK will automatically request a new token if none is valid
27
+ The SDK will automatically request a new token if none is valid.
28
+ the second parameter in the constructor is to allow for specific events to execute a function. Currently onRefresh and onLoop are supported. This reduces the number of requests for token therefore increasing speed between executions (when testing was 2.5 seconds down to 1.5 seconds for one rest and one soap request)
26
29
 
27
30
  ```javascript
28
31
  const SDK = require('sfmc-sdk');
29
- const sfmc = new SDK({
30
- client_id: 'XXXXX',
31
- client_secret: 'YYYYYY',
32
- auth_url: 'https://ZZZZZZZ.auth.marketingcloudapis.com/',
33
- account_id: 7281698,
34
- });
32
+ const sfmc = new SDK(
33
+ {
34
+ client_id: 'XXXXX',
35
+ client_secret: 'YYYYYY',
36
+ auth_url: 'https://ZZZZZZZ.auth.marketingcloudapis.com/',
37
+ account_id: 7281698,
38
+ },
39
+ true
40
+ );
35
41
  ```
36
42
 
37
43
  ### SOAP
38
44
 
39
- SOAP currently only supports retrieve, will be updating soon for other types.
45
+ SOAP currently only supports all the standard SOAP action types. Some examples below
40
46
 
41
47
  ```javascript
42
- const soapResponse = await sfmc.soap.retrieve('DataExtension', ['ObjectID'], {});
48
+ 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
+ }});
62
+ const soapUpdate = await sfmc.soap.update('Role', {
63
+ "CustomerKey": "12345123",
64
+ "Name": "UpdatedName"
65
+ }, {});
66
+ const soapExecute = await sfmc.soap.execute('LogUnsubEvent', [{
67
+ "SubscriberKey": "12345123",
68
+ "EmailAddress": "example@example.com"
69
+ }], {});
43
70
  ```
44
71
 
45
72
  ### REST
@@ -51,6 +78,8 @@ const restResponse = await sfmc.rest.get('/interaction/v1/interactions');
51
78
  const restResponse = await sfmc.rest.post('/interaction/v1/interactions', jsonPayload);
52
79
  const restResponse = await sfmc.rest.patch('/interaction/v1/interactions/IDHERE', jsonPayload); // PUT ALSO
53
80
  const restResponse = await sfmc.rest.delete('/interaction/v1/interactions/IDHERE');
81
+ const restResponse = await sfmc.rest.getBulk('/interaction/v1/interactions'); // auto-paginate based on $pageSize
82
+ const restResponse = await sfmc.rest.getCollection(['/interaction/v1/interactions/213', '/interaction/v1/interactions/123'], 3); // parallel requests
54
83
  ```
55
84
 
56
85
  ## Contributing
@@ -62,7 +91,6 @@ Please make sure to update tests as appropriate.
62
91
  ## To Do
63
92
 
64
93
  - No tests are in place
65
- - Improve handling for other SOAP Actions than Retrieve
66
94
  - Look at persisting access tokens across sessions as an option
67
95
  - Validation improvement
68
96
  - Support Scopes in API Requests
package/lib/auth.js CHANGED
@@ -1,24 +1,18 @@
1
- /*
2
- * Copyright (c) 2018, salesforce.com, inc.
3
- * All rights reserved.
4
- * Licensed under the BSD 3-Clause license.
5
- * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
- */
7
1
  'use strict';
8
2
 
9
3
  const axios = require('axios');
10
-
11
4
  module.exports = class Auth {
12
5
  /**
13
6
  * Creates an instance of Auth.
14
7
  * @param {Object} options Auth Payload
15
- * @param {String} options.client_id Client Id from SFMC config
16
- * @param {String} options.client_secret Client Secret from SFMC config
17
- * @param {Number} options.account_id MID of Business Unit used for API Calls
18
- * @param {String} options.auth_url Auth URL from SFMC config
19
- * @param {String} [options.scope] Scope used for requests
8
+ * @param {string} options.client_id Client Id from SFMC config
9
+ * @param {string} options.client_secret Client Secret from SFMC config
10
+ * @param {number} options.account_id MID of Business Unit used for API Calls
11
+ * @param {string} options.auth_url Auth URL from SFMC config
12
+ * @param {string} [options.scope] Scope used for requests
13
+ * @param {Object} eventHandlers collection of handler functions (for examplef or logging)
20
14
  */
21
- constructor(options) {
15
+ constructor(options, eventHandlers) {
22
16
  if (options) {
23
17
  if (!options.client_id) {
24
18
  throw new Error('clientId or clientSecret is missing or invalid');
@@ -45,7 +39,7 @@ module.exports = class Auth {
45
39
  throw new Error('auth_url must be a string');
46
40
  }
47
41
  if (
48
- !/https:\/\/[a-z0-9]{28}\.auth\.marketingcloudapis\.com\//gm.test(options.auth_url)
42
+ !/https:\/\/[a-z0-9\-]{28}\.auth\.marketingcloudapis\.com\//gm.test(options.auth_url)
49
43
  ) {
50
44
  throw new Error(
51
45
  'auth_url must be in format https://mcXXXXXXXXXXXXXXXXXXXXXXXXXX.auth.marketingcloudapis.com/'
@@ -57,17 +51,22 @@ module.exports = class Auth {
57
51
  }
58
52
 
59
53
  this.options = Object.assign(this.options || {}, options);
54
+ this.eventHandlers = eventHandlers;
60
55
  }
61
56
  /**
62
57
  *
63
58
  *
64
59
  * @param {Boolean} forceRefresh used to enforce a refresh of token
65
- * @return {void}
60
+ * @return {Promise<Object>} current session information
66
61
  */
67
62
  async getAccessToken(forceRefresh) {
68
63
  if (Boolean(forceRefresh) || _isExpired(this.options)) {
69
64
  this.options = await _requestToken(this.options);
65
+ if (this.eventHandlers && this.eventHandlers.onRefresh) {
66
+ this.eventHandlers.onRefresh(this.options);
67
+ }
70
68
  }
69
+ return this.options;
71
70
  }
72
71
  };
73
72
  /**
@@ -103,19 +102,13 @@ async function _requestToken(options) {
103
102
  if (options.scope) {
104
103
  payload.scope = options.scope;
105
104
  }
106
- try {
107
- const res = await axios({
108
- method: 'post',
109
- baseURL: options.auth_url,
110
- url: '/v2/token',
111
- data: payload,
112
- });
113
- const newAuth = Object.assign(options, res.data);
114
- newAuth.expiration = process.hrtime()[0] + res.data.expires_in;
115
- return newAuth;
116
- } catch (ex) {
117
- console.log(ex.response.status, ex.response.statusCode);
118
- console.log(ex.response.data);
119
- console.log(payload);
120
- }
105
+ const res = await axios({
106
+ method: 'post',
107
+ baseURL: options.auth_url,
108
+ url: '/v2/token',
109
+ data: payload,
110
+ });
111
+ const newAuth = Object.assign(options, res.data);
112
+ newAuth.expiration = process.hrtime()[0] + res.data.expires_in;
113
+ return newAuth;
121
114
  }
package/lib/index.js CHANGED
@@ -6,10 +6,11 @@ module.exports = class SDK {
6
6
  /**
7
7
  * Creates an instance of SDK.
8
8
  * @param {Object} options Auth Object for making requests
9
+ * @param {Object} eventHandlers collection of handler functions (for example for logging)
9
10
  */
10
- constructor(options) {
11
- const auth = new Auth(options);
12
- this.rest = new Rest(auth);
13
- this.soap = new Soap(auth);
11
+ constructor(options, eventHandlers) {
12
+ this.auth = new Auth(options, eventHandlers);
13
+ this.rest = new Rest(this.auth, eventHandlers);
14
+ this.soap = new Soap(this.auth, eventHandlers);
14
15
  }
15
16
  };
package/lib/rest.js CHANGED
@@ -2,88 +2,155 @@
2
2
 
3
3
  const axios = require('axios');
4
4
  const { isObject } = require('./util');
5
+ const pLimit = require('p-limit');
5
6
 
6
7
  module.exports = class Rest {
7
8
  /**
8
9
  * Constuctor of Rest object
9
10
  * @constructor
10
11
  * @param {Object} auth Auth object used for initializing
12
+ * @param {Object} eventHandlers collection of handler functions (for examplef or logging)
11
13
  */
12
- constructor(auth) {
14
+ constructor(auth, eventHandlers) {
13
15
  this.auth = auth;
16
+ this.eventHandlers = eventHandlers;
14
17
  }
15
18
 
16
19
  /**
17
- * Method that makes the GET api request
18
- * @param {String} url of the resource to retrieve
20
+ * Method that makes the GET API request
21
+ * @param {string} url of the resource to retrieve
19
22
  * @returns {Promise<Object>} API response
20
23
  */
21
24
  get(url) {
22
- return _apiRequest(this.auth, {
23
- method: 'GET',
24
- retry: true,
25
- url: url,
26
- });
25
+ return _apiRequest(
26
+ this.auth,
27
+ {
28
+ method: 'GET',
29
+ url: url,
30
+ },
31
+ 1
32
+ );
33
+ }
34
+ /**
35
+ * Method that makes paginated GET API Requests using $pageSize and $page parameters
36
+ * @param {string} url of the resource to retrieve
37
+ * @returns {Promise<Object>} API response combined items
38
+ */
39
+ async getBulk(url) {
40
+ let page = 1;
41
+ const baseUrl = url.split('?')[0];
42
+ const queryParams = new URLSearchParams(url.split('?')[1]);
43
+ let collector;
44
+ let shouldContinue = false;
45
+ queryParams.set('$pageSize', Number(2).toString());
46
+ do {
47
+ queryParams.set('$page', Number(page).toString());
48
+
49
+ const temp = await _apiRequest(
50
+ this.auth,
51
+ {
52
+ method: 'GET',
53
+ url: baseUrl + '?' + decodeURIComponent(queryParams.toString()),
54
+ },
55
+ 1
56
+ );
57
+ if (collector && Array.isArray(temp.items)) {
58
+ collector.items.push(...temp.items);
59
+ } else if (collector == null) {
60
+ collector = temp;
61
+ }
62
+ if (Array.isArray(collector.items) && collector.items.length >= temp.count) {
63
+ shouldContinue = false;
64
+ } else {
65
+ page++;
66
+ shouldContinue = true;
67
+ }
68
+ } while (shouldContinue);
69
+ return collector;
70
+ }
71
+ /**
72
+ * Method that makes a GET API request for each URL (including rate limiting)
73
+ * @param {Array<String>} urlArray of the resource to retrieve
74
+ * @param {number} [concurrentLimit=5] number of requests to execute at once
75
+ * @returns {Promise<Array>} API response
76
+ */
77
+ getCollection(urlArray, concurrentLimit) {
78
+ const limit = pLimit(concurrentLimit || 5);
79
+ return Promise.all(
80
+ urlArray.map((url) =>
81
+ limit(() =>
82
+ _apiRequest(
83
+ this.auth,
84
+ {
85
+ method: 'GET',
86
+ url: url,
87
+ },
88
+ 1
89
+ )
90
+ )
91
+ )
92
+ );
27
93
  }
28
94
  /**
29
95
  * Method that makes the POST api request
30
- * @param {String} url of the resource to create
96
+ * @param {string} url of the resource to create
31
97
  * @param {Object} payload for the POST request body
32
98
  * @returns {Promise<Object>} API response
33
99
  */
34
100
  post(url, payload) {
35
101
  const options = {
36
102
  method: 'POST',
37
- retry: true,
38
103
  url: url,
39
104
  data: payload,
40
105
  };
41
106
  _checkPayload(options);
42
- return _apiRequest(this.auth, options);
107
+ return _apiRequest(this.auth, options, 1);
43
108
  }
44
109
  /**
45
110
  * Method that makes the PUT api request
46
- * @param {String} url of the resource to replace
111
+ * @param {string} url of the resource to replace
47
112
  * @param {Object} payload for the PUT request body
48
113
  * @returns {Promise<Object>} API response
49
114
  */
50
115
  put(url, payload) {
51
116
  const options = {
52
117
  method: 'PUT',
53
- retry: true,
54
118
  url: url,
55
119
  data: payload,
56
120
  };
57
121
  _checkPayload(options);
58
- return _apiRequest(this.auth, options);
122
+ return _apiRequest(this.auth, options, 1);
59
123
  }
60
124
  /**
61
125
  * Method that makes the PATCH api request
62
- * @param {String} url of the resource to update
126
+ * @param {string} url of the resource to update
63
127
  * @param {Object} payload for the PATCH request body
64
128
  * @returns {Promise<Object>} API response
65
129
  */
66
130
  patch(url, payload) {
67
131
  const options = {
68
132
  method: 'PATCH',
69
- retry: true,
70
133
  url: url,
71
134
  data: payload,
72
135
  };
73
136
  _checkPayload(options);
74
- return _apiRequest(this.auth, options);
137
+ return _apiRequest(this.auth, options, 1);
75
138
  }
76
139
  /**
77
140
  * Method that makes the DELETE api request
78
- * @param {String} url of the resource to delete
141
+ * @param {string} url of the resource to delete
79
142
  * @returns {Promise<Object>} API response
80
143
  */
81
144
  delete(url) {
82
- return _apiRequest(this.auth, {
83
- method: 'DELETE',
84
- retry: true,
85
- url: url,
86
- });
145
+ return _apiRequest(
146
+ this.auth,
147
+ {
148
+ method: 'DELETE',
149
+
150
+ url: url,
151
+ },
152
+ 1
153
+ );
87
154
  }
88
155
  };
89
156
  /**
@@ -100,9 +167,10 @@ function _checkPayload(options) {
100
167
  * Method that makes the api request
101
168
  * @param {Object} auth - Auth Object used to make request
102
169
  * @param {Object} options configuration for the request including body
170
+ * @param {number} remainingAttempts number of times this request should be reattempted in case of error
103
171
  * @returns {Promise<Object>} Results from the Rest request in Object format
104
172
  */
105
- async function _apiRequest(auth, options) {
173
+ async function _apiRequest(auth, options, remainingAttempts) {
106
174
  if (!isObject(options)) {
107
175
  throw new TypeError('options argument is required');
108
176
  }
@@ -115,12 +183,18 @@ async function _apiRequest(auth, options) {
115
183
  headers: { Authorization: `Bearer ` + auth.options.access_token },
116
184
  };
117
185
  if (options.method.includes('POST', 'PATCH', 'PUT')) {
118
- requestOptions.data = options.payload;
186
+ requestOptions.data = options.data;
119
187
  }
188
+
120
189
  const res = await axios(requestOptions);
121
190
  return res.data;
122
191
  } catch (ex) {
123
- console.error(ex.message, ex.response);
124
- console.error(options);
192
+ if (ex.response && ex.response.status === 404 && remainingAttempts) {
193
+ // force refresh due to url related issue
194
+ await auth.getAccessToken(true);
195
+ return _apiRequest(auth, options, remainingAttempts--);
196
+ } else {
197
+ throw ex;
198
+ }
125
199
  }
126
200
  }
package/lib/soap.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
  const axios = require('axios');
3
- const xml2js = require('xml2js');
3
+ const xmlToJson = require('fast-xml-parser');
4
+ const JsonToXml = require('fast-xml-parser').j2xParser;
4
5
  const { isObject } = require('./util');
5
6
 
6
7
  module.exports = class Soap {
@@ -8,77 +9,313 @@ module.exports = class Soap {
8
9
  * Constuctor of Soap object
9
10
  * @constructor
10
11
  * @param {Object} auth Auth object used for initializing
12
+ * @param {Object} eventHandlers collection of handler functions (for examplef or logging)
11
13
  */
12
- constructor(auth) {
14
+ constructor(auth, eventHandlers) {
13
15
  this.auth = auth;
16
+ this.eventHandlers = eventHandlers;
14
17
  }
15
18
 
16
19
  /**
17
20
  * Method used to retrieve data via SOAP API
18
- * @param {String} type -SOAP Object type
21
+ * @param {string} type -SOAP Object type
19
22
  * @param {Array<String>} props - Properties which should be retrieved
20
- * @param {Object} options - configuration of the request
21
- * @param {Array<Number>} [clientIDs] - MIDs which should be added to the request payload
23
+ * @param {Object} [requestParams] - additional RetrieveRequest parameters, for example filter or options
22
24
  * @returns {Promise<Object>} SOAP object converted from XML
23
25
  */
24
- retrieve(type, props, options, clientIDs) {
25
- // const defaults = {
26
- // paginate: false,
27
- // };
26
+ retrieve(type, props, requestParams) {
28
27
  const body = {
29
28
  RetrieveRequestMsg: {
30
- $: {
31
- xmlns: 'http://exacttarget.com/wsdl/partnerAPI',
32
- },
29
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
33
30
  RetrieveRequest: {
34
31
  ObjectType: type,
35
32
  Properties: props,
36
33
  },
37
34
  },
38
35
  };
39
- if (clientIDs) {
40
- body.RetrieveRequestMsg.RetrieveRequest.ClientIDs = clientIDs;
41
- }
42
- // filter can be simple or complex and has three properties leftOperand, rightOperand, and operator
43
- if (options.filter) {
44
- body.RetrieveRequestMsg.RetrieveRequest.Filter = _parseFilter(options.filter);
36
+
37
+ if (requestParams) {
38
+ validateOptions(requestParams.options, [
39
+ 'BatchSize',
40
+ 'IncludeObjects',
41
+ 'OnlyIncludeBase',
42
+ ]);
43
+ if (requestParams.options) {
44
+ body.RetrieveRequestMsg.RetrieveRequest.Options = requestParams.options;
45
+ }
46
+ if (requestParams.ClientIDs) {
47
+ body.RetrieveRequestMsg.RetrieveRequest.ClientIDs = requestParams.clientIDs;
48
+ }
49
+ // filter can be simple or complex and has three properties leftOperand, rightOperand, and operator
50
+ if (requestParams.filter) {
51
+ body.RetrieveRequestMsg.RetrieveRequest.Filter = _parseFilter(requestParams.filter);
52
+ }
53
+ if (requestParams.QueryAllAccounts) {
54
+ body.RetrieveRequestMsg.RetrieveRequest.QueryAllAccounts = true;
55
+ }
56
+ if (requestParams.continueRequest) {
57
+ body.RetrieveRequestMsg.RetrieveRequest.ContinueRequest =
58
+ requestParams.continueRequest;
59
+ }
45
60
  }
46
61
 
47
- if (options.queryAllAccounts) {
48
- body.RetrieveRequestMsg.RetrieveRequest.QuertAllAccounts = true;
62
+ return _apiRequest(
63
+ this.auth,
64
+ {
65
+ action: 'Retrieve',
66
+ req: body,
67
+ key: 'RetrieveResponseMsg',
68
+ },
69
+ 1
70
+ );
71
+ }
72
+ /**
73
+ * Method used to retrieve all data via SOAP API
74
+ * @param {string} type -SOAP Object type
75
+ * @param {Array<String>} props - Properties which should be retrieved
76
+ * @param {Object} [requestParams] - additional RetrieveRequest parameters, for example filter or options
77
+ * @returns {Promise<Object>} SOAP object converted from XML
78
+ */
79
+ async retrieveBulk(type, props, requestParams) {
80
+ let status;
81
+ let resultsBulk;
82
+ do {
83
+ const resultsBatch = await this.retrieve(type, props, requestParams);
84
+ if (resultsBulk) {
85
+ // once first batch is done, the follow just add to result payload
86
+ resultsBulk.Results.push(...resultsBatch.Results);
87
+ } else {
88
+ resultsBulk = resultsBatch;
89
+ }
90
+ status = resultsBatch.OverallStatus;
91
+ if (status === 'MoreDataAvailable') {
92
+ requestParams.continueRequest = resultsBatch.RequestID;
93
+ if (this.eventHandlers && this.eventHandlers.onLoop) {
94
+ this.eventHandlers.onLoop(type, resultsBulk);
95
+ }
96
+ }
97
+ } while (status === 'MoreDataAvailable');
98
+ return resultsBulk;
99
+ }
100
+ /**
101
+ * Method used to create data via SOAP API
102
+ * @param {string} type -SOAP Object type
103
+ * @param {Array<String>} props - Properties which should be created
104
+ * @param {Object} options - configuration of the request
105
+ * @returns {Promise<Object>} SOAP object converted from XML
106
+ */
107
+ create(type, props, options) {
108
+ const body = {
109
+ CreateRequest: {
110
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
111
+ Options: options,
112
+ Objects: props,
113
+ },
114
+ };
115
+ body.CreateRequest.Objects['@_xsi:type'] = type;
116
+ validateOptions(options);
117
+ return _apiRequest(
118
+ this.auth,
119
+ {
120
+ action: 'Create',
121
+ req: body,
122
+ key: 'CreateResponse',
123
+ },
124
+ 1
125
+ );
126
+ }
127
+ /**
128
+ * Method used to update data via SOAP API
129
+ * @param {string} type -SOAP Object type
130
+ * @param {Array<String>} props - Properties which should be updated
131
+ * @param {Object} options - configuration of the request
132
+ * @returns {Promise<Object>} SOAP object converted from XML
133
+ */
134
+ update(type, props, options) {
135
+ const body = {
136
+ UpdateRequest: {
137
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
138
+ Options: options,
139
+ Objects: props,
140
+ },
141
+ };
142
+ body.UpdateRequest.Objects['@_xsi:type'] = type;
143
+ validateOptions(options);
144
+ return _apiRequest(
145
+ this.auth,
146
+ {
147
+ action: 'Update',
148
+ req: body,
149
+ key: 'UpdateResponse',
150
+ },
151
+ 1
152
+ );
153
+ }
154
+ /**
155
+ * Method used to delete data via SOAP API
156
+ * @param {string} type -SOAP Object type
157
+ * @param {Array<String>} props - Properties which should be retrieved
158
+ * @param {Object} options - configuration of the request
159
+ * @returns {Promise<Object>} SOAP object converted from XML
160
+ */
161
+ delete(type, props, options) {
162
+ const body = {
163
+ DeleteRequest: {
164
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
165
+ Options: options,
166
+ Objects: props,
167
+ },
168
+ };
169
+ body.DeleteRequest.Objects['@_xsi:type'] = type;
170
+ validateOptions(options);
171
+ return _apiRequest(
172
+ this.auth,
173
+ {
174
+ action: 'Delete',
175
+ req: body,
176
+ key: 'DeleteResponse',
177
+ },
178
+ 1
179
+ );
180
+ }
181
+ /**
182
+ * Method used to schedule data via SOAP API
183
+ * @param {string} type -SOAP Object type
184
+ * @param {Object} schedule -object for what the schedule should be
185
+ * @param {Array|Object} interactions - Object or array of interactions
186
+ * @param {string} action - type of schedul
187
+ * @param {Object} options - configuration of the request
188
+ * @returns {Promise<Object>} SOAP object converted from XML
189
+ */
190
+ schedule(type, schedule, interactions, action, options) {
191
+ const body = {
192
+ ScheduleRequestMsg: {
193
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
194
+ Action: action,
195
+ Options: options,
196
+ Schedule: schedule,
197
+ Interactions: interactions,
198
+ },
199
+ };
200
+ if (Array.isArray(body.ScheduleRequestMsg.Interactions)) {
201
+ body.ScheduleRequestMsg.Interactions = body.ScheduleRequestMsg.Interactions.map((i) => {
202
+ i.Interaction['@_xsi:type'] = type;
203
+ return i;
204
+ });
205
+ } else if (isObject(body.ScheduleRequestMsg.Interactions)) {
206
+ body.ScheduleRequestMsg.Interactions.Interaction['@_xsi:type'] = type;
207
+ } else {
208
+ throw new TypeError('Interactions must be of Array or Object Type');
49
209
  }
50
- // TODO continueRequest + Pagination
51
- return _apiRequest(this.auth, {
52
- action: 'Retrieve',
53
- req: body,
54
- key: 'RetrieveResponseMsg',
55
- retry: true,
56
- });
210
+ validateOptions(options);
211
+ return _apiRequest(
212
+ this.auth,
213
+ {
214
+ action: 'Schedule',
215
+ req: body,
216
+ key: 'ScheduleResponse',
217
+ },
218
+ 1
219
+ );
220
+ }
221
+ /**
222
+ * Method used to describe metadata via SOAP API
223
+ * @param {string} type -SOAP Object type
224
+ * @returns {Promise<Object>} SOAP object converted from XML
225
+ */
226
+ describe(type) {
227
+ return _apiRequest(
228
+ this.auth,
229
+ {
230
+ action: 'Describe',
231
+ req: {
232
+ DefinitionRequestMsg: {
233
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
234
+ DescribeRequests: {
235
+ ObjectDefinitionRequest: {
236
+ ObjectType: type,
237
+ },
238
+ },
239
+ },
240
+ },
241
+ key: 'DefinitionResponseMsg',
242
+ },
243
+ 1
244
+ );
245
+ }
246
+ /**
247
+ * Method used to execute data via SOAP API
248
+ * @param {string} type -SOAP Object type
249
+ * @param {Array<String>} props - Properties which should be retrieved
250
+ * @returns {Promise<Object>} SOAP object converted from XML
251
+ */
252
+ execute(type, props) {
253
+ return _apiRequest(
254
+ this.auth,
255
+ {
256
+ action: 'Execute',
257
+ req: {
258
+ ExecuteRequestMsg: {
259
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
260
+ Requests: {
261
+ Name: type,
262
+ Parameters: props,
263
+ },
264
+ },
265
+ },
266
+ key: 'ExecuteResponseMsg',
267
+ },
268
+ 1
269
+ );
270
+ }
271
+ /**
272
+ * Method used to execute data via SOAP API
273
+ * @param {string} type -SOAP Object type
274
+ * @param {Array<String>} props - Properties which should be retrieved
275
+ * @returns {Promise<Object>} SOAP object converted from XML
276
+ */
277
+ perform(type) {
278
+ return _apiRequest(
279
+ this.auth,
280
+ {
281
+ action: 'perform',
282
+ req: {
283
+ PerformRequestMsg: {
284
+ '@_xmlns': 'http://exacttarget.com/wsdl/partnerAPI',
285
+ Action: 'start',
286
+ Definitions: [
287
+ {
288
+ Definition: {
289
+ '@_xsi:type': type,
290
+ },
291
+ },
292
+ ],
293
+ },
294
+ },
295
+ key: 'PerformResponseMsg',
296
+ },
297
+ 1
298
+ );
57
299
  }
58
300
  };
59
301
  /**
60
302
  * Method to build the payload then conver to XML
61
303
  * @param {Object} request Object form of the payload
62
- * @param {String} token access token for authentication
304
+ * @param {string} token access token for authentication
63
305
  * @return {Promise<String>} XML string payload
64
306
  */
65
307
  function _buildEnvelope(request, token) {
66
- const builder = new xml2js.Builder({
67
- rootName: 'Envelope',
68
- headless: true,
69
- });
70
- return builder.buildObject({
71
- Body: request,
72
- $: {
73
- xmlns: 'http://schemas.xmlsoap.org/soap/envelope/',
74
- 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
75
- },
76
- Header: {
77
- fueloauth: {
78
- $: {
79
- xmlns: 'http://exacttarget.com',
308
+ const jsonToXml = new JsonToXml({ ignoreAttributes: false });
309
+ return jsonToXml.parse({
310
+ Envelope: {
311
+ Body: request,
312
+ '@_xmlns': 'http://schemas.xmlsoap.org/soap/envelope/',
313
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
314
+ Header: {
315
+ fueloauth: {
316
+ '@_xmlns': 'http://exacttarget.com',
317
+ '#text': token,
80
318
  },
81
- _: token,
82
319
  },
83
320
  },
84
321
  });
@@ -110,25 +347,18 @@ function _parseFilter(filter) {
110
347
  break;
111
348
  }
112
349
 
113
- obj.$ = { 'xsi:type': filterType + 'FilterPart' };
350
+ obj['@_xsi:type'] = filterType + 'FilterPart';
114
351
 
115
352
  return obj;
116
353
  }
117
354
  /**
118
355
  * Method to parse the XML response
119
- * @param {String} body payload in XML format
120
- * @param {String} key key of the expected response body
356
+ * @param {string} body payload in XML format
357
+ * @param {string} key key of the expected response body
121
358
  * @return {Promise<Object|Error>} Result of request in Object format
122
359
  */
123
360
  async function _parseResponse(body, key) {
124
- const parser = new xml2js.Parser({
125
- trim: true,
126
- normalize: true,
127
- explicitArray: false,
128
- ignoreAttrs: true,
129
- });
130
-
131
- const payload = await parser.parseStringPromise(body);
361
+ const payload = xmlToJson.parse(body);
132
362
  const soapBody = payload['soap:Envelope']['soap:Body'];
133
363
  // checks errors in Body Fault
134
364
  if (soapBody['soap:Fault']) {
@@ -151,8 +381,13 @@ async function _parseResponse(body, key) {
151
381
  soapError.errorPropagatedFrom = key;
152
382
  throw soapError;
153
383
  }
384
+ // These should always be run no matter the execution
385
+ // Results should always be an array
386
+ if (isObject(soapBody[key].Results)) {
387
+ soapBody[key].Results = [soapBody[key].Results];
388
+ }
154
389
  // retrieve parse
155
- if (key === 'RetrieveResponseMsg') {
390
+ if (['CreateResponse', 'RetrieveResponseMsg', 'UpdateResponse'].includes(key)) {
156
391
  if (
157
392
  soapBody[key].OverallStatus === 'OK' ||
158
393
  soapBody[key].OverallStatus === 'MoreDataAvailable'
@@ -160,9 +395,12 @@ async function _parseResponse(body, key) {
160
395
  return soapBody[key];
161
396
  } else {
162
397
  // This is an error
163
- const soapError = new Error(soapBody[key].OverallStatus.split(':')[1].trim());
398
+ const errorType = soapBody[key].OverallStatus.includes(':')
399
+ ? soapBody[key].OverallStatus.split(':')[1].trim()
400
+ : soapBody[key].OverallStatus;
401
+ const soapError = new Error(errorType);
164
402
  soapError.requestId = soapBody[key].RequestID;
165
- soapError.errorPropagatedFrom = 'Retrieve Response';
403
+ soapError.errorPropagatedFrom = key;
166
404
  throw soapError;
167
405
  }
168
406
  }
@@ -174,9 +412,10 @@ async function _parseResponse(body, key) {
174
412
  * Method that makes the api request
175
413
  * @param {Object} auth - Auth Object used to make request
176
414
  * @param {Object} options configuration for the request including body
415
+ * @param {number} remainingAttempts number of times this request should be reattempted in case of error
177
416
  * @returns {Promise<Object>} Results from the SOAP request in Object format
178
417
  */
179
- async function _apiRequest(auth, options) {
418
+ async function _apiRequest(auth, options, remainingAttempts) {
180
419
  if (!isObject(options)) {
181
420
  throw new TypeError('options argument is required');
182
421
  }
@@ -192,10 +431,40 @@ async function _apiRequest(auth, options) {
192
431
  },
193
432
  data: _buildEnvelope(options.req, auth.options.access_token),
194
433
  };
434
+
195
435
  const res = await axios(requestOptions);
196
436
  return _parseResponse(res.data, options.key);
197
437
  } catch (ex) {
198
- console.error(ex.message, ex.response);
199
- console.error(options);
438
+ if (ex.response && [404, 596].includes(Number(ex.response.status)) && remainingAttempts) {
439
+ // force refresh due to url related issue
440
+ await auth.getAccessToken(true);
441
+ return _apiRequest(auth, options, remainingAttempts--);
442
+ }
443
+ throw ex;
444
+ }
445
+ }
446
+ /**
447
+ * Method checks options object for validity
448
+ * @param {Object} options configuration for the request including body
449
+ * @param {Array<String>} additional - additional keys which are acceptable
450
+ * @returns {Void} throws error if not supported
451
+ */
452
+ function validateOptions(options, additional) {
453
+ additional = additional || [];
454
+ const defaultOptions = [
455
+ 'CallsInConversation',
456
+ 'Client',
457
+ 'ConversationID',
458
+ 'Priority',
459
+ 'RequestType',
460
+ 'SaveOptions',
461
+ 'ScheduledTime',
462
+ 'SendResponseTo',
463
+ 'SequenceCode',
464
+ ];
465
+ for (const key in options) {
466
+ if (!defaultOptions.concat(additional).includes(key)) {
467
+ throw new Error(`${key} is not a supported Option`);
468
+ }
200
469
  }
201
470
  }
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "sfmc-sdk",
3
- "version": "0.0.2",
3
+ "version": "0.0.6",
4
4
  "description": "Libarary to simplify SFMC requests with updated dependencies and less overhead",
5
5
  "main": "./lib/index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/DougMidgley/SFMC-SDK.git"
12
+ },
9
13
  "author": "Doug Midgley <douglasmidgley@gmail.com>",
10
14
  "license": "BSD-3-Clause",
11
15
  "dependencies": {
12
- "axios": "^0.21.1",
13
- "xml2js": "^0.4.23"
16
+ "axios": "^0.24.0",
17
+ "fast-xml-parser": "3.21.1",
18
+ "p-limit": "4.0.0"
14
19
  },
15
20
  "keywords": [
16
21
  "fuel",