@inteli.city/node-red-contrib-http-plus 1.0.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 +202 -0
- package/README.md +516 -0
- package/http-auth-config+.html +167 -0
- package/http-auth-config+.js +29 -0
- package/httpin+.html +494 -0
- package/httpin+.js +626 -0
- package/httpproxy+.html +140 -0
- package/httpproxy+.js +138 -0
- package/httprequest+.html +233 -0
- package/httprequest+.js +311 -0
- package/libs/swagger.js +213 -0
- package/package.json +37 -0
package/httprequest+.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright JS Foundation and other contributors, http://js.foundation
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
const STATUS_INTERVAL_MS = 1000;
|
|
19
|
+
|
|
20
|
+
module.exports = async function(RED) {
|
|
21
|
+
const { got } = await import('got');
|
|
22
|
+
const mustache = require("mustache");
|
|
23
|
+
|
|
24
|
+
// --- HTTP admin endpoints (registered once per runtime) ---
|
|
25
|
+
RED.httpAdmin.get("/http-request-plus/:id/status", function(req, res) {
|
|
26
|
+
const n = RED.nodes.getNode(req.params.id);
|
|
27
|
+
if (!n || typeof n.getStatus !== "function") { return res.sendStatus(404); }
|
|
28
|
+
res.json(n.getStatus());
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
RED.httpAdmin.post("/http-request-plus/:id/kill", function(req, res) {
|
|
32
|
+
const n = RED.nodes.getNode(req.params.id);
|
|
33
|
+
if (!n || typeof n.killAll !== "function") { return res.sendStatus(404); }
|
|
34
|
+
n.killAll();
|
|
35
|
+
res.sendStatus(200);
|
|
36
|
+
});
|
|
37
|
+
// ---
|
|
38
|
+
|
|
39
|
+
function HTTPRequest(n) {
|
|
40
|
+
RED.nodes.createNode(this, n);
|
|
41
|
+
const node = this;
|
|
42
|
+
const nodeUrl = n.url;
|
|
43
|
+
const isTemplatedUrl = (nodeUrl || "").indexOf("{{") !== -1;
|
|
44
|
+
const nodeMethod = (n.method || "GET").toUpperCase();
|
|
45
|
+
node.ret = n.ret || "txt";
|
|
46
|
+
node.queueLimit = Math.max(1, Number(n.queue) || 1);
|
|
47
|
+
|
|
48
|
+
let pendingQueue = [];
|
|
49
|
+
let activeCount = 0;
|
|
50
|
+
let activeJobs = new Set();
|
|
51
|
+
let closing = false;
|
|
52
|
+
|
|
53
|
+
// --- state tracking ---
|
|
54
|
+
let currentState = { waiting: 0, executing: 0 };
|
|
55
|
+
let lastEmittedState = null;
|
|
56
|
+
let statusTimer = null;
|
|
57
|
+
let lastEmitTime = 0;
|
|
58
|
+
|
|
59
|
+
function closeError() {
|
|
60
|
+
const err = new Error("Node closed");
|
|
61
|
+
err.nodeClosing = true;
|
|
62
|
+
return err;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function computeState() {
|
|
66
|
+
return { waiting: pendingQueue.length, executing: activeCount };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function computeStatus(state) {
|
|
70
|
+
const method = nodeMethod === "use" ? "HTTP" : nodeMethod;
|
|
71
|
+
const urlLabel = (n.url || "").replace(/^https?:\/\//, "").slice(0, 25);
|
|
72
|
+
return {
|
|
73
|
+
fill: state.executing > 0 ? "blue" : "grey",
|
|
74
|
+
shape: state.executing > 0 ? "ring" : "dot",
|
|
75
|
+
text: state.waiting + " (" + state.executing + "/" + node.queueLimit + ") " + method + " " + urlLabel
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function emitStatus() {
|
|
80
|
+
lastEmitTime = Date.now();
|
|
81
|
+
if (
|
|
82
|
+
lastEmittedState &&
|
|
83
|
+
currentState.waiting === lastEmittedState.waiting &&
|
|
84
|
+
currentState.executing === lastEmittedState.executing
|
|
85
|
+
) { return; }
|
|
86
|
+
lastEmittedState = { waiting: currentState.waiting, executing: currentState.executing };
|
|
87
|
+
node.status(computeStatus(currentState));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scheduleStatusEmit() {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const idle = currentState.waiting === 0 && currentState.executing === 0;
|
|
93
|
+
if (idle) {
|
|
94
|
+
if (statusTimer) { clearTimeout(statusTimer); statusTimer = null; }
|
|
95
|
+
emitStatus();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (now - lastEmitTime >= STATUS_INTERVAL_MS) {
|
|
99
|
+
emitStatus();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!statusTimer) {
|
|
103
|
+
const delay = STATUS_INTERVAL_MS - (now - lastEmitTime);
|
|
104
|
+
statusTimer = setTimeout(function() {
|
|
105
|
+
statusTimer = null;
|
|
106
|
+
emitStatus();
|
|
107
|
+
}, delay);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function computeAndStoreState() {
|
|
112
|
+
currentState = computeState();
|
|
113
|
+
scheduleStatusEmit();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- request execution ---
|
|
117
|
+
async function performRequest(msg, signal) {
|
|
118
|
+
let url = nodeUrl || msg.url;
|
|
119
|
+
if (!url) { throw new Error("URL is required"); }
|
|
120
|
+
if (isTemplatedUrl) { url = mustache.render(nodeUrl, msg); }
|
|
121
|
+
if (!/^https?:\/\//i.test(url)) { url = "http://" + url; }
|
|
122
|
+
|
|
123
|
+
const method = (n.method === "use" && msg.method ? msg.method : nodeMethod).toUpperCase();
|
|
124
|
+
|
|
125
|
+
// Headers — silently ignore arrays or invalid JSON
|
|
126
|
+
let nodeHeaders = {};
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(n.headers || "{}");
|
|
129
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
130
|
+
nodeHeaders = parsed;
|
|
131
|
+
}
|
|
132
|
+
} catch(e) { /* fallback to {} */ }
|
|
133
|
+
const msgHeaders = (msg.headers && typeof msg.headers === 'object' && !Array.isArray(msg.headers))
|
|
134
|
+
? msg.headers : {};
|
|
135
|
+
const headers = Object.assign({}, msgHeaders, nodeHeaders);
|
|
136
|
+
|
|
137
|
+
const opts = {
|
|
138
|
+
method,
|
|
139
|
+
headers,
|
|
140
|
+
timeout: { request: parseInt(RED.settings.httpRequestTimeout) || 120000 },
|
|
141
|
+
throwHttpErrors: false,
|
|
142
|
+
retry: { limit: 0 },
|
|
143
|
+
responseType: 'buffer',
|
|
144
|
+
decompress: true
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (signal) { opts.signal = signal; }
|
|
148
|
+
|
|
149
|
+
// Payload routing
|
|
150
|
+
if (method === 'GET' || method === 'DELETE') {
|
|
151
|
+
if (msg.payload !== null && msg.payload !== undefined &&
|
|
152
|
+
typeof msg.payload === 'object' && !Array.isArray(msg.payload)) {
|
|
153
|
+
opts.searchParams = msg.payload;
|
|
154
|
+
}
|
|
155
|
+
// non-object payloads silently ignored for GET/DELETE
|
|
156
|
+
} else if (method !== 'HEAD') {
|
|
157
|
+
let body;
|
|
158
|
+
if (n.payloadAsBody) {
|
|
159
|
+
body = msg.payload;
|
|
160
|
+
} else {
|
|
161
|
+
try { body = JSON.parse(n.body || "{}"); } catch(e) { body = {}; }
|
|
162
|
+
if (msg.payload !== undefined) body = msg.payload;
|
|
163
|
+
}
|
|
164
|
+
if (typeof body === 'object' && !Buffer.isBuffer(body)) {
|
|
165
|
+
opts.json = body;
|
|
166
|
+
} else if (typeof body === 'string' || Buffer.isBuffer(body)) {
|
|
167
|
+
opts.body = body;
|
|
168
|
+
} else if (typeof body === 'number') {
|
|
169
|
+
opts.body = String(body);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const res = await got(url, opts);
|
|
174
|
+
|
|
175
|
+
msg.statusCode = res.statusCode;
|
|
176
|
+
msg.headers = res.headers;
|
|
177
|
+
msg.responseUrl = res.url;
|
|
178
|
+
msg.payload = res.body;
|
|
179
|
+
|
|
180
|
+
if (node.ret !== 'bin') {
|
|
181
|
+
msg.payload = msg.payload.toString('utf8');
|
|
182
|
+
if (node.ret === 'obj') {
|
|
183
|
+
if (res.statusCode === 204) {
|
|
184
|
+
msg.payload = {};
|
|
185
|
+
} else {
|
|
186
|
+
try { msg.payload = JSON.parse(msg.payload); }
|
|
187
|
+
catch(e) { node.warn("http.request+: response is not valid JSON"); }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return msg;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- queue engine ---
|
|
196
|
+
function executeTask(task) {
|
|
197
|
+
let cancelled = false;
|
|
198
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
199
|
+
const signal = controller ? controller.signal : null;
|
|
200
|
+
|
|
201
|
+
task.cancel = function() {
|
|
202
|
+
if (!cancelled) {
|
|
203
|
+
cancelled = true;
|
|
204
|
+
if (controller) { controller.abort(); }
|
|
205
|
+
activeCount--;
|
|
206
|
+
task.reject(closeError());
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
activeJobs.add(task);
|
|
211
|
+
|
|
212
|
+
performRequest(task.msg, signal)
|
|
213
|
+
.then(function(resultMsg) {
|
|
214
|
+
activeJobs.delete(task);
|
|
215
|
+
if (!cancelled) {
|
|
216
|
+
activeCount--;
|
|
217
|
+
processQueue();
|
|
218
|
+
computeAndStoreState();
|
|
219
|
+
task.resolve(resultMsg);
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
.catch(function(err) {
|
|
223
|
+
activeJobs.delete(task);
|
|
224
|
+
if (!cancelled) {
|
|
225
|
+
activeCount--;
|
|
226
|
+
processQueue();
|
|
227
|
+
computeAndStoreState();
|
|
228
|
+
if (err.response) {
|
|
229
|
+
err.statusCode = err.response.statusCode;
|
|
230
|
+
err.responseBody = err.response.body;
|
|
231
|
+
}
|
|
232
|
+
task.reject(err);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function processQueue() {
|
|
238
|
+
while (activeCount < node.queueLimit && pendingQueue.length > 0) {
|
|
239
|
+
const task = pendingQueue.shift();
|
|
240
|
+
activeCount++;
|
|
241
|
+
executeTask(task);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function enqueue(msg, send, done) {
|
|
246
|
+
new Promise(function(resolve, reject) {
|
|
247
|
+
if (closing) { return reject(closeError()); }
|
|
248
|
+
pendingQueue.push({ msg, resolve, reject });
|
|
249
|
+
processQueue();
|
|
250
|
+
computeAndStoreState();
|
|
251
|
+
}).then(function(resultMsg) {
|
|
252
|
+
send(resultMsg);
|
|
253
|
+
done();
|
|
254
|
+
}).catch(function(err) {
|
|
255
|
+
if (!err.nodeClosing) {
|
|
256
|
+
const statusCode = err.statusCode;
|
|
257
|
+
const label = statusCode
|
|
258
|
+
? statusCode + " " + err.message
|
|
259
|
+
: err.message;
|
|
260
|
+
node.error("http.request+: " + label, msg);
|
|
261
|
+
node.status({ fill: "red", shape: "ring", text: label });
|
|
262
|
+
msg.payload = err.responseBody ? err.responseBody.toString('utf8') : err.message;
|
|
263
|
+
msg.statusCode = statusCode || err.code;
|
|
264
|
+
send(msg);
|
|
265
|
+
}
|
|
266
|
+
done();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- public API ---
|
|
271
|
+
node.getStatus = function() {
|
|
272
|
+
return { executing: activeCount, waiting: pendingQueue.length };
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
node.killAll = function() {
|
|
276
|
+
for (const job of activeJobs) {
|
|
277
|
+
try { job.cancel(); } catch (_) {}
|
|
278
|
+
}
|
|
279
|
+
activeJobs.clear();
|
|
280
|
+
|
|
281
|
+
for (const task of pendingQueue) {
|
|
282
|
+
try { task.reject(closeError()); } catch (_) {}
|
|
283
|
+
}
|
|
284
|
+
pendingQueue = [];
|
|
285
|
+
|
|
286
|
+
computeAndStoreState();
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
node.on("close", function(done) {
|
|
290
|
+
closing = true;
|
|
291
|
+
if (statusTimer) { clearTimeout(statusTimer); statusTimer = null; }
|
|
292
|
+
node.killAll();
|
|
293
|
+
node.status({});
|
|
294
|
+
done();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
computeAndStoreState();
|
|
298
|
+
|
|
299
|
+
node.on("input", function(msg, send, done) {
|
|
300
|
+
if (closing) { done(); return; }
|
|
301
|
+
if (msg.stop === true) {
|
|
302
|
+
node.killAll();
|
|
303
|
+
done();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
enqueue(msg, send, done);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
RED.nodes.registerType("http.request+", HTTPRequest);
|
|
311
|
+
};
|
package/libs/swagger.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const endpoints = new Map();
|
|
4
|
+
let cachedSpec = null;
|
|
5
|
+
|
|
6
|
+
function normalizePath(path) {
|
|
7
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function zodToJsonSchema(zodType) {
|
|
11
|
+
if (!zodType || !zodType._def) return {};
|
|
12
|
+
|
|
13
|
+
const def = zodType._def;
|
|
14
|
+
const typeName = def.typeName;
|
|
15
|
+
|
|
16
|
+
if (typeName === "ZodString") return { type: "string" };
|
|
17
|
+
if (typeName === "ZodNumber") return { type: "number" };
|
|
18
|
+
if (typeName === "ZodBoolean") return { type: "boolean" };
|
|
19
|
+
|
|
20
|
+
if (typeName === "ZodEnum") {
|
|
21
|
+
return { type: "string", enum: def.values };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeName === "ZodArray") {
|
|
25
|
+
return { type: "array", items: zodToJsonSchema(def.type) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeName === "ZodObject") {
|
|
29
|
+
const properties = {};
|
|
30
|
+
const required = [];
|
|
31
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
32
|
+
for (const key in shape) {
|
|
33
|
+
const field = shape[key];
|
|
34
|
+
const fieldDef = field._def;
|
|
35
|
+
const isOptional = fieldDef.typeName === "ZodOptional";
|
|
36
|
+
const innerType = isOptional ? fieldDef.innerType : field;
|
|
37
|
+
properties[key] = zodToJsonSchema(innerType);
|
|
38
|
+
if (!isOptional) required.push(key);
|
|
39
|
+
}
|
|
40
|
+
const schema = { type: "object", properties };
|
|
41
|
+
if (required.length > 0) schema.required = required;
|
|
42
|
+
return schema;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeName === "ZodOptional") {
|
|
46
|
+
return zodToJsonSchema(def.innerType);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeName === "ZodUnion") {
|
|
50
|
+
return { oneOf: def.options.map(zodToJsonSchema) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildOperationObject(meta) {
|
|
57
|
+
const { schema, method } = meta;
|
|
58
|
+
const def = schema._def;
|
|
59
|
+
const typeName = def && def.typeName;
|
|
60
|
+
|
|
61
|
+
// Expect top-level z.object with keys: body, query, params
|
|
62
|
+
if (typeName !== "ZodObject") {
|
|
63
|
+
return { summary: "Auto-generated" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
67
|
+
|
|
68
|
+
const parameters = [];
|
|
69
|
+
const pathParamNames = [];
|
|
70
|
+
|
|
71
|
+
// Extract {param} names from normalized path
|
|
72
|
+
const normalizedPath = normalizePath(meta.path);
|
|
73
|
+
const pathParamRegex = /\{([^}]+)\}/g;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = pathParamRegex.exec(normalizedPath)) !== null) {
|
|
76
|
+
pathParamNames.push(m[1]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// params → path parameters
|
|
80
|
+
if (shape.params) {
|
|
81
|
+
const paramsDef = shape.params._def;
|
|
82
|
+
const paramsTypeName = paramsDef && paramsDef.typeName;
|
|
83
|
+
if (paramsTypeName === "ZodObject") {
|
|
84
|
+
const paramsShape = typeof paramsDef.shape === "function" ? paramsDef.shape() : paramsDef.shape;
|
|
85
|
+
for (const key in paramsShape) {
|
|
86
|
+
parameters.push({
|
|
87
|
+
name: key,
|
|
88
|
+
in: "path",
|
|
89
|
+
required: true,
|
|
90
|
+
schema: zodToJsonSchema(paramsShape[key])
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// auto-inject path params from URL if no explicit params schema
|
|
96
|
+
for (const name of pathParamNames) {
|
|
97
|
+
parameters.push({ name, in: "path", required: true, schema: { type: "string" } });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// query → query parameters
|
|
102
|
+
if (shape.query) {
|
|
103
|
+
const queryDef = shape.query._def;
|
|
104
|
+
const queryTypeName = queryDef && queryDef.typeName;
|
|
105
|
+
if (queryTypeName === "ZodObject") {
|
|
106
|
+
const queryShape = typeof queryDef.shape === "function" ? queryDef.shape() : queryDef.shape;
|
|
107
|
+
for (const key in queryShape) {
|
|
108
|
+
const fieldDef = queryShape[key]._def;
|
|
109
|
+
const isOptional = fieldDef.typeName === "ZodOptional";
|
|
110
|
+
parameters.push({
|
|
111
|
+
name: key,
|
|
112
|
+
in: "query",
|
|
113
|
+
required: !isOptional,
|
|
114
|
+
schema: zodToJsonSchema(isOptional ? fieldDef.innerType : queryShape[key])
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (method === "get" && shape.body) {
|
|
121
|
+
console.warn(
|
|
122
|
+
"[swagger] GET " + meta.path + " defines a body schema. " +
|
|
123
|
+
"GET requests should use 'query' instead of 'body'."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const operation = { summary: shape.body ? "Auto-generated (validated request)" : "Auto-generated" };
|
|
128
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
129
|
+
|
|
130
|
+
// body → requestBody (only relevant for non-GET methods)
|
|
131
|
+
if (shape.body && method !== "get") {
|
|
132
|
+
operation.requestBody = {
|
|
133
|
+
required: true,
|
|
134
|
+
content: {
|
|
135
|
+
"application/json": {
|
|
136
|
+
schema: zodToJsonSchema(shape.body)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
operation.responses = { "200": { description: "OK" } };
|
|
143
|
+
|
|
144
|
+
return operation;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function registerEndpoint(meta) {
|
|
148
|
+
if (!meta || !meta.schema || !meta.nodeId) return;
|
|
149
|
+
cachedSpec = null;
|
|
150
|
+
endpoints.set(meta.nodeId, meta);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unregisterEndpoint(nodeId) {
|
|
154
|
+
if (!nodeId) return;
|
|
155
|
+
endpoints.delete(nodeId);
|
|
156
|
+
cachedSpec = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function generateSpec() {
|
|
160
|
+
const paths = {};
|
|
161
|
+
|
|
162
|
+
for (const meta of endpoints.values()) {
|
|
163
|
+
const normalizedPath = normalizePath(meta.path);
|
|
164
|
+
if (!paths[normalizedPath]) paths[normalizedPath] = {};
|
|
165
|
+
paths[normalizedPath][meta.method] = buildOperationObject(meta);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
cachedSpec = {
|
|
169
|
+
openapi: "3.0.0",
|
|
170
|
+
info: { title: "Node-RED API", version: "1.0.0" },
|
|
171
|
+
paths
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return cachedSpec;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getCachedSpec() {
|
|
178
|
+
if (cachedSpec) return cachedSpec;
|
|
179
|
+
return generateSpec();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function registerRoutes(RED) {
|
|
183
|
+
RED.httpNode.get("/openapi.json", function(_req, res) {
|
|
184
|
+
res.json(getCachedSpec());
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
RED.httpNode.get("/docs", function(_req, res) {
|
|
188
|
+
res.setHeader("Content-Type", "text/html");
|
|
189
|
+
res.send(`<!DOCTYPE html>
|
|
190
|
+
<html lang="en">
|
|
191
|
+
<head>
|
|
192
|
+
<meta charset="UTF-8" />
|
|
193
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
194
|
+
<title>Node-RED API Docs</title>
|
|
195
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
|
|
196
|
+
</head>
|
|
197
|
+
<body>
|
|
198
|
+
<div id="swagger-ui"></div>
|
|
199
|
+
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
200
|
+
<script>
|
|
201
|
+
SwaggerUIBundle({
|
|
202
|
+
url: "/openapi.json",
|
|
203
|
+
dom_id: "#swagger-ui",
|
|
204
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
|
|
205
|
+
layout: "BaseLayout"
|
|
206
|
+
});
|
|
207
|
+
</script>
|
|
208
|
+
</body>
|
|
209
|
+
</html>`);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { registerEndpoint, unregisterEndpoint, generateSpec, getCachedSpec, registerRoutes };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inteli.city/node-red-contrib-http-plus",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"body-parser": "1.20.3",
|
|
6
|
+
"content-type": "1.0.5",
|
|
7
|
+
"cookie-parser": "1.4.7",
|
|
8
|
+
"cookie": "0.7.2",
|
|
9
|
+
"cors": "2.8.5",
|
|
10
|
+
"form-data": "4.0.4",
|
|
11
|
+
"got": "12.6.1",
|
|
12
|
+
"hash-sum": "2.0.0",
|
|
13
|
+
"hpagent": "1.2.0",
|
|
14
|
+
"is-utf8": "0.2.1",
|
|
15
|
+
"media-typer": "1.1.0",
|
|
16
|
+
"multer": "2.0.2",
|
|
17
|
+
"mustache": "4.2.0",
|
|
18
|
+
"on-headers": "1.1.0",
|
|
19
|
+
"raw-body": "3.0.0",
|
|
20
|
+
"tough-cookie": "5.1.2",
|
|
21
|
+
"uuid": "9.0.1",
|
|
22
|
+
"jsonwebtoken": "^9.0.2",
|
|
23
|
+
"jwks-rsa": "^3.1.0",
|
|
24
|
+
"zod": "^3.23.8"
|
|
25
|
+
},
|
|
26
|
+
"keywords": ["node-red"],
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"node-red": {
|
|
29
|
+
"version": ">=2.0.0",
|
|
30
|
+
"nodes": {
|
|
31
|
+
"httpproxy+": "httpproxy+.js",
|
|
32
|
+
"http-auth-config+": "http-auth-config+.js",
|
|
33
|
+
"httpin+": "httpin+.js",
|
|
34
|
+
"httprequest+": "httprequest+.js"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|