├── .gitignore ├── package.json ├── Readme.md ├── LICENSE ├── index.js └── test └── _spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-context-redis", 3 | "version": "0.0.1", 4 | "description": "A Node-RED Context store plugin backed by Redis", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "nyc --cache mocha ./test/_spec.js --timeout=8000", 8 | "coverage": "nyc report --reporter=lcov --reporter=html" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/node-red/node-red-context-redis.git" 13 | }, 14 | "license": "Apache-2.0", 15 | "dependencies": { 16 | "@node-red/util": "0.20.0-beta.2", 17 | "json-stringify-safe": "^5.0.1", 18 | "redis": "^2.8.0" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^5.2.0", 22 | "nyc": "^10.0.0", 23 | "should": "^13.2.1", 24 | "should-sinon": "0.0.6", 25 | "sinon": "^7.2.2" 26 | }, 27 | "keywords": [ 28 | "node-red", 29 | "redis" 30 | ], 31 | "engines": { 32 | "node": ">=8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ***This is a work in progress.*** 2 | 3 | # Redis plugin 4 | 5 | The Redis plugin holds context data in the Redis. 6 | 7 | ## Pre-requisite 8 | 9 | To run this you need a Redis server running. For details see the Redis site. 10 | 11 | ## Install 12 | 13 | 1. Run the following command in your Node-RED user directory - typically `~/.node-red` 14 | 15 | npm install git+https://github.com/node-red/node-red-context-redis 16 | 17 | 1. Add a configuration in settings.js: 18 | 19 | ```javascript 20 | contextStorage: { 21 | redis: { 22 | module: require("node-red-context-redis"), 23 | config: { 24 | // see below options 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | ### Options 31 | 32 | This plugin exposes some options defined in [node_redis](https://github.com/NodeRedis/node_redis) as itself options. 33 | It needs following configuration options: 34 | 35 | | Options | Description | 36 | | -------------- | ----------------------------------------------------------------------------------------------------------- | 37 | | host | The IP address of the Redis server. `Default: "127.0.0.1"` | 38 | | port | The port of the Redis server. `Default: 6379` | 39 | | db | The Redis logical database to connect. `Default: 0` | 40 | | prefix | If set, the string used to prefix all used keys. | 41 | | password | If set, the plugin will run Redis AUTH command on connect. *Note: the password will be sent as plaintext.* | 42 | | tls | An object containing options to pass to tls.connect to set up a TLS connection to the server. | 43 | | retry_strategy | Specifies a function to reconnect if the connection to Redis is lost. | 44 | | | `default: undefined (Use the default retry strategy)` | 45 | 46 | see https://github.com/NodeRedis/node_redis#options-object-properties 47 | 48 | ## Data Model 49 | 50 | ```text 51 | Node-RED Redis 52 | +-------------------+ +-------------------------------+ 53 | | global context | | logical database | 54 | | +---------------+ | | +---------------------------+ | 55 | | | +-----+-----+ | | | | +-----------------+-----+ | | 56 | | | | key |value| | | <-----> | | | global:key |value| | | 57 | | | +-----+-----+ | | | | +-----------------+-----+ | | 58 | | +---------------+ | | | | | 59 | | | | | | | 60 | | flow context | | | | | 61 | | +---------------+ | | | | | 62 | | | +-----+-----+ | | | | +-----------------+-----+ | | 63 | | | | key |value| | | <-----> | | | :key |value| | | 64 | | | +-----+-----+ | | | | +-----------------+-----+ | | 65 | | +---------------+ | | | | | 66 | | | | | | | 67 | | node context | | | | | 68 | | +---------------+ | | | | | 69 | | | +-----+-----+ | | | | +-----------------+-----+ | | 70 | | | | key |value| | | <-----> | | | :key |value| | | 71 | | | +-----+-----+ | | | | +-----------------+-----+ | | 72 | | +---------------+ | | +---------------------------+ | 73 | +-------------------+ +-------------------------------+ 74 | ``` 75 | 76 | - This plugin uses a Redis logical database for all context scope. 77 | - This plugin prefixes all used keys with context scope in order to identify the scope of the key. 78 | - The keys of `global context` will be prefixed with `global:` . 79 | e.g. Set `"foo"` to hold `"bar"` in the global context -> Set `"global:foo"` to hold `"bar"` in the Redis logical database. 80 | - The keys of `flow context` will be prefixed with `:` . 81 | e.g. Set `"foo"` to hold `"bar"` in the flow context whose id is `8588e4b8.784b38` -> Set `"8588e4b8.784b38:foo"` to hold `"bar"` in the Redis. 82 | - The keys of `node context` will be prefixed with `:` . 83 | e.g. Set `"foo"` to hold `"bar"` in the node context whose id is `80d8039e.2b82:8588e4b8.784b38` -> Set `"80d8039e.2b82:8588e4b8.784b38:foo"` to hold `"bar"` in the Redis. 84 | 85 | ## Data Structure 86 | 87 | - This plugin converts a value of context to JSON and stores it as string type to the Redis. 88 | - After getting a value from the Redis, the plugin also converts the value to an object or a primitive value. 89 | 90 | ```text 91 | Node-RED Redis 92 | +------------------------------+ +---------------------------------------------+ 93 | | global context | | logical database | 94 | | +--------------------------+ | | +-----------------------------------------+ | 95 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 96 | | | | str | "foo" | | | <-----> | | | global:str | "\"foo\"" | | | 97 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 98 | | | | num | 1 | | | <-----> | | | global:num | "1" | | | 99 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 100 | | | | nstr | "10" | | | <-----> | | | global:nstr | "\"10\"" | | | 101 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 102 | | | | bool | false | | | <-----> | | | global:bool | "false" | | | 103 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 104 | | | | arr | ["a","b"] | | | <-----> | | | global:arr | "[\"a\",\"b\"]" | | | 105 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 106 | | | | obj | {foo,"bar"} | | | <-----> | | | global:obj | "{\"foo\",\"bar\"}" | | | 107 | | | +--------+-------------+ | | | | +---------------+---------------------+ | | 108 | | +--------------------------+ | | +-----------------------------------------+ | 109 | +------------------------------+ +---------------------------------------------+ 110 | ``` 111 | 112 | Other Redis client(e.g. redis-cli) can get the value stored by Node-RED like followings. 113 | 114 | Node-RED 115 | 116 | ```javascript 117 | global.set("foo","bar","redis"); 118 | global.set("obj",{key:"value"},"redis"); 119 | ``` 120 | 121 | redis-cli 122 | 123 | ```console 124 | redis> GET global:foo 125 | "\"var\"" 126 | redis> GET global:obj 127 | "{\"key\":\"value\"}" 128 | redis> 129 | ``` 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | /** 18 | * Redis based context storage 19 | * 20 | * Configuration options: 21 | * { 22 | * host: '127.0.0.1', // The IP address of the Redis server 23 | * // default: '127.0.0.1' 24 | * port: 6379, // The port of the Redis server 25 | * // default: 6379 26 | * db: 0 // The Redis logical database to connect 27 | * // default: 0 28 | * prefix: // The string used to prefix all used keys 29 | * // If set, the plugin uses 'prefix + scope + keyname' as key 30 | * // (e.g. prefix:'foo', global.get('key') -> GET foo:global:key ) 31 | * // default: undefined 32 | * password: // If set, the plugin will run Redis AUTH command on connect 33 | * // Note: the password will be sent as plaintext 34 | * // default: undefined 35 | * tls: // An object containing options to pass to tls.connect to set up a TLS connection to Redis 36 | * // default: undefined 37 | * retry_strategy: // Specifies a function to reconnect if the connection to Redis is lost. 38 | * // default: undefined (Use the default retry strategy) 39 | * } 40 | * 41 | * This plugin prefixes all used keys with context scope. 42 | * For example 43 | * context.get('foo') -> The plugin will get the value of ':foo' (e.g. '36b85111.47f5fe:5b17c82f.6a0888:foo') 44 | * flow.get('foo') -> The plugin will get the value of ':foo' (e.g. '5b17c82f.6a0888:foo') 45 | * global.get('foo') -> The plugin will get the value of 'global:foo' 46 | * 47 | * If 'prefix' in above options is set, the key will be prefixed with it additionally. 48 | */ 49 | 50 | const redis = require('redis'); 51 | // Require @node-red/util loaded in the Node-RED runtime. 52 | const util = process.env.NODE_RED_HOME ? 53 | require(require.resolve('@node-red/util', { paths: [process.env.NODE_RED_HOME] })).util : 54 | require('@node-red/util').util; 55 | const log = process.env.NODE_RED_HOME ? 56 | require(require.resolve('@node-red/util', { paths: [process.env.NODE_RED_HOME] })).log : 57 | require('@node-red/util').log; 58 | 59 | const safeJSONStringify = require('json-stringify-safe'); 60 | 61 | // This lua script sets a nested property to JSON atomically 62 | // Usage: EVALSHA(SHA, 1, key, property, [property...], JSON) 63 | // e.g. Set obj.a.b.c to {foo: 'bar'} -> EVALSHA(SHA, 1, 'obj', 'a', 'b', 'c', '{"foo":"bar"}'); 64 | const setScript = ` 65 | -- get the value of key 66 | local data = redis.call('GET', KEYS[1]); 67 | if data then 68 | data = cjson.decode(data); 69 | else 70 | data = {} 71 | end 72 | -- parse path 73 | local path = data; 74 | local next; 75 | for i = 1, #ARGV-1 do 76 | next = tonumber(ARGV[i]); 77 | if next then 78 | next = next + 1; 79 | else 80 | next = ARGV[i] 81 | end 82 | if i == #ARGV-1 then 83 | break; 84 | end 85 | if not path[next] then 86 | path[next] = {}; 87 | end 88 | path = path[next]; 89 | end 90 | path[next] = cjson.decode(ARGV[#ARGV]); 91 | -- convert and set the value 92 | return redis.call('SET', KEYS[1], cjson.encode(data)); 93 | `; 94 | 95 | // This lua script deletes a nested property atomically 96 | // Usage: EVALSHA(SHA, 1, key, property, [property...]); 97 | // e.g. Delete obj.a.b.c -> EVALSHA(SHA, 1, 'obj', 'a', 'b', 'c'); 98 | const deleteScript = ` 99 | -- get the value of key 100 | local data = redis.call('GET', KEYS[1]); 101 | if data then 102 | data = cjson.decode(data); 103 | end 104 | -- parse path 105 | local path = data; 106 | local next = tonumber(ARGV[1]); 107 | for i = 2, #ARGV do 108 | if not path then 109 | return 0; 110 | end 111 | if next then 112 | path = path[next+1]; 113 | else 114 | path = path[ARGV[i-1]]; 115 | end 116 | next = tonumber(ARGV[i]); 117 | end 118 | -- delete the property 119 | if next and path[next+1] then 120 | table.remove(path, next+1); 121 | elseif path[ARGV[#ARGV]] then 122 | path[ARGV[#ARGV]] = nil; 123 | else 124 | -- return if try to delete non-existent value 125 | return 0 126 | end 127 | -- convert and set the value 128 | return redis.call('SET', KEYS[1], cjson.encode(data)); 129 | `; 130 | 131 | function stringify(value) { 132 | let hasCircular; 133 | let result = safeJSONStringify(value, null, null, function (k, v) { hasCircular = true; }); 134 | return { json: result, circular: hasCircular }; 135 | } 136 | 137 | function addPrefix(prefix, scope, key) { 138 | if (prefix) { 139 | scope = prefix + ':' + scope; 140 | } 141 | return scope + ':' + key; 142 | } 143 | 144 | function removePrefix(prefix, scope, key) { 145 | if (prefix) { 146 | key = key.substring((prefix + ':').length); 147 | } 148 | return key.substring((scope + ':').length); 149 | } 150 | 151 | function scan(client, pattern, cursor = 0) { 152 | return new Promise((resolve, reject) => { 153 | client.SCAN(cursor, 'MATCH', pattern, 'COUNT', 1000, (err, results) => { 154 | if (err) { 155 | return reject(err); 156 | } else { 157 | const cursor = results[0]; 158 | const elements = results[1]; 159 | if (cursor === "0") { 160 | //the iteration finished 161 | resolve(elements); 162 | } else { 163 | scan(client, pattern, cursor).then(result => { 164 | resolve(elements.concat(result)); 165 | }); 166 | } 167 | } 168 | }); 169 | }); 170 | } 171 | 172 | function Redis(config) { 173 | this.host = config.host || '127.0.0.1'; 174 | this.port = config.port || 6379; 175 | this.prefix = config.prefix; 176 | this.options = { 177 | db: config.db || 0, 178 | password: config.password, 179 | tls: config.tls, 180 | retry_strategy: config.retry_strategy || undefined 181 | }; 182 | this.client = null; 183 | this.knownCircularRefs = {}; 184 | } 185 | 186 | Redis.prototype.open = function () { 187 | const promises = []; 188 | this.client = redis.createClient(this.port, this.host, this.options); 189 | this.client.on('error', function (err) { 190 | log.error(err); 191 | }); 192 | promises.push(new Promise((resolve, reject) => { 193 | // Load the script into the scripts cache of Redis 194 | this.client.SCRIPT('load', setScript, (err, res) => { 195 | if (err) { 196 | reject(err.origin || err); 197 | } else { 198 | this.setSHA = res; 199 | resolve(); 200 | } 201 | }); 202 | })); 203 | promises.push(new Promise((resolve, reject) => { 204 | // Load the script into the scripts cache of Redis 205 | this.client.SCRIPT('load', deleteScript, (err, res) => { 206 | if (err) { 207 | reject(err.origin || err); 208 | } else { 209 | this.deleteSHA = res; 210 | resolve(); 211 | } 212 | }); 213 | })); 214 | return Promise.all(promises); 215 | }; 216 | 217 | Redis.prototype.close = function () { 218 | return new Promise((resolve, reject) => { 219 | this.client.QUIT((err) => { 220 | if (err) { 221 | reject(err); 222 | } else { 223 | resolve(); 224 | } 225 | }); 226 | }); 227 | }; 228 | 229 | Redis.prototype.get = function (scope, key, callback) { 230 | if (typeof callback !== 'function') { 231 | throw new Error('Callback must be a function'); 232 | } 233 | try { 234 | if (!Array.isArray(key)) { 235 | key = [key]; 236 | } 237 | const mgetArgs = []; 238 | // Filter duplicate keys in order to reduce response data 239 | const rootKeys = key.map(key => util.normalisePropertyExpression(key)[0]).filter((key, index, self) => self.indexOf(key) === index); 240 | rootKeys.forEach(key => mgetArgs.push(addPrefix(this.prefix, scope, key))); 241 | this.client.MGET(...mgetArgs, (err, replies) => { 242 | if (err) { 243 | callback(err); 244 | } else { 245 | let results = []; 246 | let data = {}; 247 | let value; 248 | for (let i = 0; i < rootKeys.length; i++) { 249 | try { 250 | if (replies[i]) { 251 | data[rootKeys[i]] = JSON.parse(replies[i]); 252 | } 253 | } catch (err) { 254 | // If data is not JSON, return `undefined` 255 | break; 256 | } 257 | } 258 | for (let i = 0; i < key.length; i++) { 259 | try { 260 | value = util.getObjectProperty(data, key[i]); 261 | } catch (err) { 262 | if (err.code === 'INVALID_EXPR') { 263 | throw err; 264 | } 265 | value = undefined; 266 | } 267 | results.push(value); 268 | } 269 | callback(null, ...results); 270 | } 271 | }); 272 | } catch (err) { 273 | callback(err); 274 | return; 275 | } 276 | }; 277 | 278 | Redis.prototype.set = function (scope, key, value, callback) { 279 | if (callback && typeof callback !== 'function') { 280 | throw new Error('Callback must be a function'); 281 | } 282 | try { 283 | if (!Array.isArray(key)) { 284 | key = [key]; 285 | value = [value]; 286 | } else if (!Array.isArray(value)) { 287 | // key is an array, but value is not - wrap it as an array 288 | value = [value]; 289 | } 290 | const multi = this.client.MULTI(); 291 | let msetArgs = []; 292 | let delArgs = []; 293 | // parse key 294 | const keyParts = key.map(key => util.normalisePropertyExpression(key)); 295 | 296 | for (let i = 0; i < key.length; i++) { 297 | if (i >= value.length) { 298 | value[i] = null; 299 | } 300 | keyParts[i][0] = addPrefix(this.prefix, scope, keyParts[i][0]); 301 | 302 | if (value[i] !== undefined) { // set a value 303 | const stringifiedContext = stringify(value[i]); 304 | 305 | if (stringifiedContext.circular && !this.knownCircularRefs[keyParts[i][0]]) { 306 | log.warn(log._('context.localfilesystem.error-circular', { scope: keyParts[i][0] })); 307 | this.knownCircularRefs[keyParts[i][0]] = true; 308 | } else { 309 | delete this.knownCircularRefs[keyParts[i][0]]; 310 | } 311 | 312 | if (delArgs.length > 0) { 313 | // Queue a command in order to execute commands sequentially 314 | multi.DEL(...delArgs); 315 | delArgs = []; 316 | } 317 | if (keyParts[i].length === 1) { 318 | msetArgs.push(keyParts[i][0], stringifiedContext.json); 319 | } else { 320 | if (msetArgs.length > 0) { 321 | multi.MSET(...msetArgs); 322 | msetArgs = []; 323 | } 324 | // To set a nested property atomically, call the lua script 325 | multi.EVALSHA(this.setSHA, 1, ...keyParts[i], stringifiedContext.json); 326 | } 327 | } else { // delete a value 328 | delete this.knownCircularRefs[keyParts[i][0]]; 329 | 330 | if (msetArgs.length > 0) { 331 | // Queue a command in order to execute commands sequentially 332 | multi.MSET(...msetArgs); 333 | msetArgs = []; 334 | } 335 | if (keyParts[i].length === 1) { 336 | delArgs.push(keyParts[i][0]); 337 | } else { 338 | if (delArgs.length > 0) { 339 | multi.DEL(...delArgs); 340 | delArgs = []; 341 | } 342 | // To delete a nested property atomically, call the lua script 343 | multi.EVALSHA(this.deleteSHA, 1, ...keyParts[i]); 344 | } 345 | } 346 | } 347 | if (msetArgs.length > 0) { 348 | multi.MSET(...msetArgs); 349 | } 350 | if (delArgs.length > 0) { 351 | multi.DEL(...delArgs); 352 | } 353 | // Execute commands at once with transactions 354 | multi.EXEC((err, replies) => { 355 | if (err) { 356 | if (callback) { 357 | callback(err); 358 | } 359 | } else { 360 | if (callback) { 361 | callback(null); 362 | } 363 | } 364 | }); 365 | } catch (err) { 366 | if (callback) { 367 | callback(err); 368 | } 369 | } 370 | }; 371 | 372 | Redis.prototype.keys = function (scope, callback) { 373 | if (typeof callback !== 'function') { 374 | throw new Error('Callback must be a function'); 375 | } 376 | scan(this.client, addPrefix(this.prefix, scope, '*')).then(result => { 377 | callback(null, result.map(v => removePrefix(this.prefix, scope, v))); 378 | }).catch(err => { 379 | callback(err); 380 | }); 381 | }; 382 | 383 | Redis.prototype.delete = function (scope) { 384 | return scan(this.client, addPrefix(this.prefix, scope, '*')).then(result => { 385 | if (result.length === 0) { 386 | return; 387 | } else { 388 | return new Promise((resolve, reject) => { 389 | this.client.DEL(...result, err => { 390 | if (err) { 391 | reject(err); 392 | } else { 393 | resolve(); 394 | } 395 | }); 396 | }); 397 | } 398 | }); 399 | }; 400 | 401 | Redis.prototype.clean = function (_activeNodes) { 402 | this.knownCircularRefs = {}; 403 | return new Promise((resolve, reject) => { 404 | this.client.KEYS((this.prefix || '') + '*', (err, res) => { 405 | if (err) { 406 | reject(err); 407 | } else { 408 | if(this.prefix){ 409 | res = res.map(key => key.substring(this.prefix.length + 1)) 410 | } 411 | res = res.filter(key => !key.startsWith("global")) 412 | _activeNodes.forEach(scope => { 413 | res = res.filter(key => !key.startsWith(scope)) 414 | }) 415 | var remove = []; 416 | res.forEach(key => remove.push(this.prefix + ":" + key)); 417 | if (remove.length > 0) { 418 | this.client.DEL(...remove, (err) => { 419 | if (err) { 420 | reject(err); 421 | } else { 422 | resolve(); 423 | } 424 | }); 425 | } else { 426 | resolve() 427 | } 428 | } 429 | }); 430 | }); 431 | }; 432 | 433 | module.exports = function (config) { 434 | return new Redis(config); 435 | }; 436 | -------------------------------------------------------------------------------- /test/_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | const sinon = require('sinon'); 18 | const should = require('should'); 19 | require('should-sinon'); 20 | const redis = require('redis'); 21 | const util = require("@node-red/util").util; 22 | const redisPlugin = require('../index.js'); 23 | 24 | describe('redis', function () { 25 | before(function () { 26 | const self = this; 27 | const context = redisPlugin({}); 28 | return context.open().then(() => { 29 | return context.close(); 30 | }).catch(() => { 31 | // You need a local Redis Server(127.0.0.1:6379) to run the test cases. 32 | console.log('Can not connect to Redis Server(127.0.0.1:6379), All tests will be skipped!'); 33 | self.test.parent.pending = true; 34 | self.skip(); 35 | }); 36 | }); 37 | 38 | describe('#open', function () { 39 | it('should connect to redis', function () { 40 | const context = redisPlugin({}); 41 | return context.open().then(() => { 42 | context.client.connected.should.be.true(); 43 | return context.close(); 44 | }); 45 | 46 | }); 47 | it('should load configs', function () { 48 | const fakeCreateClient = function () { 49 | return { 50 | SCRIPT: (a, b, cb) => cb(null), 51 | on: () => { } 52 | }; 53 | }; 54 | const stubCreateClient = sinon.stub(redis, "createClient").callsFake(fakeCreateClient); 55 | const context = redisPlugin({ host: "foo", port: 12345, db: 1, prefix: "bar", password: "baz", tls: { a: "b" } }); 56 | return context.open().then(function () { 57 | context.should.have.properties({ host: "foo", port: 12345, prefix: "bar" }); 58 | context.options.should.have.properties({ db: 1, password: "baz", tls: { a: "b" } }); 59 | stubCreateClient.should.be.calledWithMatch(12345, "foo", { db: 1, password: "baz", tls: { a: "b" } }); 60 | }).finally(() => { 61 | stubCreateClient.restore(); 62 | }); 63 | }); 64 | it('should throw an error if cannot connect to redis', function () { 65 | const context = redisPlugin({ host: "foobar", retry_strategy: () => undefined }); 66 | return context.open().should.be.rejected(); 67 | }); 68 | }); 69 | 70 | describe('#get/set', function () { 71 | const prefix = util.generateId(); 72 | const context = redisPlugin({ prefix: prefix }); 73 | 74 | before(function () { 75 | return context.open(); 76 | }); 77 | afterEach(function () { 78 | return context.delete("*").then(() => context.clean([])); 79 | }); 80 | after(function () { 81 | return context.close(); 82 | }); 83 | 84 | it('should store property', function (done) { 85 | context.get("nodeX", "foo", function (err, value) { 86 | if (err) { return done(err); } 87 | should.not.exist(value); 88 | context.set("nodeX", "foo", "test", function (err) { 89 | if (err) { return done(err); } 90 | context.get("nodeX", "foo", function (err, value) { 91 | if (err) { return done(err); } 92 | value.should.be.equal("test"); 93 | done(); 94 | }); 95 | }); 96 | }); 97 | }); 98 | 99 | it('should store property - creates parent properties', function (done) { 100 | context.set("nodeX", "foo.bar", "test", function (err) { 101 | context.get("nodeX", "foo", function (err, value) { 102 | value.should.be.eql({ bar: "test" }); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | 108 | it('should store local scope property', function (done) { 109 | context.set("abc:def", "foo.bar", "test", function (err) { 110 | context.get("abc:def", "foo", function (err, value) { 111 | value.should.be.eql({ bar: "test" }); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | it('should delete property', function (done) { 118 | context.set("nodeX", "foo.abc.bar1", "test1", function (err) { 119 | context.set("nodeX", "foo.abc.bar2", "test2", function (err) { 120 | context.get("nodeX", "foo.abc", function (err, value) { 121 | value.should.be.eql({ bar1: "test1", bar2: "test2" }); 122 | context.set("nodeX", "foo.abc.bar1", undefined, function (err) { 123 | context.get("nodeX", "foo.abc", function (err, value) { 124 | value.should.be.eql({ bar2: "test2" }); 125 | context.set("nodeX", "foo.abc", undefined, function (err) { 126 | context.get("nodeX", "foo.abc", function (err, value) { 127 | should.not.exist(value); 128 | context.set("nodeX", "foo", undefined, function (err) { 129 | context.get("nodeX", "foo", function (err, value) { 130 | should.not.exist(value); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | }); 136 | }); 137 | }); 138 | }); 139 | }); 140 | }); 141 | }); 142 | 143 | it('should do nothing if try to delete non-existent value', function (done) { 144 | context.set("nodeX", "foo.abc", { bar1: "test1", bar2: "test2", arr: ["test1", "test2"] }, function (err) { 145 | context.set("nodeX", ["foo.non", "foo.abc.bar3", "foo.abc[2]"],[undefined, undefined, undefined], function (err) { 146 | context.get("nodeX", "foo.abc", function (err, value) { 147 | value.should.be.eql({ bar1: "test1", bar2: "test2", arr: ["test1", "test2"] }); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | }); 153 | 154 | it('should not shared context with other scope', function (done) { 155 | context.get("nodeX", "foo", function (err, value) { 156 | should.not.exist(value); 157 | context.get("nodeY", "foo", function (err, value) { 158 | should.not.exist(value); 159 | context.set("nodeX", "foo", "testX", function (err) { 160 | context.set("nodeY", "foo", "testY", function (err) { 161 | context.get("nodeX", "foo", function (err, value) { 162 | value.should.be.equal("testX"); 163 | context.get("nodeY", "foo", function (err, value) { 164 | value.should.be.equal("testY"); 165 | done(); 166 | }); 167 | }); 168 | }); 169 | }); 170 | }); 171 | }); 172 | }); 173 | 174 | it('should store a string', function (done) { 175 | context.get("nodeX", "foo", function (err, value) { 176 | should.not.exist(value); 177 | context.set("nodeX", "foo", "bar", function (err) { 178 | context.get("nodeX", "foo", function (err, value) { 179 | value.should.be.String(); 180 | value.should.be.equal("bar"); 181 | context.set("nodeX", "foo", "1", function (err) { 182 | context.get("nodeX", "foo", function (err, value) { 183 | value.should.be.String(); 184 | value.should.be.equal("1"); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | }); 190 | }); 191 | }); 192 | 193 | it('should store a number', function (done) { 194 | context.get("nodeX", "foo", function (err, value) { 195 | should.not.exist(value); 196 | context.set("nodeX", "foo", 1, function (err) { 197 | context.get("nodeX", "foo", function (err, value) { 198 | value.should.be.Number(); 199 | value.should.be.equal(1); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | 206 | it('should store a null', function (done) { 207 | context.get("nodeX", "foo", function (err, value) { 208 | should.not.exist(value); 209 | context.set("nodeX", "foo", null, function (err) { 210 | context.get("nodeX", "foo", function (err, value) { 211 | should(value).be.null(); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | }); 217 | 218 | it('should store a boolean', function (done) { 219 | context.get("nodeX", "foo", function (err, value) { 220 | should.not.exist(value); 221 | context.set("nodeX", "foo", true, function (err) { 222 | context.get("nodeX", "foo", function (err, value) { 223 | value.should.be.Boolean().and.true(); 224 | context.set("nodeX", "foo", false, function (err) { 225 | context.get("nodeX", "foo", function (err, value) { 226 | value.should.be.Boolean().and.false(); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | }); 232 | }); 233 | }); 234 | 235 | it('should store an object', function (done) { 236 | context.get("nodeX", "foo", function (err, value) { 237 | should.not.exist(value); 238 | context.set("nodeX", "foo", { obj: "bar" }, function (err) { 239 | context.get("nodeX", "foo", function (err, value) { 240 | value.should.be.Object(); 241 | value.should.eql({ obj: "bar" }); 242 | done(); 243 | }); 244 | }); 245 | }); 246 | }); 247 | 248 | it('should store an array', function (done) { 249 | context.get("nodeX", "foo", function (err, value) { 250 | should.not.exist(value); 251 | context.set("nodeX", "foo", ["a", "b", "c"], function (err) { 252 | context.get("nodeX", "foo", function (err, value) { 253 | value.should.be.Array(); 254 | value.should.eql(["a", "b", "c"]); 255 | context.get("nodeX", "foo[1]", function (err, value) { 256 | value.should.be.String(); 257 | value.should.equal("b"); 258 | done(); 259 | }); 260 | }); 261 | }); 262 | }); 263 | }); 264 | 265 | it('should store an array of arrays', function (done) { 266 | context.get("nodeX", "foo", function (err, value) { 267 | should.not.exist(value); 268 | context.set("nodeX", "foo", [["a", "b", "c"], [1, 2, 3, 4], [true, false]], function (err) { 269 | context.get("nodeX", "foo", function (err, value) { 270 | value.should.be.Array(); 271 | value.should.have.length(3); 272 | value[0].should.have.length(3); 273 | value[1].should.have.length(4); 274 | value[2].should.have.length(2); 275 | context.get("nodeX", "foo[1]", function (err, value) { 276 | value.should.be.Array(); 277 | value.should.have.length(4); 278 | value.should.be.eql([1, 2, 3, 4]); 279 | done(); 280 | }); 281 | }); 282 | }); 283 | }); 284 | }); 285 | 286 | it('should store an array of objects', function (done) { 287 | context.get("nodeX", "foo", function (err, value) { 288 | should.not.exist(value); 289 | context.set("nodeX", "foo", [{ obj: "bar1" }, { obj: "bar2" }, { obj: "bar3" }], function (err) { 290 | context.get("nodeX", "foo", function (err, value) { 291 | value.should.be.Array(); 292 | value.should.have.length(3); 293 | value[0].should.be.Object(); 294 | value[1].should.be.Object(); 295 | value[2].should.be.Object(); 296 | context.get("nodeX", "foo[1]", function (err, value) { 297 | value.should.be.Object(); 298 | value.should.be.eql({ obj: "bar2" }); 299 | done(); 300 | }); 301 | }); 302 | }); 303 | }); 304 | }); 305 | 306 | it('should handle a circular object', function (done) { 307 | const foo = { bar: 'baz' }; 308 | foo.foo = foo; 309 | context.get("nodeX", "foo", function (err, value) { 310 | should.not.exist(value); 311 | context.set("nodeX", "foo", foo, function (err) { 312 | context.get("nodeX", "foo", function (err, value) { 313 | should.not.exist(value.foo); 314 | done(); 315 | }); 316 | }); 317 | }); 318 | }); 319 | 320 | it('should set/get multiple values', function (done) { 321 | context.set("nodeX", ["one", "two", "three"], ["test1", "test2", "test3"], function (err) { 322 | context.get("nodeX", ["one", "two"], function () { 323 | Array.prototype.slice.apply(arguments).should.eql([null, "test1", "test2"]); 324 | context.set("nodeX", ["foo", "foo", "foo", "foo"], ["bar", undefined, "baz", undefined], function (err) { 325 | context.get("nodeX", "foo", function (err, value) { 326 | should.not.exist(value); 327 | context.set("nodeX", ["foo", "foo.bar", "foo", "foo.bar"], [{bar:"baz"}, undefined, undefined, "baz"], function (err) { 328 | context.get("nodeX", "foo", function (err, value) { 329 | value.should.eql({bar:"baz"}); 330 | done(); 331 | }); 332 | }); 333 | }); 334 | }); 335 | }); 336 | }); 337 | }); 338 | 339 | it('should set/get multiple values - get unknown', function (done) { 340 | context.set("nodeX", ["one", "two", "three"], ["test1", "test2", "test3"], function (err) { 341 | context.get("nodeX", ["one", "two", "unknown"], function () { 342 | Array.prototype.slice.apply(arguments).should.eql([null, "test1", "test2", undefined]); 343 | done(); 344 | }); 345 | }); 346 | }); 347 | 348 | it('should set/get multiple values - single value provided', function (done) { 349 | context.set("nodeX", ["one", "two", "three"], "test1", function (err) { 350 | context.get("nodeX", ["one", "two"], function () { 351 | Array.prototype.slice.apply(arguments).should.eql([null, "test1", null]); 352 | done(); 353 | }); 354 | }); 355 | }); 356 | 357 | it('should set/get multiple nested properties', function (done) { 358 | context.set("nodeX", ["a.b.c.d", "f", "h.i", "k.l.m"], ["e", "g", "j", "n"], function () { 359 | context.get("nodeX", ["a.b", "f", "h", "k.l.m"], function () { 360 | Array.prototype.slice.apply(arguments).should.eql([null, {c:{d:"e"}}, "g", {i:"j"}, "n"]); 361 | done(); 362 | }); 363 | }); 364 | }); 365 | 366 | it('should delete multiple values', function (done) { 367 | context.set("nodeX", ["one", "two", "three"], ["test1", "test2", "test3"], function () { 368 | context.set("nodeX", ["one", "three"], [undefined, undefined], function () { 369 | context.get("nodeX", ["one", "two", "three"], function () { 370 | Array.prototype.slice.apply(arguments).should.eql([null, undefined, "test2", undefined]); 371 | done(); 372 | }); 373 | }); 374 | }); 375 | }); 376 | 377 | it('should delete multiple nested properties', function (done) { 378 | context.set("nodeX", ["a.b.c.d", "f.g.h.i", "k.l.m.n", "p.q.r.s"], ["e", "j", "o", "t"], function () { 379 | context.set("nodeX", ["a.b.c.d", "f", "k.l.m", "p.q"], [undefined, undefined, undefined, undefined], function () { 380 | context.get("nodeX", ["a", "f", "k", "p"], function () { 381 | Array.prototype.slice.apply(arguments).should.eql([null, {b:{c:{}}}, undefined, {l:{}}, {}]); 382 | done(); 383 | }); 384 | }); 385 | }); 386 | }); 387 | 388 | it('should throw error if bad key included in multiple keys - get', function (done) { 389 | context.set("nodeX", ["one", "two", "three"], ["test1", "test2", "test3"], function (err) { 390 | context.get("nodeX", ["one", ".foo", "three"], function (err) { 391 | should.exist(err); 392 | done(); 393 | }); 394 | }); 395 | }); 396 | 397 | it('should throw error if bad key included in multiple keys - set', function (done) { 398 | context.set("nodeX", ["one", ".foo", "three"], ["test1", "test2", "test3"], function (err) { 399 | should.exist(err); 400 | // Check 'one' didn't get set as a result 401 | context.get("nodeX", "one", function (err, one) { 402 | should.not.exist(one); 403 | done(); 404 | }); 405 | }); 406 | }); 407 | 408 | it('should throw an error when getting a value with invalid key', function (done) { 409 | context.set("nodeX", "foo", "bar", function (err) { 410 | context.get("nodeX", " ", function (err, value) { 411 | should.exist(err); 412 | done(); 413 | }); 414 | }); 415 | }); 416 | 417 | it('should throw an error when setting a value with invalid key', function (done) { 418 | context.set("nodeX", " ", "bar", function (err) { 419 | should.exist(err); 420 | done(); 421 | }); 422 | }); 423 | 424 | it('should throw an error when callback of get() is not a function', function (done) { 425 | try { 426 | context.get("nodeX", "foo", "callback"); 427 | done("should throw an error."); 428 | } catch (err) { 429 | done(); 430 | } 431 | }); 432 | 433 | it('should throw an error when callback of get() is not specified', function (done) { 434 | try { 435 | context.get("nodeX", "foo"); 436 | done("should throw an error."); 437 | } catch (err) { 438 | done(); 439 | } 440 | }); 441 | 442 | it('should throw an error when callback of set() is not a function', function (done) { 443 | try { 444 | context.set("nodeX", "foo", "bar", "callback"); 445 | done("should throw an error."); 446 | } catch (err) { 447 | done(); 448 | } 449 | }); 450 | 451 | it('should not throw an error when callback of set() is not specified', function (done) { 452 | try { 453 | context.set("nodeX", "foo", "bar"); 454 | done(); 455 | } catch (err) { 456 | done("should not throw an error."); 457 | } 458 | }); 459 | }); 460 | 461 | describe('#keys', function () { 462 | const prefix = util.generateId(); 463 | const context = redisPlugin({ prefix: prefix }); 464 | 465 | before(function () { 466 | return context.open(); 467 | }); 468 | afterEach(function () { 469 | return context.delete("*").then(() => context.clean([])); 470 | }); 471 | after(function () { 472 | return context.close(); 473 | }); 474 | 475 | it('should enumerate context keys', function (done) { 476 | context.keys("nodeX", function (err, value) { 477 | value.should.be.an.Array(); 478 | value.should.be.empty(); 479 | context.set("nodeX", "foo", "bar", function (err) { 480 | context.keys("nodeX", function (err, value) { 481 | value.should.have.length(1); 482 | value[0].should.equal("foo"); 483 | context.set("nodeX", "abc.def", "bar", function (err) { 484 | context.keys("nodeX", function (err, value) { 485 | value.should.have.length(2); 486 | value.should.containDeep(["foo", "abc"]); 487 | done(); 488 | }); 489 | }); 490 | }); 491 | }); 492 | }); 493 | }); 494 | 495 | it('should enumerate context keys in each scopes', function (done) { 496 | context.keys("nodeX", function (err, value) { 497 | value.should.be.an.Array(); 498 | value.should.be.empty(); 499 | context.keys("nodeY", function (err, value) { 500 | value.should.be.an.Array(); 501 | value.should.be.empty(); 502 | context.set("nodeX", "foo", "bar", function (err) { 503 | context.set("nodeY", "hoge", "piyo", function (err) { 504 | context.keys("nodeX", function (err, value) { 505 | value.should.have.length(1); 506 | value[0].should.equal("foo"); 507 | context.keys("nodeY", function (err, value) { 508 | value.should.have.length(1); 509 | value[0].should.equal("hoge"); 510 | done(); 511 | }); 512 | }); 513 | }); 514 | }); 515 | }); 516 | }); 517 | }); 518 | 519 | it('should throw an error when callback of keys() is not a function', function (done) { 520 | try { 521 | context.keys("nodeX", "callback"); 522 | done("should throw an error."); 523 | } catch (err) { 524 | done(); 525 | } 526 | }); 527 | 528 | it('should throw an error when callback of keys() is not specified', function (done) { 529 | try { 530 | context.keys("nodeX"); 531 | done("should throw an error."); 532 | } catch (err) { 533 | done(); 534 | } 535 | }); 536 | }); 537 | 538 | describe('#delete', function () { 539 | const prefix = util.generateId(); 540 | const context = redisPlugin({ prefix: prefix }); 541 | 542 | before(function () { 543 | return context.open(); 544 | }); 545 | afterEach(function () { 546 | return context.delete("*").then(() => context.clean([])); 547 | }); 548 | after(function () { 549 | return context.close(); 550 | }); 551 | it('should delete context', function (done) { 552 | context.get("nodeX", "foo", function (err, value) { 553 | should.not.exist(value); 554 | context.get("nodeY", "foo", function (err, value) { 555 | should.not.exist(value); 556 | context.set("nodeX", "foo", "testX", function (err) { 557 | context.set("nodeY", "foo", "testY", function (err) { 558 | context.get("nodeX", "foo", function (err, value) { 559 | value.should.be.equal("testX"); 560 | context.get("nodeY", "foo", function (err, value) { 561 | value.should.be.equal("testY"); 562 | context.delete("nodeX").then(function () { 563 | context.get("nodeX", "foo", function (err, value) { 564 | should.not.exist(value); 565 | context.get("nodeY", "foo", function (err, value) { 566 | value.should.be.equal("testY"); 567 | done(); 568 | }); 569 | }); 570 | }).catch(done); 571 | }); 572 | }); 573 | }); 574 | }); 575 | }); 576 | }); 577 | }); 578 | }); 579 | 580 | describe('#clean', function () { 581 | const prefix = util.generateId(); 582 | const context = redisPlugin({ prefix: prefix }); 583 | function redisGet(scope, key) { 584 | return new Promise((res, rej) => { 585 | context.get(scope, key, function (err, value) { 586 | if (err) { 587 | rej(err); 588 | } else { 589 | res(value); 590 | } 591 | }); 592 | }); 593 | } 594 | function redisSet(scope, key, value) { 595 | return new Promise((res, rej) => { 596 | context.set(scope, key, value, function (err) { 597 | if (err) { 598 | rej(err); 599 | } else { 600 | res(); 601 | } 602 | }); 603 | }); 604 | } 605 | before(function () { 606 | return context.open(); 607 | }); 608 | afterEach(function () { 609 | return context.clean([]); 610 | }); 611 | after(function () { 612 | return context.close(); 613 | }); 614 | 615 | it('should not clean active context', function () { 616 | return redisSet("global", "foo", "testGlobal").then(function () { 617 | return redisSet("nodeX:flow1", "foo", "testX"); 618 | }).then(function () { 619 | return redisSet("nodeY:flow2", "foo", "testY"); 620 | }).then(function () { 621 | return redisGet("nodeX:flow1", "foo").should.be.fulfilledWith("testX"); 622 | }).then(function () { 623 | return redisGet("nodeY:flow2", "foo").should.be.fulfilledWith("testY"); 624 | }).then(function () { 625 | return context.clean(["flow1", "nodeX"]); 626 | }).then(function () { 627 | return redisGet("nodeX:flow1", "foo").should.be.fulfilledWith("testX"); 628 | }).then(function () { 629 | return redisGet("nodeY:flow2", "foo").should.be.fulfilledWith(undefined); 630 | }).then(function () { 631 | return redisGet("global", "foo").should.be.fulfilledWith("testGlobal"); 632 | }); 633 | }); 634 | 635 | it('should clean unnecessary context', function () { 636 | return redisSet("global", "foo", "testGlobal").then(function () { 637 | return redisSet("nodeX:flow1", "foo", "testX"); 638 | }).then(function () { 639 | return redisSet("nodeY:flow2", "foo", "testY"); 640 | }).then(function () { 641 | return redisGet("nodeX:flow1", "foo").should.be.fulfilledWith("testX"); 642 | }).then(function () { 643 | return redisGet("nodeY:flow2", "foo").should.be.fulfilledWith("testY"); 644 | }).then(function () { 645 | return context.clean([]); 646 | }).then(function () { 647 | return redisGet("nodeX:flow1", "foo").should.be.fulfilledWith(undefined); 648 | }).then(function () { 649 | return redisGet("nodeY:flow2", "foo").should.be.fulfilledWith(undefined); 650 | }).then(function () { 651 | return redisGet("global", "foo").should.be.fulfilledWith("testGlobal"); 652 | }); 653 | }); 654 | }); 655 | }); 656 | --------------------------------------------------------------------------------