Closed rygo6 closed 4 years ago
@matthieunelmes Fair enough - hopefully, this will be sorted out soon.
@matthieunelmes By any chance can you share the resolvers you're using to achieve the many-to-many relationship manually?
+1 for out of the box many-many relationship. Having worked with Prisma it feels so much cleaner than manually having to add the 1-M and 1-M link types.
@davidfarinha sure so I've got my schema designed as:
type User @model {
id: ID!
email: String
name: String
friends: [UserFriend] @connection(name: "UserFriends")
}
# Friends Join - Create m-2-m join between users and users
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type UserFriend @model{
id: ID!
user: User! @connection(name: "UserFriends")
friend: User! @connection(name: "UserFriends")
}
Then resolver on User.friends -> UserFriendTable request:
#set( $limit = $util.defaultIfNull($context.args.limit, 10) )
{
"version": "2017-02-28",
"operation": "Query",
"query": {
"expression": "#connectionAttribute = :connectionAttribute",
"expressionNames": {
"#connectionAttribute": "userFriendUserId"
},
"expressionValues": {
":connectionAttribute": {
"S": "$context.source.id"
}
}
},
"scanIndexForward": #if( $context.args.sortDirection )
#if( $context.args.sortDirection == "ASC" )
true
#else
false
#end
#else
true
#end,
"filter": #if( $context.args.filter )
$util.transform.toDynamoDBFilterExpression($ctx.args.filter)
#else
null
#end,
"limit": $limit,
"nextToken": #if( $context.args.nextToken )
"$context.args.nextToken"
#else
null
#end,
"index": "gsi-UserFriends"
}
response:
#if( !$result )
#set( $result = $ctx.result )
#end
$util.toJson($result)
I can't remember if I had to manually write resolvers for the querys and mutations on UserFriends. Such as createUserFriend updateUserFriend etc etc. I can share those if needed.
So to create a connection between 2 users. You need to add 2 entries to UserFriend, alternating the userFriendUserId and userFriendFriendId so that when each user queries there respective friends list, they can see each other.
Thanks! Much appreciated. Also did you have to modify the queries to get around the filter fields not being created on the many-to-many mappings?
@davidfarinha Yes the way I did it, I had to stop using the amplify push
method and associated codegen which meant I had to manually add filters and hook up query and mutation resolvers manually in the AppSync schema dashboard.
This method is obviously disgusting as any future amplify push
would wipe out any changes. But it was the only way I could get it to work without the CLI throwing a tantrum.
@dogsonacid I'll take a look this eve. I'll need to spin up a project from scratch to confirm exactly what I did as it took a lot of trial and error. Also, I think a new version of the CLI has been released although I can't see anything about m-2-m in the changelog
Hi @mikeparisstuff sorry to bother you but may I ask if there is any ETA for this feature? You said here https://github.com/aws-amplify/amplify-cli/issues/91#issuecomment-424529111 that there is a TODO about this but this thing still holds me back from using this AWS service. I just need to know if it is better to wait or use some other tool Thanks!
While I think the many-to-one and one-to-many workaround presented earlier is fine, I believe the limitation really shows when trying to do many-to-many self-to-self relationships like with the user-friends example, where a friend is also a user. This example makes no sense no matter how you break down the data types and requires a --max-depth
of 7 on top of that, which would have adverse effects on performance for any app larger than a school project that uses the generated code. Having an official solution for this would be really appreciated for the self-to-self many-to-many relationships
For anyone else looking for a workaround for the User => UserFriend many-to-many scenario that wants to stay within the Amplify CLI (and without any manual edits in the AppSync console), I got it working using the following based on the answer from @matthieunelmes: 1) User/User Friend schema
type User @model {
id: ID!
userName: String!
friends: [UserFriend] @connection(name: "UserFriends")
friendsOf: [UserFriend] @connection(name: "FriendUsers")
}
type UserFriend @model {
id: ID!
user: User! @connection(name: "UserFriends")
friend: User! @connection(name: "FriendUsers")
}
2) Override user friend model filter object (adding ability to filter on the generated listUserFriends query by user id) and run amplify push.
input ModelIDFilterInput {
ne: ID
eq: ID
le: ID
lt: ID
ge: ID
gt: ID
contains: ID
notContains: ID
between: [ID]
beginsWith: ID
}
input ModelUserFriendFilterInput {
id: ModelIDFilterInput
userFriendUserId: ModelIDFilterInput
userFriendFriendId: ModelIDFilterInput
and: [ModelUserFriendFilterInput]
or: [ModelUserFriendFilterInput]
not: ModelUserFriendFilterInput
}
Example queries to show a particular user's friends (using both getUser and listUserFriends) would look like:
query listUserFriends {
listUserFriends(filter: {
userFriendUserId: {
eq:"c914d47e-2a4a-4789-8759-ecd2090678e4"
}
}) {
items {
id,
friend {
id,
userName
},
user {
id,
userName
}
},
nextToken
}
getUser(id: "c914d47e-2a4a-4789-8759-ecd2090678e4") {
id,
userName,
friends {
items {
id,
user {
id
},
friend {
id
}
}
},
friendsOf {
nextToken,
items {
id,
user {
id
},
friend {
id
}
}
}
}
}
Hi, I've gotten the Many-To-Many setup working [big thanks to @michaelcuneo for pointing out "--max-depth", above] but nowhere have I read what's the best way to DELETE a Post/User?
Simply doing:
await API.graphql(graphqlOperation(DBMutations.deletePost, { input: { id: post.id } }))
complains:
Cannot return null for non-nullable type: 'User' within parent 'PostEditor' (/deletePost/posts/items[0]/editor)
So I gather deletes don't cascade...
Next, I tried deleting the joining PostEditor
entry, instead:
await API.graphql(graphqlOperation(DBMutations.deletePostEditor, { input: { id: link.id } }))
But that gave a similar message:
Cannot return null for non-nullable type: 'User' within parent 'PostEditor' (/deletePostEditor/post)
UPDATE: Dontcha know it... Right after I posted this, I realized I had a race condition where the subsequent deletePost
was actually finishing first, so by the time deletePostEditor
actually ran, the Post it was associated with was gone/null. «smack»
I ended up synchronizing each step with Promise.all
, thusly:
let found = posts.find(p => p.id === post.id)
if (found) {
await Promise.all(
found.editors.items
.filter(link => link.post.id === found.id)
.map(link => API.graphql(graphqlOperation(DBMutations.deletePostEditor, { input: { id: link.id } })))
)
}
API.graphql(graphqlOperation(DBMutations.deletePost, { input: { id: post.id } }))
Is there a way to handle batch create items for a join table? I have a table reservation that can contain several costItems. I created a reservationCostItem join table that has a connection to the reservation and the costItem. Now my users need to be able to create a reservation with multiple reservationCostItems. Is there a way of doing this?
Thanks!!
First-class many-to-many support is a definite need for me too!
Here's how I'm currently handling many-to-many. This is for an app that tracks records for students. Each student can have many records (like book reading progress or read an internet resource). Also, one record can apply to many students (such as when two students complete the same reading assignment).
enum Subject { SCIENCE MATH LANGUAGE SOCIAL_STUDIES PHYS_ED HEALTH ART MUSIC RELIGION FOREIGN_LANGUAGE } type Student @model @auth(rules: [{allow: owner}]) { id: ID! firstName: String! lastName: String! birthdate: AWSDate! bookMaps: [BookMap]! @connection(name: "StudentBooks") resourceMaps: [ResourceMap]! @connection(name: "StudentResources") } interface Record { id: ID! week: String! subjects: [Subject] } type BookMap @model(queries: null) @auth(rules: [{allow: owner}]) { id: ID! bookRecord: BookRecord! @connection(name: "BookStudents") student: Student! @connection(name: "StudentBooks") } type BookRecord implements Record @model @auth(rules: [{allow: owner}]) { id: ID! week: String! subjects: [Subject!]! studentMaps: [BookMap!]! @connection(name: "BookStudents") title: String! progressStart: Int! progressEnd: Int! } type ResourceMap @model(queries: null) @auth(rules: [{allow: owner}]) { id: ID! resourceRecord: ResourceRecord! @connection(name: "ResourceStudents") student: Student! @connection(name: "StudentResources") } type ResourceRecord implements Record @model @auth(rules: [{allow: owner}]) { id: ID! week: String! subjects: [Subject!]! studentMaps: [ResourceMap!]! @connection(name: "ResourceStudents") location: String! notes: String }
And here's what full nested queries look like. Not super clean, eh? :)
query { listStudents { items { id firstName bookMaps { items { bookRecord { id title } } } } } listBookRecords { items { id week title studentMaps { items { student { firstName } } } } } }
Hey! Is it possible to filter BookMap by a specific Student (get books of student A)? And also, which is the best way for managing nesting objects of the response?
So far to get queries from my mapping of many to many is convoluted and kind of negates the whole purpose of GraphQL... but I've been including ID's of the relevant search requirement with the record... so it's literally inserted twice, once as the many to many link (Which can't be filtered) and once with the literal ID of the link (Which can be filtered)
i.e. for your project.
enum Subject {
SCIENCE
MATH
LANGUAGE
SOCIAL_STUDIES
PHYS_ED
HEALTH
ART
MUSIC
RELIGION
FOREIGN_LANGUAGE
}
type Student @model @auth(rules: [{allow: owner}]) {
id: ID!
firstName: String!
lastName: String!
birthdate: AWSDate!
bookMapIDs: [ID]! // Array of BookMap ID's Identical to the ID's of the list of bookMaps.
bookMaps: [BookMap]! @connection(name: "StudentBooks")
resourceMaps: [ResourceMap]! @connection(name: "StudentResources")
}
interface Record {
id: ID!
week: String!
subjects: [Subject]
}
type BookMap @model(queries: null) @auth(rules: [{allow: owner}]) {
id: ID!
bookRecord: BookRecord! @connection(name: "BookStudents")
student: Student! @connection(name: "StudentBooks")
}
type BookRecord implements Record @model @auth(rules: [{allow: owner}]) {
id: ID!
week: String!
subjects: [Subject!]!
studentMapIDs: [ID!] // Array of Students ID's Identical to the ID's of the list of studentMaps.
studentMaps: [BookMap!]! @connection(name: "BookStudents")
title: String!
progressStart: Int!
progressEnd: Int!
}
type ResourceMap @model(queries: null) @auth(rules: [{allow: owner}]) {
id: ID!
resourceRecord: ResourceRecord! @connection(name: "ResourceStudents")
student: Student! @connection(name: "StudentResources")
}
type ResourceRecord implements Record @model @auth(rules: [{allow: owner}]) {
id: ID!
week: String!
subjects: [Subject!]!
studentMapIDs: [ResourceMap!]! // Array of Students ID's Identical to the ID's of studentMaps.
studentMaps: [ResourceMap!]! @connection(name: "ResourceStudents")
location: String!
notes: String
}
query {
listStudents {
items {
id
firstName
bookMapIDs [
items
]
bookMaps {
items {
bookRecord {
id
title
}
}
}
}
}
listBookRecords {
items {
id
week
title
studentMapIDs [
items
]
studentMaps {
items {
student {
firstName
}
}
}
}
}
}
I think this is what you need?
For nesting the objects, they just come the way they come... changing --max-depth to something more than 2, will get your data back if you're finding that studentMaps is null. I typically use at least 4 for these scenarios. Anything more than that and you really need a redesign of the data because 5 or 6 deep and you're going to start getting really slow data returns.
Hi, I've gotten the Many-To-Many setup working [big thanks to @michaelcuneo for pointing out "--max-depth", above] but nowhere have I read what's the best way to DELETE a Post/User?
Simply doing:
await API.graphql(graphqlOperation(DBMutations.deletePost, { input: { id: post.id } }))
complains:
Cannot return null for non-nullable type: 'User' within parent 'PostEditor' (/deletePost/posts/items[0]/editor)
So I gather deletes don't cascade...
Next, I tried deleting the joining
PostEditor
entry, instead:await API.graphql(graphqlOperation(DBMutations.deletePostEditor, { input: { id: link.id } }))
But that gave a similar message:
Cannot return null for non-nullable type: 'User' within parent 'PostEditor' (/deletePostEditor/post)
UPDATE: Dontcha know it... Right after I posted this, I realized I had a race condition where the subsequent
deletePost
was actually finishing first, so by the timedeletePostEditor
actually ran, the Post it was associated with was gone/null. «smack»I ended up synchronizing each step with
Promise.all
, thusly:let found = posts.find(p => p.id === post.id) if (found) { await Promise.all( found.editors.items .filter(link => link.post.id === found.id) .map(link => API.graphql(graphqlOperation(DBMutations.deletePostEditor, { input: { id: link.id } }))) ) } API.graphql(graphqlOperation(DBMutations.deletePost, { input: { id: post.id } }))
No deleting does not cascade, you can't just pick the top most branch and delete, and it will delete all of the nests within, you have to do it in reverse of the creation... i.e. delete a comment, then post, then image, then gallery, then blog. If it were a forum for example. I have never been able to achieve a proper delete scenario where I can delete the topmost branch and have it somehow filter down the entire tree and delete everything within the relationship. :(
Looks like I've replied to this while you were editing... Sorry about that. Yes. Looks like you've solved it. 👍
Has this feature been added?
Is there any update about this feature?
@flybayer @quangctkm9207 Nope, still TODO:
If it’s not done, then what is the reason for closing it?
How would you work around the lack of deletes cascading in many-to-many relationships? Consider the following schema:
type Document @model {
id: ID!
name: String!
body: String
tags: [TaggedDocument!]! @connection(name: "TagsForDocument")
}
type Tag @model {
id: ID!
title: String!
topic: String
documents: [TaggedDocument!]! @connection(name: "DocumentsWithTag")
}
type TaggedDocument @model {
id: ID!
tagId: String! @connection(name: "DocumentsWithTag")
docId: String! @connection(name: "TagsForDocument")
}
How would you delete a tag from a document with many tags? I understand that you would delete the TaggedDocument but with the lack of cascades, wouldn't you have to update the document first to remove the tag? What would that update query look like?
I have this schema deployed with aws amplify
type Event @model @searchable{
id: ID!
subscribers: [EventSubscribers] @connection(name: "EventEvent")
}
type EventSubscribers @model(queries: null){
id: ID!
event: Event! @connection(name: "EventEvent")
subscriber: Subscriber! @connection(name: "EventSubscriber")
}
type Subscriber @model @searchable{
id: ID!
name: String
events: [EventSubscribers] @connection(name: "EventSubscriber")
}
Then I use this query:
export const getSubscriber = `query GetSubscriber($id: ID!) {
getSubscriber(id: $id) {
id
name
events {
items {
id
subscriber
}
nextToken
}
}
}
But I receive this error
message: "Validation error of type SubSelectionRequired: Sub selection required for type null of field subscriber @ 'getSubscriber/events/items/subscriber'"
path: null
__proto__: Object
length: 1
__proto__: Array(0)
__proto__: Object
Does someone has an idea how this error occurs?
EDIT
I figured it out. I had to add sub fields:
export const getSubscriber = `query GetSubscriber($id: ID!) {
getSubscriber(id: $id) {
id
name
events {
items {
id
subscriber {
name
}
nextToken
}
}
}
Any updates?
Any update on this?
+1
Is there an estimated release date for this? Or even just an update on if it's still in development?
I don't understand why we get so poor communication here. The issue is over 1 year old and there's not a tiniest hint on a roadmap, if that's ever to be delivered or should we switch to a different product
Running into the same issue here with a deadline rapidly approaching. I'm not a huge fan of creating the additional join model, but i'm afraid I've run out of time. It would be great to know if this is something in development.
@mikeparisstuff Hello, any updates please on this ? We want to start many projects with Amplify, and we need this feature, or we will be forced to switch to another solution :(.
This is the most commented feature-request.
Thank you and keep up the good work.
@kaustavghosh06 @UnleashedMind @yuth @mikeparisstuff @haverchuck @attilah @nikhname @SwaySway Sorry to ping y'all but frustration is building up here... Maybe address the issue with a new piece of information? We know this is complex, and we're not asking for something to be delivered tomorrow.
Hi all - we have been working on this and (as noted above) it was quite complex. To ensure that this functionality was stable we rolled out a pilot into the existing mainline code branch. For us to fully support this it would help if the community members here could upgrade to the latest CLI and test out the use cases described in this documentation PR:
If you could try this out and let us know if it works/doesn't work, or if the docs have a problem then we'll be able to move this into official support and announce.
Regarding the example provided by @mikeparisstuff using two 1-M @connections and a joining @model as a workaround for many-To-many, how to pass an array of post editors as input in order to add multiple editor in a single one mutation?
So far I was able to create: A user mutation:
mutation {
createUser(input:{
username: "aUsername"
}){
username
}
}
A post mutation:
mutation {
createPost(input: {
title: "second post"
}){
title
}
}
But if I want to add multiple editors to a post, I ended up doing this mutation for every new editor:
mutation {
createPostEditor(input:{
postEditorPostId: "xxx-xxx-xxx"
postEditorEditorId: "yyy-yyy-yyy"
}){
post {
title
}
editor {
username
posts {
items {
post {
title
}
}
}
}
}
}
Finally after doing the previous mutations, I can retrieve all the editors for one post doing this query:
query {
getPost(id:"xxx-xxx-xxx"){
title
editors {
items {
editor {
username
}
}
}
}
}
Is there a better way to use this workaround, using an array of post editors for example?
@devnantes44 can you look at the new examples and documentation in the link I posted above?
I just finished to test the Many-To-Many Connections example and It worked for me. However, I still can't figure it out how to use only one mutation (and if it's even possible) to achieve the same result as the one given in the documentation. For example:
mutation CreateLinks {
p1u1: createPostEditor(input: { id: "P1U1", postID: "P1", editorID: "U1" }) {
id
}
p1u2: createPostEditor(input: { id: "P1U2", postID: "P1", editorID: "U2" }) {
id
}
}
So far I was able to handle this with promise, but I'm not sure if this is the best approach (because it involves to execute as much mutations as there are users):
const users = [{id: "U1", username: "user1"}, {id: "U2", username: "user2"}];
const post = { id: "P1", title: "Post 1" };
/*
After creating two users and a post using the approriate mutations
Using the CreatePost join below to make user1 and user2 editor on Post 1
*/
function graphqlCreatePostEditor(editorID) {
return new Promise((resolve, reject) => {
resolve(
API.graphql(graphqlOperation(createPostEditor, {
input: {
postID: post.id,
editorID
}
}))
)
})
}
let promises = users.map(user=> {
return graphqlCreatePostEditor(user.id)
.then(e => {
console.log(e)
return e;
})
});
Promise.all(promises)
.then(results => {
console.log(results)
})
.catch(e => {
console.error(e);
})
@devnantes44 it sounds like you're ok with the functionality in the documentation I linked to for testing the new Many-To-Many with @key
and @connection
is that correct? But now you're looking for guidance on aliased GraphQL operations in a single request. If that's the case could you open a new issue for that?
Yes I tested the new functionnality Many-To-Many with @key and @connection you linked, I'm ok with it. I will open a ticket for my use case, thank you for helping.
I did try to read the documentation but it's unclear to me still if the use case we failed on is now supported: Many-to-many between products and orders. Many-to-many between products and categories. Now I want to search for products with a specific name being on all vip orders having certain categories. So:
Hello all,
I'm struggling to find the right schema for my following use case :
I have users, teams and events:
Here's my current schema :
type Team
@model
{
id: ID!
name: String!
members: [Membership] @connection(keyName: "byTeam", fields: ["id"])
}
#join model
type Membership
@model(queries: null)
@key(name: "byTeam", fields: ["teamID", "memberID"])
@key(name: "byMember", fields: ["memberID", "teamID"])
{
id: ID!
memberID: ID!
teamID: ID!
team: Team! @connection(fields: ["teamID"])
member: User! @connection(fields: ["memberID"])
}
type User
@model
{
id: ID!
name: String!
teams: [Membership] @connection(keyName: "byMember", fields: ["id"])
events: [UserEvent] @connection(keyName: "byMember", fields: ["id"])
}
#join model
type UserEvent
@model(queries: null)
@key(name: "byEvent", fields: ["eventID", "memberID"])
@key(name: "byMember", fields: ["memberID", "eventID"])
{
id: ID!
memberID: ID!
eventID: ID!
event: Event! @connection(fields: ["eventID"])
member: User! @connection(fields: ["memberID"])
}
type Event
@model
{
id: ID!
name: String!
location: String!
teamID: ID!
team: Team! @connection(fields: ["id"])
members: [UserEvent] @connection(keyName: "byEvent", fields: ["id"])
}
Am i doing this right ?
Two join models is the right way to do it ?
Any help is much appreciated.
Hi all - we have been working on this and (as noted above) it was quite complex. To ensure that this functionality was stable we rolled out a pilot into the existing mainline code branch. For us to fully support this it would help if the community members here could upgrade to the latest CLI and test out the use cases described in this documentation PR:
If you could try this out and let us know if it works/doesn't work, or if the docs have a problem then we'll be able to move this into official support and announce.
Is this ready for use or just for testing? @undefobj
We've merged the code and the docs fully and released the updates, you can find the details here:
https://aws-amplify.github.io/docs/cli-toolchain/graphql#connection https://aws-amplify.github.io/docs/cli-toolchain/graphql#data-access-patterns
@onerider @khalidbourhaba could you please go through the fully released functionality and documentation to try and match them up with your use case, and if you're still struggling please open a new issue so a team member can help with your specific schema.
For all others - thank you for your feedback on the design and patience while we worked on this feature. Please let us know in any new issues if there's additional enhancements we can make.
Thanks @undefobj and the whole team that worked on this functionality!
Can you share with us how the new implementation works? In particular, does this behave differently than @mikeparisstuff's suggestion above of using two 1-M @connection(name:...)
s and a joining @model
, and does one perform better than the other? The new syntax and documentation is a win, but our team shares @ev-dev's hope above that we might avoid defining an associating/joining/through type for every M-M connection.
Thanks!
@cpiro as the documentation shows, you use @key
and @connection
to join with a type, along with @model(queries: null)
on that type so that it doesn't generate any information in your schema as it's just an underlying implementation detail. We looked at other options and they are not very flexible and this tradeoff allows you maximum flexibility with large scale.
We understand the syntax to express a joining model (the new docs are very clear, thanks!), but what is the "underlying implementation detail" that you added recently? Does the new syntax generate GSIs or adjacency lists on the backend, or does it behave similarly to the older @connection(name:...)
syntax? Is there a performance benefit to switching to the new syntax, or does it work just the same? Is there any benefit to changing our app to use the new syntax if it already works with the older one?
We looked at other options and they are not very flexible [...]
Does this mean that there are no plans to support different storage patterns or schemas without explicit joining types? We're also not sure what you mean by "flexible"; the new docs describe exactly one way for end users to implement M-M, and e.g. everything in type PostEditor
can be inferred from the definitions of type Post
and type User
, and mutation CreateLinks
can similarly be created mechanically, so why not support that in GraphQL Transform? The joining model is a good fit when each "edge" of the connection carries data, but in the simplest (and likely most common) case:
type Post {
id: ID!
title: String!
editors: [User!]!
}
type User {
id: ID!
username: String!
posts: [Post!]!
}
an intervening type
and table to contain just a pair of ids seems not only complex for the end user but liable not to perform well. Is there other context that we're missing?
I'm sorry to push for more transparency, but our team took this thread at face value, particularly the assertion that "This is exactly the type of behavior that will be supported in the future", and have been planning development around these features. Is a different solution still on the roadmap, or should we stick with this workaround?
The documentation outlines the details on how @key
generates indexes appropriately. I think it's probably best to walk through that and reference your DynamoDB table if you'd like to inspect the details as it shows how compound keys and LSIs/GSIs are appropriately created.
On the performance benefits/behaviors, that depends on your use case and I'd recommend going through the documentation and testing so that you can match up for your business appropriately. I'm not sure what else I could offer in this space that hasn't already been covered.
We're always looking to evolve designs in the future from customer feedback, but I would say this was a very challenging feature to do and as you can see took over a year. It's better to get things out and see where solutions are good and where they can be improved. My suggestion as above would be to see where this works for you and if you have use cases not met then please open up a feature request and we can gauge community feedback.
Why is this closed?
This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.
Looking for a help forum? We recommend joining the Amplify Community Discord server *-help
channels for those types of questions.
Was charging forth on this amplify graphql annotation stuff, until I noticed this from the documentation:
Unfortunately my app is many-to-many. So that's a bummer. I am curious, when will many-to-many relationship work?
Also, could you please as advise on how to do this? I presume I can just forego the @connection annotation and then attach my own resolver which will do some kind of many-to-many look up. Will that work? Will I be able to mix the @searchable annotations with this?