pixl-server-storage 3.1.1 → 3.1.2

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/README.md CHANGED
@@ -611,6 +611,32 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
611
611
 
612
612
  The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
613
613
 
614
+ ## SQLite
615
+
616
+ If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [sqlite3](https://www.npmjs.com/package/sqlite3) module into your app:
617
+
618
+ ```
619
+ npm install --save sqlite3
620
+ ```
621
+
622
+ Then configure your storage thusly:
623
+
624
+ ```javascript
625
+ {
626
+ "engine": "SQLite",
627
+ "SQLite": {
628
+ "base_dir": "data",
629
+ "filename": "sqlite.db"
630
+ }
631
+ }
632
+ ```
633
+
634
+ The `base_dir` defaults to the current working directory, and will be created on startup if necessary.
635
+
636
+ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key-prefix) feature. It allows you to prefix all the SQLite keys with a common string, to separate your application's data in a shared database situation.
637
+
638
+ The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
639
+
614
640
  ## Hybrid
615
641
 
616
642
  Your application may need the features of multiple engines. Specifically, you may want JSON (document) records to use one engine, and binary records to use another. Binary records are specified with keys that end in a file extension, e.g. `.jpg`. To facilitate this, there is a `Hybrid` engine available, which can load multiple sub-engines, one for JSON keys and one for binary keys. Example use:
