I mention hashids pretty frequently, but thought this was nifty enough to share. I've been writing a server with koa
and postgresql
, but trying to avoid numeric ids in json request and response bodies. The idea is to output encoded numeric ids as hash ids, and input decoded hashids as numeric ids.
The basic usage is to encode and decode a numeric id.
var hashids = new Hashids('company name', 8)
hashids.encode(5)
// => 'qrVqXN0Y'
hashids.decode('qrVqXN0Y')
// => [ 5 ]
One clear advantage is that you can include a parent namespace in the id. For example, you could encode an app_id
and user_id
as the consumer-facing user_id
.
hashids.encode(5, 10)
// => 'pNZxCQlv'
hashids.decode('pNZxCQlv')
// => [ 5, 10 ]
I wanted to recursively replace all hashids in the json body so the application always sees the numeric id, and the user always sees the hashid. It's actually not too hard with substack/js-traverse.
var traverse = require('traverse')
var _ = require('lodash')
function encode (object) {
return traverse(object).map(function () {
if (_.endsWith(this.key, 'id') && _.isNumber(this.node)) {
return hashids.encode(this.node)
} else {
return this.node
}
})
}
function decode (object) {
return traverse(object).map(function () {
if (_.endsWith(this.key, 'id') && _.isString(this.node) && this.node.length == 8) {
return hashids.decode(this.node)[0]
} else {
return this.node
}
})
}
This allows whole json objects to be encoded and decoded.
encode({ id: 1, stripe_id: 'cust_23aef04092' })
// => { id: 'olejRejN', stripe_id: 'cust_23aef04092' }
decode({ id: 'olejRejN', stripe_id: 'cust_23aef04092' })
// => { id: 1, stripe_id: 'cust_23aef04092' }
For testing I can still use normal ids.
decode({ user_id: 10, vendor_id: 4 })
// => { user_id: 10, vendor_id: 4 }
Finally, with koa
it's possible to rewrite every json body.
// app.js
var app = require('koa')()
app.use(function * (next) {
yield next
if (this.body) {
this.type = 'application/json'
this.body = encode(this.body)
}
})
app.use(function * () {
// render list of objects
this.body = _.times(100, function (i) {
return { id: i, name: 'User #' + i, balance: Math.random() * 10000 }
})
})
app.listen(8000)
To test it out.
$ iojs app.js
$ time curl localhost:8000
[
{ "id": "qgVRZVmR",
"name": "User #0",
"balance": 5336.400221567601 },
{ "id": "WZ7G8Nkj",
"name": "User #1",
"balance": 6245.425492525101 },
...
real 0m0.028s
user 0m0.005s
sys 0m0.007s
Fast enough for me. To parse the request body, wrap the normal parser in a new generator.
var parse1 = require('co-body')
function * parse2 (context) {
var body = yield parse1.json(context, { limit: '10kb' })
return hashids.decode(body)
}
// for example in a route
function * (next) {
var body = yield parse2(this)
}
Well that's it. Tweet @aj0strow if you wanna talk about it.