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.
- package/.github/workflows/publish.yml +0 -13
- package/CHANGELOG.md +7 -0
- package/PULL_REQUEST_TEMPLATE.md +1 -4
- package/README.md +38 -10
- package/lib/auth.js +23 -30
- package/lib/index.js +5 -4
- package/lib/rest.js +101 -27
- package/lib/soap.js +329 -60
- package/package.json +8 -3
|
@@ -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
|
package/PULL_REQUEST_TEMPLATE.md
CHANGED
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- [ ] Documentation update
|
|
6
6
|
- [ ] Bug fix
|
|
7
|
-
- [ ]
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
45
|
+
SOAP currently only supports all the standard SOAP action types. Some examples below
|
|
40
46
|
|
|
41
47
|
```javascript
|
|
42
|
-
const
|
|
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 {
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
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 {
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
* @param {
|
|
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(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
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
|
-
|
|
124
|
-
|
|
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
|
|
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 {
|
|
21
|
+
* @param {string} type -SOAP Object type
|
|
19
22
|
* @param {Array<String>} props - Properties which should be retrieved
|
|
20
|
-
* @param {Object}
|
|
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,
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
return _apiRequest(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 {
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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 {
|
|
120
|
-
* @param {
|
|
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
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
-
|
|
199
|
-
|
|
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.
|
|
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.
|
|
13
|
-
"
|
|
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",
|