pixl-server-storage 3.1.12 → 3.1.13

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
@@ -426,7 +426,7 @@ If you want to use [Amazon S3](http://aws.amazon.com/s3/) as a backing store, co
426
426
  "region": "us-west-1",
427
427
  "credentials": {
428
428
  "accessKeyId": "YOUR_AMAZON_ACCESS_KEY",
429
- "secretAccessKey": "YOUR_AMAZON_SECRET_KEY",
429
+ "secretAccessKey": "YOUR_AMAZON_SECRET_KEY"
430
430
  }
431
431
  },
432
432
  "S3": {
package/docs/API.md CHANGED
@@ -180,13 +180,27 @@ storage.getMulti( ['test1', 'test2', 'test3'], function(err, values) {
180
180
 
181
181
  Note that if *any* of the records fail, the entire operation fails, and the first error is passed to your callback.
182
182
 
183
+ ## getBuffer
184
+
185
+ ```javascript
186
+ storage.getBuffer( KEY, CALLBACK );
187
+ ```
188
+
189
+ The `getBuffer()` method retrieves a [Buffer](https://nodejs.org/api/buffer.html) to a given record's data, regardless if the key points to a JSON record or a binary record. Your callback function is passed an error if one occurred, and the buffer value for the given record. Example:
190
+
191
+ ```javascript
192
+ storage.getBuffer( 'test1', function(err, buf) {
193
+ if (err) throw err;
194
+ } );
195
+ ```
196
+
183
197
  ## getStream
184
198
 
185
199
  ```javascript
186
200
  storage.getStream( KEY, CALLBACK );
187
201
  ```
188
202
 
189
- The `getStream()` method retrieves a [readable stream](https://nodejs.org/api/stream.html#class-streamreadable) to a given record's data, so it can be read or piped to a writable stream. This is for very large records, so nothing is loaded into memory. Example of spooling to a local file:
203
+ The `getStream()` method retrieves a [readable stream](https://nodejs.org/api/stream.html#readable-streams) to a given record's data, so it can be read or piped to a writable stream. This is for very large records, so nothing is loaded into memory. Example of spooling to a local file:
190
204
 
191
205
  ```javascript
192
206
  var fs = require('fs');
@@ -982,6 +996,22 @@ storage.hashGetMulti( 'users', ['bsanders', 'hclinton', 'dtrump'], function(err,
982
996
  } );
983
997
  ```
984
998
 
999
+ ## hashUpdate
1000
+
1001
+ ```javascript
1002
+ storage.hashUpdate( PATH, KEY, UPDATES, CALLBACK );
1003
+ ```
1004
+
1005
+ The `hashUpdate()` method updates an existing key/value pair in a hash. The `PATH` specifies the main storage path of the hash, and the hash key itself is identified by `KEY`, which should be a string. The `UPDATES` must be an object, but it can contain sparse keys. Furthermore, it can contain dot or slash delimited paths, to update inner nested keys. The updates are essentially applied atop the exiting record, merging and replacing (overwriting) where appropriate. Example:
1006
+
1007
+ ```javascript
1008
+ storage.hashUpdate( 'users', 'bsanders', { age: 81 }, function(err) {
1009
+ if (err) throw err;
1010
+ } );
1011
+ ```
1012
+
1013
+ This would update Bernie's age to 81 without affecting any of the other properties in his user record.
1014
+
985
1015
  ## hashEach
986
1016
 
987
1017
  ```javascript
@@ -199,6 +199,7 @@ When transactions are enabled, many compound storage methods (those that execute
199
199
  - **Hash Methods**
200
200
  - [hashCreate()](API.md#hashcreate)
201
201
  - [hashPut()](API.md#hashput)
202
+ - [hashUpdate()](API.md#hashupdate)
202
203
  - [hashPutMulti()](API.md#hashputmulti)
203
204
  - [hashCopy()](API.md#hashcopy)
204
205
  - [hashRename()](API.md#hashrename)
@@ -203,6 +203,37 @@ module.exports = Class.create({
203
203
  } );
204
204
  },
205
205
 
206
+ getBuffer: function(key, callback) {
207
+ // fetch Couchbase buffer given key
208
+ var self = this;
209
+ key = this.prepKey(key);
210
+
211
+ this.logDebug(9, "Fetching Couchbase Object: " + key);
212
+
213
+ this.bucket.get( key, function(err, result) {
214
+ if (!result) {
215
+ if (err && (err.code != CouchbaseAPI.errors.keyNotFound)) {
216
+ // some other error
217
+ err.message = "Failed to fetch key: " + key + ": " + err.message;
218
+ self.logError('couchbase', err.message);
219
+ callback( err, null );
220
+ }
221
+ else {
222
+ // record not found
223
+ // always use "NoSuchKey" in error code
224
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
225
+ err.code = "NoSuchKey";
226
+ callback( err, null );
227
+ }
228
+ }
229
+ else {
230
+ var body = result.value;
231
+ self.logDebug(9, "Binary fetch complete: " + key, '' + body.length + ' bytes');
232
+ callback( null, body );
233
+ }
234
+ } );
235
+ },
236
+
206
237
  getStream: function(key, callback) {
207
238
  // get readable stream to record value given key
208
239
  var self = this;
@@ -477,6 +477,33 @@ module.exports = Class.create({
477
477
  } );
478
478
  },
479
479
 
480
+ getBuffer: function(key, callback) {
481
+ // fetch buffer given key
482
+ var self = this;
483
+ var file = this.getFilePath(key);
484
+
485
+ this.logDebug(9, "Fetching Object: " + key, file);
486
+
487
+ fs.readFile(file, function (err, data) {
488
+ if (err) {
489
+ if (err.message.match(/ENOENT/)) {
490
+ err.message = "File not found";
491
+ err.code = "NoSuchKey";
492
+ }
493
+ else {
494
+ // log fs errors that aren't simple missing files (i.e. I/O errors)
495
+ self.logError('file', "Failed to read file: " + key + ": " + file + ": " + err.message);
496
+ }
497
+
498
+ err.message = "Failed to fetch key: " + key + ": " + err.message;
499
+ return callback( err, null );
500
+ }
501
+
502
+ self.logDebug(9, "Binary fetch complete: " + key, '' + data.length + ' bytes');
503
+ callback( null, data );
504
+ } );
505
+ },
506
+
480
507
  getStream: function(key, callback) {
481
508
  // get readable stream to record value given key
482
509
  var self = this;
package/engines/Hybrid.js CHANGED
@@ -73,6 +73,16 @@ module.exports = Class.create({
73
73
  }
74
74
  },
75
75
 
76
+ getBuffer: function(key, callback) {
77
+ // fetch hybrid buffer given key
78
+ if (this.storage.isBinaryKey(key)) {
79
+ this.binaryEngine.getBuffer( key, callback );
80
+ }
81
+ else {
82
+ this.docEngine.getBuffer( key, callback );
83
+ }
84
+ },
85
+
76
86
  getStream: function(key, callback) {
77
87
  // get readable stream to record value given key
78
88
  // streams are binary only!
package/engines/Redis.js CHANGED
@@ -203,6 +203,36 @@ module.exports = Class.create({
203
203
  } );
204
204
  },
205
205
 
206
+ getBuffer: function(key, callback) {
207
+ // get buffer to record value given key
208
+ var self = this;
209
+ key = this.prepKey(key);
210
+
211
+ this.logDebug(9, "Fetching Redis Object: " + key);
212
+
213
+ this.redis.get( key, function(err, result) {
214
+ if (!result) {
215
+ if (err) {
216
+ // an actual error
217
+ err.message = "Failed to fetch key: " + key + ": " + err;
218
+ self.logError('redis', ''+err);
219
+ callback( err, null );
220
+ }
221
+ else {
222
+ // record not found
223
+ // always use "NoSuchKey" in error code
224
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
225
+ err.code = "NoSuchKey";
226
+ callback( err, null );
227
+ }
228
+ }
229
+ else {
230
+ self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
231
+ callback( null, result );
232
+ }
233
+ } );
234
+ },
235
+
206
236
  getStream: function(key, callback) {
207
237
  // get readable stream to record value given key
208
238
  var self = this;
@@ -220,6 +220,36 @@ module.exports = Class.create({
220
220
  } );
221
221
  },
222
222
 
223
+ getBuffer: function(key, callback) {
224
+ // fetch Redis buffer given key
225
+ var self = this;
226
+ key = this.prepKey(key);
227
+
228
+ this.logDebug(9, "Fetching Redis Object: " + key);
229
+
230
+ this.redis.getBuffer( key, function(err, result) {
231
+ if (!result) {
232
+ if (err) {
233
+ // an actual error
234
+ err.message = "Failed to fetch key: " + key + ": " + err;
235
+ self.logError('redis', ''+err);
236
+ callback( err, null );
237
+ }
238
+ else {
239
+ // record not found
240
+ // always use "NoSuchKey" in error code
241
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
242
+ err.code = "NoSuchKey";
243
+ callback( err, null );
244
+ }
245
+ }
246
+ else {
247
+ self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes');
248
+ callback( null, result );
249
+ }
250
+ } );
251
+ },
252
+
223
253
  getStream: function(key, callback) {
224
254
  // get readable stream to record value given key
225
255
  var self = this;
package/engines/S3.js CHANGED
@@ -355,6 +355,49 @@ module.exports = Class.create({
355
355
  } );
356
356
  },
357
357
 
358
+ getBuffer: function(key, callback) {
359
+ // fetch s3 buffer given key
360
+ var self = this;
361
+ var orig_key = key;
362
+ key = this.prepKey(key);
363
+
364
+ this.logDebug(9, "Fetching S3 Object: " + key);
365
+
366
+ var params = Tools.copyHash( this.s3Params );
367
+ params.Key = this.extKey(key, orig_key);
368
+
369
+ this.s3.send( new S3.GetObjectCommand(params) )
370
+ .then( function(data) {
371
+ // stream to buffer
372
+ streamToBuffer( data.Body, function (err, body) {
373
+ if (err) {
374
+ self.logError('s3', "Failed to fetch key: " + key + ": " + (err.message || err), err);
375
+ return callback(err);
376
+ }
377
+
378
+ self.logDebug(9, "Binary fetch complete: " + key, '' + body.length + ' bytes');
379
+
380
+ callback( null, body, {
381
+ mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
382
+ len: data.ContentLength
383
+ } );
384
+ } ); // streamToBuffer
385
+ } )
386
+ .catch( function(err) {
387
+ if ((err.name == 'NoSuchKey') || (err.name == 'NotFound') || (err.code == 'NoSuchKey') || (err.code == 'NotFound')) {
388
+ // key not found, special case, don't log an error
389
+ // always include "Not found" in error message
390
+ err = new Error("Failed to fetch key: " + key + ": Not found");
391
+ err.code = "NoSuchKey";
392
+ }
393
+ else {
394
+ // some other error
395
+ self.logError('s3', "Failed to fetch key: " + key + ": " + (err.message || err), err);
396
+ }
397
+ process.nextTick( function() { callback( err ); } );
398
+ } );
399
+ },
400
+
358
401
  getStream: function(key, callback) {
359
402
  // get readable stream to record value given key
360
403
  var self = this;
package/engines/SQLite.js CHANGED
@@ -227,6 +227,35 @@ module.exports = Class.create({
227
227
  }); // get
228
228
  },
229
229
 
230
+ getBuffer: function(key, callback) {
231
+ // fetch SQLite buffer given key
232
+ var self = this;
233
+ key = this.prepKey(key);
234
+
235
+ this.logDebug(9, "Fetching SQLite Object: " + key);
236
+
237
+ this.commands.get.get({ $key: key }, function(err, row) {
238
+ if (err) {
239
+ // an actual error
240
+ err.message = "Failed to fetch key: " + key + ": " + err;
241
+ self.logError('sqlite', '' + err);
242
+ callback(err);
243
+ }
244
+ else if (!row) {
245
+ // record not found
246
+ // always use "NoSuchKey" in error code
247
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
248
+ err.code = "NoSuchKey";
249
+ callback( err, null );
250
+ }
251
+ else {
252
+ // success
253
+ self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
254
+ callback( null, row.value );
255
+ }
256
+ }); // get
257
+ },
258
+
230
259
  getStream: function(key, callback) {
231
260
  // get readable stream to record value given key
232
261
  var self = this;
package/hash.js CHANGED
@@ -311,7 +311,7 @@ module.exports = Class.create({
311
311
 
312
312
  if (!(state.hkey in data.items)) {
313
313
  // key not found
314
- var err = new Error("Failed to fetch key: " + state.hkey + ": Not found");
314
+ var err = new Error("Failed to fetch hash key: " + state.path + ": " + state.hkey + ": Not found");
315
315
  err.code = "NoSuchKey";
316
316
  return callback(err);
317
317
  }
@@ -325,7 +325,7 @@ module.exports = Class.create({
325
325
  // fetch multiple hash records at once, given array of keys
326
326
  // callback is provided an array of values in matching order to keys
327
327
  var self = this;
328
- var records = {};
328
+ var records = Object.create(null);
329
329
 
330
330
  async.eachLimit(hkeys, this.concurrency,
331
331
  function(hkey, callback) {
@@ -350,6 +350,82 @@ module.exports = Class.create({
350
350
  );
351
351
  },
352
352
 
353
+ hashUpdate: function(path, hkey, updates, callback) {
354
+ // update existing key/value pair in hash table
355
+ var self = this;
356
+ if (!path) return callback(new Error("Hash path must be a valid string."));
357
+ if (!hkey) return callback(new Error("Hash key must be a valid string."));
358
+ if (!Tools.isaHash(updates)) return callback(new Error("Hash updates must be an object."));
359
+
360
+ this.logDebug(9, "Updating hash key: " + path + ": " + hkey, this.debugLevel(10) ? updates : null);
361
+
362
+ // lock hash for this
363
+ this._hashLock(path, true, function() {
364
+
365
+ // load header, do not create new
366
+ self._hashLoad(path, false, function(err, hash) {
367
+ if (err) {
368
+ self._hashUnlock(path);
369
+ return callback(err);
370
+ }
371
+
372
+ var state = {
373
+ path: path,
374
+ data_path: path + '/data',
375
+ hkey: ''+hkey,
376
+ updates: updates,
377
+ hash: hash,
378
+ index_depth: -1,
379
+ key_digest: Tools.digestHex(hkey, 'md5')
380
+ };
381
+
382
+ self._hashUpdateKey(state, function(err) {
383
+ // done
384
+ self._hashUnlock(path);
385
+ return callback(err);
386
+ }); // _hashPutKey
387
+ }); // load
388
+ }); // lock
389
+ },
390
+
391
+ _hashUpdateKey: function(state, callback) {
392
+ // internal hash update method, store at one hashing level
393
+ // recurse for deeper indexes
394
+ var self = this;
395
+
396
+ self.get(state.data_path, function(err, data) {
397
+ if (err) data = { type: 'hash_page', length: 0, items: {} };
398
+
399
+ if (data.type == 'hash_index') {
400
+ // recurse for deeper level
401
+ state.index_depth++;
402
+ state.data_path += '/' + state.key_digest.substring(state.index_depth, state.index_depth + 1);
403
+ return self._hashUpdateKey(state, callback);
404
+ }
405
+ else {
406
+ // got page, our key should be at this level
407
+ data.items = Tools.copyHashRemoveProto( data.items );
408
+
409
+ if (!(state.hkey in data.items)) {
410
+ // key not found
411
+ var err = new Error("Failed to fetch hash key: " + state.path + ": " + state.hkey + ": Not found");
412
+ err.code = "NoSuchKey";
413
+ return callback(err);
414
+ }
415
+
416
+ var hvalue = data.items[state.hkey];
417
+
418
+ // apply updates directly to forehead
419
+ for (var key in state.updates) {
420
+ Tools.setPath( hvalue, key, state.updates[key] );
421
+ }
422
+
423
+ // save page
424
+ self.put(state.data_path, data, callback);
425
+ } // hash_page
426
+ }); // get
427
+ },
428
+
353
429
  hashEachPage: function(path, iterator, callback) {
354
430
  // call user iterator for each populated hash page, data only
355
431
  // iterator will be passed page items hash object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-storage",
3
- "version": "3.1.12",
3
+ "version": "3.1.13",
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",
package/storage.js CHANGED
@@ -376,6 +376,30 @@ module.exports = Class.create({
376
376
  } );
377
377
  },
378
378
 
379
+ getBuffer: function(key, callback) {
380
+ // fetch buffer given key
381
+ var self = this;
382
+ if (!this.started) return callback( new Error("Storage has not completed startup.") );
383
+
384
+ key = this.normalizeKey( key );
385
+
386
+ // invoke engine and track perf
387
+ var pf = this.perf.begin('get');
388
+
389
+ this.engine.getBuffer( key, function(err, value, info) {
390
+ // get complete
391
+ var elapsed = pf.end();
392
+ if (err) return callback(err);
393
+
394
+ self.logTransaction('get', key, {
395
+ elapsed_ms: elapsed
396
+ });
397
+
398
+ callback(null, value, info);
399
+ if (!err) self.emit('get', key, value, info);
400
+ } );
401
+ },
402
+
379
403
  getStream: function(key, callback) {
380
404
  // fetch value via stream pipe
381
405
  var self = this;
package/test/test-hash.js CHANGED
@@ -38,7 +38,18 @@ module.exports = {
38
38
  var self = this;
39
39
  test.expect(2);
40
40
 
41
- this.storage.hashPut( 'hash1', 'key1', { foo: 'bar', number: 123 }, function(err) {
41
+ this.storage.hashPut( 'hash1', 'key1', { foo: 'bar', number: 122 }, function(err) {
42
+ test.ok( !err, "No error storing into hash: " + err );
43
+ test.ok( Object.keys(self.storage.locks).length == 0, "No more locks leftover in storage" );
44
+ test.done();
45
+ } );
46
+ },
47
+
48
+ function hashUpdate1(test) {
49
+ var self = this;
50
+ test.expect(2);
51
+
52
+ this.storage.hashUpdate( 'hash1', 'key1', { number: 123 }, function(err) {
42
53
  test.ok( !err, "No error storing into hash: " + err );
43
54
  test.ok( Object.keys(self.storage.locks).length == 0, "No more locks leftover in storage" );
44
55
  test.done();
@@ -52,8 +63,8 @@ module.exports = {
52
63
  this.storage.hashGet( 'hash1', 'key1', function(err, item) {
53
64
  test.ok( !err, "No error fetching hash key: " + err );
54
65
  test.ok( !!item, "Item is real" );
55
- test.ok( item.number == 123, "List item number matches" );
56
- test.ok( item.foo == 'bar', "List item value matches" );
66
+ test.ok( item.number == 123, "Hash item number matches" );
67
+ test.ok( item.foo == 'bar', "Hash item value matches" );
57
68
 
58
69
  // check internals
59
70
  self.storage.get( 'hash1', function(err, hash) {
package/test/test-main.js CHANGED
@@ -374,6 +374,33 @@ module.exports = {
374
374
  } );
375
375
  },
376
376
 
377
+ function testBuffer(test) {
378
+ test.expect(8);
379
+ var self = this;
380
+ var key = 'buftest';
381
+ var value = { buf: "test" };
382
+
383
+ this.storage.put( key, value, function(err) {
384
+ test.ok( !err, "No error creating buftest: " + err );
385
+
386
+ self.storage.getBuffer( key, function(err, data) {
387
+ test.ok( !err, "No error fetching binary: " + err );
388
+ test.ok( !!data, "Data is true" );
389
+ test.ok( typeof(data) == 'object', "Data is an object (not a string)" );
390
+ test.ok( data.length > 0, "Data length is non-zero" );
391
+
392
+ var json = JSON.parse( data.toString() );
393
+ test.ok( !!json, "Parsed JSON object from buftest" );
394
+ test.ok( json.buf === "test", "Correct data inside JSON buftest" );
395
+
396
+ self.storage.delete( key, function(err) {
397
+ test.ok( !err, "No error deleting buftest key: " + err );
398
+ test.done();
399
+ } );
400
+ } );
401
+ } );
402
+ },
403
+
377
404
  function testStream(test) {
378
405
  test.expect(14);
379
406
  var self = this;
package/transaction.js CHANGED
@@ -454,6 +454,7 @@ module.exports = Class.create({
454
454
  'listEachPageUpdate',
455
455
  'hashCreate',
456
456
  'hashPut',
457
+ 'hashUpdate',
457
458
  'hashPutMulti',
458
459
  'hashCopy',
459
460
  'hashRename',