pixl-server-storage 3.1.12 → 3.1.14

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
@@ -17,6 +17,7 @@ var storage = server.Storage;
17
17
  * [putStream](#putstream)
18
18
  * [get](#get)
19
19
  * [getMulti](#getmulti)
20
+ * [getBuffer](#getbuffer)
20
21
  * [getStream](#getstream)
21
22
  * [getStreamRange](#getstreamrange)
22
23
  * [head](#head)
@@ -60,6 +61,8 @@ var storage = server.Storage;
60
61
  * [hashPutMulti](#hashputmulti)
61
62
  * [hashGet](#hashget)
62
63
  * [hashGetMulti](#hashgetmulti)
64
+ * [hashUpdate](#hashupdate)
65
+ * [hashUpdateMulti](#hashupdatemulti)
63
66
  * [hashEach](#hasheach)
64
67
  * [hashEachSync](#hasheachsync)
65
68
  * [hashEachPage](#hasheachpage)
@@ -180,13 +183,27 @@ storage.getMulti( ['test1', 'test2', 'test3'], function(err, values) {
180
183
 
181
184
  Note that if *any* of the records fail, the entire operation fails, and the first error is passed to your callback.
182
185
 
186
+ ## getBuffer
187
+
188
+ ```javascript
189
+ storage.getBuffer( KEY, CALLBACK );
190
+ ```
191
+
192
+ 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:
193
+
194
+ ```javascript
195
+ storage.getBuffer( 'test1', function(err, buf) {
196
+ if (err) throw err;
197
+ } );
198
+ ```
199
+
183
200
  ## getStream
184
201
 
185
202
  ```javascript
186
203
  storage.getStream( KEY, CALLBACK );
187
204
  ```
188
205
 
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:
206
+ 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
207
 
191
208
  ```javascript
192
209
  var fs = require('fs');
@@ -982,6 +999,44 @@ storage.hashGetMulti( 'users', ['bsanders', 'hclinton', 'dtrump'], function(err,
982
999
  } );
983
1000
  ```
984
1001
 
1002
+ ## hashUpdate
1003
+
1004
+ ```javascript
1005
+ storage.hashUpdate( PATH, KEY, UPDATES, CALLBACK );
1006
+ ```
1007
+
1008
+ 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:
1009
+
1010
+ ```javascript
1011
+ storage.hashUpdate( 'users', 'bsanders', { age: 81 }, function(err) {
1012
+ if (err) throw err;
1013
+ } );
1014
+ ```
1015
+
1016
+ This would update Bernie's age to 81 without affecting any of the other properties in his user record.
1017
+
1018
+ ## hashUpdateMulti
1019
+
1020
+ ```javascript
1021
+ storage.hashUpdateMulti( PATH, RECORDS, CALLBACK );
1022
+ ```
1023
+
1024
+ The `hashUpdateMulti()` method updates multiple key/value pairs in a hash. The `PATH` specifies the main storage path of the hash, and `RECORDS` should be an object containing all the keys and values you want to update. See [hashUpdate()](#hashupdate) for details on the update format.
1025
+
1026
+ ```js
1027
+ var updates = {
1028
+ "bsanders": { age: 81 },
1029
+ "hclinton": { age: 75 },
1030
+ "dtrump": { age: 77 }
1031
+ };
1032
+
1033
+ storage.hashUpdateMulti( 'users', records, function(err) {
1034
+ if (err) throw err;
1035
+ } );
1036
+ ```
1037
+
1038
+ This would update the `age` properties in each record, without affecting the other data.
1039
+
985
1040
  ## hashEach
986
1041
 
987
1042
  ```javascript
@@ -200,6 +200,8 @@ When transactions are enabled, many compound storage methods (those that execute
200
200
  - [hashCreate()](API.md#hashcreate)
201
201
  - [hashPut()](API.md#hashput)
202
202
  - [hashPutMulti()](API.md#hashputmulti)
203
+ - [hashUpdate()](API.md#hashupdate)
204
+ - [hashUpdateMulti()](API.md#hashupdatemulti)
203
205
  - [hashCopy()](API.md#hashcopy)
204
206
  - [hashRename()](API.md#hashrename)
205
207
  - [hashDeleteMulti()](API.md#hashdeletemulti)
@@ -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,101 @@ 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
+
429
+ hashUpdateMulti: function(path, records, callback) {
430
+ // update multiple hash records at once, given object of keys and values
431
+ // need concurrency limit of 1 because hashUpdate locks
432
+ var self = this;
433
+
434
+ async.eachLimit(Object.keys(records), 1,
435
+ function(hkey, callback) {
436
+ // iterator for each key
437
+ self.hashUpdate(path, hkey, records[hkey], function(err) {
438
+ callback(err);
439
+ } );
440
+ },
441
+ function(err) {
442
+ // all keys updated
443
+ callback(err);
444
+ }
445
+ );
446
+ },
447
+
353
448
  hashEachPage: function(path, iterator, callback) {
354
449
  // call user iterator for each populated hash page, data only
355
450
  // 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.14",
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,7 +454,9 @@ module.exports = Class.create({
454
454
  'listEachPageUpdate',
455
455
  'hashCreate',
456
456
  'hashPut',
457
- 'hashPutMulti',
457
+ 'hashPutMulti',
458
+ 'hashUpdate',
459
+ 'hashUpdateMulti',
458
460
  'hashCopy',
459
461
  'hashRename',
460
462
  'hashDeleteMulti',