-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The Datastore class provides a persistent, database-backed key-value data storage class. This complements the existing Brain class in a few ways: 1. Each get/set operation is directly backed by the backing database, allowing multiple Hubot instances to share cooperative access to data simultaneously; 2. get/set operations are asynchronous, mapping well to the async access methods used by many database adapters.
- Loading branch information
1 parent
a3d80c1
commit a494d0e
Showing
7 changed files
with
273 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
'use strict' | ||
|
||
class DataStore { | ||
// Represents a persistent, database-backed storage for the robot. Extend this. | ||
// | ||
// Returns a new Datastore with no storage. | ||
constructor (robot) { | ||
this.robot = robot | ||
} | ||
|
||
// Public: Set value for key in the database. Overwrites existing | ||
// values if present. Returns a promise which resolves when the | ||
// write has completed. | ||
// | ||
// Value can be any JSON-serializable type. | ||
set (key, value) { | ||
return this._set(key, value, 'global') | ||
} | ||
|
||
// Public: Assuming `key` represents an object in the database, | ||
// sets its `objectKey` to `value`. If `key` isn't already | ||
// present, it's instantiated as an empty object. | ||
setObject (key, objectKey, value) { | ||
return this.get(key).then((object) => { | ||
let target = object || {} | ||
target[objectKey] = value | ||
return this.set(key, target) | ||
}) | ||
} | ||
|
||
// Public: Adds the supplied value(s) to the end of the existing | ||
// array in the database marked by `key`. If `key` isn't already | ||
// present, it's instantiated as an empty array. | ||
setArray (key, value) { | ||
return this.get(key).then((object) => { | ||
let target = object || [] | ||
// Extend the array if the value is also an array, otherwise | ||
// push the single value on the end. | ||
if (Array.isArray(value)) { | ||
return this.set(key, target.push.apply(target, value)) | ||
} else { | ||
return this.set(key, target.concat(value)) | ||
} | ||
}) | ||
} | ||
|
||
// Public: Get value by key if in the database or return `undefined` | ||
// if not found. Returns a promise which resolves to the | ||
// requested value. | ||
get (key) { | ||
return this._get(key, 'global') | ||
} | ||
|
||
// Public: Digs inside the object at `key` for a key named | ||
// `objectKey`. If `key` isn't already present, or if it doesn't | ||
// contain an `objectKey`, returns `undefined`. | ||
getObject (key, objectKey) { | ||
return this.get(key).then((object) => { | ||
let target = object || {} | ||
return target[objectKey] | ||
}) | ||
} | ||
|
||
// Private: Implements the underlying `set` logic for the datastore. | ||
// This will be called by the public methods. This is one of two | ||
// methods that must be implemented by subclasses of this class. | ||
// `table` represents a unique namespace for this key, such as a | ||
// table in a SQL database. | ||
// | ||
// This returns a resolved promise when the `set` operation is | ||
// successful, and a rejected promise if the operation fails. | ||
_set (key, value, table) { | ||
return Promise.reject(new DataStoreUnavailable('Setter called on the abstract class.')) | ||
} | ||
|
||
// Private: Implements the underlying `get` logic for the datastore. | ||
// This will be called by the public methods. This is one of two | ||
// methods that must be implemented by subclasses of this class. | ||
// `table` represents a unique namespace for this key, such as a | ||
// table in a SQL database. | ||
// | ||
// This returns a resolved promise containing the fetched value on | ||
// success, and a rejected promise if the operation fails. | ||
_get (key, table) { | ||
return Promise.reject(new DataStoreUnavailable('Getter called on the abstract class.')) | ||
} | ||
} | ||
|
||
class DataStoreUnavailable extends Error {} | ||
|
||
module.exports = { | ||
DataStore, | ||
DataStoreUnavailable | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
'use strict' | ||
|
||
const DataStore = require('../datastore').DataStore | ||
|
||
class InMemoryDataStore extends DataStore { | ||
constructor (robot) { | ||
super(robot) | ||
this.data = { | ||
global: {}, | ||
users: {} | ||
} | ||
} | ||
|
||
_get (key, table) { | ||
return Promise.resolve(this.data[table][key]) | ||
} | ||
|
||
_set (key, value, table) { | ||
return Promise.resolve(this.data[table][key] = value) | ||
} | ||
} | ||
|
||
module.exports = InMemoryDataStore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
'use strict' | ||
|
||
/* global describe, beforeEach, it */ | ||
|
||
const chai = require('chai') | ||
const sinon = require('sinon') | ||
chai.use(require('sinon-chai')) | ||
|
||
const expect = chai.expect | ||
|
||
const Brain = require('../src/brain') | ||
const InMemoryDataStore = require('../src/datastores/memory') | ||
|
||
describe('Datastore', function () { | ||
beforeEach(function () { | ||
this.clock = sinon.useFakeTimers() | ||
this.robot = { | ||
emit () {}, | ||
on () {}, | ||
receive: sinon.spy() | ||
} | ||
|
||
// This *should* be callsArgAsync to match the 'on' API, but that makes | ||
// the tests more complicated and seems irrelevant. | ||
sinon.stub(this.robot, 'on').withArgs('running').callsArg(1) | ||
|
||
this.robot.brain = new Brain(this.robot) | ||
this.robot.datastore = new InMemoryDataStore(this.robot) | ||
this.robot.brain.userForId('1', {name: 'User One'}) | ||
this.robot.brain.userForId('2', {name: 'User Two'}) | ||
}) | ||
|
||
describe('global scope', function () { | ||
it('returns undefined for values not in the datastore', function () { | ||
return this.robot.datastore.get('blah').then(function (value) { | ||
expect(value).to.be.an('undefined') | ||
}) | ||
}) | ||
|
||
it('can store simple values', function () { | ||
return this.robot.datastore.set('key', 'value').then(() => { | ||
return this.robot.datastore.get('key').then((value) => { | ||
expect(value).to.equal('value') | ||
}) | ||
}) | ||
}) | ||
|
||
it('can store arbitrary JavaScript values', function () { | ||
let object = { | ||
'name': 'test', | ||
'data': [1, 2, 3] | ||
} | ||
return this.robot.datastore.set('key', object).then(() => { | ||
return this.robot.datastore.get('key').then((value) => { | ||
expect(value.name).to.equal('test') | ||
expect(value.data).to.deep.equal([1, 2, 3]) | ||
}) | ||
}) | ||
}) | ||
|
||
it('can dig inside objects for values', function () { | ||
let object = { | ||
'a': 'one', | ||
'b': 'two' | ||
} | ||
return this.robot.datastore.set('key', object).then(() => { | ||
return this.robot.datastore.getObject('key', 'a').then((value) => { | ||
expect(value).to.equal('one') | ||
}) | ||
}) | ||
}) | ||
|
||
it('can set individual keys inside objects', function () { | ||
let object = { | ||
'a': 'one', | ||
'b': 'two' | ||
} | ||
return this.robot.datastore.set('object', object).then(() => { | ||
return this.robot.datastore.setObject('object', 'c', 'three').then(() => { | ||
return this.robot.datastore.get('object').then((value) => { | ||
expect(value.a).to.equal('one') | ||
expect(value.b).to.equal('two') | ||
expect(value.c).to.equal('three') | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
it('creates an object from scratch when none exists', function () { | ||
return this.robot.datastore.setObject('object', 'key', 'value').then(() => { | ||
return this.robot.datastore.get('object').then((value) => { | ||
let expected = {'key': 'value'} | ||
expect(value).to.deep.equal(expected) | ||
}) | ||
}) | ||
}) | ||
|
||
it('can append to an existing array', function () { | ||
return this.robot.datastore.set('array', [1, 2, 3]).then(() => { | ||
return this.robot.datastore.setArray('array', 4).then(() => { | ||
return this.robot.datastore.get('array').then((value) => { | ||
expect(value).to.deep.equal([1, 2, 3, 4]) | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
it('creates an array from scratch when none exists', function () { | ||
return this.robot.datastore.setArray('array', 4).then(() => { | ||
return this.robot.datastore.get('array').then((value) => { | ||
expect(value).to.deep.equal([4]) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |