@5minds/node-red-contrib-processcube-tools 1.1.0-feature-6eab97-mg0ov11s → 1.1.0-feature-975857-mg4vqpai

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Copyright 2015, 2016 IBM Corp.
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
+ const DEFAULT_TEMPLATE = {
18
+ openapi: '3.0.0',
19
+ info: {
20
+ title: 'My Node-RED API',
21
+ version: '1.0.0',
22
+ description: 'A sample API',
23
+ },
24
+ servers: [
25
+ {
26
+ url: 'http://localhost:1880/',
27
+ description: 'Local server',
28
+ },
29
+ ],
30
+ paths: {},
31
+ components: {
32
+ schemas: {},
33
+ responses: {},
34
+ parameters: {},
35
+ securitySchemes: {},
36
+ },
37
+ tags: [],
38
+ };
39
+
40
+ module.exports = function (RED) {
41
+ 'use strict';
42
+
43
+ const path = require('path');
44
+
45
+ const convToSwaggerPath = (x) => `/{${x.substring(2)}}`;
46
+ const trimAll = (ary) => ary.map((x) => x.trim());
47
+ const csvStrToArray = (csvStr) => (csvStr ? trimAll(csvStr.split(',')) : []);
48
+ const ensureLeadingSlash = (url) => (url.startsWith('/') ? url : '/' + url);
49
+ const stripTerminalSlash = (url) => (url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url);
50
+ const regexColons = /\/:\w*/g;
51
+
52
+ // Helper function to convert collection format to OpenAPI 3.0 style
53
+ const getStyleFromCollectionFormat = (collectionFormat) => {
54
+ const formatMap = {
55
+ csv: 'simple',
56
+ ssv: 'spaceDelimited',
57
+ tsv: 'pipeDelimited',
58
+ pipes: 'pipeDelimited',
59
+ multi: 'form',
60
+ };
61
+ return formatMap[collectionFormat] || 'simple';
62
+ };
63
+
64
+ RED.httpNode.get('/http-api/swagger.json', (req, res) => {
65
+ try {
66
+ const { httpNodeRoot, openapi: { template = {}, parameters: additionalParams = [] } = {} } = RED.settings;
67
+
68
+ const resp = { ...DEFAULT_TEMPLATE, ...template };
69
+ resp.paths = {};
70
+
71
+ // Update server URL to include the httpNodeRoot
72
+ if (httpNodeRoot && httpNodeRoot !== '/') {
73
+ resp.servers = resp.servers.map((server) => ({
74
+ ...server,
75
+ url: server.url.replace(/\/$/, '') + httpNodeRoot,
76
+ }));
77
+ }
78
+
79
+ RED.nodes.eachNode((node) => {
80
+ const { name, type, method, swaggerDoc, url } = node;
81
+
82
+ if (type === 'http in' && swaggerDoc) {
83
+ const swaggerDocNode = RED.nodes.getNode(swaggerDoc);
84
+
85
+ if (swaggerDocNode) {
86
+ // Convert Node-RED path parameters to OpenAPI format
87
+ const endPoint = stripTerminalSlash(
88
+ ensureLeadingSlash(url.replace(regexColons, convToSwaggerPath)),
89
+ );
90
+
91
+ if (!resp.paths[endPoint]) resp.paths[endPoint] = {};
92
+
93
+ const {
94
+ summary = name || `${method.toUpperCase()} ${endPoint}`,
95
+ description = '',
96
+ tags = '',
97
+ deprecated = false,
98
+ parameters = [],
99
+ requestBody = null,
100
+ responses = {},
101
+ } = swaggerDocNode;
102
+
103
+ const aryTags = csvStrToArray(tags);
104
+
105
+ const operation = {
106
+ summary,
107
+ description,
108
+ tags: aryTags,
109
+ deprecated,
110
+ parameters: [...parameters, ...additionalParams].map((param) => {
111
+ const paramDef = {
112
+ name: param.name,
113
+ in: param.in,
114
+ required: param.required || false,
115
+ description: param.description || '',
116
+ };
117
+
118
+ // Handle parameter schema - preserve the original schema structure
119
+ if (param.schema) {
120
+ // If there's already a schema object, use it
121
+ paramDef.schema = param.schema;
122
+ } else if (param.type) {
123
+ // Build schema from individual type properties
124
+ paramDef.schema = { type: param.type };
125
+ if (param.format) {
126
+ paramDef.schema.format = param.format;
127
+ }
128
+ if (param.type === 'array' && param.items) {
129
+ paramDef.schema.items = param.items;
130
+ }
131
+ if (param.collectionFormat && param.type === 'array') {
132
+ paramDef.style = getStyleFromCollectionFormat(param.collectionFormat);
133
+ paramDef.explode = param.collectionFormat === 'multi';
134
+ }
135
+ }
136
+
137
+ return paramDef;
138
+ }),
139
+ responses: {},
140
+ };
141
+
142
+ // Add request body if it exists
143
+ if (requestBody && Object.keys(requestBody.content || {}).length > 0) {
144
+ const content = requestBody.content;
145
+ Object.keys(content).forEach(contentType => {
146
+ if (contentType.includes('xml') && content[contentType].example) {
147
+ content['text/plain'] = {
148
+ schema: { type: 'string' },
149
+ example: content[contentType].example.replace(/\\n/g, '\n')
150
+ };
151
+ delete content[contentType];
152
+ }
153
+ });
154
+ operation.requestBody = requestBody;
155
+ }
156
+
157
+ // Process responses
158
+ if (responses && typeof responses === 'object') {
159
+ Object.keys(responses).forEach((status) => {
160
+ const responseDetails = responses[status];
161
+ operation.responses[status] = {
162
+ description: responseDetails.description || 'No description',
163
+ };
164
+
165
+ // Add content if schema exists
166
+ if (responseDetails.schema) {
167
+ operation.responses[status].content = {
168
+ 'application/json': {
169
+ schema: responseDetails.schema,
170
+ },
171
+ };
172
+ }
173
+ });
174
+ }
175
+
176
+ // Ensure at least one response exists
177
+ if (Object.keys(operation.responses).length === 0) {
178
+ operation.responses['200'] = {
179
+ description: 'Successful response',
180
+ };
181
+ }
182
+
183
+ resp.paths[endPoint][method.toLowerCase()] = operation;
184
+ }
185
+ }
186
+ });
187
+
188
+ // Clean up empty sections
189
+ cleanupOpenAPISpec(resp);
190
+ res.json(resp);
191
+ } catch (error) {
192
+ console.error('Error generating Swagger JSON:', error);
193
+ res.status(500).json({ error: 'Internal server error' });
194
+ }
195
+ });
196
+
197
+ function cleanupOpenAPISpec(spec) {
198
+ // Clean up components
199
+ if (spec.components) {
200
+ ['schemas', 'responses', 'parameters', 'securitySchemes'].forEach((key) => {
201
+ if (spec.components[key] && Object.keys(spec.components[key]).length === 0) {
202
+ delete spec.components[key];
203
+ }
204
+ });
205
+
206
+ // If all components are empty, remove the components object itself
207
+ if (Object.keys(spec.components).length === 0) {
208
+ delete spec.components;
209
+ }
210
+ }
211
+
212
+ // Clean up empty tags array
213
+ if (Array.isArray(spec.tags) && spec.tags.length === 0) {
214
+ delete spec.tags;
215
+ }
216
+ }
217
+
218
+ function SwaggerDoc(n) {
219
+ RED.nodes.createNode(this, n);
220
+ this.summary = n.summary;
221
+ this.description = n.description;
222
+ this.tags = n.tags;
223
+ this.parameters = n.parameters || [];
224
+ this.responses = n.responses || {};
225
+ this.requestBody = n.requestBody || null;
226
+ this.deprecated = n.deprecated || false;
227
+ }
228
+ RED.nodes.registerType('swagger-doc', SwaggerDoc);
229
+
230
+ // Serve the main Swagger UI HTML file
231
+ RED.httpAdmin.get('/swagger-ui/swagger-ui.html', (req, res) => {
232
+ const filename = path.join(__dirname, 'swagger-ui', 'swagger-ui.html');
233
+ sendFile(res, filename);
234
+ });
235
+
236
+ // Serve Swagger UI assets
237
+ RED.httpAdmin.get('/swagger-ui/*', (req, res, next) => {
238
+ let filename = req.params[0];
239
+
240
+ // Skip if it's the HTML file (handled by specific route above)
241
+ if (filename === 'swagger-ui.html') {
242
+ return next();
243
+ }
244
+
245
+ try {
246
+ const swaggerUiPath = require('swagger-ui-dist').getAbsoluteFSPath();
247
+ const filePath = path.join(swaggerUiPath, filename);
248
+ sendFile(res, filePath);
249
+ } catch (err) {
250
+ console.error('Error serving Swagger UI asset:', err);
251
+ res.status(404).send('File not found');
252
+ }
253
+ });
254
+
255
+ // Serve localization files
256
+ RED.httpAdmin.get('/swagger-ui/nls/*', (req, res) => {
257
+ const filename = path.join(__dirname, 'locales', req.params[0]);
258
+ sendFile(res, filename);
259
+ });
260
+
261
+ // Generic function to send files
262
+ function sendFile(res, filePath) {
263
+ const fs = require('fs');
264
+
265
+ // Check if file exists
266
+ if (!fs.existsSync(filePath)) {
267
+ return res.status(404).send('File not found');
268
+ }
269
+
270
+ res.sendFile(path.resolve(filePath), (err) => {
271
+ if (err) {
272
+ console.error('Error sending file:', err);
273
+ res.status(err.status || 500).send('Error sending file');
274
+ }
275
+ });
276
+ }
277
+ };
@@ -1,97 +0,0 @@
1
- <script type="text/javascript">
2
- RED.nodes.registerType('file-storage', {
3
- category: 'ProcessCube Tools',
4
- color: '#02AFD6',
5
- defaults: {
6
- name: { value: '' },
7
- provider: { value: 'fs' },
8
- baseDir: { value: '' },
9
- pgConnectionString: { value: '' },
10
- pgSchema: { value: 'public' },
11
- pgTable: { value: 'files' },
12
- outputAs: { value: 'stream' },
13
- defaultAction: { value: 'store' },
14
- },
15
- inputs: 1,
16
- outputs: 1,
17
- icon: 'file.png',
18
- label: function () {
19
- return this.name || 'file-storage';
20
- },
21
- });
22
- </script>
23
-
24
- <script type="text/html" data-template-name="file-storage">
25
- <div class="form-row">
26
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
- <input type="text" id="node-input-name" placeholder="file-storage" />
28
- </div>
29
- <div class="form-row">
30
- <label for="node-input-provider"><i class="fa fa-database"></i> Provider</label>
31
- <select id="node-input-provider">
32
- <option value="fs">Filesystem</option>
33
- <option value="pg">PostgreSQL</option>
34
- </select>
35
- </div>
36
- <div class="form-row">
37
- <label for="node-input-outputAs"><i class="fa fa-share-square-o"></i> Output</label>
38
- <select id="node-input-outputAs">
39
- <option value="stream">Stream</option>
40
- <option value="buffer">Buffer</option>
41
- <option value="path">Path (nur FS)</option>
42
- </select>
43
- </div>
44
- <hr />
45
- <div class="form-tips">Filesystem</div>
46
- <div class="form-row">
47
- <label for="node-input-baseDir"><i class="fa fa-folder-open"></i> Base Dir</label>
48
- <input type="text" id="node-input-baseDir" placeholder="/data/files" />
49
- </div>
50
- <hr />
51
- <div class="form-tips">PostgreSQL</div>
52
- <div class="form-row">
53
- <label for="node-input-pgConnectionString"><i class="fa fa-plug"></i> Connection</label>
54
- <input type="text" id="node-input-pgConnectionString" placeholder="postgres://user:pass@host:5432/db" />
55
- </div>
56
- <div class="form-row">
57
- <label for="node-input-pgSchema"><i class="fa fa-sitemap"></i> Schema</label>
58
- <input type="text" id="node-input-pgSchema" placeholder="public" />
59
- </div>
60
- <div class="form-row">
61
- <label for="node-input-pgTable"><i class="fa fa-table"></i> Tabelle</label>
62
- <input type="text" id="node-input-pgTable" placeholder="files" />
63
- </div>
64
- <hr />
65
- <div class="form-row">
66
- <label for="node-input-defaultAction"><i class="fa fa-cog"></i> Default Action</label>
67
- <select id="node-input-defaultAction">
68
- <option value="store">store</option>
69
- <option value="get">get</option>
70
- <option value="delete">delete</option>
71
- </select>
72
- </div>
73
- </script>
74
-
75
- <script type="text/html" data-help-name="file-storage">
76
- <p>
77
- File-Storage-Node zum Speichern/Abrufen/Löschen von Dateien inkl. Metadaten. Provider: Filesystem (Datei + JSON)
78
- oder PostgreSQL (Large Objects + Metadaten-Tabelle).
79
- </p>
80
- <h3>Input</h3>
81
- <pre>
82
- msg.action = "store" | "get" | "delete"
83
- msg.payload = Buffer | Readable | String (bei store)
84
- msg.file = {
85
- id?: string (bei get/delete),
86
- filename?: string,
87
- contentType?: string,
88
- metadata?: object
89
- }
90
- </pre
91
- >
92
- <h3>Output</h3>
93
- <p>
94
- Bei <code>store</code>: <code>msg.payload</code> enthält Metadaten inkl. <code>id</code>. Bei <code>get</code>:
95
- <code>msg.payload</code> ist Stream/Buffer/Pfad (je nach Option), Metadaten in <code>msg.file</code>.
96
- </p>
97
- </script>
@@ -1,70 +0,0 @@
1
- module.exports = function (RED) {
2
- const StorageCore = require('../storage/storage-core');
3
-
4
- function FileStorageNode(config) {
5
- RED.nodes.createNode(this, config);
6
- const node = this;
7
-
8
- // Node-Konfiguration
9
- node.provider = config.provider || 'fs';
10
- node.baseDir = config.baseDir;
11
- node.pg = {
12
- connectionString: config.pgConnectionString,
13
- schema: config.pgSchema || 'public',
14
- table: config.pgTable || 'files',
15
- };
16
- node.outputAs = config.outputAs || 'stream'; // 'stream' | 'buffer' | 'path' (path nur fs)
17
-
18
- // Storage-Kern
19
- const storage = new StorageCore({
20
- provider: node.provider,
21
- fs: { baseDir: node.baseDir },
22
- pg: node.pg,
23
- });
24
-
25
- storage.init().catch((err) => node.error(err));
26
-
27
- node.on('input', async function (msg, send, done) {
28
- try {
29
- const action = msg.action || config.defaultAction || 'store';
30
- if (action === 'store') {
31
- const file = msg.file || {};
32
- const result = await storage.store(msg.payload, file);
33
- msg.payload = result;
34
- msg.file = { ...file, ...result };
35
- send(msg);
36
- done();
37
- return;
38
- }
39
-
40
- if (action === 'get') {
41
- const id = msg.file && msg.file.id;
42
- if (!id) throw new Error('file.id is required for get');
43
- const { meta, payload } = await storage.get(id, { as: node.outputAs });
44
- msg.file = { ...meta, id: meta.id };
45
- msg.payload = payload;
46
- send(msg);
47
- done();
48
- return;
49
- }
50
-
51
- if (action === 'delete') {
52
- const id = msg.file && msg.file.id;
53
- if (!id) throw new Error('file.id is required for delete');
54
- const result = await storage.delete(id);
55
- msg.payload = result;
56
- send(msg);
57
- done();
58
- return;
59
- }
60
-
61
- throw new Error(`Unknown action: ${action}`);
62
- } catch (err) {
63
- node.error(err, msg);
64
- if (done) done(err);
65
- }
66
- });
67
- }
68
-
69
- RED.nodes.registerType('file-storage', FileStorageNode);
70
- };
@@ -1,117 +0,0 @@
1
- const fs = require('fs');
2
- const fsp = require('fs/promises');
3
- const path = require('path');
4
- const { pipeline } = require('stream');
5
- const { createHash } = require('crypto');
6
- const { promisify } = require('util');
7
- const pump = promisify(pipeline);
8
-
9
- class FsProvider {
10
- constructor(opts = {}) {
11
- this.baseDir = opts.baseDir || path.resolve(process.cwd(), 'data');
12
- }
13
-
14
- async init() {
15
- await fsp.mkdir(this.baseDir, { recursive: true });
16
- }
17
-
18
- _buildPaths(id) {
19
- const d = new Date();
20
- const parts = [
21
- String(d.getUTCFullYear()),
22
- String(d.getUTCMonth() + 1).padStart(2, '0'),
23
- String(d.getUTCDate()).padStart(2, '0'),
24
- ];
25
- const dir = path.join(this.baseDir, ...parts);
26
- const filePath = path.join(dir, id);
27
- const metaPath = path.join(dir, `${id}.json`);
28
- return { dir, filePath, metaPath };
29
- }
30
-
31
- async store(readable, info) {
32
- const { id, filename, contentType, metadata, createdAt } = info;
33
- const { dir, filePath, metaPath } = this._buildPaths(id);
34
- await fsp.mkdir(dir, { recursive: true });
35
-
36
- const hash = createHash('sha256');
37
- let size = 0;
38
-
39
- const out = fs.createWriteStream(filePath);
40
- readable.on('data', (chunk) => {
41
- hash.update(chunk);
42
- size += chunk.length;
43
- });
44
-
45
- await pump(readable, out);
46
-
47
- const sha256 = hash.digest('hex');
48
- const meta = { id, filename, contentType, size, sha256, metadata, createdAt };
49
- await fsp.writeFile(metaPath, JSON.stringify(meta, null, 2));
50
-
51
- return { size, sha256, path: filePath };
52
- }
53
-
54
- async get(id, options = { as: 'stream' }) {
55
- // Find meta file by searching dated folders
56
- const meta = await this._findMeta(id);
57
- if (!meta) throw new Error(`File not found: ${id}`);
58
- const filePath = meta.__filePath;
59
-
60
- if (options.as === 'path') {
61
- return { meta, payload: filePath };
62
- }
63
-
64
- if (options.as === 'buffer') {
65
- const buf = await fsp.readFile(filePath);
66
- return { meta, payload: buf };
67
- }
68
-
69
- // default: stream
70
- const stream = fs.createReadStream(filePath);
71
- return { meta, payload: stream };
72
- }
73
-
74
- async delete(id) {
75
- const meta = await this._findMeta(id);
76
- if (!meta) return; // idempotent
77
- await fsp.unlink(meta.__filePath).catch(() => {});
78
- await fsp.unlink(meta.__metaPath).catch(() => {});
79
- }
80
-
81
- async _findMeta(id) {
82
- // Walk date folders (YYYY/MM/DD). For Performance: keep index/cache in prod.
83
- const years = await this._ls(this.baseDir);
84
- for (const y of years) {
85
- const yearDir = path.join(this.baseDir, y);
86
- const months = await this._ls(yearDir);
87
- for (const m of months) {
88
- const monthDir = path.join(yearDir, m);
89
- const days = await this._ls(monthDir);
90
- for (const d of days) {
91
- const dir = path.join(monthDir, d);
92
- const metaPath = path.join(dir, `${id}.json`);
93
- try {
94
- const raw = await fsp.readFile(metaPath, 'utf-8');
95
- const meta = JSON.parse(raw);
96
- meta.__metaPath = metaPath;
97
- meta.__filePath = path.join(dir, id);
98
- return meta;
99
- } catch (_) {
100
- /* continue */
101
- }
102
- }
103
- }
104
- }
105
- return null;
106
- }
107
-
108
- async _ls(dir) {
109
- try {
110
- return (await fsp.readdir(dir)).filter((n) => !n.startsWith('.'));
111
- } catch {
112
- return [];
113
- }
114
- }
115
- }
116
-
117
- module.exports = FsProvider;