Giter Club home page Giter Club logo

bookshelf-advanced-serialization's Introduction

bookshelf-advanced-serialization

Build Status Coverage Status npm version

Description

This module is a plugin for Bookshelf.js, supporting three types of custom serialization behavior:

  1. serializing according to access permissions,
  2. serializing according to the application context in which serialization is performed, and
  3. serializing after loading relations that should be on the model / collection.

Together, these features ensure that one call to toJSON can yield an arbitrarily complex serialization result: any combination of a model's properties (i.e. in Bookshelf terminology, attributes and relations), the properties of its relations, and so on indefinitely. In other words, this module supports recursive serialization (and it does so in a way that allows infinite looping / cycling to be easily prevented if that would otherwise be a danger). This means the module excels at supporting hierarchical data models.

You can explore the source code on GitHub. This module has a comprehensive test suite.

Philosophy

This module was designed to support serializing models which represent the resources of the Sequiturs REST API. It is thus well-suited to the use case of using Bookshelf to power a REST API.

One important aspect of the REST API use case is customizing the serialization result according to the access permissions of the client. It is crucial that no data be leaked to a client who should not see it. For this reason, this module exclusively implements a whitelisting approach to serialization, and does not support blacklisting. Only properties that have been explicitly allowed to be serialized--whitelisted--will be returned by toJSON. This makes leaking data more difficult. This is good.

This strict approach to data security is reflected in another aspect of implementation: the module assumes that when a model has no visible properties, the serialized result should not even indicate to the client that the model exists! In practice, this means that:

  1. models with no visible properties are serialized to undefined rather than {}, and
  2. these undefined values are removed from arrays.

This is useful in the following situation, for instance: suppose there is a collection that contains public items, which the client should be able to see, and private items, which the client should not be able to see. The current implementation ensures that the client will not receive any indication how many private items exist or in what order they appear in the collection. In future, this behavior could be made optional, if that is desired.

How to use

Overview

