2

I've been looking all over for an answer to this and I've been banging my head on the wall. I wrote a cursor based pagination example that works well with graphql and the thing is I thought I would do the same thing with authors, that I did with books and the only way I can figure out how to do this is to completely duplicate everything. On the root query there is quite a long chunk of code handling the pagination and I would hate to do that all over for the authors endpoint but I can't seem to find a way to do this while reusing the code

Here is the code

const express = require('express')
const { graphqlHTTP } = require('express-graphql')
const {
    GraphQLSchema,
    GraphQLObjectType,
    GraphQLString,
    GraphQLList,
    GraphQLInt,
    GraphQLNonNull
} = require('graphql')

const {
    PageType,
    convertNodeToCursor,
    convertCursorToNodeId
} = require('./pagination')

const app = express()

const authors = [
    { id: 1, name: "Author 1"},
    { id: 2, name: "Author 2"},
    { id: 3, name: "Author 3"}
]

const books = [
    { id: 1, title: "Book 1", authorId: 1 },
    { id: 2, title: "Book 2", authorId: 1 },
    { id: 3, title: "Book 3", authorId: 1 },
    { id: 4, title: "Book 4", authorId: 2 },
    { id: 5, title: "Book 5", authorId: 2 },
    { id: 6, title: "Book 6", authorId: 2 },
    { id: 7, title: "Book 7", authorId: 3 },
    { id: 8, title: "Book 8", authorId: 3 },
    { id: 9, title: "Book 9", authorId: 3 }
]




const Book = new GraphQLObjectType({
    name: 'Book',
    description: 'this is a book',
    fields: () => ({
        id: { type: GraphQLNonNull(GraphQLInt) },
        title: { type: GraphQLNonNull(GraphQLString) },
        authorId: { type: GraphQLNonNull(GraphQLInt) },
        author: {
            type: Author,
            resolve: ({authorId}) => {
                return authors.find(author => author.id === authorId)
            }
        }
    })
})

const Author = new GraphQLObjectType({
    name: 'Author',
    description: 'this represents the author of a book',
    fields: () => ({
        id: { type: GraphQLNonNull(GraphQLInt) },
        name: { type: GraphQLNonNull(GraphQLString) },
        books: { 
            type: GraphQLList(Book),
            resolve: ({id}) => {
                return books.filter(book => book.authorId === id)
            }
        }
    })
})



const RootQuery = new GraphQLObjectType({
    name: 'RootQueryType',
    description: 'this is the root query',
    fields: () => ({
        book: {
            type: Book,
            description: 'a single book',
            args: {
                id: { type: GraphQLInt }
            },
            resolve: (_, { id }) => {
                return books.find(book => book.id === id)
            }
        },
        author: {
            type: Author,
            description: 'a single author',
            args: {
                id: { type: GraphQLInt },
            },
            resolve: (_, { id }) => {
                return authors.find(author => author.id === id)
            }
        },
        books: {
            type: PageType(Book),
            description: 'a list of books',
            args: {
                first: { type: GraphQLInt },
                afterCursor: { type: GraphQLString }
            },
            resolve: (_, { first, afterCursor }) => {
                let afterIndex = 0

                if (typeof afterCursor === 'string') {
                    let nodeId = convertCursorToNodeId(afterCursor)
                    let nodeIndex = books.findIndex(book => book.id === nodeId)
                    if (nodeIndex >= 0) {
                        afterIndex = nodeIndex + 1 
                    }
                }
                    
                const slicedData = books.slice(afterIndex, afterIndex + first)
                console.log('sliced data: ', slicedData)
                const edges = slicedData.map(node => ({
                    node,
                    cursor: convertNodeToCursor(node)
                }))

                let startCursor = null
                let endCursor = null
                if (edges.length > 0) {
                    startCursor = convertNodeToCursor(edges[0].node)
                    endCursor = convertNodeToCursor(edges[edges.length - 1].node)
                }

                let hasNextPage = books.length > afterIndex + first

                return {
                    totalCount: books.length,
                    edges,
                    pageInfo: {
                        startCursor,
                        endCursor,
                        hasNextPage
                    }
                }
            }

        }
    })
})

const schema = new GraphQLSchema({
    query: RootQuery
})

