diff --git a/.coveralls b/.coveralls new file mode 100644 index 0000000..ad493c9 --- /dev/null +++ b/.coveralls @@ -0,0 +1,2 @@ +service_name: travis-ci +repo_token: nzlZHxRa4UAvp2QBBovUOS82u9nR5tPTT \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c3629e..e1fb190 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,57 @@ +# node + +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log node_modules + + +# osx + +.DS_Store +.AppleDouble +.LSOverride +Icon + + +## Thumbnails +._* + +## Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# textmate + +*.tmproj +*.tmproject +tmtags + + +# vim + +*.s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + + +# subl + +*.sublime-workspace + +# component +components +build \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..b021673 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,50 @@ +{ + "bitwise": true, + "camelcase": false, + "curly": false, + "eqeqeq": true, + "es3": false, + "forin": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "noarg": true, + "noempty": true, + "nonew": true, + "plusplus": true, + "quotmark": true, + "undef": true, + "unused": true, + "strict": false, + "trailing": true, + "asi": false, + "boss": false, + "debug": true, + "eqnull": false, + "esnext": false, + "evil": false, + "expr": false, + "funcscope": false, + "globalstrict": false, + "iterator": false, + "lastsemic": true, + "laxbreak": false, + "laxcomma": false, + "loopfunc": false, + "moz": false, + "multistr": false, + "proto": false, + "scripturl": false, + "smarttabs": false, + "shadow": false, + "sub": false, + "supernew": false, + "validthis": false, + "browser": false, + "devel": false, + "jquery": false, + "devel": false, + "es5": true, + "node": true +} \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index f1250e5..0000000 --- a/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -support -test -examples -*.sock diff --git a/.tern-project b/.tern-project new file mode 100644 index 0000000..22d0529 --- /dev/null +++ b/.tern-project @@ -0,0 +1,7 @@ +{ + "libs": ["ecma5"], + "plugins": { + "doc_comment": true, + "node": {} + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2ca91f2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "0.10" + - "0.8" \ No newline at end of file diff --git a/History.md b/History.md deleted file mode 100644 index a821ef7..0000000 --- a/History.md +++ /dev/null @@ -1,26 +0,0 @@ - -0.0.5 / 2013-11-15 -================== - - * updated to work with modella 0.2.0 - * updated level to work with node 0.11.8 - -0.0.4 / 2013-09-14 -================== - - * automatically close on termination - -0.0.3 / 2013-08-04 -================== - - * bump level version - -0.0.2 / 2013-07-30 -================== - - * removed log - -0.0.1 / 2013-07-23 -================== - - * Initial commit diff --git a/Makefile b/Makefile index 4e9c8d3..d5dd8af 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,22 @@ +REPORTER = spec +UI = bdd test: - @./node_modules/.bin/mocha \ - --require should \ - --reporter spec + @NODE_ENV=test ./node_modules/.bin/mocha -u $(UI) -R $(REPORTER) -c -G -b + +lib-cov: + ./node_modules/jscoverage/bin/jscoverage src lib-cov + +test-cov: lib-cov + @LEVEL_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html + rm -rf lib-cov + +test-coveralls: lib-cov + echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) + @LEVEL_COV=1 $(MAKE) test REPORTER=mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js + rm -rf lib-cov + +clean: + @rm -rf node_modules components build .PHONY: test \ No newline at end of file diff --git a/Readme.md b/Readme.md deleted file mode 100644 index 117323c..0000000 --- a/Readme.md +++ /dev/null @@ -1,66 +0,0 @@ -# LevelDB - -[LevelDB](https://code.google.com/p/leveldb/) plugin for [modella](https://github.com/modella/modella). - -## Installation - - npm install modella-leveldb - -## Example - -```js -var model = require('modella'); -var level = require('modella-leveldb')('./mydb'); -var uid = require('uid'); - -var User = model('user') - .attr('id') - .attr('name') - .attr('email') - .attr('password'); - -User.use(level); - -/** - * Initialize - */ - -var user = new User; - -user.id(uid(6)) - .name('matt') - .email('mattmuelle@gmail.com') - .password('test'); - -user.save(function(err) { - console.log(user.toJSON()); -}); -``` - -## API - -### Level(path, options) - -Initialize leveldb with a `path` to the database. If path doesn't exist, it will be created. `options` will be passed through to [levelup](https://github.com/rvagg/node-levelup) - -### Model.all([options], fn) - -Get all models (static method) - -### Model.find(id, [options], fn) - -Find a model (static method) - -### model.save([options], fn) - -Save the model (instance method) - -### model.remove([options], fn) - -Remove the model (instance method) - -All `options` will be passed through to [levelup](https://github.com/rvagg/node-levelup). - -## License - -MIT diff --git a/history.md b/history.md new file mode 100644 index 0000000..5d7ee3e --- /dev/null +++ b/history.md @@ -0,0 +1,38 @@ +# 0.3.1 / 2013-12-03 + + * update dependencies + +# 0.3.0 / 2013-12-03 + + * add `Model.remove.all` + * rename `Model.all` to `Model.get.all` + +# 0.2.0 / 2013-12-02 + + * also accept a string and create a level instance in that case + * add a streaming `.all` + +# 0.1.0 / 2013-12-01 + + * receive a level instance instead of creating it + +# 0.0.5 / 2013-11-15 + + * updated to work with modella 0.2.0 + * updated level to work with node 0.11.8 + +# 0.0.4 / 2013-09-14 + + * automatically close on termination + +# 0.0.3 / 2013-08-04 + + * bump level version + +# 0.0.2 / 2013-07-30 + + * removed log + +# 0.0.1 / 2013-07-23 + + * Initial commit \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 6c82d84..0000000 --- a/index.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Module dependencies - */ - -var debug = require('debug')('modella:leveldb'); -var level = require('level'); -var sync = {}; - -/** - * Export `LevelDB` - */ - -module.exports = function(path, options) { - options = options || {}; - options.valueEncoding = options.valueEncoding || 'json'; - - var db = level(path, options); - - // automatically cleanup on termination - process.on('SIGTERM', db.close.bind(this)); - - return function(model) { - model.db = db; - for (fn in sync) model[fn] = sync[fn]; - }; -}; - -/** - * All - */ - -sync.all = function(options, fn) { - if (arguments.length == 1) { - fn = options; - options = {}; - } - - debug('getting all data with options %j', options); - - var rs = this.db.createReadStream(options); - var buffer = []; - - rs.on('error', function(err) { - return fn(err); - }); - - rs.on('data', function(data) { - buffer[buffer.length] = data.value; - }); - - rs.on('end', function() { - return fn(null, buffer); - }); -}; - -/** - * Get - */ - -sync.get = function(key, options, fn) { - var db = this.db; - - if(arguments.length == 2) { - fn = options; - options = {}; - } - - debug('getting %j with %j options...', key, options); - db.get(key, options, function(err, model) { - if(err) return fn(err); - else if(!model) return fn(null, false); - debug('got %j', model); - return fn(null, model); - }); -}; - -/** - * removeAll - */ - -sync.removeAll = function(query, fn) { - throw new Error('model.removeAll not implemented'); -}; - -/** - * save - */ - -sync.save = -sync.update = function(options, fn) { - if (1 == arguments.length) { - fn = options; - options = {}; - } - - var json = this.toJSON(); - debug('saving... %j', json); - var id = this.primary(); - this.model.db.put(id, json, function(err) { - if(err) return fn(err); - debug('saved %j', json); - return fn(null, json); - }); -}; - -/** - * remove - */ - -sync.remove = function(options, fn) { - if (1 == arguments.length) { - fn = options; - options = {}; - } - - var db = this.model.db; - var id = this.primary(); - debug('removing %s with options %j', id, options); - db.del(id, options, function(err) { - if(err) return fn(err); - debug('removed %s', id); - return fn(); - }); -}; diff --git a/package.json b/package.json index e2617e6..8fb513c 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,50 @@ { - "name": "modella-leveldb", - "version": "0.0.5", - "description": "modella plugin for leveldb", - "keywords": [], - "author": "matt mueller ", + "name": "level-modella", + "version": "0.6.0", + "description": "modella plugin for level", + "keywords": [ + "level", + "modella" + ], + "homepage": "https://github.com/modella/level-modella", + "bugs": "http://github.com/modella/level-modella/issues", + "license": "MIT", + "author": "Matt Mueller (http://mat.io)", + "contributors": [ + "Sérgio Ramos (http://sergioramos.me/)" + ], + "main": "src/level-modella.js", + "directories": { + "lib": "src" + }, + "repository": { + "type": "git", + "url": "git://github.com/modella/level-modella.git" + }, + "scripts": { + "test": "make test test-coveralls" + }, "dependencies": { - "level": "~0.17.0", - "debug": "~0.7.2" + "debug": "0.7.x", + "type-component": "0.0.1", + "xtend": "2.1.x", + "map-series": "0.0.x", + "ordered-through": "0.0.x", + "level": "0.18.x", + "level-cursor": "0.5.x" }, "devDependencies": { + "mocha-lcov-reporter": "0.0.x", + "jscoverage": "0.3.x", + "coveralls": "2.5.x", + "mocha": "1.15.x", "modella": "*", - "mocha": "*", - "should": "*", - "pwd": "*", - "uid": "0.0.2" + "pwd": "0.0.x", + "uid": "0.0.x", + "leveldown": "0.10.x", + "mkdirp": "0.3.x" }, - "main": "index" -} + "engines": { + "node": ">=0.8" + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2e89a2c --- /dev/null +++ b/readme.md @@ -0,0 +1,87 @@ +# level-modella + +[![NPM version](https://badge.fury.io/js/level-modella.png)](http://badge.fury.io/js/level-modella) +[![Build Status](https://secure.travis-ci.org/modella/level-modella.png)](http://travis-ci.org/modella/level-modella) +[![Dependency Status](https://gemnasium.com/modella/level-modella.png)](https://gemnasium.com/modella/level-modella) +[![Coverage Status](https://coveralls.io/repos/modella/level-modella/badge.png?branch=master)](https://coveralls.io/r/modella/level-modella?branch=master) +[![Code Climate](https://codeclimate.com/github/modella/level-modella.png)](https://codeclimate.com/github/modella/level-modella) + +[Level](https://github.com/level/level) plugin for [modella](https://github.com/modella/modella). + +## install + +```bash +npm install [--save/--save-dev] level-modella +``` + +## example + +```js +var model = require('modella'); +var store = require('level-modella'); +var level = require('level')('/tmp/level'); +var sublevel = require('sublevel'); +var uid = require('uid'); + +var users_store = sublevel(level, 'users'); + +var User = model('user'); + +User.attr('id'); +User.attr('name'); +User.attr('email'); +User.attr('password'); + +User.use(store(users_store)); + +/** + * Initialize + */ + +var user = new User; + +user.id(uid(6)) + .name('matt') + .email('mattmuelle@gmail.com') + .password('test'); + +user.save(function(err) { + console.log(user.toJSON()); +}); +``` + +## api + +### store(db) + +Use the plugin. + +### model.save([options,] fn) +### model.put([options,] fn) + +Save a model. + +### model.del([options,] fn) +### model.remove([options,] fn) + +Remove a model. + +### Model.get(key, [options,] fn) + +Get a model. The object passes to the callback is a `Model` instance. + +### Model.get.all([options]) + +Returns a stream that will emit a model instance on each `data` event. Accepted options are the [same](https://github.com/rvagg/node-levelup/#dbcreatereadstreamoptions) that levelup accepts. + +### Model.remove.all([options,] fn) + +Removes all models. + +### Model.db + +`level` instance + +## license + +MIT \ No newline at end of file diff --git a/src/level-modella.js b/src/level-modella.js new file mode 100644 index 0000000..2bc77e1 --- /dev/null +++ b/src/level-modella.js @@ -0,0 +1,249 @@ +/** + * Module dependencies + */ +var debug = require('debug')('modella:level'), + type = require('type-component'), + xtend = require('xtend'), + through = require('ordered-through'), + series = require('map-series'), + cursor = require('level-cursor'), + level = require('level'); + +// holds all the dbs, so that it can close them on process SIGTERM +var dbs = []; +// default options +var default_options = { + valueEncoding: 'json', + keyEncoding: 'utf8' +}; + +/* Parses options/fn so that it handles situations where no options + * are passed to the function + * + * @param {object} options + * @param {function} [fn] + * @return {function} callback + * @api private + */ +function get_callback(options, fn) { + return type(options) === 'function' ? options : fn; +} + +// automatically cleanup on termination +function close_all(done) { + series(dbs, function(db, fn) { + db.close(function(err) { + if(err) console.error(err); + fn(); + }); + }, function() { + if(done) done(); + }); +} + +process.on('SIGTERM', close_all); + +/* level_modella costructor + * + * @param {object} model + * @param {object} db + * @api private + */ +var level_modella = function (model) { + if(!(this instanceof level_modella)) return new level_modella(model); + var self = this + + model.get = function () { + return self.get.apply(this, arguments); + }; + + model.get.all = function () { + return self.getAll.apply(self, arguments); + }; + + model.del = model.remove = function () { + return self.del.apply(this, arguments); + }; + + model.del.all = model.remove.all = function () { + return self.delAll.apply(self, arguments); + }; + + model.all = model.get.all + model.put = model.save = model.update = self.put; + self.model = model; +}; + +/* save a model into the store + * + * ```javascript + * user.save(function(err) {}) + * ``` + * + * @param {object} [options] + * @param {function} fn + * @api public + */ +level_modella.prototype.put = function(options, fn) { + fn = get_callback(options, fn); + options = xtend(default_options, options); + + if (type(fn) !== 'function') + return this.emit('error', new Error('put() requires a callback argument')); + + var value = this.toJSON(); + var key = this.primary(); + debug('put: %s -> %j', key, value); + + this.model.db.put(key, value, options, function(err) { + if (err) return fn(err); + + debug('success put: %s -> %j', key, value); + fn(err, value); + }); +}; + +/* gets a model from the store + * + * ```javascript + * User.get(1, function(err, user) {}) + * ``` + * + * @param {any} key + * @param {object} [options] + * @param {function} fn + * @api public + */ +level_modella.prototype.get = function(key, options, fn) { + fn = get_callback(options, fn); + options = xtend(default_options, options); + var self = this; + + if (type(fn) !== 'function') + return self.emit('error', new Error('get() requires key and callback arguments')); + + debug('get: %s', key); + + self.db.get(key, options, function(err, value) { + if (err) return fn(err); + if (options.raw) return fn(err, value); + + debug('success get: %s -> %j', key, value); + fn(null, self(value)); + }); +}; + +/* get all models from the store + * + * ```javascript + * var cursor = require('level-cursor') + * cursor(User.get.all()).each(function (user) {}, function (err) {}) + * ``` + * + * @param {object} [options] + * @api public + */ +level_modella.prototype.getAll = function(options) { + options = xtend(default_options, options); + var self = this; + var db = self.model.db; + + debug('all'); + + if (options.raw && !options.keys) { + return db.createValueStream(options); + } + + if (options.raw && options.keys) { + return db.createKeyStream(options); + } + + return db.createReadStream(options).pipe(through(function(data, fn) { + debug('success get: %s -> %j', data.key, data.value); + fn(null, self.model(data.value)); + })); +}; + +/* removes a model from the store + * + * ```javascript + * user.remove(function(err) {}) + * ``` + * + * @param {object} [options] + * @param {function} fn + * @api public + */ +level_modella.prototype.del = function(options, fn) { + fn = get_callback(options, fn); + options = xtend(default_options, options); + + if (type(fn) !== 'function') + return this.emit('error', new Error('remove() requires a callback argument')); + + var key = this.primary(); + debug('remove: %s', key); + + this.model.db.del(key, options, function(err) { + if (err) return fn(err); + + debug('success remove: %s', key); + fn(err); + }); +}; + +/* get all models from the store + * + * ```javascript + * User.remove.all(function (err) {}) + * ``` + * + * @param {object} [options] + * @api public + */ +level_modella.prototype.delAll = function(options, fn) { + fn = get_callback(options, fn); + options = xtend(default_options, options); + var self = this; + + debug('remove.all'); + + cursor(self.model.db.createKeyStream(options).pipe(through(function(key, fn) { + self.model.db.del(key, options, fn); + }))).all(fn); +}; + +/* exports a function to be passed to `Model.use` + * + * ```javascript + * var level_modella = require('level-modella') + * var modella = require('modella') + * var level = require('level') + * + * User = modella('User'); + * + * User.use(level_modella(level)) + * ``` + * + * @param {object} db + * @return {fn} + * @api public + */ +module.exports = function(db){ + if(type(db) === 'string') + db = level(db); + + dbs.push(db); + + return function(model) { + model.db = db; + level_modella(model); + return model; + }; +}; + +/* closes all db instances + * + * @api private + */ +module.exports.__close_all = close_all; \ No newline at end of file diff --git a/test/test.js b/test/test.js index 7dc3825..9a4fd3c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,47 +1,234 @@ -/** - * Module Dependencies - */ - +var store = process.env.LEVEL_COV ? require('../lib-cov/level-modella') : require('../'); var model = require('modella'); -var level = require('../')(__dirname + '/mydb'); +var level = require('level'); var uid = require('uid'); +var mkdirp = require('mkdirp'); +var type = require('type-component'); +var leveldown = require('leveldown'); +var assert = require('assert'); +var cursor = require('level-cursor'); +var path = require('path'); + +var User, location = path.join(__dirname, 'db'), user = { + id: uid(), + name: 'seth cohen' +}; + +var close = function(done) { + if(type(User.db) === 'undefined') return done(); + + User.db.close(function() { + leveldown.destroy(location, done); + }); +}; + +var use = function(done) { + User.use(store(level(location, done))); +}; + +beforeEach(function(done) { + mkdirp(location, done); +}); + +beforeEach(function() { + User = model('User'); + User.attr('id'); + User.attr('name'); +}); + +afterEach(close); + +describe('store', function() { + afterEach(close); + + it('store() accept a string', function() { + User.use(store(location)); + assert(User.db); + assert(User.db.location === location); + }); + + it('store() should return a fn', function() { + var db = level(location); + var adapter = store(db); + assert(type(adapter) === 'function'); + User = {db: db} // make sure it's cleaned + }); + + it('should fill static methods', function() { + User.use(store(level(location))); + assert(type(User.get) === 'function'); + assert(type(User.save) === 'function'); + assert(type(User.remove) === 'function'); + }); + + it('close all', function(done) { + User.use(store(level(location))); + store.__close_all(done); + }); +}); + +describe('put', function() { + beforeEach(use); + afterEach(close); + + it('should save', function(done) { + var model = User(user); + + model.save(function(err, model) { + if(err) return done(err); + + model.model.db.get(model.id(), { + valueEncoding: 'json' + }, function(err, value) { + if(err) return done(err); + + assert(value.id === model.id()); + assert(value.name === model.name()); + + done() + }); + }); + }); + + it('should emit when no callback is passed', function(done) { + var model = User(user); + + model.once('error', function(err) { + assert(err && err.message === 'put() requires a callback argument'); + done(); + }); + + store(model.model.db)(User).save.call(model); + }); +}); -var User = model('user') - .attr('id') - .attr('name') - .attr('email') - .attr('password'); +describe('remove', function() { + beforeEach(use); + afterEach(close); -User.use(level); + it('should remove', function(done) { + var model = User(user); -/** - * Initialize - */ + model.save(function(err, model) { + if(err) return done(err); -var user = new User; + model.remove(function(err) { + if(err) return done(err); -user.id(uid(6)) - .name('matt') - .email('mattmuelle@gmail.com') - .password('test'); + model.model.db.get(model.id(), function(err) { + assert(err && err.type === 'NotFoundError') + done(); + }); + }); + }); + }); -// user.save(function(err, user) { -// console.log(user); -// }); + it('should emit when no callback is passed', function(done) { + var model = User(user); -User.all(function(err, users) { - console.log(users); + model.once('error', function(err) { + assert(err && err.message === 'remove() requires a callback argument'); + done(); + }); + + store(model.model.db)(User).remove.call(model); + }); + + it('should remove all', function(done) { + var model = User(user); + + model.save(function(err, model) { + if(err) return done(err); + + User.remove.all(function(err){ + if(err) return done(err); + + cursor(User.get.all()).all(function(err, users){ + if(err) return done(err); + assert(users.length === 0); + done(); + }); + }); + }); + }); +}); + +describe('raw', function() { + beforeEach(use); + afterEach(close); + + it('get', function(done) { + var model = User(user); + + model.save(function(err, model) { + if(err) return done(err); + + User.get(model.id(), {raw: true}, function(err, value) { + if(err) return done(err); + + assert(value.id === model.id()); + assert(value.name === model.name()); + + done(); + }); + }); + }); + + it('get all', function(done) { + var model = User(user); + + model.save(function(err, model) { + if(err) return done(err); + + cursor(User.get.all({raw: true})).all(function(err, users){ + assert(users[0].id === model.primary()); + done(err); + }); + }); + }); }); -// User.find('ewcbix', function(err, user) { -// if (err) throw err; -// console.log(user); -// }); - -// User.find('ewcbix', function(err, user) { -// if (err) throw err; -// user.remove(function(err) { -// if (err) throw err; -// console.log('removed'); -// }); -// }); + +describe('get', function() { + beforeEach(use); + afterEach(close); + + it('should get', function(done) { + var model = User(user); + + model.save(function(err, model) { + if(err) return done(err); + + User.get(model.id(), function(err, value) { + if(err) return done(err); + + assert(value.id() === model.id()); + assert(value.name() === model.name()); + + done(); + }); + }); + }); + + it('should emit when no callback/key is passed', function(done) { + User.once('error', function(err) { + assert(err && err.message === 'get() requires key and callback arguments'); + done(); + }); + + User.get(); + }); + + it('should get all', function(done) { + var model = User(user); + + model.save(function(err, model) { + if(err) return done(err); + + cursor(User.get.all()).all(function(err, users){ + assert(users[0].primary() === model.primary()); + done(err); + }); + }); + }); +}); \ No newline at end of file