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 +21 -0
- package/README.md +162 -0
- package/interplay.html +501 -0
- package/interplay.js +443 -0
- package/package.json +46 -0
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>
|
|
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> 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> 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>
|
|
169
|
+
<code style="color:#2a7">[R]</code> = required
|
|
170
|
+
<code style="color:#a70">[O]</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> 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><return></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, "&")
|
|
89
|
+
.replace(/</g, "<")
|
|
90
|
+
.replace(/>/g, ">")
|
|
91
|
+
.replace(/"/g, """)
|
|
92
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|