Can I restrict a mutation to update an object based on the object itself? - enums

I have the following objects in my database:
[
{
"id": 1,
"name": "foo",
"objType": "A"
},
{
"id": 2,
"name": "bar",
"objType": "B"
}
]
And the following users:
[
{
"id": 3,
"name": "User A",
"role": "admin"
},
{
"id": 4,
"name": "User B",
"role": "client"
}
]
And I have a schema like:
enum ObjTypeEnum {
A
B
}
type MyObj {
id: Int
name: String
objType: ObjTypeEnum
}
type Mutation {
updateObj(id: Int!, name: String): MyObj
}
The user A can update any obj that he wants because he is an admin. However, the user B can only update an object only if this object is of type B.
That means:
If the user B tries to update the object 2, using the mutation updateObj(2, "new name"), this should be totally ok. However, if he tries to update the object 1, updateObj(1, "new name"), now this should return an error for this user.
My naïve solution for this is get the object in the resolver, check its type and, if is ok for the current user, then proceed with the update, otherwise throw an error. But I have the feeling I'm in the wrong direction and not using graphql properly...
Is it possible to do it using directives or something more generic, since the key that using to validate the update is an enum?

Related

How to return complex object as scalar type in GraphQL?

Let's imagine we have GraphQL API that can return an object Entity with Id and Name properties and I requested Name only:
query {
entities {
name
}
}
And it returns
{
"data": {
"entities": [
{
"name": "Name1"
},
{
"name": "Name2"
}
]
}
}
But what if I want to have only the name of entities as a scalar type? In other words, I want to have something like:
{
"data": {
"entities": [
"Name1",
"Name2"
]
}
}
Is it possible to have such result without changes on the GraphQL API side? Aliases, Fragments, etc. GraphQL has a lot of built-in query capabilities, but none of the known me can return complex objects as scalar type.
what you're asking for is almost impossible if you don't want to change the type definition for Entities.
This: 👇🏽
Entity: id: int! name: String
entities(): [Entity]
returns an array of objects with keys name and id.
To achieve what you're asking you either change Entity to be just a string or have your client reduce that object to an array of just Entity names when they receive it.
They could do something like this:
const data = {
entities: [
{
name: 'Name1',
},
{
name: 'Name2',
},
],
};
const entityNames = data.entities.reduce(
(acc, curr) => [...acc, curr.name],
[]
);
console.log(entityNames);

Aws AppSync Query erring out while using a resolver

Im new to AWS AppSync however its been pretty easy to learn and understand.
Im trying to create a resolver that when the user runs getChore(id: "") it will return all the chore information. Which its successfully doing, the problem is within the chore there are two fields: createdBy & assignedTo which are linked to a user type.
type Chore {
id: ID!
title: String
desc: String
status: String
reward: Float
retryDeduction: Float
required: Boolean
createdDate: AWSDateTime
date: AWSDateTime
interval: String
assignedTo: User
createdBy: User
}
type User {
id: ID!
age: Int
f_name: String
l_name: String
type: Int
admin: Boolean
family: Family
}
within aws appsync in trying to attach a resolver to assignedTo: User and createdBy: User so my query will look like:
query getChore {
getChore(id: "36d597c8-2c7e-4f63-93ee-38e5aa8f1d5b") {
id
...
...
assignedTo {
id
f_name
l_name
}
createdBy {
id
f_name
l_name
}
}
}
however when i fire off this query im getting an error:
The provided key element does not match the schema (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException;
which i have researched and cant seem to find the correct soltuion.
The resolver im using is:
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
}
}
return:
$util.toJson($ctx.result)
When you get the The provided key element does not match the schema error, it's because your request mapping template key doesn't match the primary key in DynamoDB. You can enable CloudWatch Logs in your Application settings to see exactly what was sent to DynamoDB.
I'm not able to know what's wrong with your template because your sample lacks some information, if you can answers the questions pertaining to your application:
- Where are the users stored? Are they stored in their own DDB table separate from the chores, and is the hash key on the users table id as well?
- In the chores table how do you know which user your chore is assignedTo or createdBy? Is there a user id stored on the chore DDB item?
- Is the request mapping template you posted corresponding to the resolver attached to Chore.assignedTo? If yes, using $ctx.args.id will actually do a GetItem based on the chore id not the user it's assigned to.
Finally, I reproduced your application and I was able to make it work with a few changes.
Prerequisites:
I have a chores and a users DynamoDB table with both having id as hash key. These two tables are mapped as datasources in AppSync.
I have one chore in the chores tables that looks like
{
"assignedTo": "1",
"createdBy": "2",
"id": "36d597c8-2c7e-4f63-93ee-38e5aa8f1d5b",
"title": "Chore1"
}
and two users in the users table:
{
"f_name": "Alice",
"id": "2",
"l_name": "Wonderland"
}
and
{
"f_name": "John",
"id": "1",
"l_name": "McCain"
}
I used your GraphQL schema
Resolvers
Resolver on Query.getChore pointing to the chores table:
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id),
}
}
Resolver on Chore.assignedTo pointing to the users table (note the $ctx.source.assignedTo instead of $ctx.args)
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.source.assignedTo),
}
}
Similarly, resolver on Chore.createdBy pointing to the users table:
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.source.createdBy),
}
}
All resolvers response mapping template use the pass-through.
Running the query
Finally, when running your query:
query getChore {
getChore(id: "36d597c8-2c7e-4f63-93ee-38e5aa8f1d5b") {
id
assignedTo {
id
f_name
l_name
}
createdBy {
id
f_name
l_name
}
}
}
I get the following results:
{
"data": {
"getChore": {
"id": "36d597c8-2c7e-4f63-93ee-38e5aa8f1d5b",
"assignedTo": {
"id": "1",
"f_name": "John",
"l_name": "McCain"
},
"createdBy": {
"id": "2",
"f_name": "Alice",
"l_name": "Wonderland"
}
}
}
}
Hope it helps!

