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");
+ });
+ });
+
+ });
+});