How to customize Strapi backend (content-manager) controllers, services and validation?

Hi all!

From what I understand, the API controllers for content-type are triggered only when the API are consumed from outside Strapi. So, If I would customize for example the update() method for a content type in the backend, have I to turn to the content-manager plugin logic, right? But how can I modify the controllers, the services and the valuators of the content-manager. For example I see that content-manager relies, for the services (and the validation), on strapi/lib/services, that is part of the core…

2 Likes

Hey @aminta :wave:,

To extend the controllers or service of the content-manager you will need to dig into the strapi-plugin-content-manager package and use plugin extensions to (partly) modify the behaviour of the content-manager functions.

I hope this helps you out, let me know if you have any questions. :+1:

Please do not the warning :warning: in the docs!

Hi @MattieBelt and thanks for the quick answer!

I try to explain better. What I’m trying to achieve is to check if a user leave a relation field blank in the backend editing a content type item and, if the relation field is blank, throw a validation error and to stop the saving process.

So, I now that this process is manages, in Strapi, by (in Strapi/lib/services/entity-service.js):

async update({ params, data, files }, { model }) {
const modelDef = db.getModel(model);
const existingEntry = await db.query(model).findOne(params);

const isDraft = contentTypesUtils.isDraft(existingEntry, modelDef);

const validData = await entityValidator.validateEntityUpdate(modelDef, data, {
  isDraft,
});

How can I “hook” this async update method?

Thanks!

1 Like

For the default controllers and services, you do what is an overwrite method. Using the examples provided in:

In your case, you are looking at a service, depending on what you are intending to do (such as returning an error to the user) I would usually suggest modifying the controller for validation related stuff.

So for your model you will find an “empty” file at ./api/yourModel/controllers/yourModel.js where you would put something like:

const { parseMultipartData, sanitizeEntity } = require('strapi-utils');

module.exports = {
  /**
   * Update a record.
   *
   * @return {Object}
   */

  async update(ctx) {
    const { id } = ctx.params;

    if (!missing something) {
      return ctx.badRequest('Some error message')
    }

    let entity;
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
      entity = await strapi.services.restaurant.update({ id }, data, {
        files,
      });
    } else {
      entity = await strapi.services.restaurant.update({ id }, ctx.request.body);
    }

    return sanitizeEntity(entity, { model: strapi.models.restaurant });
  },
};

@DMehaffy Is there a way to make these model controllers work with the content-manager? I think this was the initial question of the thread. As far as I understand (and I was very surprised by that): model controllers don’t run on content manager crud actions. Is that right?

What I’m trying to implement is an isOwner check, where the editors can assign an owner to a certain entity and only this (admin) user should be allowed to update this page. The documentation mentions this should be done in the API controller of the content type, by I can’t make it work - nothing inside the controller (create / update) is executed by the content manager …

Do you have any suggestions regarding that? Is that something which is planned for v4?

For the Admin panel that would be our custom conditionals, which can only be customized in our enterprise edition. It is possible to do using extensions to the content-manager plugin but it’s a bit of a dirty solution that we want to get rid of in the v4 (at least the way it’s done right now, possibly replaced by a hook system).

The logic you are looking for is the conditionals system though: Configurations - Strapi Developer Documentation

@DMehaffy

Thanks for your quick response. So far I had not seen the documentation for conditionals. We are fresh EE users, so yes, we could use that. Do you think something like the following would work (pseudocode):

const condition = {
  displayName: "Groups should only be allowed to edit their own pages",
  name: "group-is-owner",
  async handler(user) {
    if (user.role === 'group') {
      return {
        author: {
          $eq: user.id
        }
      };
    }

    return true;
  },
};

I suppose that could work for some time, but it’s not very clear, since this logic should really live inside of a common controller.

It is possible to do using extensions to the content-manager plugin but it’s a bit of a dirty solution that we want to get rid of in the v4 (at least the way it’s done right now, possibly replaced by a hook system).

Yeah, I went into that rabbithole, but it’s not as easy as I imagined it to be (because the docs don’t mention anything about controllers not being executed by the content manager). Isn’t that a very basic usecase, to be able to change a previously set owner of an entity?

Do I understand you correctly: there will be a new hook system in v4, which will allow to hook into content manager actions? In case the idea of controllers is kept around I’d really appreciate an (internal) discussion if these shouldn’t be used everywhere, so there is a central place for the logic of a content type. Right now, there are so many concepts which all seem to fit somehow. I would have expected to solve this task I have by using a policy and some way to enforce that on all CRUD operations of the model.

Thanks and have a nice weekend!

1 Like

I believe so, I’ll be honest I have only lightly played with the conditionals since my engineering team advised me they are very unstable and subject to change quickly. We originally built that system for the features we were building are slowly starting to tweak it to allow for better user control.

Indeed, the v4 will make an even clearly distinction since we are completely splitting what we call the “Content API” (End-user code) and the “Admin API” (Admin panel).

I believe that’s the plan, I haven’t seen any of our internal information about it (I don’t believe we have built it yet since we are still working on the database/query layer and design system right now).

