resource-schema

    2.1.3 • Public • Published

    Resource Schema

    NPM version Build Status

    Define a translation "schema" between mongoose models and API resources. Once you do this, you can:

    • Call methods to convert your models to resources and resources to models.
    • Generate express middleware to handle GET, POST, PUT, and DELETE requests for that resource.

    Table of Contents

    Why ResourceSchema?

    ResourceSchema abstracts a lot of the boilerplate when creating API endpoints for RESTful resources. It helps you:

    • translate models to and from their corresponding resource representation
    • validate values
    • handle for malformed data
    • automatically convert url query parameters to their corresponding mongoose query parameters.

    All of which allows you to focus on higher-level resource design.

    Usage

    Create a schema translation:

    Product = require './models/product'
     
    var schema = {
      '_id': '_id',
     
      // Get resource field 'name' from model field 'name'
      // Convert the name to lowercase whenever saved
      'name': {
        field: 'name',
        set: function (productResource) { return productResource.name.toLowerCase(); }
      },
     
      // make sure the day matches the specified format before saving
      'day': {
        field: 'day',
        match: /[0-9]{4}-[0-9]{2}-[0-9]{2}/
      },
     
      // Model field 'active' renamed to resource field 'isActive'
      'isActive': 'active',
     
      // Dynamically get field 'code' whenever the resource is requested:
      'code': {
        get: function (productModel) { productModel.letter + productModel.number }
      },
     
      // Dynamically get totalQuantitySold whenever the resource is requested.
      // Resolve 'totalQuantitySoldByProductId' before applying the getter.
      'totalQuantitySold': {
        resolve: {
          totalQuantitySoldByProductId: function ({models}, done) {
            getTotalQuantitySoldById(models, done)
          }
        },
        get: function (productModel, {totalQuantitySoldByProductId}) {
          totalQuantitySoldByProductId[productModel._id]
        }
      }
     
      // field soldOn allows you to query for products sold on the specified days
      // e.g. api/products?soldOn=2014-10-01&soldOn=2014-10-05
      'soldOn': {
        type: String,
        isArray: true,
        match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/,
        find: function (days) { return { 'day': $in: days } }
      },
     
      // field fromLastWeek allows you to query for products sold in the last week
      // e.g. api/products?fromLastWeek=true
      'fromLastWeek': {
        type: Boolean,
        find: (days) -> { 'day': $gt: '2014-10-12' }
      }
    };
     
    module.exports = new ResourceSchema(Product, schema);

    Then generate middleware to automatically handle requests for the resource:

    resourceConverter = require './resource-converter'
     
    // generate express middleware that automatically handles GETPOSTPUTand DELETE requests:
    app.get('/products'resourceConverter.get()resourceConverter.send);
    app.post('/products'resourceConverter.post()resourceConverter.send);
    app.put('products/:_id'resourceConverter.put('_id')resourceConverter.send);
    app.get('products/:_id'resourceConverter.get('_id')resourceConverter.send);
    app.delete('products/:_id'resourceConverter.delete('_id')resourceConverter.send);

    Install

    npm install resource-schema --save
    

    Creating a Resource

    new ResourceSchema(model, [schema], [options])

    • model - mongoose model to generate the resource from
    • [schema] - optional object to configure custom resource fields. If no schema is provided, the resource schema is automatically generated from the model schema.
    • [options] - optional object to configure schema options, like document limits and default mongoose queries.

    Defining a Schema

    The schema allows define the shape of your resource. If you do not provide a schema, the resource will look exactly like the model.

    We can define the schema using these properties:

    • field - string that maps a resource field to a mongoose model field.
    • get - function that dynamically gets the value whenever a resource is requested.
    • set - function that dynamically sets the value whenever a resource is PUT or POSTed
    • resolve - function for getting async data needed to build resource
    • find - function that dynamically builds a mongoose query whenever querying by this field
    • findAsync - TODO asynchronous version of find
    • optional - do not include this field in the resource unless specifically requested with the $add query parameter
    • validate - function that validates the field before saving or updating
    • match - regexp to validate field before saving
    • type - convert the type of the field before saving/querying. This is especially for converting query parameters, which default to a string.
    • isArray - convert value to array before saving/querying. This is especially for converting query parameters, which will not be an array of only querying by on value.

    field: String

    Maps a resource field to a mongoose model field.

    schema = {
      'name': { field: 'name' }
    }

    We can also define this with a shorthand notation:

    schema = {
      'name': 'name'
    }

    Or even simpler with coffeescript:

    schema = {
      'name'
    }

    Note, this can be used to rename a model field to a new field on the resource:

    schema = {
      'category.name': 'categoryName'
    }
    // => {
    //  category: {
    //    name: 'value'
    //  }
    // }

    get: function(model, context)

    • model - corresponding POJO model for requested resource (use the fat: true option on the ResourceSchema to get a mongoose model)
    • context - object containing req, res, next, and resolved values (see "resolve" for details)

    Dynamically get the value whenever a resource is requested.

    var schema = {
      'fullName': {
        get: function (resource, context) {
          resource.firstName + ' ' + resource.lastName
        }
      }
    }

    set: function(resource, context)

    • resource - resource saved by client
    • context - object containing req, res, next, and resolved values (see "resolve" for details)

    Function that dynamically sets the value whenever a resource is saved or updated.

    var schema = {
      'name': {
        set: function (resource, context) {
          return resource.name.toLowerCase()
        }
      }
    }

    resolve: Object

    Key value object where the key is the name of the variable to resolve, and the value is an asynchronous function that returns the value. The function accepts one argument:

    • context - object containing req, res, next, models, and resources associated with this request

    Once a variable is resolved, it is attached to the "context" object, and is available to all the getters and setters for that field.

    var schema = {
      'note': {
        resolve:
          userNoteByUserId: function(context, done) {
            var userIds = context.models.map(function(user) { return user._id });
            UserNote.find({userId: $in: userIds}).then(function(notes) {
              var userNoteByUserId = _(notes).indexBy('userId');
              done(null, userNoteByUserId);
            });
          })
        },
     
        // userNoteByUserId now available on context object
        get: function (user, context) {
          var userNoteByUserId = context.userNoteByUserId;
          return userNoteByUserId[user._id];
        }
      }
    }

    find: function(value, context)

    • value - value of query parameter from client
    • context - object containing req, res, next, and resolved values (see "resolve" for details)

    Function that dynamically builds a mongoose query whenever querying by this field. Return an object that will extend the mongoose query.

    var schema = {
      'soldOn': {
        find: function (days, context) {
          return { 'day': $in: days }
        }
      }
    }

    optional: Boolean

    If true, do not include this field in the resource unless specifically requested with the $add query parameter

    // GET /api/products?$add=name
     
    var schema = {
      'name': {
        optional: true,
        field: 'name'
      }
    }
     

    validate: function(value)

    • value - value of query parameter from client, or value on object

    Return a 400 invalid request if the provided value does not pass the validation test.

    var schema = {
      'date': {
        validate: function(value) {
          return /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/.test(value)
        }
      }
    }

    match: RegExp

    Return a 400 invalid request if the provided value does match the given regular expression.

    var schema = {
      'date': {
        match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
      }
    }

    type: Object

    Convert the type of the value.

    Valid types include

    • String
    • Date
    • Number
    • Boolean
    • and any other "newable" class
    schema = {
      'active': {
        type: Boolean
      }
    }

    This is especially useful for query parameters, which are a string by default

    isArray: Boolean

    Convert value to array before saving/querying. This is especially for converting query parameters, which will not be an array if only querying by on value.

    schema = {
      'daysToSelect': {
        isArray: true
        find: function(days, context) { ... }
      }
    }

    Options

    Options allow you to make configurations for the entire resource.

    filter: function(models)

    • models - all models found from the query

    Filter limits resources returned from every GET request.

    new ResourceSchema(Model, schema, {
      filter: function(models) {
        models.filter(function(model) {
          return model.isActive
        })
      }
    })
     

    limit: Number

    Limit the number of returned documents for GET requests. Defaults to 1000. 0 signifies unlimited.

    new ResourceSchema(Model, schema, {
      limit: 100
    })
     

    find: function(context)

    • context - object containing req, res, next, and resolved values (see "resolve" for details)

    Function returns an object that will be used at as starting point to build every query.

    new ResourceSchema(Product, schema, {
      find: function(context) {
        active: true,
        createdAt: $gt: '2013-01-01'
      }
    })
     

    resolve: Object

    Like resolve on schema, but resolved variable available to every getter and setter on the resource.

    queryParams: Object

    Define query parameters for this resource. Note, you could define these directly on the schema, but some people prefer to separate query parameters from all other fields.

    new ResourceSchema(Product, schema, {
      queryParams: {
        'soldOn': {
          type: String,
          isArray: true,
          match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/,
          find: function(days) {
            return { 'day': $in: days }
          }
        },
        'fromLastWeek': {
          type: Boolean,
          find: function(days) {
            return { 'day': $gt: '2014-10-12' }
          }
        }
      }
    })
     

    Converting with Methods

    .createModelFromResource(model, {req, res, next})

    Convert from resource representation to model representation

    .createModelsFromResources(models, {req, res, next})

    Convert from multiple resource representations to their corresponding model representations

    .createResourceFromModel(resource, {req, res, next})

    Convert from model representation to resource representation

    .createResourcesFromModels(resources, {req, res, next})

    Convert from multiple model representations to their corresponding resource representations

    Generating Middleware

    Once you've defined a new resource, call .get(), .post(), .put(), or .delete() to generate the appropriate middleware to handle the request.

    var resource = new ResourceSchema(Model, schema, options);
    app.get('/products', resource.get(), function(req, res, next) {
      # resources on res.body
    });

    The middleware will attach the resources to res.body, which can be used by other middleware, or sent immediately back to the client.

    get()

    Handle bulk GET requests. Results can by filtered by query parameters. Limits response to 1000 resources by default.

    var resource = new ResourceSchema(Model, schema, options);
    app.get('/products', resource.get(), function(req, res, next) {
      // resources on res.body
    });
     
    // GET /products?name=magicbox

    get(idField)

    • idField - field to use as resource identifier. Note, this must match the field name defined on the resource and the name on req.params.

    Handle GET requests for single resource.

    var resource = new ResourceSchema(Model, schema, options);
    app.get('/products/:_id', resource.get('_id'), function(req, res, next) {
      // resources on res.body
    });
     
    // GET /products/1234
    // => {
    //  _id: 1234
    //  name: 'banana bread'
    // }

    post()

    Handle POST requests. Can take a single resource or an array of resources.

    var resource = new ResourceSchema(Model, schema, options);
    app.post('/products', resource.post(), function(req, res, next) {
      // resources on res.body
    });
     
    // POST /products
    // {
    //  _id: 1234
    //  name: 'banana bread'
    // }
    //
    // or
    //
    // POST /products
    // [
    //  {
    //    _id: 1234
    //    name: 'banana bread'
    //  },
    //  {
    //    _id: 4567
    //    name: 'apples'
    //  }
    // ]

    put(idField)

    • idField - field to use as resource identifier. Note, this must match the field name defined on the resource and the name on req.params.

    Generate middleware to handle PUT requests to a resource. This does an upsert, so if the resource does not exist, it will create one.

    This will handle bulk PUT requests as well, automatically reading the idField and upserting for each resource.

    var resource = new ResourceSchema(Model, schema, options);
    app.put('/products/:_id', resource.put('_id'), function(req, res, next) {
      // resources on res.body
    });
     
    // PUT /products/1234
    // {
    //  _id: 1234
    //  name: 'banana bread'
    // }
    //
    // or
    //
    // PUT /products
    // [
    //  {
    //    _id: 1234
    //    name: 'banana bread'
    //  },
    //  {
    //    _id: 4567
    //    name: 'apples'
    //  }
    // ]

    delete(idField)

    • idField - field to use as resource identifier. Note, this must match the field name defined on the resource and the name on req.params.

    Generate middleware to handle DELETE requests to a single resource.

    var resource = new ResourceSchema(Model, schema, options);
    app.delete('/products/:_id', resource.delete('_id'), function(req, res, next) {
      // resources on res.body
    });
     
    // DELETE /products/1234

    send

    Convenience method for sending the resources on res.body back to the client.

    var resource = new ResourceSchema(Model, schema, options);
    app.get('/products', resource.get(), resource.send);

    Query Parameters

    ResourceSchema allows you to use a variety of query parameters to interact with your resources.

    $select

    Select fields to return on the resource. Similar to mongoose select.

    GET /products?$select=name&$select=active
    GET /products?$select[]=name&$select[]=active
    GET /products?$select=name active
    

    $limit

    Limit the number of resources to return in the response

    GET /products?$limit=10
    

    $skip

    Skip number of documents

    GET /products?$skip=5
    

    $sort

    Sort the returned documents

    GET /products?$sort=name&$sort=-price
    

    $addResourceCount

    Count total number of resources that would be available for this query if results were not limited. The result is added in the response header as 'x-resource-count'. This is useful for calculating total number of pages when paginating.

    GET /products?$addResourceCount=true
    

    $add

    Add an optional field to the resource. See optional schema field for more details.

    GET /products?$add=quantitySold
    

    querying resource fields

    You can query by any resource field with a 'field', 'find', or 'filter' attribute.

    GET /products?name=strawberry
    

    If the querying against one with a 'field' attribute, it will automatically perform an $in query.

    GET /products?name=strawberry&name=apple
    =>
    Product.find({ 'name': $in: ['strawberry', 'apple'] })
    

    Note that you can query nested fields with Express' [bracket] notation.

    GET /products?categrory[name]=fruit
    

    Contributing

    $ git clone https://github.com/goodeggs/resource-schema && cd resource-schema
    $ npm install
    $ npm test
    

    Code of Conduct

    Code of Conduct for contributing to or participating in this project.

    License

    MIT

    Install

    npm i resource-schema

    DownloadsWeekly Downloads

    1

    Version

    2.1.3

    License

    MIT

    Unpacked Size

    162 kB

    Total Files

    14

    Last publish

    Collaborators

    • goodeggs-admin
    • benbuckman
    • dannynelson
    • michaelkebbekus
    • demands
    • d.golyshev