Custom model validation across all layers

Originating from the following GitHub discussion: https://github.com/strapi/strapi/discussions/8101

I needed to implement an “alias” feature for an item type in a Strapi database, which necessitates non-standard validation logic:

  • Each Item has a self-referential relationship reimplements_item
  • When setting reimplements_item, it cannot be set to an Item that also has a value for reimplements_item (because this would necessitate looking up the root item down an arbitrarily long chain)
  • When deleting an Item, it must throw an error if there are any items with that item in their reimplements_item field

I was able to implement this validation by overriding the custom services with logic similar to this:

async update(params, data, { files } = {}) {
  const validData = await strapi.entityValidator.validateEntityUpdate(strapi.models.item, data)

  // This utility function checks for valid reimplements_item data (shared between create and update)
  // Returns undefined on success, or an error object if it fails
  const reimplementsError = await checkReimplementsItem(validData)
  if (reimplementsError) throw reimplementsError

  const entry = await strapi.query('item').update(params, validData)
  // ...and continue on with the standard logic
}

This works alright; I also needed to make a light override for the controller update method with a try/catch block around the service to avoid generic 500 server errors, which isn’t ideal but works.

However, if a user modifies an Item through the Strapi admin screen, all of this fancy logic is apparently bypassed (according to @DMehaffy).

Suggestions for things that would make this whole process a lot less onerous:

  • Add a way to attach arbitrary validation logic that will be called by the entityValidator on a per-model basis. This would allow me to leave the vanilla services and controller methods in place, because I could just add some validation if the reimplements_item property is populated. It would presumably also affect the Strapi admin.
  • As an alternative, provide generic errors that can be thrown by services that will be automatically converted into ctx error output (instead of getting obscured by 500 server error). This would allow customization of generic services without needing to also add logic to the generic controller.
  • Ensure that CRUD logic is centralized. It’s weird that the Strapi admin would be circumventing the logic that’s in place for handling model creation. At that point, it seems kind of redundant to have the service at all since it’s just being used by simple CRUD (unless maybe it’s invoked when creating items via relationships? Dunno, haven’t played with that)
2 Likes

As a follow-up to this: I think it’s SUPER fragile that your recommended method for modifying default services and controllers is copy/pasting the original code and then tweaking it. What happens when you need to update the default behavior? That’s going to be a really difficult to track down backwards-incompatible change for end-users who have customized them. I would vastly prefer if it were possible to call into the original default service (that way I could perform some custom validation or whatever, then pass the data along and let the default handler do the rest).

1 Like

Adding @alexandrebodin @Convly to the convo so get their feedback.

I think the larger topic is about handling validation in general.