diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4387b45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules +coverage +.nyc_output diff --git a/README.md b/README.md index fe439a0..003ba2a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,195 @@ -# node-red-context-pouchdb -A Node-RED Context store plugin backed by PouchDB +# PouchDB Context store plugin + +The PouchDB Context store plugin holds context data in the PouchDB. + +## Install + +1. Run the following command in your Node-RED user directory - typically `~/.node-red` + + npm install git+https://github.com/node-red/node-red-context-pouchdb + +2. Add a configuration in settings.js: + +```javascript +contextStorage: { + pouchdb: { + module: require("node-red-context-pouchdb"), + config: { + // see below options + } + } +} +``` + +## Options + +| Options | Description | +| -------- | ----------------------------------------------------------------------------- | +| name | Specifies the `name` argument for creating the PouchDB databse. You can specify the following.
- SQLite : Database storage file path
- LevelDB: Database storage folder path
- CouchDB: Database URL
`default(SQLite): settings.userDir/context/context.db` | +| options | Specifies the PouchDB database `options`.
Example
- SQLite : {adapter: 'websql'}
- LevelDB: {adapter: 'leveldb'} or {}
- CouchDB: {}
`default(SQLite): {adapter: 'websql'}`| + +Reference: [PouchDB Create a database options](https://pouchdb.com/api.html#create_database) + +## Data Model + +- This plugin uses a PouchDB database for all context scope. +- The NodeSQLite adapter is added to the PouchDB adapter. You can save the data in SQLite3. +- You can also specify saving to a database (LevelDB, CouchDB) that PouchDB supports as standard. +- This plugin saves a JSON object of keys and values in a document for each scope. + - The keys of `global context` will be id with `global` . + - The keys of `flow context` will be id with `` . + - The keys of `node context` will be id with `` . + - Context data is stored in `doc.data` as a document for each scope. + +Structure of data stored in PouchDB: +```json +{ + "total_rows": 3, + "offset": 0, + "rows": [ + { + "id": "2052fca8.312154:a77d79a4.d1a908", + "key": "2052fca8.312154:a77d79a4.d1a908", + "value": { + "rev": "6-55e0513ffba64a8b8efec1ba8e43c90f" + }, + "doc": { + "data": { + "NODE-KEY-1": "NODE-DATA-1", + "NODE-KEY-2": "NODE-DATA-2" + }, + "_id": "2052fca8.312154:a77d79a4.d1a908", + "_rev": "6-55e0513ffba64a8b8efec1ba8e43c90f" + } + }, + { + "id": "a77d79a4.d1a908", + "key": "a77d79a4.d1a908", + "value": { + "rev": "61-2c2a457388db1c3859b79e4bb62e9375" + }, + "doc": { + "data": { + "FLOW-KEY-1": "FLOW-DATA-1", + "FLOW-KEY-2": "FLOW-DATA-2", + }, + "_id": "a77d79a4.d1a908", + "_rev": "61-2c2a457388db1c3859b79e4bb62e9375" + } + }, + { + "id": "global", + "key": "global", + "value": { + "rev": "73-ee260387e51ae20132076ccc83957600" + }, + "doc": { + "data": { + "GLOBAL-KEY-1": "GLOBAL-DATA-1", + "GLOBAL-KEY-2": "GLOBAL-DATA-2", + }, + "_id": "global", + "_rev": "73-ee260387e51ae20132076ccc83957600" + } + } + ] +} +``` + +## Data Structure + +- Data is saved in the JSON object format supported by PouchDB. The plugin does not convert JSON data to a string for storage. + +Code example that references database data : +```javascript +var pd = require('pouchdb'); +pd.plugin(require('pouchdb-adapter-node-websql')); +var db; + +db = new pd( "/home/user/.node-red/context/context.db", { adapter: 'websql' }); +db.allDocs({include_docs: true}, function(err, doc) { + if (err) { + return console.log(err); + } else { + var data = JSON.stringify(doc,null,4); + console.log(data); + } +}); +``` + +## Database replication + +- The data in the context store can be replicated to other DBs using the PouchDB feature. +This allows you to back up the data stored in your local SQLite to a remote CouchDB.This allows you to back up context data stored in your local SQLite to a remote CouchDB. + +- In an environment with multiple context stores, only contexts using the PouchDB plugin will be backed up. + +Code example of replication to remote database(CouchDB) : +```javascript +var pd = require('pouchdb'); +pd.plugin(require('pouchdb-adapter-node-websql')); + +var source = "/home/user/.node-red/context/context.db"; +var target = "http://localhost:5984/couchdb_mycouchdb_1"; + +var db_source = new pd(source, { adapter: 'websql' }); +var db_target = new pd(target); + +db_source.replicate.to(db_target) +.on('complete', function () { + console.log ("Database replicated."); +}).on('error', function (err) { + console.log(err); +}); +``` +Reference: [PouchDB replication](https://pouchdb.com/api.html#replication) + +- Replication can also be filtered.You can also consider replicating a partial database (for example, only the global context part). + +Code example of replication filtering in global context : +```javascript +db_source.replicate.to(db_target, { + filter: function (doc) { + return doc._id === 'global'; +}}) +``` +Reference: [PouchDB filtered replication](https://pouchdb.com/api.html#filtered-replication) + +- You can perform replication from the function node. +To execute the flow, you need to add pouchdb require to the functionGlobalContext in setting.js. + +Setting example of setting.js: +```javascript +functionGlobalContext { + pouchdb: require('pouchdb').plugin(require('pouchdb-adapter-node-websql')) +} +``` +```javascript +contextStorage: { + default: "memoryOnly", + memoryOnly: { + module: 'memory' + }, + pouchdb: { + module: require("node-red-context-pouchdb"), + } +}, +``` +The following is a sample flow that replicates the global context of machine A to machine B using remote database. +For operational safety reasons, do not run the context update flow at the same time. + +Replicate the global context of Node-RED running on machine A to a remote DB (CouchDB). + +Flow example of global context replication to CouchDB: +```json +[{"id":"c054df6e.63e4f","type":"inject","z":"24e3dfb2.5e0d2","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":220,"y":220,"wires":[["fb91e7f9.f74868"]]},{"id":"fb91e7f9.f74868","type":"function","z":"24e3dfb2.5e0d2","name":"Global context replication to CouchDB","func":"var pd = global.get(\"pouchdb\");\n\nvar source = \"/home/user/.node-red/context/context.db\";\nvar target = \"http://couchdb-server:5984/couchdb_mycouchdb_1\";\n\nvar db_source = new pd(source, { adapter: 'websql' });\nvar db_target = new pd(target);\n\ndb_source.replicate.to(db_target,{doc_ids: ['global']})\n.on('complete', function () {\n console.log (\"Database replicated.\");\n}).on('error', function (err) {\n console.log(err);\n});","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":220,"wires":[[]]}] +``` +Set the source and target in the function node according to the settings of the usage environment. + +Replicate the global context saved in the remote DB (CouchDB) to Node-RED running on machine B. + +Flow example of global context replication from CouchDB: +```json +[{"id":"e2aa34a6.f1d6f8","type":"inject","z":"d2880c73.3f51f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":100,"wires":[["bbc87d2e.47404"]]},{"id":"bbc87d2e.47404","type":"function","z":"d2880c73.3f51f","name":"Global context replication from CouchDB","func":"var pd = global.get(\"pouchdb\");\n\nvar source = \"http://couchdb-server:5984/couchdb_mycouchdb_1\";\nvar target = \"/home/user/.node-red/context/context.db\";\n\nvar db_source = new pd(source);\nvar db_target = new pd(target,{adapter: 'websql'});\n\ndb_target.replicate.from(db_source,{doc_ids: ['global']})\n.on('complete', function () {\n console.log (\"Database replicated.\");\n}).on('error', function (err) {\n console.log(err);\n});","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":100,"wires":[[]]}] +``` +Set the source and target in the function node according to the settings of the usage environment. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..a773477 --- /dev/null +++ b/index.js @@ -0,0 +1,367 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +/** + * PouchDB context storage + * + * Configuration options: + * { + * name: "/path/to/storage/context.db" // Specifies the name argument for creating the PouchDB databse. + * // You can specify the following. + * // SQLite : Database storage file path + * // LevelDB: Database storage folder path + * // CouchDB: Database URL + * // default(SQLite): settings.userDir/context.db + * options: {adapter: 'websql'}, // Specifies the PouchDB database options + * // Example + * // SQLite : {adapter: 'websql'} + * // LevelDB: {adapter: 'leveldb'} or {} + * // CouchDB: {} + * // default: {adapter: 'websql'} + * // PouchDB options detail: + * // https://pouchdb.com/api.html#create_database + * } + * + * $HOME/.node-red/context/context.db + */ + +// Require @node-red/util loaded in the Node-RED runtime. +var util = process.env.NODE_RED_HOME ? + require(require.resolve('@node-red/util', { paths: [process.env.NODE_RED_HOME] })).util : + require('@node-red/util').util; +var log = process.env.NODE_RED_HOME ? + require(require.resolve('@node-red/util', { paths: [process.env.NODE_RED_HOME] })).log : + require('@node-red/util').log; + +var fs = require('fs-extra'); +var path = require("path"); +var pd = require('pouchdb'); +pd.plugin(require('pouchdb-adapter-node-websql')); +pd.plugin(require('pouchdb-upsert')); +var db; + +function getDbDir(config) { + var dbDir; + if (!config.name) { + if(config.settings && config.settings.userDir){ + dbDir = path.join(config.settings.userDir, "context"); + }else{ + try { + fs.statSync(path.join(process.env.NODE_RED_HOME,".config.json")); + dbDir = path.join(process.env.NODE_RED_HOME, "context"); + } catch(err) { + try { + // Consider compatibility for older versions + if (process.env.HOMEPATH) { + fs.statSync(path.join(process.env.HOMEPATH,".node-red",".config.json")); + dbDir = path.join(process.env.HOMEPATH, ".node-red", "context"); + } + } catch(err) { + } + if (!dbDir) { + dbDir = path.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME,".node-red", "context"); + } + } + } + } else { + dbDir = path.dirname(config.name); + } + return dbDir; +} + +function getDbOptions(config) { + var dbOptions; + if (config.options) { + dbOptions = config.options; + } else { + dbOptions = { adapter: 'websql' }; + } + return dbOptions; +} + +function getDbBase(config) { + var dbBase; + if (config.name) { + dbBase = path.basename(config.name); + } else { + dbBase = "context.db"; + } + return dbBase; +} + +function getDbURL(config) { + var dbURL; + if (config.name && (config.name.startsWith("http://") || config.name.startsWith("https://"))) { + dbURL = config.name; + } else { + dbURL = null; + } + return dbURL; +} + +function updateDocData(doc, key, value) { + for (var i=0; i { + db.get(scope).then(function (doc) { + db.remove(scope, doc._rev).then(function (res) { + resolve(); + }).catch(function (err){ + // Failed to delete context data + reject(err); + }); + }).catch(function (err) { + if (err.status === 404) { + resolve(); + } else { + reject(err); + } + }); + }); +}; + +PouchDB.prototype.clean = function (_activeNodes) { + return new Promise((resolve, reject) => { + db.allDocs({include_docs: true}).then(function(docs) { + var res = docs.rows; + res = res.filter(doc => !doc.id.startsWith("global")) + _activeNodes.forEach(key => { + res = res.filter(doc => !doc.id.startsWith(key)) + }); + var promises = []; + res.forEach(function(doc) { + var removePromise = db.get(doc.id).then(function(data) { + db.remove(doc.id, data._rev).then(function(res) { + resolve(); + }).catch(function (err) { + if (err.status === 409) { + // Already deleted. conflict status= 409 + resolve(); + } else { + reject(err); + } + }); + }).catch(function (err) { + // Failed to get context data + reject(err); + }); + if(removePromise) { + promises.push(removePromise); + } + }); + if (promises.length != 0) { + return Promise.all(promises); + } else { + resolve(); + } + }).catch(function (err) { + // Failed to get all context data + reject(err); + }); + }); +} + +module.exports = function (config) { + return new PouchDB(config); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..302a567 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "node-red-context-pouchdb", + "version": "0.0.1", + "description": "A Node-RED Context store plugin backed by PouchDB", + "main": "index.js", + "scripts": { + "test": "nyc --cache mocha ./test/_spec.js --timeout=8000", + "coverage": "nyc report --reporter=lcov --reporter=html" + }, + "repository": { + "type": "git", + "url": "https://github.com/node-red/node-red-context-pouchdb.git" + }, + "license": "Apache-2.0", + "dependencies": { + "@node-red/util": "1.2.9", + "fs-extra": "8.1.0", + "pouchdb": "^7.2.2", + "pouchdb-adapter-node-websql": "^7.0.0", + "pouchdb-upsert": "^2.2.0" + }, + "devDependencies": { + "mocha": "^5.2.0", + "nyc": "^10.0.0", + "should": "^13.2.1", + "should-sinon": "0.0.6", + "sinon": "^7.2.2" + }, + "keywords": [ + "node-red", + "pouchdb", + "sqlite" + ], + "engines": { + "node": ">=8" + } +} diff --git a/test/_spec.js b/test/_spec.js new file mode 100644 index 0000000..3d7640c --- /dev/null +++ b/test/_spec.js @@ -0,0 +1,590 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var should = require('should'); +const pouchdbPlugin = require('../index.js'); + +describe('pouchdb', function () { + before(function () { + const self = this; + const context = pouchdbPlugin({}); + return context.open().then(function(){ + return context.close(); + }).catch(() => { + console.log('Can not connect to pouchdb, All tests will be skipped!'); + self.test.parent.pending = true; + self.skip(); + }); + }); + + describe('#open', function () { + it('should load configs sqlite', function () { + const context1 = pouchdbPlugin({ name: "/tmp/pouchdb/context.db" }); + return context1.open().then(function () { + context1.should.have.properties( + { config: { name: '/tmp/pouchdb/context.db' }, + dbURL: null, + dbDir: '/tmp/pouchdb', + dbBase: 'context.db', + dbOptions: { adapter: 'websql', deterministic_revs: true } + }); + return context1.close(); + }); + }); + + it('should load configs leveldb', function () { + const context2 = pouchdbPlugin({ name: "/tmp/leveldb/db", options: { adapter: 'leveldb' }}); + return context2.open().then(function () { + context2.should.have.properties( + { config: { name: '/tmp/leveldb/db', options: { adapter: 'leveldb', deterministic_revs: true}}, + dbURL: null, + dbDir: '/tmp/leveldb', + dbBase: 'db', + dbOptions: { adapter: 'leveldb', deterministic_revs: true } + }); + return context2.close(); + }); + }); + + it('should load configs couchdb', function () { + const context3 = pouchdbPlugin({ name: "http://localhost:5984/couchdb_mycouchdb_1", options: {}}); + return context3.open().then(function () { + context3.should.have.properties( + { config: { name: 'http://localhost:5984/couchdb_mycouchdb_1', options: {deterministic_revs: true}}, + dbURL: 'http://localhost:5984/couchdb_mycouchdb_1', + dbDir: null, + dbBase: null, + dbOptions: {deterministic_revs: true} + }); + return context3.close(); + }); + }); + }); + + describe('#get/set', function () { + const context = pouchdbPlugin({}); + + beforeEach(function () { + return context.open(); + }); + afterEach(function () { + return context.clean([]).then(function(){ + return context.close(); + }); + }); + + it('should store property',function(done) { + context.get("nodeX","foo",function(err, value){ + if (err) { return done(err); } + should.not.exist(value); + context.set("nodeX","foo","test",function(err){ + if (err) { return done(err); } + context.get("nodeX","foo",function(err, value){ + if (err) { return done(err); } + value.should.be.equal("test"); + done(); + }); + }); + }); + }); + + it('should store property - creates parent properties',function(done) { + context.set("nodeX","foo.bar","test",function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.eql({bar:"test"}); + done(); + }); + }); + }); + + it('should store local scope property', function (done) { + context.set("abc:def", "foo.bar", "test", function (err) { + context.get("abc:def", "foo", function (err, value) { + value.should.be.eql({ bar: "test" }); + done(); + }); + }); + }); + + it('should delete property',function(done) { + context.set("nodeX","foo.abc.bar1","test1",function(err){ + context.set("nodeX","foo.abc.bar2","test2",function(err){ + context.get("nodeX","foo.abc",function(err, value){ + value.should.be.eql({bar1:"test1",bar2:"test2"}); + context.set("nodeX","foo.abc.bar1",undefined,function(err){ + context.get("nodeX","foo.abc",function(err, value){ + value.should.be.eql({bar2:"test2"}); + context.set("nodeX","foo.abc",undefined,function(err){ + context.get("nodeX","foo.abc",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",undefined,function(err){ + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should not shared context with other scope', function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.get("nodeY","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo","testX",function(err){ + context.set("nodeY","foo","testY",function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.equal("testX"); + context.get("nodeY","foo",function(err, value){ + value.should.be.equal("testY"); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should store string',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo","bar",function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.String(); + value.should.be.equal("bar"); + context.set("nodeX","foo","1",function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.String(); + value.should.be.equal("1"); + done(); + }); + }); + }); + }); + }); + }); + + it('should store number',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",1,function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Number(); + value.should.be.equal(1); + done(); + }); + }); + }); + }); + + it('should store null',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",null,function(err){ + context.get("nodeX","foo",function(err, value){ + should(value).be.null(); + done(); + }); + }); + }); + }); + + it('should store boolean',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",true,function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Boolean().and.true(); + context.set("nodeX","foo",false,function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Boolean().and.false(); + done(); + }); + }); + }); + }); + }); + }); + + it('should store object',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",{obj:"bar"},function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Object(); + value.should.eql({obj:"bar"}); + done(); + }); + }); + }); + }); + + it('should store array',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",["a","b","c"],function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Array(); + value.should.eql(["a","b","c"]); + context.get("nodeX","foo[1]",function(err, value){ + value.should.be.String(); + value.should.equal("b"); + done(); + }); + }); + }); + }); + }); + + it('should store array of arrays',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",[["a","b","c"],[1,2,3,4],[true,false]],function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Array(); + value.should.have.length(3); + value[0].should.have.length(3); + value[1].should.have.length(4); + value[2].should.have.length(2); + context.get("nodeX","foo[1]",function(err, value){ + value.should.be.Array(); + value.should.have.length(4); + value.should.be.eql([1,2,3,4]); + done(); + }); + }); + }); + }); + }); + + it('should store array of objects',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo",[{obj:"bar1"},{obj:"bar2"},{obj:"bar3"}],function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.Array(); + value.should.have.length(3); + value[0].should.be.Object(); + value[1].should.be.Object(); + value[2].should.be.Object(); + context.get("nodeX","foo[1]",function(err, value){ + value.should.be.Object(); + value.should.be.eql({obj:"bar2"}); + done(); + }); + }); + }); + }); + }); + + it('should set/get multiple values', function(done) { + context.set("nodeX",["one","two","three"],["test1","test2","test3"], function(err) { + context.get("nodeX",["one","two"], function() { + Array.prototype.slice.apply(arguments).should.eql([undefined,"test1","test2"]) + done(); + }); + }); + }) + it('should set/get multiple values - get unknown', function(done) { + context.set("nodeX",["one","two","three"],["test1","test2","test3"], function(err) { + context.get("nodeX",["one","two","unknown"], function() { + Array.prototype.slice.apply(arguments).should.eql([undefined,"test1","test2",undefined]) + done(); + }); + }); + }) + it('should set/get multiple values - single value providd', function(done) { + context.set("nodeX",["one","two","three"],"test1", function(err) { + context.get("nodeX",["one","two"], function() { + Array.prototype.slice.apply(arguments).should.eql([undefined,"test1",null]) + done(); + }); + }); + }) + + it('should throw error if bad key included in multiple keys - get', function(done) { + context.set("nodeX",["one","two","three"],["test1","test2","test3"], function(err) { + context.get("nodeX",["one",".foo","three"], function(err) { + should.exist(err); + done(); + }); + }); + }) + + it('should throw error if bad key included in multiple keys - set', function(done) { + context.set("nodeX",["one",".foo","three"],["test1","test2","test3"], function(err) { + should.exist(err); + // Check 'one' didn't get set as a result + context.get("nodeX","one",function(err,one) { + should.not.exist(one); + done(); + }) + }); + }) + + it('should throw an error when getting a value with invalid key', function (done) { + context.set("nodeX","foo","bar",function(err) { + context.get("nodeX"," ",function(err,value) { + should.exist(err); + done(); + }); + }); + }); + + it('should throw an error when setting a value with invalid key',function (done) { + context.set("nodeX"," ","bar",function (err) { + should.exist(err); + done(); + }); + }); + + it('should throw an error when callback of get() is not a function',function (done) { + try { + context.get("nodeX","foo","callback"); + done("should throw an error."); + } catch (err) { + done(); + } + }); + + it('should throw an error when callback of get() is not specified',function (done) { + try { + context.get("nodeX","foo"); + done("should throw an error."); + } catch (err) { + done(); + } + }); + + it('should throw an error when callback of set() is not a function',function (done) { + try { + context.set("nodeX","foo","bar","callback"); + done("should throw an error."); + } catch (err) { + done(); + } + }); + + it('should not throw an error when callback of set() is not specified', function (done) { + try { + context.set("nodeX"," ","bar"); + done(); + } catch (err) { + done("should not throw an error."); + } + }); + + }); + + describe('#keys', function () { + const context = pouchdbPlugin({}); + + beforeEach(function () { + return context.open(); + }); + afterEach(function () { + return context.clean([]).then(function(){ + return context.close(); + }); + }); + + it('should enumerate context keys', function(done) { + context.keys("nodeX",function(err, value){ + value.should.be.an.Array(); + value.should.be.empty(); + context.set("nodeX","foo","bar",function(err){ + context.keys("nodeX",function(err, value){ + value.should.have.length(1); + value[0].should.equal("foo"); + context.set("nodeX","abc.def","bar",function(err){ + context.keys("nodeX",function(err, value){ + value.should.have.length(2); + value[1].should.equal("abc"); + done(); + }); + }); + }); + }); + }); + }); + + it('should enumerate context keys in each scopes', function(done) { + context.keys("nodeX",function(err, value){ + value.should.be.an.Array(); + value.should.be.empty(); + context.keys("nodeY",function(err, value){ + value.should.be.an.Array(); + value.should.be.empty(); + context.set("nodeX","foo","bar",function(err){ + context.set("nodeY","hoge","piyo",function(err){ + context.keys("nodeX",function(err, value){ + value.should.have.length(1); + value[0].should.equal("foo"); + context.keys("nodeY",function(err, value){ + value.should.have.length(1); + value[0].should.equal("hoge"); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should throw an error when callback of keys() is not a function', function (done) { + try { + context.keys("nodeX", "callback"); + done("should throw an error."); + } catch (err) { + done(); + } + }); + + it('should throw an error when callback of keys() is not specified', function (done) { + try { + context.keys("nodeX"); + done("should throw an error."); + } catch (err) { + done(); + } + }); + }); + + describe('#delete', function () { + const context = pouchdbPlugin({}); + + beforeEach(function () { + return context.open(); + }); + afterEach(function () { + return context.clean([]).then(function(){ + return context.close(); + }); + }); + + it('should delete context',function(done) { + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.get("nodeY","foo",function(err, value){ + should.not.exist(value); + context.set("nodeX","foo","testX",function(err){ + context.set("nodeY","foo","testY",function(err){ + context.get("nodeX","foo",function(err, value){ + value.should.be.equal("testX"); + context.get("nodeY","foo",function(err, value){ + value.should.be.equal("testY"); + context.delete("nodeX").then(function(){ + context.get("nodeX","foo",function(err, value){ + should.not.exist(value); + context.get("nodeY","foo",function(err, value){ + value.should.be.equal("testY"); + done(); + }); + }); + }).catch(done); + }); + }); + }); + }); + }); + }); + }); + }); + + describe('#clean', function () { + const context = pouchdbPlugin({}); + function pouchdbGet(scope, key) { + return new Promise((res, rej) => { + context.get(scope, key, function (err, value) { + if (err) { + rej(err); + } else { + res(value); + } + }); + }); + } + function pouchdbSet(scope, key, value) { + return new Promise((res, rej) => { + context.set(scope, key, value, function (err) { + if (err) { + rej(err); + } else { + res(); + } + }); + }); + } + beforeEach(function () { + return context.open(); + }); + afterEach(function () { + return context.clean([]).then(function(){ + return context.close(); + }); + }); + + it('should not clean active context', function () { + return pouchdbSet("global", "foo", "testGlobal").then(function () { + return pouchdbSet("nodeX:flow1", "foo", "testX"); + }).then(function () { + return pouchdbSet("nodeY:flow2", "foo", "testY"); + }).then(function () { + return pouchdbGet("nodeX:flow1", "foo").should.be.fulfilledWith("testX"); + }).then(function () { + return pouchdbGet("nodeY:flow2", "foo").should.be.fulfilledWith("testY"); + }).then(function () { + return context.clean(["nodeX:flow1"]); + }).then(function () { + return pouchdbGet("nodeX:flow1", "foo").should.be.fulfilledWith("testX"); + }).then(function () { + return pouchdbGet("nodeY:flow2", "foo").should.be.fulfilledWith(undefined); + }).then(function () { + return pouchdbGet("global", "foo").should.be.fulfilledWith("testGlobal"); + }); + }); + + + it('should clean unnecessary context', function () { + return pouchdbSet("global", "foo", "testGlobal").then(function () { + return pouchdbSet("nodeX:flow1", "foo", "testX"); + }).then(function () { + return pouchdbSet("nodeY:flow2", "foo", "testY"); + }).then(function () { + return pouchdbGet("nodeX:flow1", "foo").should.be.fulfilledWith("testX"); + }).then(function () { + return pouchdbGet("nodeY:flow2", "foo").should.be.fulfilledWith("testY"); + }).then(function () { + return context.clean([]); + }).then(function () { + return pouchdbGet("nodeX:flow1", "foo").should.be.fulfilledWith(undefined); + }).then(function () { + return pouchdbGet("nodeY:flow2", "foo").should.be.fulfilledWith(undefined); + }).then(function () { + return pouchdbGet("global", "foo").should.be.fulfilledWith("testGlobal"); + }); + }); + + }); +});