pixl-server-storage 3.1.1 → 3.1.3

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,34 @@ 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
+ "pragmas": {
631
+ "auto_vacuum": 0,
632
+ "cache_size": -100000
633
+ }
634
+ }
635
+ }
636
+ ```
637
+
638
+ The `base_dir` defaults to the current working directory, and will be created on startup if necessary. The `filename` is the name of the SQLite DB file on disk (also created if necessary).
639
+
640
+ The optional `pragmas` object allows you set one or more [SQLite Pragmas](https://www.sqlite.org/pragma.html#toc) (configuration settings) on the database at startup. Here you can specify things such as [auto_vacuum](https://www.sqlite.org/pragma.html#pragma_auto_vacuum) and [cache_size](https://www.sqlite.org/pragma.html#pragma_cache_size), among many others.
641
+
614
642
  ## Hybrid
615
643
 
616
644
  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,375 @@
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
+ // optionally set pragmas on the db
66
+ if (!sql_config.pragmas) return process.nextTick(callback);
67
+ async.eachSeries( Object.keys(sql_config.pragmas),
68
+ function(key, callback) {
69
+ var value = sql_config.pragmas[key];
70
+ self.db.run(`PRAGMA ${key} = ${value};`, callback);
71
+ },
72
+ callback
73
+ ); // eachSeries
74
+ },
75
+ function(callback) {
76
+ // create our table if necessary
77
+ self.db.run( 'CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )', callback );
78
+ },
79
+ function(callback) {
80
+ self.commands.get = self.db.prepare( 'SELECT value FROM items WHERE key = $key LIMIT 1', callback );
81
+ },
82
+ function(callback) {
83
+ self.commands.head = self.db.prepare( 'SELECT modified, length(value) FROM items WHERE key = $key LIMIT 1', callback );
84
+ },
85
+ function(callback) {
86
+ 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 );
87
+ },
88
+ function(callback) {
89
+ self.commands.delete = self.db.prepare( 'DELETE FROM items WHERE key = $key', callback );
90
+ }
91
+ ],
92
+ function(err) {
93
+ if (err) {
94
+ self.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
95
+ return callback(err);
96
+ }
97
+ self.logDebug(3, "Setup complete");
98
+ callback();
99
+ }); // async.series
100
+ },
101
+
102
+ prepKey: function(key) {
103
+ // prepare key based on config
104
+ var md5 = Tools.digestHex(key, 'md5');
105
+
106
+ if (this.keyPrefix) {
107
+ key = this.keyPrefix + key;
108
+ }
109
+
110
+ if (this.keyTemplate) {
111
+ var idx = 0;
112
+ var temp = this.keyTemplate.replace( /\#/g, function() {
113
+ return md5.substr(idx++, 1);
114
+ } );
115
+ key = Tools.substitute( temp, { key: key, md5: md5 } );
116
+ }
117
+
118
+ return key;
119
+ },
120
+
121
+ put: function(key, value, callback) {
122
+ // store key+value in SQLite
123
+ var self = this;
124
+ var now = Tools.timeNow(true);
125
+ key = this.prepKey(key);
126
+
127
+ if (this.storage.isBinaryKey(key)) {
128
+ this.logDebug(9, "Storing SQLite Binary Object: " + key, '' + value.length + ' bytes');
129
+ }
130
+ else {
131
+ this.logDebug(9, "Storing SQLite JSON Object: " + key, this.debugLevel(10) ? value : null);
132
+ value = Buffer.from( JSON.stringify( value ) );
133
+ }
134
+
135
+ this.commands.put.run({ $key: key, $value: value, $now: now }, function(err) {
136
+ if (err) {
137
+ err.message = "Failed to store object: " + key + ": " + err;
138
+ self.logError('sqlite', '' + err);
139
+ }
140
+ else self.logDebug(9, "Store complete: " + key);
141
+
142
+ if (callback) callback(err);
143
+ } ); // put
144
+ },
145
+
146
+ putStream: function(key, inp, callback) {
147
+ // store key+value in SQLite using read stream
148
+ var self = this;
149
+
150
+ // The SQLite API has no stream support.
151
+ // So, we have to do this the RAM-hard way...
152
+
153
+ var chunks = [];
154
+ inp.on('data', function(chunk) {
155
+ chunks.push( chunk );
156
+ } );
157
+ inp.on('end', function() {
158
+ var buf = Buffer.concat(chunks);
159
+ self.put( key, buf, callback );
160
+ } );
161
+ },
162
+
163
+ head: function(key, callback) {
164
+ // head sqlite item given key
165
+ var self = this;
166
+ key = this.prepKey(key);
167
+
168
+ this.commands.head.get({ $key: key }, function(err, row) {
169
+ if (err) {
170
+ // an actual error
171
+ err.message = "Failed to head key: " + key + ": " + err;
172
+ self.logError('sqlite', '' + err);
173
+ callback(err);
174
+ }
175
+ else if (!row) {
176
+ // record not found
177
+ // always use "NoSuchKey" in error code
178
+ var err = new Error("Failed to head key: " + key + ": Not found");
179
+ err.code = "NoSuchKey";
180
+ callback( err, null );
181
+ }
182
+ else {
183
+ callback( null, { mod: row.modified, len: row['length(value)'] } );
184
+ }
185
+ }); // head
186
+ },
187
+
188
+ get: function(key, callback) {
189
+ // fetch SQLite value given key
190
+ var self = this;
191
+ key = this.prepKey(key);
192
+
193
+ this.logDebug(9, "Fetching SQLite Object: " + key);
194
+
195
+ this.commands.get.get({ $key: key }, function(err, row) {
196
+ if (err) {
197
+ // an actual error
198
+ err.message = "Failed to fetch key: " + key + ": " + err;
199
+ self.logError('sqlite', '' + err);
200
+ callback(err);
201
+ }
202
+ else if (!row) {
203
+ // record not found
204
+ // always use "NoSuchKey" in error code
205
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
206
+ err.code = "NoSuchKey";
207
+ callback( err, null );
208
+ }
209
+ else {
210
+ // success
211
+ if (self.storage.isBinaryKey(key)) {
212
+ self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
213
+ callback( null, row.value );
214
+ }
215
+ else {
216
+ var json = null;
217
+ try { json = JSON.parse( row.value.toString() ); }
218
+ catch (err) {
219
+ self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
220
+ callback( err, null );
221
+ return;
222
+ }
223
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
224
+ callback( null, json );
225
+ }
226
+ }
227
+ }); // get
228
+ },
229
+
230
+ getStream: function(key, callback) {
231
+ // get readable stream to record value given key
232
+ var self = this;
233
+
234
+ // The SQLite API has no stream support.
235
+ // So, we have to do this the RAM-hard way...
236
+
237
+ this.get( key, function(err, buf) {
238
+ if (err) {
239
+ // an actual error
240
+ err.message = "Failed to fetch key: " + key + ": " + err;
241
+ self.logError('sqlite', '' + err);
242
+ return callback(err);
243
+ }
244
+ else if (!buf) {
245
+ // record not found
246
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
247
+ err.code = "NoSuchKey";
248
+ return callback( err, null );
249
+ }
250
+
251
+ var stream = new BufferStream(buf);
252
+ callback(null, stream, { mod: 1, len: buf.length });
253
+ } );
254
+ },
255
+
256
+ getStreamRange: function(key, start, end, callback) {
257
+ // get readable stream to record value given key and range
258
+ var self = this;
259
+
260
+ // The SQLite API has no stream support.
261
+ // So, we have to do this the RAM-hard way...
262
+
263
+ this.get( key, function(err, buf) {
264
+ if (err) {
265
+ // an actual error
266
+ err.message = "Failed to fetch key: " + key + ": " + err;
267
+ self.logError('sqlite', '' + err);
268
+ return callback(err);
269
+ }
270
+ else if (!buf) {
271
+ // record not found
272
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
273
+ err.code = "NoSuchKey";
274
+ return callback( err, null );
275
+ }
276
+
277
+ // validate byte range, now that we have the head info
278
+ if (isNaN(start) && !isNaN(end)) {
279
+ start = buf.length - end;
280
+ end = buf.length ? buf.length - 1 : 0;
281
+ }
282
+ else if (!isNaN(start) && isNaN(end)) {
283
+ end = buf.length ? buf.length - 1 : 0;
284
+ }
285
+ if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
286
+ download.destroy();
287
+ callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
288
+ return;
289
+ }
290
+
291
+ var range = buf.slice(start, end + 1);
292
+ var stream = new BufferStream(range);
293
+ callback(null, stream, { mod: 1, len: buf.length });
294
+ } );
295
+ },
296
+
297
+ delete: function(key, callback) {
298
+ // delete SQLite key given key
299
+ var self = this;
300
+ key = this.prepKey(key);
301
+
302
+ this.logDebug(9, "Deleting SQLite Object: " + key);
303
+
304
+ this.commands.delete.run({ $key: key }, function(err) {
305
+ // In sqlite3 callbacks `this` is special, and contains `changes`
306
+ if (!err && !this.changes) {
307
+ err = new Error("Not found");
308
+ err.code = "NoSuchKey";
309
+ }
310
+ if (err) {
311
+ self.logError('sqlite', "Failed to delete object: " + key + ": " + err);
312
+ err = new Error("Failed to delete object: " + key + ": " + err);
313
+ return callback(err);
314
+ }
315
+ self.logDebug(9, "Delete complete: " + key);
316
+ callback();
317
+ }); // delete
318
+ },
319
+
320
+ runMaintenance: function(callback) {
321
+ // run daily maintenance
322
+ callback();
323
+ },
324
+
325
+ shutdown: function(callback) {
326
+ // shutdown storage
327
+ var self = this;
328
+ this.logDebug(2, "Shutting down SQLite");
329
+
330
+ if (this.db) {
331
+ // finalize all statements and close db
332
+ async.eachSeries( Object.keys(this.commands),
333
+ function(key, callback) {
334
+ self.commands[key].finalize(callback);
335
+ },
336
+ function() {
337
+ self.db.close( function(err) {
338
+ if (err) self.logError('sqlite', "Failed to shutdown database cleanly: " + err);
339
+ else self.logDebug(3, "Shutdown complete");
340
+ callback();
341
+ } );
342
+ self.db = null;
343
+ self.commands = null;
344
+ }
345
+ ); // eachSeries
346
+ }
347
+ }
348
+
349
+ });
350
+
351
+ // Modified the following snippet from node-streamifier:
352
+ // Copyright (c) 2014 Gabriel Llamas, MIT Licensed
353
+
354
+ var util = require('util');
355
+ var stream = require('stream');
356
+
357
+ var BufferStream = function (object, options) {
358
+ if (object instanceof Buffer || typeof object === 'string') {
359
+ options = options || {};
360
+ stream.Readable.call(this, {
361
+ highWaterMark: options.highWaterMark,
362
+ encoding: options.encoding
363
+ });
364
+ } else {
365
+ stream.Readable.call(this, { objectMode: true });
366
+ }
367
+ this._object = object;
368
+ };
369
+
370
+ util.inherits(BufferStream, stream.Readable);
371
+
372
+ BufferStream.prototype._read = function () {
373
+ this.push(this._object);
374
+ this._object = null;
375
+ };
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.3",
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,14 @@
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",
71
+ "pragmas": {
72
+ "auto_vacuum": 0,
73
+ "cache_size": -100000
74
+ }
67
75
  }
68
76
  }
69
77
  }