Can I add data to a GraphQL edge?

I'm playing around with GraphQL, and I've run across the concept of connections and edges.
From what I understand it's not uncommon to see metadata on the connection, like the totalCount property in the following snippet.
type UserFriendsConnection {
pageInfo: PageInfo!
edges: [UserFriendsEdge]
totalCount: Int
}
My questions is whether it's OK to put arbitrary metadata on the edge also, and if the following would be a decent way to do that.
I felt like a query and a response would best illustrate what I'm looking for. It's the role property I want to place somewhere that makes sense.
I feel like it doesn't belong in the User type since the role describes the type of connection/relationship the User has with a Group.
# Query
{
me {
id
name
groupsConnection {
edges {
node {
id
name
membersConnection {
edges {
node {
id
name
}
role <--- HERE
}
}
}
role <--- HERE
}
}
}
}
# Response
{
"data": {
"me": {
"id": "1Edj3hZFg",
"name": "John Doe",
"groupsConnection": {
"edges": [
{
"node": {
"id": "bpQgdZweQE",
"name": "Fishing Team",
"membersConnection": {
"edges": [
{
"node": {
"id": "1Edj3hZFg",
"name": "John Doe"
},
"role": "ADMINISTRATOR" <--- HERE
},
{
"node": {
"id": "7dj37dH2d",
"name": "Rebecca Anderson"
},
"role": "MEMBER" <--- HERE
}
]
}
},
"role": "ADMINISTRATOR" <--- HERE
}
]
}
}
}
}
Connections are part of the Relay specification. Relay itself is a GraphQL client, although you can have a Relay-compliant GraphQL server without actually using Relay on the front end. According to the spec:
Edge types must have fields named node and cursor. They may have additional fields related to the edge, as the schema designer sees fit.
It's common enough to see additional fields on these types and it certainly makes sense. One word of caution though. If we have a User type, we may create a UserConnection and a UserEdge:
type UserConnection {
pageInfo: PageInfo!
egdes: [UserEdge!]!
}
type UserEdge {
cursor: String!
edge: User!
}
We can then use that connection type in all sorts of places...
type Query {
allUsers: UserConnection!
# other fields
}
type Group {
members: UserConnection!
# other fields
}
type User {
coworkers: UserConnection!
# other fields
}
However, if you add a field like role to UserEdge, that field will only make sense in the context of the members field on the Group type. It would have to return null or some dummy value in all other contexts, which can introduce unnecessary confusion.
That means, if you're going to introduce extra fields on your edge type that are relationship-dependent, you should probably create connection and edge types that are specific to that relationship:
type GroupUserConnection {
pageInfo: PageInfo!
egdes: [GroupUserEdge!]!
}
type GroupUserEdge {
cursor: String!
edge: User!
role: Role!
}
This way, you can still use a regular UserConnection for other fields and avoid clients unnecessarily requesting a role where there isn't one.

AWS AppSync: pass arguments from parent resolver to children

