Open asciidisco opened 3 years ago
This would be amazing to add!
@asciidisco any news, please? It's a very needed feature.
I‘ve just worked on it yesterday (had a very busy quarter at work that left me no space for any side quests) as I‘m on vacation right now. I wanted to finish the Cache Control feature before we celebrate new years. There are just a couple of unit tests left to be written.
wow! good news!
@asciidisco do you have progress with it?
@asciidisco any news, please?
This issue is more about picking your folks brains than "just" requesting a feature. I do have a prototypical version of this running, but would like to get your input on the
before I'm going to issue a PR. Generally speaking, it adds features to
fastify-gql
that enable plain old HTTP caching methods (namelyCache-Control
Headers &Last-Modified
Headers) to be generated, as well as adhering to theextensions
&directive
format created & implemented by Apollo.Cache-Control
Apollo implements an interface (steered by directives or a programmatic api) to describe the cache expiration (max-age) and scope as well as to distill fine grained data via the
extensions
response propertyout of it & to generate
Cache-Control
Headers.Response Format
Slightly modified, originally taken from the Apollo docs:
Apollo Cache Control exposes cache control hints for an individual request under a
cacheControl
key inextensions
:path
is the response path in a format similar to the error result format specified in the GraphQL specification:maxAge
indicates that anything under this path shouldn't be cached for more than the specified number of seconds, unless the value is overridden on a sub-path.If
scope
is set toPRIVATE
, that indicates anything under this path should only be cached per-user, unless the value is overridden on a sub-path.PUBLIC
is the default and means anything under this path can be stored in a shared cache.This would also attach a
Cache-Control: max-age=30
header, indicating that the whole response could be cached for30 seconds
. Theextension
data could be leveraged by a client, knowing that, if this request would be issued again within the240 seconds
time window, to leave thevotes
field out of the subsequent request.The
@cacheControl
directive can be added to an individual field or to a type.Hints on a field describe the cache policy for that field itself. Given the above example,
Post.votes
can be cached for 30 seconds.Hints on a type apply to all fields that return objects of that type (possibly wrapped in lists and non-null specifiers). For example, the hint
@cacheControl(maxAge: 30)
onPost
applies to the fieldComment.post
, and the hint@cacheControl(maxAge:1000)
onComment
applies to the fieldPost.comments
in the example below:Hints on fields override hints specified on the target type. For example, the hint
@cacheControl(maxAge: 10)
onQuery.latestPost
takes precedence over the hint@cacheControl(maxAge: 30)
onPost
.Request Format
For Apollo compatibility reasons, I'd also implement the following strategy, for clients:
Clients can include cache control instructions in a request. The only specified field is
noCache
, which forces the proxy never to return a cached response, but always fetch the query from the origin.Programmatic API
It can be used within resolvers using the
info.cacheControl.setCacheHint
API programmatically, either in addition to existingcacheControl
directives, or standalone.Setting a default
maxAge
By default, root fields (ie, fields on
Query
andMutation
) and fields returning object and interface types are considered to have amaxAge
of 0 (ie, uncacheable) if they don't have a static or dynamic cache hint. (Non-root scalar fields inherit their cacheability from their parent, so that in the common case of an object type with a bunch of strings and numbers which all have the same cacheability, you just need to declare the hint on the object type.)The overall cache policy
If the overall cache policy has a non-zero
maxAge
, its scope isPRIVATE
if any hints have scopePRIVATE
, andPUBLIC
otherwise.Behaviour within a GraphQL Federation
The federation gateway checks each of the responses from downstream services for the
Cache-Control
header and settles for the lowest number of seconds given & applies it to its own, accumulated, response. If one of the downstream responses doesn't contain aCache-Control
header, none will be send by the federation gateway.The contents of the
extensions
property will be merged & included in the federated response as well.Last-Modified
Support for
Last-Modified
Headers is not implemented in Apollo, but I believe it could be beneficial, to implement it with a similar strategy likeCache-Control
Response Format
The response format would not utilize the
extension
field, it's solely purpose would be to generate a header for the original response & answer sub-sequent requests with an empty response body & aStatus code: 304
if the criteria of the request headers are met.Request Format
No special request format in form of
extensions
or alike is needed, as the client can control the behaviour by utilizing request headers.The
lastModified
directive can only be applied totype
declarations in the schema.In this example, the
Last-Modified
header would look like the following:Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
In case we have conflicting values, the date closest to the current date will be chosen. If one of the dates given does not adhere to the headers date format, no header will be send with the response.
So, given the following query:
with the response
the
Last-Modified
header would look like the following:Last-Modified: Wed, 22 Sep 2019 09:33:00 GMT
If the
author
would be requested alongside the requestno header would be generated as the
User
type hasn't been annotated with alastModified
directive (given no value has been set programmatically)Programmatic API
It can be used within resolvers using the
info.cacheControl.setLastModified
API programmatically, either in addition to existinglastModified
directives, or standalone.Behaviour within a GraphQL Federation
The federation gateway checks each of the responses from downstream services for the
Last-Modified
header and settles for the one closest to the current date & applies it to its own, accumulated, response. If one of the downstream responses doesn't contain aLast-Modiefied
header, none will be send by the federation gateway.Proposed configuration
I propose the following configuration:
cacheControl
Configuration root, set tofalse
by default. Any value aside from an object with a fitting sub-configuration ortrue
, will be treated asfalse
. Caching is not enabled by default. If set totrue
, it will use the defaults of the sub-configuration listed below.cacheControl.defaultMaxAge
Can define the maximum number of seconds of themax-age
property of theCache-Control
header and extensions. For example, if your schema defines@cacheControl(maxAge: 1000)
& the configuration option is set to30
, the response will only contain a header (and extensions property) with amax-age
of30
.The default value is
0
and will be treated as "no limit". Anything other than a positive integer value > 0 will be treated as "no limit" as well.cacheControl.extensions
Takes a boolean, if set totrue
, theextensions
property will be included in the response, if set to anything other than strict booleantrue
, noextensions
property will be included. Set tofalse
by default.cacheControl.cacheControlHeader
Takes a boolean, if set tofalse
, noCache-Control
header will be attached to the response. (The extensions property will be send, if configured)true
by default.cacheControl.lastModifiedHeader
Takes a boolean, if set tofalse
, noLast-Modified
header will be attached to the response.true
by default.cacheControl.cacheControlDirective
Takes either a boolean value or a string, all other types default tofalse
. In order to use thecacheControl
directive, a definition of it (and the corresponding enum for the scope) must be inserted into the schema. If one would only use the programmatic API, they could supplyfalse
to the configuration, which will NOT automatically insert the directive into the schema. In order to avoid conflicts with existing directives or types, astring
can be supplied to the configuration (f.e.httpCacheControl
), then the directive & the enum will be available under this specified name. The enum will always be postfixed withScope
. Defaults totrue
.cacheControl.lastModifiedDirective
Takes either a boolean value or a string, all other types default tofalse
. In order to use thelastModified
directive, a definition of it must be inserted into the schema. If one would only use the programmatic API, they could supplyfalse
to the configuration, which will NOT automatically insert the directive into the schema. In order to avoid conflicts with existing directives or types, astring
can be supplied to the configuration (f.e.httpLastModified
), then the directive will be available under this specified name. Defaults totrue
.What about ETags
ETags are deliberately left out of this, as you could make use of them already today & are taking the whole response object in consideration. So there is just no point in adding any special directives or similar to the system.
Feedback
Please let me know your thoughts, and also if you spend some time & effort implementing something in that direction as well. I'm happy for anyone who wants to join the effort and/or gives constructive feedback.