You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

解决方案

问题根源

  1. resolver参数未处理null值:limit参数被标记为nullable,当客户端未传入(比如SSR阶段)时,limit为null,此时Math.min(50, limit)会返回NaN,导致realLimitPlusOne也是NaN,TypeORM的.take()方法无法识别NaN,抛出错误。
  2. SSR查询未传变量:getServerSideProps中调用apolloClient.query时未传递limit变量,导致resolver收到的limit为null。
  3. fetchMore变量可能为undefined:客户端点击“加载更多”时,variables?.limit可能因某些情况变为undefined,再次触发参数非数字的问题。

具体修复步骤

  1. 给resolver的limit参数设置默认值
    修改resolver的@Arg装饰器,添加defaultValue确保limit始终为数字:

    @Arg('limit', () => Int, { nullable: true, defaultValue: 10 }) limit: number,
    

    或者在计算realLimit时用空值合并运算符兜底:

    const realLimit = Math.min(50, limit ?? 10);
    
  2. 修复SSR查询的变量传递
    在getServerSideProps中调用query时明确传递变量:

    await apolloClient.query({ 
      query: PostsDocument,
      variables: { limit: 10, cursor: null }
    });
    
  3. 确保fetchMore的limit变量不为空
    修改客户端fetchMore的变量设置,添加默认值:

    fetchMore({
      variables: {
        limit: variables?.limit ?? 10,
        cursor: data.posts.posts[data.posts.posts.length - 1].createdAt,
      },
    });
    

完成以上修改后,既能保留游标分页的逻辑,又能解决客户端抛出的“take值非数字”错误。

内容的提问来源于stack exchange,提问作者Konstantinos Kaskantiris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 09:30:38