ultravisor 1.0.2 → 1.0.4
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/.claude/launch.json +11 -0
- package/.claude/ultravisor-dev-config.json +3 -0
- package/.ultravisor.json +426 -0
- package/docs/README.md +63 -0
- package/package.json +12 -8
- package/source/Ultravisor.cjs +22 -3
- package/source/cli/Ultravisor-CLIProgram.cjs +35 -23
- package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +29 -18
- package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +62 -19
- package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +27 -15
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +5 -3
- package/source/services/Ultravisor-ExecutionEngine.cjs +1039 -0
- package/source/services/Ultravisor-ExecutionManifest.cjs +399 -0
- package/source/services/Ultravisor-Hypervisor-State.cjs +270 -97
- package/source/services/Ultravisor-Hypervisor.cjs +38 -83
- package/source/services/Ultravisor-StateManager.cjs +241 -0
- package/source/services/Ultravisor-TaskTypeRegistry.cjs +143 -0
- package/source/services/tasks/Ultravisor-TaskType-Base.cjs +105 -0
- package/source/services/tasks/control/Ultravisor-TaskType-IfConditional.cjs +148 -0
- package/source/services/tasks/control/Ultravisor-TaskType-LaunchOperation.cjs +187 -0
- package/source/services/tasks/control/Ultravisor-TaskType-SplitExecute.cjs +184 -0
- package/source/services/tasks/data/Ultravisor-TaskType-ReplaceString.cjs +82 -0
- package/source/services/tasks/data/Ultravisor-TaskType-SetValues.cjs +81 -0
- package/source/services/tasks/data/Ultravisor-TaskType-StringAppender.cjs +101 -0
- package/source/services/tasks/file-io/Ultravisor-TaskType-ReadFile.cjs +103 -0
- package/source/services/tasks/file-io/Ultravisor-TaskType-WriteFile.cjs +117 -0
- package/source/services/tasks/interaction/Ultravisor-TaskType-ErrorMessage.cjs +54 -0
- package/source/services/tasks/interaction/Ultravisor-TaskType-ValueInput.cjs +62 -0
- package/source/web_server/Ultravisor-API-Server.cjs +237 -124
- package/test/Ultravisor_browser_tests.js +2226 -0
- package/test/Ultravisor_tests.js +1143 -5830
- package/webinterface/css/ultravisor.css +23 -0
- package/webinterface/package.json +6 -3
- package/webinterface/source/Pict-Application-Ultravisor.js +93 -73
- package/webinterface/source/cards/FlowCard-CSVTransform.js +43 -0
- package/webinterface/source/cards/FlowCard-Command.js +86 -0
- package/webinterface/source/cards/FlowCard-ComprehensionIntersect.js +40 -0
- package/webinterface/source/cards/FlowCard-Conditional.js +87 -0
- package/webinterface/source/cards/FlowCard-CopyFile.js +55 -0
- package/webinterface/source/cards/FlowCard-End.js +29 -0
- package/webinterface/source/cards/FlowCard-GetJSON.js +55 -0
- package/webinterface/source/cards/FlowCard-GetText.js +54 -0
- package/webinterface/source/cards/FlowCard-Histogram.js +176 -0
- package/webinterface/source/cards/FlowCard-LaunchOperation.js +82 -0
- package/webinterface/source/cards/FlowCard-ListFiles.js +55 -0
- package/webinterface/source/cards/FlowCard-MeadowCount.js +44 -0
- package/webinterface/source/cards/FlowCard-MeadowCreate.js +44 -0
- package/webinterface/source/cards/FlowCard-MeadowDelete.js +45 -0
- package/webinterface/source/cards/FlowCard-MeadowRead.js +46 -0
- package/webinterface/source/cards/FlowCard-MeadowReads.js +46 -0
- package/webinterface/source/cards/FlowCard-MeadowUpdate.js +44 -0
- package/webinterface/source/cards/FlowCard-ParseCSV.js +85 -0
- package/webinterface/source/cards/FlowCard-ReadJSON.js +54 -0
- package/webinterface/source/cards/FlowCard-ReadText.js +54 -0
- package/webinterface/source/cards/FlowCard-RestRequest.js +59 -0
- package/webinterface/source/cards/FlowCard-SendJSON.js +57 -0
- package/webinterface/source/cards/FlowCard-Solver.js +77 -0
- package/webinterface/source/cards/FlowCard-Start.js +29 -0
- package/webinterface/source/cards/FlowCard-TemplateString.js +77 -0
- package/webinterface/source/cards/FlowCard-WriteJSON.js +54 -0
- package/webinterface/source/cards/FlowCard-WriteText.js +54 -0
- package/webinterface/source/data/ExampleFlow-CSVPipeline.js +231 -0
- package/webinterface/source/data/ExampleFlow-FileProcessor.js +315 -0
- package/webinterface/source/data/ExampleFlow-MeadowPipeline.js +328 -0
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +8 -8
- package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +6 -6
- package/webinterface/source/views/PictView-Ultravisor-FlowEditor.js +436 -0
- package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +45 -43
- package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +34 -89
- package/webinterface/source/views/PictView-Ultravisor-OperationList.js +128 -13
- package/webinterface/source/views/PictView-Ultravisor-PendingInput.js +314 -0
- package/webinterface/source/views/PictView-Ultravisor-Schedule.js +18 -53
- package/webinterface/source/views/PictView-Ultravisor-TimingView.js +27 -14
- package/webinterface/source/views/PictView-Ultravisor-TopBar.js +2 -1
- package/.babelrc +0 -6
- package/.browserslistrc +0 -1
- package/.browserslistrc-BACKUP +0 -1
- package/.gulpfile-quackage-config.json +0 -7
- package/.gulpfile-quackage.js +0 -2
- package/debug/Harness.js +0 -5
- package/source/services/Ultravisor-Operation-Manifest.cjs +0 -160
- package/source/services/Ultravisor-Operation.cjs +0 -200
- package/source/services/Ultravisor-Task.cjs +0 -349
- package/source/services/events/Ultravisor-Hypervisor-Event-Solver.cjs +0 -11
- package/source/services/tasks/Ultravisor-Task-Base.cjs +0 -264
- package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +0 -188
- package/source/services/tasks/Ultravisor-Task-Command.cjs +0 -65
- package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +0 -190
- package/source/services/tasks/Ultravisor-Task-Conditional.cjs +0 -104
- package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +0 -72
- package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +0 -336
- package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +0 -143
- package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +0 -146
- package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +0 -158
- package/source/services/tasks/Ultravisor-Task-Request.cjs +0 -56
- package/source/services/tasks/Ultravisor-Task-Solver.cjs +0 -89
- package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +0 -93
- package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +0 -127
- package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +0 -119
- package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +0 -109
- package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +0 -112
- package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +0 -499
- package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +0 -150
- package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +0 -110
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +0 -89
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +0 -87
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +0 -67
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +0 -66
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +0 -69
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +0 -95
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +0 -96
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +0 -99
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +0 -102
- package/webinterface/.babelrc +0 -6
- package/webinterface/.browserslistrc +0 -1
- package/webinterface/.browserslistrc-BACKUP +0 -1
- package/webinterface/.gulpfile-quackage-config.json +0 -7
- package/webinterface/.gulpfile-quackage.js +0 -2
- package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +0 -220
- package/webinterface/source/views/PictView-Ultravisor-TaskList.js +0 -248
- /package/docs/{cover.md → _cover.md} +0 -0
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
const libUltravisorTaskBase = require('../Ultravisor-Task-Base.cjs');
|
|
2
|
-
|
|
3
|
-
const libHTTP = require('http');
|
|
4
|
-
const libHTTPS = require('https');
|
|
5
|
-
|
|
6
|
-
class UltravisorTaskRestRequest extends libUltravisorTaskBase
|
|
7
|
-
{
|
|
8
|
-
constructor(pFable)
|
|
9
|
-
{
|
|
10
|
-
super(pFable);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generic REST request with full control over method, body, headers
|
|
15
|
-
* and cookies.
|
|
16
|
-
*
|
|
17
|
-
* Task definition fields:
|
|
18
|
-
* - URL: the endpoint to request (literal string)
|
|
19
|
-
* - URLAddress (optional): dot-notation address into GlobalState
|
|
20
|
-
* that resolves to the URL string. When set, overrides URL.
|
|
21
|
-
* - Method (optional): HTTP method (defaults to "GET")
|
|
22
|
-
* - Body (optional): request body -- string or object (serialised
|
|
23
|
-
* as JSON when object)
|
|
24
|
-
* - ContentType (optional): Content-Type header value (defaults to
|
|
25
|
-
* "application/json" when Body is an object, omitted otherwise)
|
|
26
|
-
* - Headers (optional): object of request headers
|
|
27
|
-
* - Cookies (optional): object of cookies to send (name:value pairs)
|
|
28
|
-
* - StoreCookies (optional, default true): whether to capture
|
|
29
|
-
* Set-Cookie response headers into pContext.GlobalState.Cookies
|
|
30
|
-
* - CaptureToken (optional): extract a value from the JSON response
|
|
31
|
-
* body and store it in the shared cookie jar. Can be:
|
|
32
|
-
* - A string: dot-notation path into the JSON body (the value
|
|
33
|
-
* is stored as a cookie named "Token")
|
|
34
|
-
* - An object: { "Address": "JSON.path", "Cookie": "CookieName" }
|
|
35
|
-
* - CaptureHeader (optional): object mapping response header names
|
|
36
|
-
* to GlobalState addresses. E.g.
|
|
37
|
-
* { "X-Auth-Token": "AuthToken" } stores the value of the
|
|
38
|
-
* X-Auth-Token response header at GlobalState.AuthToken
|
|
39
|
-
* - Destination (optional): manyfest address in GlobalState for the
|
|
40
|
-
* response data (defaults to "Output")
|
|
41
|
-
* - Persist (optional): where to store the response
|
|
42
|
-
*
|
|
43
|
-
* Shared cookie jar:
|
|
44
|
-
* When the response contains Set-Cookie headers they are parsed and
|
|
45
|
-
* stored at pContext.GlobalState.Cookies (an object keyed by cookie
|
|
46
|
-
* name). Subsequent RestRequest tasks automatically include any
|
|
47
|
-
* cookies found at that location. Task-level Cookies merge on top,
|
|
48
|
-
* so explicit values override the jar.
|
|
49
|
-
*
|
|
50
|
-
* Token capture (CaptureToken):
|
|
51
|
-
* Many APIs return session tokens in the JSON body rather than via
|
|
52
|
-
* Set-Cookie headers. CaptureToken extracts a value from the parsed
|
|
53
|
-
* JSON response and stores it in GlobalState.Cookies so that
|
|
54
|
-
* subsequent RestRequest tasks automatically send it.
|
|
55
|
-
*
|
|
56
|
-
* Retries:
|
|
57
|
-
* When Retries is set to a number > 0, the request will be retried
|
|
58
|
-
* up to that many times on network errors, timeouts, or non-2xx
|
|
59
|
-
* status codes. Each retry waits 1 second before re-attempting.
|
|
60
|
-
* All retry attempts are logged in the manifest entry.
|
|
61
|
-
*/
|
|
62
|
-
execute(pTaskDefinition, pContext, pManifestEntry, fCallback)
|
|
63
|
-
{
|
|
64
|
-
// Resolve URL from GlobalState via URLAddress, or fall back to literal URL
|
|
65
|
-
let tmpURL = pTaskDefinition.URL || pTaskDefinition.Parameters || '';
|
|
66
|
-
|
|
67
|
-
if (pTaskDefinition.URLAddress && typeof(pTaskDefinition.URLAddress) === 'string')
|
|
68
|
-
{
|
|
69
|
-
let tmpResolved = this.resolveAddress(pTaskDefinition.URLAddress, pContext);
|
|
70
|
-
if (tmpResolved && typeof(tmpResolved) === 'string')
|
|
71
|
-
{
|
|
72
|
-
tmpURL = tmpResolved;
|
|
73
|
-
pManifestEntry.Log.push(`RestRequest: resolved URL from GlobalState address "${pTaskDefinition.URLAddress}".`);
|
|
74
|
-
}
|
|
75
|
-
else
|
|
76
|
-
{
|
|
77
|
-
pManifestEntry.Log.push(`RestRequest: WARNING URLAddress "${pTaskDefinition.URLAddress}" could not be resolved, falling back to URL field.`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!tmpURL || tmpURL.length === 0)
|
|
82
|
-
{
|
|
83
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
84
|
-
pManifestEntry.Status = 'Error';
|
|
85
|
-
pManifestEntry.Log.push(`RestRequest: missing URL (neither URL nor URLAddress provided a value).`);
|
|
86
|
-
return fCallback(null, pManifestEntry);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let tmpMethod = (pTaskDefinition.Method || 'GET').toUpperCase();
|
|
90
|
-
|
|
91
|
-
pManifestEntry.Log.push(`RestRequest: ${tmpMethod} ${tmpURL}`);
|
|
92
|
-
|
|
93
|
-
// --- Build headers ---
|
|
94
|
-
let tmpHeaders = Object.assign({}, pTaskDefinition.Headers || {});
|
|
95
|
-
|
|
96
|
-
// --- Prepare request body ---
|
|
97
|
-
let tmpBody = '';
|
|
98
|
-
if (pTaskDefinition.hasOwnProperty('Body'))
|
|
99
|
-
{
|
|
100
|
-
if (typeof(pTaskDefinition.Body) === 'object' && pTaskDefinition.Body !== null)
|
|
101
|
-
{
|
|
102
|
-
tmpBody = JSON.stringify(pTaskDefinition.Body);
|
|
103
|
-
if (!tmpHeaders['Content-Type'] && !pTaskDefinition.ContentType)
|
|
104
|
-
{
|
|
105
|
-
tmpHeaders['Content-Type'] = 'application/json';
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
else if (typeof(pTaskDefinition.Body) === 'string')
|
|
109
|
-
{
|
|
110
|
-
tmpBody = pTaskDefinition.Body;
|
|
111
|
-
}
|
|
112
|
-
else
|
|
113
|
-
{
|
|
114
|
-
tmpBody = String(pTaskDefinition.Body);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (pTaskDefinition.ContentType && typeof(pTaskDefinition.ContentType) === 'string')
|
|
119
|
-
{
|
|
120
|
-
tmpHeaders['Content-Type'] = pTaskDefinition.ContentType;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (tmpBody.length > 0)
|
|
124
|
-
{
|
|
125
|
-
tmpHeaders['Content-Length'] = Buffer.byteLength(tmpBody);
|
|
126
|
-
pManifestEntry.Log.push(`RestRequest: body ${tmpBody.length} bytes.`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// --- Build cookie header ---
|
|
130
|
-
// Start with the shared cookie jar from GlobalState
|
|
131
|
-
if (!pContext.GlobalState || typeof(pContext.GlobalState) !== 'object')
|
|
132
|
-
{
|
|
133
|
-
pContext.GlobalState = {};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let tmpCookieJar = pContext.GlobalState.Cookies;
|
|
137
|
-
let tmpMergedCookies = {};
|
|
138
|
-
|
|
139
|
-
// Merge shared jar cookies first
|
|
140
|
-
if (tmpCookieJar && typeof(tmpCookieJar) === 'object')
|
|
141
|
-
{
|
|
142
|
-
let tmpJarKeys = Object.keys(tmpCookieJar);
|
|
143
|
-
for (let i = 0; i < tmpJarKeys.length; i++)
|
|
144
|
-
{
|
|
145
|
-
tmpMergedCookies[tmpJarKeys[i]] = tmpCookieJar[tmpJarKeys[i]];
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Merge task-level cookies on top (override jar values)
|
|
150
|
-
if (pTaskDefinition.Cookies && typeof(pTaskDefinition.Cookies) === 'object')
|
|
151
|
-
{
|
|
152
|
-
let tmpTaskCookieKeys = Object.keys(pTaskDefinition.Cookies);
|
|
153
|
-
for (let i = 0; i < tmpTaskCookieKeys.length; i++)
|
|
154
|
-
{
|
|
155
|
-
tmpMergedCookies[tmpTaskCookieKeys[i]] = pTaskDefinition.Cookies[tmpTaskCookieKeys[i]];
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let tmpCookieKeys = Object.keys(tmpMergedCookies);
|
|
160
|
-
if (tmpCookieKeys.length > 0)
|
|
161
|
-
{
|
|
162
|
-
let tmpCookieString = tmpCookieKeys
|
|
163
|
-
.map((pKey) => { return `${pKey}=${tmpMergedCookies[pKey]}`; })
|
|
164
|
-
.join('; ');
|
|
165
|
-
|
|
166
|
-
tmpHeaders['Cookie'] = tmpCookieString;
|
|
167
|
-
pManifestEntry.Log.push(`RestRequest: sending ${tmpCookieKeys.length} cookie(s).`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// --- Parse URL ---
|
|
171
|
-
let tmpParsedURL;
|
|
172
|
-
try
|
|
173
|
-
{
|
|
174
|
-
tmpParsedURL = new URL(tmpURL);
|
|
175
|
-
}
|
|
176
|
-
catch (pParseError)
|
|
177
|
-
{
|
|
178
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
179
|
-
pManifestEntry.Status = 'Error';
|
|
180
|
-
pManifestEntry.Log.push(`RestRequest: invalid URL: ${pParseError.message}`);
|
|
181
|
-
return fCallback(null, pManifestEntry);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let tmpTransport = (tmpParsedURL.protocol === 'https:') ? libHTTPS : libHTTP;
|
|
185
|
-
let tmpTimeout = (this.fable?.ProgramConfiguration?.UltravisorCommandTimeoutMilliseconds) || 300000;
|
|
186
|
-
let tmpMaxRetries = (typeof(pTaskDefinition.Retries) === 'number' && pTaskDefinition.Retries > 0) ? pTaskDefinition.Retries : 0;
|
|
187
|
-
|
|
188
|
-
let tmpRequestOptions = {
|
|
189
|
-
method: tmpMethod,
|
|
190
|
-
headers: tmpHeaders,
|
|
191
|
-
timeout: tmpTimeout
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// --- Retry wrapper ---
|
|
195
|
-
let tmpAttempt = 0;
|
|
196
|
-
let tmpSelf = this;
|
|
197
|
-
|
|
198
|
-
let fPerformRequest = () =>
|
|
199
|
-
{
|
|
200
|
-
tmpAttempt++;
|
|
201
|
-
|
|
202
|
-
if (tmpAttempt > 1)
|
|
203
|
-
{
|
|
204
|
-
pManifestEntry.Log.push(`RestRequest: retry attempt ${tmpAttempt - 1} of ${tmpMaxRetries}.`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let tmpRequest = tmpTransport.request(tmpURL, tmpRequestOptions,
|
|
208
|
-
(pResponse) =>
|
|
209
|
-
{
|
|
210
|
-
let tmpChunks = [];
|
|
211
|
-
|
|
212
|
-
pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
|
|
213
|
-
|
|
214
|
-
pResponse.on('end', () =>
|
|
215
|
-
{
|
|
216
|
-
let tmpBuffer = Buffer.concat(tmpChunks);
|
|
217
|
-
let tmpResponseText = tmpBuffer.toString('utf8');
|
|
218
|
-
|
|
219
|
-
pManifestEntry.Log.push(`RestRequest: received ${tmpBuffer.length} bytes, status ${pResponse.statusCode}.`);
|
|
220
|
-
|
|
221
|
-
// --- Check for retryable HTTP status ---
|
|
222
|
-
if (pResponse.statusCode >= 300 && tmpAttempt <= tmpMaxRetries)
|
|
223
|
-
{
|
|
224
|
-
pManifestEntry.Log.push(`RestRequest: non-2xx status ${pResponse.statusCode}, will retry in 1s.`);
|
|
225
|
-
return setTimeout(fPerformRequest, 1000);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
229
|
-
|
|
230
|
-
// --- Store response cookies in the shared jar ---
|
|
231
|
-
let tmpStoreCookies = (pTaskDefinition.StoreCookies !== false);
|
|
232
|
-
if (tmpStoreCookies)
|
|
233
|
-
{
|
|
234
|
-
tmpSelf._captureResponseCookies(pResponse, pContext, pManifestEntry);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// --- Build the result object ---
|
|
238
|
-
let tmpResult = {
|
|
239
|
-
StatusCode: pResponse.statusCode,
|
|
240
|
-
Headers: pResponse.headers,
|
|
241
|
-
Body: tmpResponseText
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
// Attempt to parse body as JSON for convenience
|
|
245
|
-
try
|
|
246
|
-
{
|
|
247
|
-
tmpResult.JSON = JSON.parse(tmpResponseText);
|
|
248
|
-
}
|
|
249
|
-
catch (pIgnore)
|
|
250
|
-
{
|
|
251
|
-
// Not JSON -- that's fine, raw Body is still available
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// --- CaptureToken: extract a token from JSON body into the cookie jar ---
|
|
255
|
-
if (pTaskDefinition.CaptureToken && tmpResult.JSON)
|
|
256
|
-
{
|
|
257
|
-
tmpSelf._captureToken(pTaskDefinition.CaptureToken, tmpResult.JSON, pContext, pManifestEntry);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// --- CaptureHeader: extract response headers into GlobalState ---
|
|
261
|
-
if (pTaskDefinition.CaptureHeader && typeof(pTaskDefinition.CaptureHeader) === 'object')
|
|
262
|
-
{
|
|
263
|
-
tmpSelf._captureHeaders(pTaskDefinition.CaptureHeader, pResponse.headers, pContext, pManifestEntry);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
pManifestEntry.Output = JSON.stringify(tmpResult);
|
|
267
|
-
|
|
268
|
-
if (pResponse.statusCode >= 300)
|
|
269
|
-
{
|
|
270
|
-
pManifestEntry.Status = 'Error';
|
|
271
|
-
pManifestEntry.Log.push(`RestRequest: ${tmpMethod} failed with status ${pResponse.statusCode} after ${tmpAttempt} attempt(s).`);
|
|
272
|
-
}
|
|
273
|
-
else
|
|
274
|
-
{
|
|
275
|
-
pManifestEntry.Status = 'Complete';
|
|
276
|
-
pManifestEntry.Success = true;
|
|
277
|
-
pManifestEntry.Log.push(`RestRequest: ${tmpMethod} completed successfully.`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Store via Destination
|
|
281
|
-
tmpSelf.storeDestination(pTaskDefinition, pContext, pManifestEntry, tmpResult);
|
|
282
|
-
|
|
283
|
-
// Persist
|
|
284
|
-
tmpSelf.storeResult(pTaskDefinition, pContext, pManifestEntry, tmpResult);
|
|
285
|
-
|
|
286
|
-
return fCallback(null, pManifestEntry);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
tmpRequest.on('error', (pError) =>
|
|
291
|
-
{
|
|
292
|
-
pManifestEntry.Log.push(`RestRequest: request error: ${pError.message}`);
|
|
293
|
-
|
|
294
|
-
if (tmpAttempt <= tmpMaxRetries)
|
|
295
|
-
{
|
|
296
|
-
pManifestEntry.Log.push(`RestRequest: will retry in 1s.`);
|
|
297
|
-
return setTimeout(fPerformRequest, 1000);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
301
|
-
pManifestEntry.Status = 'Error';
|
|
302
|
-
pManifestEntry.Log.push(`RestRequest: ${tmpMethod} failed after ${tmpAttempt} attempt(s).`);
|
|
303
|
-
return fCallback(null, pManifestEntry);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
tmpRequest.on('timeout', () =>
|
|
307
|
-
{
|
|
308
|
-
tmpRequest.destroy();
|
|
309
|
-
pManifestEntry.Log.push(`RestRequest: request timed out after ${tmpTimeout}ms.`);
|
|
310
|
-
|
|
311
|
-
if (tmpAttempt <= tmpMaxRetries)
|
|
312
|
-
{
|
|
313
|
-
pManifestEntry.Log.push(`RestRequest: will retry in 1s.`);
|
|
314
|
-
return setTimeout(fPerformRequest, 1000);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
318
|
-
pManifestEntry.Status = 'Error';
|
|
319
|
-
pManifestEntry.Log.push(`RestRequest: ${tmpMethod} failed after ${tmpAttempt} attempt(s).`);
|
|
320
|
-
return fCallback(null, pManifestEntry);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (tmpBody.length > 0)
|
|
324
|
-
{
|
|
325
|
-
tmpRequest.write(tmpBody);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
tmpRequest.end();
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
fPerformRequest();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Parse Set-Cookie response headers and merge them into the shared
|
|
336
|
-
* cookie jar at pContext.GlobalState.Cookies.
|
|
337
|
-
*
|
|
338
|
-
* @param {object} pResponse - The HTTP response.
|
|
339
|
-
* @param {object} pContext - Execution context.
|
|
340
|
-
* @param {object} pManifestEntry - Manifest entry for logging.
|
|
341
|
-
*/
|
|
342
|
-
_captureResponseCookies(pResponse, pContext, pManifestEntry)
|
|
343
|
-
{
|
|
344
|
-
let tmpSetCookieHeaders = pResponse.headers['set-cookie'];
|
|
345
|
-
|
|
346
|
-
if (!tmpSetCookieHeaders)
|
|
347
|
-
{
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Normalise to an array (some HTTP libs return a single string)
|
|
352
|
-
if (!Array.isArray(tmpSetCookieHeaders))
|
|
353
|
-
{
|
|
354
|
-
tmpSetCookieHeaders = [tmpSetCookieHeaders];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (!pContext.GlobalState.Cookies || typeof(pContext.GlobalState.Cookies) !== 'object')
|
|
358
|
-
{
|
|
359
|
-
pContext.GlobalState.Cookies = {};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
let tmpCapturedCount = 0;
|
|
363
|
-
for (let i = 0; i < tmpSetCookieHeaders.length; i++)
|
|
364
|
-
{
|
|
365
|
-
let tmpCookieStr = tmpSetCookieHeaders[i];
|
|
366
|
-
// Parse the first name=value pair from the Set-Cookie header
|
|
367
|
-
let tmpSemiIndex = tmpCookieStr.indexOf(';');
|
|
368
|
-
let tmpPairStr = (tmpSemiIndex !== -1) ? tmpCookieStr.substring(0, tmpSemiIndex) : tmpCookieStr;
|
|
369
|
-
let tmpEqualsIndex = tmpPairStr.indexOf('=');
|
|
370
|
-
|
|
371
|
-
if (tmpEqualsIndex !== -1)
|
|
372
|
-
{
|
|
373
|
-
let tmpName = tmpPairStr.substring(0, tmpEqualsIndex).trim();
|
|
374
|
-
let tmpValue = tmpPairStr.substring(tmpEqualsIndex + 1).trim();
|
|
375
|
-
pContext.GlobalState.Cookies[tmpName] = tmpValue;
|
|
376
|
-
tmpCapturedCount++;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (tmpCapturedCount > 0)
|
|
381
|
-
{
|
|
382
|
-
pManifestEntry.Log.push(`RestRequest: captured ${tmpCapturedCount} cookie(s) into GlobalState.Cookies.`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Extract a token from the JSON response body and store it in the
|
|
388
|
-
* shared cookie jar at pContext.GlobalState.Cookies.
|
|
389
|
-
*
|
|
390
|
-
* CaptureToken can be:
|
|
391
|
-
* - A string: dot-notation path into the JSON body. The resolved
|
|
392
|
-
* value is stored as a cookie named "Token".
|
|
393
|
-
* - An object with:
|
|
394
|
-
* { "Address": "JSON.path", "Cookie": "CookieName" }
|
|
395
|
-
*
|
|
396
|
-
* @param {string|object} pCaptureToken - The CaptureToken definition.
|
|
397
|
-
* @param {object} pJSON - The parsed JSON response body.
|
|
398
|
-
* @param {object} pContext - Execution context.
|
|
399
|
-
* @param {object} pManifestEntry - Manifest entry for logging.
|
|
400
|
-
*/
|
|
401
|
-
_captureToken(pCaptureToken, pJSON, pContext, pManifestEntry)
|
|
402
|
-
{
|
|
403
|
-
let tmpAddress;
|
|
404
|
-
let tmpCookieName;
|
|
405
|
-
|
|
406
|
-
if (typeof(pCaptureToken) === 'string')
|
|
407
|
-
{
|
|
408
|
-
tmpAddress = pCaptureToken;
|
|
409
|
-
tmpCookieName = 'Token';
|
|
410
|
-
}
|
|
411
|
-
else if (typeof(pCaptureToken) === 'object' && pCaptureToken !== null)
|
|
412
|
-
{
|
|
413
|
-
tmpAddress = pCaptureToken.Address || '';
|
|
414
|
-
tmpCookieName = pCaptureToken.Cookie || 'Token';
|
|
415
|
-
}
|
|
416
|
-
else
|
|
417
|
-
{
|
|
418
|
-
pManifestEntry.Log.push(`RestRequest: CaptureToken must be a string or object.`);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!tmpAddress || tmpAddress.length === 0)
|
|
423
|
-
{
|
|
424
|
-
pManifestEntry.Log.push(`RestRequest: CaptureToken address is empty.`);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Walk the JSON body to resolve the token value
|
|
429
|
-
let tmpParts = tmpAddress.split('.');
|
|
430
|
-
let tmpValue = pJSON;
|
|
431
|
-
for (let i = 0; i < tmpParts.length; i++)
|
|
432
|
-
{
|
|
433
|
-
if (tmpValue === null || tmpValue === undefined || typeof(tmpValue) !== 'object')
|
|
434
|
-
{
|
|
435
|
-
pManifestEntry.Log.push(`RestRequest: CaptureToken could not resolve "${tmpAddress}" in response body.`);
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
tmpValue = tmpValue[tmpParts[i]];
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (tmpValue === undefined || tmpValue === null)
|
|
442
|
-
{
|
|
443
|
-
pManifestEntry.Log.push(`RestRequest: CaptureToken resolved "${tmpAddress}" to null/undefined.`);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (!pContext.GlobalState.Cookies || typeof(pContext.GlobalState.Cookies) !== 'object')
|
|
448
|
-
{
|
|
449
|
-
pContext.GlobalState.Cookies = {};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
pContext.GlobalState.Cookies[tmpCookieName] = String(tmpValue);
|
|
453
|
-
pManifestEntry.Log.push(`RestRequest: CaptureToken stored "${tmpAddress}" as cookie "${tmpCookieName}".`);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Extract response header values and store them at manyfest
|
|
458
|
-
* addresses in pContext.GlobalState.
|
|
459
|
-
*
|
|
460
|
-
* CaptureHeader is an object mapping response header names to
|
|
461
|
-
* GlobalState dot-notation addresses. Header names are matched
|
|
462
|
-
* case-insensitively.
|
|
463
|
-
*
|
|
464
|
-
* @param {object} pCaptureHeader - Mapping of header name → GlobalState address.
|
|
465
|
-
* @param {object} pResponseHeaders - The response headers (lowercased by Node).
|
|
466
|
-
* @param {object} pContext - Execution context.
|
|
467
|
-
* @param {object} pManifestEntry - Manifest entry for logging.
|
|
468
|
-
*/
|
|
469
|
-
_captureHeaders(pCaptureHeader, pResponseHeaders, pContext, pManifestEntry)
|
|
470
|
-
{
|
|
471
|
-
let tmpKeys = Object.keys(pCaptureHeader);
|
|
472
|
-
let tmpCapturedCount = 0;
|
|
473
|
-
|
|
474
|
-
for (let i = 0; i < tmpKeys.length; i++)
|
|
475
|
-
{
|
|
476
|
-
let tmpHeaderName = tmpKeys[i].toLowerCase();
|
|
477
|
-
let tmpDestAddress = pCaptureHeader[tmpKeys[i]];
|
|
478
|
-
|
|
479
|
-
if (typeof(tmpDestAddress) !== 'string' || tmpDestAddress.length === 0)
|
|
480
|
-
{
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
let tmpHeaderValue = pResponseHeaders[tmpHeaderName];
|
|
485
|
-
if (tmpHeaderValue !== undefined)
|
|
486
|
-
{
|
|
487
|
-
this._Manyfest.setValueAtAddress(pContext.GlobalState, tmpDestAddress, tmpHeaderValue);
|
|
488
|
-
tmpCapturedCount++;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (tmpCapturedCount > 0)
|
|
493
|
-
{
|
|
494
|
-
pManifestEntry.Log.push(`RestRequest: CaptureHeader stored ${tmpCapturedCount} header value(s) into GlobalState.`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
module.exports = UltravisorTaskRestRequest;
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
const libUltravisorTaskBase = require('../Ultravisor-Task-Base.cjs');
|
|
2
|
-
|
|
3
|
-
const libHTTP = require('http');
|
|
4
|
-
const libHTTPS = require('https');
|
|
5
|
-
|
|
6
|
-
class UltravisorTaskSendJSON extends libUltravisorTaskBase
|
|
7
|
-
{
|
|
8
|
-
constructor(pFable)
|
|
9
|
-
{
|
|
10
|
-
super(pFable);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Send JSON data to a REST URL using any HTTP method.
|
|
15
|
-
*
|
|
16
|
-
* Task definition fields:
|
|
17
|
-
* - URL: the endpoint to request
|
|
18
|
-
* - Method (optional): HTTP method (defaults to POST)
|
|
19
|
-
* - Data (optional): object to serialise and send as the request body
|
|
20
|
-
* - Headers (optional): object of request headers
|
|
21
|
-
* - Persist (optional): where to store the response
|
|
22
|
-
* string -- manyfest address into GlobalState
|
|
23
|
-
* { Address: "..." } -- same as string form
|
|
24
|
-
* { File: "..." } -- writes response to a staging file
|
|
25
|
-
*/
|
|
26
|
-
execute(pTaskDefinition, pContext, pManifestEntry, fCallback)
|
|
27
|
-
{
|
|
28
|
-
let tmpURL = pTaskDefinition.URL || pTaskDefinition.Parameters || '';
|
|
29
|
-
|
|
30
|
-
if (!tmpURL || tmpURL.length === 0)
|
|
31
|
-
{
|
|
32
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
33
|
-
pManifestEntry.Status = 'Error';
|
|
34
|
-
pManifestEntry.Log.push(`SendJSON: missing URL.`);
|
|
35
|
-
return fCallback(null, pManifestEntry);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let tmpMethod = (pTaskDefinition.Method || 'POST').toUpperCase();
|
|
39
|
-
let tmpBody = pTaskDefinition.hasOwnProperty('Data')
|
|
40
|
-
? JSON.stringify(pTaskDefinition.Data)
|
|
41
|
-
: '';
|
|
42
|
-
|
|
43
|
-
pManifestEntry.Log.push(`SendJSON: ${tmpMethod} ${tmpURL} (${tmpBody.length} bytes body)`);
|
|
44
|
-
|
|
45
|
-
let tmpHeaders = Object.assign(
|
|
46
|
-
{
|
|
47
|
-
'Content-Type': 'application/json',
|
|
48
|
-
'Accept': 'application/json'
|
|
49
|
-
},
|
|
50
|
-
pTaskDefinition.Headers || {});
|
|
51
|
-
|
|
52
|
-
if (tmpBody.length > 0)
|
|
53
|
-
{
|
|
54
|
-
tmpHeaders['Content-Length'] = Buffer.byteLength(tmpBody);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let tmpParsedURL;
|
|
58
|
-
try
|
|
59
|
-
{
|
|
60
|
-
tmpParsedURL = new URL(tmpURL);
|
|
61
|
-
}
|
|
62
|
-
catch (pParseError)
|
|
63
|
-
{
|
|
64
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
65
|
-
pManifestEntry.Status = 'Error';
|
|
66
|
-
pManifestEntry.Log.push(`SendJSON: invalid URL: ${pParseError.message}`);
|
|
67
|
-
return fCallback(null, pManifestEntry);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let tmpTransport = (tmpParsedURL.protocol === 'https:') ? libHTTPS : libHTTP;
|
|
71
|
-
let tmpTimeout = (this.fable?.ProgramConfiguration?.UltravisorCommandTimeoutMilliseconds) || 300000;
|
|
72
|
-
|
|
73
|
-
let tmpRequestOptions = {
|
|
74
|
-
method: tmpMethod,
|
|
75
|
-
headers: tmpHeaders,
|
|
76
|
-
timeout: tmpTimeout
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
let tmpRequest = tmpTransport.request(tmpURL, tmpRequestOptions,
|
|
80
|
-
(pResponse) =>
|
|
81
|
-
{
|
|
82
|
-
let tmpData = '';
|
|
83
|
-
|
|
84
|
-
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
85
|
-
|
|
86
|
-
pResponse.on('end', () =>
|
|
87
|
-
{
|
|
88
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
89
|
-
pManifestEntry.Log.push(`SendJSON: received ${tmpData.length} bytes, status ${pResponse.statusCode}.`);
|
|
90
|
-
|
|
91
|
-
try
|
|
92
|
-
{
|
|
93
|
-
let tmpParsed = JSON.parse(tmpData);
|
|
94
|
-
pManifestEntry.Output = JSON.stringify(tmpParsed);
|
|
95
|
-
}
|
|
96
|
-
catch (pJsonError)
|
|
97
|
-
{
|
|
98
|
-
pManifestEntry.Output = tmpData.substring(0, 2000);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
pManifestEntry.Status = 'Complete';
|
|
102
|
-
pManifestEntry.Success = true;
|
|
103
|
-
pManifestEntry.Log.push(`SendJSON: ${tmpMethod} completed successfully.`);
|
|
104
|
-
|
|
105
|
-
// Persist the response (parsed JSON if possible, raw string otherwise)
|
|
106
|
-
if (pTaskDefinition.Persist)
|
|
107
|
-
{
|
|
108
|
-
let tmpPersistData;
|
|
109
|
-
try
|
|
110
|
-
{
|
|
111
|
-
tmpPersistData = JSON.parse(pManifestEntry.Output);
|
|
112
|
-
}
|
|
113
|
-
catch (pIgnore)
|
|
114
|
-
{
|
|
115
|
-
tmpPersistData = pManifestEntry.Output;
|
|
116
|
-
}
|
|
117
|
-
this.storeResult(pTaskDefinition, pContext, pManifestEntry, tmpPersistData);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return fCallback(null, pManifestEntry);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
tmpRequest.on('error', (pError) =>
|
|
125
|
-
{
|
|
126
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
127
|
-
pManifestEntry.Status = 'Error';
|
|
128
|
-
pManifestEntry.Log.push(`SendJSON: request error: ${pError.message}`);
|
|
129
|
-
return fCallback(null, pManifestEntry);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
tmpRequest.on('timeout', () =>
|
|
133
|
-
{
|
|
134
|
-
tmpRequest.destroy();
|
|
135
|
-
pManifestEntry.StopTime = new Date().toISOString();
|
|
136
|
-
pManifestEntry.Status = 'Error';
|
|
137
|
-
pManifestEntry.Log.push(`SendJSON: request timed out after ${tmpTimeout}ms.`);
|
|
138
|
-
return fCallback(null, pManifestEntry);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (tmpBody.length > 0)
|
|
142
|
-
{
|
|
143
|
-
tmpRequest.write(tmpBody);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
tmpRequest.end();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
module.exports = UltravisorTaskSendJSON;
|