使用基于 GraphQL 游标的分页避免代码重复

Posted

技术标签:

【中文标题】使用基于 GraphQL 游标的分页避免代码重复【英文标题】:Avoid Code Duplication with GraphQL Cursor based Pagination 【发布时间】:2021-12-27 02:35:57 【问题描述】:

我一直在寻找这个问题的答案,我一直在用头撞墙。我写了一个基于光标的分页示例,它适用于 graphql,问题是我认为我会对作者做同样的事情,就像我对书籍所做的那样,我能弄清楚如何做到这一点的唯一方法是完全复制所有内容。在根查询中,有相当长的代码块处理分页,我不想为作者端点做这一切,但我似乎无法在重用代码时找到这样做的方法

这里是代码

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

我在这里处理另一个文件中的分页:

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


现在,如果我复制并粘贴书籍端点并将其更改为作者,并将类型更改为 PageType(Author),那么我会收到另一个错误:

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

所以这显然也不是解决方案

【问题讨论】:

【参考方案1】:

您不能拥有一个包含Authors 的EdgeType 和另一个包含BooksEdgeType。相反,您需要一个 AuthorEdge 和一个 BookEdge 类型。

PageType 也是如此 - 不能有两种不同类型的不同字段但名称相同。

虽然解决方案相对简单 - 如果您在函数中动态生成这些类型,也可以动态命名它们:

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

【讨论】:

太棒了,谢谢!另一件事是我理解这是 Apollo 旨在帮助复制的方式,我想知道是否有办法避免在所有游标逻辑所在的 RootQuery 上复制书籍查询。复制整个块并在作者端点上使用它是一种耻辱 如果resolve方法非常相似,你的意思是重复代码? 确实,RootQuery 上会有一个作者解析器,看起来与书籍解析器相同。我在想以某种方式将所有这些都提取到自己的功能中可能会更好。 嗯,不完全相同,因为它使用不同的服务/对象集合/数据库表/示例数组:-) 但是可以肯定的是,您可以编写一个辅助函数,传递该工件并返回解析器函数.

以上是关于使用基于 GraphQL 游标的分页避免代码重复的主要内容,如果未能解决你的问题,请参考以下文章

在使用 graphql 缓存的基于游标的分页中跟踪页码

如何从数据库中获取 Graphql 中的分页游标?

具有多列的基于 MySQL 游标的分页

用于 Java-GraphQL 服务器的 Java 中基于中继的分页

基于游标的分页接口实现

如何在 Relay 中管理游标和排序?