node-red-contrib-avid-interplay 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ZZZCROSSS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # node-red-contrib-avid-interplay
2
+
3
+ A [Node-RED](https://nodered.org) package for calling the **Avid Interplay PAM SOAP Web Services**.
4
+
5
+ Supports all standard Avid Interplay services: **Assets**, **Archive**, **Infrastructure**, **Jobs**, **Transfer**, **UserManagement**.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - Service and operation selected via dropdown menus in the node editor
12
+ - **Live WSDL introspection** — operations are fetched directly from the server at design time
13
+ - **Payload template generator** — reads the XSD schema and produces an annotated `msg.payload` template with `[R]` / `[O]` markers
14
+ - WS-Security `UsernameToken` authentication
15
+ - Configurable HTTP timeout
16
+ - Accepts self-signed / internal CA certificates
17
+ - Runtime override via `msg.service` and `msg.operation`
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # inside your Node-RED user directory (usually ~/.node-red)
25
+ npm install node-red-contrib-avid-interplay
26
+ ```
27
+
28
+ Or use the **Manage Palette** panel in the Node-RED editor.
29
+
30
+ ---
31
+
32
+ ## Nodes
33
+
34
+ ### `interplay-config` (config node)
35
+
36
+ Holds the connection settings shared by all request nodes.
37
+
38
+ | Field | Description |
39
+ |---|---|
40
+ | **Base URL** | Root CXF endpoint, e.g. `https://host:1881/services` |
41
+ | **Username** | Interplay username (optional) |
42
+ | **Password** | Interplay password (optional) |
43
+ | **Timeout** | HTTP request timeout in ms (default: 30 000) |
44
+ | **Allow self-signed SSL** | Bypass certificate verification for internal servers |
45
+
46
+ ### `interplay-request` (function node)
47
+
48
+ Executes a single SOAP operation and outputs the response.
49
+
50
+ | Field | Description |
51
+ |---|---|
52
+ | **Server** | Reference to an `interplay-config` node |
53
+ | **Service** | One of `Assets`, `Archive`, `Infrastructure`, `Jobs`, `Transfer`, `UserManagement` |
54
+ | **Operation** | Loaded from the live WSDL after clicking **Load operations from WSDL** |
55
+
56
+ #### Buttons
57
+
58
+ | Button | Action |
59
+ |---|---|
60
+ | **Load operations from WSDL** | Fetches the operation list from `{baseUrl}/{service}?wsdl` |
61
+ | **Generate payload template** | Parses the XSD schema and renders an annotated `msg.payload` template |
62
+
63
+ ---
64
+
65
+ ## Usage
66
+
67
+ 1. Add an **`interplay-config`** node and fill in the server URL and credentials.
68
+ 2. Drop an **`interplay-request`** node onto the canvas and open it.
69
+ 3. Select the **Service** (e.g. `Assets`).
70
+ 4. Click **Load operations from WSDL** — the Operation dropdown will populate.
71
+ 5. Select an **Operation** (e.g. `GetChildren`).
72
+ 6. Click **Generate payload template** — an annotated JSON template appears.
73
+ 7. Copy the template and paste it into an upstream **Change** or **Function** node,
74
+ replacing placeholder values with real data.
75
+ 8. Wire the output of that node to the `interplay-request` node.
76
+
77
+ ### Payload template legend
78
+
79
+ ```
80
+ [R] Required — must be provided
81
+ [O] Optional — may be omitted
82
+ ```
83
+
84
+ A value wrapped in `[…]` is a repeatable array element.
85
+ The `_legend` key is metadata only — **do not include it** in `msg.payload`.
86
+
87
+ ### Example
88
+
89
+ ```javascript
90
+ // Function node upstream of interplay-request
91
+ msg.payload = {
92
+ InterplayURI: "interplay://MyWorkgroup/MyDatabase/Projects/MyFolder"
93
+ };
94
+ return msg;
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Message properties
100
+
101
+ ### Input
102
+
103
+ | Property | Type | Description |
104
+ |---|---|---|
105
+ | `msg.payload` | object | Operation parameters (property names must match the XSD element names) |
106
+ | `msg.service` | string | *(optional)* Runtime override for the service name |
107
+ | `msg.operation` | string | *(optional)* Runtime override for the operation name |
108
+
109
+ ### Output
110
+
111
+ | Property | Type | Description |
112
+ |---|---|---|
113
+ | `msg.payload` | object | Deserialised SOAP response. A top-level `<return>` element is unwrapped automatically |
114
+ | `msg.statusCode` | number | HTTP status code of the SOAP response |
115
+ | `msg.service` | string | Name of the service that was called |
116
+ | `msg.operation` | string | Name of the operation that was called |
117
+
118
+ ---
119
+
120
+ ## Supported services
121
+
122
+ | Service | Endpoint path |
123
+ |---|---|
124
+ | Assets | `/services/Assets` |
125
+ | Archive | `/services/Archive` |
126
+ | Infrastructure | `/services/Infrastructure` |
127
+ | Jobs | `/services/Jobs` |
128
+ | Transfer | `/services/Transfer` |
129
+ | UserManagement | `/services/UserManagement` |
130
+
131
+ ---
132
+
133
+ ## Cache management
134
+
135
+ WSDL and XSD documents are cached in memory after the first load. To force a reload
136
+ (e.g. after an Interplay server upgrade), send a `POST` request from Node-RED to the
137
+ internal admin endpoint:
138
+
139
+ ```
140
+ POST /interplay/clear-cache
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Error handling
146
+
147
+ On SOAP fault or network error the node raises a Node-RED error catchable with a
148
+ **Catch** node. The error message is in `error.message`.
149
+
150
+ ---
151
+
152
+ ## Requirements
153
+
154
+ - Node-RED ≥ 3.0.0
155
+ - Node.js ≥ 14.0.0
156
+ - Network access to the Avid Interplay server at design time (for WSDL/XSD introspection)
157
+
158
+ ---
159
+
160
+ ## License
161
+
162
+ [MIT](LICENSE)
package/interplay.html ADDED
@@ -0,0 +1,501 @@
1
+ <!-- ══════════════════════════════════════════════════════════════════════════
2
+ interplay-config – Config node
3
+ ══════════════════════════════════════════════════════════════════════════ -->
4
+ <script type="text/html" data-template-name="interplay-config">
5
+
6
+ <div class="form-row">
7
+ <label for="node-config-input-name">
8
+ <i class="fa fa-tag"></i> Name
9
+ </label>
10
+ <input type="text" id="node-config-input-name" placeholder="Interplay Server">
11
+ </div>
12
+
13
+ <div class="form-row">
14
+ <label for="node-config-input-baseUrl">
15
+ <i class="fa fa-globe"></i> Base URL
16
+ </label>
17
+ <input type="text" id="node-config-input-baseUrl"
18
+ placeholder="https://host:1881/services"
19
+ style="width:72%">
20
+ </div>
21
+
22
+ <div class="form-row">
23
+ <label for="node-config-input-username">
24
+ <i class="fa fa-user"></i> Username
25
+ </label>
26
+ <input type="text" id="node-config-input-username" placeholder="(optional)">
27
+ </div>
28
+
29
+ <div class="form-row">
30
+ <label for="node-config-input-password">
31
+ <i class="fa fa-lock"></i> Password
32
+ </label>
33
+ <input type="password" id="node-config-input-password" placeholder="(optional)">
34
+ </div>
35
+
36
+ <div class="form-row">
37
+ <label for="node-config-input-timeout">
38
+ <i class="fa fa-clock-o"></i> Timeout
39
+ </label>
40
+ <input type="number" id="node-config-input-timeout"
41
+ placeholder="30000" style="width:100px"> ms
42
+ </div>
43
+
44
+ <div class="form-row">
45
+ <label style="width:auto">
46
+ <i class="fa fa-shield"></i>&nbsp;
47
+ </label>
48
+ <input type="checkbox" id="node-config-input-allowSelfSigned"
49
+ style="display:inline-block;width:auto;margin-right:6px">
50
+ <label for="node-config-input-allowSelfSigned" style="width:auto">
51
+ Allow self-signed / internal SSL certificates
52
+ </label>
53
+ </div>
54
+
55
+ <div class="form-tips">
56
+ Credentials are injected as a WS-Security <code>UsernameToken</code> SOAP header on every request.<br>
57
+ Enable <em>Allow self-signed SSL</em> when the server uses an internal CA certificate.
58
+ </div>
59
+
60
+ </script>
61
+
62
+ <script type="text/javascript">
63
+ RED.nodes.registerType("interplay-config", {
64
+ category: "config",
65
+ defaults: {
66
+ name: { value: "" },
67
+ baseUrl: { value: "https://yourwebservice.yourdomain:1881/services", required: true },
68
+ timeout: { value: 30000 },
69
+ allowSelfSigned: { value: true },
70
+ },
71
+ credentials: {
72
+ username: { type: "text" },
73
+ password: { type: "password" },
74
+ },
75
+ label: function () {
76
+ return this.name || this.baseUrl || "Interplay Server";
77
+ },
78
+ });
79
+ </script>
80
+
81
+
82
+ <!-- ══════════════════════════════════════════════════════════════════════════
83
+ interplay-request – Request node
84
+ ══════════════════════════════════════════════════════════════════════════ -->
85
+ <script type="text/html" data-template-name="interplay-request">
86
+
87
+ <!-- Name -->
88
+ <div class="form-row">
89
+ <label for="node-input-name">
90
+ <i class="fa fa-tag"></i> Name
91
+ </label>
92
+ <input type="text" id="node-input-name" placeholder="Interplay Request">
93
+ </div>
94
+
95
+ <!-- Server -->
96
+ <div class="form-row">
97
+ <label for="node-input-server">
98
+ <i class="fa fa-server"></i> Server
99
+ </label>
100
+ <input type="text" id="node-input-server">
101
+ </div>
102
+
103
+ <!-- Service selector -->
104
+ <div class="form-row">
105
+ <label for="node-input-service">
106
+ <i class="fa fa-cogs"></i> Service
107
+ </label>
108
+ <select id="node-input-service" style="width:72%">
109
+ <option value="Assets">Assets</option>
110
+ <option value="Archive">Archive</option>
111
+ <option value="Infrastructure">Infrastructure</option>
112
+ <option value="Jobs">Jobs</option>
113
+ <option value="Transfer">Transfer</option>
114
+ <option value="UserManagement">UserManagement</option>
115
+ </select>
116
+ </div>
117
+
118
+ <!-- Load operations button -->
119
+ <div class="form-row">
120
+ <label></label>
121
+ <button id="interplay-load-ops-btn" type="button"
122
+ class="red-ui-button" style="font-size:12px">
123
+ <i class="fa fa-refresh"></i>&nbsp;Load operations from WSDL
124
+ </button>
125
+ <span id="interplay-ops-spinner"
126
+ style="display:none;margin-left:8px;font-size:12px;color:#888">
127
+ <i class="fa fa-spinner fa-spin"></i> Loading…
128
+ </span>
129
+ </div>
130
+
131
+ <!-- Operation selector -->
132
+ <div class="form-row">
133
+ <label for="node-input-operation">
134
+ <i class="fa fa-bolt"></i> Operation
135
+ </label>
136
+ <select id="node-input-operation" style="width:72%">
137
+ <option value="">— select operation —</option>
138
+ </select>
139
+ </div>
140
+
141
+ <!-- Namespace info (shown after WSDL load) -->
142
+ <div id="interplay-ns-row" style="display:none;margin-left:112px;margin-bottom:8px">
143
+ <span style="font-size:11px;color:#888">
144
+ Namespace: <code id="interplay-ns-lbl" style="color:#4a90d9"></code>
145
+ </span>
146
+ </div>
147
+
148
+ <!-- Load parameters button -->
149
+ <div class="form-row" id="interplay-load-params-row" style="display:none">
150
+ <label></label>
151
+ <button id="interplay-load-params-btn" type="button"
152
+ class="red-ui-button" style="font-size:12px">
153
+ <i class="fa fa-magic"></i>&nbsp;Generate payload template
154
+ </button>
155
+ <span id="interplay-params-spinner"
156
+ style="display:none;margin-left:8px;font-size:12px;color:#888">
157
+ <i class="fa fa-spinner fa-spin"></i> Loading…
158
+ </span>
159
+ </div>
160
+
161
+ <!-- Schema / template box -->
162
+ <div id="interplay-schema-wrap" style="display:none;margin-top:4px">
163
+
164
+ <div style="font-size:11px;padding:4px 8px;margin-bottom:4px;
165
+ background:#f5f5f5;border:1px solid #ddd;border-radius:3px;
166
+ display:flex;justify-content:space-between;align-items:center">
167
+ <span>
168
+ <strong>msg.payload template</strong>&nbsp;&nbsp;
169
+ <code style="color:#2a7">&#91;R&#93;</code> = required&nbsp;&nbsp;
170
+ <code style="color:#a70">&#91;O&#93;</code> = optional
171
+ </span>
172
+ <span>
173
+ <button id="interplay-copy-btn" type="button"
174
+ class="red-ui-button" style="font-size:11px;padding:2px 8px">
175
+ <i class="fa fa-copy"></i>&nbsp;Copy
176
+ </button>
177
+ <span id="interplay-copy-ok"
178
+ style="display:none;font-size:11px;color:green;margin-left:6px">
179
+ Copied!
180
+ </span>
181
+ </span>
182
+ </div>
183
+
184
+ <pre id="interplay-schema-json"
185
+ style="background:#1e1e1e;color:#d4d4d4;padding:12px;border-radius:4px;
186
+ font-size:12px;overflow:auto;max-height:340px;margin:0;
187
+ white-space:pre;line-height:1.5"></pre>
188
+
189
+ <div style="margin-top:4px;font-size:11px;color:#888">
190
+ <i class="fa fa-info-circle"></i>
191
+ Populate a <b>Change</b> or <b>Function</b> node upstream with this JSON
192
+ (replace the placeholder values), then wire its output to this node.
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Error box -->
197
+ <div id="interplay-error"
198
+ style="display:none;margin-top:6px;padding:6px 10px;
199
+ background:#fff0f0;border:1px solid #f5a0a0;
200
+ border-radius:3px;font-size:12px;color:#c00">
201
+ </div>
202
+
203
+ <hr style="margin:12px 0">
204
+
205
+ <div class="form-tips">
206
+ <b>Input →</b> <code>msg.payload</code> = operation parameters as a JS object.<br>
207
+ Runtime override: <code>msg.service</code>, <code>msg.operation</code>.<br>
208
+ <b>Output →</b> <code>msg.payload</code> = deserialised SOAP response.
209
+ </div>
210
+
211
+ </script>
212
+
213
+ <script type="text/javascript">
214
+ (function () {
215
+ "use strict";
216
+
217
+ // ── helpers ──────────────────────────────────────────────────────────────────
218
+ function showError(msg) {
219
+ $("#interplay-error").text(msg).show();
220
+ }
221
+ function hideError() {
222
+ $("#interplay-error").hide();
223
+ }
224
+
225
+ // Colour-code [R] and [O] markers in the JSON string
226
+ function colorizeTemplate(raw) {
227
+ return raw
228
+ .replace(/"\[R\]/g, '"<span style="color:#4ec9b0;font-weight:bold">[R]</span>')
229
+ .replace(/"\[O\]/g, '"<span style="color:#ce9178">[O]</span>');
230
+ }
231
+
232
+ // ── node registration ────────────────────────────────────────────────────────
233
+ RED.nodes.registerType("interplay-request", {
234
+ category: "Avid Interplay",
235
+ color: "#3a7bd5",
236
+ defaults: {
237
+ name: { value: "" },
238
+ server: { value: "", type: "interplay-config", required: true },
239
+ service: { value: "Assets" },
240
+ operation: { value: "" },
241
+ },
242
+ inputs: 1,
243
+ outputs: 1,
244
+ icon: "font-awesome/fa-film",
245
+ paletteLabel: "interplay request",
246
+
247
+ label: function () {
248
+ if (this.name) return this.name;
249
+ if (this.service && this.operation)
250
+ return `${this.service} · ${this.operation}`;
251
+ return "interplay request";
252
+ },
253
+
254
+ labelStyle: function () {
255
+ return this.name ? "node_label_italic" : "";
256
+ },
257
+
258
+ oneditprepare: function () {
259
+ var node = this;
260
+ var $svcSel = $("#node-input-service");
261
+ var $opSel = $("#node-input-operation");
262
+ var currentOp = node.operation || "";
263
+
264
+ // ── restore saved state ─────────────────────────────────────────────────
265
+ $svcSel.val(node.service || "Assets");
266
+
267
+ if (currentOp) {
268
+ $opSel.empty();
269
+ $opSel.append(
270
+ $("<option>").val(currentOp).text(currentOp).prop("selected", true)
271
+ );
272
+ $("#interplay-load-params-row").show();
273
+ }
274
+
275
+ // ── reset when service changes ──────────────────────────────────────────
276
+ $svcSel.on("change", function () {
277
+ $opSel.empty().append('<option value="">— select operation —</option>');
278
+ $("#interplay-load-params-row").hide();
279
+ $("#interplay-schema-wrap").hide();
280
+ $("#interplay-ns-row").hide();
281
+ hideError();
282
+ });
283
+
284
+ // ── show/hide "Generate payload" when operation changes ─────────────────
285
+ $opSel.on("change", function () {
286
+ var val = $(this).val();
287
+ $("#interplay-load-params-row").toggle(!!val);
288
+ $("#interplay-schema-wrap").hide();
289
+ hideError();
290
+ });
291
+
292
+ // ── Load operations from WSDL ───────────────────────────────────────────
293
+ $("#interplay-load-ops-btn").on("click", function () {
294
+ var serverId = $("#node-input-server").val();
295
+ var service = $svcSel.val();
296
+
297
+ if (!serverId) { showError("Please select a server first."); return; }
298
+ if (!service) { showError("Please select a service."); return; }
299
+
300
+ $("#interplay-load-ops-btn").prop("disabled", true);
301
+ $("#interplay-ops-spinner").show();
302
+ hideError();
303
+
304
+ $.get("interplay/operations", { configId: serverId, service: service })
305
+ .done(function (data) {
306
+ if (!data || !data.ok) {
307
+ showError((data && data.error) || "Unknown error.");
308
+ return;
309
+ }
310
+
311
+ var saved = currentOp || $opSel.val();
312
+ $opSel.empty().append('<option value="">— select operation —</option>');
313
+ (data.operations || []).forEach(function (op) {
314
+ $opSel.append($("<option>").val(op).text(op));
315
+ });
316
+ if (saved) $opSel.val(saved);
317
+
318
+ if (data.targetNamespace) {
319
+ $("#interplay-ns-lbl").text(data.targetNamespace);
320
+ $("#interplay-ns-row").show();
321
+ }
322
+
323
+ var sel = $opSel.val();
324
+ $("#interplay-load-params-row").toggle(!!sel);
325
+ })
326
+ .fail(function (xhr) {
327
+ var msg = "Communication error with Node-RED.";
328
+ try { var j = JSON.parse(xhr.responseText); if (j && j.error) msg = j.error; } catch (_) {}
329
+ showError(msg);
330
+ })
331
+ .always(function () {
332
+ $("#interplay-load-ops-btn").prop("disabled", false);
333
+ $("#interplay-ops-spinner").hide();
334
+ });
335
+ });
336
+
337
+ // ── Generate payload template ───────────────────────────────────────────
338
+ $("#interplay-load-params-btn").on("click", function () {
339
+ var serverId = $("#node-input-server").val();
340
+ var service = $svcSel.val();
341
+ var operation = $opSel.val();
342
+
343
+ if (!serverId) { showError("Please select a server first."); return; }
344
+ if (!service) { showError("Please select a service."); return; }
345
+ if (!operation) { showError("Please select an operation."); return; }
346
+
347
+ $("#interplay-load-params-btn").prop("disabled", true);
348
+ $("#interplay-params-spinner").show();
349
+ $("#interplay-schema-wrap").hide();
350
+ hideError();
351
+
352
+ $.get("interplay/schema", { configId: serverId, service: service, operation: operation })
353
+ .done(function (data) {
354
+ if (data && data.ok) {
355
+ $("#interplay-schema-json").html(colorizeTemplate(data.annotated));
356
+ $("#interplay-schema-wrap").show();
357
+ hideError();
358
+ } else {
359
+ showError((data && data.error) || "Unknown error.");
360
+ }
361
+ })
362
+ .fail(function (xhr) {
363
+ var msg = "Communication error with Node-RED.";
364
+ try { var j = JSON.parse(xhr.responseText); if (j && j.error) msg = j.error; } catch (_) {}
365
+ showError(msg);
366
+ })
367
+ .always(function () {
368
+ $("#interplay-load-params-btn").prop("disabled", false);
369
+ $("#interplay-params-spinner").hide();
370
+ });
371
+ });
372
+
373
+ // ── Copy to clipboard ───────────────────────────────────────────────────
374
+ $("#interplay-copy-btn").on("click", function () {
375
+ // Get raw text (strip HTML colour spans)
376
+ var text = $("#interplay-schema-json").text();
377
+ if (!text) return;
378
+
379
+ function flash() {
380
+ var $ok = $("#interplay-copy-ok");
381
+ $ok.show();
382
+ setTimeout(function () { $ok.hide(); }, 2000);
383
+ }
384
+
385
+ if (navigator.clipboard && navigator.clipboard.writeText) {
386
+ navigator.clipboard.writeText(text).then(flash);
387
+ } else {
388
+ var ta = document.createElement("textarea");
389
+ ta.value = text;
390
+ ta.style.cssText = "position:fixed;opacity:0";
391
+ document.body.appendChild(ta);
392
+ ta.select();
393
+ document.execCommand("copy");
394
+ document.body.removeChild(ta);
395
+ flash();
396
+ }
397
+ });
398
+ },
399
+
400
+ oneditsave: function () {
401
+ this.service = $("#node-input-service").val() || "Assets";
402
+ this.operation = $("#node-input-operation").val() || "";
403
+ },
404
+ });
405
+
406
+ }());
407
+ </script>
408
+
409
+
410
+ <!-- ══════════════════════════════════════════════════════════════════════════
411
+ Help panels
412
+ ══════════════════════════════════════════════════════════════════════════ -->
413
+ <script type="text/html" data-help-name="interplay-request">
414
+ <p>
415
+ Executes SOAP operations on the <b>Avid Interplay PAM Web Services</b>.
416
+ </p>
417
+
418
+ <h3>Setup</h3>
419
+ <ol>
420
+ <li>Configure a server with the <b>interplay-config</b> node (URL, credentials, timeout).</li>
421
+ <li>Select the <b>Service</b> (Assets, Archive, Infrastructure, Jobs, Transfer, UserManagement).</li>
422
+ <li>Click <b>Load operations from WSDL</b> to fetch the available operations from the live server.</li>
423
+ <li>Select an <b>Operation</b> and click <b>Generate payload template</b>
424
+ to view the required and optional parameters.</li>
425
+ <li>Copy the template and use it as the basis for <code>msg.payload</code>
426
+ in an upstream Change or Function node.</li>
427
+ </ol>
428
+
429
+ <h3>Input</h3>
430
+ <dl class="message-properties">
431
+ <dt>payload <span class="property-type">object</span></dt>
432
+ <dd>Operation parameters as a JS object — property names must match the XSD element names.</dd>
433
+ <dt class="optional">service <span class="property-type">string</span></dt>
434
+ <dd>Runtime override for the service (e.g. <code>"Assets"</code>).</dd>
435
+ <dt class="optional">operation <span class="property-type">string</span></dt>
436
+ <dd>Runtime override for the operation (e.g. <code>"GetChildren"</code>).</dd>
437
+ </dl>
438
+
439
+ <h3>Output</h3>
440
+ <dl class="message-properties">
441
+ <dt>payload <span class="property-type">object</span></dt>
442
+ <dd>Deserialised SOAP response. A top-level <code>&lt;return&gt;</code> element is unwrapped automatically.</dd>
443
+ <dt>statusCode <span class="property-type">number</span></dt>
444
+ <dd>HTTP status code of the SOAP response.</dd>
445
+ <dt>service <span class="property-type">string</span></dt>
446
+ <dd>Name of the service that was called.</dd>
447
+ <dt>operation <span class="property-type">string</span></dt>
448
+ <dd>Name of the operation that was called.</dd>
449
+ </dl>
450
+
451
+ <h3>Authentication</h3>
452
+ <p>
453
+ Username and password are sent as a WS-Security <code>UsernameToken</code>
454
+ in the SOAP header of every request.
455
+ </p>
456
+
457
+ <h3>Error handling</h3>
458
+ <p>
459
+ On SOAP fault or network error the node raises a Node-RED error catchable
460
+ with a <b>Catch</b> node. The error message is available in <code>error.message</code>.
461
+ </p>
462
+
463
+ <h3>Payload template legend</h3>
464
+ <ul>
465
+ <li><code style="color:#4ec9b0">[R]</code> = <b>Required</b> — must be provided</li>
466
+ <li><code style="color:#ce9178">[O]</code> = <b>Optional</b> — may be omitted</li>
467
+ <li>Values wrapped in <code>[]</code> denote a repeatable array element.</li>
468
+ <li>The <code>_legend</code> key is documentation only — <b>do not include it</b> in <code>msg.payload</code>.</li>
469
+ </ul>
470
+
471
+ <h3>Example</h3>
472
+ <pre>// Get children of a folder
473
+ msg.payload = {
474
+ InterplayURI: "interplay://MyWorkgroup/MyDB/Projects/MyFolder"
475
+ };
476
+ return msg;</pre>
477
+ </script>
478
+
479
+ <script type="text/html" data-help-name="interplay-config">
480
+ <p>Connection settings for the <b>Avid Interplay PAM SOAP Web Services</b>.</p>
481
+
482
+ <dl class="message-properties">
483
+ <dt>Base URL</dt>
484
+ <dd>
485
+ Root CXF endpoint, e.g. <code>https://host:1881/services</code>.<br>
486
+ The service name (Assets, Archive, Jobs…) is appended automatically.
487
+ </dd>
488
+ <dt>Username / Password</dt>
489
+ <dd>
490
+ Interplay credentials sent as a WS-Security UsernameToken header.
491
+ Leave empty if the service does not require authentication.
492
+ </dd>
493
+ <dt>Timeout</dt>
494
+ <dd>HTTP request timeout in milliseconds (default: 30000 ms).</dd>
495
+ <dt>Allow self-signed SSL</dt>
496
+ <dd>
497
+ Bypasses SSL certificate verification — required for servers using
498
+ an internal CA or self-signed certificates.
499
+ </dd>
500
+ </dl>
501
+ </script>
package/interplay.js ADDED
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const { XMLParser } = require("fast-xml-parser");
6
+
7
+ // ─── Parsers ──────────────────────────────────────────────────────────────────
8
+ const soapParser = new XMLParser({
9
+ removeNSPrefix: true,
10
+ ignoreAttributes: false,
11
+ attributeNamePrefix: "@_"
12
+ });
13
+
14
+ // isArray ensures repeated elements are always arrays even when there is only one
15
+ const xsdParser = new XMLParser({
16
+ removeNSPrefix: true,
17
+ ignoreAttributes: false,
18
+ attributeNamePrefix: "@_",
19
+ isArray: tag => [
20
+ "operation","message","portType","part",
21
+ "element","attribute","complexType","simpleType",
22
+ "import","include","sequence","choice","all",
23
+ "enumeration"
24
+ ].includes(tag)
25
+ });
26
+
27
+ // ─── In-memory caches ─────────────────────────────────────────────────────────
28
+ const wsdlCache = {}; // `${baseUrl}/${service}` -> wsdl info
29
+ const schemaCache = {}; // `${baseUrl}/${service}` -> schema registry
30
+
31
+ // ─── HTTP helpers ─────────────────────────────────────────────────────────────
32
+ function httpGet(url, allowSelfSigned, timeoutMs) {
33
+ return new Promise((resolve, reject) => {
34
+ const u = new URL(url);
35
+ const opts = {
36
+ hostname: u.hostname,
37
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
38
+ path: u.pathname + u.search,
39
+ method: "GET",
40
+ headers: {},
41
+ rejectUnauthorized: !allowSelfSigned,
42
+ };
43
+ const lib = u.protocol === "https:" ? https : http;
44
+ const req = lib.request(opts, res => {
45
+ const chunks = [];
46
+ res.on("data", d => chunks.push(d));
47
+ res.on("end", () => resolve(Buffer.concat(chunks).toString()));
48
+ });
49
+ req.setTimeout(timeoutMs || 20000, () => {
50
+ req.destroy();
51
+ reject(new Error(`Timeout fetching ${url}`));
52
+ });
53
+ req.on("error", reject);
54
+ req.end();
55
+ });
56
+ }
57
+
58
+ function httpPost(url, body, headers, allowSelfSigned, timeoutMs) {
59
+ return new Promise((resolve, reject) => {
60
+ const u = new URL(url);
61
+ const opts = {
62
+ hostname: u.hostname,
63
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
64
+ path: u.pathname + u.search,
65
+ method: "POST",
66
+ headers,
67
+ rejectUnauthorized: !allowSelfSigned,
68
+ };
69
+ const lib = u.protocol === "https:" ? https : http;
70
+ const req = lib.request(opts, res => {
71
+ const chunks = [];
72
+ res.on("data", d => chunks.push(d));
73
+ res.on("end", () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() }));
74
+ });
75
+ req.setTimeout(timeoutMs || 30000, () => {
76
+ req.destroy();
77
+ reject(new Error("Request timed out"));
78
+ });
79
+ req.on("error", reject);
80
+ req.write(body);
81
+ req.end();
82
+ });
83
+ }
84
+
85
+ // ─── XML escape ───────────────────────────────────────────────────────────────
86
+ function escXml(v) {
87
+ return String(v)
88
+ .replace(/&/g, "&amp;")
89
+ .replace(/</g, "&lt;")
90
+ .replace(/>/g, "&gt;")
91
+ .replace(/"/g, "&quot;")
92
+ .replace(/'/g, "&apos;");
93
+ }
94
+
95
+ // ─── SOAP XML builder ─────────────────────────────────────────────────────────
96
+ function toXml(obj, tag, ns) {
97
+ const q = tag ? `${ns}:${tag}` : null;
98
+ if (obj === null || obj === undefined) {
99
+ return q
100
+ ? `<${q} xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>`
101
+ : "";
102
+ }
103
+ if (Array.isArray(obj)) return obj.map(i => toXml(i, tag, ns)).join("");
104
+ if (typeof obj === "object") {
105
+ const inner = Object.entries(obj).map(([k, v]) => toXml(v, k, ns)).join("");
106
+ return q ? `<${q}>${inner}</${q}>` : inner;
107
+ }
108
+ return q ? `<${q}>${escXml(obj)}</${q}>` : escXml(obj);
109
+ }
110
+
111
+ function buildEnvelope(operation, payload, ns, credentials) {
112
+ const bodyContent = payload ? toXml(payload, null, "ns") : "";
113
+
114
+ // WS-Security UsernameToken header (standard for Avid Interplay WS)
115
+ let wssHeader = "";
116
+ if (credentials && credentials.username) {
117
+ wssHeader = `
118
+ <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
119
+ <wsse:UsernameToken>
120
+ <wsse:Username>${escXml(credentials.username)}</wsse:Username>
121
+ <wsse:Password>${escXml(credentials.password || "")}</wsse:Password>
122
+ </wsse:UsernameToken>
123
+ </wsse:Security>`;
124
+ }
125
+
126
+ return `<?xml version="1.0" encoding="UTF-8"?>
127
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
128
+ xmlns:ns="${ns}">
129
+ <soap:Header>${wssHeader}
130
+ </soap:Header>
131
+ <soap:Body>
132
+ <ns:${operation}>${bodyContent}</ns:${operation}>
133
+ </soap:Body>
134
+ </soap:Envelope>`;
135
+ }
136
+
137
+ function parseSOAPResponse(xml) {
138
+ const json = soapParser.parse(xml);
139
+ const body = json?.Envelope?.Body;
140
+ if (!body) throw new Error("Invalid SOAP response — no Body element");
141
+ if (body.Fault) {
142
+ const f = body.Fault;
143
+ const msg = f.faultstring || f.Reason?.Text || JSON.stringify(f);
144
+ throw new Error(`SOAP Fault: ${msg}`);
145
+ }
146
+ const key = Object.keys(body)[0];
147
+ return body[key]?.return ?? body[key];
148
+ }
149
+
150
+ // ─── WSDL loading ─────────────────────────────────────────────────────────────
151
+ async function loadWsdl(baseUrl, service, allowSelfSigned) {
152
+ const cacheKey = `${baseUrl}/${service}`;
153
+ if (wsdlCache[cacheKey]) return wsdlCache[cacheKey];
154
+
155
+ const text = await httpGet(`${cacheKey}?wsdl`, allowSelfSigned, 20000);
156
+ const doc = xsdParser.parse(text);
157
+ const defs = doc.definitions;
158
+ if (!defs) throw new Error("Not a valid WSDL document");
159
+
160
+ const targetNamespace = defs["@_targetNamespace"] || "";
161
+
162
+ // messages: name -> input element name
163
+ const msgToElement = {};
164
+ (defs.message || []).forEach(m => {
165
+ const part = (m.part || [])[0];
166
+ if (!part) return;
167
+ const el = (part["@_element"] || "").replace(/^[^:]+:/, "");
168
+ if (el) msgToElement[m["@_name"]] = el;
169
+ });
170
+
171
+ // operations
172
+ const operations = [];
173
+ const opInputElements = {};
174
+ (defs.portType || []).forEach(pt => {
175
+ (pt.operation || []).forEach(o => {
176
+ const name = o["@_name"];
177
+ if (!name) return;
178
+ operations.push(name);
179
+ if (o.input) {
180
+ const msgName = (o.input["@_message"] || "").replace(/^[^:]+:/, "");
181
+ opInputElements[name] = msgToElement[msgName] || name;
182
+ }
183
+ });
184
+ });
185
+
186
+ // XSD import locations
187
+ const xsdUrls = [];
188
+ const types = defs.types;
189
+ if (types) {
190
+ const schemas = Array.isArray(types.schema)
191
+ ? types.schema
192
+ : (types.schema ? [types.schema] : []);
193
+ schemas.forEach(schema => {
194
+ (schema.import || []).forEach(imp => {
195
+ const loc = imp["@_schemaLocation"];
196
+ if (!loc) return;
197
+ xsdUrls.push(
198
+ loc.startsWith("http") ? loc
199
+ : `${cacheKey}${loc.startsWith("?") ? "" : "/"}${loc}`
200
+ );
201
+ });
202
+ });
203
+ }
204
+
205
+ const result = { operations, opInputElements, targetNamespace, xsdUrls };
206
+ wsdlCache[cacheKey] = result;
207
+ return result;
208
+ }
209
+
210
+ // ─── XSD schema registry ──────────────────────────────────────────────────────
211
+ async function loadSchemaRegistry(baseUrl, service, wsdlInfo, allowSelfSigned) {
212
+ const cacheKey = `${baseUrl}/${service}`;
213
+ if (schemaCache[cacheKey]) return schemaCache[cacheKey];
214
+
215
+ const elements = {};
216
+ const complexTypes = {};
217
+
218
+ function ingest(schema) {
219
+ (schema.element || []).forEach(e => { if (e["@_name"]) elements[e["@_name"]] = e; });
220
+ (schema.complexType || []).forEach(t => { if (t["@_name"]) complexTypes[t["@_name"]] = t; });
221
+ }
222
+
223
+ for (const url of wsdlInfo.xsdUrls) {
224
+ try {
225
+ const text = await httpGet(url, allowSelfSigned, 20000);
226
+ const doc = xsdParser.parse(text);
227
+ const schema = doc.schema || doc["xs:schema"] || doc["xsd:schema"];
228
+ if (schema) ingest(schema);
229
+ } catch (_) { /* skip XSD load errors silently */ }
230
+ }
231
+
232
+ const result = { elements, complexTypes };
233
+ schemaCache[cacheKey] = result;
234
+ return result;
235
+ }
236
+
237
+ // ─── Payload template generator ───────────────────────────────────────────────
238
+ function stripNs(s) { return (s || "").replace(/^[^:]+:/, ""); }
239
+
240
+ function elementTemplate(el, registry, depth) {
241
+ if (depth > 10) return "...";
242
+
243
+ const typeRef = stripNs(el["@_type"]);
244
+ const minOcc = el["@_minOccurs"] !== undefined ? String(el["@_minOccurs"]) : "1";
245
+ const maxOcc = el["@_maxOccurs"] !== undefined ? String(el["@_maxOccurs"]) : "1";
246
+ const req = minOcc !== "0";
247
+ const isArr = maxOcc === "unbounded" || (maxOcc !== "1" && Number(maxOcc) > 1);
248
+ const tag = req ? "[R]" : "[O]";
249
+
250
+ // inline complexType
251
+ if (el.complexType) {
252
+ const inner = ctTemplate(el.complexType, registry, depth + 1);
253
+ return isArr ? [inner] : inner;
254
+ }
255
+
256
+ // named complex type
257
+ if (typeRef && registry.complexTypes[typeRef]) {
258
+ const inner = ctTemplate(registry.complexTypes[typeRef], registry, depth + 1);
259
+ return isArr ? [inner] : inner;
260
+ }
261
+
262
+ // ref to another element
263
+ if (el["@_ref"]) {
264
+ const refName = stripNs(el["@_ref"]);
265
+ if (registry.elements[refName]) {
266
+ return elementTemplate(registry.elements[refName], registry, depth + 1);
267
+ }
268
+ return isArr ? [`${tag} ref:${refName}`] : `${tag} ref:${refName}`;
269
+ }
270
+
271
+ // primitive / unknown scalar
272
+ const prim = typeRef || "string";
273
+ return isArr ? [`${tag} ${prim}`] : `${tag} ${prim}`;
274
+ }
275
+
276
+ function ctTemplate(ct, registry, depth) {
277
+ if (depth > 10) return {};
278
+ const result = {};
279
+
280
+ let container = ct.sequence || ct.all || ct.choice;
281
+ if (!container && ct.complexContent) {
282
+ const ext = ct.complexContent.extension || ct.complexContent.restriction;
283
+ if (ext) container = ext.sequence || ext.all || ext.choice;
284
+ }
285
+ if (!container) return result;
286
+
287
+ (container.element || []).forEach(el => {
288
+ const elName = el["@_name"] || stripNs(el["@_ref"]) || "?";
289
+ result[elName] = elementTemplate(el, registry, depth + 1);
290
+ });
291
+
292
+ return result;
293
+ }
294
+
295
+ function generatePayloadTemplate(opElementName, registry) {
296
+ const rootEl = registry.elements[opElementName];
297
+ if (!rootEl) return null;
298
+
299
+ if (rootEl.complexType) return ctTemplate(rootEl.complexType, registry, 0);
300
+
301
+ const typeRef = stripNs(rootEl["@_type"]);
302
+ if (typeRef && registry.complexTypes[typeRef]) {
303
+ return ctTemplate(registry.complexTypes[typeRef], registry, 0);
304
+ }
305
+
306
+ return {};
307
+ }
308
+
309
+ // ─── Module export ────────────────────────────────────────────────────────────
310
+ module.exports = function (RED) {
311
+
312
+ // ── Config node ─────────────────────────────────────────────────────────────
313
+ function InterplayConfig(n) {
314
+ RED.nodes.createNode(this, n);
315
+ this.name = n.name;
316
+ this.baseUrl = (n.baseUrl || "").replace(/\/$/, "");
317
+ this.timeout = parseInt(n.timeout) || 30000;
318
+ this.allowSelfSigned = n.allowSelfSigned !== false;
319
+ }
320
+
321
+ RED.nodes.registerType("interplay-config", InterplayConfig, {
322
+ credentials: {
323
+ username: { type: "text" },
324
+ password: { type: "password" },
325
+ }
326
+ });
327
+
328
+ // ── Admin: load operations from WSDL ────────────────────────────────────────
329
+ // GET /interplay/operations?configId=<id>&service=<Assets|Infrastructure|...>
330
+ RED.httpAdmin.get("/interplay/operations", RED.auth.needsPermission("flows.read"), async (req, res) => {
331
+ try {
332
+ const cfg = RED.nodes.getNode(req.query.configId);
333
+ if (!cfg) { return res.json({ ok: false, error: "Config node not found" }); }
334
+ const service = req.query.service;
335
+ if (!service) { return res.json({ ok: false, error: "'service' parameter required" }); }
336
+
337
+ const info = await loadWsdl(cfg.baseUrl, service, cfg.allowSelfSigned);
338
+ res.json({ ok: true, operations: info.operations, targetNamespace: info.targetNamespace });
339
+ } catch (e) {
340
+ res.json({ ok: false, error: e.message });
341
+ }
342
+ });
343
+
344
+ // ── Admin: generate payload template from XSD ────────────────────────────────
345
+ // GET /interplay/schema?configId=<id>&service=<...>&operation=<...>
346
+ RED.httpAdmin.get("/interplay/schema", RED.auth.needsPermission("flows.read"), async (req, res) => {
347
+ try {
348
+ const cfg = RED.nodes.getNode(req.query.configId);
349
+ if (!cfg) { return res.json({ ok: false, error: "Config node not found" }); }
350
+
351
+ const { service, operation } = req.query;
352
+ if (!service || !operation) {
353
+ return res.json({ ok: false, error: "'service' and 'operation' parameters required" });
354
+ }
355
+
356
+ const wsdlInfo = await loadWsdl(cfg.baseUrl, service, cfg.allowSelfSigned);
357
+ const registry = await loadSchemaRegistry(cfg.baseUrl, service, wsdlInfo, cfg.allowSelfSigned);
358
+
359
+ const inputElName = wsdlInfo.opInputElements[operation] || operation;
360
+ let template = generatePayloadTemplate(inputElName, registry);
361
+
362
+ if (!template) {
363
+ template = { "_note": `[O] Schema not found for '${operation}'. Inspect the WSDL manually.` };
364
+ }
365
+
366
+ const annotated = JSON.stringify(
367
+ { "_legend": "[R] = required | [O] = optional", ...template },
368
+ null, 2
369
+ );
370
+
371
+ res.json({ ok: true, annotated, inputElementName: inputElName });
372
+ } catch (e) {
373
+ res.json({ ok: false, error: e.message });
374
+ }
375
+ });
376
+
377
+ // ── Admin: clear caches (useful after config change) ────────────────────────
378
+ // POST /interplay/clear-cache
379
+ RED.httpAdmin.post("/interplay/clear-cache", RED.auth.needsPermission("flows.write"), (req, res) => {
380
+ Object.keys(wsdlCache).forEach(k => delete wsdlCache[k]);
381
+ Object.keys(schemaCache).forEach(k => delete schemaCache[k]);
382
+ res.json({ ok: true });
383
+ });
384
+
385
+ // ── Request node ─────────────────────────────────────────────────────────────
386
+ function InterplayRequest(config) {
387
+ RED.nodes.createNode(this, config);
388
+ const node = this;
389
+ node.cfg = RED.nodes.getNode(config.server);
390
+ node.service = config.service || "Assets";
391
+ node.operation = config.operation || "";
392
+
393
+ node.on("input", async (msg, send, done) => {
394
+ send = send || ((...a) => node.send(...a));
395
+ done = done || ((e) => { if (e) node.error(e, msg); });
396
+
397
+ const cfg = node.cfg;
398
+ if (!cfg) {
399
+ node.status({ fill: "red", shape: "ring", text: "no server config" });
400
+ return done(new Error("No server configuration"));
401
+ }
402
+
403
+ const service = msg.service || node.service;
404
+ const operation = msg.operation || node.operation;
405
+
406
+ if (!service || !operation) {
407
+ node.status({ fill: "red", shape: "dot", text: "service/operation missing" });
408
+ return done(new Error("msg.service and msg.operation (or node config) must be set"));
409
+ }
410
+
411
+ try {
412
+ node.status({ fill: "blue", shape: "dot", text: `${service}.${operation}…` });
413
+
414
+ const wsdlInfo = await loadWsdl(cfg.baseUrl, service, cfg.allowSelfSigned);
415
+ const ns = wsdlInfo.targetNamespace;
416
+ const url = `${cfg.baseUrl}/${service}`;
417
+ const xml = buildEnvelope(operation, msg.payload || null, ns, cfg.credentials);
418
+
419
+ const result = await httpPost(url, xml, {
420
+ "Content-Type": "text/xml; charset=utf-8",
421
+ "SOAPAction": `"${ns}/${operation}"`,
422
+ "Content-Length": Buffer.byteLength(xml),
423
+ }, cfg.allowSelfSigned, cfg.timeout);
424
+
425
+ msg.payload = parseSOAPResponse(result.body);
426
+ msg.statusCode = result.status;
427
+ msg.service = service;
428
+ msg.operation = operation;
429
+
430
+ node.status({ fill: "green", shape: "dot", text: `${service}.${operation} ✓` });
431
+ send(msg);
432
+ done();
433
+ } catch (err) {
434
+ node.status({ fill: "red", shape: "dot", text: String(err.message).substring(0, 60) });
435
+ done(err);
436
+ }
437
+ });
438
+
439
+ node.on("close", () => node.status({}));
440
+ }
441
+
442
+ RED.nodes.registerType("interplay-request", InterplayRequest);
443
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "node-red-contrib-avid-interplay",
3
+ "version": "1.1.0",
4
+ "description": "Node-RED nodes for Avid Interplay PAM SOAP Web Services",
5
+ "keywords": [
6
+ "node-red",
7
+ "avid",
8
+ "interplay",
9
+ "soap",
10
+ "media",
11
+ "asset-management",
12
+ "pam"
13
+ ],
14
+ "author": {
15
+ "name": "ZZZCROSSS",
16
+ "email": "YOUR_EMAIL@example.com",
17
+ "url": "https://github.com/zzzcrosss"
18
+ },
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/zzzcrosss/node-red-contrib-avid-interplay.git"
23
+ },
24
+ "homepage": "https://github.com/zzzcrosss/node-red-contrib-avid-interplay#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/zzzcrosss/node-red-contrib-avid-interplay/issues"
27
+ },
28
+ "files": [
29
+ "interplay.js",
30
+ "interplay.html",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "dependencies": {
35
+ "fast-xml-parser": "^5.8.0"
36
+ },
37
+ "node-red": {
38
+ "version": ">=3.0.0",
39
+ "nodes": {
40
+ "avid-interplay": "interplay.js"
41
+ }
42
+ },
43
+ "engines": {
44
+ "node": ">=14.0.0"
45
+ }
46
+ }