@yousolution/node-red-contrib-you-sap-service-layer 0.0.3

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.
@@ -0,0 +1,306 @@
1
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
2
+ const axiosLibrary = require('axios');
3
+ const buildQuery = require('odata-query').default;
4
+
5
+ const thickIdApi = [
6
+ 'AccrualTypes',
7
+ 'AssetClasses',
8
+ 'AssetDepreciationGroups',
9
+ 'AssetGroups',
10
+ 'BankChargesAllocationCodes',
11
+ 'BusinessPartners',
12
+ 'CampaignResponseType',
13
+ 'CashDiscounts',
14
+ 'ChartOfAccounts',
15
+ 'ChooseFromList',
16
+ 'ContractTemplates',
17
+ 'CostCenterTypes',
18
+ 'CostElements',
19
+ 'Countries',
20
+ 'CreditCardPayments',
21
+ 'Currencies',
22
+ 'CustomsDeclaration',
23
+ 'CycleCountDeterminations',
24
+ 'DeductionTaxSubGroups',
25
+ 'DepreciationAreas',
26
+ 'DepreciationTypePools',
27
+ 'DepreciationTypes',
28
+ 'DistributionRules',
29
+ 'DunningTerms',
30
+ 'EmailGroups',
31
+ 'EmployeeIDType',
32
+ 'FAAccountDeterminations',
33
+ 'FactoringIndicators',
34
+ 'FiscalPrinter',
35
+ 'ItemImages',
36
+ 'Items',
37
+ 'JournalEntryDocumentTypes',
38
+ 'KPIs',
39
+ 'LandedCostsCodes',
40
+ 'LocalEra',
41
+ 'MobileAddOnSetting',
42
+ 'NFModels',
43
+ 'ProductTrees',
44
+ 'ProfitCenters',
45
+ 'Projects',
46
+ 'Queue',
47
+ 'ReportTypes',
48
+ 'Resources',
49
+ 'SalesTaxCodes',
50
+ 'TargetGroups',
51
+ 'TaxInvoiceReport',
52
+ 'TransactionCodes',
53
+ 'UserDefaultGroups',
54
+ 'UserObjectsMD',
55
+ 'UserPermissionTree',
56
+ 'UserTablesMD',
57
+ 'VatGroups',
58
+ 'Warehouses',
59
+ 'WithholdingTaxCodes',
60
+ 'WizardPaymentMethods',
61
+ 'UDT',
62
+ ];
63
+
64
+ async function login(node, idAuth) {
65
+ const flowContext = node.context().flow;
66
+
67
+ const host = flowContext.get(`_YOU_SapServiceLayer_${idAuth}.host`);
68
+ const port = flowContext.get(`_YOU_SapServiceLayer_${idAuth}.port`);
69
+ const version = flowContext.get(`_YOU_SapServiceLayer_${idAuth}.version`);
70
+
71
+ const url = `https://${host}:${port}/b1s/${version}/Login`;
72
+
73
+ const credentials = flowContext.get(`_YOU_SapServiceLayer_${idAuth}.credentials`);
74
+ const dataString = JSON.stringify(credentials);
75
+
76
+ const options = {
77
+ method: 'POST',
78
+ url: url,
79
+ rejectUnauthorized: false,
80
+ data: credentials,
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ 'Content-Length': dataString.length,
84
+ },
85
+ };
86
+
87
+ // try {
88
+ return await axiosLibrary(options);
89
+ // } catch (error) {
90
+ // throw Error(`ERROR FUNCTION LOGIN: ${error}`);
91
+ // }
92
+ }
93
+
94
+ async function sendRequest({ node, msg, config, axios, login, options }) {
95
+ if (!node || !msg || !config || !axios || !login) {
96
+ const missingParams = [];
97
+ node ? null : missingParams.push('node');
98
+ msg ? null : missingParams.push('msg');
99
+ config ? null : missingParams.push('config');
100
+ axios ? null : missingParams.push('axios');
101
+ login ? null : missingParams.push('login');
102
+ throw new Error(`Missing mandatory params: ${missingParams.join(',')}.`);
103
+ }
104
+ let requestOptions = generateRequest(node, msg, config, options);
105
+ try {
106
+ return await axios(requestOptions.axiosOptions);
107
+ } catch (error) {
108
+ // Refresh headers re-login
109
+ if (error.response && (error.response.status == 401 || error.response.status == 301)) {
110
+ const flowContext = node.context().flow;
111
+ // try {
112
+ // update cookies for session timeout
113
+ const result = await login(node, requestOptions.idAuthNode);
114
+ flowContext.set(`_YOU_SapServiceLayer_${requestOptions.idAuthNode}.headers`, result.headers['set-cookie']);
115
+
116
+ try {
117
+ const headers = flowContext.get(`_YOU_SapServiceLayer_${requestOptions.idAuthNode}.headers`).join(';');
118
+
119
+ requestOptions.axiosOptions.headers.Cookie = headers;
120
+
121
+ return await axios(requestOptions.axiosOptions);
122
+ } catch (error) {
123
+ if (error.response && error.response.data) {
124
+ msg.statusCode = error.response.status;
125
+ msg.payload = error.response.data;
126
+ msg.requestUrl = requestOptions.axiosOptions.url;
127
+ node.send(msg);
128
+ throw new Error(JSON.stringify(error.response.data));
129
+ }
130
+ throw error;
131
+ }
132
+ // }
133
+ }
134
+ if (error.response && error.response.data) {
135
+ msg.statusCode = error.response.status;
136
+ msg.payload = error.response.data;
137
+ msg.requestUrl = requestOptions.axiosOptions.url;
138
+ node.send(msg);
139
+ throw new Error(JSON.stringify(error.response.data));
140
+ }
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ function generateRequest(node, msg, config, options) {
146
+ options = options || {
147
+ hasRawQuery: false,
148
+ hasEntityId: false,
149
+ isClose: false,
150
+ isCrossJoin: false,
151
+ method: 'GET',
152
+ data: null,
153
+ };
154
+ // if (!options.typeOfNode) {
155
+ // throw new Error('Missing type of node');
156
+ // }
157
+ options.hasRawQuery = options.hasRawQuery || false;
158
+ options.method = options.method || 'GET';
159
+ options.data = options.data || null;
160
+ options.hasEntityId = options.hasEntityId || false;
161
+ options.isClose = options.isClose || false;
162
+ options.isCrossJoin = options.isCrossJoin || false;
163
+
164
+ const { idAuthNode, host, port, version, cookies } = getSapParams(node, msg, config);
165
+
166
+ let rawQuery = null;
167
+ if (options.hasRawQuery) {
168
+ try {
169
+ rawQuery = eval(config.query);
170
+ } catch (error) {
171
+ throw new Error('Query editor error');
172
+ }
173
+ }
174
+
175
+ let entity = config.entity;
176
+ if (!entity) {
177
+ throw new Error('Missing entity');
178
+ }
179
+
180
+ if (entity == 'UDO') {
181
+ entity = config.udo;
182
+ }
183
+
184
+ if (entity == 'UDT') {
185
+ entity = config.udt;
186
+ }
187
+
188
+ if (entity == 'script') {
189
+ const partnerName = config.partnerName;
190
+ const scriptName = config.scriptName;
191
+ url = `https://${host}:${port}/b1s/${version}/${entity}/${partnerName}/${scriptName}`;
192
+ }
193
+
194
+ let url;
195
+
196
+ const odataNextLink = msg[config.nextLink];
197
+
198
+ if (!odataNextLink) {
199
+ url = `https://${host}:${port}/b1s/${version}/${entity}`;
200
+ }
201
+
202
+ if (options.isCrossJoin) {
203
+ url = `https://${host}:${port}/b1s/${version}/$crossjoin(${entity})`;
204
+ }
205
+
206
+ if (odataNextLink) {
207
+ url = `https://${host}:${port}/b1s/${version}/${odataNextLink}`;
208
+ }
209
+ if (options.isClose && !options.hasEntityId) {
210
+ throw new Error(`The options are not correct. If 'isClose' is true then 'hasEntityId' must be true.`);
211
+ }
212
+
213
+ if (options.hasEntityId) {
214
+ let entityId = msg[config.entityId];
215
+ if (!entityId && config.entity != 'UDO' && config.entity != 'UDT') {
216
+ throw new Error('Missing entityId');
217
+ }
218
+ const docEntry = msg[config.docEntry];
219
+ if (config.entity == 'UDO') {
220
+ if (!docEntry) {
221
+ throw new Error('Missing docEntry');
222
+ }
223
+ entityId = docEntry;
224
+ }
225
+
226
+ const code = msg[config.code];
227
+ if (config.entity == 'UDT') {
228
+ if (!code) {
229
+ throw new Error('Missing Code');
230
+ }
231
+ entityId = code;
232
+ }
233
+
234
+ if (thickIdApi.includes(entity) || config.entity === 'UDT') {
235
+ url = `https://${host}:${port}/b1s/${version}/${entity}('${entityId}')`;
236
+ } else {
237
+ url = `https://${host}:${port}/b1s/${version}/${entity}(${entityId})`;
238
+ }
239
+
240
+ if (options.isClose) {
241
+ url += `/Close`;
242
+ }
243
+ }
244
+
245
+ if (rawQuery && !odataNextLink) {
246
+ const urlOdata = buildQuery(rawQuery);
247
+ msg.odata = urlOdata;
248
+ url = `${url}${urlOdata}`;
249
+ }
250
+
251
+ // const cookies = flowContext.get(`_YOU_SapServiceLayer_${idAuthNode}.headers`).join(';');
252
+ const headers = { ...msg[config.headers], Cookie: cookies };
253
+
254
+ let axiosOptions = {
255
+ method: options.method,
256
+ url: url,
257
+ rejectUnauthorized: false,
258
+ withCredentials: true,
259
+ headers: headers,
260
+ };
261
+
262
+ if (options.data) {
263
+ axiosOptions = { ...axiosOptions, ...{ data: options.data } };
264
+ }
265
+
266
+ return {
267
+ axiosOptions: axiosOptions,
268
+ idAuthNode: idAuthNode,
269
+ };
270
+ }
271
+
272
+ function getSapParams(node, msg) {
273
+ try {
274
+ const flowContext = node.context().flow;
275
+
276
+ const idAuthNode = msg._YOU_SapServiceLayer.idAuth;
277
+ const host = flowContext.get(`_YOU_SapServiceLayer_${idAuthNode}.host`);
278
+ const port = flowContext.get(`_YOU_SapServiceLayer_${idAuthNode}.port`);
279
+ const version = flowContext.get(`_YOU_SapServiceLayer_${idAuthNode}.version`);
280
+
281
+ // if (!flowContext.get(`_YOU_SapServiceLayer_${idAuthNode}.headers`)) {
282
+ // throw new Error('Authentication failed');
283
+ // }
284
+ const cookies = flowContext.get(`_YOU_SapServiceLayer_${idAuthNode}.headers`).join(';');
285
+
286
+ return { idAuthNode: idAuthNode, host: host, port: port, version: version, cookies: cookies };
287
+ } catch (error) {
288
+ throw new Error('Authentication failed');
289
+ }
290
+ }
291
+
292
+ module.exports = {
293
+ login: login,
294
+ generateRequest: generateRequest,
295
+ sendRequest: sendRequest,
296
+ thickIdApi: thickIdApi,
297
+ };
298
+ // if (process.env.NODE_ENV === 'test') {
299
+ // console.log('TEST');
300
+ // module.exports = {
301
+ // login: login,
302
+ // generateRequest: generateRequest,
303
+ // sendRequest: sendRequest,
304
+ // thickIdApi: thickIdApi,
305
+ // };
306
+ // }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@yousolution/node-red-contrib-you-sap-service-layer",
3
+ "version": "0.0.3",
4
+ "description": "Unofficial module SAP Service Layer for NODE-RED",
5
+ "license": "MIT",
6
+ "scripts": {
7
+ "update": "npm pack && mv yousolution-node-red-contrib-you-sap-service-layer-$npm_package_version.tgz ./data && cd data && npm i yousolution-node-red-contrib-you-sap-service-layer-$npm_package_version.tgz && docker-compose restart",
8
+ "test": "mocha 'test/**/*.spec.js'",
9
+ "coverage": "nyc npm run test"
10
+ },
11
+ "keywords": [
12
+ "node-red",
13
+ "SAP",
14
+ "service layer",
15
+ "API",
16
+ "ERP",
17
+ "integration",
18
+ "youSolution.Cloud"
19
+ ],
20
+ "author": "Andrea Trentin <andrea.trentin@yousolution.cloud>",
21
+ "node-red": {
22
+ "nodes": {
23
+ "authenticateSap": "/nodes/authenticateSap.js",
24
+ "listSap": "/nodes/listSap.js",
25
+ "createSap": "/nodes/createSap.js",
26
+ "deleteSap": "/nodes/deleteSap.js",
27
+ "getSap": "/nodes/getSap.js",
28
+ "patchSap": "/nodes/patchSap.js",
29
+ "closeSap": "/nodes/closeSap.js",
30
+ "crossJoinSap": "/nodes/crossJoinSap.js",
31
+ "nextLink": "/nodes/nextLink.js"
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "axios": "^0.23.0",
36
+ "odata-query": "^6.7.1"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/yousolution-cloud/node-red-contrib-you-sap-service-layer.git"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "devDependencies": {
46
+ "@types/chai": "^4.3.0",
47
+ "@types/node-red-node-test-helper": "^0.2.2",
48
+ "@types/sinon": "^10.0.6",
49
+ "chai": "^4.3.4",
50
+ "mocha": "^9.1.3",
51
+ "node-red": "^2.1.4",
52
+ "node-red-node-test-helper": "^0.2.7",
53
+ "nyc": "^15.1.0",
54
+ "sinon": "^12.0.1"
55
+ }
56
+ }
@@ -0,0 +1,261 @@
1
+ const should = require('should');
2
+ const helper = require('node-red-node-test-helper');
3
+ const authenticateSap = require('../nodes/authenticateSap');
4
+ const Context = require('../node_modules/./@node-red/runtime/lib/nodes/context/index');
5
+ const sinon = require('sinon');
6
+ const Support = require('../nodes/support');
7
+
8
+ helper.init(require.resolve('node-red'));
9
+
10
+ describe('authenticateSap Node', () => {
11
+ beforeEach((done) => {
12
+ helper.startServer(done);
13
+ });
14
+
15
+ function initContext(done) {
16
+ Context.init({
17
+ contextStorage: {
18
+ memory0: {
19
+ module: 'memory',
20
+ },
21
+ memory1: {
22
+ module: 'memory',
23
+ },
24
+ },
25
+ });
26
+ Context.load().then(function () {
27
+ done();
28
+ });
29
+ }
30
+
31
+ afterEach((done) => {
32
+ helper
33
+ .unload()
34
+ .then(function () {
35
+ return Context.clean({ allNodes: {} });
36
+ })
37
+ .then(function () {
38
+ return Context.close();
39
+ })
40
+ .then(function () {
41
+ helper.stopServer(done);
42
+ });
43
+
44
+ // Restore the default sandbox here
45
+ sinon.restore();
46
+
47
+ // helper.unload();
48
+ // helper.stopServer(done);
49
+ });
50
+
51
+ it('should be loaded', (done) => {
52
+ const flow = [
53
+ {
54
+ id: 'n1',
55
+ type: 'authenticateSap',
56
+ name: 'authenticateSap',
57
+ wires: [['n2']],
58
+ z: 'flow',
59
+ rules: [{ t: 'set', p: 'payload', to: '#:(memory1)::flowValue', tot: 'flow' }],
60
+ },
61
+ ];
62
+
63
+ helper.load(authenticateSap, flow, () => {
64
+ initContext(function () {
65
+ const n1 = helper.getNode('n1');
66
+
67
+ // console.log(helper.log().args);
68
+
69
+ // n1.context().flow.set('A', 1, 'memory1', function (error) {
70
+ // console.log(error);
71
+ // });
72
+
73
+ // console.log(n1.context().flow.get('A', 'memory1'));
74
+ // console.log('- - - B- - - -');
75
+
76
+ // n1.id
77
+ // flow.push({
78
+ // [`_YOU_SapServiceLayer_${n1.id}.headers`]:
79
+ // })
80
+
81
+ try {
82
+ // n1.status.calledWith({ fill: 'gray', shape: 'ring', text: 'Set credentials1' });
83
+ // should.equal(n1.status.calledWith({ fill: 'gray', shape: 'ring', text: 'Missing credentials' }), true);
84
+ n1.should.have.property('name', 'authenticateSap');
85
+ done();
86
+ } catch (err) {
87
+ done(err);
88
+ }
89
+ });
90
+ });
91
+ });
92
+
93
+ it('should no have credentials', (done) => {
94
+ const flow = [
95
+ {
96
+ id: 'n1',
97
+ type: 'authenticateSap',
98
+ name: 'authenticateSap',
99
+ wires: [['n2']],
100
+ z: 'flow',
101
+ rules: [{ t: 'set', p: 'payload', to: '#:(memory1)::flowValue', tot: 'flow' }],
102
+ },
103
+ ];
104
+ helper.load(authenticateSap, flow, () => {
105
+ initContext(function () {
106
+ const n1 = helper.getNode('n1');
107
+
108
+ try {
109
+ should.equal(n1.status.calledWith({ fill: 'gray', shape: 'ring', text: 'Missing credentials' }), true);
110
+ n1.should.have.property('name', 'authenticateSap');
111
+ done();
112
+ } catch (err) {
113
+ done(err);
114
+ }
115
+ });
116
+ });
117
+ });
118
+
119
+ it('should no have credentials when send message', (done) => {
120
+ const flow = [
121
+ {
122
+ id: 'n1',
123
+ type: 'authenticateSap',
124
+ name: 'authenticateSap',
125
+ wires: [['n2']],
126
+ z: 'flow',
127
+ rules: [{ t: 'set', p: 'payload', to: '#:(memory1)::flowValue', tot: 'flow' }],
128
+ },
129
+ ];
130
+ helper.load(authenticateSap, flow, () => {
131
+ const n1 = helper.getNode('n1');
132
+
133
+ n1.receive({});
134
+
135
+ n1.on('call:error', (error) => {
136
+ try {
137
+ should.equal(n1.status.lastCall.calledWith({ fill: 'red', shape: 'dot', text: 'Missing credentials' }), true);
138
+ error.should.have.property('firstArg', new Error('Missing credentials'));
139
+ done();
140
+ } catch (err) {
141
+ done(err);
142
+ }
143
+ });
144
+ });
145
+ });
146
+
147
+ it('should have without headers connected', (done) => {
148
+ const flow = [
149
+ {
150
+ id: 'n1',
151
+ type: 'authenticateSap',
152
+ name: 'authenticateSap',
153
+ wires: [['n2']],
154
+ z: 'flow',
155
+ rules: [{ t: 'set', p: 'payload', to: '#:(memory1)::flowValue', tot: 'flow' }],
156
+ },
157
+ { id: 'n2', type: 'helper' },
158
+ ];
159
+ helper.load(authenticateSap, flow, () => {
160
+ const n2 = helper.getNode('n2');
161
+ const n1 = helper.getNode('n1');
162
+ n1.credentials.user = 'user';
163
+ n1.credentials.password = 'password';
164
+ n1.credentials.company = 'company';
165
+
166
+ sinon.stub(Support, 'login').resolves({
167
+ headers: {
168
+ 'Content-Type': 'application/json',
169
+ 'Content-Length': 100,
170
+ },
171
+ });
172
+
173
+ n1.receive({});
174
+
175
+ n2.on('input', (msg) => {
176
+ try {
177
+ msg.should.have.property('_msgid');
178
+ done();
179
+ } catch (err) {
180
+ done(err);
181
+ }
182
+ });
183
+ });
184
+ });
185
+
186
+ it('should have with headers connected', (done) => {
187
+ const flow = [
188
+ {
189
+ id: 'n1',
190
+ type: 'authenticateSap',
191
+ name: 'authenticateSap',
192
+ wires: [['n2']],
193
+ z: 'flow',
194
+ rules: [{ t: 'set', p: 'payload', to: '#:(memory1)::flowValue', tot: 'flow' }],
195
+ },
196
+ { id: 'n2', type: 'helper' },
197
+ ];
198
+ helper.load(authenticateSap, flow, () => {
199
+ const n2 = helper.getNode('n2');
200
+ const n1 = helper.getNode('n1');
201
+ n1.credentials.user = 'user';
202
+ n1.credentials.password = 'password';
203
+ n1.credentials.company = 'company';
204
+
205
+ sinon.stub(Support, 'login').resolves({
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ 'Content-Length': 100,
209
+ },
210
+ });
211
+
212
+ n1.context().flow.set(`_YOU_SapServiceLayer_${n1.id}.headers`, true, 'memory1', function (error) {
213
+ // console.log(error);
214
+ });
215
+
216
+ n1.receive({});
217
+
218
+ n2.on('input', (msg) => {
219
+ try {
220
+ msg.should.have.property('_msgid');
221
+ done();
222
+ } catch (err) {
223
+ done(err);
224
+ }
225
+ });
226
+ });
227
+ });
228
+
229
+ it('should have disconnected', (done) => {
230
+ const flow = [
231
+ {
232
+ id: 'n1',
233
+ type: 'authenticateSap',
234
+ name: 'authenticateSap',
235
+ wires: [['n2']],
236
+ z: 'flow',
237
+ rules: [{ t: 'set', p: 'payload', to: '#:(memory1)::flowValue', tot: 'flow' }],
238
+ },
239
+ ];
240
+ helper.load(authenticateSap, flow, () => {
241
+ // const n2 = helper.getNode('n2');
242
+ const n1 = helper.getNode('n1');
243
+ n1.credentials.user = 'user';
244
+ n1.credentials.password = 'password';
245
+ n1.credentials.company = 'company';
246
+
247
+ sinon.stub(Support, 'login').rejects();
248
+
249
+ n1.receive({});
250
+
251
+ n1.on('call:error', (error) => {
252
+ try {
253
+ error.should.have.property('firstArg', new Error('Error'));
254
+ done();
255
+ } catch (err) {
256
+ done(err);
257
+ }
258
+ });
259
+ });
260
+ });
261
+ });