feathersjs / feathers

The API and real-time application framework
https://feathersjs.com
MIT License
15.08k stars 750 forks source link

[@feathersjs/schema] how to filter valid data on data array ? #2706

Open claustres opened 2 years ago

claustres commented 2 years ago

My use case is to validate input data according to a schema on a service, if any. Once my schema has been created I am using the following hook:

import commonHooks from 'feathers-hooks-common'
import { validateData } from '@feathersjs/schema'

before: {
    create: [
      // Perform validation if setup
      commonHooks.when((hook) => hook.service.schema, (hook) => validateData(hook.service.schema)(hook))
    ]
   ...
}

This works as expected when creating a single object: if the object is valid then it is created, otherwise an error is raised.

However, I wonder if the current behavior with an array of objects is the most useful. Indeed, the first invalid object found in the array will currently stop the validation process and throw the first encountered error. It seems to me that it would be more useful to process all objects anyway and return the array of invalid objects with associated errors instead of the first one only. That way we could probably do something like catching the error, filter invalid objects and create valid ones anyway if required. Indeed IMHO most people submitting a set of objects expect that valid ones are created while an error is raised for invalid ones.

Of course I can create a dedicated hook to do that but any insight is welcome as I am new to this module, for instance maybe resolvers can help to do that ? Any improvement in the module to address real-world app use cases could benefit the community, I will be glad to start a PR if enhancement is required.

marshallswain commented 2 years ago

Indeed IMHO most people submitting a set of objects expect that valid ones are created while an error is raised for invalid ones.

I agree

for instance maybe resolvers can help to do that?

@daffl today mentioned "converter" resolvers that maybe could help with this. I've not used them, yet.

claustres commented 2 years ago

I recently wrote something like this, let me know if it helps:

import errors from '@feathersjs/errors'
import commonHooks from 'feathers-hooks-common'

const { BadRequest } = errors
const { getItems, replaceItems } = commonHooks

export function validateData (schema) {
  return async (hook) => {
    let items = getItems(hook)
    const isArray = Array.isArray(items)
    items = (isArray ? items : [items])
    // Perform validation
    items = await Promise.allSettled(items.map((item) => schema.validate(item)))
    // Keep track of validation errors, even if invalid data will be filtered this ensure
    // original data with validation error will be "tagged"
    items.forEach(item => {
      if (item.status === 'rejected') {
        item.validationError = (item.reason.ajv ? new BadRequest(item.reason.message, item.reason.errors) : item.reason)
      }
    })
    // Filter errors/valid data
    const errors = items.filter(item => item.validationError).map(item => item.validationError)
    items = items.filter(item => !item.validationError).map(item => item.value)

    // Raise if no valid data is found
    const hasValidData = (items.length > 0)
    const hasError = (errors.length > 0)
    if (hasError) {
      const firstError = errors[0]
      // Single item case => raise the error
      if (!isArray) throw firstError
      // Multiple items case => raise if no valid data found
      else if (!hasValidData) {
        // Keep track of all errors
        throw new BadRequest(firstError.message, errors)
      }
    }
    replaceItems(hook, isArray ? items : items[0])
    return hook
  }
}