I have JSON coming from the server which looks like:
data: {
user: {
address: {
id: "id",
city: "city",
street: "street",
.......
}
name: "name",
......
}
authentication-token: {
token: "token",
id: "id"
}
}
The idea is to store this two models (user, authentication-token) in ember store under the same names. When I gat the above mentioned response from a server, model user is saved successfully, but model authentication-token does not get saved to the store at all. When I log the data (in the adapter) before the data is passed to serializer I see that JSON has a structure which Ember-Data expects. I don't know whether the problem is that Ember-Data cannot handle two models in success at one time, and then save it to the corresponding models, or something else. Ideas?
Now it all makes sense to me. Of course, this was the problem in your last question. Anyway, ember-data's RESTAdapter can't handle this. If you're requesting a singular resource user it expects at most this user as a singular answer. Any other resource that may be "side-loaded" has to be an array. The requested singular user can either be a record under the user key or the first entry in an array unter the users key. Everything else (except meta data) has to be plural.
In case you're requesting a plural resource users, the main response must be under the users key, any sideloaded users that aren't part of the response prfixed with _ under the _users key.
For your example all this means that your json must be formatted like this:
data: {
user: {
address: {
id: "id",
city: "city",
street: "street",
.......
}
name: "name",
......
}
authentication-tokens: [{
token: "token",
id: "id"
}]
}
If you can't change your server, you'd have to configure the RESTAdapter to normalize the JSON data through normalize of the Serializer.
Related
I am working on chat functionality using AWS Amplify and I have a simple Post model in my graphql schema:
type Post
...
{
id: ID!
channelId: ID #index(
name: "byChannel", sortKeyFields: ["createdAt"],
queryField: "listPostsByChannel"
)
customerId: ID #index(
name: "byCustomer", sortKeyFields: ["postType", "createdAt"]
)
text: String!
postTempId: String
postType: String
reactions: [PostReaction] #hasMany(fields: ["id"])
createdAt: AWSDateTime
updatedAt: AWSDateTime
}
What I want to achieve is to have similar to other popular chat apps - reactions with emojis attached to each post, so I've created another table and the PostReaction model.
type PostReaction
...
{
postId: ID! #primaryKey(sortKeyFields: ["customerId", "emojiUnicode"])
customerId: String!
customerMeta: CustomerMeta
emojiUnicode: String!
createdAt: AWSDateTime
updatedAt: AWSDateTime
}
Of course, each customer could add multiple emojis to a single post, the custom primary key is for handling duplicates later.
There is one disadvantage here.
Emojis will be listed in an array in the reactions field in the post, even if it's the same emoji added by many people.
Instead of a simple array of reactions that frontend would need to merge for each post, the best would be to get a result from the AppSync query for each Post like:
...
reactions: [{
emojiUnicode: "U+1F44D",
customerIds: ["ID1234", "ID5678"],
...
}, {...}]
I thought that I can use a JSON object in the reactions field, but the DynamoDB has the max size limit for a single item which is 400KB. That's not a problem for now, but next when I will add more attributes to the Post model, and when there will be many reactions from many people at the same time, this might be an issue.
Is there an option how to achieve this in the simplest way?
Best thing to not over-complicate your schema would be to enforce a maximum number of emojis just as Slack does for example:
You can add up to 23 emoji reactions to any message, but the maximum per message is 50 unique emoji.
Other than that, you could keep an item for each emoji reacted
pk
sk
data
thread123
metadata
metadata about thread
thread123
post#001
First message in thread
thread123
post#002
Second message in thread
thread123
post#003
Third message in thread
thread123
post#003#emoji#U+1F44D
[user1, user2, user45]
thread123
post#003#emoji#U+1F33R
[user56, user8, user7, user10]
Now when you want all the data to populate a given thread on your UI, you just issue a query with the pk as a parameter:
SELECT * FROM table WHERE PK = 'thread123'
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.
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
}
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.
The API that I am connecting to isn't using JSON-API for its response format, so I am having to serialize the responses for my Ember app.
The problem I am having is that my response from the API looks like this:
{
#type: "application/x.app-name.nurse-collection+json",
#type.item: "application/x.app-name.nurse+json",
#type.collection: "application/x.app-name.nurse-collection+json",
#href: "http://192.168.33.10:3000/api/nurses{?includePatients}",
#item: "http://192.168.33.10:3000/api/nurses/{id}",
items: [
{
#type: "application/x.app-name.nurse+json",
#type.item: "application/x.app-name.nurse+json",
#type.collection: "application/x.app-name.nurse-collection+json",
#href: "http://192.168.33.10:3000/api/nurses/1{?includePatients}",
id: 1,
Nurse: "Maria Holmes",
NurseTelephoneNo: "123 4567",
NurseMobileNo: "0123345566",
Email: "someone#example.com",
Patients: {
#type: "application/x.app-name.patient-collection+json",
#href: "http://192.168.33.10:3000/api/nurses/1/patients{?name,gender,includeNurse,includeEvents,includeReferrals,includeReviews,includePanels,includeRetrospectives,includePhbs,includeDisputeStage2s,includeDisputeResolutionPanels,includeEthnicity,includeFirstLanguage,includeMaritalStatus,includeReligion,includeSecurityLevel,includeSexualOrientation,includeWhichReview}",
#rel: "http://192.168.33.10:3000/api/nurses/1/patients{?name,gender,includeNurse,includeEvents,includeReferrals,includeReviews,includePanels,includeRetrospectives,includePhbs,includeDisputeStage2s,includeDisputeResolutionPanels,includeEthnicity,includeFirstLanguage,includeMaritalStatus,includeReligion,includeSecurityLevel,includeSexualOrientation,includeWhichReview}"
}
},
{
#type: "application/x.app-name.nurse+json",
#type.item: "application/x.app-name.nurse+json",
#type.collection: "application/x.app-name.nurse-collection+json",
#href: "http://192.168.33.10:3000/api/nurses/2{?includePatients}",
id: 2,
Nurse: "Julie Smart",
NurseTelephoneNo: "543 1234",
NurseMobileNo: null,
Email: "someone#example.com",
Patients: {
#type: "application/x.app-name.patient-collection+json",
#href: "http://192.168.33.10:3000/api/nurses/2/patients{?name,gender,includeNurse,includeEvents,includeReferrals,includeReviews,includePanels,includeRetrospectives,includePhbs,includeDisputeStage2s,includeDisputeResolutionPanels,includeEthnicity,includeFirstLanguage,includeMaritalStatus,includeReligion,includeSecurityLevel,includeSexualOrientation,includeWhichReview}",
#rel: "http://192.168.33.10:3000/api/nurses/2/patients{?name,gender,includeNurse,includeEvents,includeReferrals,includeReviews,includePanels,includeRetrospectives,includePhbs,includeDisputeStage2s,includeDisputeResolutionPanels,includeEthnicity,includeFirstLanguage,includeMaritalStatus,includeReligion,includeSecurityLevel,includeSexualOrientation,includeWhichReview}"
}
},
...
]
}
So in this example, patients could be a collection, but I don't have the id's for the hasMany relationship until I have gone to that endpoint.
I'm wondering if there is a way to serialize this in a way that is JSON-API compliant, but will let me load relationship data asynchronously, once my app has decided to go to the patients endpoint.
I would write a custom serializer that fits this response type instead of converting it to the JSON-API spec.
It doesn't look like there's any information here about the patient records (ie a list of ids), so ignore it here unless you want to set the link attribute for the hasMany.
Loading relationship data asynchronously will work fine with ember-data. When you get an array of patient records and add them to the store they will be available with nurse.get('patients') as long as the loaded records each have a nurse_id.