diegohaz / schm

Composable schemas for JavaScript and Node.js

Home Page:https://git.io/schm

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Implementing a 'filter' option to choose which 'fields' you wish to validate or to be returned from parsing?

codinsonn opened this issue · comments

Hi, I've been playing around with this solution in combination with Mongoose and have been loving it so far.

One thing I'm missing though is the ability to choose which fields you either:

  • ... want back after parsing (something similar to how mongoose does it with their fields filter)
  • ... wish to validate (for edge cases when validating certain fields isn't necessary)

Is a way to implement this functionality already by any chance?

Thanks in advance and congrats on creating this awesome package!

commented

You can modify parse and validate methods by extending the schema. For example:

const schema = require('schm')
const omit = require('lodash/omit')

const ignore = (...params) => prevSchema => prevSchema.merge({
  parse(values) {
    return prevSchema.parse(omit(values, params))
  },
  validate(values) {
    return prevSchema.validate(omit(values, params))
  }
})

const mySchema = schema(
  {
    foo: String,
    bar: String,
  },
  ignore('foo'),
)

Does it work for you?

Aha, I'd already gotten a similar solution working based on lodash/pick 👍

parseWithOptions.js

import cherryPick from './cherryPick';

module.exports = (obj, schema, options = {}) => {

  console.log('-i- [parse:7] Parsing obj:', options);

  // Parse obj
  let _obj = schema.parse(obj);

  // Cherrypick props
  if (options.fields || options.omit || options.removeFalsy || options.removeNull || options.removeUndefined) {
    cherryPick(_obj, options);
  }

  // Return object
  console.log('-i- [parse:23] Returning parsed obj:', _obj);
  return _obj;

};

Where cherryPick is a helper function that uses lodash/pick and lodash/omit to cherrypick values based on options.

cherryPick.js

import _ from 'lodash';

module.exports = (obj, options = {}) => {

  // Defaults
  fields = _.defaultTo(options.fields, '');
  omit = _.defaultTo(options.omit, '');
  removeFalsy = _.defaultTo(options.removeFalsy, false);
  removeNull = _.defaultTo(options.removeNull, false);
  removeUndefined = _.defaultTo(options.removeUndefined, false);
  removeNaN = _.defaultTo(options.removeNaN, false);

  // Cherrypick values
  if (fields !== '') obj = _.pick(obj, fields.split(' ')); // only includes supplied values
  if (omit !== '') obj = _.omit(obj, omit.split(' ')); // deletes supplied values

  // Omit falsy values
  if (removeFalsy) obj = _.pickBy(obj);
  if (removeNull) obj = _.omitBy(obj, _.isNull);
  if (removeUndefined) obj = _.omitBy(obj, _.isUndefined);
  if (removeNaN) obj = _.omitBy(obj, _.isNaN);

  // Return object
  return obj;

};

But I didn't know I could simply extend it like this without needing the 'schm-methods' package as well, so this will definitely help me further, thanks!

I'm still a bit confused as to how extension like in the example you provided actually works.

Does this add ignore as a callable method on the schema? (with variable arguments)

In the end result I'm looking for I'd like to be able to define extensions and call them like so:

// Before parsing, as a kind of plugin
MySchema.validateWithOptions(obj, { omit: "name.middle" }); // Ignores name.middle in validation
MySchema.parseWithOptions(obj, { fields: "name.first name.last foo bar" }); // Cherrypick fields

// After parsing (with 'schm-methods'?)
parsedUser.validateWithOptions(parsedUser, { fields: "name.first name.last foo bar" });

Is this something that's possible with your proposed solution?

Also, can methods defined by 'schm-methods' reference the schema they're called on by way of using 'this'? (lexical binding of this could become a problem, I guess)

Because if that's the case I could then just pass the helper/util I already wrote as method like so:

const MySchema = schema(
   { ... }, 
   methods(
      parseWithOptions: (obj, options = {}) => parseWithOptions(obj, this, options),
      validateWithOptions: (obj, options = {}) => validateWithOptions(obj, this, options),
   )
);

Would that be possible? Or is there another way to add a "plugin" to a schema?

Perhaps being able to chain a plugin and/or method is a good option for this? (#32)

const MySchema = schema({ ... })
.plugin(parseWithOptions)
.plugin(validateWithOptions)
.method(validateWithOptions);
commented

@ThorrStevens

This is the interface of schema:

schema(...args: Object | Schema | Function)

Where Object is the literal object, Schema is another schema and Function some function that receives the previous schema as the only argument.

Passing a literal object to schema is the same as using group (when you pass a literal object, it's passed to group under the hood):

// these are the same
schema({ foo: String })
schema(group({ foo: String }))

The implementation of group is something like this:

const group = params => prevSchema => prevSchema.merge({ params })

The result of prevSchema.merge({ params }) is the schema itself, which is an object with this structure:

{
  params: Object,
  parse: Function,
  validate: Function,
  parsers: Object,
  validators: Object,
  merge: Function,
}

That said, if you want to call MySchema.validateWithOptions you need to add the method to the object above. And you can do that the same way that group added params:

const withValidateWithOptions = prevSchema => prevSchema.merge({
  validateWithOptions(values, options) {
    return validateWithOptions(values, this, options)
  }
})

const mySchema = schema({ ... }, withValidateWithOptions)

mySchema.validateWithOptions({ ... }, options)
commented

To clarify, schm-methods adds methods to the parsed object:

const mySchema = schema(
  { ... }, 
  methods({
    foo: () => {}
  })
)

const parsed = mySchema.parse({ ... })

parsed.foo()

Maybe docs aren't clear enough. If you find a better way to explain it there I'll be glad to accept a PR. :)