Package Exports
- vas
- vas/lib/is
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (vas) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
vas
🌱 composable client/server data services using pull streams
features
- API is a data structure: easy to understand and simple to extend
- functional: services are just objects and functions, no magic
- fractal: compose one API from many smaller APIs
- database-agnostic: create API services on top of anything
- hookable: hook before, after, and around methods
- adaptable: use adapters to transport over http, websockets
- omakse: consistent flavoring with pull streams all the way down
for a user interface complement, see inu
demos
TODO
if you want to share anything using vas
, add your thing here!
example
const vas = require('vas')
const pull = require('pull-stream')
const values = require('object-values')
const data = {
1: 'human',
2: 'computer',
3: 'JavaScript'
}
const things = {
path: ['things'],
manifest: {
all: 'source',
get: 'async'
},
methods: {
all: function () {
const things = values(data)
return pull.values(things)
},
get: function ({ id }, cb) {
cb(null, data[id])
}
}
}
const api = vas.create(vas.Service, [things])
api.things.get(1, (err, value) => {
if (err) throw err
console.log('get', value)
// get human
})
pull(
api.things.all(),
pull.drain(v => console.log('all', v))
)
// all human
// all computer
// all JavaScript
concepts
let's say we're writing a todo app (so lame right).
we want to be able to get all the todo items, update a todo item, and add another one.
if we think of these methods as functions, it might look like this (using knex):
const toPull = require('stream-to-pull-stream')
const Db = require('knex')
const db = Db({
client: 'sqlite3',
connection: {
filename: './mydb.sqlite'
}
})
const methods = {
getAll,
update,
add
}
function getAll () {
return toPull(db('todos').select().stream())
}
function update (nextTodo, cb) {
db('todos')
.where('id', nextTodo.id)
.update(nextTodo)
.asCallback(cb)
}
function add (todo, cb) {
db('todos').insert(todo).asCallback(cb)
}
what if we could call these functions directly from the front-end?
to do so, we need to specify which functions are available and of what type they are, which is called a manifest.
const manifest = {
getAll: 'source',
update: 'async',
add: 'async'
}
where 'source' corresponds to a source pull stream and 'async' corresponds to a function that receives an error-first callback.
this manifest provides us with enough information to construct a mirrored function on the client:
pull(
getAll(),
pull.log()
)
together, this could become a service, complete with a name and version:
const service = {
path: ['todos'],
version: '1.0.0',
manifest,
methods
}
// TODO hooks
combine these concepts together and welcome to vas
. :)
usage
a vas
service definition is defined by an object with the following keys:
name
: a string nameversion
(optional): a string semantic versionmanifest
: a manifest object mapping method names to strings representing the method type (sync
,async
,source
, orsink
)methods
: method functions.hooks
: hooks which correspond to methods. each hook is an tuple of shape[type, fn]
, wheretype
is eitheraround
,before
, orafter
andfn
is an asynchronous function that accepts the same arguments as the method (and an additional callback if the method is notasync
).adapter
: object where keys are names of adapters and values are options objects per method.
a vas
service is defined by an object with the following keys:
handler
: a function which receives({ type, path, options })
and returns either a continuable or a stream.manifest
: a manifest object as above but paths are prefixed with service definition path.adapter
: an adapter object as above but paths are prefixed with service definition path.
an vas
adapter is a function of shape ({ manifest, options }) => handler
- where
adapter.name
must be defined and match keys indefinition.adapter
andservice.adapter
.
vas = require('vas')
the top-level vas
module is a grab bag of all vas/*
modules.
you can also require each module separately like require('vas/Server')
.
service = vas.Service(definition)
creates a vas
service from the server definition.
service = vas.Client(adapter, definition)
creates a vas
service from the adapter and client definition.
server = vas.Server(adapter, service)
creates a "server" from the adapter and service.
what a "server" is depends on the adapter.
service = vas.combine(services)
combines many vas
services into one.
does this by:
- deeply merging the
manifest
andadapter
objects - calling each
handler
until the first that returns something
emitter = vas.Emitter(service)
frequently asked questions (FAQ)
how to reduce browser bundles
by design, service definitions are re-used between client and server creations.
this leads to all the server code being included in the browser, when really we only need the service names and manifests to create the client.
to reduce our bundles to only this information (eliminating any require
calls or other bloat in our service files), use the evalify
browserify transform.
to evalify
only service files, where service files are always named service.js
, install evalify
and add the following to your package.json
{
"browserify": {
"transform": [
["evalify", { "files": ["**/service.js"] } ]
]
}
}
how to do authentication
TODO: re-write for v3
authentication is answers the question of who you are.
here's an example of how to do this in vas
, stolen stolen from holodex/app/dex/user/service
:
(where config.tickets
corresponds to an instance of ticket-auth
)
const Route = require('http-routes')
const service = {
name: 'user',
manifest: {
whoami: 'sync'
},
authenticate: function (server, config) {
return (req, cb) => {
config.tickets.check(req.headers.cookie, cb)
}
},
methods: function (server, config) {
return { whoami }
function whoami () {
return this.id
}
},
handlers: (server, config) => {
return [
Route([
// redeem a user ticket at /login/<ticket> and set cookie.
['login/:ticket', function (req, res, next) {
config.tickets.redeem(req.params.ticket, function (err, cookie) {
if(err) return next(err)
// ticket is redeemed! set it as a cookie,
res.setHeader('Set-Cookie', cookie)
res.setHeader('Location', '/') // redirect to the login page.
res.statusCode = 303
res.end()
})
}],
// clear cookie.
['logout', function (req, res, next) {
res.setHeader('Set-Cookie', 'cookie=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;')
res.setHeader('Location', '/') // redirect to the login page.
res.statusCode = 303
res.end()
}],
// return current user. (for debugging)
['whoami', function (req, res, next) {
res.end(JSON.stringify(req.id) + '\n')
}]
])
]
}
}
install
npm install --save vas@pre-release
inspiration
license
The Apache License
Copyright © 2016 Michael Williams
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.