@5minds/node-red-contrib-processcube-tools 1.2.0-feature-37541f-mg92jkdw → 1.2.0-feature-608421-mg9cjskq

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.
Files changed (61) hide show
  1. package/.env.template +7 -1
  2. package/email-receiver/email-receiver.js +304 -0
  3. package/email-sender/email-sender.js +178 -0
  4. package/examples/.gitkeep +0 -0
  5. package/file-storage/file-storage.html +203 -0
  6. package/file-storage/file-storage.js +148 -0
  7. package/package.json +17 -26
  8. package/{src/html-to-text/html-to-text.html → processcube-html-to-text/processcube-html-to-text.html} +3 -3
  9. package/processcube-html-to-text/processcube-html-to-text.js +22 -0
  10. package/storage/providers/fs.js +117 -0
  11. package/storage/providers/postgres.js +160 -0
  12. package/storage/storage-core.js +77 -0
  13. package/test/helpers/email-receiver.mocks.js +447 -0
  14. package/test/helpers/email-sender.mocks.js +368 -0
  15. package/test/integration/email-receiver.integration.test.js +515 -0
  16. package/test/integration/email-sender.integration.test.js +239 -0
  17. package/test/unit/email-receiver.unit.test.js +304 -0
  18. package/test/unit/email-sender.unit.test.js +570 -0
  19. package/.mocharc.json +0 -5
  20. package/src/custom-node-template/custom-node-template.html.template +0 -45
  21. package/src/custom-node-template/custom-node-template.ts.template +0 -69
  22. package/src/email-receiver/email-receiver.ts +0 -439
  23. package/src/email-sender/email-sender.ts +0 -210
  24. package/src/html-to-text/html-to-text.ts +0 -53
  25. package/src/index.ts +0 -12
  26. package/src/interfaces/EmailReceiverMessage.ts +0 -22
  27. package/src/interfaces/EmailSenderNodeProperties.ts +0 -37
  28. package/src/interfaces/FetchState.ts +0 -9
  29. package/src/interfaces/ImapConnectionConfig.ts +0 -14
  30. package/src/test/framework/advanced-test-patterns.ts +0 -224
  31. package/src/test/framework/generic-node-test-suite.ts +0 -58
  32. package/src/test/framework/index.ts +0 -17
  33. package/src/test/framework/integration-assertions.ts +0 -67
  34. package/src/test/framework/integration-scenario-builder.ts +0 -77
  35. package/src/test/framework/integration-test-runner.ts +0 -101
  36. package/src/test/framework/node-assertions.ts +0 -63
  37. package/src/test/framework/node-test-runner.ts +0 -260
  38. package/src/test/framework/test-scenario-builder.ts +0 -74
  39. package/src/test/framework/types.ts +0 -61
  40. package/src/test/helpers/email-receiver-test-configs.ts +0 -67
  41. package/src/test/helpers/email-receiver-test-flows.ts +0 -16
  42. package/src/test/helpers/email-sender-test-configs.ts +0 -123
  43. package/src/test/helpers/email-sender-test-flows.ts +0 -16
  44. package/src/test/integration/email-receiver.integration.test.ts +0 -41
  45. package/src/test/integration/email-sender.integration.test.ts +0 -129
  46. package/src/test/interfaces/email-data.ts +0 -10
  47. package/src/test/interfaces/email-receiver-config.ts +0 -12
  48. package/src/test/interfaces/email-sender-config.ts +0 -26
  49. package/src/test/interfaces/imap-config.ts +0 -9
  50. package/src/test/interfaces/imap-mailbox.ts +0 -5
  51. package/src/test/interfaces/mail-options.ts +0 -20
  52. package/src/test/interfaces/parsed-email.ts +0 -11
  53. package/src/test/interfaces/send-mail-result.ts +0 -7
  54. package/src/test/mocks/imap-mock.ts +0 -147
  55. package/src/test/mocks/mailparser-mock.ts +0 -82
  56. package/src/test/mocks/nodemailer-mock.ts +0 -118
  57. package/src/test/unit/email-receiver.unit.test.ts +0 -471
  58. package/src/test/unit/email-sender.unit.test.ts +0 -550
  59. package/tsconfig.json +0 -23
  60. /package/{src/email-receiver → email-receiver}/email-receiver.html +0 -0
  61. /package/{src/email-sender → email-sender}/email-sender.html +0 -0
