querying amplify graphql many-to-many relationship

1.4k views Asked by At

I'm exploring amplify with typescript by making a library and have set up a many-to-many connection

type Book @model {
  # ! means non-null GraphQL fields are allowed to be null by default
  id: ID!
  name: String!
  picture: String!
  genres: [BookGenre] @connection(keyName: "byBook", fields: ["id"])
  createdAt: String!
  userId: String!
}

type BookGenre
@model(queries: null)
@key(name: "byBook", fields: ["bookID", "genreID"])
@key(name: "byGenre", fields: ["genreID", "bookID"]) {
  id: ID!
  bookID: ID!
  genreID: ID!
  book: Book! @connection(fields: ["bookID"])
  genre: Genre! @connection(fields: ["genreID"])
}

type Genre @model {
  id: ID!
  name: String!
  createdAt: String!
  books: [BookGenre] @connection(keyName: "byGenre", fields: ["id"])
}

I can create a new book and add the user provided genres like this

const handleSubmit = async (evt) => {
        evt.preventDefault();
        const createdAt = new Date().toISOString();
        const bookID = hash(name);
        const book: CreateBookInput = {
            id: bookID,
            name,
            picture: "",
            createdAt: createdAt,
            userId: "faked"
        };
        try {
            await API.graphql(
                graphqlOperation(createBook, {
                    input: book
                })
            );
            for (const t of tags) {
                const genreID = hash(t)
                const genre: CreateGenreInput = {
                    id: genreID, name: t, createdAt
                }
                await API.graphql(graphqlOperation(createGenre, {input: genre}))

                const bookGenre: CreateBookGenreInput = {
                    bookID, genreID
                }
                await API.graphql(graphqlOperation(createBookGenre, {input: bookGenre}))
            }

            setName("")
        } catch (e) {
            console.error(e)
        }
    }

This might not be the best way to do it! As it is many writes for each book but I can see the genres and their join records being written to Dynamo

I want to load a page that lists all of the books.

I've done this by:

    let result;
    try {
        result = await API.graphql(graphqlOperation(listBooks));
    } catch (err) {
        console.warn(err);
        return { props: { books: [] }};
    }
    if (result.errors) {
        console.warn("Failed to fetch books. ", result.errors);
        return { props: { books: [] }};
    }
    
    const queryResult: ListBooksQuery = result.data
    if (queryResult.listBooks !== null) {
        let books = queryResult.listBooks.items
        return { props: { books }};
    }

    return { props: { books: [] }};

But this doesn't load the genres...

I'm guessing I need to query through @key(name: "byBook", fields: ["bookID", "genreID"]) but I'm not understanding how to do that by looking at the generated code...

I'm new to amplify and to graphql so am assuming I'm missing something trivial :)

What's the correct way to query for a list of all items and their children through a many-to-many relationship?

1

There are 1 answers

0
Moritz Schmitz v. Hülst On

It seems amplify does not correctly generate the queries.

I assume your generated query looks something like this:

export const listBooks = /* GraphQL */ `
  query ListBooks(
    $filter: ModelBookFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listBooks(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        name
        picture
        genres {
          nextToken
        }
        createdAt
        userId
      }
      nextToken
    }
  }
`;

In order to load the Genre as well, you need to modify the query to:

export const listBooks = /* GraphQL */ `
  query ListBooks(
    $filter: ModelBookFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listBooks(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        name
        picture
        genres {
          items {
            genre {
              name
            }
          }
          nextToken
        }
        createdAt
        userId
      }
      nextToken
    }
  }
`;

genres.items will load all the entities from the join table. genre will then load the field on the entity, which references the Genre table.