In AWS AppSync, arguments send on the main query don't seem to be forwarded to all children resolvers.
type Query {
article(id: String!, consistentRead: Boolean): Article
book(id: String!, consistentRead: Boolean): Book
}
type Article {
title: String!
id: String!
}
type Book {
articleIds: [String]!
articles: [Article]!
id: String!
}
when I call:
query GetBook {
book(id: 123, consistentRead: true) {
articles {
title
}
}
}
the first query to get the book receives the consistentRead param in $context.arguments, but the subsequent query to retrieve the article does not. ($context.arguments is empty)
I also tried articles(consistentRead: Boolean): [Article]! inside book but no luck.
Does anyone know if it's possible in AppSync to pass arguments to all queries part of the same request?
It is possible to pass arguments from parent to child via the response. Let me explain ...
AppSync has several containers inside $context:
arguments
stash
source
arguments and stash are always cleared before invoking a child resolver as evident from these Cloudwatch logs:
At the very end of the parent execution - arguments and stash data are present.
{
"errors": [],
"mappingTemplateType": "After Mapping",
"path": "[getLatestDeviceState]",
"resolverArn": "arn:aws:appsync:us-east-1:xxx:apis/yyy/types/Query/fields/getLatestDeviceState",
"context": {
"arguments": {
"device": "ddddd"
},
"prev": {
"result": {
"items": [
{
"version": "849",
"device": "ddddd",
"timestamp": "2019-01-29T12:18:34.504+13:00"
}
]
}
},
"stash": {"testKey": "testValue"},
"outErrors": []
},
"fieldInError": false
}
and then at the very beginning of the child resolver - arguments and stash are always blank.
{
"errors": [],
"mappingTemplateType": "Before Mapping",
"path": "[getLatestDeviceState, media]",
"resolverArn": "arn:aws:appsync:us-east-1:yyy:apis/xxx/types/DeviceStatePRODConnection/fields/media",
"context": {
"arguments": {},
"source": {
"items": [
{
"version": "849",
"device": "ddddd",
"timestamp": "2019-01-29T12:18:34.504+13:00"
}
]
},
"stash": {},
"outErrors": []
},
"fieldInError": false
}
Workaround 1 - get the argument from the previous result.
In the example above device is always present in the response of the parent resolver, so I inserted
#set($device = $util.defaultIfNullOrBlank($ctx.args.device, $ctx.source.items[0].device))
into the request mapping template of the child resolver. It will try to get the ID it needs from the arguments and then fall back onto the previous result.
Workaround 2 - add the argument to the parent response
Modify your parent resolver response template to include the arguments:
{
"items": $utils.toJson($context.result.items),
"device": "${ctx.args.device}"
}
and then retrieve it in the request mapping template of the child the same way as in the first workaround.
To achieve availability across all related resolvers (nested or those collection-entity related) for me was fine Workaround 2 (tnx Max for such a good answer) but just for child resolvers.
In another case when I needed to resolve entities from collection query (contains other fields besides entity) property added to response mapping template wasn't available anymore.
So my solution was to set it to request headers:
##Set parent query profile parameter to headers to achieve availability accross related resolvers.
#set( $headers = $context.request.headers )
$util.qr($headers.put("profile", $util.defaultIfNullOrBlank($context.args.profile, "default")))
And read this value from your nested/other request mapping templates:
#set($profile = $ctx.request.headers.profile)
This makes the parent argument available wherever I need it between related resolvers. In your case, it would be 'device' and some default value or without that part if not needed.
Add this to BookQuery Response Mapping Template
#set( $book = $ctx.result )
#set($Articles = []);
#foreach($article in $book.articles)
#set( $newArticle = $article )
$util.qr($newArticle.put("bookID", $book.id))
$util.qr($Articles.add($newArticle))
#end
$util.qr($book.put("articles", $Articles))
$util.toJson($book)
Now, every article will have bookID
You should be able to find consistentRead in $context.info.variables ($context.info.variables.consistentRead):
https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#aws-appsync-resolver-context-reference-info
You don't need to pass arguments to sub-query. Base on your schema and use-case, I think you can adjust your schema like below to have a relationship between Author and Book
type Author {
# parent's id
bookID: ID!
# author id
id: ID!
name: String!
}
type Book {
id: ID!
title: String!
author: [Author]!
}
type Mutation {
insertAuthor(bookID: ID!, id: ID!, name: String!): Author
insertBook(id: ID!, title: String!): Book
}
type Query {
getBook(id: ID!): Book
}
- Create table Author with Author.bookID as a primary key and Author.id as a sort key
- Create table Book with Book.id as a primary key
Then, you have to attach a resolver for Book.author
And here is a resolver for insertAuthor mutation
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
"bookID" : $util.dynamodb.toDynamoDBJson($ctx.args.bookID),
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
},
"attributeValues" : {
"name" : $util.dynamodb.toDynamoDBJson($ctx.args.name)
}
}
And when you do query getBook you will get a list of author that has the same book id as below
Simply in the child use $ctx.source.id where id is the parameter you need reference from the parent.

