├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── package.json ├── registry.js ├── templates └── view.map.template.js ├── test ├── integration │ └── runner.js └── unit │ ├── README.md │ └── register.js └── views.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | .\#* 3 | *# 4 | node_modules 5 | ssl 6 | .DS_STORE 7 | *~ 8 | .idea 9 | nbproject 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | 5 | sudo: false 6 | 7 | services: 8 | - couchdb 9 | 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image_squidhome@2x.png](http://i.imgur.com/RIvu9.png) 2 | 3 | # Waterline CouchDB Adapter 4 | 5 | [Waterline](https://github.com/balderdashy/waterline) adapter for CouchDB. 6 | 7 | ## Versions 8 | 9 | The 0.9.x version of this adapter is maintained in a branch available here: https://github.com/codeswarm/sails-couchdb-orm/tree/0.9.x 10 | 11 | The 0.10.x version of this adapter is maintained on the master branch. 12 | 13 | ## Install 14 | 15 | Add this module to your sails.js project: 16 | 17 | ```bash 18 | $ npm install sails-couchdb-orm --save 19 | ``` 20 | 21 | Example entry in `config/connections.js`: 22 | 23 | ```json 24 | couch: { 25 | adapter: 'sails-couchdb-orm', 26 | host: 'localhost', 27 | port: 5984, 28 | username: 'myuser', 29 | password: 'mypassword' 30 | } 31 | ``` 32 | 33 | ## Use 34 | 35 | Example use in a model `models/MyModel.js`, specifying the adapter defined above: 36 | 37 | ```javascript 38 | module.exports = { 39 | adapter: 'couch', 40 | migrate: 'safe', 41 | id: { 42 | primaryKey: true, 43 | type: 'string' 44 | }, 45 | attributes: { 46 | name: 'string', 47 | //... 48 | } 49 | } 50 | ``` 51 | 52 | Or set "couch" as the default adapter for all models, inside `config/models.js`: 53 | 54 | ```javascript 55 | module.exports.models = { 56 | connection: 'couch' 57 | }; 58 | ``` 59 | 60 | ### Class methods 61 | 62 | Besides the usual Waterline stuff, this adapter also provides the class methods `merge`, `authenticate` and `session`. 63 | 64 | #### View 65 | 66 | TODO docs 67 | 68 | #### Merge 69 | 70 | Merge some attributes into one document. 71 | 72 | Example: 73 | 74 | ```javascript 75 | var someAttributes = { 76 | lastName: 'Simpson', 77 | favoriteFood: 'beer' 78 | }; 79 | 80 | var id = 'homer@simpsons.com'; 81 | 82 | User.merge(id, someAttributes, function(err, homer) { 83 | // ... 84 | }); 85 | ``` 86 | 87 | #### Authenticate 88 | 89 | Authenticate against the CouchDB user database (`_users`). 90 | 91 | Example: 92 | 93 | ```javascript 94 | var username = req.param('username'); 95 | var password = req.param('password'); 96 | 97 | Users.authenticate(username, password, function(err, sessionId, username, roles) { 98 | // ... 99 | }); 100 | ``` 101 | 102 | 103 | #### Session 104 | 105 | Get the CouchDB session object. 106 | 107 | Example: 108 | 109 | ```javascript 110 | var sessionId = req.cookies.sid; 111 | Users.session(sessionId, function(err, session) { 112 | // ... 113 | }); 114 | ``` 115 | 116 | ## Sails.js 117 | 118 | http://sailsjs.org 119 | 120 | ## Waterline 121 | 122 | [Waterline](https://github.com/balderdashy/waterline) is a brand new kind of storage and retrieval engine. 123 | 124 | It provides a uniform API for accessing stuff from different kinds of databases, protocols, and 3rd party APIs. That means you write the same code to get users, whether they live in MySQL, LDAP, MongoDB, or Facebook. 125 | 126 | 127 | ## Contributors 128 | 129 | Thanks so much to Pedro Teixeira([@pgte](https://twitter.com/pgte)) for building this adapter. 130 | 131 | ## License 132 | 133 | ### The MIT License (MIT) 134 | 135 | Copyright © 2014 CodeSwarm, Inc. 136 | 137 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 138 | 139 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 142 | 143 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var nano = require('nano'); 6 | var async = require('async'); 7 | var extend = require('xtend'); 8 | var cookie = require('cookie'); 9 | var DeepMerge = require('deep-merge'); 10 | var _ = require('underscore'); 11 | 12 | var merge = DeepMerge(function(a, b) { 13 | return b; 14 | }); 15 | 16 | var registry = require('./registry'); 17 | var views = require('./views'); 18 | 19 | 20 | 21 | // You'll want to maintain a reference to each collection 22 | // (aka model) that gets registered with this adapter. 23 | 24 | 25 | 26 | var adapter = exports; 27 | 28 | // Set to true if this adapter supports (or requires) things like data types, validations, keys, etc. 29 | // If true, the schema for models using this adapter will be automatically synced when the server starts. 30 | // Not terribly relevant if your data store is not SQL/schemaful. 31 | adapter.syncable = false; 32 | 33 | 34 | // Reserved attributes. 35 | // These attributes get passed in to the `adapter.update` function even if they're not declared 36 | // in the model schema. 37 | adapter.reservedAttributes = ['id', 'rev']; 38 | 39 | 40 | // Default configuration for collections 41 | // (same effect as if these properties were included at the top level of the model definitions) 42 | adapter.defaults = { 43 | 44 | port: 5984, 45 | host: 'localhost', 46 | https: false, 47 | username: null, 48 | password: null, 49 | 50 | schema: true, 51 | syncable: false, 52 | autoPK: false, 53 | pkFormat: 'string', 54 | 55 | maxMergeAttempts: 5, 56 | 57 | 58 | 59 | // If setting syncable, you should consider the migrate option, 60 | // which allows you to set how the sync will be performed. 61 | // It can be overridden globally in an app (config/adapters.js) 62 | // and on a per-model basis. 63 | // 64 | // IMPORTANT: 65 | // `migrate` is not a production data migration solution! 66 | // In production, always use `migrate: safe` 67 | // 68 | // drop => Drop schema and data, then recreate it 69 | // alter => Drop/add columns as necessary. 70 | // safe => Don't change anything (good for production DBs) 71 | migrate: 'safe' 72 | }; 73 | 74 | /** 75 | * 76 | * This method runs when a model is initially registered 77 | * at server-start-time. This is the only required method. 78 | * 79 | * @param {[type]} collection [description] 80 | * @param {Function} cb [description] 81 | * @return {[type]} [description] 82 | */ 83 | adapter.registerConnection = function registerConnection(connection, collections, cb) { 84 | 85 | var url = urlForConfig(connection); 86 | var db = nano(url); 87 | 88 | // Save the connection 89 | registry.connection(connection.identity, connection); 90 | 91 | async.each(_.keys(collections),function(modelIdentity,next) { 92 | adapter.registerSingleCollection(connection, modelIdentity, collections[modelIdentity], next); 93 | }, function afterAsyncEach (err) { 94 | if(err) { 95 | return cb(new Error(err.description)); 96 | } 97 | 98 | return cb(); 99 | }); 100 | }; 101 | 102 | /** 103 | * 104 | * This method runs to register a single model, or collection. 105 | * 106 | * @param {[type]} connection [description] 107 | * @param {[type]} collection [description] 108 | * @param {Function} cb [description] 109 | * @return {[type]} [description] 110 | */ 111 | adapter.registerSingleCollection = function registerCollection(connection, collectionName, collection, cb) { 112 | collectionName = sanitizeCollectionName(collectionName); 113 | 114 | var url = urlForConfig(connection); 115 | 116 | // Wire up nano to the configured couchdb connection 117 | var db = nano(url); 118 | 119 | // console.log('registering %s', collectionName); 120 | // console.log('got db.db or whatever', db); 121 | // console.log('urlForConfig', url,connection); 122 | 123 | db.db.get(collectionName, function gotDatabase(err) { 124 | 125 | // No error means we're good! The collection (or in couch terms, the "db") 126 | // is already known and ready to use. 127 | if (!err) { 128 | registry.collection(collectionName, collection); 129 | registry.db(collectionName, nano(url + collectionName)); 130 | return cb(); 131 | } 132 | 133 | try { 134 | if (err.status_code == 404 && err.reason == 'no_db_file') { 135 | db.db.create(collectionName, function createdDB(err) { 136 | if (err) { 137 | return cb(err); 138 | } 139 | 140 | adapter.registerSingleCollection(connection, collectionName, collection, cb); 141 | }); 142 | return; 143 | } 144 | // console.log('unexpected ERROR', err); 145 | return cb(err); 146 | } 147 | catch (e) { return cb(e); } 148 | 149 | }); 150 | 151 | }; 152 | 153 | 154 | /** 155 | * Fired when a model is unregistered, typically when the server 156 | * is killed. Useful for tearing-down remaining open connections, 157 | * etc. 158 | * 159 | * @param {Function} cb [description] 160 | * @return {[type]} [description] 161 | */ 162 | adapter.teardown = function teardown(connection, cb) { 163 | process.nextTick(cb); 164 | }; 165 | 166 | 167 | /** 168 | * 169 | * REQUIRED method if integrating with a schemaful 170 | * (SQL-ish) database. 171 | * 172 | * @param {[type]} collectionName [description] 173 | * @param {Function} cb [description] 174 | * @return {[type]} [description] 175 | */ 176 | adapter.describe = function describe(connection, collectionName, cb) { 177 | collectionName = sanitizeCollectionName(collectionName); 178 | var collection = registry.collection(collectionName); 179 | if (! collection) 180 | return cb(new Error('no such collection')); 181 | 182 | return cb(null, collection.definition); 183 | }; 184 | 185 | 186 | /** 187 | * 188 | * 189 | * REQUIRED method if integrating with a schemaful 190 | * (SQL-ish) database. 191 | * 192 | * @param {[type]} collectionName [description] 193 | * @param {[type]} relations [description] 194 | * @param {Function} cb [description] 195 | * @return {[type]} [description] 196 | */ 197 | adapter.drop = function drop(connectionName, collectionName, relations, cb) { 198 | collectionName = sanitizeCollectionName(collectionName); 199 | var connection = registry.connection(connectionName); 200 | var url = urlForConfig(connection); 201 | var db = nano(url); 202 | db.db.destroy(collectionName, cb); 203 | }; 204 | 205 | 206 | /** 207 | * REQUIRED method if users expect to call Model.find(), Model.findOne(), 208 | * or related. 209 | * 210 | * You should implement this method to respond with an array of instances. 211 | * Waterline core will take care of supporting all the other different 212 | * find methods/usages. 213 | * 214 | * @param {[type]} connectionName [description] 215 | * @param {[type]} collectionName [description] 216 | * @param {[type]} criteria [description] 217 | * @param {Function} cb [description] 218 | * @return {[type]} [description] 219 | */ 220 | 221 | adapter.find = find; 222 | 223 | function find(connectionName, collectionName, criteria, cb, round) { 224 | collectionName = sanitizeCollectionName(collectionName); 225 | if ('number' != typeof round) round = 0; 226 | 227 | // If you need to access your private data for this collection: 228 | var db = registry.db(collectionName); 229 | 230 | console.log('GETTING DB FOR "%s"."%s"', connectionName, collectionName); 231 | // console.log('got: ',db); 232 | if (!db) { 233 | return cb((function buildError(){ 234 | var e = new Error(); 235 | e.name = 'Adapter Error'; 236 | e.type = 'adapter'; 237 | e.code = 'E_ADAPTER'; 238 | e.message = util.format('Could not acquire data access object (`db`) object for CouchDB connection "%s" for collection "%s"', connectionName, collectionName); 239 | e.connectionName = connectionName; 240 | e.collectionName = collectionName; 241 | return e; 242 | })()); 243 | } 244 | 245 | // Build initial `dbOptions` 246 | var dbOptions = (function (dbOptions){ 247 | if (criteria.limit) dbOptions.limit = criteria.limit; 248 | if (criteria.skip) dbOptions.skip = criteria.skip; 249 | return dbOptions; 250 | })({}); 251 | 252 | var queriedAttributes = Object.keys(criteria.where || {}); 253 | //console.log("Queried Attributes: ",queriedAttributes); 254 | 255 | // Handle case where no criteria is specified at all 256 | // (list all documents in the couch collection) 257 | if (queriedAttributes.length === 0) { 258 | console.log('Queried Attributes" (aka criteria\'s WHERE clause) doesn\'t contain any values-- listing everything!'); 259 | 260 | // All docs 261 | dbOptions.include_docs = true; 262 | // TODO: test if limit works? 263 | // TODO: test if skip works? 264 | // TODO: test if sort works? 265 | db.list(dbOptions, function listReplied(err, docs) { 266 | if (err) { 267 | return cb(err); 268 | } 269 | 270 | if (!Array.isArray(docs) && docs.rows) { 271 | docs = docs.rows.map(prop('doc')); 272 | } 273 | else {} 274 | 275 | // either way... 276 | return cb(null, docs.map(docForReply)); 277 | }); 278 | return; 279 | } 280 | 281 | 282 | // Handle query for a single doc using the provided primary key criteria (e.g. `findOne()`) 283 | if (queriedAttributes.length == 1 && (queriedAttributes[0] == 'id' || queriedAttributes[0] == '_id')) { 284 | var id = criteria.where.id || criteria.where._id; 285 | 286 | db.get(id, dbOptions, function(err, doc) { 287 | if (err) { 288 | if (err.status_code == 404) { 289 | return cb(null, []); 290 | } 291 | return cb(err); 292 | } 293 | var docs = doc ? [doc] : []; 294 | return cb(null, docs.map(docForReply)); 295 | }); 296 | return; 297 | } 298 | 299 | // Take a look at `criteria.where.like`... 300 | asyncx_ifTruthy(criteria.where.like, 301 | 302 | // Handle "like" modifier using a view 303 | function ifSoDo(next){ 304 | var viewName = views.name(criteria.where.like); 305 | var value = views.likeValue(criteria.where.like, true); 306 | dbOptions.startkey = value.startkey; 307 | dbOptions.endkey = value.endkey; 308 | return db.view('views', viewName, dbOptions, next); 309 | }, 310 | 311 | // Handle general-case criteria queries using a view 312 | function elseDo (next){ 313 | var viewName = views.name(criteria.where); 314 | dbOptions.key = views.value(criteria.where); 315 | return db.view('views', viewName, dbOptions, next); 316 | }, 317 | 318 | function finallyDo(err, reply) { 319 | if (err) { 320 | if (err.status_code === 404 && round < 1) { 321 | views.create(db, criteria.where.like || criteria.where, function createdView(err) { 322 | if (err) { 323 | return cb(err); 324 | } 325 | find.call(connectionName, connectionName, collectionName, criteria, cb, round + 1); 326 | }); 327 | return; 328 | } 329 | 330 | return cb(err); 331 | } 332 | 333 | return cb(null, reply.rows.map(prop('value')).map(docForReply)); 334 | } 335 | ); 336 | 337 | 338 | } 339 | 340 | 341 | /** 342 | * 343 | * REQUIRED method if users expect to call Model.create() or any methods 344 | * 345 | * @param {[type]} collectionName [description] 346 | * @param {[type]} values [description] 347 | * @param {Function} cb [description] 348 | * @return {[type]} [description] 349 | */ 350 | adapter.create = function create(connectionName, collectionName, values, cb) { 351 | collectionName = sanitizeCollectionName(collectionName); 352 | 353 | var db = registry.db(collectionName); 354 | db.insert(docForIngestion(values), replied); 355 | 356 | function replied(err, reply) { 357 | if (err) cb(err); 358 | else { 359 | var attrs = extend({}, values, { _id: reply.id, _rev: reply.rev }); 360 | cb(null, docForReply(attrs)); 361 | } 362 | } 363 | }; 364 | 365 | 366 | 367 | /** 368 | * 369 | * 370 | * REQUIRED method if users expect to call Model.update() 371 | * 372 | * @param {[type]} collectionName [description] 373 | * @param {[type]} options [description] 374 | * @param {[type]} values [description] 375 | * @param {Function} cb [description] 376 | * @return {[type]} [description] 377 | */ 378 | adapter.update = function update(connectionName, collectionName, options, values, cb) { 379 | collectionName = sanitizeCollectionName(collectionName); 380 | 381 | var searchAttributes = Object.keys(options.where); 382 | if (searchAttributes.length != 1) 383 | return cb(new Error('only support updating one object by id')); 384 | if (searchAttributes[0] != 'id') 385 | return cb(new Error('only support updating one object by id')); 386 | 387 | // Find the document 388 | adapter.find(connectionName, collectionName, options, function(err,docs) { 389 | var doc = docs[0]; // only one document with that id 390 | if(!doc) return cb('No document found to update.'); 391 | 392 | delete values.id; // deleting id from values attr 393 | Object.keys(values).forEach(function(key) { 394 | doc[key] = values[key]; 395 | }); 396 | 397 | //console.log('Document to update: ', doc); 398 | var db = registry.db(collectionName); 399 | db.insert(docForIngestion(doc), options.where.id, function(err, reply) { 400 | if (err) cb(err); 401 | else { 402 | var attrs = extend({}, doc, { _id: reply.id, _rev: reply.rev }); 403 | cb(null, docForReply(attrs)); 404 | } 405 | }); 406 | }); 407 | }; 408 | 409 | 410 | /** 411 | * 412 | * REQUIRED method if users expect to call Model.destroy() 413 | * 414 | * @param {[type]} collectionName [description] 415 | * @param {[type]} options [description] 416 | * @param {Function} cb [description] 417 | * @return {[type]} [description] 418 | */ 419 | adapter.destroy = function destroy(connectionName, collectionName, options, cb) { 420 | collectionName = sanitizeCollectionName(collectionName); 421 | var db = registry.db(collectionName); 422 | 423 | // Find the record 424 | adapter.find(connectionName,collectionName,options, function(err,docs) { 425 | async.each(docs,function(item) { // Shoud have only one. 426 | db.destroy(item.id, item.rev, function(err, doc) { 427 | cb(err,[item]); // Waterline expects an array as result. 428 | }); 429 | }); 430 | }); 431 | }; 432 | 433 | 434 | 435 | /********************************************** 436 | * Custom methods 437 | **********************************************/ 438 | 439 | 440 | /// Authenticate 441 | 442 | adapter.authenticate = function authenticate(connection, collectionName, username, password, cb) { 443 | collectionName = sanitizeCollectionName(collectionName); 444 | var db = registry.db(collectionName); 445 | 446 | db.auth(username, password, replied); 447 | 448 | function replied(err, body, headers) { 449 | if (err) cb(err); 450 | else { 451 | var sessionId; 452 | var header = headers['set-cookie'][0]; 453 | if (header) sessionId = cookie.parse(header).AuthSession; 454 | cb(null, sessionId, username, body.roles); 455 | } 456 | } 457 | }; 458 | 459 | 460 | /// Session 461 | 462 | adapter.session = function session(connection, collectionName, sid, cb) { 463 | collectionName = sanitizeCollectionName(collectionName); 464 | var url = urlForConfig(registry.connection(connection)); 465 | 466 | var sessionDb = nano({ 467 | url: url, 468 | cookie: 'AuthSession=' + encodeURIComponent(sid) 469 | }); 470 | 471 | sessionDb.session(cb); 472 | }; 473 | 474 | 475 | 476 | /// Merge 477 | 478 | adapter.merge = function adapterMerge(connectionName, collectionName, id, attrs, cb, attempts) { 479 | collectionName = sanitizeCollectionName(collectionName); 480 | var doc; 481 | var db = registry.db(collectionName); 482 | 483 | var coll = registry.collection(collectionName); 484 | /* 485 | console.log('------------------------------------------'); 486 | console.log('Attempting merge on ',collectionName,id,attrs); 487 | console.log('------------------------------------------'); 488 | */ 489 | 490 | if ('number' != typeof attempts) attempts = 0; 491 | else if (attempts > 0) { 492 | //var config = coll.adapter.config; 493 | // Reference to maxMergeAttempts 494 | if (attempts > 5) { 495 | return cb(new Error('max attempts of merging reached')); 496 | } 497 | } 498 | 499 | db.get(id, got); 500 | 501 | function got(err, _doc) { 502 | if (err && err.status_code == 404) _doc = {}; 503 | else if (err) return cb(err); 504 | 505 | delete attrs._rev; 506 | 507 | _doc = docForReply(_doc); 508 | 509 | doc = merge(_doc, attrs); 510 | //console.log('----------Callbacks',coll._callbacks.beforeUpdate); 511 | async.eachSeries(coll._callbacks.beforeUpdate || [], invokeCallback, afterBeforeUpdate); 512 | } 513 | 514 | function invokeCallback(fn, cb) { 515 | //console.log("----------Calling Function ",fn); 516 | fn.call(null, doc, cb); 517 | } 518 | 519 | function afterBeforeUpdate(err) { 520 | if (err) return cb(err); 521 | 522 | var newdoc = docForIngestion(doc); 523 | //console.log('----------Heres our final doc',newdoc._id,newdoc._rev); 524 | console.trace(); 525 | 526 | db.insert(newdoc, id, saved); 527 | } 528 | 529 | function saved(err, reply) { 530 | if (err && err.status_code == 409) { 531 | //console.log('Calling merge again!'); 532 | adapter.merge(connectionName, collectionName, id, attrs, cb, attempts + 1); 533 | } 534 | else if (err) cb(err); 535 | else { 536 | extend(doc, { _rev: reply.rev, _id: reply.id }); 537 | doc = docForReply(doc); 538 | callbackAfter(); 539 | } 540 | } 541 | 542 | function callbackAfter() { 543 | async.eachSeries(coll._callbacks.afterUpdate || [], invokeCallback, finalCallback); 544 | } 545 | 546 | function finalCallback(err) { 547 | if (err) cb(err); 548 | else cb(null, doc); 549 | } 550 | 551 | }; 552 | 553 | 554 | 555 | /// View 556 | 557 | adapter.view = function view(connectionName, collectionName, viewName, options, cb, round) { 558 | collectionName = sanitizeCollectionName(collectionName); 559 | if ('number' != typeof round) round = 0; 560 | var db = registry.db(collectionName); 561 | 562 | db.view('views', viewName, options, viewResult); 563 | 564 | function viewResult(err, results) { 565 | if (err && err.status_code == 404 && round < 2) 566 | populateView(connectionName, collectionName, viewName, populatedView); 567 | else if (err) cb(err); 568 | else cb(null, (results && results.rows && results.rows || []).map(prop('value')).map(docForReply)); 569 | } 570 | 571 | function populatedView(err) { 572 | if (err) cb(err); 573 | else adapter.view(connectionName, collectionName, viewName, options, cb, round + 1); 574 | } 575 | }; 576 | 577 | function populateView(connectionName, collectionName, viewName, cb) { 578 | collectionName = sanitizeCollectionName(collectionName); 579 | var collection = registry.collection(collectionName); 580 | 581 | var view = collection.views && collection.views[viewName]; 582 | if (! view) return cb(new Error('No view named ' + viewName + ' defined in model ' + collectionName)); 583 | else { 584 | var db = registry.db(collectionName); 585 | db.get('_design/views', gotDDoc); 586 | } 587 | 588 | function gotDDoc(err, ddoc) { 589 | if (! ddoc) ddoc = {}; 590 | if (! ddoc.views) ddoc.views = {}; 591 | if (! ddoc._id) ddoc._id = '_design/views'; 592 | 593 | ddoc.views[viewName] = view; 594 | ddoc.language = 'javascript'; 595 | 596 | db.insert(ddoc, insertedDDoc); 597 | } 598 | 599 | function insertedDDoc(err) { 600 | cb(err); 601 | } 602 | } 603 | 604 | 605 | 606 | /// Utils 607 | 608 | 609 | function urlForConfig(config) { 610 | var schema = 'http'; 611 | if (config.https) schema += 's'; 612 | 613 | var auth = ''; 614 | if (config.username && config.password) { 615 | auth = encodeURIComponent(config.username) + ':' + encodeURIComponent(config.password) + '@'; 616 | } 617 | 618 | return [schema, '://', auth, config.host, ':', config.port, '/'].join(''); 619 | } 620 | 621 | function prop(p) { 622 | return function(o) { 623 | return o[p]; 624 | }; 625 | } 626 | 627 | function docForReply(doc) { 628 | if (doc._id) { 629 | doc.id = doc._id; 630 | delete doc._id; 631 | } 632 | if (doc._rev) { 633 | doc.rev = doc._rev; 634 | delete doc._rev; 635 | } 636 | 637 | return doc; 638 | } 639 | 640 | function docForIngestion(doc) { 641 | doc = extend({}, doc); 642 | if (doc.id) { 643 | doc._id = doc.id; 644 | delete doc.id; 645 | } 646 | if (doc.rev) { 647 | doc._rev = doc.rev; 648 | delete doc.rev; 649 | } 650 | 651 | return doc; 652 | } 653 | 654 | 655 | 656 | /** 657 | * Branch to the appropriate fn if the provided value is truthy. 658 | * 659 | * @param {*} valToTest 660 | * @param {Function} ifSoDo(next) 661 | * @param {Function} elseDo(next) 662 | * @param {Function} finallyDo(err, results) 663 | */ 664 | function asyncx_ifTruthy (valToTest, ifSoDo, elseDo, finallyDo){ 665 | return (valToTest ? ifSoDo : elseDo)(finallyDo); 666 | } 667 | 668 | function sanitizeCollectionName (collectionName) { 669 | return collectionName.replace(/([A-Z])/g, function (c) { 670 | return c.toLowerCase(); 671 | }); 672 | 673 | } 674 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-couchdb", 3 | "version": "0.10.3", 4 | "description": "CouchDB adapter for Sails", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha test/unit -t 10000 && node test/integration/runner -R spec -b" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/codeswarm/sails-couchdb-orm.git" 12 | }, 13 | "keywords": [ 14 | "couchdb", 15 | "couch", 16 | "adapter", 17 | "sails", 18 | "waterline", 19 | "orm", 20 | "sailsjs", 21 | "sails.js", 22 | "codeswarm" 23 | ], 24 | "author": [ 25 | "Pedro Teixeira ", 26 | "Mike Hostetler " 27 | ], 28 | "contributors": [ 29 | "Matheus Hoffmann Silva ", 30 | "Mike McNeil <@mikermcneil>" 31 | ], 32 | "license": "MIT", 33 | "readmeFilename": "README.md", 34 | "dependencies": { 35 | "nano": "~5.7.1", 36 | "xtend": "~2.1.2", 37 | "cookie": "~0.1.1", 38 | "handlebars": "~2.0.0-alpha.2", 39 | "deep-merge": "~0.3.1", 40 | "underscore": "^1.6.0", 41 | "async": "^0.7.0" 42 | }, 43 | "devDependencies": { 44 | "waterline-adapter-tests": "~0.10.0", 45 | "mocha": "*", 46 | "root-require": "~0.2.0", 47 | "captains-log": "~0.10.6" 48 | }, 49 | "sailsAdapter": { 50 | "sailsVersion": "~0.10.0", 51 | "implements": [ 52 | "semantic", 53 | "queryable" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /registry.js: -------------------------------------------------------------------------------- 1 | var connections = {}; 2 | var collections = {}; 3 | var dbs = {}; 4 | 5 | /// connections 6 | exports.connection = connection; 7 | 8 | function connection(name, connection) { 9 | if (! connection) return getConnection(name); 10 | else return setConnection(name, connection); 11 | } 12 | 13 | function setConnection(name, connection) { 14 | connections[name] = connection; 15 | } 16 | 17 | function getConnection(name) { 18 | return connections[name]; 19 | } 20 | 21 | /// collection 22 | 23 | exports.collection = collection; 24 | 25 | function collection(name, collection) { 26 | if (! collection) return getCollection(name); 27 | else return setCollection(name, collection); 28 | } 29 | 30 | function setCollection(name, collection) { 31 | collections[name] = collection; 32 | } 33 | 34 | function getCollection(name) { 35 | return collections[name]; 36 | } 37 | 38 | 39 | /// dbs 40 | 41 | exports.db = db; 42 | 43 | function db(name, db) { 44 | if (! db) return getDb(name); 45 | else return setDb(name, db); 46 | } 47 | 48 | function setDb(name, db) { 49 | dbs[name] = db; 50 | } 51 | 52 | function getDb(name) { 53 | return dbs[name]; 54 | } 55 | -------------------------------------------------------------------------------- /templates/view.map.template.js: -------------------------------------------------------------------------------- 1 | function(doc) { 2 | if ( 3 | {{#each attributes}} 4 | 'undefined' != typeof doc['{{.}}'] 5 | {{#unless @last}} && {{/unless}} 6 | {{/each}} 7 | ) { 8 | var keys = []; 9 | {{#each attributes}} 10 | if (! Array.isArray(doc['{{.}}'])) keys.push([doc['{{.}}']]); 11 | else keys.push(doc['{{.}}']); 12 | {{/each}} 13 | 14 | _emit(keys); 15 | } 16 | 17 | function _emit(values) { 18 | var value; 19 | var broke = false; 20 | for(var i = 0 ; i < values.length && ! broke; i ++) { 21 | value = values[i]; 22 | if (Array.isArray(value)) { 23 | value.forEach(function(value) { 24 | values[i] = value; 25 | _emit(values); 26 | }); 27 | broke = true 28 | } 29 | } 30 | if (! broke) emit(values, doc); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /test/integration/runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test runner dependencies 3 | */ 4 | var util = require('util'); 5 | var mocha = require('mocha'); 6 | var log = new (require('captains-log'))(); 7 | 8 | 9 | var TestRunner = require('waterline-adapter-tests'); 10 | var Adapter = require('../../'); 11 | 12 | 13 | 14 | // Grab targeted interfaces from this adapter's `package.json` file: 15 | var package = {}; 16 | var interfaces = []; 17 | try { 18 | package = require('root-require')('package.json'); 19 | interfaces = package['sailsAdapter'].implements; 20 | } 21 | catch (e) { 22 | throw new Error( 23 | '\n'+ 24 | 'Could not read supported interfaces from "sails-adapter"."interfaces"'+'\n' + 25 | 'in this adapter\'s `package.json` file ::' + '\n' + 26 | util.inspect(e) 27 | ); 28 | } 29 | 30 | 31 | 32 | 33 | 34 | log.info('Testing `' + package.name + '`, a Sails adapter.'); 35 | log.info('Running `waterline-adapter-tests` against ' + interfaces.length + ' interfaces...'); 36 | log.info('( ' + interfaces.join(', ') + ' )'); 37 | console.log(); 38 | log('Latest draft of Waterline adapter interface spec:'); 39 | log('https://github.com/balderdashy/sails-docs/blob/master/adapter-specification.md'); 40 | console.log(); 41 | 42 | 43 | 44 | 45 | /** 46 | * Integration Test Runner 47 | * 48 | * Uses the `waterline-adapter-tests` module to 49 | * run mocha tests against the specified interfaces 50 | * of the currently-implemented Waterline adapter API. 51 | */ 52 | new TestRunner({ 53 | 54 | // Load the adapter module. 55 | adapter: Adapter, 56 | 57 | // Default adapter config to use. 58 | config: { 59 | schema: false 60 | }, 61 | 62 | // The set of adapter interfaces to test against. 63 | // (grabbed these from this adapter's package.json file above) 64 | interfaces: interfaces 65 | 66 | // Most databases implement 'semantic' and 'queryable'. 67 | // 68 | // As of Sails/Waterline v0.10, the 'associations' interface 69 | // is also available. If you don't implement 'associations', 70 | // it will be polyfilled for you by Waterline core. The core 71 | // implementation will always be used for cross-adapter / cross-connection 72 | // joins. 73 | // 74 | // In future versions of Sails/Waterline, 'queryable' may be also 75 | // be polyfilled by core. 76 | // 77 | // These polyfilled implementations can usually be further optimized at the 78 | // adapter level, since most databases provide optimizations for internal 79 | // operations. 80 | // 81 | // Full interface reference: 82 | // https://github.com/balderdashy/sails-docs/blob/master/adapter-specification.md 83 | }); 84 | -------------------------------------------------------------------------------- /test/unit/README.md: -------------------------------------------------------------------------------- 1 | # Adapter Unit Tests 2 | 3 | `waterline-adapter-tests` provide a good layer of basic coverage for adapters. Since usage is standarized, tests are highly reusable. 4 | 5 | That said, if there is adapter-specific logic that you feel should be unit tested, this is the place to do it. 6 | -------------------------------------------------------------------------------- /test/unit/register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test dependencies 3 | */ 4 | var adapter = require('../../'); 5 | var registry = require('../../registry'); 6 | var assert = require('assert'); 7 | 8 | 9 | describe('registerConnection', function() { 10 | 11 | it('should not hang or encounter any errors', function(done) { 12 | adapter.registerConnection({ 13 | identity: 'public npm registry', 14 | adapter: { 15 | config: {} 16 | }, 17 | host: 'registry.npmjs.org', 18 | port: 80 19 | }, { 20 | registry: { 21 | identity: 'registry' 22 | } 23 | }, cb); 24 | 25 | function cb(err) { 26 | if (err) { 27 | return done(err); 28 | } 29 | 30 | done(); 31 | } 32 | 33 | }); 34 | 35 | 36 | // e.g. 37 | // it('should create a mysql connection pool', function () {}) 38 | // it('should create an HTTP connection pool', function () {}) 39 | // ... and so on. 40 | }); 41 | -------------------------------------------------------------------------------- /views.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var Handlebars = require('handlebars'); 3 | var extend = require('xtend'); 4 | 5 | var templates = { 6 | map: Handlebars.compile(fs.readFileSync(__dirname + '/templates/view.map.template.js', 'utf8')) 7 | } 8 | 9 | /// Name 10 | 11 | exports.name = viewName; 12 | 13 | function viewName(where) { 14 | return ['by'].concat(Object.keys(where).sort()).join('_'); 15 | }; 16 | 17 | 18 | /// Value 19 | 20 | exports.value = value; 21 | 22 | function value(options, isLike) { 23 | return Object.keys(options).sort().map(function(key) { 24 | return options[key]; 25 | }); 26 | } 27 | 28 | exports.likeValue = likeValue; 29 | 30 | function likeValue(options) { 31 | var startKey = []; 32 | var endKey = []; 33 | Object.keys(options).sort().forEach(function(key) { 34 | var value = options[key]; 35 | if ('string' != typeof value) throw new Error('like value must be a string'); 36 | if (value.charAt(value.length - 1) == '%') value = value.substring(0, value.length - 1); 37 | startKey.push(value); 38 | endKey.push(value + '\ufff0'); 39 | }); 40 | 41 | return { 42 | startkey: startKey, 43 | endkey: endKey 44 | }; 45 | } 46 | 47 | 48 | /// Create 49 | 50 | exports.create = createView; 51 | 52 | function createView(db, where, cb) { 53 | var attributes = Object.keys(where).sort().map(fixAttributeName); 54 | var map = templates.map({ 55 | attributes: attributes, 56 | attribute: attributes.length == 1 && attributes[0], 57 | singleAttribute: attributes.length == 1 58 | }); 59 | 60 | db.get('_design/views', gotDesignDoc); 61 | 62 | function gotDesignDoc(err, ddoc) { 63 | if (! ddoc) ddoc = {}; 64 | if (! ddoc.views) ddoc.views = {}; 65 | ddoc.views[viewName(where)] = { 66 | map: map 67 | }; 68 | 69 | //console.log('ABOUT TO INSERT DDOC', ddoc); 70 | 71 | db.insert(ddoc, '_design/views', cb); 72 | } 73 | } 74 | 75 | function fixAttributeName(attrName) { 76 | if (attrName == 'id') attrName = '_id'; 77 | return attrName; 78 | } 79 | --------------------------------------------------------------------------------