Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support sublevel and plugin extensions #8

Merged
merged 29 commits into from
Apr 21, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e3a2f77
update deps
dominictarr Apr 13, 2013
230ea78
support sublevels, and plugin manifests
dominictarr Apr 13, 2013
c17c46a
must pass in manifest
dominictarr Apr 13, 2013
f32709d
update deps
dominictarr Apr 13, 2013
af49dfd
use close to detect when stream has finished
dominictarr Apr 13, 2013
b2ad1a5
use new mux-demux that uses new through that supports autoDestroy=false
dominictarr Apr 13, 2013
6825f65
tidy
dominictarr Apr 13, 2013
27be8f4
add test for sublevels
dominictarr Apr 13, 2013
dfaf56c
tidy
dominictarr Apr 13, 2013
0e06c2c
update deps
dominictarr Apr 13, 2013
76593eb
add test for extending multilevel for plugins!
dominictarr Apr 13, 2013
2cc6bbe
tidy
dominictarr Apr 13, 2013
1d481a4
update dev deps
dominictarr Apr 13, 2013
040c682
test with a real plugin: live-stream
dominictarr Apr 13, 2013
0435593
give client db.sublevel(name) api
dominictarr Apr 14, 2013
40dff89
test for remote batch operations
dominictarr Apr 16, 2013
52400af
tidy
dominictarr Apr 16, 2013
df9b1ca
update deps
dominictarr Apr 16, 2013
8693bd3
allow remote batch ops
dominictarr Apr 16, 2013
dd80313
test isClient
dominictarr Apr 16, 2013
a44ac3e
expose whether this is a client, or a real database
dominictarr Apr 16, 2013
982e85b
use new rpc-stream
dominictarr Apr 19, 2013
46a3d89
update to new rpc-stream api
dominictarr Apr 19, 2013
f032012
allow setting auth opts in test setup0
dominictarr Apr 19, 2013
5ed13ba
test for auth
dominictarr Apr 19, 2013
e0a7c0b
implement auth/deauth
dominictarr Apr 19, 2013
45d4a8b
test that auth can return data to user
dominictarr Apr 20, 2013
f01f40e
allow auth to respond
dominictarr Apr 20, 2013
c6ff5c4
document new features
dominictarr Apr 21, 2013
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 117 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,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 @@ -57,13 +142,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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
client : require('./lib/client'),
server : require('./lib/server')
}
}
139 changes: 75 additions & 64 deletions lib/client.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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, [])
}
Loading