serialize the **** out of your data. JSON API v1.0 compliant.
JavaScript
Latest commit 26460b6 Apr 14, 2016 @cludden cludden Merge pull request #23 from kutlerskaggs/next
Next
Permalink
Failed to load latest commit information.
lib fixes #21 Apr 14, 2016
test remove console.log from test file Apr 14, 2016
.gitignore initial commit Feb 17, 2016
.jshintrc progress Feb 18, 2016
.travis.yml adds #serializeError tests, adds travis-ci config Feb 21, 2016
Gruntfile.js adds missing gruntfile.js Apr 6, 2016
LICENSE.md progress Feb 19, 2016
README.md update README Apr 14, 2016
index.js progress Feb 18, 2016
package.json 1.1.1 Apr 14, 2016

README.md

json-api-ify

Build Status

a node.js library for serializing your data to JSON API v1.0 compliant documents, inspired by jsonapi-serializer. this library makes no assumptions regarding your choice of ORM/ODM, or the structure of your data. simply define your types and how their related and let this library do the heavy lifting.

Installing

npm install --save json-api-ify

Getting Started

Create a new reusable serializer.

var Serializer = require('json-api-ify');

let serializer = new Serializer({
    baseUrl: 'https://www.example.com/api',
    topLevelMeta: {
        'api-version': 'v1.0.0'
    }
});

Define a type. (read more about options below)

serializer.define('users', {
    id: '_id',
    blacklist: [
        'password',
        'phone.mobile'
    ],
    links: {
        self(resource, options, cb) {
            let link = options.baseUrl + '/users/' + resource.id;
            cb(null, link);
        }
    },
    meta: {
        nickname(resource, options, cb) {
            let nickname = 'lil ' + resource.attributes.first;
            cb(null, nickname);
        }
    },
    processResource(resource, cb) {
        return cb(null, resource.toObject());
    },
    topLevelLinks: {
        self(options, cb) {
            let link = options.baseUrl + '/users';
            cb(null, link);
        },
        next(options, cb) {
            let link = options.baseUrl + '/users';
            if (options.nextPage) {
                link += '?page=' + options.nextPage;
            }
            cb(null, link);
        }
    },
    topLevelMeta: {
        total(options, cb) {
            cb(null, options.total);
        }
    }
}, function(err) {
    // check for definition errors
})

Get a hold of some data that needs to be serialized.

let data = [new User({
    first: 'Kendrick',
    last: 'Lamar',
    email: 'klamar@example.com',
    password: 'elkjqe0920oqhvrophepohiwveproihgqp398yr9pq8gehpqe9rf9q8er',
    phone: {
        home: '+18001234567',
        mobile: '+180045678910'
    },
    address: {
        addressLine1: '406 Madison Court',
        zipCode: '49426',
        country: 'USA'
    }
}), new User({
    first: 'Kanye',
    last: 'West',
    email: 'kwest@example.com',
    password: 'asdlkj2430r3r0ghubwf9u3rbg9u3rbgi2q3oubgoubeqfnpviquberpibq',
    phone: {
        home: '+18002345678',
        mobile: '+18007890123'
    },
    address: {
        addressLine1: '361 Shady Lane',
        zipCode: '23185',
        country: 'USA'
    }
})];

Serialize it

serializer.serialize('users', data, function(err, payload) {
    console.log(payload);
});

Or, use it in a route

function(req, res) {
    async.auto({
        users: function findUsers(fn) {
            User.find({})
                .limit(10)
                .skip(parseInt(req.query.page || 0) * 10)
                .exec(fn);
        },

        count: function countUsers(fn) {
            User.count({}).exec(fn);
        },

        payload: ['users', 'count', function serialize(fn, results) {
            serializer.serialize('users', results.users, {
                total: results.count,
                nextPage: (req.query.page || 1) + 1
            }, fn);
        }]
    }, function(err, payload) {
        if (err) {
            return res.json(500, {errors: [{
                status: 500,
                detail: err.message
            }]});
        }
        res.json(200, payload);
    });
}

Response body:

{
    "links": {
        "self": "https://www.example.com/api/users",
        "next": "https://www.example.com/api/users?page=2"
    },
    "data": [
        {
            "type": "users",
            "id": "54735750e16638ba1eee59cb",
            "attributes": {
                "first": "Kendrick",
                "last": "Lamar",
                "email": "klamar@example.com",
                "phone": {
                    "home": "+18001234567"
                },
                "address": {
                    "addressLine1": "406 Madison Court",
                    "zipCode": "49426",
                    "country": "USA"
                }
            },
            "relationships": {},
            "links": {
                "self": "https://www.example.com/api/users/54735750e16638ba1eee59cb"
            },
            "meta": {
                "nickname": "lil Kendrick"
            }
        },
        {
            "type": "users",
            "id": "5490143e69e49d0c8f9fc6bc",
            "attributes": {
                "first": "Kanye",
                "last": "West",
                "email": "kwest@example.com",
                "phone": {
                    "home": "+18002345678"
                },
                "address": {
                    "addressLine1": "361 Shady Lane",
                    "zipCode": "23185",
                    "country": "USA"
                }
            },
            "relationships": {},
            "links": {
                "self": "https://www.example.com/api/users/5490143e69e49d0c8f9fc6bc"
            },
            "meta": {
                "nickname": "lil Kanye"
            }
        }
    ],
    "included": [],
    "meta": {
        "api-version": "v1.0.0",
        "total": 2
    }
}

Schemas

A type can have multiple serialization schemas, which you can create by calling define with a schema name. Any schema options provided will augment the default schema.

serializer.define('users', 'names-only', {
    whitelist: [
        'first',
        'last'
    ]
}, callback);
serializer.serialize('users', 'names-only', data, function(err, payload) {
    console.log(payload);
});
{
    "links": {
        "self": "https://www.example.com/api/users"
    },
    "data": [
        {
            "type": "users",
            "id": "54735750e16638ba1eee59cb",
            "attributes": {
                "first": "Kendrick",
                "last": "Lamar"
            },
            "relationships": {},
            "links": {
                "self": "https://www.example.com/api/users/54735750e16638ba1eee59cb"
            },
            "meta": {
                "nickname": "lil Kendrick"
            }
        },
        {
            "type": "users",
            "id": "5490143e69e49d0c8f9fc6bc",
            "attributes": {
                "first": "Kanye",
                "last": "West"
            },
            "relationships": {},
            "links": {
                "self": "https://www.example.com/api/users/5490143e69e49d0c8f9fc6bc"
            },
            "meta": {
                "nickname": "lil Kanye"
            }
        }
    ],
    "included": [],
    "meta": {
        "api-version": "v1.0.0"
    }
}

Relationships

Relationships are easy as well. First, include a relationship map in your type/schema options.

serializer.define('users', {
    // ..
    relationships: {
        groups: {
            type: 'groups',
            include: true,
            links: {
                self(resource, options, cb) {
                    let link = options.baseUrl + '/users/' + resource.id + '/relationships/groups';
                    cb(null, link);
                },
                related(resource, options, cb) {
                    let link = options.baseUrl + '/users/' + resource.id + '/groups';
                    cb(null, link);
                }
            }
        }
    }
    // ..
}, callback);

Lastly, define the related type.

serializer.define('groups', {
    // ..
    relationships: {
        users: {
            type: 'users',
            include: true,
            schema: 'names-only',
            links: {
                self(resource, options, cb) {
                    let link = options.baseUrl + '/groups/' + resource.id + '/relationships/users';
                    cb(null, link);
                },
                related(resource, options, cb) {
                    let link = options.baseUrl + '/groups/' + resource.id + '/users';
                    cb(null, link);
                }
            }
        }
    }
    // ..
}, callback);

Deserialize

extract the data from a payload in a slightly more usable fashion

let payload = {
    data: {
        type: 'user',
        attributes: {
            first: 'a$ap',
            last: 'ferg',
            email: 'aferg@example.com',
            phone: {
                home: '1-111-111-1111'
            }
        },
        relationships: {
            groups: {
                data: [{
                    type: 'group',
                    id: '56cd74546033f8d420bc1c11'
                },{
                    type: 'group',
                    id: '56cd74546033f8d420bc1c12'
                }]
            }
        }
    }
};
serializer.deserialize(payload, function(err, data) { /* .. */ });

here, data would look like:

{
    "user": {
        "first": "a$ap",
        "last": "ferg",
        "email": "aferg@example.com",
        "phone": {
            "home": "1-111-111-1111"
        },
        "groups": [{
            "_id": "56cd74546033f8d420bc1c11"
        },{
            "_id": "56cd74546033f8d420bc1c12"
        }]
    },
    "groups": [{
        "_id": "56cd74546033f8d420bc1c11"
    },{
        "_id": "56cd74546033f8d420bc1c12"
    }]
}