@@ -0,0 +1,363 @@
1
+ // SQLite Storage Plugin
2
+ // Copyright (c) 2023 Joseph Huckaby
3
+ // Released under the MIT License
4
+
5
+ // Requires the 'sqlite3' module from npm
6
+ // npm install --save sqlite3
7
+
8
+ const Path = require('path');
9
+ const Class = require("pixl-class");
10
+ const Component = require("pixl-server/component");
11
+ const SQLite3 = require('sqlite3');
12
+ const Tools = require("pixl-tools");
13
+ const async = require('async');
14
+
15
+ module.exports = Class.create({
16
+
17
+ __name: 'SQLite',
18
+ __parent: Component,
19
+
20
+ defaultConfig: {
21
+ base_dir: '',
22
+ filename: 'sqlite.db',
23
+ keyPrefix: "",
24
+ keyTemplate: ""
25
+ },
26
+
27
+ startup: function(callback) {
28
+ // setup SQLite connection
29
+ var self = this;
30
+ this.logDebug(2, "Setting up SQLite", this.config.get() );
31
+ this.setup(callback);
32
+ },
33
+
34
+ setup: function(callback) {
35
+ // setup SQLite connection
36
+ var self = this;
37
+ var sql_config = this.config.get();
38
+
39
+ this.keyPrefix = (sql_config.keyPrefix || '').replace(/^\//, '');
40
+ if (this.keyPrefix && !this.keyPrefix.match(/\/$/)) this.keyPrefix += '/';
41
+
42
+ this.keyTemplate = (sql_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
43
+
44
+ this.baseDir = sql_config.base_dir || process.cwd();
45
+ this.commands = {};
46
+
47
+ // create initial data dir if necessary
48
+ try {
49
+ Tools.mkdirp.sync( this.baseDir );
50
+ }
51
+ catch (e) {
52
+ var msg = "FATAL ERROR: Base directory could not be created: " + this.baseDir + ": " + e;
53
+ this.logError('sqlite', msg);
54
+ throw new Error(msg);
55
+ }
56
+
57
+ var db_file = Path.join( this.baseDir, sql_config.filename );
58
+ this.logDebug(3, "Opening database file: " + db_file);
59
+
60
+ async.series([
61
+ function(callback) {
62
+ self.db = new SQLite3.Database( db_file, callback );
63
+ },
64
+ function(callback) {
65
+ self.db.run( 'CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )', callback );
66
+ },
67
+ function(callback) {
68
+ self.commands.get = self.db.prepare( 'SELECT value FROM items WHERE key = $key LIMIT 1', callback );
69
+ },
70
+ function(callback) {
71
+ self.commands.head = self.db.prepare( 'SELECT modified, length(value) FROM items WHERE key = $key LIMIT 1', callback );
72
+ },
73
+ function(callback) {
74
+ self.commands.put = self.db.prepare( 'INSERT INTO items VALUES($key, $value, $now) ON CONFLICT (key) DO UPDATE SET value = $value, modified = $now WHERE key = $key', callback );
75
+ },
76
+ function(callback) {
77
+ self.commands.delete = self.db.prepare( 'DELETE FROM items WHERE key = $key', callback );
78
+ }
79
+ ],
80
+ function(err) {
81
+ if (err) {
82
+ self.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
83
+ return callback(err);
84
+ }
85
+ self.logDebug(3, "Setup complete");
86
+ callback();
87
+ }); // async.series
88
+ },
89
+
90
+ prepKey: function(key) {
91
+ // prepare key based on config
92
+ var md5 = Tools.digestHex(key, 'md5');
93
+
94
+ if (this.keyPrefix) {
95
+ key = this.keyPrefix + key;
96
+ }
97
+
98
+ if (this.keyTemplate) {
99
+ var idx = 0;
100
+ var temp = this.keyTemplate.replace( /\#/g, function() {
101
+ return md5.substr(idx++, 1);
102
+ } );
103
+ key = Tools.substitute( temp, { key: key, md5: md5 } );
104
+ }
105
+
106
+ return key;
107
+ },
108
+
109
+ put: function(key, value, callback) {
110
+ // store key+value in SQLite
111
+ var self = this;
112
+ var now = Tools.timeNow(true);
113
+ key = this.prepKey(key);
114
+
115
+ if (this.storage.isBinaryKey(key)) {
116
+ this.logDebug(9, "Storing SQLite Binary Object: " + key, '' + value.length + ' bytes');
117
+ }
118
+ else {
119
+ this.logDebug(9, "Storing SQLite JSON Object: " + key, this.debugLevel(10) ? value : null);
120
+ value = Buffer.from( JSON.stringify( value ) );
121
+ }
122
+
123
+ this.commands.put.run({ $key: key, $value: value, $now: now }, function(err) {
124
+ if (err) {
125
+ err.message = "Failed to store object: " + key + ": " + err;
126
+ self.logError('sqlite', '' + err);
127
+ }
128
+ else self.logDebug(9, "Store complete: " + key);
129
+
130
+ if (callback) callback(err);
131
+ } ); // put
132
+ },
133
+
134
+ putStream: function(key, inp, callback) {
135
+ // store key+value in SQLite using read stream
136
+ var self = this;
137
+
138
+ // The SQLite API has no stream support.
139
+ // So, we have to do this the RAM-hard way...
140
+
141
+ var chunks = [];
142
+ inp.on('data', function(chunk) {
143
+ chunks.push( chunk );
144
+ } );
145
+ inp.on('end', function() {
146
+ var buf = Buffer.concat(chunks);
147
+ self.put( key, buf, callback );
148
+ } );
149
+ },
150
+
151
+ head: function(key, callback) {
152
+ // head sqlite item given key
153
+ var self = this;
154
+ key = this.prepKey(key);
155
+
156
+ this.commands.head.get({ $key: key }, function(err, row) {
157
+ if (err) {
158
+ // an actual error
159
+ err.message = "Failed to head key: " + key + ": " + err;
160
+ self.logError('sqlite', '' + err);
161
+ callback(err);
162
+ }
163
+ else if (!row) {
164
+ // record not found
165
+ // always use "NoSuchKey" in error code
166
+ var err = new Error("Failed to head key: " + key + ": Not found");
167
+ err.code = "NoSuchKey";
168
+ callback( err, null );
169
+ }
170
+ else {
171
+ callback( null, { mod: row.modified, len: row['length(value)'] } );
172
+ }
173
+ }); // head
174
+ },
175
+
176
+ get: function(key, callback) {
177
+ // fetch SQLite value given key
178
+ var self = this;
179
+ key = this.prepKey(key);
180
+
181
+ this.logDebug(9, "Fetching SQLite Object: " + key);
182
+
183
+ this.commands.get.get({ $key: key }, function(err, row) {
184
+ if (err) {
185
+ // an actual error
186
+ err.message = "Failed to fetch key: " + key + ": " + err;
187
+ self.logError('sqlite', '' + err);
188
+ callback(err);
189
+ }
190
+ else if (!row) {
191
+ // record not found
192
+ // always use "NoSuchKey" in error code
193
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
194
+ err.code = "NoSuchKey";
195
+ callback( err, null );
196
+ }
197
+ else {
198
+ // success
199
+ if (self.storage.isBinaryKey(key)) {
200
+ self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
201
+ callback( null, row.value );
202
+ }
203
+ else {
204
+ var json = null;
205
+ try { json = JSON.parse( row.value.toString() ); }
206
+ catch (err) {
207
+ self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
208
+ callback( err, null );
209
+ return;
210
+ }
211
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
212
+ callback( null, json );
213
+ }
214
+ }
215
+ }); // get
216
+ },
217
+
218
+ getStream: function(key, callback) {
219
+ // get readable stream to record value given key
220
+ var self = this;
221
+
222
+ // The SQLite API has no stream support.
223
+ // So, we have to do this the RAM-hard way...
224
+
225
+ this.get( key, function(err, buf) {
226
+ if (err) {
227
+ // an actual error
228
+ err.message = "Failed to fetch key: " + key + ": " + err;
229
+ self.logError('sqlite', '' + err);
230
+ return callback(err);
231
+ }
232
+ else if (!buf) {
233
+ // record not found
234
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
235
+ err.code = "NoSuchKey";
236
+ return callback( err, null );
237
+ }
238
+
239
+ var stream = new BufferStream(buf);
240
+ callback(null, stream, { mod: 1, len: buf.length });
241
+ } );
242
+ },
243
+
244
+ getStreamRange: function(key, start, end, callback) {
245
+ // get readable stream to record value given key and range
246
+ var self = this;
247
+
248
+ // The SQLite API has no stream support.
249
+ // So, we have to do this the RAM-hard way...
250
+
251
+ this.get( key, function(err, buf) {
252
+ if (err) {
253
+ // an actual error
254
+ err.message = "Failed to fetch key: " + key + ": " + err;
255
+ self.logError('sqlite', '' + err);
256
+ return callback(err);
257
+ }
258
+ else if (!buf) {
259
+ // record not found
260
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
261
+ err.code = "NoSuchKey";
262
+ return callback( err, null );
263
+ }
264
+
265
+ // validate byte range, now that we have the head info
266
+ if (isNaN(start) && !isNaN(end)) {
267
+ start = buf.length - end;
268
+ end = buf.length ? buf.length - 1 : 0;
269
+ }
270
+ else if (!isNaN(start) && isNaN(end)) {
271
+ end = buf.length ? buf.length - 1 : 0;
272
+ }
273
+ if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
274
+ download.destroy();
275
+ callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
276
+ return;
277
+ }
278
+
279
+ var range = buf.slice(start, end + 1);
280
+ var stream = new BufferStream(range);
281
+ callback(null, stream, { mod: 1, len: buf.length });
282
+ } );
283
+ },
284
+
285
+ delete: function(key, callback) {
286
+ // delete SQLite key given key
287
+ var self = this;
288
+ key = this.prepKey(key);
289
+
290
+ this.logDebug(9, "Deleting SQLite Object: " + key);
291
+
292
+ this.commands.delete.run({ $key: key }, function(err) {
293
+ // In sqlite3 callbacks `this` is special, and contains `changes`
294
+ if (!err && !this.changes) {
295
+ err = new Error("Not found");
296
+ err.code = "NoSuchKey";
297
+ }
298
+ if (err) {
299
+ self.logError('sqlite', "Failed to delete object: " + key + ": " + err);
300
+ err = new Error("Failed to delete object: " + key + ": " + err);
301
+ return callback(err);
302
+ }
303
+ self.logDebug(9, "Delete complete: " + key);
304
+ callback();
305
+ }); // delete
306
+ },
307
+
308
+ runMaintenance: function(callback) {
309
+ // run daily maintenance
310
+ callback();
311
+ },
312
+
313
+ shutdown: function(callback) {
314
+ // shutdown storage
315
+ var self = this;
316
+ this.logDebug(2, "Shutting down SQLite");
317
+
318
+ if (this.db) {
319
+ // finalize all statements and close db
320
+ async.eachSeries( Object.keys(this.commands),
321
+ function(key, callback) {
322
+ self.commands[key].finalize(callback);
323
+ },
324
+ function() {
325
+ self.db.close( function(err) {
326
+ if (err) self.logError('sqlite', "Failed to shutdown database cleanly: " + err);
327
+ else self.logDebug(3, "Shutdown complete");
328
+ callback();
329
+ } );
330
+ self.db = null;
331
+ self.commands = null;
332
+ }
333
+ ); // eachSeries
334
+ }
335
+ }
336
+
337
+ });
338
+
339
+ // Modified the following snippet from node-streamifier:
340
+ // Copyright (c) 2014 Gabriel Llamas, MIT Licensed
341
+
342
+ var util = require('util');
343
+ var stream = require('stream');
344
+
345
+ var BufferStream = function (object, options) {
346
+ if (object instanceof Buffer || typeof object === 'string') {
347
+ options = options || {};
348
+ stream.Readable.call(this, {
349
+ highWaterMark: options.highWaterMark,
350
+ encoding: options.encoding
351
+ });
352
+ } else {
353
+ stream.Readable.call(this, { objectMode: true });
354
+ }
355
+ this._object = object;
356
+ };
357
+
358
+ util.inherits(BufferStream, stream.Readable);
359
+
360
+ BufferStream.prototype._read = function () {
361
+ this.push(this._object);
362
+ this._object = null;
363
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-storage",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "A key/value/list storage component for the pixl-server framework.",
5
5
  "author": "Joseph Huckaby <jhuckaby@gmail.com>",
6
6
  "homepage": "https://github.com/jhuckaby/pixl-server-storage",
@@ -18,7 +18,8 @@
18
18
  "nosql",
19
19
  "s3",
20
20
  "couchbase",
21
- "redis"
21
+ "redis",
22
+ "sqlite"
22
23
  ],