Empty data object returns in <Query> after mutation is applied for the same

Github Issue Posted Here
"apollo-boost": "^0.1.13",
"apollo-link-context": "^1.0.8",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"react-apollo": "^2.1.11",
Current Code Structure
<div>
<Query
query={FETCH_CATEGORIES_AUTOCOMPLETE}
variables={{ ...filters }}
fetchPolicy="no-cache"
>
{({ loading, error, data }) => {
console.log('category', loading, error, data); // _______Label_( * )_______
if (error) return 'Error fetching products';
const { categories } = data;
return (
<React.Fragment>
{categories && (
<ReactSelectAsync
{...this.props.attributes}
options={categories.data}
handleFilterChange={this.props.handleCategoryFilterChange}
loading={loading}
labelKey="appendName"
/>
)}
</React.Fragment>
);
}}
</Query>
<Mutation mutation={CREATE_CATEGORY}>
{createCategory => (
<div>
// category create form
</div>
)}
</Mutation>
</div>
Behavior
Initially, the query fetches data and I get list of categories inside data given in Label_( * ) .
After entering form details, the submission occurs successfully.
Issue: Then, suddenly, in the Label_( * ), the data object is empty.
How can I solve this?
Edit
These are the response:
Categories GET
{
"data": {
"categories": {
"page": 1,
"rows": 2,
"rowCount": 20,
"pages": 10,
"data": [
{
"id": "1",
"appendName": "Category A",
"__typename": "CategoryGETtype"
},
{
"id": "2",
"appendName": "Category B",
"__typename": "CategoryGETtype"
}
],
"__typename": "CategoryPageType"
}
}
}
Category Create
{
"data": {
"createCategory": {
"msg": "success",
"status": 200,
"category": {
"id": "21",
"name": "Category New",
"parent": null,
"__typename": "CategoryGETtype"
},
"__typename": "createCategory"
}
}
}
(I came across this question while facing a similar issue, which I have now solved)
In Apollo, when a mutation returns data that is used in a different query, then the results of that query will be updated. e.g. this query that returns all the todos
query {
todos {
id
description
status
}
}
Then if we mark a todo as completed with a mutation
mutation CompleteTodo {
markCompleted(id: 3) {
todo {
id
status
}
}
}
And the result is
{
todo: {
id: 3,
status: "completed"
__typename: "Todo"
}
}
Then the todo item with id 1 will have its status updated. The issue comes when the mutation tells the query it's stale, but doesn't provide enough information for the query to be updated. e.g.
query {
todos {
id
description
status
owner {
id
name
}
}
}
and
mutation CompleteTodo {
assignToUser(todoId: 3, userId: 12) {
todo {
id
owner {
id
}
}
}
}
Example result:
{
todo: {
id: 3,
owner: {
id: 12,
__typename: "User"
},
__typename: "Todo"
}
}
Imagine your app previously knew nothing about User:12. Here's what happens
The cache for Todo:3 knows that it now has owner User:12
The cache for User:12 contains just {id:12}, since the mutation didn't return the name field
The query cannot give accurate information for the name field for the owner without refetching (which doesn't happen by default). It updates to return data: {}
Possible solutions
Change the return query of the mutation to include all the fields that the query needs.
Trigger a refetch of the query after the mutation (via refetchQueries), or a different query that includes everything the cache needs
manual update of the cache
Of those, the first is probably the easiest and the fastest. There's some discussion of this in the apollo docs, but I don't see anything that describes the specific behavior of the data going empty.
https://www.apollographql.com/docs/angular/features/cache-updates.html
Tip of the hat to #clément-prévost for his comment that provided a clue:
Queries and mutations that fetch the same entity must query the same
fields. This is a way to avoid local cache issues.
After changing fetchPolicy to cache-and-network. It solved the issue. Link to fetchPolicy Documentation
Doing so, I also had to perform refetch query.

Resources