├── .gitignore ├── README.md ├── index.js ├── lib └── connect-couchbase.js ├── license.txt ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | connect-couchbase 2 | ================= 3 | 4 | NodeJS Session Store for Couchbase backed applications. 5 | 6 | ```` 7 | npm install connect-couchbase 8 | ```` 9 | 10 | This is based off of connect-redis, found at https://github.com/visionmedia/connect-redis. 11 | You can use like so, when setting up your Express 4.x app: 12 | 13 | ```` 14 | var debug = require('debug')('Couchbase Session Store Example') 15 | var session = require('express-session'); 16 | var CouchbaseStore = require('connect-couchbase')(session); 17 | var couchbaseStore = new CouchbaseStore({ 18 | bucket:"default", //optional 19 | host:"127.0.0.1:8091", //optional 20 | connectionTimeout: 2000, //optional 21 | operationTimeout: 2000, //optional 22 | cachefile: '', //optional 23 | ttl: 86400, //optional 24 | prefix: 'sess' //optional 25 | }); 26 | 27 | /* 28 | * cachefile: '' 29 | * ttl: 86400, 30 | * prefix: 'sess', 31 | * operationTimeout:2000, 32 | connectionTimeout:2000,*/ 33 | 34 | couchbaseStore.on('connect', function() { 35 | debug("Couchbase Session store is ready for use"); 36 | }); 37 | 38 | 39 | couchbaseStore.on('disconnect', function() { 40 | debug("An error occurred connecting to Couchbase Session Storage"); 41 | }); 42 | 43 | 44 | var app = express(); 45 | app.use(session({ 46 | store: couchbaseStore, 47 | secret: 'your secret', 48 | cookie: {maxAge:24*60*60*1000} //stay open for 1 day of inactivity 49 | })); 50 | ```` 51 | 52 | Please file any bugs at https://github.com/christophermina/connect-couchbase/issues 53 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by christophermina on 5/9/14. 3 | */ 4 | module.exports = require('./lib/connect-couchbase'); 5 | -------------------------------------------------------------------------------- /lib/connect-couchbase.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Connect - Couchbase 3 | * Copyright(c) 2014 Christopher Mina 4 | * 5 | * MIT Licensed 6 | * 7 | * This is an adaption from connect-redis, see: 8 | * https://github.com/visionmedia/connect-redis 9 | */ 10 | 11 | 'use strict' 12 | 13 | /** 14 | * Module dependencies. 15 | */ 16 | 17 | var debug = require('debug')('connect:couchbase'); 18 | 19 | /** 20 | * One day in seconds. 21 | */ 22 | 23 | var oneDay = 86400; 24 | 25 | /** 26 | * No op 27 | */ 28 | var noop = function () {}; 29 | 30 | /** 31 | * Return the `CouchbaseStore` extending `express`'s session Store. 32 | * 33 | * @param {object} express session 34 | * @return {Function} 35 | * @api public 36 | */ 37 | 38 | module.exports = function(session){ 39 | 40 | /** 41 | * Express's session Store. 42 | */ 43 | 44 | var Store = session.Store; 45 | 46 | /** 47 | * Initialize CouchbaseStore with the given `options`. 48 | * 49 | * @param {Object} options 50 | * { 51 | * host: 127.0.0.1:8091 (default) -- Can be one or more address:ports, separated by semi-colon, or an array 52 | * username: '', -- Should be same as bucket name, if provided 53 | * password: '', 54 | * bucket: 'default' (default) 55 | * cachefile: '' 56 | * ttl: 86400, 57 | * prefix: 'sess', 58 | * operationTimeout:2000, 59 | connectionTimeout:2000, 60 | * } 61 | * @api public 62 | */ 63 | 64 | function CouchbaseStore(options) { 65 | var self = this; 66 | 67 | options = options || {}; 68 | Store.call(this, options); 69 | this.prefix = null == options.prefix 70 | ? 'sess:' 71 | : options.prefix; 72 | 73 | var connectOptions = {}; 74 | if (options.hasOwnProperty("host")) { 75 | connectOptions.host = options.host; 76 | } else if (options.hasOwnProperty("hosts")) { 77 | connectOptions.host = options.hosts; 78 | } 79 | 80 | if (options.hasOwnProperty("username")) { 81 | connectOptions.username = options.username; 82 | } 83 | 84 | if (options.hasOwnProperty("password")) { 85 | connectOptions.password = options.password; 86 | } 87 | 88 | if (options.hasOwnProperty("bucket")) { 89 | connectOptions.bucket = options.bucket; 90 | } 91 | 92 | if (options.hasOwnProperty("cachefile")) { 93 | connectOptions.cachefile = options.cachefile; 94 | } 95 | 96 | if (options.hasOwnProperty("connectionTimeout")) { 97 | connectOptions.connectionTimeout = options.connectionTimeout; 98 | } 99 | 100 | if (options.hasOwnProperty("operationTimeout")) { 101 | connectOptions.operationTimeout = options.operationTimeout; 102 | } 103 | 104 | 105 | if (options.hasOwnProperty("db")) { 106 | connectOptions.db = options.db; // DB Instance 107 | } 108 | 109 | if ( typeof(connectOptions.db) != 'undefined' ) { 110 | this.client = connectOptions.db; 111 | } else { 112 | var Couchbase = require('couchbase'); 113 | var cluster = new Couchbase.Cluster(connectOptions.host); 114 | 115 | var bucket = connectOptions.bucket; 116 | var prefix = this.prefix; 117 | 118 | this.queryAll = Couchbase.N1qlQuery.fromString('SELECT `' 119 | + bucket + '`.* FROM `' 120 | + bucket + '` WHERE SUBSTR(META(`' 121 | + bucket + '`).id, 0, ' 122 | + prefix.length + ') = "' 123 | + prefix + '"'); 124 | 125 | this.client = cluster.openBucket(connectOptions.bucket, connectOptions.password, function(err) { 126 | if (err) { 127 | console.log("Could not connect to couchbase with bucket: " + connectOptions.bucket); 128 | self.emit('disconnect', err); 129 | } else { 130 | self.emit('connect'); 131 | } 132 | }); 133 | } 134 | 135 | this.client.connectionTimeout = connectOptions.connectionTimeout || 10000; 136 | this.client.operationTimeout = connectOptions.operationTimeout || 10000; 137 | 138 | this.ttl = options.ttl || null; 139 | } 140 | 141 | /** 142 | * Inherit from `Store`. 143 | */ 144 | 145 | CouchbaseStore.prototype.__proto__ = Store.prototype; 146 | 147 | /** 148 | * Attempt to fetch session by the given `sid`. 149 | * 150 | * @param {String} sid 151 | * @param {Function} fn 152 | * @api public 153 | */ 154 | 155 | CouchbaseStore.prototype.get = function(sid, fn){ 156 | if ('function' !== typeof fn) { fn = noop; } 157 | sid = this.prefix + sid; 158 | debug('GET "%s"', sid); 159 | this.client.get(sid, function(err, data){ 160 | //Handle Key Not Found error 161 | if (err && err.code == 13) { 162 | return fn(); 163 | } 164 | if (err) return fn(err); 165 | if (!data || !data.value) return fn(); 166 | var result; 167 | data = data.value.toString(); 168 | debug('GOT %s', data); 169 | try { 170 | result = JSON.parse(data); 171 | } catch (err) { 172 | return fn(err); 173 | } 174 | return fn(null, result); 175 | }); 176 | }; 177 | 178 | /** 179 | * Commit the given `sess` object associated with the given `sid`. 180 | * 181 | * @param {String} sid 182 | * @param {Session} sess 183 | * @param {Function} fn 184 | * @api public 185 | */ 186 | 187 | CouchbaseStore.prototype.set = function(sid, sess, fn){ 188 | if ('function' !== typeof fn) { fn = noop; } 189 | sid = this.prefix + sid; 190 | try { 191 | var maxAge = sess.cookie.maxAge 192 | , ttl = this.ttl 193 | , sess = JSON.stringify(sess); 194 | 195 | ttl = ttl || ('number' == typeof maxAge 196 | ? maxAge / 1000 | 0 197 | : oneDay); 198 | 199 | debug('SETEX "%s" ttl:%s %s', sid, ttl, sess); 200 | this.client.upsert(sid, sess, {expiry:ttl}, function(err){ 201 | err || debug('Session Set complete'); 202 | fn && fn.apply(this, arguments); 203 | }); 204 | } catch (err) { 205 | fn && fn(err); 206 | } 207 | }; 208 | 209 | /** 210 | * Returns all active session 211 | * 212 | * @param {Function} fn 213 | * @api public 214 | */ 215 | 216 | CouchbaseStore.prototype.all = function(fn){ 217 | this.client.query(this.queryAll, function(err, rows) { 218 | fn(err, rows); 219 | }); 220 | }; 221 | 222 | /** 223 | * Destroy the session associated with the given `sid`. 224 | * 225 | * @param {String} sid 226 | * @api public 227 | */ 228 | 229 | CouchbaseStore.prototype.destroy = function(sid, fn){ 230 | if ('function' !== typeof fn) { fn = noop; } 231 | sid = this.prefix + sid; 232 | this.client.remove(sid, fn); 233 | }; 234 | 235 | 236 | /** 237 | * Refresh the time-to-live for the session with the given `sid`. 238 | * 239 | * @param {String} sid 240 | * @param {Session} sess 241 | * @param {Function} fn 242 | * @api public 243 | */ 244 | 245 | CouchbaseStore.prototype.touch = function (sid, sess, fn) { 246 | if ('function' !== typeof fn) { fn = noop; } 247 | 248 | var maxAge = sess.cookie.maxAge 249 | , ttl = this.ttl 250 | , sess = JSON.stringify(sess); 251 | 252 | ttl = ttl || ('number' == typeof maxAge 253 | ? maxAge / 1000 | 0 254 | : oneDay); 255 | 256 | 257 | debug('EXPIRE "%s" ttl:%s', sid, ttl); 258 | this.client.touch(this.prefix + sid, ttl, fn); 259 | }; 260 | 261 | return CouchbaseStore; 262 | }; 263 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-couchbase", 3 | "description": "Couchbase session store for Connect, updated for Couchnode 2.0.x", 4 | "version": "0.2.10", 5 | "author": "Christopher Mina ", 6 | "main": "./index.js", 7 | "repository": { "type": "git", "url": "git@github.com:christophermina/connect-couchbase.git" }, 8 | "dependencies": { 9 | "debug": "*" 10 | }, 11 | "devDependencies": { "express-session": "^1.9.1" }, 12 | "engines": { "node": "*" }, 13 | "bugs": { 14 | "url": "https://github.com/christophermina/connect-couchbase/issues" 15 | }, 16 | "scripts":{ 17 | "test":"node test.js" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by christophermina on 5/9/14. 3 | */ 4 | 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | var assert = require('assert') 11 | , session = require('express-session') 12 | , CouchbaseStore = require('./')(session); 13 | 14 | var store = new CouchbaseStore({host:"127.0.0.1:8091", bucket:"default"}); 15 | 16 | store.on('connect', function(){ 17 | // #set() 18 | store.set('123', { cookie: { maxAge: 2000 }, name: 'cm' }, function(err, ok){ 19 | assert.ok(!err, '#set() got an error'); 20 | assert.ok(ok, '#set() is not ok'); 21 | 22 | // #get() 23 | store.get('123', function(err, data){ 24 | console.log("RETRIEVED: " + data.name); 25 | assert.ok(!err, '#get() got an error'); 26 | assert.deepEqual({ cookie: { maxAge: 2000 }, name: 'cm' }, data); 27 | 28 | // #all() 29 | store.all(function(err, sessions) { 30 | if (err) { 31 | console.log("AN ERROR OCCURRED GETTING ALL SESSION: " + err); 32 | } 33 | 34 | if (sessions.length != 0) { 35 | assert.deepEqual({ cookie: { maxAge: 2000 }, name: 'cm' }, sessions[0]); 36 | } 37 | 38 | // #set null 39 | store.set('123', { cookie: { maxAge: 2000 }, name: 'cm' }, function(err){ 40 | if (err) { 41 | console.log("AN ERROR OCCURRED SETTING SESSION: " + err); 42 | } 43 | 44 | store.destroy('123', function(err){ 45 | if (err) { 46 | console.log("AN ERROR OCCURRED DESTROYING SESSION: " + err); 47 | } 48 | 49 | console.log('done'); 50 | store.client.disconnect(); 51 | process.exit(0); 52 | }); 53 | }); 54 | }); 55 | throw new Error('Error in fn'); 56 | }); 57 | }); 58 | }); 59 | 60 | process.once('uncaughtException', function (err) { 61 | assert.ok(err.message === 'Error in fn', '#get() catch wrong error'); 62 | }); 63 | --------------------------------------------------------------------------------