Closed te-online closed 6 years ago
@te-online Are you using v3? If so, the easiest way to do this probably a query transform that returns a query with a different returning
function. The Query.returning
function controls how the json:api response is built from the query results, so overriding that lets you add custom headers.
Something like:
app.get('/api/:type(users)',
Front.transformedAPIRequest((req, query) => {
const origReturning = query.returning;
return query.resultsIn((...args) => {
const origResult = origReturning(...args);
return {
...origResult,
headers: {
...origResult.headers,
'X-Total-Count': origResult.document.primary.resources.length
}
};
})
})
);
A couple notes on the above:
query.resultsIn
returns a new Query object where the value for query.returning
is the argument you pass in. When you return that query in your query transform, the library will use the new query's returning
function to build the response.
origResult.document.primary.resources
will only be defined on requests that return an array of resources; on single resource requests, origResult.document.primary
will hold the single resource. Also, v3 is still in beta, so these details are subject to change, but any migrations should be relatively painless.
Thank you so much, this is a drop in solution and it would have taken me hours to figure out by myself! 😊 🎉 Thanks for the great library, also. Looking forward to the first v3 stable release 🤓
@ethanresnick Sorry to jump in here again. I found that this solution of course counts the documents in the response. In theory, the X-Total-Count header should contain the complete number of documents, because this value is used for pagination in my app. Can I achieve outputting the total number of documents in a similar way?
@te-online Yes. The response document generated by the library's default query.returning
function also includes the total collection size when pagination is in effect. It puts that size in meta.total
, so you just have to read that value from the document, similar to before. Something like:
app.get('/api/:type(users)',
Front.transformedAPIRequest((req, query) => {
const origReturning = query.returning;
return query.resultsIn((...args) => {
const origResult = origReturning(...args);
// meta.total will be missing if no pagination params are in use,
// in which case the collection size === the number of resources in the document,
// so fall back to reading that
const count = (origResult.document.meta && origResult.document.meta.total)
|| origResult.document.primary.values.length;
return {
...origResult,
headers: {
...origResult.headers,
'X-Total-Count': count
}
};
})
})
);
Thank you, once again!
This seems to work for me, because it also takes care of single document requests.
const transformedApiRequest = Front.transformedAPIRequest((req, query) => {
const origReturning = query.returning
return query.resultsIn((...args) => {
const origResult = origReturning(...args)
let count = 1
if(origResult.document && origResult.document.meta && origResult.document.meta.total) {
count = origResult.document.meta.total
} else if(origResult.document && origResult.document.primary.data) {
count = origResult.document.primary.data.value.data.length
}
return {
...origResult,
headers: {
...origResult.headers,
'X-Total-Count': count,
'Access-Control-Expose-Headers': 'X-Total-Count'
},
}
})
})
That looks pretty good, except for the else if
block. In that condition, and the assignment on the next line, you shouldn't access origResult.document.primary.data
, as the .data
property is intended to be "private" (and has actually been renamed to ._data
in the last couple betas). Instead, use the public origResult.document.primary.values
to get an array of the primary data values (which might be empty in the data: null
case).
These APIs don't yet have separate documentation outside the code, so it would've been hard to know that .data
wasn't meant to be public. The best way to handle this, then, is to actually look in the code to see what's public and what types are possible where. E.g., Document.primary
is public, and the values it can hold all extend MaybeDataWithLinks
(if it's not empty), which has the public values getter.
Thank you for the heads-up.
Does this look correct? (Maybe not perfect, but I'd be satisfied with “quite correct” ;-))
const transformedApiRequest = Front.transformedAPIRequest((req, query) => {
const origReturning = query.returning
return query.resultsIn((...args) => {
const origResult = origReturning(...args)
let count = 1
if(origResult.document && origResult.document.meta && origResult.document.meta.total) {
count = origResult.document.meta.total
} else if(origResult.document && origResult.document.primary) {
count = origResult.document.primary.values.length
}
return {
...origResult,
headers: {
...origResult.headers,
'X-Total-Count': count,
'Access-Control-Expose-Headers': 'X-Total-Count'
},
}
})
})
I'll keep in mind to look at the source next time instead of spamming the issues.
I think so.
What do you want X-Total-Count
to be when the primary data is null
(e.g., on an empty to-one relationship)? Right now, this code will make it zero, which is probably correct, because origResult.document.primary.values
will be an empty array in that case.
Also, origResult.document.primary.values
is always defined (if origResult.document.primary
is), so you don't really need that check in your else if
.
Another small bug is that if meta.total
is zero, it'll fail the truthiness check in your if, and the count will be reported as 1.
Thanks for your post – I'm learning a lot here 😉
What do you want X-Total-Count to be when the primary data is null
Basically, the frontend only needs the x-total-count header for pagination and therefore only for multi-document-responses. I'm not particular sure which requests fall into the category that need this header right now, so I simply added it for all responses and – on purpose – chose it to be 1 if the value is not available. This is not a general solution, thanks for pointing this out!
That it will be 0 for empty-to-one-relationships is also fine by me, since there are no entries to show.
Also, origResult.document.primary.values is always defined
I fixed this my code and also in the snippet above for copy & pasters.
I'm wondering if there is a standard regarding the total-count of a collection. When you need to implement pagination you will also need to know how many documents exist to calculate the number of pages, right? So this is an essential information for most frontends.
Thanks for your post – I'm learning a lot here 😉
Sure thing :)
I fixed this my code and also in the snippet above for copy & pasters.
👍
I'm wondering if there is a standard regarding the total-count of a collection.
There isn't a standard that I know of. I'd imagine there are some RDF vocabularies that cover this (and could be brought into JSON using JSON-LD), and maybe there are some other media types that have first class link templates, where you could express the total size as a constraint in your template's instructions for how the client should build links to individual pages. All of that's a bit beyond the scope of JSON:API, though.
I think the most natural way in JSON:API is a key in meta
, which could be associated in the future with a formal specification if/when profile extensions happen. That's why this library defaults to putting the value in meta.total
on requests where pagination is in use.
Is there a way to add a custom header that counts data-items returned in a request?
Right now for testing, I've hardcoded this to beforeRender:
Is there a hook where I can count the items in a response and set the number as a header?
Thanks, Thomas