API

Constructor Summary

Serializer([options])

constructs a new serializer instance

Arguments
Param Type Description
[options] {Object} global options. see serialize() options for more detail

Method Summary

define(type, [schema], options, callback)

defines a type serialization schema

Arguments
Param Type Description
type {String} the resource type
[schema] {String} the serialization schema to use. defaults to default
options {Object} schema options
callback(err) {Function} a function that receives any definition error.

deserialize(payload, callback)

deserializes the data attribute of the payload

Arguments
Param Type Description
payload {Object} a valid JSON API payload
callback(err, data) {Function} a function that receives any deserialization error and the extracted data.

serialize(type, [schema], data, [options], callback)

serializes data into a JSON API v1.0 compliant document

Arguments
Param Type Description
type {String} the resource type
[schema] {String} the serialization schema to use. defaults to default
data {*} the data to serialize
[options] {Object} single use options. these options will be merged with the global options, default schema options, and any applicable non-default schema options
callback(err, payload) {Function} a function that receives any serialization error and JSON API document.
Options
{
    // an array of string paths to omit from the resource, this option
    // includes relationships that you may wish to omit
    blacklist: [],

     // the path to the primary key on the resource
    id: 'id',

    // a map of resource links
    links: {
        // asynchronous
        self(resource, options, cb) {
            // each key can be a value to set, or asynchronous function that
            // receives the processed resource, serialization options, and
            // a callback that should pass any error and the link value
            cb(null, link);
        },
        // synchronous
        self(resource, options) {
            return options.baseUrl + '/api/users/'  + resource.id;
        }
    },

    // a map of meta members
    meta: {
        // asynchronous
        self(resource, options, cb) {
            // each key can be a value to set, or asynchronous function that
            // receives the processed resource, serialization options, and
            // a callback that should pass any error and the meta value
            cb(null, meta);
        },
        // synchronous
        self(resource, options) {
            return meta;
        }
    },

    // preprocess your resources
    // all resources must be objects, otherwise they're assumed to be
    // unpopulated ids. NOTE!! If you're working with mongoose models,
    // unpopulated ids can be objects, so you will need to convert them
    // to strings
    processResource(resource, /* cb */) {
        if (typeof resource.toJSON === 'function') {
            resource = resource.toJSON();
        } else if (resource instanceof mongoose.Types.ObjectId) {
            resource = resource.toString();
        }
        return resource;
    },

    // relationship configuration
    relationships: {
        // each key represents a resource path that points to a
        // nested resource or collection of nested resources
        'groups': {
            // the type of resource
            type: 'groups',

            // whether or not to include the nested resource(s)
            include: true,

            // optionally specify a non-default schema to use
            schema: 'my-schema',

            // a map of links to define on the relationship object
            links: {
                self(resource, options, cb) {

                },
                related(resource, options, cb) {

                }
            }
        }
    },

    // a map of top-level links
    topLevelLinks: {
        self(options, cb) {

        }
    },

    // a map of top-level meta members
    meta: {
        total(options, cb) {

        }
    },

    // an array of string paths to pick from the resource. this option
    // overrides any specified blacklist and also includes relationships
    whitelist: [],
}

serializeError(error, [meta], [defaultStatusCode]) => {object} document

serializes any error into a JSON API v1.0 compliant error document. error can be anything, this method will attempt to intelligently construct a valid JSON API error object. the return document will contain a top level meta member with a status attribute that represents the status code with the greatest frequency.

Arguments
Param Type Description
error {*} the error data to serialize
[meta] {Object} any top level meta information
[defaultStatusCode] {Number|String} a default status code to apply to any error object(s) without a specified status
Example
function(req, res) {
    async.waterfall([
        // ..
    ], function(err, payload) {
        let status = 200;
        if (err) {
            payload = serializer.serializeError(err);
            status = payload.meta.status;
        }
        res.json(status, payload);
    });
}

Events

The serializer inherits from node's EventEmitter. Below is a summary of the events exposed by this library.

error

The global error event.

Arguments
Param Type Description
error {Object} the error object
Example
serializer.on('error', function(error) {
    bugsnag.notify(error);
});

To Do

  • implement jsonapi top-level member
  • implement deserialize method
  • implement support for unpopulated relationships (an id, or array of ids)
  • implement templates
  • ADD MORE TESTS!

Testing

run tests

npm test

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

License

Copyright (c) 2016 Chris Ludden.
Licensed under the MIT license.