@5minds/node-red-contrib-processcube-tools 1.2.0-feature-7ac247-mg92hchl → 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.
- package/.env.template +7 -1
- package/email-receiver/email-receiver.js +304 -0
- package/email-sender/email-sender.js +178 -0
- package/examples/.gitkeep +0 -0
- package/file-storage/file-storage.html +203 -0
- package/file-storage/file-storage.js +148 -0
- package/package.json +17 -26
- package/{src/html-to-text/html-to-text.html → processcube-html-to-text/processcube-html-to-text.html} +3 -3
- package/processcube-html-to-text/processcube-html-to-text.js +22 -0
- package/storage/providers/fs.js +117 -0
- package/storage/providers/postgres.js +160 -0
- package/storage/storage-core.js +77 -0
- package/test/helpers/email-receiver.mocks.js +447 -0
- package/test/helpers/email-sender.mocks.js +368 -0
- package/test/integration/email-receiver.integration.test.js +515 -0
- package/test/integration/email-sender.integration.test.js +239 -0
- package/test/unit/email-receiver.unit.test.js +304 -0
- package/test/unit/email-sender.unit.test.js +570 -0
- package/.mocharc.json +0 -5
- package/src/custom-node-template/custom-node-template.html.template +0 -45
- package/src/custom-node-template/custom-node-template.ts.template +0 -69
- package/src/email-receiver/email-receiver.ts +0 -439
- package/src/email-sender/email-sender.ts +0 -210
- package/src/html-to-text/html-to-text.ts +0 -53
- package/src/index.ts +0 -12
- package/src/interfaces/EmailReceiverMessage.ts +0 -22
- package/src/interfaces/EmailSenderNodeProperties.ts +0 -37
- package/src/interfaces/FetchState.ts +0 -9
- package/src/interfaces/ImapConnectionConfig.ts +0 -14
- package/src/test/framework/advanced-test-patterns.ts +0 -224
- package/src/test/framework/generic-node-test-suite.ts +0 -58
- package/src/test/framework/index.ts +0 -17
- package/src/test/framework/integration-assertions.ts +0 -67
- package/src/test/framework/integration-scenario-builder.ts +0 -77
- package/src/test/framework/integration-test-runner.ts +0 -101
- package/src/test/framework/node-assertions.ts +0 -63
- package/src/test/framework/node-test-runner.ts +0 -260
- package/src/test/framework/test-scenario-builder.ts +0 -74
- package/src/test/framework/types.ts +0 -61
- package/src/test/helpers/email-receiver-test-configs.ts +0 -67
- package/src/test/helpers/email-receiver-test-flows.ts +0 -16
- package/src/test/helpers/email-sender-test-configs.ts +0 -123
- package/src/test/helpers/email-sender-test-flows.ts +0 -16
- package/src/test/integration/email-receiver.integration.test.ts +0 -41
- package/src/test/integration/email-sender.integration.test.ts +0 -129
- package/src/test/interfaces/email-data.ts +0 -10
- package/src/test/interfaces/email-receiver-config.ts +0 -12
- package/src/test/interfaces/email-sender-config.ts +0 -26
- package/src/test/interfaces/imap-config.ts +0 -9
- package/src/test/interfaces/imap-mailbox.ts +0 -5
- package/src/test/interfaces/mail-options.ts +0 -20
- package/src/test/interfaces/parsed-email.ts +0 -11
- package/src/test/interfaces/send-mail-result.ts +0 -7
- package/src/test/mocks/imap-mock.ts +0 -147
- package/src/test/mocks/mailparser-mock.ts +0 -82
- package/src/test/mocks/nodemailer-mock.ts +0 -118
- package/src/test/unit/email-receiver.unit.test.ts +0 -471
- package/src/test/unit/email-sender.unit.test.ts +0 -550
- package/tsconfig.json +0 -23
- /package/{src/email-receiver → email-receiver}/email-receiver.html +0 -0
- /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-
|
|
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
|
-
"
|
|
8
|
-
"
|
|
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": "
|
|
41
|
+
"version": ">=3.1.9",
|
|
43
42
|
"nodes": {
|
|
44
|
-
"EmailReceiver": "
|
|
45
|
-
"EmailSender": "
|
|
46
|
-
"HtmlToText": "
|
|
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.
|
|
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;
|