AWS AppSync User relations for FriendsList - amazon-web-services

I have a problem with AWS AppSync and ApolloClient.
How can I use an association between users in the Amazon Service named AppSync, that is, a connection as node and edge. What I want to do is when I follow the users, I would like to see the flow of all users with a single request.
It is the request that I want to be. How do I build a structure for this?
query {
getFeeds(id:"myUserId") {
following {
userFeed {
id
ImageDataUrl
textData
date
}
}
}
}
The schema I created is as follows
type Comments {
id: ID!
date: Int!
message: String!
user: User
}
type Feed {
id: ID!
user: User!
date: Int!
textData: String
ImageDataUrl: String
VideoDataUrl: String
likes: Like
comments: [Comments]
}
#Objects
type Like {
id: ID!
number: Int!
likers: [User]
}
}
type Query {
getAllUsers(limit: Int): [User]
}
type User {
id: ID!
name: String!
email: String!
imageUrl: String!
imageThumbUrl: String!
followers: [User]
following: [User]
userFeed: [Feed]
}
schema {
query: Query
}

This is possible in AppSync today.
To accomplish this, you could add a query field to your schema called getUser (getUser makes more sense than getFeeds in this case) and it would have a resolver which retrieves a User object from a data source.
type Query {
getAllUsers(limit: Int): [User]
getUser(id:ID!): User
}
Then, you can also add resolvers on the User.following and User.userFeed fields. The User.following resolver would query your data source and retrieve users whom somebody is following. The User.userFeed resolver would query your data source to retrieve a list of user feeds.
Both of these resolvers (User.following and User.userFeed) should utilize $context.source in the resolver's request mapping template. This variable will contain the result of your getUser resolver. The request mapping template's job is to create a query which your data source understands.
An example request mapping template which might be attached to User.following could be similar to the following. It would query a table named "Following", which has a primary partition key of id (the id of the user):
{
"version" : "2017-02-28",
"operation" : "Query",
"query" : {
## Provide a query expression. **
"expression": "id = :id",
"expressionValues" : {
":id" : {
## Use the result of getUser to populate the query parameter **
"S" : "${ctx.source.id}"
}
}
}
}
You would have to do something similar for User.userFeed resolver.
After you're all setup, you can run the below query, and the following will happen:
query {
getUser(id:"myUserId") {
following {
userFeed {
id
ImageDataUrl
textData
date
}
}
}
}
getUser resolver will run first. It will query your User data source and retrieve the user.
User.following resolver will run. It will use the result of it's parent field resolver (getUser) to query the data source for following.
User.userFeed resolver will run. It will use the result of it's parent field resolver (getUser) to query the user feed data source.

Related

Filtering List Query By Another Table's Field (a.k.a Cross-Table or Nested Filtering) in AWS Amplify GraphQL DynamoDB

