Intended usage & best practices on Cell.afterQuery hook?

I just discovered the afterQuery lifecycle hook in the docs and got interested. The doc section currently is as brief as brief can get, but maybe we can gather some intelligence here that could be used to add to the docs.

Two questions arose for me:

  1. “Use it to sanitize data” it says. I was wondering what the code pattern would be there, b/c the data object passed to that function is completely immutable, also any fields contained in it. Maybe it’s specific to my use case, but i always get “object is not extensible” or “Cannot assign to read only property” errors, so the only option i saw fit was to clone it, modify the clone and return that instead.
    I would assume that sealing the object is not a thing that redwood does on purpose, but rather Apollo. But then again it doesn’t make sense why it’s allowed to edit the query object retrieved from cache.readQuery, but not the default query object the cell uses. So is it true the only way is to act on a clone? Maybe somebody could provide a nice SFW example that we could eventually add to the documentation.
  2. I got interested in this hook because i have a lot of function calls rendering custom display values in my components (they consume the query and render a nice chakra UI accordion list from it) that (a.) were not very DRY and (b.) were needlessly executed over and over again on each interaction due to react renders … so i was wondering if there was a way to pre-render those “display fields” before i pass them to the components. Effectively i am now using afterQuery to add additional fields to the resultset and have extended the query type to refer to from my components. I had a quick thought about adding these fields to the service in the api, but (a): they are not part of the model, they are just “virtual” for display/frontend rendering purposes only and (b): they rely on the l10n setting of the user’s browser, so this being done the web side seems the natural spot. So, what do you think: is adding additional “for display purposes only” fields, thus generating a custom extended query type an acceptable usage of this hook – or am i going to hell for this and only not knowing it yet?
Click for a code
export type PrerenderedInterview = {
  displayTime: string
  shortDisplayDate: string
  longDisplayDate: string
} & InterviewsQuery['interviews'][0]

// https://redwoodjs.com/docs/cells#afterquery
export const afterQuery = (data: CellSuccessProps<InterviewsQuery>) => {
  const newData = { ...data }
  newData.interviews = data.interviews.map((record) => {
    return {
      ...record,
      shortDisplayDate: date.getShortDisplayDate(record.startAt),
      longDisplayDate: date.getLongDisplayDate(record.startAt),
      displayTime:
        date.getDisplayTime(record.startAt) +
        (record.startAt &&
          record.endAt &&
          ' – ' + date.getDisplayTime(record.endAt)),
    }
  })

  return newData
}

@Philzen have you looked at Transformer directives?

They can be used to transform or mask individual fields or an entire query response.

And is perfect for formatting.

Yes i remembered your words from the other threads we had on transformer directives @dthyresson :smiling_face: … so i briefly thought about that but wouldn’t know how to inform the directive about the user’s browser locale :person_shrugging: (or is it possible to modify context from the web side?).

If there’s a way to easily do that and directives are the best place to do it i will reconsider (would have to have to split or share my formatting functions on the web with the API though, would have to keep fingers crossed how that goes then).

Would love to also gather some advise on the intended (mis-)use of afterQuery (and why that query object is sealed while the query object retrieved from cache.readQuery is not) in this thread though.

You could set and send the locale in the headers or a cookie and then extend the GraphQL context with that bit of info which can then be used in the Transformer directive.

See this example that sets the ipAddress from the event … which will also have access to headers.

Wow that looks a bit of boilerplate code – and i haven’t even ever done a custom request from web to API other than the standard way RedwoodJS teaches in the tutorials (would guess that would be the fetch-api?) and also it feels like i’m not going to need the cell anymore (which currently everything else in our application builds on). What transpires to me is the feeling that i’m still missing may of pieces of the puzzle before i can refactor and move my existing implementation to a transformer – and that that would mean a major refactoring for our project. Thanks for hinting how to read values from the context header though, it’s really good to know that. I realize i’ll need to wrap my head around the GraphQL engine part or otherwise may be missing out on a lot of fun! From what i learned in the tutorials there seems to be a tight correlation between model and graphQL surface, but as you’re suggesting i could generate my custom, read-only display fields in a transformer that may not be a rule that generally applies. I guess i come back to you via another thread to look into that.

Right now, as said, it looks most feasible and natural design-wise to have all things l10n on the web-side, b/c that’s where i can directly create a cached Intl.DateTimeFormat-object with the correct l10n setting matching the user’s browser locale and render the formatted values with it, which themself will be individual for every user anyway.

So the intent of this thread is to listen in for solid arguments against using afterQuery as i currently do (or what i may even be breaking with it) and to understand how it is intended to be used are already used by people.