How do I respect filters, when setting up Apollo server cachehints? - apollo-server

My objective is to configure my apollo node server with server-side caching.
I struggle to understand how I would differentiate a query, depending on the filters.
With this query:
type GroupAverageType #cacheControl(maxAge: 1000) {
rounded: Float!
}
# How do I respect the groupId here - so I don't cache the same thing for different groups?
expensiveGroupAverageQuery(groupId: ID!): GroupAverageType!
How do I cache the expensiveGroupAverageQuery, but ensure that a query for groupA won't return groupB's cached data.
Thanks in advance.

Related

How to retrieve (double) nested properties in AppSync

I'm trying to figure out how I can retrieve properties which are deeply nested in AppSync. I currently have defined the dailyEnergyUsage to retrieve these levels in a lambda resolver.
Consider the following schema I'm currently using:
query SomeQuery {
me {
energy {
dailyEnergyUsage { // The resolver function is only attached to the following type
...
}
}
}
}
The me top level query/object is supposed to group authenticated queries altogether and the energy does the same but then for the related functions related to energy usage etc.
DailyEnergyUsageResolver:
Type: "AWS::AppSync::Resolver"
Properties:
ApiId: !GetAtt AppSyncApi.ApiId
FieldName: "dailyEnergyUsage"
TypeName: "Energy"
DataSourceName: !GetAtt "DailyEnergyUsageDataSource.Name"
Currently I only have the DailyEnergyUsageResolver to retrieve the energy usage which works fine if I don't have the grouping in between. But now I wanted to introduce this kind of grouping and was expecting that this would be automatically handled by AppSync but unfortunately doesn't work and returns null at the me field.
type Query {
me: AuthenticatedQuery
}
type AuthenticatedQuery {
energy: Energy
}
type Energy {
dailyEnergyUsage: [DataPoint]
}
Now my question is how can I get the grouping working within AppSync using direct Lambda resolvers or Lambda as data source?
I've tried to check the documentation of AWS and checked this Stackoverflow thread but I don't know if it is the solution and how to get it to working.

GraphQL | How to implement conditional nesting?

Please consider the following GraphQL schema:
type User {
id: ID!
events: [Event]
}
type Event {
id: ID!
user: User!
asset: Asset!
}
type Asset {
id: ID
price: Number!
name: String!
}
GraphQL is a fantastic framework for fetching nested objects, but I'm struggling to understand how conditional nesting is implemented.
Example:
I want to retrieve all events for a specific user where asset.price is greater than x.
Or
I want to retrieve all events for an asset that belongs to a list of users [].
Question: Is conditional nesting a concept in GraphQL and how is it implemented?
Side note: I use AWS AppSync and resolvers are fetching data from AWS DynamoDB.
You can define a filter/condition on any GraphQL query such as:
query {
users(permission: 'ADMIN') {
...
}
}
The permission param is passed to your resolver (say DynamoDb VTL template, Lambda etc) to be handled however you want - to GQL this is just another parameter.
You can carry this concept into nested field by creating an events resolver and you'd then call it like this:
query {
user(id: '123') {
name
events(minPrice: 200) {
nodes: {
id
eventName
eventDate
}
}
dob
...
}
}
In above case I am using a simple minPrice param but you could do more complex things such price ranges, even pass operators (eq, gt, ...). It's all irrelevant to GraphQL - all gets passed to the resolver.
How you implement that on backend depends on your setup. I use AppSync without Amplify and write my own VTL templates and build the DynamoDb request using the provided GQL fields.
Here is an SO post that shows how to create a date filter.

GraphQL Request: Determine requested resource directly out of request