Which Category is your question related to?
DynamoDB, AppSync(GraphQL)
Amplify CLI Version
4.50.2
Provide additional details e.g. code snippets
BACKGROUND:
I'm new in AWS serverless app systems and as a frontend dev, I'm quite enjoying it thanks to auto-generated APIs, tables, connections, resolvers etc. I'm using Angular/Ionic in frontend and S3, DynamoDB, AppSync, Cognito, Amplify-cli for the backend.
WHAT I HAVE:
Here is a part of my schema. I can easily use auto-generated APIs to List/Get Feedbacks with additional filters (i.e. score: { ge: 3 }). And thanks to the #connection I can see the User's details in the listed Feedback items.
type User #model #auth(rules: [{ allow: owner }]) {
id: ID!
email: String!
name: String!
region: String!
sector: String!
companyType: String!
}
type Feedback #model #auth(rules: [{ allow: owner }]) {
id: ID!
user: User #connection
score: Int!
content: String
}
WHAT I WANT:
I want to list Feedbacks based on several fields on User type, such as user's region (i.e. user.region: { contains: 'United States' }). Now I searched for a solution quite a lot like, #2311 , and I learned that amplify codegen only creates top-level filtering. In order to use cross-table filtering, I believe I need to modify resolvers, lambda functions, queries and inputs. Which, for a beginner, it looks quite complex.
WHAT I TRIED/CONSIDERED:
I tried listing all Users and Feedbacks separately and filtering them in front-end. But then the client downloads all these unnecessary data. Also because of the pagination limit, user experience takes a hit as they see an empty list and repeatedly need to click Load More button.
Thanks to some suggestions, I also thought about duplicating the User details in Feedback table to be able to search/filter them. Then the problem is that if User updates his/her info, duplicated values will be out-of-date. Also there will be too many duplicated data, as I need this feature for other tables also.
I also heard about using ElasticSearch for this problem but someone mentioned for a simple filtering he got 30$ monthly cost, so I got cold feet.
I tried the resolver solution to add a custom filtering in it. But I found that quite complex for a beginner. Also I will need this cross-table filtering in many other tables as well, so I think would be hard to manage. If that is the best-practice, I'd appreciate it if someone can guide me through it.
QUESTIONS:
What would be the easiest/beginner-friendly solution for me to achieve this cross-table filtering? I am open to alternative solutions.
Is this cross-table filtering a bad approach for a no-SQL setup? Since I need some relationship between two tables. (I thought #connection would be enough). Should I switch to an SQL setup before it is too late?
Is it possible for Amplify to auto-generate a solution for this in the future? I feel like many people are experiencing the same issue.
Thank you in advance.
Amplify, and really DynamoDB in general, requires you to think about your access patterns ahead of time. There is a lot of really good information out there to help guide you through what this thought process can look like. Particularly, I like Nader Dabit's https://dev.to/dabit3/data-modeling-in-depth-with-graphql-aws-amplify-17-data-access-patterns-4meh
At first glance, I think I would add a new #key called byCountry to the User model, which will create a new Global Secondary Index on that property for you in DDB and will give you some new query methods as well. Check out https://docs.amplify.aws/cli/graphql-transformer/key#designing-data-models-using-key for more examples.
Once you have User.getByCountry in place, you should then be able to also bring back each user's Feedbacks.
query USAUsersWithFeedbacks {
listUsersByCountry(country: "USA") {
items {
feedbacks {
items {
content
}
nextToken
}
}
nextToken
}
}
Finally, you can use JavaScript to fetch all while the nextToken is not null. You will be able to re-use this function for each country you are interested in and you should be able to extend this example for other properties by adding additional #keys.
My former answer can still be useful for others in specific scenarios, but I found a better way to achieve nested filtering when I realized you can filter nested items in custom queries.
Schema:
type User #model {
id: ID!
email: String!
name: String!
region: String!
sector: String!
companyType: String!
feedbacks: [Feedback] #connection # <-- User has many feedbacks
}
Custom query:
query ListUserWithFeedback(
$filter: ModelUserFilterInput # <-- Filter Users by Region or any other User field
$limit: Int
$nextToken: String
$filterFeedback: ModelFeedbackFilterInput # <-- Filter inner Feedbacks by Feedback fields
$nextTokenFeedback: String
) {
listUsers(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
email
name
region
sector
companyType
feedbacks(filter: $filterFeedback, nextToken: $nextTokenFeedback) {
items {
content
createdAt
id
score
}
nextToken
}
createdAt
updatedAt
}
nextToken
}
}
$filter can be something like:
{ region: { contains: 'Turkey' } }
$filterFeedback can be like:
{
and: [{ content: { contains: 'hello' }, score: { ge: 4 } }]
}
This way both Users and Feedbacks can be filtered at the same time.
Ok thanks to #alex's answers I implemented the following. The idea is instead of listing Feedbacks and trying to filter them by User fields, we list Users and collect their Feedbacks from the response:
Updated schema.graphql as follows:
type User
#model
#auth(rules: [{ allow: owner }])
#key(name: "byRegion", fields: ["region"], queryField: "userByRegion") # <-- added byRegion key {
id: ID!
email: String!
name: String!
region: String!
sector: String!
companyType: String!
feedbacks: [Feedback] #connection # <-- added feedbacks connection
}
Added userFeedbacksId parameter while calling CreateFeedback. So they will appear while listing Users.
Added custom query UserByRegionWithFeedback under src/graphql/custom-queries.graphl and used amplify codegen to build it:
query UserByRegionWithFeedback(
$region: String
$sortDirection: ModelSortDirection
$filter: ModelUserFilterInput
$limit: Int
$nextToken: String # <-- nextToken for getting more Users
$nextTokenFeedback: String # <-- nextToken for getting more Feedbacks
) {
userByRegion(
region: $region
sortDirection: $sortDirection
filter: $filter
limit: $limit
nextToken: $nextToken
) {
items {
id
email
name
region
sector
companyType
feedbacks(nextToken: $nextTokenFeedback) {
items {
content
createdAt
id
score
}
nextToken
}
createdAt
updatedAt
owner
}
nextToken
}
}
Now I call this API like the following:
nextToken = {
user: null,
feedback: null
};
feedbacks: any;
async listFeedbacks() {
try {
const res = await this.api.UserByRegionWithFeedback(
'Turkey', // <-- region: filter Users by their region, I will add UI input later
null, // <-- sortDirection
null, // <-- filter
null, // <-- limit
this.nextToken.feedback == null ? this.nextToken.user : null, // <-- User nextToken: Only send if Feedback NextToken is null
this.nextToken.feedback // <-- Feedback nextToken
);
// Get User NextToken
this.nextToken.user = res.nextToken;
// Initialize Feedback NextToken as null
this.nextToken.feedback = null;
// Loop Users in the response
res.items.map((user) => {
// Get Feedback NextToken from User if it is not null (Or else last User in the list could overrite it)
if (user.feedbacks.nextToken) {
this.nextToken.feedback = user.feedbacks.nextToken;
}
// Push the feedback items into the list to diplay in UI
this.feedbacks.push(...user.feedbacks.items);
});
} catch (error) {
this.handleError.show(error);
}
}
Lastly I added a Load More button in the UI which calls listFeedbacks() function. So if there is any Feedback NextToken, I send it to the API. (Note that multiple user feedbacks can have a nextToken).
If all feedbacks are ok and if there is a User NextToken, I send that to the API and repeat the process for new Users.
I believe this could be much simpler with an SQL setup, but this will work for now. I hope it helps others in my situation. And if there is any ideas to make this better I'm all ears.

