Skip to content

Commit

Permalink
Merge pull request #8 from dominictarr/master
Browse files Browse the repository at this point in the history
support sublevel and plugin extensions
  • Loading branch information
juliangruber committed Apr 21, 2013
2 parents 29fef99 + c6ff5c4 commit a7cba5f
Show file tree
Hide file tree
Showing 16 changed files with 585 additions and 129 deletions.
127 changes: 117 additions & 10 deletions README.md
Expand Up @@ -33,17 +33,102 @@ db.pipe(net.connect(3000)).pipe(db)
// asynchronous methods
db.get('foo', function () { /* */ })

// synchronous methods (see API below)
db.isOpen(function (err, isOpen) { /* */ })
var isOpen = db.isOpen()

// events
db.on('put', function () { /* */ })

// streams
db.createReadStream().on('data', function () { /* */ })
```

## sublevel plugins

You can also expose custom methods and [sublevels](https://github.com/dominictarr/level-sublevel)
with `multilevel`!

When using sublevels, you must generate a manifest, and require it in the client.
``` js
//server.js
var db = require('./setup-db') //all your database customizations
var fs = require('fs')
var createManifest = require('level-manifest')

//write out manifest
fs.writeFileSync('./manifest.json', JSON.stringify(createManifest(db)))

shoe(function (stream) {
stream.pipe(multilevel.server(db)).pipe(stream)
})
...
```
Then, the manifest is required from the client when bundling with browserify.

``` js
//client.js
var manifest = require('./manifest.json')
var stream = shoe()
var db = multilevel.client(manifest)
stream.pipe(db).pipe(stream)
//now, get remote access to your extensions!
db.sublevel('foo').createLiveStream()
```

## auth

You do not want to expose every database feature to every user,
yet, you may want to provide some read-only access, or something.

Auth controls may be injected when creating the server stream.

Allow read only access, unless logged in as root.
``` js
//server.js
var db = require('./setup-db') //all your database customizations
var fs = require('fs')
var createManifest = require('level-manifest')

//write out manifest
fs.writeFileSync('./manifest.json', JSON.stringify(createManifest(db)))

shoe(function (stream) {
stream.pipe(multilevel.server(db, {
auth: function (user, cb) {
if(user.name == 'root' && user.pass == 'toor') {
//the data returned will be attached to the mulilevel stream
//and passed to `access`
cb(null, {name: 'root'})
} else
cb(new Error('not authorized')
},
access: function (user, db, method, args) {
//`user` is the {name: 'root'} object that `auth`
//returned.

//if not a privliged user...
if(!user || user.name !== 'root') {
//do not allow any write access
if(/^put|^del|^batch|write/i.test(method))
throw new Error('read-only access')
}
})
})).pipe(stream)
})
...
```
The client authorizes by calling the auth method.
``` js
var stream = shoe()
var db = multilevel.client()
stream.pipe(db).pipe(stream)

db.auth({name: 'root', pass: 'toor'}, function (err, data) {
if(err) throw err
//later, they can sign out, too.

db.deauth(function (err) {
//signed out!
})
})
```
## API
The exposed DB has the exact same API as
Expand All @@ -55,13 +140,35 @@ since they work synchronouly in levelUp. You can use the return value of
If that's not acceptable for you, `db#isOpen(cb)` and `db#isClosed(cb)` will always
call `cb` with the correct result.
### multilevel.server(db)
### multilevel.server(db, authOpts?)
Returns a server-stream that exposes `db`, an instance of levelUp.

### var db = multilevel.client()
`authOpts` is optional, it should match this:
``` js
var authOpts = {
auth: function (userData, cb) {
//call back an error, if the user is not authorized.

},
access: function (userData, db, method, args) {
//throw if this user is not authorized for this action.
}
}
```
### var db = multilevel.client(manifest?)
Returns a `db` that is to be piped into a server-stream.
`manifest` may be optionally be provided,
which will allow client access to extensions.
#### db.auth(data, cb)
Authorize with the server.
#### db.deauth (cb)
Deauthorize with the server.
## Installation
Expand Down
2 changes: 1 addition & 1 deletion index.js
@@ -1,4 +1,4 @@
module.exports = {
client : require('./lib/client'),
server : require('./lib/server')
}
}
139 changes: 75 additions & 64 deletions lib/client.js
Expand Up @@ -4,97 +4,108 @@

