Amplify-cli: Many-To-Many

Created on 1 Sep 2018  路  93Comments  路  Source: aws-amplify/amplify-cli

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?

feature-request graphql-transformer

Most helpful comment

You can implement many to many yourself using two 1-M @connections and a joining @model. For example:

type Post @model {
  id: ID!
  title: String!
  editors: [PostEditor] @connection(name: "PostEditors")
}
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type PostEditor @model(queries: null) {
  id: ID!
  post: Post! @connection(name: "PostEditors")
  editor: User! @connection(name: "UserEditors")
}
type User @model {
  id: ID!
  username: String!
  posts: [PostEditor] @connection(name: "UserEditors")
}

You can then create Posts & Users independently and join them in a many-to-many by creating PostEditor objects. In the future we will support many to many out of the box and will manage setting up the join model for you and configuring better names for the mutations that relate objects. In the future we will also support using batch operations to create bi-directional relationships like friendships that may have some qualifying information on the joining model itself.

All 93 comments

You can implement many to many yourself using two 1-M @connections and a joining @model. For example:

type Post @model {
  id: ID!
  title: String!
  editors: [PostEditor] @connection(name: "PostEditors")
}
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type PostEditor @model(queries: null) {
  id: ID!
  post: Post! @connection(name: "PostEditors")
  editor: User! @connection(name: "UserEditors")
}
type User @model {
  id: ID!
  username: String!
  posts: [PostEditor] @connection(name: "UserEditors")
}

You can then create Posts & Users independently and join them in a many-to-many by creating PostEditor objects. In the future we will support many to many out of the box and will manage setting up the join model for you and configuring better names for the mutations that relate objects. In the future we will also support using batch operations to create bi-directional relationships like friendships that may have some qualifying information on the joining model itself.

Looking forward to more support for association features, having to explicitly define the association types in cases like the example (where no additional columns are needed) can grow a bit out of hand and lead to confusions for larger data schemas that have lots of many-to-many associations. Definitely loses some of the readability that's so nice with defining data models via GraphQL SDL.

Ideally the above could be written as:

type Post {
  id: ID!
  title: String!
  editors: [User!]!
}
type User {
  id: ID!
  username: String!
  posts: [Post!]!
}

The Prisma project has support for this and it's real nice to work with. Not sure the comparison is entirely fair tho as AppSync's project focus seems to be more along integrating a variety of data sources while Prisma has been far slower in adopting more sources but is arguably more feature rich at the model declaration level

@ev-dev This is exactly the type of behavior that will be supported in the future. There is a TODO here https://github.com/aws-amplify/amplify-cli/blob/master/packages/graphql-connection-transformer/src/ModelConnectionTransformer.ts#L132 that is waiting to be worked on and will automatically handle hooking up the join table and additional mutations to enable the many to many.

@mikeparisstuff Are you able to elaborate on the details of how many-to-many connections will be modeled in DynamoDB. For example, will it create another GSI or use some form of GSI Overloading? I haven't messed around with amplify-cli in a while, but it seems like creating a GSI for every relationship will hit DynamoDB's limit of five global secondary indexes per table quite fast.

The reason I ask, is that we are evaluating the use of amplify-cli for an upcoming greenfield project. We have been using AWS Appsync and love it 馃槂 . Keep up the good work 馃憤

@affablebloke I will write up a more detailed design and post it here soon. I have been looking into both a general adjacency list approach as well overloading GSIs to get around the GSI limit issues but this may need to be made configurable because there are pros/cons to the approaches. Do you have any approaches in mind that I have not mentioned?

Thanks for the kind words! I'm glad to hear you are enjoying the service thus far!

@mikeparisstuff this service has been a god send please keep up the good work

@mikeparisstuff Sounds good 馃憤. Those are the same strategies that we use at our company.

For future reference, we have new docs on @connection here https://aws-amplify.github.io/docs/js/graphql#connection.

What is the best way to use this relationship in practice? I have my Many to Many relationships setup but it is unclear what the best way is to utilize the join table. Is it just an additional mutation of creating that join model with the previously created models that you want to relate?

@mikeparisstuff It appears to me that a recent update to the CLI has broken this approach. I believe it's recent only because I had one of these setup at one point and it was working. I thought I didn't need it anymore so I removed it. Now I'm going to add it back again and I'm getting the error below.