hi, I’m trying too, to customize a create and update method used in the backend of strapi.
I saw that I can customize the content manager plugin like :
extensions/content/manager/controllers/collection-types.js → and customize the async create(ctx) method for example.

But I just want to customize one specific content type, not all the contents types.
I could use a if(myContentType) to do that inside the create method, but I do not think it is clean.

How can I do that ?
Thanks

Hello ! :slight_smile:
As v4 is now live, I’m trying to do the same things as @aminta : customize core controllers called in the content-manager.
I tried the different solutions discussed above but it doesn’t seem to work…
Is there a way to extend core methods called in the content-manager for v4 ? :thinking:

1 Like

There are two methods, the one we have documented is for wrapping the existing controllers (which is recommended to make upgrading versions easier in the future).

What are you trying to exist and on which controller?

Thank you for the quick reply. :slight_smile:

I will try to explain my use-case: we keep a copy of all strapi data in another internal database (it is simpler to handle some logic from this other database).
We then need some data consistency between strapi db and internal db.
Each time an item is created/deleted through strapi content-manager, it must be created/deleted in our internal database. If an operation fails, everything should revert (~ saga pattern, i.e: if I create an item in strapi and the creation fails in our internal database, created item in strapi should be removed as well).
Lifecycle hooks completely cover the use-case for item creation, but it becomes harder for item deletion, especially bulk deletion.
When bulk deletion is trigger, afterDeleteMany only exposes deleted IDs. If for some reason, the deletion from our internal db fails, we must re-create deleted items in Strapi. And we can’t access them anymore as they are actually deleted. :sweat_smile:
That’s why the idea of extending core controllers by wrapping logic around the actual action is more convenient in our case: we could fetch the whole data before actually deleting it, and trigger other action after the deletion…

I hope I am clear enough, and maybe you have an idea of how we could achieve such mechanism with V4…? :pray:

Well first suggestion is not to extend controllers for this and instead extend the services but this one falls wayyyyyyyyyyyyyyyyyy outside “normal usage” and honestly I wouldn’t really advise what your doing (as you can already tell the complexity of this is mildly frustrating at a minimum).

If you don’t mind me asking, why are you duplicating this in two isolated databases? Whatever “pulls” from that other database, why not just have it call Strapi’s REST API instead? (That is kinda the point of a headless cms :stuck_out_tongue: )

Yes you are right, it’s kind of a complex way of handling things actually :sweat_smile:

We are duplicating the data from Strapi to keep the same “data management pattern” between all the data used in our system. We thought it would be more convenient to access strapi data the same way we do for other services (instead of reaching strapi API to access it), but after thinking about it and the way Strapi is built and with your feedback, it will be way more convenient to just have Strapi related data in Strapi database and reach it from Strapi API. Thanks again for your help :pray:

No problem, but yeah I strongly recommend this “federated” approach instead

Hi @DMehaffy !

I am having an issue with the new documentation. Could you please let me know what are the new wrapping that’s documented? I have tried adding a function ‘create’ into the coreController when it is being created, and also tried adding a function ‘create’ into the coreService when it is being created, but it does not seem to access any of them when I’m creating a new entry from the content manager.

What am I missing?

Thanks for your help in advance

So for v4, to update the controllers that are called when you CRUD in the Content Manager, you can now follow the flow described here:

  1. Create a file called ./src/extensions/content-manager/strapi-server.js. With following boiler plate:
module.exports = (plugin) => {
  let original = plugin.controllers.["collection-types"].find;
  plugin.controllers.["collection-types"].find = (ctx) => {
     // Do your thing
     // Call original function
     original(ctx)
  };

  return plugin;
};
  1. To find what exists, you can check node_modules/@strapi/plugin-content-manager/server/controllers; each file there corresponds to the key in plugin.controllers, and each function inside of the exports can be overwritten. (in the example I used collection-types and find respectively).
5 Likes

Note: For some controller functions (such as createOrUpdate), you need to return original(ctx):

module.exports = (plugin) => {
  let original = plugin.controllers.["collection-types"].createOrUpdate;
  plugin.controllers.["single-types"].createOrUpdate = (ctx) => {
     // Do your thing
     // Call original function
     return original(ctx);
  };

  return plugin;
};
1 Like

Well, well, WELL!

Turns out, none of the “solutions” worked for me.

What I ended up doing was creating my own inbound Webhook API, by the help of strapi generate (or yarn strapi generate ymmv).

I then set up a custom controller that listens for POST requests to /webhook and with an extensible lookup table, I fire certain functions depending on what specific event and model was in that request.

The events and models I am talking about of course come from the outbound webhook that I set up in the Strapi settings.

Now I have working, completely extensible, completely secure code, that can be set up to listen to each and every thinkable action performed in the Content Manager and act accordingly :smiley:

There really are no limits to what you can use this for, but for me, I wanted to be able to enrich address information behind the scenes with coordinates from OpenStreetMap, when a user clicks Save or Create, and it works beautifully!

In my case the code below worked. Hope this helps someone.

    let original = plugin.controllers[`collection-types`].find;
    plugin.controllers[`collection-types`].find = (ctx) => {
        // Do your thing
        // Call original function
        return original(ctx)
    };