Search code examples
node.jsmiddlewarereact-adminstrapi

Strapi & react-admin : I'd like to set 'Content-Range' header dynamically when any fetchAll query fires


I'm still a novice web developer, so please bear with me if I miss something fundamental !

I'm creating a backoffice for a Strapi backend, using react-admin.

React-admin library uses a 'data provider' to link itself with an API. Luckily someone already wrote a data provider for Strapi. I had no problem with step 1 and 2 of this README, and I can authenticate to Strapi within my React app.

I now want to fetch and display my Strapi data, starting with Users. In order to do that, quoting Step 3 of this readme : 'In controllers I need to set the Content-Range header with the total number of results to build the pagination'.

So far I tried to do this in my User controller, with no success.

What I try to achieve:

  • First, I'd like it to simply work with the ctx.set('Content-Range', ...) hard-coded in the controller like aforementioned Step 3.

  • Second, I've thought it would be very dirty to c/p this logic in every controller (not to mention in any future controllers), instead of having some callback function dynamically appending the Content-Range header to any fetchAll request. Ultimately that's what I aim for, because with ~40 Strapi objects to administrate already and plenty more to come, it has to scale.

Technical infos

node -v: 11.13.0

npm -v: 6.7.0

strapi version: 3.0.0-alpha.25.2

uname -r output: Linux 4.14.106-97.85.amzn2.x86_64

DB: mySQL v2.16


So far I've tried accessing the count() method of User model like aforementioned step3, but my controller doesn't look like the example as I'm working with users-permissions plugin.

This is the action I've tried to edit (located in project/plugins/users-permissions/controllers/User.js)

  find: async (ctx) => {
    let data = await strapi.plugins['users-permissions'].services.user.fetchAll(ctx.query);
    data.reduce((acc, user) => {
      acc.push(_.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken']));
      return acc;
    }, []);

    // Send 200 `ok`
    ctx.send(data);
  },

From what I've gathered on Strapi documentation (here and also here), context is a sort of wrapper object. I only worked with Express-generated APIs before, so I understood this snippet as 'use fetchAll method of the User model object, with ctx.query as an argument', but I had no luck logging this ctx.query. And as I can't log stuff, I'm kinda blocked.

In my exploration, I naively tried to log the full ctx object and work from there:

    // Send 200 `ok`
    ctx.send(data);
    strapi.log.info(ctx.query, ' were query');
    strapi.log.info(ctx.request, 'were request');
    strapi.log.info(ctx.response, 'were response');
    strapi.log.info(ctx.res, 'were res');
    strapi.log.info(ctx.req, 'were req');
    strapi.log.info(ctx, 'is full context')
  },

Unfortunately, I fear I miss something obvious, as it gives me no input at all. Making a fetchAll request from my React app with these console.logs print this in my terminal:

[2019-09-19T12:43:03.409Z] info  were query
[2019-09-19T12:43:03.410Z] info were request
[2019-09-19T12:43:03.418Z] info were response
[2019-09-19T12:43:03.419Z] info were res
[2019-09-19T12:43:03.419Z] info were req
[2019-09-19T12:43:03.419Z] info is full context
[2019-09-19T12:43:03.435Z] debug GET /users?_sort=id:DESC&_start=0&_limit=10& (74 ms)

While in my frontend I get the good ol' The Content-Range header is missing in the HTTP Response message I'm trying to solve.

After writing this wall of text I realize the logging issue is separated from my original problem, but if I was able to at least log ctx properly, maybe I'd be able to find the solution myself.

Trying to summarize:

  • Actual problem is, how do I set my Content-Range properly in my strapi controller ? (partially answered cf. edit 3)
  • Collateral problem n°1: Can't even log ctx object (cf. edit 2)
  • Collateral problem n°2: Once I figure out the actual problem, is it feasible to address it dynamically (basically some callback function for index/fetchAll routes, in which the model is a variable, on which I'd call the appropriate count() method, and finally append the result to my response header)? I'm not asking for the code here, just if you think it's feasible and/or know a more elegant way.

Thank you for reading through and excuse me if it was confuse; I wasn't sure which infos would be relevant, so I thought the more the better.

/edit1: forgot to mention, in my controller I also tried to log strapi.plugins['users-permissions'].services.user object to see if it actually has a count() method but got no luck with that either. Also tried the original snippet (Step 3 of aforementioned README), but failed as expected as afaik I don't see the User model being imported anywhere (the only import in User.js being lodash)

/edit2: About the logs, my bad, I just misunderstood the documentation. I now do:

    ctx.send(data);
    strapi.log.info('ctx should be : ', {ctx});
    strapi.log.info('ctx.req = ', {...ctx.req});
    strapi.log.info('ctx.res = ', {...ctx.res});
    strapi.log.info('ctx.request = ', {...ctx.request});
    ctrapi.log.info('ctx.response = ', {...ctx.response});

Ctx logs this way; also it seems that it needs the spread operator to display nested objects ({ctx.req} crash the server, {...ctx.req} is okay). Cool, because it narrows the question to what's interesting.

/edit3: As expected, having logs helps big time. I've managed to display my users (although in the dirty way). Couldn't find any count() method, but watching the data object that is passed to ctx.send(), it's equivalent to your typical 'res.data' i.e a pure JSON with my user list. So a simple .length did the trick:

    let data = await strapi.plugins['users-permissions'].services.user.fetchAll(ctx.query);
    data.reduce((acc, user) => {
      acc.push(_.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken']));
      return acc;
    }, []);
    ctx.set('Content-Range', data.length) // <-- it did the trick
    // Send 200 `ok`
    ctx.send(data);

Now starting to work on the hard part: the dynamic callback function that will do that for any index/fetchAll call. Will update once I figure it out


Solution

  • I'm using React Admin and Strapi together and installed ra-strapi-provider. A little boring to paste Content-Range header into all of my controllers, so I searched for a better solution. Then I've found middleware concept and created one that fits my needs. It's probably not the best solution, but do its job well:

    const _ = require("lodash");
    
    module.exports = strapi => {
      return {
        // can also be async
        initialize() {
          strapi.app.use(async (ctx, next) => {
            await next();
            if (_.isArray(ctx.response.body))
              ctx.set("Content-Range", ctx.response.body.length);
          });
        }
      };
    };

    I hope it helps