app.use('/graphql', graphqlHTTP({
    schema,
    graphiql: true
}))

app.listen(3000, () => console.log('app running at http://localhost:3000/graphql'))

and I handle the pagination in another file here:

const {
    GraphQLString,
    GraphQLInt,
    GraphQLBoolean,
    GraphQLObjectType,
    GraphQLList,
} = require('graphql')


const Edge = (itemType) => {
    return new GraphQLObjectType({
        name: 'EdgeType',
        fields: () => ({
            node: { type: itemType },
            cursor: { type: GraphQLString }
        })
    })
}

const PageInfo = new GraphQLObjectType({
    name: 'PageInfoType',
    fields: () => ({
        startCursor: { type: GraphQLString },
        endCursor: { type: GraphQLString },
        hasNextPage: { type: GraphQLBoolean }
    })
})

const PageType = (itemType) => {
    return new GraphQLObjectType({
        name: 'PageType',
        fields: () => ({
            totalCount: { type: GraphQLInt },
            edges: { type: new GraphQLList(Edge(itemType)) },
            pageInfo: { type: PageInfo }
        })
    })
}



const convertNodeToCursor = (node) => {
    // Encoding the cursor value to Base 64 as suggested in GraphQL documentation
    return Buffer.from((node.id).toString()).toString('base64')
}

const convertCursorToNodeId = (cursor) => {
    // Decoding the cursor value from Base 64 to integer
    return parseInt(Buffer.from(cursor, 'base64').toString('ascii'))
}

module.exports = {
    PageType,
    convertNodeToCursor,
    convertCursorToNodeId
}

Now if I copy and paste the books endpoint and change it to authors, and change the type to PageType(Author) then I get another error:

Schema must contain uniquely named types but contains multiple types named "PageType".

So this clearly isn't a solution either

dbzx10299
  • 722
  • 2
  • 14

1 Answers1

1

You cannot have one EdgeType that contains Authors and another EdgeType that contains Books. Instead, you will need one AuthorEdge and one BookEdge type.

The same holds for the PageType - there can't be two different types with different fields but the same name.

The solution is relatively simple though - if you dynamically generated these types in a function, also name them dynamically:

const Edge = (itemType) => {
    return new GraphQLObjectType({
        name: itemType.name + 'Edge',
//            ^^^^^^^^^^^^^^^^^^^^^^
        fields: () => ({
            node: { type: itemType },
            cursor: { type: GraphQLString }
        })
    })
}

const PageInfo = new GraphQLObjectType({
    name: 'PageInfo',
    fields: () => ({
        startCursor: { type: GraphQLString },
        endCursor: { type: GraphQLString },
        hasNextPage: { type: GraphQLBoolean }
    })
})

const PageType = (itemType) => {
    return new GraphQLObjectType({
        name: itemType.name + 'sPage',
//            ^^^^^^^^^^^^^^^^^^^^^^^
        fields: () => ({
            totalCount: { type: GraphQLInt },
            edges: { type: new GraphQLList(Edge(itemType)) },
            pageInfo: { type: PageInfo }
        })
    })
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    Aweome Thanks! The other thing is the way I understand this is Apollo aims to help with duplication and I was wondering if there is a way to avoid copying the books query on the RootQuery where all the cursor logic is. That would be a shame to copy that whole block and use it on the authors endpoint – dbzx10299 Nov 15 '21 at 23:32
  • You mean the duplication of code if the `resolve` methods are very similar? – Bergi Nov 16 '21 at 00:03
  • 1
    Exactly, there would be an authors resolver on the RootQuery that looks identical to the books resolver. I was thinking it might be better to pull all that out into its own function somehow. – dbzx10299 Nov 16 '21 at 00:11
  • Well, not identical, since it uses a difference service/object collection/database table/example array :-) But sure, you can write a helper function that is passed that artifact and returns the resolver function. – Bergi Nov 16 '21 at 02:38
  • @chablis-omoba Not sure what you mean, the gist already *is* pulled out into 3 separate functions? – Bergi Oct 13 '22 at 21:51
  • @chablis-omoba There is no git repo. All the code is in my answer. – Bergi Oct 13 '22 at 22:21