TypeORM+TypeGraphQL+PostgreSQL游标分页参数类型错误问题
解决TypeORM游标分页中客户端"take值非数字"错误的方案
问题描述
使用TypeORM、TypeGraphQL和PostgreSQL实现游标分页时,Apollo Server Playground能正常返回结果,但客户端抛出错误:'Provided "take" value is not a number. Please provide a numeric value.'。若将代码中的.take(realLimitPlusOne)改为固定limit值可消除错误,但会破坏游标分页的核心逻辑。
相关代码
服务端post.ts resolver代码
@Query(() => PaginatedPosts) async posts( @Arg('limit', () => Int, { nullable: true }) limit: number, @Arg('cursor', () => String, { nullable: true }) cursor: string | null ): Promise<PaginatedPosts> { const realLimit = Math.min(50, limit); const realLimitPlusOne = realLimit + 1; const qb = AppDataSource.getRepository(Post) .createQueryBuilder('p') .orderBy('"createdAt"', 'DESC') .take(realLimitPlusOne); if (cursor) { qb.where('"createdAt" < :cursor', { cursor: new Date(parseInt(cursor)), }); } const posts = await qb.getMany(); return { posts: posts.slice(0, realLimit), hasMore: posts.length === realLimitPlusOne, }; }
客户端index.tsx代码
import { Box, Button, Flex, Heading, Link, Stack, Text, } from '@chakra-ui/react'; import { GetServerSidePropsContext } from 'next'; import Layout from '../components/Layout'; import { PostsDocument, usePostsQuery } from '../generated/graphql'; import { addApolloState, initializeApollo } from '../lib/apolloClient'; // SSR获取帖子数据 export const getServerSideProps = async ({ req, }: GetServerSidePropsContext) => { const apolloClient = initializeApollo({ ctx: { req } }); await apolloClient.query({ query: PostsDocument }); return addApolloState(apolloClient, { props: {}, }); }; const Index = () => { const { data, loading, fetchMore, variables } = usePostsQuery({ variables: { limit: 10, cursor: null, }, notifyOnNetworkStatusChange: true, }); if (!loading && !data) { return <div>查询失败,请检查服务状态</div>; } return ( <Layout> <Flex> <Heading>Lireddit</Heading> <Link href="/create-post" ml="auto"> 创建帖子 </Link> </Flex> <br></br> {!data && loading ? ( <div>加载中...</div> ) : ( <Stack spacing={8}> {data!.posts.posts.map((p) => ( <Box p={5} shadow="md" borderWidth="1px" key={p.id}> <Heading fontSize="xl">{p.title}</Heading> <Text mt={4}>{p.textSnippet}</Text> </Box> ))} </Stack> )} {data ? ( <Flex> <Button onClick={() => { fetchMore({ variables: { limit: variables?.limit, cursor: data.posts.posts[data.posts.posts.length - 1].createdAt, }, }); }} isLoading={loading} m="auto" my={8} > 加载更多 </Button> </Flex> ) : null} </Layout> ); }; export default Index;
posts.graphql查询代码
query Posts($cursor: String, $limit: Int) { posts(cursor: $cursor, limit: $limit) { hasMore posts { id title createdAt updatedAt textSnippet } } }
apolloClient.ts代码
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql', credentials: 'include', }); function createApolloClient() { return new ApolloClient({ ssrMode: typeof window === 'undefined', link: from([httpLink]), cache: new InMemoryCache({ typePolicies: { Query: { fields: { posts: { keyArgs: false, merge( existing: PaginatedPosts | undefined, incoming: PaginatedPosts ): PaginatedPosts { return { ...incoming, posts: [...(existing?.posts || []), ...incoming?.posts], }; }, }, }, }, }, }), }); }
PaginatedPosts定义
@ObjectType() export class PaginatedPosts { @Field(() => [Post]) posts: Post[]; @Field() hasMore: boolean; }
解决方案
问题根源
- resolver参数未处理null值:
limit参数被标记为nullable,当客户端未传入(比如SSR阶段)时,limit为null,此时Math.min(50, limit)会返回NaN,导致realLimitPlusOne也是NaN,TypeORM的.take()方法无法识别NaN,抛出错误。 - SSR查询未传变量:
getServerSideProps中调用apolloClient.query时未传递limit变量,导致resolver收到的limit为null。 - fetchMore变量可能为undefined:客户端点击“加载更多”时,
variables?.limit可能因某些情况变为undefined,再次触发参数非数字的问题。
具体修复步骤
给resolver的limit参数设置默认值
修改resolver的@Arg装饰器,添加defaultValue确保limit始终为数字:@Arg('limit', () => Int, { nullable: true, defaultValue: 10 }) limit: number,或者在计算
realLimit时用空值合并运算符兜底:const realLimit = Math.min(50, limit ?? 10);修复SSR查询的变量传递
在getServerSideProps中调用query时明确传递变量:await apolloClient.query({ query: PostsDocument, variables: { limit: 10, cursor: null } });确保fetchMore的limit变量不为空
修改客户端fetchMore的变量设置,添加默认值:fetchMore({ variables: { limit: variables?.limit ?? 10, cursor: data.posts.posts[data.posts.posts.length - 1].createdAt, }, });
完成以上修改后,既能保留游标分页的逻辑,又能解决客户端抛出的“take值非数字”错误。
内容的提问来源于stack exchange,提问作者Konstantinos Kaskantiris
相关产品推荐
相关产品推荐