How should be the schema on relations between microservices

I'm using NestJS + Prisma + Apollo Federation.
On microservice A is the definition of user, on microservice B is defined posts.
The relation is 1 - N, a user can have N posts.
In Prisma, datamodel of Post is defined with a String for user, since userId is a uuid.
type Post {
id: Int! #id
createdAt: DateTime! #createdAt
updatedAt: DateTime! #updatedAt
user: String!
}
In generated schema (with https://graphql-code-generator.com), Post has a attribute of type User, and this type User extends the id and a array of posts:
type Post #key(fields: "id") {
id: Int!
createdAt: DateTime!
updatedAt: DateTime!
user: User!
}
extend type User #key(fields: "id") {
id: ID! #external
posts: [Post]
}
In apollo federation, all works as expected, except when a query is made trying to link between both microservices.
On playground, if you try to query posts with its user without setting subfields, it breaks the schema and say you have to set the subfields of User, and if you set the subfields graphql responds with a message that you cannot use subfields because its type is String.
The only way that I could make this work correctly was setting in Prisma a userId field of type string and setting another field in schema called user of type User. But all the examples didn't show a field to work with db and a field to work with schema.
My question is if that is the recommended or am I missing something.
In order to get User from Post, you have to create a resolver in your post and user service.
Post Service
const resolvers = {
Post:{//before you do this you have to extend User schema which you already did.
// you are basically asking the 'User' service, which field should be used to query user.
user: ref => ({ __typename: 'User', id: ref.userId })
}
Query:{
// query resolvers
},
Mutation:{
// mutation resolvers
}
User service
const resolvers = {
User:{//the code below allows other services to extend User in their own schemas
__resolveReference: (ref, { userDataLoader }) => userDataLoader.load(ref.id),
}
Query:{
// query resolvers
},
Mutation:{
// mutation resolvers
}
Now linking arrays like [Post] must be done purely in the post service
Post Service
const resolvers = {
Post:{//before you do this you have to extend User schema which you already did.
// you are basically telling the user service, which field should be used to query user.
user: ref => ({ __typename: 'User', id: ref.user })
},
User:{
posts:(ref, args, {postDataLoader}) => getOrders(ref.postIds) //or ref.userId(foreign key)
},
Query:{
// query resolvers
},
Mutation:{
// mutation resolvers
}

AWS AppSync only returns 10 items on query on connection

I'm new to AppSync and trying to see how this works and what's the proper way to set this up.
I created schema.graphql looks like below.
type User #model {
id: String!
following: [String]
follower: [String]
journals: [Journal] #connection(name: "UserJournals", sortField: "createdAt")
notifications: [Notification] #connection(name: "UserNotifications", sortField: "createdAt")
}
type Journal #model {
id: ID!
author: User! #connection(name: "UserJournals")
privacy: String!
content: AWSJSON!
loved: [String]
createdAt: String
updatedAt: String
}
and this created queries.js automatically by AppSync.
export const getUser = `query GetUser($id: ID!) {
getUser(id: $id) {
id
following
follower
journals {
items {
id
privacy
content
loved
createdAt
updatedAt
}
nextToken
}
notifications {
items {
id
content
category
link
createdAt
}
nextToken
}
}
}
`;
I noticed that querying getUser only returns 10 journals items and not sure how to set that to more than 10 or proper way to query and add more journals into that 10 items that were queried by getUser.
Since you do not pass the limit argument explicitly in your query, the Request Mapping Template of the journals resolver defaults it to 10 items. If you would like to change this default value, go to your schema page on the AppSync console, navigate to the journals field, found under the Resolvers section of the schema page. This will then show the resolver definition for this field, and you can then update the default value of 10 to anything you like. Alternatively, you can pass this as your query argument.
FYI - This default value is defined in the amplify-cli repo on GitHub and can be found here.

AWS AppSync - Subscription to a mutation does not return the desired fields

I try to subscribe to mutations in a DynamoDB table in AWS AppSync. The schema briefly looks like follows:
type Post {
id: ID!
userId: String!
title: String
body: String!
}
input UpdatePostInput {
id: ID!
title: String
body: String
}
type Mutation {
updatePost(input: UpdatePostInput!): Post
}
type Subscription {
onUpdatePost(id: ID!): Post
#aws_subscribe(mutations: ["updatePost"])
}
Given the ID of the post, when I want to get the changes in the body of that post I tried making use of that subscription above as:
subscription OnUpdatePost {
onUpdatePost(id: "some-id") {
id
body ## This line should make the trick, but it does not
}
}
The subscription is fired -which is fine. However, the result contains only the ID and __typename, NOT the body:
{
"data": {
"onUpdatePost": {
"id": "some-id",
"__typename": "Post"
}
}
}
Having body among the fields should be enough following the guide here.
Am I missing something with this subscription setup?
Note:
The mutation works i.e. the body can be updated in the table behind the scenes.
I did not attach a resolver to the subscription entry, but there is one for the mutation. It should be this way afaik.
Subscriptions in AWS AppSync are invoked as a response to a mutation. Subscriptions are triggered from mutations and the mutation selection set is sent to subscribers.
I suspect that you aren't returning body in your updatePost mutation selection set. Add that field and the subscription will contain body e.g.
mutation {
updatePost(input: { id: "some-id" }) {
id
body
}
}

AWS Appsync resolvers for updated related tables

I am trying to write a resolver for related entities.
Here is how my schema looks.
type User{
id:ID!
name: String!
posts:[Post] #Resolver 1
}
type Post{
id:ID!,
title: String!
body: String!
}
type CreatePostInput{
id:ID!,
title: String!
body: String!
}
type mutation{
addUserPost(userid:ID!, input:CreatePostInput!): Post
}
Now I added a resolver for posts (see #resolver 1) as
{
"version" : "2017-02-28",
"operation" : "Scan",
"key": {
"userid" : { "S" : "${context.source.id}" }
},
}
Now I added a resolver for the mutation addUserPost as
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key": {
"userid" : { "S" : "${context.arguments.userid}" },
"input" : $util.dynamodb.toDynamoDBJson(${context.arguments.input})
}
}
Now when i run the query
mutation addnewposttest{
addChapterToCourse(addUserPost:"c85a0508-ee0e-4ad8-8629-34880e1c6d74",
input:{
title:"Demo",
body:"Test Body",
id: "c85a0508-c85a0-508c-85a-0508"
}){
id
}
}
I get DynamoDB:AmazonDynamoDBException as One or more parameter values were invalid: Missing the key id in the item (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: XXXXXXXXXXXX
I tried changing the data source for the second resolver, but with no luck. I did not find any good documentation form AWS except for this , but this talks about simple string data type, not for object type collection.
Can someone help me understand, how to deal with relationships in resolvers? Thanks
I was referring to two separate data tables and to store the data in two tables I need to use BatchUpdate item and specify the table names. However, I could not do it as in my usecase, i had to insert in one table and update in another table.
Finally, I ended up copying the Id in another collection and building an index on it.
This is how my final schema looked
type User{
id:ID!
name: String!
posts:[Post] #Resolver 1 -
}
type Post{
id:ID!,
userId:ID!# I will look for source.id on this field
title: String!
body: String!
}
Thanks everyone for pitching in!
As I commented, you have some update to make.
But the main problem is that your resolver template for addUserPost contains userid but it looks like you need to change it to id. It looks like your User type or the Post type don't have a field named userid