unlike REST, GraphQL has only one endpoint, usually called /graphql.
I have had good experiences with REST by outsourcing the authorisation to a separate upstream service (e.g. to a proxy like Nginx / Envoy in combination with Open Policy Agent) and using the path and the HTTP verb for the decision. For example, the GET /billing route could only be used by a user with the JWT roles claim "accountant".
Now I am looking for a way to adapt this with GraphQL.
The only possibility I have found is to interpret the query in the request body, e.g.:
body: {
query: 'query {\r\n cats {\r\n id,\r\n name\r\n }\r\n}\r\n'
}
However, this seems to be quite complex and error-prone, as a lot of knowledge and logic would have to be outsourced, especially since the proxies (resp. OPA / other authorisation solutions) don't necessarily have any GraphQL capabilities.
Is there any better way to trustworthily identify which resolver / query / mutation / entity is being requested in a GraphQL request? Headers and other enrichments set by the client are not suitable here, right?
I would highly appreciate any appraoch!
That does indeed look error prone. The GraphQL docs recommend moving authorization checks to the business logic layer. Quoting their example here for completeness:
// Authorization logic lives inside postRepository
var postRepository = require('postRepository');
var postType = new GraphQLObjectType({
name: ‘Post’,
fields: {
body: {
type: GraphQLString,
resolve: (post, args, context, { rootValue }) => {
return postRepository.getBody(context.user, post);
}
}
}
});
So rather than trying to parse the query the authz check is done in the resolver. Some discussion on using OPA with GraphQL can be found in this issue from the OPA contrib repo.

AWS Amplify GraphQL filter by dynamic Cognito User Group

Given the following AWS Amplify GraphQL Schema (schema.graphql):
type Organization
#model
#auth(rules: [
{ allow: groups, groups: ["Full-Access-Admin"], mutations: [create, update, delete], queries: [list, get] },
{ allow: owner },
{ allow: groups, groupsField: "orgAdminsCognitoGroup", mutations: null, queries: [list, get] }
]) {
id: ID!
name: String!
address: String!
industry: [String]!
owner: String
orgAdminsCognitoGroup: String
}
I can filter out all organizations except the ones that belong to the current authenticated user via the following:
res = await API.graphql(graphqlOperation(listOrganizations, {
// todo: filter by owner OR by is org admin
filter: {
owner: {
eq: this.props.currentUser.username
}
}
}));
but is there anyway to also filter by the orgAdminsCognitGroup which is a dynamic group in Cognito belonging to the organization? I have not found any success trying to use an additional #model to help with the #auth rules to protect each entity.
So, the question is wanting to filter groups that the user is either the owner of, or in the 'orgAdminsCognitoGroup'?
I think it's possible, though I don't think the best way is what you had in mind. Instead, I might recommend you set up a response mapping template that does some server side filtering for you.
Specifically, you would first get the groups from the current user's auth token:
#set($claimPermissions = $ctx.identity.claims.get("cognito:groups"))
Then you could iterate over every organization in the results. If any have an owner that is the current user, add them to a response list. If they aren't, continue to check the orgAdminsCognitoGroup. You'd do that by checking whether or not $claimPermissions contains the group that the orgAdmin is set to for that organization. If it is contained, add it to the response list. If not, ignore it and continue iterating.
It would be possible, theoretically, to do this client side with the token the user has signed in with. Much in the same way the response mapping template did it, the groups the user is in are inside the token. If you crack it open and pull out the groups, you could apply the filtering there. I would recommend not doing this for security reasons, though it is possible.

What are the rules of cacheControling responses using apollo engine (using cache hints)?

I'm studying response caching and curious to know about the proper ways of response caching.
Even though there are only 2 types of cache hints, I don't really have a clear understanding of how to use them properly.
My rule of thumb is that PRIVATE scope should be implemented whenever we fetch data that only a logged in user can access.
And I feel completely lost, when it comes to maxAge. It seems that there's no difference whether the data will be cashed for 40 sec or 60...
Given an example from Apollo docs, I fail to see the reason behind caching votes in type Post for 500 seconds and setting caching for type Post to 240 (according to the docs: a smaller maxAge will override a longer one)
type Post #cacheControl(maxAge: 240) {
id: Int!
title: String
author: Author
votes: Int #cacheControl(maxAge: 500)
readByCurrentUser: Boolean! #cacheControl(scope: PRIVATE)
}
type Author #cacheControl(maxAge: 60) {
id: Int
firstName: String
lastName: String
posts: [Post]
}
Could you, please, help me understand these concepts as without them it'd be hard to move forward.
You define cache control hints on the schema, and Apollo Engine will then compute the TTL for a response based on the fields included in the query. Given the following schema:
type Post #cacheControl(maxAge: 240) {
id: Int!
title: String
author: Author
votes: Int #cacheControl(maxAge: 30)
readByCurrentUser: Boolean! #cacheControl(scope: PRIVATE)
}
A query for a post will be cacheable for 240 seconds, unless it includes votes, which will make the response only cacheable for 30 seconds.
Similarly, including readByCurrentUser will mark the entire response as private, so it won't be stored in a public cache.

Resources