@@ -0,0 +1,203 @@
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
+ usernameType: { value: 'str' },
10
+ username: { value: '', required: true, validate: RED.validators.typedInput('usernameType') },
11
+ passwordType: { value: 'str' },
12
+ password: { value: '', required: true, validate: RED.validators.typedInput('passwordType') },
13
+ hostType: { value: 'str' },
14
+ host: { value: '', required: true, validate: RED.validators.typedInput('hostType') },
15
+ portType: { value: 'str' },
16
+ port: { value: '', required: true, validate: RED.validators.typedInput('portType') },
17
+ databaseType: { value: 'str' },
18
+ database: { value: '', required: true, validate: RED.validators.typedInput('databaseType') },
19
+
20
+ pgSchema: { value: 'public' },
21
+ pgTable: { value: 'files' },
22
+ outputAs: { value: 'stream' },
23
+ defaultAction: { value: 'store' },
24
+ },
25
+ inputs: 1,
26
+ outputs: 1,
27
+ icon: 'file.png',
28
+ label: function () {
29
+ return this.name || 'file-storage';
30
+ },
31
+ oneditprepare: function () {
32
+ // postgres fields user, password, host, port, database
33
+ $('#node-input-username').typedInput({
34
+ default: 'str',
35
+ types: ['str', 'env'],
36
+ typeField: '#node-input-usernameType',
37
+ });
38
+ $('#node-input-password').typedInput({
39
+ default: 'str',
40
+ types: ['str', 'env'],
41
+ typeField: '#node-input-passwordType',
42
+ });
43
+ $('#node-input-host').typedInput({
44
+ default: 'str',
45
+ types: ['str', 'env'],
46
+ typeField: '#node-input-hostType',
47
+ });
48
+ $('#node-input-port').typedInput({
49
+ default: 'str',
50
+ types: ['str', 'env'],
51
+ typeField: '#node-input-portType',
52
+ });
53
+ $('#node-input-database').typedInput({
54
+ default: 'str',
55
+ types: ['str', 'env'],
56
+ typeField: '#node-input-databaseType',
57
+ });
58
+ },
59
+ });
60
+ </script>
61
+
62
+ <script type="text/html" data-template-name="file-storage">
63
+ <div class="form-row">
64
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
65
+ <input type="text" id="node-input-name" placeholder="file-storage" />
66
+ </div>
67
+ <div class="form-row">
68
+ <label for="node-input-provider"><i class="fa fa-database"></i> Provider</label>
69
+ <select id="node-input-provider">
70
+ <option value="fs">Filesystem</option>
71
+ <option value="pg">PostgreSQL</option>
72
+ </select>
73
+ </div>
74
+ <div class="form-row">
75
+ <label for="node-input-outputAs"><i class="fa fa-share-square-o"></i> Output</label>
76
+ <select id="node-input-outputAs">
77
+ <option value="stream">Stream</option>
78
+ <option value="buffer">Buffer</option>
79
+ <option value="path">Path (nur FS)</option>
80
+ </select>
81
+ </div>
82
+ <hr />
83
+ <div class="form-tips">Filesystem</div>
84
+ <div class="form-row">
85
+ <label for="node-input-baseDir"><i class="fa fa-folder-open"></i> Base Dir</label>
86
+ <input type="text" id="node-input-baseDir" placeholder="/data/files" />
87
+ </div>
88
+ <hr />
89
+ <div class="form-tips">PostgreSQL</div>
90
+ <div class="form-row">
91
+ <label for="node-input-username"><i class="fa fa-user"></i> Username</label>
92
+ <input type="text" id="node-input-username" placeholder="postgres" />
93
+ <input type="hidden" id="node-input-usernameType" />
94
+ </div>
95
+ <div class="form-row">
96
+ <label for="node-input-password"><i class="fa fa-key"></i> Password</label>
97
+ <input type="text" id="node-input-password" placeholder="postgres" />
98
+ <input type="hidden" id="node-input-passwordType" />
99
+ </div>
100
+ <div class="form-row">
101
+ <label for="node-input-host"><i class="fa fa-server"></i> Host</label>
102
+ <input type="text" id="node-input-host" placeholder="localhost" />
103
+ <input type="hidden" id="node-input-hostType" />
104
+ </div>
105
+ <div class="form-row">
106
+ <label for="node-input-port"><i class="fa fa-plug"></i> Port</label>
107
+ <input type="text" id="node-input-port" placeholder="5432" />
108
+ <input type="hidden" id="node-input-portType" />
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-input-database"><i class="fa fa-database"></i> Database</label>
112
+ <input type="text" id="node-input-database" placeholder="postgres" />
113
+ <input type="hidden" id="node-input-databaseType" />
114
+ </div>
115
+ <div class="form-row">
116
+ <label for="node-input-pgSchema"><i class="fa fa-sitemap"></i> Schema</label>
117
+ <input type="text" id="node-input-pgSchema" placeholder="public" />
118
+ </div>
119
+ <div class="form-row">
120
+ <label for="node-input-pgTable"><i class="fa fa-table"></i> Table</label>
121
+ <input type="text" id="node-input-pgTable" placeholder="files" />
122
+ </div>
123
+ <hr />
124
+ <div class="form-row">
125
+ <label for="node-input-defaultAction"><i class="fa fa-cog"></i> Default Action</label>
126
+ <select id="node-input-defaultAction">
127
+ <option value="store">store</option>
128
+ <option value="get">get</option>
129
+ <option value="delete">delete</option>
130
+ </select>
131
+ </div>
132
+ </script>
133
+
134
+ <script type="text/html" data-help-name="file-storage">
135
+ <h2>File Storage Node</h2>
136
+ <p>
137
+ A Node-RED node for storing, retrieving, and deleting files (including metadata) using either the local filesystem or PostgreSQL (Large Objects + metadata table) as a backend.
138
+ </p>
139
+ <h3>Features</h3>
140
+ <ul>
141
+ <li><b>Providers:</b>
142
+ <ul>
143
+ <li>Filesystem (stores file and metadata as JSON)</li>
144
+ <li>PostgreSQL (stores file as Large Object and metadata in a table)</li>
145
+ </ul>
146
+ </li>
147
+ <li><b>Actions:</b> Store, Retrieve, Delete files</li>
148
+ <li><b>Flexible output:</b> Stream, Buffer, or Path (filesystem only)</li>
149
+ </ul>
150
+ <h3>Node Properties</h3>
151
+ <ul>
152
+ <li><b>Name:</b> Optional node label.</li>
153
+ <li><b>Provider:</b> Select between Filesystem and PostgreSQL.</li>
154
+ <li><b>Output:</b> Choose the output type for retrieval: Stream, Buffer, or Path (Path only for filesystem).</li>
155
+ <li><b>Base Dir:</b> (Filesystem) Directory where files are stored.</li>
156
+ <li><b>Schema:</b> (PostgreSQL) Database schema (default: public).</li>
157
+ <li><b>Table:</b> (PostgreSQL) Table for metadata (default: files).</li>
158
+ <li><b>Default Action:</b> Default action if not specified in the message (store, get, or delete).</li>
159
+ </ul>
160
+ <h3>Input</h3>
161
+ <pre>
162
+ msg.action = "store" | "get" | "delete" // Optional, overrides defaultAction
163
+ msg.payload = Buffer | ReadableStream | String // For "store"
164
+ msg.file = {
165
+ id?: string, // For "get" or "delete"
166
+ filename?: string, // For "store"
167
+ contentType?: string,// For "store"
168
+ metadata?: object // For "store"
169
+ }
170
+ </pre>
171
+ <h3>Output</h3>
172
+ <ul>
173
+ <li><b>store:</b> <code>msg.payload</code> contains metadata including the generated <code>id</code>. <code>msg.file</code> contains the merged file info and result.</li>
174
+ <li><b>get:</b> <code>msg.payload</code> contains the file as a Stream, Buffer, or Path (depending on output setting). <code>msg.file</code> contains the file metadata.</li>
175
+ <li><b>delete:</b> <code>msg.payload</code> contains the result of the delete operation.</li>
176
+ </ul>
177
+ <h3>Example Usage</h3>
178
+ <pre>
179
+ // Store a file:
180
+ msg.action = "store";
181
+ msg.payload = Buffer.from("Hello World");
182
+ msg.file = {
183
+ filename: "hello.txt",
184
+ contentType: "text/plain",
185
+ metadata: { author: "Alice" }
186
+ };
187
+
188
+ // Retrieve a file:
189
+ msg.action = "get";
190
+ msg.file = { id: "your-file-id" };
191
+
192
+ // Delete a file:
193
+ msg.action = "delete";
194
+ msg.file = { id: "your-file-id" };
195
+ </pre>
196
+ <h3>Notes</h3>
197
+ <ul>
198
+ <li>For PostgreSQL, ensure the connection string, schema, and table exist and the user has the necessary permissions.</li>
199
+ <li>For filesystem storage, ensure the base directory is writable by Node-RED.</li>
200
+ <li>The node is designed to handle large files efficiently using streams.</li>
201
+ </ul>
202
+ <p><b>Enjoy using the File Storage Node in your Node-RED flows!</b></p>
203
+ </script>
@@ -0,0 +1,148 @@
1
+ /**
2
+ * # File Storage Node
3
+ *
4
+ * A Node-RED node for storing, retrieving, and deleting files (including metadata) using either the local filesystem or PostgreSQL (Large Objects + metadata table) as a backend.
5
+ *
6
+ * ## Features
7
+ * - **Providers:**
8
+ * - Filesystem (stores file and metadata as JSON)
9
+ * - PostgreSQL (stores file as Large Object and metadata in a table)
10
+ * - **Actions:**
11
+ * - Store a file
12
+ * - Retrieve a file
13
+ * - Delete a file
14
+ * - **Flexible output:**
15
+ * - Stream, Buffer, or Path (filesystem only)
16
+ *
17
+ * ## Node Properties
18
+ * - **Name:** Optional node label.
19
+ * - **Provider:** Select between `Filesystem` and `PostgreSQL`.
20
+ * - **Output:** Choose the output type for retrieval: `Stream`, `Buffer`, or `Path` (Path only for filesystem).
21
+ * - **Base Dir:** (Filesystem) Directory where files are stored.
22
+ * - **Connection:** (PostgreSQL) Connection string for the database.
23
+ * - **Schema:** (PostgreSQL) Database schema (default: `public`).
24
+ * - **Table:** (PostgreSQL) Table for metadata (default: `files`).
25
+ * - **Default Action:** Default action if not specified in the message (`store`, `get`, or `delete`).
26
+ *
27
+ * ## Input
28
+ * The node expects the following properties in the incoming message:
29
+ * ```
30
+ * msg.action = "store" | "get" | "delete" // Optional, overrides defaultAction
31
+ * msg.payload = Buffer | ReadableStream | String // For "store"
32
+ * msg.file = {
33
+ * id?: string, // For "get" or "delete"
34
+ * filename?: string, // For "store"
35
+ * contentType?: string,// For "store"
36
+ * metadata?: object // For "store"
37
+ * }
38
+ * ```
39
+ *
40
+ * ## Output
41
+ * - For **store**:
42
+ * - `msg.payload` contains metadata including the generated `id`.
43
+ * - `msg.file` contains the merged file info and result.
44
+ * - For **get**:
45
+ * - `msg.payload` contains the file as a Stream, Buffer, or Path (depending on output setting).
46
+ * - `msg.file` contains the file metadata.
47
+ * - For **delete**:
48
+ * - `msg.payload` contains the result of the delete operation.
49
+ *
50
+ * ## Example Usage
51
+ * // Store a file:
52
+ * msg.action = "store";
53
+ * msg.payload = Buffer.from("Hello World");
54
+ * msg.file = {
55
+ * filename: "hello.txt",
56
+ * contentType: "text/plain",
57
+ * metadata: { author: "Alice" }
58
+ * };
59
+ *
60
+ * // Retrieve a file:
61
+ * msg.action = "get";
62
+ * msg.file = { id: "your-file-id" };
63
+ *
64
+ * // Delete a file:
65
+ * msg.action = "delete";
66
+ * msg.file = { id: "your-file-id" };
67
+ *
68
+ * ## Notes
69
+ * - For PostgreSQL, ensure the connection string, schema, and table exist and the user has the necessary permissions.
70
+ * - For filesystem storage, ensure the base directory is writable by Node-RED.
71
+ * - The node is designed to handle large files efficiently using streams.
72
+ *
73
+ * Enjoy using the File Storage Node in your Node-RED flows!
74
+ */
75
+ module.exports = function (RED) {
76
+ const StorageCore = require('../storage/storage-core');
77
+
78
+ function FileStorageNode(config) {
79
+ RED.nodes.createNode(this, config);
80
+ const node = this;
81
+
82
+ // Node-Konfiguration
83
+ node.provider = config.provider || 'fs';
84
+ node.baseDir = config.baseDir;
85
+ node.pg = {
86
+ username: RED.util.evaluateNodeProperty(config.username, config.usernameType, node) || 'postgres',
87
+ password: RED.util.evaluateNodeProperty(config.password, config.passwordType, node) || 'postgres',
88
+ host: RED.util.evaluateNodeProperty(config.host, config.hostType, node) || 'localhost',
89
+ port: RED.util.evaluateNodeProperty(config.port, config.portType, node) || 5432,
90
+ database: RED.util.evaluateNodeProperty(config.database, config.databaseType, node) || 'postgres',
91
+ schema: config.pgSchema || 'public',
92
+ table: config.pgTable || 'files',
93
+ };
94
+ node.outputAs = config.outputAs || 'stream'; // 'stream' | 'buffer' | 'path' (path nur fs)
95
+
96
+ // Storage-Kern
97
+ const storage = new StorageCore({
98
+ provider: node.provider,
99
+ fs: { baseDir: node.baseDir },
100
+ pg: node.pg,
101
+ });
102
+
103
+ storage.init().catch((err) => node.error(err));
104
+
105
+ node.on('input', async function (msg, send, done) {
106
+ try {
107
+ const action = msg.action || config.defaultAction || 'store';
108
+ if (action === 'store') {
109
+ const file = msg.file || {};
110
+ const result = await storage.store(msg.payload, file);
111
+ msg.payload = result;
112
+ msg.file = { ...file, ...result };
113
+ send(msg);
114
+ done();
115
+ return;
116
+ }
117
+
118
+ if (action === 'get') {
119
+ const id = msg.file && msg.file.id;
120
+ if (!id) throw new Error('file.id is required for get');
121
+ const { meta, payload } = await storage.get(id, { as: node.outputAs });
122
+ msg.file = { ...meta, id: meta.id };
123
+ msg.payload = payload;
124
+ send(msg);
125
+ done();
126
+ return;
127
+ }
128
+
129
+ if (action === 'delete') {
130
+ const id = msg.file && msg.file.id;
131
+ if (!id) throw new Error('file.id is required for delete');
132
+ const result = await storage.delete(id);
133
+ msg.payload = result;
134
+ send(msg);
135
+ done();
136
+ return;
137
+ }
138
+
139
+ throw new Error(`Unknown action: ${action}`);
140
+ } catch (err) {
141
+ node.error(err, msg);
142
+ if (done) done(err);
143
+ }
144
+ });
145
+ }
146
+
147
+ RED.nodes.registerType('file-storage', FileStorageNode);
148
+ };
package/package.json CHANGED
@@ -1,16 +1,11 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube-tools",
3
- "version": "1.2.0-feature-37541f-mg92jkdw",
3
+ "version": "1.2.0-feature-608421-mg9cjskq",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED tools nodes for ProcessCube",
6
6
  "scripts": {
7
- "build": "npm-run-all build:*",
8
- "build:ts": "tsc",
9
- "build:html": "cpx \"./src/**/*.html\" ./dist",
10
- "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,ts}\"",
11
- "test": "mocha --require ts-node/register 'src/test/**/*.test.ts'",
12
- "test:unit": "mocha --require ts-node/register 'src/test/unit/**/*.test.ts'",
13
- "test:integration": "mocha --require ts-node/register 'src/test/integration/**/*.test.ts'"
7
+ "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\"",
8
+ "test": "mocha test/unit/ test/integration"
14
9
  },
