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 +13 -35
- package/.github/workflows/prvalidation.yml +2 -0
- package/.github/workflows/release.yml +1 -1
- package/.vscode/settings.json +3 -0
- package/README.md +25 -15
- package/lib/auth.js +35 -43
- package/lib/index.js +3 -2
- package/lib/rest.js +42 -31
- package/lib/soap.js +411 -337
- package/lib/util.js +3 -2
- package/package.json +18 -3
- package/test/auth.test.js +121 -0
- package/test/resources/auth.json +32 -0
- package/test/resources/rest.json +373 -0
- package/test/resources/soap.json +586 -0
- package/test/rest.test.js +235 -0
- package/test/soap.test.js +281 -0
- package/test/utils.js +23 -0
package/.eslintrc.json
CHANGED
|
@@ -4,44 +4,22 @@
|
|
|
4
4
|
"node": true,
|
|
5
5
|
"mocha": true
|
|
6
6
|
},
|
|
7
|
-
"extends": [
|
|
8
|
-
|
|
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":
|
|
22
|
+
"ecmaVersion": 2020,
|
|
15
23
|
"sourceType": "module"
|
|
16
|
-
}
|
|
17
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
37
|
+
git push origin main
|
|
38
38
|
- uses: eregon/publish-release@v1
|
|
39
39
|
env:
|
|
40
40
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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
|
-
|
|
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'],
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
*
|
|
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 {
|
|
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 {
|
|
60
|
-
* @
|
|
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 {
|
|
74
|
-
* @
|
|
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 {
|
|
92
|
-
* @
|
|
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
|
-
*
|
|
9
|
-
* @param {
|
|
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
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
* @param {
|
|
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<
|
|
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
|
-
* @
|
|
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
|
|
45
|
-
queryParams.set('$pageSize', Number(
|
|
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
|
-
|
|
66
|
+
shouldPaginate = false;
|
|
64
67
|
} else {
|
|
65
68
|
page++;
|
|
66
|
-
|
|
69
|
+
shouldPaginate = true;
|
|
67
70
|
}
|
|
68
|
-
} while (
|
|
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
|
-
*
|
|
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 {
|
|
98
|
-
* @returns {Promise<
|
|
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 {
|
|
113
|
-
* @returns {Promise<
|
|
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 {
|
|
128
|
-
* @returns {Promise<
|
|
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<
|
|
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
|
-
*
|
|
159
|
-
* @
|
|
168
|
+
*
|
|
169
|
+
* @param {object} options API request opptions
|
|
160
170
|
*/
|
|
161
171
|
function _checkPayload(options) {
|
|
162
172
|
if (!isObject(options.data)) {
|
|
163
|
-
throw new
|
|
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
|
-
*
|
|
169
|
-
* @param {
|
|
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<
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
205
|
+
remainingAttempts--;
|
|
206
|
+
return _apiRequest(auth, options, remainingAttempts);
|
|
196
207
|
} else {
|
|
197
208
|
throw ex;
|
|
198
209
|
}
|