Cloudformation gives me the following error when attempting to create a similar many-to-many relationship:
Only one resolver is allowed per field. (Service: AWSAppSync; Status Code: 400; Error Code: BadRequestException; Request ID: 06480039-0ade-11e9-aca0-3707b99c7ce2

Edit: I re-created my project for another environment and didn't receive this error again.

What is the best way to use this relationship in practice? I have my Many to Many relationships setup but it is unclear what the best way is to utilize the join table. Is it just an additional mutation of creating that join model with the previously created models that you want to relate?

Yes, that's the way it works. You create the model using the mutation with the IDs of the two related items.

Any updates on this? because I'm about to start a new project that needs lots of many-to-many relationships, so I'm wondering what is the correct way to create the schema of if I should wait until this gets delivered

@mikeparisstuff

You can then create Posts & Users independently and join them in a many-to-many by creating PostEditor objects.

Does it mean that if I query a User Posts via the PostEditor intermediate entity, then it will cause multiple reads on DynamoDB? For instance, 20 user posts will lead to 20 reads on the Posts table instead of one if I want to display a Post title for each PostEditor link in my UI. It may consume the DynamoDB throughput inefficiently. Is it recommended to denormalize data from User and Post into PostEditor manually to prevent the mentioned extra data requests? Is it going to be solved somehow in the future?

I'm waiting too for this update. I heavily need many-to-many relationship for my new project and I would prefer to avoid adding models just to create this relationship...

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

++

So what would be the mutation call if I want to create a new blogpost and also create and attach the associated comments.
Or do I have to fire multiple mutation calls to realize this?

+1 on this as well, from the perspective of someone evaluating options, this particular issue presents as a pain point.

In the future we will support many to many out of the box and will manage setting up the join model for you and configuring better names for the mutations that relate objects.

How long until The Future? I'm ready for my flying car but I _need_ many-to-many.

[鈥 I had one of these setup at one point and it was working. I thought I didn't need it anymore so I removed it. Now I'm going to add it back again and I'm getting the error below. [鈥

Only one resolver is allowed per field. (Service: AWSAppSync; Status Code: 400; Error Code: BadRequestException; Request ID: 06480039-0ade-11e9-aca0-3707b99c7ce2

This workaround resolved this for me without having to recreate the AppSync API.

Hi @mikeparisstuff, I have a concern with something, according to Rich Houliah in this video about DynamoDB best practices https://www.youtube.com/watch?v=HaEPXoXVf2k&t=3184s, he is recommending to use only one table and an adjacency list model for the many to many relationships. But from what I see here the intention with Amplify is to create a separate join table, something that he completely refuses in his talk. I'm already using this model in my App and I'm afraid that I won't be able to use amplify in the future because of it. Is there any chance to be aligned with DynamodDB best practices?

Future awaits! this is a very seriously missing piece, when can we expect this?

@mikeparisstuff When I get a user, I have have access to the list of posts. But, how can I get those posts to be returned sorted by a specific field?

What would be the best way to do self-relations?

Something like this

type User @model {
  id: ID!
  username: String!
  friends: [User] @connection(name: "UserFriends")
}

this is actually not a many-to-many, but when I try to do that, that's what the cli says, Many to Many connections are not yet supported.

related to #1027

@flybayer ... how do you get information in and out of this though? I have a similar Many-to-many setup, and I can't get anything to link up. I can create Projects, I can create Staff, I can create ProjectStaff (A many to many linking the two), but Staff ProjectStaff is null, and Project ProjectStaff is null. I've tried adding a single item to say ... Staff. A single item to Project. Then I see that ProjectStaff link in both is null... so I create a ProjectStaff, then add the ID from the ProjectStaff as the ID to collaborators in Project, but that won't work either because it requires an Array or them. It's like Holy Hell... In one area of my project, I do like 8 GraphQL calls to do one thing... isn't this the opposite of GraphQL's purpose?

type Project @model {
  id: ID!
  title: String!
  details: String!
  category: [String!]
  gallery: Gallery @connection(name: "ProjectGallery")
  collaborators: [ProjectStaff] @connection(name: "ProjectCollaborators")
}

type ProjectStaff @model {
  id: ID!
  project: Project! @connection(name: "ProjectCollaborators")
  staff: Staff! @connection(name: "StaffCollaborators")
}

type Staff @model {
  id: ID!
  name: String!
  firstName: String!
  lastName: String!
  mugshot: String!
  title: String!
  bio: String!
  email: String!
  website: String!
  cognitoID: ID
  group: String!
  projects: [ProjectStaff] @connection(name: "StaffCollaborators")
}

type Image @model {
  id: ID!
  key: String!
  mime: String!
  gallery: Gallery @connection(name: "ImageList")
}

type Gallery @model {
  id: ID!
  name: String!
  project: [Project] @connection(name: "ProjectGallery")
  images: [Image] @connection(name: "ImageList")
}

Guys we need urgently your help with a many-to-many relationship.

We followed this thread and followed the documentation. Maybe its the same problem from @michaelcuneo

In our case, we have a many-to-many relationship between cities and benefits. Many Cities can be assigned to one benefit and many benefits can be assigned to one city.

We have the following schema:

type City @model {
    id: ID!
    name: String!
    benefits: [CityBenefit] @connection(name: "cityBenefit")
}
type Benefit @model {
    id: ID!
    name: String!
    category: [Category] @connection
    company: String!
    description: String!
    website: String
    email: String
    phone: String
    address: String
    global: Boolean!
    cities: [CityBenefit] @connection(name: "benefitCity")
}
type Category @model {
    id: ID!
    name: String!
}
type CityBenefit @model {
    id: ID!
    city: City @connection(name: "cityBenefit")
    benefit: Benefit @connection(name: "benefitCity")
}
  1. After adding a city and a benefit, we can鈥檛 get this information in and out. the city in CityBenefit is null and same for benefit. Its the same problem from @michaelcuneo
  2. With this model, it's still not possible to filter for all benefits in a given city.

    @mikeparisstuff @flybayer you can please help us? What is missing or wrong exactly?

@michaelcuneo @marcschroeter it sounds like you can create the joins fine, but you can't read them? In my previous post I show the necessary query to get all the data. Make sure you have all the items in the correct spots. If you skip items somewhere, you will get a null result.

So when you do a query in your case... with your schema, what is Items. under BookMaps and StudentMaps, is this a query that you've custom written into the schema, or is this all autogenerated.

When I create a student in your case I'd assume the following.
I create the Student with firstName, lastName, birthday, ... and that's all ... because bookMaps doesn't exist at this point... right.
Then I create a BookRecord, with week, subjects, title, progressStart, progressEnd,... and that's all... because studentMaps doesn't exist at this point either... yes?

Now, I need to create a BookMap so that the two are linked... when I create the items, for this, do I provide the entire object as in...

type BookMap @model(queries: null) @auth(rules: [{allow: owner}]) {
  id: ID!
  bookRecord: BookRecord! @connection(name: "BookStudents")
  student: Student! @connection(name: "StudentBooks")
}

Is bookRecord the ID! of a bookRecord, or is it the entire bookRecord object. I assume it's an ID! yes? When I provide an ID! to a mapped connection in this manner, I do get data back in the connection, but it's a nextToken, which I don't know what to do with.

Am I meant to create all of the records first then mutate update them with correct data... or am I meant to provide every single part of the tree all at once? I'm calling about 6 GraphQL Operations to do one thing right now... creating Student, Creating Book Record, Creating BookMap,... is this really correct?

@flybayer @marcschroeter I have found my issue... Creating and retrieving is limited by the Tree Depth, the importance of this isn't really discussed in the documentation. I did see this asked once when setting up the GraphQL Schema initially, but didn't know what it referred to...

After dropping out of amplify serve, and running amplify codegen --max-depth 4 ... I'm now able to go further into my schema and get real data, with items, instead of NULL.

But... further to this, it's only solving half my problem, The relationships are still not being created properly. But I can access my image galleries and images.

Looks as though, if I create everything in the right order, it will successfully work as planned, I just have to ensure that I rerun amplify codeine --max-depth and change this to numbers way bigger than I had assumed... for some reason, something like Projects -> Staff -> Projects -> Items, to me that looks like 2 depth, but it's larger than 4? To go past Items into an array, which is only meant to be a singular, adds another to the list... so yeah. I've been adding --max-depth 10 now just to make sure I don't keep hitting nulls as I progress.

@marcschroeter

Add your City... Add your Benefit... then create a CityBenefit, supplying the entire object structure of the City and Benefit to the CityBenefit... not just it's ID's. And add them one by one... not all at once. I loop through mine as a map. So it adds a project/staff member, another project/staff member, another project/staff member... each time Project might be the same, Staff member might change, or Staff member might change, and Project is different... depending on the circumstance, it'll link them all up and provide the link back to the Project / Staff in the applicable areas. I have been at this all day and can guarantee now that it works, after these small changes.

After this though, you will still get null because of the depth issue... change your depth and see if they are no longer null.

Hello, I have a question about many-to-many relationship 馃憤

type City @model {
    id: ID!
    name: String!
    benefits: [CityBenefit] @connection(name: "cityBenefit")
}
type Benefit @model {
    id: ID!
    name: String!
    category: Category @connection(name: "BenefitCat")
    company: String!
    description: String!
    website: String
    email: String
    phone: String
    address: String
    global: Boolean!
    cities: [CityBenefit] @connection(name: "benefitCity")
}
type Category @model {
    id: ID!
    name: String!
    benefit: [Benefit] @connection(name: "BenefitCat")
}
type CityBenefit @model {
    id: ID!
    city: City @connection(name: "cityBenefit")
    benefit: Benefit @connection(name: "benefitCity")
}

and the query :
query getAllBenefits { listBenefits{ items{ id name category{ id name} city{ items{ city { id name } }}} } }
I am able to query for example a benefit and its category and city, my question is : is it possible to filter a benefit per city and category ? It did work to filter per city alone but both I don't know if it's possible
Do you have an idea please ?

Hi, I'm sorry, is there any ETA about this?

++

Has anyone managed to solve many-to-many self-relations? For instance users with friends.

My current attempt is:

type User @model {
  id: ID!
  email: String
  name: String
  friends: [UserFriend] @connection(name: "UserFriends")
}

type UserFriend @model {
  id: ID!
  user: User! @connection(name: "UserFriends")
  friend: User! @connection(name: "UserFriends")
}

But I'm getting
CREATE_FAILED UserFriendTable AWS::DynamoDB::Table Tue Apr 16 2019 23:15:21 GMT+0100 (British Summer Time) Property AttributeDefinitions is inconsistent with the KeySchema of the table and the secondary indexes

@matthieunelmes that doesn't look right at all... you need a friend model.

type User @model {
id: ID!
email: String
name: String
friends: [UserFriend] @connection(name: "UserFriendFriend")
}

type UserFriend @model {
id: ID!
user: User! @connection(name: "UserFriendUser")
friend: Friend! @connection(name: "UserFriendFriend")
}

type Friend @model {
id: ID!
name: String
user: [UserFriend] @connection(name: "UserFriendUser")
}

... See the User is having Many friends, and the Friend is having many users, but because they can't be directly linked to each other, they're linked to a UserFriend. So for every Friend there can be many UserFriends, and for every User there can be many UserFriends, and each link will be one link.

After you successfully create the resources though, you must ensure that you're running amplify codegen --max-depth ... with a --max-depth higher than 2. in my particular instance I need at least 7. Which is probably stupid, but it saves me from multiple calls.

When I say, mutate for listUsers, I'd get.
Level 1... User,
Level 2... items,
Level 3... Friends,
Level 4... items,
Level 5... User,
Level 6... items,
Level 7... etc...

So you're on at least --max-depth 7 to receive everything up until the items of the users within friends of users.

@michaelcuneo so, If I understand that setup correctly. To create a link between two users I'd need to insert two items.

One with userFriendUserId and userFriendFriendId into UserFriend
then friendUserId into Friend?

Wow, what a tongue twister!

Assuming the name parameter is redundant on the Friend table

Are you trying to make friends as a link of friends... ? If so then you could just have the friend model house the ID of the user that should be the friend, instead of extending that to include names or anything, but yes... it's a bit confusing at first.

Friend doesn't even need name... because the User that it links to has a name, it will just be 'id: id!, and user: [UserFriend] @connection(name: "UserFriendUser")...

In your case to add a new friend relationship, you never need to look at 'Friends' you use Users and UserFriend... to make the relationship, just make a UserFriend ... and provide it two ID's... of two different users.

In the following case I'm creating many friends for one user. I just feed it the user, and all of the friends.

async function handleCreateConnection(user, userFriends) {
  return userFriends.map(collaborator =>
    API.graphql(
      graphqlOperation(createUserFriend, {
        input: {
          UserFriendUser: firstUser.id,
          UserFriendFriend: secondUser.id,
        },
      }),
    )
      .then(result => result)
      .catch(err => err),
  );
}

Later on when I call mutation on the single user... it'll have something like this...

Users: {
  items: { 
    id: 1,
      email: blah,
      name: blah,
      friends: {
        items: {
          id: 3,
          email: blah,
          name: friend one,
        },
        {
          id: 7,
          email: blah,
          name: friend two,
        },
     },
  },
},

Easy.

That's only with the assumption that you cannot link a many to many to a single model. I'm pretty sure you cannot.

I can successfully create a many-to-many with the @connection approach above which requires 1 joining table. The issue arises when I'm trying to create a self-relating connection. So a User can have multiple friends which are of the type User

Yeah a relation to self doesn鈥檛 work does it? That wouldn鈥檛 even make sense? Or you鈥檇 end up with Users with Users in it. Sounds sensible in a lateral sense, but logically it would break it.

Hmmm, the schema compiles but it's not pushing to Amplify.

I'm getting
Cannot perform more than one GSI creation or deletion in a single update

@michaelcuneo @matthieunelmes would you mind discussing that issue somewhere else, e.g. in a separate issue here or on StackOverflow? I feel it's bloating this thread without adding a lot to the topic at hand, which is how to implement Many-to-Many natively in Amplify.

@janpapenbrock but this is about how to add many-to-many natively within Amplify. As https://github.com/aws-amplify/amplify-cli/issues/91#issuecomment-419226819 is the only given solution but in this instance, we're discussing a self-relating join which was mention also in this thread https://github.com/aws-amplify/amplify-cli/issues/91#issuecomment-475978115

Creating another issue would just be creating a duplicate of what's already being discussed here

My answers to @Matthieunelmes were directly related to implementing Many to Many. The questions relating to many to many with self referencing is also directly related to implementing Many to Many. If Matthieunelmes wants to repost in a seperate thread I鈥檒l respond accordingly.

You can implement many to many yourself using two 1-M @connections and a joining @model. For example:

type Post @model {
  id: ID!
  title: String!
  editors: [PostEditor] @connection(name: "PostEditors")
}
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type PostEditor @model(queries: null) {
  id: ID!
  post: Post! @connection(name: "PostEditors")
  editor: User! @connection(name: "UserEditors")
}
type User @model {
  id: ID!
  username: String!
  posts: [PostEditor] @connection(name: "UserEditors")
}

You can then create Posts & Users independently and join them in a many-to-many by creating PostEditor objects. In the future we will support many to many out of the box and will manage setting up the join model for you and configuring better names for the mutations that relate objects. In the future we will also support using batch operations to create bi-directional relationships like friendships that may have some qualifying information on the joining model itself.

How can I filter on PostEditor for a specific User and Post? Thanks!

@ev-dev This is exactly the type of behavior that will be supported in the future. There is a TODO here https://github.com/aws-amplify/amplify-cli/blob/master/packages/graphql-connection-transformer/src/ModelConnectionTransformer.ts#L132 that is waiting to be worked on and will automatically handle hooking up the join table and additional mutations to enable the many to many.

@mikeparisstuff @kaustavghosh06 Until when can we expect this feature? This piece is so central to database design.

@michaelcuneo I'm attempting to set up the User and UserFriend, but it then complains about "InvalidDirectiveError: Found one half of connection "UserFriendUser" at UserFriend.user but no related field on type User"

Is you examples correct?

@helloniklas the concept works, but unfortunately, it cannot currently be handled by the amplify CLI.

I managed to get it working by firstly creating the basic models such as:

type User @model {
  id: ID!
  email: String
  name: String
  profile_pic: String
  friends: [UserFriend]
}

type UserFriend @model{
    id: ID!
    user: User! 
    friend: User! 
}

Then manually creating the resolvers through the AppSync dashboard in the schema editor.
It took a lot of fiddling, and I believe you can actually get it to work through the CLI if you add one @connection at a time.

@matthieunelmes Watch out as your manual changes will be overridden by amplify push.

@sebastienfi I'm aware of this. Hence why I keep a backup of the code gen'd schema.graphql it's far from ideal but the only way to get it to work.

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

Has this feature been added?

Is there any update about this feature?

If it鈥檚 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:

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.

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:

  • Product with name X
  • Product with Categories Electronic or Jewellery
  • Product is part of Orders labeled as VIP

Hello all,

I'm struggling to find the right schema for my following use case :

I have users, teams and events:

  • A user can be member of one or many teams,
  • A Team have one or many users,
  • A team can have many events,
  • An event belongs to one team, and one or many users could attend it;

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:

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

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jkeys-ecg-nmsu picture jkeys-ecg-nmsu  路  3Comments

onlybakam picture onlybakam  路  3Comments

nicksmithr picture nicksmithr  路  3Comments

jexh picture jexh  路  3Comments

nason picture nason  路  3Comments