15
10
  "authors": [
16
11
  {
@@ -24,6 +19,10 @@
24
19
  {
25
20
  "name": "Diana Stefan",
26
21
  "email": "Diana.Stefan@5Minds.de"
22
+ },
23
+ {
24
+ "name": "Thorsten Kallweit",
25
+ "email": "Thorsten.Kallweit@5Minds.de"
27
26
  }
28
27
  ],
29
28
  "repository": {
@@ -39,43 +38,35 @@
39
38
  "npm": ">=8.0.0"
40
39
  },
41
40
  "node-red": {
42
- "version": "^4.1.0",
41
+ "version": ">=3.1.9",
43
42
  "nodes": {
44
- "EmailReceiver": "dist/email-receiver/email-receiver.js",
45
- "EmailSender": "dist/email-sender/email-sender.js",
46
- "HtmlToText": "dist/html-to-text/html-to-text.js"
43
+ "EmailReceiver": "email-receiver/email-receiver.js",
44
+ "EmailSender": "email-sender/email-sender.js",
45
+ "HtmlToText": "processcube-html-to-text/processcube-html-to-text.js",
46
+ "FileStorage": "file-storage/file-storage.js"
47
47
  },
48
48
  "examples": "examples"
49
49
  },
50
50
  "dependencies": {
51
51
  "html-to-text": "^9.0.5",
52
- "mailparser": "^3.7.4",
52
+ "mailparser": "^3.6.8",
53
53
  "node-imap": "^0.9.6",
54
54
  "nodemailer": "^7.0.6",
55
+ "pg": "^8.16.3",
56
+ "pg-large-object": "^2.0.0",
55
57
  "utf7": "^1.0.2"
56
58
  },
57
59
  "devDependencies": {
58
- "@types/chai": "^5.2.2",
59
- "@types/mailparser": "^3.4.6",
60
- "@types/mocha": "^10.0.10",
61
- "@types/node": "^24.5.2",
62
- "@types/node-imap": "^0.9.3",
63
- "@types/node-red": "^1.3.5",
64
- "@types/node-red-node-test-helper": "^0.3.4",
65
- "@types/nodemailer": "^7.0.1",
66
60
  "chai": "^4.3.4",
67
- "cpx2": "^8.0.0",
68
61
  "mocha": "^11.7.2",
69
62
  "node-red": "^4.0.9",
70
- "node-red-node-test-helper": "^0.3.5",
71
- "npm-run-all": "^4.1.5",
72
- "ts-node": "^10.9.2",
73
- "typescript": "^5.9.2"
63
+ "node-red-node-test-helper": "^0.3.5"
74
64
  },
75
65
  "overrides": {
76
66
  "semver": ">=7.5.2",
77
67
  "axios": ">=1.12.0",
78
68
  "html-to-text": "^9.0.5",
69
+ "mailparser": "^3.6.8",
79
70
  "node-imap": "^0.9.6",
80
71
  "nodemailer": "^7.0.6",
81
72
  "utf7": "^1.0.2"
@@ -1,5 +1,5 @@
1
1
  <script type="text/javascript">
2
- RED.nodes.registerType('html-to-text', {
2
+ RED.nodes.registerType('processcube-html-to-text', {
3
3
  category: 'ProcessCube Tools',
4
4
  color: '#02AFD6',
5
5
  defaults: {
@@ -9,12 +9,12 @@
9
9
  outputs: 1,
10
10
  icon: 'font-awesome/fa-sign-in',
11
11
  label: function () {
12
- return this.name || 'html-to-text';
12
+ return this.name || 'processcube-html-to-text';
13
13
  },
14
14
  });
15
15
  </script>
16
16
 
17
- <script type="text/html" data-template-name="html-to-text">
17
+ <script type="text/html" data-template-name="processcube-html-to-text">
18
18
  <div class="form-row">
19
19
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
20
20
  <input type="text" id="node-input-name" placeholder="Name" />
@@ -0,0 +1,22 @@
1
+ module.exports = function (RED) {
2
+ const { compile } = require('html-to-text');
3
+
4
+ function ProcesscubeHtmlToText(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ const options = {
9
+ wordwrap: 130,
10
+ // ...
11
+ };
12
+ const compiledConvert = compile(options); // options passed here
13
+
14
+ node.on('input', async function (msg) {
15
+ msg.payload = compiledConvert(msg.payload);
16
+
17
+ node.send(msg);
18
+ });
19
+ }
20
+
21
+ RED.nodes.registerType('processcube-html-to-text', ProcesscubeHtmlToText);
22
+ };
@@ -0,0 +1,117 @@
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;