0
votes

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
Can you not use the raw property on the query. docs - Shivam
As I understood those docs (from some time ago) raw will not create the Sequelize models for me, which means I need to do the mapping myself ... which I could do, but I was hoping there was a way using my current approach. Mapping is not an issue, after all Sequelize is doing it to make the models, if there is no way to do it in my current design. - click2install
@Shivam if you want to post your comment I will award it as the answer. Due to lack of activity, I went with your suggestion, then pass the raw result to a mapping function I created that maps from a Sequelize model definition to a plain object that Joi then validates. - click2install

1 Answers

0
votes

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

Can you try without it.