node-red-contrib-teamogy-api 0.0.1

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/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Teamogy API Node
2
+
3
+ Node for connecting to the Teamogy API.
4
+
5
+ ## Connection Configuration
6
+
7
+ You must configure a Connection before using this node.
8
+
9
+ | Parameter | Type | Required | Description |
10
+ |-----------|------|----------|-------------|
11
+ | Name | string | Optional | You can enter a name or it will be generated on first save |
12
+ | Subdomain | string | Required | Enter the name of the subdomain and the domain, in the format: subdomain.domain.com |
13
+ | Unit/Agency | number | Required | Enter the number of unit or agency |
14
+ | Token | string | Required | Enter the generated API token for the given subdomain and Unit/Agency |
15
+ | Req/min | number | Required | Enter the maximum number of API requests per minute for the given subdomain, if the limit is greater than allowed, further requests will be rejected (not processed) |
16
+
17
+ ## Input
18
+
19
+ | Parameter | Type | Required | Description |
20
+ |-----------|------|----------|-------------|
21
+ | Name | string | Optional | You can enter a name for the given node (recommended) |
22
+ | Connection | selection - list | Required | Select the configured connection |
23
+ | Request views | checkbox - yes/no | Required | Check for requests for API views (see description below) |
24
+ | Token | selection - list | Required | Select the desired entity |
25
+ | Req/min | selection - list | Required | Select an available method |
26
+ | Source | selection - list | Required | The parameters or even the request body can be static, i.e. from the Params and Body fields in the form, or dynamic as part of the input message |
27
+ | Source of body | string | Required | The parameter set the source of the body, default is msg.payload |
28
+ | Params | string \| object | Optional | You can enter the params in the case of a static request as a string, in the case of a dynamic also as an object, according to the API specification for the given entity |
29
+ | Body | JSON string \| object | Optional | You can enter the body in the case of a static request as a JSON string, in the case of a dynamic also as an object, you can nest objects and arrays in the body without restrictions according to the API specification for the given entity |
30
+ | Merge | checkbox - yes/no | Optional | Check if you want the output message to contain all returned records. In the background, processing is still taking place according to the Paging settings, but individual messages are not sent to the output, but only the final merged one containing all the returned records. (if not specified the value no is used) |
31
+ | Limit | number | Optional | Limitation of the total number of records in the response (if not specified, the value 0 is used, the value 0 means no limit) |
32
+ | Paging | number | Optional | Division of responses into multiple parts with the number of records returned (if not specified, the value 1000 is used) |
33
+ | Offset | number | Optional | Determining from which records to return, e.g. if you have a total of 100 records and want to return records 50-100, set Offset to 50, if you want to return all records leave the option at 0 (if not specified, the value 0 is used) |
34
+ | Skip | boolean | Optional, in message only | Adding the msg.skip=true will allow the message to pass through to the output without processing |
35
+
36
+ ## Examples
37
+
38
+ ### Static Request Examples
39
+
40
+ Enter the desired values according to the selected entity and method in the Params field. Values for Params are listed on separate lines:
41
+
42
+ ```
43
+ id=3
44
+ registration=12345678
45
+ ```
46
+
47
+ For API views:
48
+
49
+ ```
50
+ columns=firstName,lastName
51
+ externalFilter=id>5
52
+ ```
53
+
54
+ For Body field (JSON):
55
+
56
+ ```json
57
+ {
58
+ "id": 3,
59
+ "registration": 12345678
60
+ }
61
+ ```
62
+
63
+ ### Dynamic Request Examples
64
+
65
+ For API views, as string:
66
+ ```
67
+ id=2&name=John
68
+ ```
69
+
70
+ Or as an object:
71
+ ```javascript
72
+ msg.params.id = 2
73
+ msg.params.name = "John"
74
+ ```
75
+
76
+ For Body, as JSON string:
77
+ ```json
78
+ {"id": 3, "name": "John"}
79
+ ```
80
+
81
+ Or as an object:
82
+ ```javascript
83
+ msg.body.id = 2
84
+ msg.body.name = "John"
85
+ msg.body.address = addresses // array of objects
86
+ ```
87
+
88
+ ## Output
89
+
90
+ | Property | Type | Description |
91
+ |----------|------|-------------|
92
+ | payload | string \| object | The standard output or error of the response |
93
+ | count | number | Number of records in the output |
94
+ | msg.* | string \| object | All properties of the input message |
95
+
96
+ ## References
97
+
98
+ - [Teamogy Flow docs](https://teamogy.com/teamogy-flow) - full description of Teamogy Flow
99
+ - [Teamogy API docs](https://readme.teamogy.com/reference/integration-options) - full description of parameters for `msg.params` and `msg.body` properties, and also options for API views
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "node-red-contrib-teamogy-api",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "keywords": [
9
+ "node-red",
10
+ "teamogy",
11
+ "api"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/teamogy-team/node-red-nodes"
16
+ },
17
+ "author": "",
18
+ "license": "",
19
+ "node-red": {
20
+ "nodes": {
21
+ "teamogy-client": "teamogy-client.js"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "node-fetch": "^3.3.2"
26
+ }
27
+ }
@@ -0,0 +1,596 @@
1
+ <script type="text/javascript">
2
+ let requests = `[
3
+ {"v":"r_companies", "e":"Contacts", "m":["GET","POST","PATCH"]},
4
+ {"v":"r_documents", "e":"Documents", "m":["GET","POST","PATCH"]},
5
+ {"v":"r_jobs", "e":"Jobs", "m":["GET","POST","PATCH"]},
6
+ {"v":"r_tasks", "e":"Tasks", "m":["GET","POST","PATCH"]},
7
+ {"v":"r_time", "e":"Time", "m":["POST"]},
8
+ {"v":"r_users", "e":"Users", "m":["GET","POST"]}
9
+ ]`;
10
+
11
+ let values1 = JSON.parse(requests)
12
+
13
+ let views = `[
14
+ {"v":"v_binders", "e":"Binders", "m":["GET"]},
15
+ {"v":"v_brands", "e":"Brands", "m":["GET"]},
16
+ {"v":"v_cashboxes", "e":"Cashboxes", "m":["GET"]},
17
+ {"v":"v_companies", "e":"Companies", "m":["GET"]},
18
+ {"v":"v_companies-client", "e":"Companies - client", "m":["GET"]},
19
+ {"v":"v_companies-supplier", "e":"Companies - supplier", "m":["GET"]},
20
+ {"v":"v_documents", "e":"Documents", "m":["GET"]},
21
+ {"v":"v_documents-order-purchase", "e":"Documents order - purchase", "m":["GET"]},
22
+ {"v":"v_documents-quotation-sales", "e":"Documents quotation - sales", "m":["GET"]},
23
+ {"v":"v_gems", "e":"Gems", "m":["GET"]},
24
+ {"v":"v_groups", "e":"Groups", "m":["GET"]},
25
+ {"v":"v_internals-purchase", "e":"Internals - purchase", "m":["GET"]},
26
+ {"v":"v_internals-sales", "e":"Internals - sales", "m":["GET"]},
27
+ {"v":"v_intervals-absence", "e":"Intervals - absence", "m":["GET"]},
28
+ {"v":"v_intervals-attendance", "e":"Intervals - attendance", "m":["GET"]},
29
+ {"v":"v_intervals-track", "e":"Intervals - track", "m":["GET"]},
30
+ {"v":"v_invoices-purchase", "e":"Invoices - purchase", "m":["GET"]},
31
+ {"v":"v_invoices-sales", "e":"Invoices - sales", "m":["GET"]},
32
+ {"v":"v_jobs", "e":"Jobs", "m":["GET"]},
33
+ {"v":"v_journal", "e":"Journal", "m":["GET"]},
34
+ {"v":"v_overheads-client", "e":"Overheads - client", "m":["GET"]},
35
+ {"v":"v_overheads-internal", "e":"Overheads. - internal", "m":["GET"]},
36
+ {"v":"v_opportunities", "e":"Opportunities", "m":["GET"]},
37
+ {"v":"v_persons", "e":"Persons", "m":["GET"]},
38
+ {"v":"v_tasks", "e":"Tasks", "m":["GET"]},
39
+ {"v":"v_users", "e":"Users", "m":["GET"]}
40
+ ]`;
41
+
42
+ let values2 = JSON.parse(views)
43
+
44
+ function decodeJwt(token) {
45
+ try {
46
+ return JSON.parse(atob(token.split(".")[1]));
47
+ } catch (e) {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ </script>
53
+
54
+ <script type="text/x-red" data-template-name="teamogy-config">
55
+
56
+ <div class="form-row">
57
+ <label for="node-config-input-name"><i class="fa fa-tag"></i><span>Name</span></label>
58
+ <input type="text" id="node-config-input-name" placeholder="name">
59
+ </div>
60
+
61
+ <div class="form-row">
62
+ <ul style="min-width: 600px; margin-bottom: 20px;" id="config-tabs"></ul>
63
+ </div>
64
+
65
+ <div id="config-tabs-content" style="min-height: 170px;">
66
+
67
+ <div id="config-tab-config" style="display: none;">
68
+
69
+ <div class="form-row">
70
+ <label for="node-config-input-host"><i class="fa fa-server"></i> <span>Domain</span></label>
71
+ <input type="text" id="node-config-input-host" placeholder="subdomain.teamogy.com">
72
+ </div>
73
+
74
+ <div class="form-row">
75
+ <label for="node-config-input-unit"><i class="fa fa-square-o"></i> <span>Unit/Agency</span></label>
76
+ <input type="number" id="node-config-input-unit" min="1" max="100" step="1" placeholder="1" style="width: 20%">
77
+ </div>
78
+
79
+ <div class="form-row">
80
+ <label for="node-config-input-token"><i class="fa fa-lock"></i> <span>Token</span></label>
81
+ <input type="password" id="node-config-input-token" placeholder="token">
82
+ </div>
83
+
84
+ <div class="form-row">
85
+ <label for="node-config-input-tokendata" style="vertical-align: top;"><i class="fa fa-lock"></i> <span>Token data</span></label>
86
+ <label id="node-config-label-tokendata" style="width: 50%"></label>
87
+ <input type="hidden" id="node-config-input-tokendata" placeholder="">
88
+ </div>
89
+
90
+ <div class="form-row">
91
+ <label for="node-config-input-apilimit"><i class="fa fa-step-forward"></i> <span>Req/min</span></label>
92
+ <input type="number" id="node-config-input-apilimit" min="1" max="1000" step="1" placeholder="60" style="width: 20%">
93
+ </div>
94
+
95
+ </div>
96
+
97
+ </div>
98
+ </script>
99
+
100
+ <script type="text/javascript">
101
+ RED.nodes.registerType('teamogy-config', {
102
+ category: "config",
103
+ color: "#fe4c00",
104
+ defaults: {
105
+ name: {
106
+ value: ""
107
+ },
108
+ host: {
109
+ value: "",
110
+ required: true
111
+ },
112
+ unit: {
113
+ value: 1,
114
+ required: true
115
+ },
116
+ tokendata: {
117
+ value: "dfgd",
118
+ required: false
119
+ },
120
+ apilimit: {
121
+ value: 60,
122
+ required: true
123
+ }
124
+ },
125
+ credentials: {
126
+ token: {
127
+ type: "password",
128
+ required: true
129
+ },
130
+ required: true
131
+ },
132
+ label: function() {
133
+ if(!this.name) {
134
+ this.name = this.host + "/" + this.unit;
135
+ }
136
+ return this.name;
137
+ },
138
+ labelStyle: function() {
139
+ return this.name ? "node_label_italic" : "";
140
+ },
141
+ oneditprepare: function() {
142
+ $('#node-config-label-tokendata').html(this.tokendata);
143
+
144
+ $('#node-config-input-token').on('change', function() {
145
+
146
+ if($(this).val() != '__PWRD__') {
147
+
148
+ let pt = decodeJwt($('#node-config-input-token').val())
149
+
150
+ if(pt == null) {
151
+ $('#node-config-label-tokendata').html('Invalid token format')
152
+ $('#node-config-input-tokendata').val('Invalid token format')
153
+ } else {
154
+ pt.iat = new Date(pt.iat * 1000).toISOString()
155
+
156
+ let rt = ''
157
+ $.each(pt, function( key, value ) {
158
+ rt = rt + key + ": " + value + '<br>'
159
+ });
160
+
161
+ $('#node-config-label-tokendata').html(rt)
162
+ $('#node-config-input-tokendata').val(rt)
163
+ }
164
+ }
165
+
166
+ })
167
+
168
+ $('#node-config-input-unit').on('change', function() {
169
+ if(!$.isNumeric($(this).val())) { $(this).val(1) } else {
170
+ if($(this).val() < 1 || $(this).val() > 100 ) { $(this).val(1) }
171
+ }
172
+ })
173
+
174
+ $('#node-config-input-apilimit').on('change', function() {
175
+ if(!$.isNumeric($(this).val())) { $(this).val(60) } else {
176
+ if($(this).val() < 1 || $(this).val() > 1000 ) { $(this).val(60) }
177
+ }
178
+ })
179
+
180
+ const tabs = RED.tabs.create({
181
+ id: "config-tabs",
182
+ onchange: function(tab) {
183
+ $("#config-tabs-content").children().hide();
184
+ $("#" + tab.id).show();
185
+ }
186
+ });
187
+
188
+ tabs.addTab({
189
+ id: "config-tab-config",
190
+ label: "Connection"
191
+ });
192
+ }
193
+ });
194
+ </script>
195
+
196
+ <style type="text/css">
197
+ .dynamic-dropdown-row {
198
+ padding: 6px 0;
199
+ }
200
+ </style>
201
+
202
+ <script type="text/x-red" data-template-name="teamogy-client">
203
+
204
+ <div class="form-row">
205
+ <label for="node-input-name" style="width: 130px"><i class="icon-tag"></i><span>Name</span></label>
206
+ <input type="text" id="node-input-name" placeholder="name" style="width:65%">
207
+ </div>
208
+
209
+ <div class="form-row">
210
+ <ul style="min-width: 600px; margin-bottom: 20px;" id="config-tabs"></ul>
211
+ </div>
212
+
213
+ <div id="config-tabs-content" style="min-height: 170px;">
214
+
215
+ <div id="config-tab-config" style="display: none;">
216
+
217
+ <div class="form-row">
218
+ <label for="node-input-configuration" style="width: 130px"><i class="fa fa-globe"></i><span> Connection</span></label>
219
+ <input type="text" id="node-input-configuration" style="width:65%">
220
+ </div>
221
+
222
+ <div class="form-row">
223
+ <label for="node-input-mode" style="width: 130px"><i class="fa fa-globe"></i> <span>Request views</span></label>
224
+ <input type="checkbox" id="node-input-mode" style=" width:20px;">
225
+ </div>
226
+
227
+ <div class="form-row">
228
+ <label for="node-input-entity" style="width: 130px"><i class="fa fa-caret-down"></i> Select entity</label>
229
+ <select id="node-input-entity" style="width:65%">
230
+ </select>
231
+ </div>
232
+
233
+ <div class="form-row">
234
+ <label for="node-input-method" style="width: 130px"><i class="fa fa-caret-down"></i> Select method</label>
235
+ <select id="node-input-method" style="width:65%">
236
+ </select>
237
+ </div>
238
+
239
+ <div class="form-row">
240
+ <label for="node-input-source" style="width: 130px"><i class="fa fa-caret-down"></i> Source</label>
241
+ <select id="node-input-source" style='width:65%'>
242
+ <option value="static">Static command</option>
243
+ <option value="dynamic">Dynamic from message </option>
244
+ </select>
245
+ </div>
246
+
247
+ <div class="form-row" id="node-row-editor">
248
+ <div class="form-row">
249
+ <label for="node-input-params" style="width: 130px"><i class="fa fa-code"></i> Parameters</label>
250
+ </div>
251
+ <div class="form-row">
252
+ <div style="height: 100px; min-height:100px;" class="node-text-editor" id="node-input-params" style='width:65%'></div>
253
+ </div>
254
+ <div class="form-row">
255
+ <label for="node-input-body" style="width: 130px"><i class="fa fa-code"></i> Body</label>
256
+ </div>
257
+ <div class="form-row">
258
+ <div style="height: 100px; min-height:100px;" class="node-text-editor" id="node-input-body" style='width:65%'></div>
259
+ </div>
260
+ </div>
261
+
262
+ <div class="form-row" id="node-row-response-option">
263
+ <label style="width: 130px"><span>Response option</span></label>
264
+ <div style="margin-bottom: 10px;"></div>
265
+ <div class="form-row">
266
+ <label for="node-input-merge" style="width: 130px"><i class="fa fa-compress"></i> <span> Merge</span></label>
267
+ <input type="checkbox" id="node-input-merge" style=" width:20px;">
268
+ </div>
269
+ <div class="form-row">
270
+ <label for="node-input-limit" style="width: 130px"><i class="fa fa-step-forward"></i><span> Limit</span></label>
271
+ <input type="number" id="node-input-limit" min="0" step="1" style="width:20%">
272
+ </div>
273
+ <div class="form-row">
274
+ <label for="node-input-paging" style="width: 130px"><i class="fa fa-eject"></i><span> Paging</span></label>
275
+ <input type="number" id="node-input-paging" min="0" max="1000" step="1" style="width:20%">
276
+ </div>
277
+ <div class="form-row">
278
+ <label for="node-input-offset" style="width: 130px"><i class="fa fa-forward"></i><span> Offset</span></label>
279
+ <input type="number" id="node-input-offset" min="0" step="1" style="width:20%">
280
+ </div>
281
+ </div>
282
+
283
+ <div class="form-row" id="node-row-editor-dynamic">
284
+ <div class="form-row">
285
+ <label for="node-input-params" style="width: 130px"><i class="fa fa-code"></i> Source of body</label>
286
+ <input type="text" id="node-input-body-source" style="width: 65%">
287
+ <input type="hidden" id="node-input-body-source-type" style="width: 0px">
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </script>
293
+
294
+ <script type="text/javascript">
295
+ RED.nodes.registerType('teamogy-client',{
296
+ category: 'TF collection',
297
+ color: '#fe4c00',
298
+ defaults: {
299
+ name: {
300
+ value: ""
301
+ },
302
+ configuration: {
303
+ type: "teamogy-config",
304
+ required: true
305
+ },
306
+ mode: {
307
+ value: false
308
+ },
309
+ method: {
310
+ value: "GET"
311
+ },
312
+ entity: {
313
+ value: "r_users"
314
+ },
315
+ source: {
316
+ value: "static"
317
+ },
318
+ cparams: {
319
+ value: ""
320
+ },
321
+ cbody: {
322
+ value: ""
323
+ },
324
+ bodysource: {
325
+ value: "payload"
326
+ },
327
+ merge: {
328
+ value: true
329
+ },
330
+ limit: {
331
+ value: 0,
332
+ required: true
333
+ },
334
+ paging: {
335
+ value: 1000,
336
+ required: true
337
+ },
338
+ offset: {
339
+ value: 0,
340
+ required: true
341
+ }
342
+ },
343
+ inputs: 1,
344
+ outputs:1,
345
+ icon: "bridge.svg",
346
+ align: "left",
347
+ label: function() {
348
+ return this.name || "teamogy";
349
+ },
350
+ paletteLabel: function() {
351
+ return this.name || "teamogy";
352
+ },
353
+ labelStyle: function() {
354
+ return this.name ? "node_label_italic" : "";
355
+ },
356
+ oneditprepare : function() {
357
+ let values
358
+ let mode = this.mode;
359
+ let entity = this.entity;
360
+ let method = this.method;
361
+ let source = this.source;
362
+ let params = this.cparams;
363
+ let body = this.cbody;
364
+
365
+ $('#node-input-source').val(this.source);
366
+ $('#node-input-merge').val(this.merge);
367
+ $('#node-input-limit').val(this.limit);
368
+ $('#node-input-paging').val(this.paging);
369
+ $('#node-input-offset').val(this.offset);
370
+
371
+ $('#node-input-mode').val(this.mode);
372
+ $('#node-input-body-source').val(this.bodysource);
373
+
374
+
375
+ if(this.mode===true) {
376
+ values = values2;
377
+ } else {
378
+ values = values1;
379
+ }
380
+
381
+ $('#node-input-mode').on('change', function() {
382
+ if ($(this).is(':checked')) {
383
+ values = values2;
384
+ if($('#node-input-source').val()=='static'){ $('#node-row-response-option').show(); }
385
+ } else {
386
+ values = values1;
387
+ $('#node-row-response-option').hide();
388
+ }
389
+ let se = fillEntity (values)
390
+ fillMethod (se)
391
+ })
392
+
393
+
394
+ function fillEntity (values) {
395
+ $('#node-input-entity').empty()
396
+ for (let value of values) {
397
+ $('#node-input-entity').append($("<option></option>").attr("value", value.v).text(value.e));
398
+ }
399
+
400
+ const fe = $('#node-input-entity option[value=' + entity +']').val();
401
+ if(fe != undefined ) {
402
+ $('#node-input-entity').val(entity);
403
+ } else {
404
+ $('#node-input-entity').prop('selectedIndex',0);;
405
+ }
406
+ return $('#node-input-entity').val();
407
+ }
408
+
409
+ function fillMethod (entity) {
410
+ $('#node-input-method').empty()
411
+ for (let value of values) {
412
+ if (value.v === entity) {
413
+ for (let method of value.m) {
414
+ $('#node-input-method').append($("<option></option>").attr("value", method).text(method));
415
+ }
416
+ }
417
+ }
418
+
419
+ const fe = $('#node-input-method option[value=' + method +']').val();
420
+ if(fe != undefined ) {
421
+ $('#node-input-method').val(method);
422
+ } else {
423
+ $('#node-input-method').prop('selectedIndex',0);;
424
+ }
425
+ }
426
+
427
+ $('#node-input-entity').on('change', function() {
428
+ fillMethod ($(this).val())
429
+ })
430
+
431
+ $('#node-input-source').on('change', function() {
432
+ if($(this).val()=='static') {
433
+ $('#node-row-editor').show();
434
+ $('#node-row-editor-dynamic').hide();
435
+ if($('#node-input-mode').is(':checked') == true) {
436
+ $('#node-row-response-option').show();
437
+ }
438
+ } else {
439
+ $('#node-row-editor').hide();
440
+ $('#node-row-editor-dynamic').show();
441
+ $('#node-row-response-option').hide();
442
+ }
443
+ })
444
+
445
+ $("#node-input-body-source").typedInput({
446
+ type:"msg",
447
+ types:["msg"],
448
+ typeField: "#node-input-body-source-type"
449
+ })
450
+
451
+ $('#node-input-limit').on('change', function() {
452
+ if(!$.isNumeric($(this).val())) { $(this).val(0) }
453
+ })
454
+
455
+ $('#node-input-paging').on('change', function() {
456
+ if(!$.isNumeric($(this).val())) { $(this).val(1000) }
457
+ })
458
+
459
+ $('#node-input-offset').on('change', function() {
460
+ if(!$.isNumeric($(this).val())) { $(this).val(0) }
461
+ })
462
+
463
+ this.editorp = RED.editor.createEditor({
464
+ id: 'node-input-params',
465
+ mode: 'ace/mode/text',
466
+ value: this.cparams
467
+ });
468
+
469
+ this.editorb = RED.editor.createEditor({
470
+ id: 'node-input-body',
471
+ mode: 'ace/mode/json',
472
+ value: this.cbody
473
+ });
474
+
475
+ const tabs = RED.tabs.create({
476
+ id: "config-tabs",
477
+ onchange: function(tab) {
478
+ $("#config-tabs-content").children().hide();
479
+ $("#" + tab.id).show();
480
+ }
481
+ });
482
+
483
+ tabs.addTab({
484
+ id: "config-tab-config",
485
+ label: "Configuration"
486
+ });
487
+ },
488
+ oneditsave: function(){
489
+ this.mode = $("#node-input-mode").val();
490
+ this.entity = $("#node-input-entity").val();
491
+ this.method = $("#node-input-method").val();
492
+ this.source = $("#node-input-source").val();
493
+ this.merge = $("#node-input-merge").val();
494
+ this.bodysource = $("#node-input-body-source").val();
495
+
496
+ if(!$("#node-input-limit").val()) {this.limit = 0} else {this.limit = $("#node-input-limit").val();}
497
+ if(!$("#node-input-offset").val()) {this.offset = 0} else {this.offset = $("#node-input-offset").val();}
498
+ this.paging = $("#node-input-paging").val();
499
+
500
+ this.cparams = this.editorp.getValue();
501
+ this.editorp.destroy();
502
+ delete this.editorp;
503
+
504
+ this.cbody = this.editorb.getValue();
505
+ this.editorb.destroy();
506
+ delete this.editorb;
507
+ },
508
+ oneditcancel: function() {
509
+ this.editorp.destroy();
510
+ delete this.editorp;
511
+
512
+ this.editorb.destroy();
513
+ delete this.editorb;
514
+ }
515
+ });
516
+ </script>
517
+
518
+ <script type="text/html" data-help-name="teamogy-client">
519
+ <p>Connect to a Teamogy API.</p>
520
+
521
+ <p>You must configure Connection before using this node.</p>
522
+
523
+ <h3>Connection configuration</h3>
524
+ <dl class="message-properties">
525
+ <dt class="optional">Name (optional)<span class="property-type">string</span></dt><dd> you can enter a name or it will be generated on first save</dd>
526
+ <dt>Subdomain (required)<span class="property-type">string</span></dt><dd> enter the name of the subdomain and the domain, in the format: subdomain.domain.com</dd>
527
+ <dt>Unit/Agency (required)<span class="property-type">number</span></dt><dd> enter the number of unit or agency</dd>
528
+ <dt>Token (required)<span class="property-type">string</span></dt><dd> enter the generated API token for the given subdomain and Unit/Agency</dd>
529
+ <dt>Req/min (required)<span class="property-type">number</span></dt><dd> enter the maximum number of API requests per minute for the given subdomain, if the limit is greater than allowed, further requests will be rejected (not processed)</dd>
530
+ </dl>
531
+
532
+ <h3>Input</h3>
533
+ <dl class="message-properties">
534
+ <dt class="optional">Name (optional)<span class="property-type">string</span></dt><dd> you can enter a name for the given node (recommended)</dd>
535
+ <dt>Connection (required)<span class="property-type">selection - list</span></dt><dd> select the configured connection</dd>
536
+ <dt>Request views (required)<span class="property-type">checkbox - yes/no</span></dt><dd> check for requests for API views (see description below)</dd>
537
+ <dt>Token (required)<span class="property-type">selection - list</span></dt><dd> select the desired entity</dd>
538
+ <dt>Req/min (required)<span class="property-type">selection - list</span></dt><dd> select an available method</dd>
539
+ <dt>Source (required)<span class="property-type">selection - list</span></dt><dd> the parameters or even the request body can be static, i.e. from the Params and Body fields in the form, or dynamic as part of the input message.</dd>
540
+ <dt>Source of body (required)<span class="property-type">string</span></dt><dd> the parameter set the source of the body, default is msg.payload</dd>
541
+ <dt>Params (optional)<span class="property-type">string | object</span></dt><dd> you can enter the params in the case of a static request as a string, in the case of a dynamic also as an object, according to the API specification for the given entity</dd>
542
+ <dd>&nbsp;</dd>
543
+ <dd> Examples:</dd>
544
+ <dd> In the case of a static request, enter the desired values ​​according to the selected entity and method in the Params field. Values ​​for Params are listed on separate lines.</dd>
545
+ <dd> id=3</dd>
546
+ <dd> registration=12345678</dd>
547
+ <dd>&nbsp;</dd>
548
+ <dd> In the case of a static request for API views </dd>
549
+ <dd> columns=firstName,lastName</dd>
550
+ <dd> externalFilter=id>5</dd>
551
+ <dd>&nbsp;</dd>
552
+ <dd> In the case of a dynamic request for API views, as string</dd>
553
+ <dd> id=2&name=John</dd>
554
+ <dd>&nbsp;</dd>
555
+ <dd> or also as an object </dd>
556
+ <dd> msg.params.id = 2</dd>
557
+ <dd> msg.params.name = "John"</dd>
558
+ <dd>&nbsp;</dd>
559
+ <dt>Body (optional)<span class="property-type">JSON string | object</span></dt><dd> you can enter the body in the case of a static request as a JSON string, in the case of a dynamic also as an object, you can nest objects and arrays in the body without restrictions according to the API specification for the given entity</dd>
560
+ <dd>&nbsp;</dd>
561
+ <dd> Examples:</dd>
562
+ <dd> In the case of a static request, enter the desired values ​​according to the selected entity and method in the Body field.</dd>
563
+ <dd> {</dd>
564
+ <dd> &nbsp;&nbsp;"id": 3,</dd>
565
+ <dd> &nbsp;&nbsp;"registration": 12345678</dd>
566
+ <dd> }</dd>
567
+ <dd>&nbsp;</dd>
568
+ <dd> In the case of a dynamic request for API views, as JSON string</dd>
569
+ <dd>{"id": 3,"name": "John"}</dd>
570
+ <dd>&nbsp;</dd>
571
+ <dd> or also as an object </dd>
572
+ <dd> msg.body.id = 2</dd>
573
+ <dd> msg.body.name = "John"</dd>
574
+ <dd> msg.body.address = addresses // array of objects</dd>
575
+ <dd>&nbsp;</dd>
576
+ <dd> If you use requests for API views you can use additional options, the options can also be used in dynamic requests</dd>
577
+ <dt class="optional">Merge (optional)<span class="property-type">checkbox - yes/no</span></dt><dd> check if you want the output message to contain all returned records. In the background, processing is still taking place according to the Paging settings, but individual messages are not sent to the output, but only the final merged one containing all the returned records. (if not specified the value no is used)</dd>
578
+ <dt class="optional">Limit (optional)<span class="property-type">number</span></dt><dd> limitation of the total number of records in the response (if not specified, the value 0 is used, the value 0 means no limit)</dd>
579
+ <dt class="optional">Paging (optional)<span class="property-type">number</span></dt><dd> division of responses into multiple parts with the number of records returned (if not specified, the value 1000 is used)</dd>
580
+ <dt class="optional">Offset (optional)<span class="property-type">number</span></dt><dd> determining from which records to return, e.g. if you have a total of 100 records and want to return records 50-100, set Offset to 50, if you want to return all records leave the option at 0 (if not specified, the value 0 is used)</dd>
581
+ <dt class="optional">Skip (optional, in message only)<span class="property-type">boolean</span></dt><dd> adding the msg.skip=true will allow the message to pass through to the output without processing</dd>
582
+ </dl>
583
+
584
+ <h3>Output</h3>
585
+ <dl class="message-properties">
586
+ <dt>payload<span class="property-type">string | object</span></dt><dd> the standard output or error of the response</dd>
587
+ <dt>count<span class="property-type">number</span></dt><dd> number of records in the output</dd>
588
+ <dt>msg.*<span class="property-type">string | object</span></dt><dd> all properties of the input message</dd>
589
+ </dl>
590
+
591
+ <h3>References</h3>
592
+ <ul>
593
+ <li><a href="https://teamogy.com/teamogy-flow">Teamogy Flow docs</a> - full description of Teamogy Flow</li>
594
+ <li><a href="https://readme.teamogy.com/reference/integration-options">Teamogy API docs</a> - full description of parameters for <code>msg.params</code> and <code>msg.body</code> properties, and also options for API views</li>
595
+ </ul>
596
+ </script>
@@ -0,0 +1,288 @@
1
+ function isEmpty(value) { return (value == null || (typeof value === "string" && value.trim().length === 0)); }
2
+
3
+ module.exports = function(RED) {
4
+
5
+ function ConnectionNode(n) {
6
+ try {
7
+ RED.nodes.createNode(this, n);
8
+
9
+ this.name = n.name;
10
+ this.host = n.host;
11
+ this.unit = n.unit;
12
+ this.apilimit = n.apilimit;
13
+ this.tokendata = n.tokendata;
14
+ if (this.credentials) {
15
+ this.token = this.credentials.token;
16
+ }
17
+
18
+ if(typeof this.context().global.get('cache_' + this.host ) == 'undefined') {
19
+ let cache = []
20
+ this.context().global.set('cache_' + this.host, cache)
21
+ }
22
+
23
+ } catch (e) {
24
+ node.error(e);
25
+ }
26
+ }
27
+
28
+ RED.nodes.registerType('teamogy-config', ConnectionNode, {
29
+ credentials: {
30
+ token: {type: 'password'}
31
+ }
32
+ });
33
+
34
+ function teamogyClient(data) {
35
+ try {
36
+ var node = this;
37
+
38
+ RED.nodes.createNode(node,data);
39
+
40
+ this.config = RED.nodes.getNode(data.configuration);
41
+
42
+ let token = this.config.credentials.token
43
+ let host = this.config.host
44
+ let unit = this.config.unit
45
+
46
+ let clientid = data.id
47
+ let c = this.context().global.get('cache_' + host)
48
+ let st = null;
49
+
50
+ async function sendmsg(mesg) {
51
+ try {
52
+ const msg = { ... mesg.msg }
53
+ let mparams = ''
54
+ let body = ''
55
+ let mmerge = false
56
+ let mlimit = 0
57
+ let mpaging = 1000
58
+ let moffset = 0
59
+
60
+ if(data.source=='dynamic') {
61
+ if(typeof msg.params == 'string') { mparams = msg.params }
62
+ if(typeof msg.params == 'object') {
63
+
64
+ let pa=Object.entries(msg.params)
65
+ if(pa.length > 0) {
66
+ mparams = pa[0][0] + '=' + pa[0][1]
67
+
68
+ for (let i = 1; i < pa.length; i++) {
69
+ if(pa[i].length > 0) { mparams = mparams + '&' + pa[i][0] + '=' + pa[i][1] }
70
+ }
71
+ }
72
+ }
73
+
74
+ if(typeof msg[data.bodysource] == 'string') { body = msg[data.bodysource] }
75
+ if(typeof msg[data.bodysource] == 'object') { body = JSON.stringify(msg[data.bodysource]) }
76
+
77
+ if(typeof msg.merge == 'boolean') { mmerge = msg.merge }
78
+ if(typeof msg.limit == 'number') { mlimit = msg.limit }
79
+ if(typeof msg.paging == 'number') { mpaging = msg.paging }
80
+ if(typeof msg.offset == 'number') { moffset = msg.offset }
81
+
82
+ }
83
+
84
+ if(data.source=='static') {
85
+ const pa = data.cparams.split(/\r?\n/)
86
+ if(pa.length > 0) {
87
+ mparams = pa[0]
88
+
89
+ for (let i = 1; i < pa.length; i++) {
90
+ if(pa[i].length > 0) { mparams = mparams + '&' + pa[i] }
91
+ }
92
+ }
93
+ body = data.cbody
94
+ mmerge = data.merge
95
+ mlimit = data.limit
96
+ mpaging = data.paging
97
+ moffset = data.offset
98
+ }
99
+
100
+ const headers = {
101
+ 'Authorization': 'Bearer ' + token,
102
+ 'Accept': 'application/json',
103
+ 'Content-type': 'application/json'
104
+ };
105
+
106
+ let url = `https://${host}/rest/v1/${unit}/`
107
+
108
+ if(data.entity.split('_')[0] == 'v') { url = url + 'views/'}
109
+
110
+ url = url + data.entity.split('_')[1].replaceAll('-','.')
111
+
112
+ if(!isEmpty(mparams)) { url = url + '?' + mparams }
113
+
114
+ const doAsyncJobs = async () => {
115
+ try {
116
+
117
+ let newMsg = JSON.parse(JSON.stringify(mesg.msg));
118
+
119
+ let metadata = {};
120
+ metadata.count = 0
121
+ metadata.limit = 0
122
+ let rdata = [];
123
+
124
+ let method = data.method
125
+ let offset = moffset
126
+
127
+ if(method == 'GET') { body = null }
128
+
129
+ if(data.entity.split('_')[0] == 'v') {
130
+ if(isEmpty(mparams)) { url = url + '?' } else { url = url + '&' }
131
+
132
+ while (offset != null) {
133
+
134
+ if(parseInt(mlimit) == 0) { mlimit = 1000000000}
135
+ if(parseInt(mlimit) < parseInt(mpaging)) { mpaging = mlimit }
136
+
137
+ eurl = encodeURI(url + 'limit=' + mpaging +'&offset=' + offset)
138
+
139
+ const response = await fetch(eurl, {headers, method, body});
140
+
141
+ if(response.status >= 200 && response.status < 300) {
142
+
143
+ const body = await response.json();
144
+ offset = body?.metadata?.nextOffset
145
+ metadata.count = metadata.count + parseInt(body?.metadata?.count)
146
+ metadata.limit = body?.metadata?.limit
147
+ metadata.nextOffset = offset
148
+
149
+ if(mlimit - metadata.count < mpaging) { mpaging = mlimit - metadata.count}
150
+
151
+ if(mmerge == false) {
152
+ newMsg.payload=body;
153
+ newMsg.payload.count=body.data.length;
154
+ node.send(JSON.parse(JSON.stringify(newMsg)));
155
+ }
156
+
157
+ if(mmerge == true) {
158
+ for(let bd of body.data) {
159
+ rdata.push(bd)
160
+ }
161
+ }
162
+
163
+ if(mlimit <= metadata.count) { break; }
164
+
165
+ } else {
166
+ node.error('Response status: ' + response.status);
167
+ node.error('Response status: ' + await response.text());
168
+ break;
169
+ }
170
+ }
171
+
172
+ if(mmerge == true) {
173
+ let body = {}
174
+ metadata.limit = parseInt(data.paging)
175
+ body.metadata = metadata
176
+ body.data = rdata
177
+ body.count = rdata.length
178
+ newMsg.payload = body
179
+ node.send(JSON.parse(JSON.stringify(newMsg)));
180
+ }
181
+ }
182
+
183
+ if(data.entity.split('_')[0] == 'r') {
184
+
185
+ const response = await fetch(encodeURI(url), {headers, method, body});
186
+
187
+ if(response.status >= 200 && response.status < 300) {
188
+ const body = await response.json();
189
+ newMsg.payload = body
190
+ node.send(JSON.parse(JSON.stringify(newMsg)));
191
+ } else {
192
+ let payload = {}
193
+ payload.status = response.status
194
+ payload.text = await response.text()
195
+
196
+ newMsg.payload = payload
197
+ node.send(JSON.parse(JSON.stringify(newMsg)));
198
+ }
199
+ }
200
+ } catch (e) {
201
+ node.error(e);
202
+ }
203
+ }
204
+
205
+ const r = await doAsyncJobs()
206
+
207
+ } catch (e) {
208
+ node.error(e);
209
+ }
210
+ }
211
+
212
+ function fa(arr,cid) {
213
+ try {
214
+ let na = arr.filter(function (el) {
215
+ return el.clientid == cid
216
+ });
217
+ return na.length
218
+ } catch (e) {
219
+ node.error(e);
220
+ }
221
+ }
222
+
223
+ async function setTimer(host) {
224
+ try {
225
+ if(c.length > 0) {
226
+ let nd = new Date()
227
+ if(nd.getTime() > c[0].stime) {
228
+ if(c[0].clientid == clientid) {
229
+ let m = c.shift(0);
230
+ await sendmsg(m)
231
+ }
232
+ }
233
+
234
+ let fal = fa(c,clientid);
235
+
236
+ if(fal == 0) {
237
+ node.status({fill: "green", shape: "dot", text: fal + " waiting messages"});
238
+ } else {
239
+ node.status({fill: "yellow", shape: "dot", text: fal + " waiting messages"});
240
+ }
241
+ } else {
242
+ clearInterval(st)
243
+ st = null
244
+ }
245
+ }
246
+ catch (e) {
247
+ node.error(e);
248
+ }
249
+ }
250
+
251
+ node.on('input', async function(msg) {
252
+ try {
253
+ if(st == null){ st = setInterval(function () { setTimer(host) }, 1000)}
254
+
255
+ if(msg.skip != true) {
256
+ let stime = 0
257
+ let nd = new Date()
258
+
259
+ if(c.length > 0) {
260
+ let hstime = c.findLast((el) => el);
261
+ stime = hstime.stime + (60000 / this.config.apilimit)
262
+ } else {
263
+ stime = nd.getTime()
264
+ }
265
+
266
+ let md = {}
267
+ md.msg = msg
268
+ md.host = host
269
+ md.rtime = nd.getTime();
270
+ md.stime = stime
271
+ md.clientid = clientid
272
+ c.push(md)
273
+ } else {
274
+ node.send(msg)
275
+ }
276
+ }
277
+ catch (e) {
278
+ node.error(e);
279
+ }
280
+ });
281
+
282
+ } catch (e) {
283
+ node.error(e);
284
+ }
285
+ }
286
+
287
+ RED.nodes.registerType("teamogy-client",teamogyClient);
288
+ }