Add relationships between types
@connection
The @connection
directive enables you to specify relationships between @model
types. Currently, this supports one-to-one, one-to-many, and many-to-one relationships. You may implement many-to-many relationships using two one-to-many connections and a joining @model
type. See the usage section for details.
We also provide a fully working schema with 17 patterns related to relational designs.
Definition
directive @connection(keyName: String, fields: [String!]) on FIELD_DEFINITION
Usage
Relationships between types are specified by annotating fields on an @model
object type with the @connection
directive.
The fields
argument can be provided and indicates which fields can be queried by to get connected objects. The keyName
argument can optionally be used to specify the name of secondary index (an index that was set up using @key
) that should be queried from the other type in the relationship.
When specifying a keyName
, the fields
argument should be provided to indicate which field(s) will be used to get connected objects. If keyName
is not provided, then @connection
queries the target table's primary index.
Has one
In the simplest case, you can define a one-to-one connection where a project has one team:
type Project @model { id: ID! name: String team: Team @connection}
type Team @model { id: ID! name: String!}
You can also define the field you would like to use for the connection by populating the first argument to the fields array and matching it to a field on the type:
type Project @model { id: ID! name: String teamID: ID! team: Team @connection(fields: ["teamID"])}
type Team @model { id: ID! name: String!}
In this case, the Project type has a teamID
field added as an identifier for the team that the project belongs to. @connection
can then get the connected Team object by querying the Team table with this teamID
.
After it's transformed, you can create projects and query the connected team as follows:
mutation CreateProject { createProject(input: { name: "New Project", teamID: "a-team-id" }) { id name team { id name } }}
Note: The Project.team resolver is configured to work with the defined connection. This is done with a query on the Team table where
teamID
is passed in as an argument.
A Has One @connection can only reference the primary index of a model (ie. it cannot specify a "keyName" as described below in the Has Many section).
Has many
The following schema defines a Post that can have many comments:
type Post @model { id: ID! title: String! comments: [Comment] @connection(keyName: "byPost", fields: ["id"])}
type Comment @model @key(name: "byPost", fields: ["postID", "content"]) { id: ID! postID: ID! content: String!}
Note how a one-to-many connection needs an @key
that allows comments to be queried by the postID and the connection uses this key to get all comments whose postID is the id of the post was called on. After it's transformed, you can create comments and query the connected Post as follows:
mutation CreatePost { createPost(input: { id: "a-post-id", title: "Post Title" }) { id title }}
mutation CreateCommentOnPost { createComment( input: { id: "a-comment-id", content: "A comment", postID: "a-post-id" } ) { id content }}
And you can query a Post with its comments as follows:
query getPost { getPost(id: "a-post-id") { id title comments { items { id content } } }}
Belongs to
You can make a connection bi-directional by adding a many-to-one connection to types that already have a one-to-many connection. In this case you add a connection from Comment to Post since each comment belongs to a post:
type Post @model { id: ID! title: String! comments: [Comment] @connection(keyName: "byPost", fields: ["id"])}
type Comment @model @key(name: "byPost", fields: ["postID", "content"]) { id: ID! postID: ID! content: String! post: Post @connection(fields: ["postID"])}
After it's transformed, you can create comments with a post as follows:
mutation CreatePost { createPost(input: { id: "a-post-id", title: "Post Title" }) { id title }}
mutation CreateCommentOnPost1 { createComment( input: { id: "a-comment-id-1" content: "A comment #1" postID: "a-post-id" } ) { id content }}
mutation CreateCommentOnPost2 { createComment( input: { id: "a-comment-id-2" content: "A comment #2" postID: "a-post-id" } ) { id content }}
And you can query a Comment with its Post, then all Comments of that Post by navigating the connection:
query GetCommentWithPostAndComments { getComment(id: "a-comment-id-1") { id content post { id title comments { items { id content } } } }}
Many-to-many connections
You can implement many to many using two 1-M @connections, an @key, and a joining @model. For example:
type Post @model { id: ID! title: String! editors: [PostEditor] @connection(keyName: "byPost", fields: ["id"])}
# Create a join model and disable queries as you don't need them# and can query through Post.editors and User.poststype PostEditor @model(queries: null) @key(name: "byPost", fields: ["postID", "editorID"]) @key(name: "byEditor", fields: ["editorID", "postID"]) { id: ID! postID: ID! editorID: ID! post: Post! @connection(fields: ["postID"]) editor: User! @connection(fields: ["editorID"])}
type User @model { id: ID! username: String! posts: [PostEditor] @connection(keyName: "byEditor", fields: ["id"])}
This case is a bidirectional many-to-many which is why two @key
calls are needed on the PostEditor model. You can first create a Post and a User, and then add a connection between them with by creating a PostEditor object as follows:
mutation CreateData { p1: createPost(input: { id: "P1", title: "Post 1" }) { id } p2: createPost(input: { id: "P2", title: "Post 2" }) { id } u1: createUser(input: { id: "U1", username: "user1" }) { id } u2: createUser(input: { id: "U2", username: "user2" }) { id }}
mutation CreateLinks { p1u1: createPostEditor(input: { id: "P1U1", postID: "P1", editorID: "U1" }) { id } p1u2: createPostEditor(input: { id: "P1U2", postID: "P1", editorID: "U2" }) { id } p2u1: createPostEditor(input: { id: "P2U1", postID: "P2", editorID: "U1" }) { id }}
Note that neither the User type nor the Post type have any identifiers of connected objects. The connection info is held entirely inside the PostEditor objects.
You can query a given user with their posts:
query GetUserWithPosts { getUser(id: "U1") { id username posts { items { post { title } } } }}
Also you can query a given post with the editors of that post and can list the posts of those editors, all in a single query:
query GetPostWithEditorsWithPosts { getPost(id: "P1") { id title editors { items { editor { username posts { items { post { title } } } } } } }}
Alternative definition
The above definition is the recommended way to create relationships between model types in your API. This involves defining index structures using @key
and connection resolvers using @connection
. There is an older parameterization of @connection
that creates indices and connection resolvers that is still functional for backwards compatibility reasons. It is recommended to use @key
and the new @connection
via the fields argument.
directive @connection( name: String keyField: String sortField: String limit: Int) on FIELD_DEFINITION
This parameterization is not compatible with @key
. See the parameterization above to use @connection
with indexes created by @key.
Limit
The default number of nested objects is 100. You can override this behavior by setting the limit argument. For example:
type Post @model { id: ID! title: String! comments: [Comment] @connection(limit: 50)}
type Comment @model { id: ID! content: String!}
Generates
In order to keep connection queries fast and efficient, the GraphQL transform manages global secondary indexes (GSIs) on the generated tables on your behalf when using @connection
Note: After you have pushed a
@connection
directive you should not try to change it. If you try to change it, the DynamoDB UpdateTable operation will fail. Should you need to change a@connection
, you should add a new@connection
that implements the new access pattern, update your application to use the new@connection
, and then delete the old@connection
when it's no longer needed.