The serialization result returned by toJSON is determined by:

  1. evaluating the access permissions of the recipient to determine the maximum extent of the recipient's visibility into a model,

    • This is accomplished using three parts:

      • an accessor value that is passed as an option to toJSON() or that has been set on a model instance,
      • a roleDeterminer method set on the model class, and
      • a rolesToVisibleProperties object set on the model class.
    • accessor represents who is accessing the model; this is the recipient of the serialization result. roleDeterminer is a function for determining the role of the accessor in relation to the model instance. When you call toJSON, roleDeterminer is invoked, with the accessor passed as an argument. rolesToVisibleProperties is an object that maps a role to a list of properties that should be visible to someone with that role.

  2. optionally specifying the subset of these role-determined visible properties that should be in the serialization result given the application context in which serialization is being performed,

    • This is accomplished using two parts:

      • a contextSpecificVisibleProperties object provided on the options object passed to toJSON
      • an optional contextDesignator function also provided on the options object
    • contextSpecificVisibleProperties indexes lists of the properties of a model that should be visible in light of the application context. These lists are indexed first by models' tableName, which allows for easily specifying context-specific visible properties for all models of a certain type. (We use tableName because this is the only identifier Bookshelf provides for identifying a model's type.) If you want fine-grained control over designating context beyond simply by model type, you can provide an contextDesignator function, which is invoked when you call toJSON, and which by default is passed the model's tableName, _accessedAsRelationChain, and id properties as arguments. (You can override this default behavior and pass custom arguments to contextDesignator, by passing your own getEvaluatorArguments function when registering this plugin.) The designation returned by contextDesignator will be used to lookup the list of context-specific visible properties, inside contextSpecificVisibleProperties[tableName].

  3. optionally loading specified relations on the model (or on the model's relations, recursively to any depth) before serializing, if those relations are not already loaded.

    • This is accomplished using two parts:

      • an ensureRelationsLoaded object provided on the options object passed to toJSON
      • an optional contextDesignator function also provided on the options object. This is the same contextDesignator as in 2.
    • ensureRelationsLoaded works analogously to contextSpecificVisibleProperties, except the lists contain the names of relations that it will be ensured are loaded on the model prior to serialization, rather than context-specific visible properties.

Installation

npm install bookshelf-advanced-serialization

then

'use strict';

var advancedSerialization = require('bookshelf-advanced-serialization');

var knex = require('knex')({ ... });
var bookshelf = require('bookshelf')(knex);

bookshelf.plugin(advancedSerialization());

module.exports = bookshelf;

API

See the docs.

You can also view a local copy of the docs:

  1. Clone the repo.
  2. Generate the docs by running npm run jsdoc.
  3. Open docs/index.html in your browser.

Examples

See examples/rest-api.

Within examples/rest-api/server.js, there are examples of the different ways you can use this module to control serialization behavior:

  • Using access permissions but not application context
    • See route handling for /users/:username.
  • Using access permissions and application context
    • using contextDesignator's context designations with contextSpecificVisibleProperties
      • See route handling for /comments/:id.
    • using only table names, not contextDesignator's context designations, with contextSpecificVisibleProperties
      • See route handling for /comments/:id, specifically the users table name.
    • using custom arguments in the contextDesignator function
      • See route handling for /comments/:id.
    • using the default relationChain argument to contextDesignator
      • (No example of this.)
  • After ensuring certain relations have been loaded
    • using contextDesignator's context designations with ensureRelationsLoaded
      • See route handling for /comments/:id.
    • using only table names, not contextDesignator's context designations, with ensureRelationsLoaded
      • See route handling for /users/:username.
    • using custom arguments in the contextDesignator function
      • See route handling for /comments/:id.
    • using the default relationChain argument to contextDesignator
      • (No example of this.)

License

See LICENSE.

bookshelf-advanced-serialization's People

Contributors

ihinsdale avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

bookshelf-advanced-serialization's Issues

Allow incomplete specification of context designations for a given table, with sane defaults

If contextDesignator does not return a designation, we could default to:
- for ensureRelationsLoaded, an empty array
- for contextSpecificVisibleProperties, the visible properties determined by the role

Currently, if the caller uses context designations in their specification of ensureRelationsLoaded or contextSpecificVisibleProperties, those context designations must be complete in the sense of covering all possible models with the given table name (otherwise an error is thrown, cf.

if (!Array.isArray(contextSpecificVisibleProperties)) {
,
if (!Array.isArray(relationNames)) {
).

Use consistent terminology

Use "properties" instead of "fields". "properties" refers to the properties of the serialized object, which in Bookshelf's terminology are known as attributes and/or relations. There's no way to distinguish attributes from relations on a serialized object, so the more generic "properties" term is more appropriate.

Consider whether there are better names for accessor, roleDeterminer, rolesToVisibleFields, contextSpecificVisibleFields, ensureRelationsLoaded, _accessedAsRelationChain, evaluator.

evaluator should probably be renamed to contextDesignator.

Serializing relations fetched with collections

    Venue
        .forge({}, {
            accessor: req.user
        })
        .fetchAll({
            withRelated: ['bars']
        })
        .then(venues => {
            return venues.toJSON();
        })
        .then(res.json.bind(res));

The accessor has sufficient privileges to see the related 'bars' model, however, the bar does not appear in the serialization. However, the following code does return the related bars.

    Venue
        .forge({}, {
            accessor: req.user
        })
        .fetchAll({
            withRelated: ['bars']
        })
        .then(venues => Promise.all(venues.map(v => v.related('bars')
            .toJSON())))
        .then(res.json.bind(res))

Any insights on why the related model wouldn't make it into the serialization?

Thanks

(I've really enjoyed using this package, btw; it's refreshingly well-documented.)

Only load relations that are in the final list of visible properties

This would be more efficient than the way we currently do it, where we load whatever relations the caller specified in ensureRelationsLoaded and then remove them if they're not in the list of contextSpecificVisibleFields.

Or we could potentially throw a sanity error to tell the caller that they instructed to load a relation which they also instructed not to be visible.

Support for create/update/destroy?

Nice work on this, thank you!

Do you have plans to handle writes as well, or just keep the scope of the project to reads?

It seems like you use this in production already -- how does Sequiturs currently handle access control for writes?

`ensureRelationsLoaded` when used with a collection should yield 1 join, not n joins

Currently ensureRelationsLoaded is evaluated only as part of model.toJSON(). That means each model in a collection will have its own JOIN query executed to load the specified relation. This means n JOINs, where n is the length of the collection. This is inefficient. ensureRelationsLoaded should make use of collection.load(), so that only 1 JOIN is performed.

Webpage down

The url is returning {"error":"Server error."}

Warn if `shallow:true` and `ensureRelationsLoaded` options are both passed to `toJSON`

When both options are passed, shallow: true should take priority since it is a standard Bookshelf option. Furthermore it should cause all ensureRelationsLoaded behavior to be avoided, since there's no reason to do unnecessary work loading relations that aren't going to be serialized--unless plugin option ensureRelationsVisibleAndInvisible is passed.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.