var MuxDemux = require('mux-demux')
var rpc = require('rpc-stream')
var emitStream = require('emit-stream')
var pauseStream = require('pause-stream')
//var emitStream = require('emit-stream')
var EventEmitter = require('events').EventEmitter
var methodNames = require('./method_names')
var duplexer = require('duplexer')
var manifest = require('level-manifest')

module.exports = function () {
module.exports = function (db) {

var m = manifest(db || {methods: {}}) //fill in the missing bits.

/**
* use MuxDemux transparently
*/

var mdm = MuxDemux()
var db = duplexer(mdm, mdm)

var db = duplexer(mdm, mdm) //??

db.isClient = true
/**
* rpc
*/

var client = db.rpc = rpc(null, true)
var remote = client.wrap(methodNames('sync', 'async', 'stream'))

// wait for the connection to be up
var ps = pauseStream().pause()
client.pipe(ps)
client.pipe(mdm.createStream('rpc')).pipe(client)

/**
* keep isOpen and isClosed up2date
*/
*/
/*
// var isOpen = true
// db.on('connection', function (c) {
// if (c.meta != 'rpc') return
// remote.isOpen(function (_isOpen) {
// isOpen = _isOpen
// })
// })
// db.on('open', function () { isOpen = true })
// db.on('close', function () { isOpen = false })
*/

var isOpen = true
db.on('connection', function (c) {
if (c.meta != 'rpc') return
remote.isOpen(function (_isOpen) {
isOpen = _isOpen
})
})
db.on('open', function () { isOpen = true })
db.on('close', function () { isOpen = false })

/**
* rpc methods
* rpc methods & streams
*/

methodNames('async', 'sync').forEach(function (method) {
db[method] = function (/* args */) {
var args = [].slice.call(arguments)
if (typeof args[args.length - 1] != 'function') {
args.push(function () { /* noop */ })
}
remote[method].apply(remote, args)

var api = db

;(function buildAll (db, api, path, parent) {
var m = manifest(db)
for (var k in m.methods) {
;(function (k) {
var method = m.methods[k]
var name = path.concat(k).join('!')
if(/async|sync/.test(method.type)) {
api[k] = client.createRemoteCall(name)
}
else if(method.type == 'error')
throw new Error(method.message || 'not supported')
else {
api[k] = function () {
var args = [].slice.call(arguments)
args.unshift(name)
var ts = (
method.type === 'readable'
? mdm.createReadStream(args)
: method.type == 'writable'
? mdm.createWriteStream(args)
: method.type == 'duplex'
? mdm.createStream(args)
: (function () { throw new Error('not supported') })()
)
ts.autoDestroy = false
return ts
}
}
})(k)
}

if (method == 'isOpen' || method == 'isClosed') {
var old = db[method]
db[method] = function (cb) {
if (cb) return old(cb)
return method == 'isOpen'
? isOpen
: !isOpen
}
api._sep = db._sep
api._prefix = db._prefix
api._parent = parent

api.prefix = function (key) {
if(!this._parent) return '' + (key || '')
return (this._parent.prefix()
+ this._sep + this._prefix + this._sep + (key || ''))
}
})

/**
* methods returning streams
*/

methodNames('stream').forEach(function (method) {
db[method] = function () {
var args = [].slice.call(arguments)
args.unshift(method)
return mdm.createStream(args)

api.sublevel = function (name) {
if(api.sublevels[name])
return api.sublevels[name]
throw new Error('client cannot create new sublevels')
}
})

/**
* connection logic
*/

mdm.on('connection', function (c) {
if (c.meta == 'events') {
emitStream(c).emit = db.emit.bind(db)
} else if (c.meta == 'rpc') {
ps.pipe(c).pipe(client)
ps.resume()
c.on('end', function () {
ps.pause()
})
for(var name in db.sublevels) {
api.sublevels = api.sublevels || {}
api.sublevels[name] = {}
buildAll(db.sublevels[name], api.sublevels[name], path.concat(name), api)
}
})
})(m, api, [], null)

api.auth = client.createRemoteCall('auth')
api.deauth = client.createRemoteCall('deauth')

return db
}
9 changes: 9 additions & 0 deletions lib/for-each-branch.js
@@ -0,0 +1,9 @@

module.exports = function (obj, iter) {
;(function recurse(obj, path){
for(var k in obj.methods) {
iter(obj[k], path.concat(k))
}
recurse
}(obj, [])
}

0 comments on commit a7cba5f

Please sign in to comment.