23
24
  "dependencies": {
24
25
  "@aws-sdk/client-s3": "3.186.0",
@@ -27,16 +28,16 @@
27
28
  "he": "1.1.1",
28
29
  "moo": "0.4.3",
29
30
  "nearley": "2.20.1",
30
- "pixl-cache": "^1.0.1",
31
+ "pixl-cache": "^1.0.6",
31
32
  "pixl-class": "^1.0.3",
32
- "pixl-perf": "^1.0.0",
33
- "pixl-server": "^1.0.0",
34
- "pixl-tools": "^1.0.24",
33
+ "pixl-perf": "^1.0.9",
34
+ "pixl-server": "^1.0.39",
35
+ "pixl-tools": "^1.0.33",
35
36
  "porter-stemmer": "0.9.1",
36
37
  "unidecode": "0.1.8"
37
38
  },
38
39
  "devDependencies": {
39
- "pixl-unit": "^1.0.0"
40
+ "pixl-unit": "^1.0.14"
40
41
  },
41
42
  "scripts": {
42
43
  "test": "pixl-unit test/test.js",
package/test/config.json CHANGED
@@ -64,6 +64,10 @@
64
64
  "host": "127.0.0.1",
65
65
  "port": 6379,
66
66
  "keyPrefix": ""
67
+ },
68
+ "SQLite": {
69
+ "base_dir": "data",
70
+ "filename": "sqlite.db"
67
71
  }
68
72
  }
69
73
  }