node-red-contrib-db-storage 0.2.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 +21 -0
- package/README.md +557 -0
- package/adapters/AdapterFactory.js +87 -0
- package/adapters/DatabaseAdapter.js +135 -0
- package/adapters/MongoAdapter.js +187 -0
- package/adapters/MySQLAdapter.js +172 -0
- package/adapters/PostgresAdapter.js +167 -0
- package/adapters/SQLiteAdapter.js +161 -0
- package/adapters/index.js +15 -0
- package/constants.js +8 -0
- package/examples/node-red-app.js +115 -0
- package/index.js +185 -0
- package/package.json +69 -0
- package/utils/UrlParser.js +140 -0
- package/utils/index.js +5 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const DatabaseAdapter = require('./DatabaseAdapter');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQLite adapter for Node-RED storage
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* @param {Object} config - Standardized adapter configuration
|
|
8
|
+
* @param {string} config.url - SQLite database file path (e.g., '/path/to/database.db' or ':memory:')
|
|
9
|
+
* @param {string} config.database - Database name (used for identification, not for SQLite file)
|
|
10
|
+
*/
|
|
11
|
+
class SQLiteAdapter extends DatabaseAdapter {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
super(config);
|
|
14
|
+
// url and databaseName are set by parent class
|
|
15
|
+
this.db = null;
|
|
16
|
+
this._sqlite = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async connect() {
|
|
20
|
+
// Dynamically require better-sqlite3 to make it an optional dependency
|
|
21
|
+
try {
|
|
22
|
+
this._sqlite = require('better-sqlite3');
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new Error('SQLite adapter requires the "better-sqlite3" package. Install it with: npm install better-sqlite3');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Parse the URL to get the file path
|
|
28
|
+
let dbPath = this.url;
|
|
29
|
+
if (dbPath.startsWith('sqlite://')) {
|
|
30
|
+
dbPath = dbPath.replace('sqlite://', '');
|
|
31
|
+
} else if (dbPath.startsWith('file://')) {
|
|
32
|
+
dbPath = dbPath.replace('file://', '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.db = new this._sqlite(dbPath);
|
|
36
|
+
|
|
37
|
+
// Enable WAL mode for better concurrent performance
|
|
38
|
+
this.db.pragma('journal_mode = WAL');
|
|
39
|
+
|
|
40
|
+
// Create tables if they don't exist
|
|
41
|
+
await this._initializeTables();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async close() {
|
|
45
|
+
if (this.db) {
|
|
46
|
+
this.db.close();
|
|
47
|
+
this.db = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async _initializeTables() {
|
|
52
|
+
// We'll create tables dynamically as needed
|
|
53
|
+
// This is a helper to ensure the schema exists
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _ensureTable(tableName) {
|
|
57
|
+
const createTableQuery = `
|
|
58
|
+
CREATE TABLE IF NOT EXISTS "${tableName}" (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
path TEXT UNIQUE,
|
|
61
|
+
data TEXT NOT NULL,
|
|
62
|
+
meta TEXT,
|
|
63
|
+
body TEXT,
|
|
64
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
65
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
66
|
+
)
|
|
67
|
+
`;
|
|
68
|
+
this.db.exec(createTableQuery);
|
|
69
|
+
|
|
70
|
+
// Create index on path for faster lookups
|
|
71
|
+
const createIndexQuery = `
|
|
72
|
+
CREATE INDEX IF NOT EXISTS "${tableName}_path_idx" ON "${tableName}" (path)
|
|
73
|
+
`;
|
|
74
|
+
this.db.exec(createIndexQuery);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async findAll(collectionName) {
|
|
78
|
+
this.validateCollectionName(collectionName);
|
|
79
|
+
await this._ensureTable(collectionName);
|
|
80
|
+
|
|
81
|
+
const query = `SELECT data FROM "${collectionName}"`;
|
|
82
|
+
const rows = this.db.prepare(query).all();
|
|
83
|
+
|
|
84
|
+
if (rows.length === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return rows.map(row => JSON.parse(row.data));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async saveAll(collectionName, objects) {
|
|
92
|
+
this.validateCollectionName(collectionName);
|
|
93
|
+
this.validateObjectArray(objects);
|
|
94
|
+
await this._ensureTable(collectionName);
|
|
95
|
+
|
|
96
|
+
// Use a transaction for atomicity
|
|
97
|
+
const transaction = this.db.transaction((objs) => {
|
|
98
|
+
// Delete all existing data
|
|
99
|
+
this.db.prepare(`DELETE FROM "${collectionName}"`).run();
|
|
100
|
+
|
|
101
|
+
// Insert new data
|
|
102
|
+
if (objs.length > 0) {
|
|
103
|
+
const insert = this.db.prepare(
|
|
104
|
+
`INSERT INTO "${collectionName}" (data) VALUES (?)`
|
|
105
|
+
);
|
|
106
|
+
for (const obj of objs) {
|
|
107
|
+
insert.run(JSON.stringify(obj));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
transaction(objects);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async findOneByPath(collectionName, path) {
|
|
116
|
+
this.validateCollectionName(collectionName);
|
|
117
|
+
this.validatePath(path);
|
|
118
|
+
await this._ensureTable(collectionName);
|
|
119
|
+
|
|
120
|
+
const query = `SELECT body FROM "${collectionName}" WHERE path = ?`;
|
|
121
|
+
const row = this.db.prepare(query).get(path);
|
|
122
|
+
|
|
123
|
+
if (!row) {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const body = row.body;
|
|
128
|
+
if (body == null) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return JSON.parse(body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async saveOrUpdateByPath(collectionName, path, meta, body) {
|
|
136
|
+
this.validateCollectionName(collectionName);
|
|
137
|
+
this.validatePath(path);
|
|
138
|
+
await this._ensureTable(collectionName);
|
|
139
|
+
|
|
140
|
+
const data = { path, meta, body };
|
|
141
|
+
|
|
142
|
+
const query = `
|
|
143
|
+
INSERT INTO "${collectionName}" (path, meta, body, data, updated_at)
|
|
144
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
145
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
146
|
+
meta = excluded.meta,
|
|
147
|
+
body = excluded.body,
|
|
148
|
+
data = excluded.data,
|
|
149
|
+
updated_at = CURRENT_TIMESTAMP
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
this.db.prepare(query).run(
|
|
153
|
+
path,
|
|
154
|
+
JSON.stringify(meta),
|
|
155
|
+
JSON.stringify(body),
|
|
156
|
+
JSON.stringify(data)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = SQLiteAdapter;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const DatabaseAdapter = require('./DatabaseAdapter');
|
|
2
|
+
const MongoAdapter = require('./MongoAdapter');
|
|
3
|
+
const PostgresAdapter = require('./PostgresAdapter');
|
|
4
|
+
const MySQLAdapter = require('./MySQLAdapter');
|
|
5
|
+
const SQLiteAdapter = require('./SQLiteAdapter');
|
|
6
|
+
const AdapterFactory = require('./AdapterFactory');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
DatabaseAdapter,
|
|
10
|
+
MongoAdapter,
|
|
11
|
+
PostgresAdapter,
|
|
12
|
+
MySQLAdapter,
|
|
13
|
+
SQLiteAdapter,
|
|
14
|
+
AdapterFactory
|
|
15
|
+
};
|
package/constants.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//npm install http;
|
|
2
|
+
var http = require('http');
|
|
3
|
+
//npm install express
|
|
4
|
+
var express = require("express");
|
|
5
|
+
//npm install node-red
|
|
6
|
+
var RED = require("node-red");
|
|
7
|
+
//npm install util
|
|
8
|
+
var util = require("util");
|
|
9
|
+
|
|
10
|
+
// Create an Express app
|
|
11
|
+
var app = express();
|
|
12
|
+
|
|
13
|
+
// Add a simple route for static content served from 'public'
|
|
14
|
+
app.use("/",express.static("public"));
|
|
15
|
+
|
|
16
|
+
// Create a server
|
|
17
|
+
var server = http.createServer(app);
|
|
18
|
+
|
|
19
|
+
// Create the settings object - see default settings.js file for other options
|
|
20
|
+
var settings = {
|
|
21
|
+
httpAdminRoot:"/red",
|
|
22
|
+
httpNodeRoot: "/api",
|
|
23
|
+
//on windows
|
|
24
|
+
userDir:"c:\\node-red-files",
|
|
25
|
+
//on linux
|
|
26
|
+
// userDir:"/home/user",
|
|
27
|
+
uiPort : 1880,
|
|
28
|
+
uiHost: "localhost",
|
|
29
|
+
storageModule : require("node-red-mongo-storage-plugin"),
|
|
30
|
+
storageModuleOptions: {
|
|
31
|
+
mongoUrl: 'mongodb://localhost:27017',
|
|
32
|
+
database: 'local',
|
|
33
|
+
collectionNames:{
|
|
34
|
+
flows: "nodered-flows",
|
|
35
|
+
credentials: "nodered-credentials",
|
|
36
|
+
settings: "nodered-settings",
|
|
37
|
+
sessions: "nodered-sessions"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
functionGlobalContext: { } // enables global context
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Initialise the runtime with a server and settings
|
|
44
|
+
RED.init(server,settings);
|
|
45
|
+
|
|
46
|
+
// Serve the editor UI from /red
|
|
47
|
+
app.use(settings.httpAdminRoot,RED.httpAdmin);
|
|
48
|
+
|
|
49
|
+
// Serve the http nodes UI from /api
|
|
50
|
+
app.use(settings.httpNodeRoot,RED.httpNode);
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
function getListenPath() {
|
|
55
|
+
var port = settings.serverPort;
|
|
56
|
+
if (port === undefined){
|
|
57
|
+
port = settings.uiPort;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var listenPath = 'http'+(settings.https?'s':'')+'://'+
|
|
61
|
+
(settings.uiHost == '::'?'localhost':(settings.uiHost == '0.0.0.0'?'127.0.0.1':settings.uiHost))+
|
|
62
|
+
':'+port;
|
|
63
|
+
if (settings.httpAdminRoot !== false) {
|
|
64
|
+
listenPath += settings.httpAdminRoot;
|
|
65
|
+
} else if (settings.httpStatic) {
|
|
66
|
+
listenPath += "/";
|
|
67
|
+
}
|
|
68
|
+
return listenPath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
RED.start().then(function() {
|
|
72
|
+
if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) {
|
|
73
|
+
server.on('error', function(err) {
|
|
74
|
+
if (err.errno === "EADDRINUSE") {
|
|
75
|
+
RED.log.error(RED.log._("server.unable-to-listen", {listenpath:getListenPath()}));
|
|
76
|
+
RED.log.error(RED.log._("server.port-in-use"));
|
|
77
|
+
} else {
|
|
78
|
+
RED.log.error(RED.log._("server.uncaught-exception"));
|
|
79
|
+
if (err.stack) {
|
|
80
|
+
RED.log.error(err.stack);
|
|
81
|
+
} else {
|
|
82
|
+
RED.log.error(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
server.listen(settings.uiPort,settings.uiHost,function() {
|
|
88
|
+
if (settings.httpAdminRoot === false) {
|
|
89
|
+
RED.log.info(RED.log._("server.admin-ui-disabled"));
|
|
90
|
+
}
|
|
91
|
+
settings.serverPort = server.address().port;
|
|
92
|
+
process.title = 'node-red';
|
|
93
|
+
RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()}));
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
RED.log.info(RED.log._("server.headless-mode"));
|
|
97
|
+
}
|
|
98
|
+
}).otherwise(function(err) {
|
|
99
|
+
RED.log.error(RED.log._("server.failed-to-start"));
|
|
100
|
+
if (err.stack) {
|
|
101
|
+
RED.log.error(err.stack);
|
|
102
|
+
} else {
|
|
103
|
+
RED.log.error(err);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
process.on('uncaughtException',function(err) {
|
|
108
|
+
util.log('[red] Uncaught Exception:');
|
|
109
|
+
if (err.stack) {
|
|
110
|
+
util.log(err.stack);
|
|
111
|
+
} else {
|
|
112
|
+
util.log(err);
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const constants = require("./constants");
|
|
2
|
+
const { AdapterFactory } = require("./adapters");
|
|
3
|
+
const { UrlParser } = require("./utils");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage module for Node-RED with pluggable database backends.
|
|
7
|
+
* Encapsulates adapter and settings state within the module closure.
|
|
8
|
+
*/
|
|
9
|
+
var storageModule = (function () {
|
|
10
|
+
// Private state encapsulated in closure
|
|
11
|
+
let _settings = null;
|
|
12
|
+
let _adapter = null;
|
|
13
|
+
let _collectionNames = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse configuration options and extract database connection details
|
|
17
|
+
* @param {Object} options - Storage module options
|
|
18
|
+
* @returns {Object} Parsed configuration with dbType, dbUrl, dbName
|
|
19
|
+
*/
|
|
20
|
+
function parseConfiguration(options) {
|
|
21
|
+
let dbType, dbUrl, dbName;
|
|
22
|
+
|
|
23
|
+
if (options.type) {
|
|
24
|
+
// New configuration format with explicit type
|
|
25
|
+
dbType = options.type;
|
|
26
|
+
dbUrl = options.url;
|
|
27
|
+
dbName = options.database;
|
|
28
|
+
} else if (options.mongoUrl) {
|
|
29
|
+
// Legacy MongoDB configuration for backward compatibility
|
|
30
|
+
dbType = "mongodb";
|
|
31
|
+
dbUrl = options.mongoUrl;
|
|
32
|
+
dbName = options.database;
|
|
33
|
+
} else if (options.postgresUrl) {
|
|
34
|
+
// PostgreSQL configuration
|
|
35
|
+
dbType = "postgres";
|
|
36
|
+
dbUrl = options.postgresUrl;
|
|
37
|
+
dbName = options.database;
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Database URL is required. Provide 'url' with 'type', or 'mongoUrl'/'postgresUrl'",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!dbUrl) {
|
|
45
|
+
throw new Error("Database URL is required");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract database name from URL if not provided
|
|
49
|
+
if (!dbName) {
|
|
50
|
+
dbName = UrlParser.extractDatabaseName(dbType, dbUrl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Standardized interface requires database name for all adapters
|
|
54
|
+
if (!dbName) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Database name is required. Provide 'database' in configuration or include it in the connection URL",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { dbType, dbUrl, dbName };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize collection names from configuration
|
|
65
|
+
* @param {Object} options - Storage module options
|
|
66
|
+
* @returns {Object} Collection names mapping
|
|
67
|
+
*/
|
|
68
|
+
function initializeCollectionNames(options) {
|
|
69
|
+
const collectionNames = Object.assign({}, constants.DefaultCollectionNames);
|
|
70
|
+
if (options.collectionNames != null) {
|
|
71
|
+
for (let name of Object.keys(options.collectionNames)) {
|
|
72
|
+
collectionNames[name] = options.collectionNames[name];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return collectionNames;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
init: function (settings) {
|
|
80
|
+
_settings = settings;
|
|
81
|
+
|
|
82
|
+
// Validate required options
|
|
83
|
+
if (_settings.storageModuleOptions == null) {
|
|
84
|
+
throw new Error("storageModuleOptions is required");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const options = _settings.storageModuleOptions;
|
|
88
|
+
|
|
89
|
+
// Parse configuration
|
|
90
|
+
const { dbType, dbUrl, dbName } = parseConfiguration(options);
|
|
91
|
+
|
|
92
|
+
// Set up collection names
|
|
93
|
+
_collectionNames = initializeCollectionNames(options);
|
|
94
|
+
|
|
95
|
+
// Create adapter
|
|
96
|
+
_adapter = AdapterFactory.create(dbType, {
|
|
97
|
+
url: dbUrl,
|
|
98
|
+
database: dbName,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return _adapter.connect();
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Expose collection names for external access
|
|
105
|
+
get collectionNames() {
|
|
106
|
+
return _collectionNames;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
getFlows: function () {
|
|
110
|
+
return _adapter.findAll(_collectionNames.flows);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
saveFlows: function (flows) {
|
|
114
|
+
return _adapter.saveAll(_collectionNames.flows, flows);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
getCredentials: function () {
|
|
118
|
+
return _adapter.findAll(_collectionNames.credentials).then((result) => {
|
|
119
|
+
// Convert array back to object format that Node-RED expects
|
|
120
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
121
|
+
return result[0];
|
|
122
|
+
}
|
|
123
|
+
return {};
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
saveCredentials: function (credentials) {
|
|
128
|
+
// Node-RED passes credentials as an object, convert to array for storage
|
|
129
|
+
const credentialsArray =
|
|
130
|
+
credentials && Object.keys(credentials).length > 0 ? [credentials] : [];
|
|
131
|
+
return _adapter.saveAll(_collectionNames.credentials, credentialsArray);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
getSettings: function () {
|
|
135
|
+
return _adapter.findAll(_collectionNames.settings).then((result) => {
|
|
136
|
+
// Convert array back to object format that Node-RED expects
|
|
137
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
138
|
+
return result[0];
|
|
139
|
+
}
|
|
140
|
+
return {};
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
saveSettings: function (settings) {
|
|
145
|
+
// Node-RED passes settings as an object, convert to array for storage
|
|
146
|
+
const settingsArray =
|
|
147
|
+
settings && Object.keys(settings).length > 0 ? [settings] : [];
|
|
148
|
+
return _adapter.saveAll(_collectionNames.settings, settingsArray);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
getSessions: function () {
|
|
152
|
+
return _adapter.findAll(_collectionNames.sessions).then((result) => {
|
|
153
|
+
// Convert array back to object format that Node-RED expects
|
|
154
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
155
|
+
return result[0];
|
|
156
|
+
}
|
|
157
|
+
return {};
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
saveSessions: function (sessions) {
|
|
162
|
+
// Node-RED passes sessions as an object, convert to array for storage
|
|
163
|
+
const sessionsArray =
|
|
164
|
+
sessions && Object.keys(sessions).length > 0 ? [sessions] : [];
|
|
165
|
+
return _adapter.saveAll(_collectionNames.sessions, sessionsArray);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
getLibraryEntry: function (type, path) {
|
|
169
|
+
return _adapter.findOneByPath(type, path);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
saveLibraryEntry: function (type, path, meta, body) {
|
|
173
|
+
return _adapter.saveOrUpdateByPath(type, path, meta, body);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
close: function () {
|
|
177
|
+
if (_adapter) {
|
|
178
|
+
return _adapter.close();
|
|
179
|
+
}
|
|
180
|
+
return Promise.resolve();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
})();
|
|
184
|
+
|
|
185
|
+
module.exports = storageModule;
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-db-storage",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Node-RED storage plugin with support for MongoDB, PostgreSQL, and other databases",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest --coverage --testPathIgnorePatterns=integration",
|
|
8
|
+
"test:unit": "jest --coverage --testPathIgnorePatterns=integration",
|
|
9
|
+
"test:integration": "jest --testMatch='**/*.integration.test.js' --runInBand",
|
|
10
|
+
"test:integration:mongo": "TEST_DATABASES=mongodb jest --testMatch='**/*.integration.test.js' --runInBand",
|
|
11
|
+
"test:integration:postgres": "TEST_DATABASES=postgres jest --testMatch='**/*.integration.test.js' --runInBand",
|
|
12
|
+
"test:all": "npm run test:unit && npm run test:integration",
|
|
13
|
+
"docker:up": "docker-compose up -d",
|
|
14
|
+
"docker:down": "docker-compose down",
|
|
15
|
+
"docker:logs": "docker-compose logs -f"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/srmurali002/node-red-contrib-db-storage.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"node-red",
|
|
23
|
+
"storage",
|
|
24
|
+
"mongodb",
|
|
25
|
+
"postgres",
|
|
26
|
+
"postgresql",
|
|
27
|
+
"database"
|
|
28
|
+
],
|
|
29
|
+
"author": "srmurali002",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/srmurali002/node-red-contrib-db-storage/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/srmurali002/node-red-contrib-db-storage#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=10.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"better-sqlite3": "^12.4.1",
|
|
40
|
+
"mongodb": "^3.4.1",
|
|
41
|
+
"mysql2": "^3.15.3"
|
|
42
|
+
},
|
|
43
|
+
"optionalDependencies": {
|
|
44
|
+
"pg": "^8.11.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"jest": "^29.7.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"pg": "^8.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"pg": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"jest": {
|
|
58
|
+
"testEnvironment": "node",
|
|
59
|
+
"coverageDirectory": "coverage",
|
|
60
|
+
"collectCoverageFrom": [
|
|
61
|
+
"*.js",
|
|
62
|
+
"adapters/**/*.js",
|
|
63
|
+
"!jest.config.js"
|
|
64
|
+
],
|
|
65
|
+
"testMatch": [
|
|
66
|
+
"**/*.test.js"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for parsing database connection URLs
|
|
3
|
+
* Extracts database names from various connection URL formats
|
|
4
|
+
*/
|
|
5
|
+
class UrlParser {
|
|
6
|
+
/**
|
|
7
|
+
* Extract database name from a connection URL based on database type
|
|
8
|
+
* @param {string} dbType - The database type (postgres, mysql, sqlite, etc.)
|
|
9
|
+
* @param {string} url - The connection URL
|
|
10
|
+
* @returns {string|null} The extracted database name or null if not found
|
|
11
|
+
*/
|
|
12
|
+
static extractDatabaseName(dbType, url) {
|
|
13
|
+
if (!dbType || !url) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dbTypeLower = dbType.toLowerCase();
|
|
18
|
+
|
|
19
|
+
// PostgreSQL and MySQL: extract from connection URL
|
|
20
|
+
// Format: postgresql://user:password@host:port/database or mysql://user:password@host:port/database
|
|
21
|
+
if (this.isRelationalDatabase(dbTypeLower)) {
|
|
22
|
+
return this.extractFromConnectionUrl(url);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// SQLite: use the filename as database name
|
|
26
|
+
if (this.isSQLite(dbTypeLower)) {
|
|
27
|
+
return this.extractFromSQLitePath(url);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MongoDB: extract from connection URL
|
|
31
|
+
if (this.isMongoDB(dbTypeLower)) {
|
|
32
|
+
return this.extractFromConnectionUrl(url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the database type is a relational database (PostgreSQL or MySQL)
|
|
40
|
+
* @param {string} dbType - The database type in lowercase
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
static isRelationalDatabase(dbType) {
|
|
44
|
+
return ['postgres', 'postgresql', 'pg', 'mysql', 'mariadb'].includes(dbType);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if the database type is SQLite
|
|
49
|
+
* @param {string} dbType - The database type in lowercase
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
static isSQLite(dbType) {
|
|
53
|
+
return ['sqlite', 'sqlite3'].includes(dbType);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if the database type is MongoDB
|
|
58
|
+
* @param {string} dbType - The database type in lowercase
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
static isMongoDB(dbType) {
|
|
62
|
+
return ['mongodb', 'mongo'].includes(dbType);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract database name from a standard connection URL
|
|
67
|
+
* @param {string} url - The connection URL
|
|
68
|
+
* @returns {string|null} The database name or null if not found
|
|
69
|
+
*/
|
|
70
|
+
static extractFromConnectionUrl(url) {
|
|
71
|
+
// Parse URL to extract path after host:port
|
|
72
|
+
// Format: protocol://[user:pass@]host[:port]/database[?params]
|
|
73
|
+
// We need to find the database name which is in the path after the authority
|
|
74
|
+
|
|
75
|
+
// Remove protocol
|
|
76
|
+
let remaining = url.replace(/^[a-zA-Z]+:\/\//, '');
|
|
77
|
+
|
|
78
|
+
// Find the start of the path (first slash after host:port)
|
|
79
|
+
const pathStart = remaining.indexOf('/');
|
|
80
|
+
if (pathStart === -1) {
|
|
81
|
+
return null; // No path, no database
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract path
|
|
85
|
+
let path = remaining.substring(pathStart + 1);
|
|
86
|
+
|
|
87
|
+
// Remove query parameters
|
|
88
|
+
const queryStart = path.indexOf('?');
|
|
89
|
+
if (queryStart !== -1) {
|
|
90
|
+
path = path.substring(0, queryStart);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The database name is what's left
|
|
94
|
+
return path || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract database name from SQLite file path
|
|
99
|
+
* @param {string} url - The SQLite file path or URL
|
|
100
|
+
* @returns {string} The database name (filename without extension)
|
|
101
|
+
*/
|
|
102
|
+
static extractFromSQLitePath(url) {
|
|
103
|
+
let filePath = url;
|
|
104
|
+
|
|
105
|
+
// Remove URL prefixes
|
|
106
|
+
if (filePath.startsWith('sqlite://')) {
|
|
107
|
+
filePath = filePath.replace('sqlite://', '');
|
|
108
|
+
} else if (filePath.startsWith('file://')) {
|
|
109
|
+
filePath = filePath.replace('file://', '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle memory databases
|
|
113
|
+
if (filePath === ':memory:') {
|
|
114
|
+
return 'memory';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract filename without extension as database name
|
|
118
|
+
const fileName = filePath.split('/').pop();
|
|
119
|
+
return fileName.replace(/\.[^/.]+$/, '') || 'sqlite';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Normalize SQLite file path by removing URL prefixes
|
|
124
|
+
* @param {string} url - The SQLite file path or URL
|
|
125
|
+
* @returns {string} The normalized file path
|
|
126
|
+
*/
|
|
127
|
+
static normalizeSQLitePath(url) {
|
|
128
|
+
let filePath = url;
|
|
129
|
+
|
|
130
|
+
if (filePath.startsWith('sqlite://')) {
|
|
131
|
+
filePath = filePath.replace('sqlite://', '');
|
|
132
|
+
} else if (filePath.startsWith('file://')) {
|
|
133
|
+
filePath = filePath.replace('file://', '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return filePath;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = UrlParser;
|