How to stripUnknown using HapiJS Joi and SequelizeJS?

736 views Asked by At

So I am mapping database fields onto response fields so I do not expose a data layer to consumers using the field property on the SequelizeJS models like:

module.exports = function (sequelize, DataTypes)
{
    return sequelize.define('Product',
    {
      id: 
      {
        type: DataTypes.INTEGER,
        primaryKey: true,
        field: 'sku_id'
      },
      name: 
      {
        type: DataTypes.STRING,
        field: 'sku_name'
      },
      code: 
      {
        type: DataTypes.STRING,
        field: 'sku_code'
      }
    },
    {
      timestamps: false,
      freezeTableName: true
    });
 };

The route controller then looks like:

module.exports = server => 
{
  const Joi = require('joi');
  const db = require('rfr')('models/index');
  const _ = require('lodash');

  const schema = Joi.array()
    .items(Joi.object(
      {
        id: Joi.number().integer().positive().required().description('The id of the product'),
        name: Joi.string().required().description('The name of the product'),
        code: Joi.string().required().description('The code of the product')
      })
      .label('Product'))
    .label('ProductCollection');

  return server.route(
    {
      method: 'GET',
      path: '/products/group/{id}',
      config:
      {
        handler,
        validate:
        {
          params:
          {
            id: Joi.number().integer().positive().required().description('the product group id')
          }
        },
        response:
        {
          schema,
          modify: true,
          options: { stripUnknown: true }
        },
        tags: ['api', 'products'],
        description: 'Get a list of products by group'
      }
    });

  ////////////////////////

  function handler(request, reply)
  {
    var sql = db.sequelize;
    var options =
      {
        replacements:
        {
          id: request.params.id
        },
        model: db.Product,
        mapToModel: true,
        type: sql.QueryTypes.SELECT
      };

    sql.query('sp_GetProducts @RowId = :id', options)
      .then(results => 
      {
        let sorted = _.orderBy(results, 'code');
        return reply(sorted);
      })
      .catch(err => reply(require('boom').wrap(err)));
  }
};

The challenge is that:

  1. Not having modify: true and stripUnknown: true on the response causes validation errors due to all the private properties on the SequelizeJS model, like; _changed, _options, _previousDataValues, etc
  2. To address #1 and still leaving off the modify: true and stripUnknown: true values, we could add unknown(true) to the Joi validation ... but then all public properties (not the private ones listed above in #1) are included in the response as the validation is allowing them and we are not stripping them
  3. If we remove unknown(true) from the Joi validation and add the modify: true and stripUnknown: true properties (like the code above shows) then an error is thrown as the private properties (and the public) ones are being stripped from the Sequelize model ... so the model is bugging out as it expects these to exist

Therefore, rather than manually mapping database objects to response objects, the only way past this I can see is to:

  1. Set unknown(true) on the validation
  2. Implement some sort of global handler (or per route handler) that will do the stripping after the validation has been done and the response is about to be sent

I am not sure what the correct extension point in #2 above would be, or if there is a neater approach to achieving the desired result.

or using something like map-obj and have the handler interrogate the Joi schema to determine if the source object (model) key => value should be copied to the new (response) object, I feel that this is just more overhead (cpu cycles and expensive for large arrays of objects) as opposed to letting Sequelize do it as it builds the model.

EDIT

This is the util function I created to acheive the mapping @Shivan suggested. Which utilises the object-mapper package.

function mapFromModel(data, model)
{
  if (!data) { return []; }
  if (_.isArray(data) === false) { data = [data]; }
  if (!model.attributes && !model.sequelize) { throw new Error('Expecting `model` argument to be a sequelize model.'); }

  let transform = {};

  Object
    .keys(model.attributes)
    .forEach(key => 
    {
      let obj = model.attributes[key];
      transform[obj.field] = `${obj.fieldName}?`;
    });

  return data.map(value => objectMapper(value, {}, transform));
}

My use case here is that I use the model to define the definition of what I want the object to look like and then this function does the mapping, which also modifies property names based on the field property in the model for each data field. This waY I can also easily build a model from the object this function returns if I need to re-use this object for updates/deletes, which I don't see being needed in the near future.

1

There are 1 answers

2
Shivam On BEST ANSWER

You seem to be using raw property on the query Which flattens the result

Can you try without it.