aws-amplify / amplify-cli

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development.
Apache License 2.0
2.82k stars 821 forks source link

Many-To-Many #91

Closed rygo6 closed 4 years ago

rygo6 commented 6 years ago

Was charging forth on this amplify graphql annotation stuff, until I noticed this from the documentation:

The @connection directive enables you to specify relationships between @model object types. Currently, this supports one-to-one, one-to-many, and many-to-one relationships. An error is thrown if you try to configure a many-to-many relationship.

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?

sebastienfi commented 5 years ago

@matthieunelmes Fair enough - hopefully, this will be sorted out soon.

davidfarinha commented 5 years ago

@matthieunelmes By any chance can you share the resolvers you're using to achieve the many-to-many relationship manually?

oreillyross commented 5 years ago

+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.

matthieunelmes commented 5 years ago

@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.

davidfarinha commented 5 years ago

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?

matthieunelmes commented 5 years ago

@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.

matthieunelmes commented 5 years ago

@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

AlessandroAnnini commented 5 years ago

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!

selipso commented 5 years ago

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

davidfarinha commented 5 years ago

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
        }
      }
    }
  }
}
DabeDotCom commented 5 years ago

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 } }))
gkpty commented 5 years ago

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!!

diegopizzarello commented 5 years ago

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?

michaelcuneo commented 5 years ago

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.

michaelcuneo commented 5 years ago

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 } }))

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. 👍

flybayer commented 5 years ago

Has this feature been added?

quangctkm9207 commented 5 years ago

Is there any update about this feature?

matthieunelmes commented 5 years ago

@flybayer @quangctkm9207 Nope, still TODO:

https://github.com/aws-amplify/amplify-cli/blob/master/packages/graphql-connection-transformer/src/ModelConnectionTransformer.ts#L179

d-sandman commented 5 years ago

If it’s not done, then what is the reason for closing it?

selipso commented 5 years ago

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?

rpostulart commented 5 years ago

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
    }
  }
}
Lasim commented 5 years ago

Any updates?

sirshannon commented 5 years ago

Any update on this?

khaschuluu commented 5 years ago

+1

jzstern commented 5 years ago

Is there an estimated release date for this? Or even just an update on if it's still in development?

TeoTN commented 5 years ago

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

coreybrown89 commented 5 years ago

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.

khalidbourhaba commented 5 years ago

@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.

sebastienfi commented 5 years ago

@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.

undefobj commented 5 years ago

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:

https://github.com/aws-amplify/docs/pull/900/files?short_path=a8516a8#diff-8076c2db248c895d2ade586451fa9b55

https://github.com/aws-amplify/docs/pull/900/files?short_path=a8516a8#diff-a8516a884b9597a490fdec8e8c96ce9a

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.

devnantes44 commented 5 years ago

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?

undefobj commented 5 years ago

@devnantes44 can you look at the new examples and documentation in the link I posted above?

devnantes44 commented 5 years ago

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);
            })
undefobj commented 5 years ago

@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?

devnantes44 commented 5 years ago

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.

onerider commented 5 years ago

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:

khalidbourhaba commented 4 years ago

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.

elimence commented 4 years ago

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:

https://github.com/aws-amplify/docs/pull/900/files?short_path=a8516a8#diff-8076c2db248c895d2ade586451fa9b55

https://github.com/aws-amplify/docs/pull/900/files?short_path=a8516a8#diff-a8516a884b9597a490fdec8e8c96ce9a

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

undefobj commented 4 years ago

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.

cpiro commented 4 years ago

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!

undefobj commented 4 years ago

@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.

cpiro commented 4 years ago

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?

undefobj commented 4 years ago

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.

NotSoShaby commented 3 years ago

Why is this closed?

github-actions[bot] commented 3 years ago

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.