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

如何在GraphQL Pothos Builder中实现登录接口的自定义Mutation输出?

修复Pothos Builder中自定义UserAndToken类型的问题

我使用GraphQL Pothos Builder开发了登录组件,ORM采用Prisma。目前仅能返回User类型的响应,希望实现包含用户信息与令牌的自定义输出。尝试创建UserAndToken ObjectRef但未成功,已尝试一周仍无法解决,请求帮助修复该问题。

原代码如下:

import prisma from "@/prisma/client";
import { User as UserModel } from "@prisma/client";
import bcrypt from "bcrypt";
import { GraphQLError } from "graphql";
import ms from "ms";
import { cookies } from "next/headers";
import { builder } from "../builder";
import { User } from "../types/User";
import {
  generateAccessToken,
  generateRefreshToken,
} from "../utils/generateToken";

const UserAndToken = builder
  .objectRef<{ user: UserModel, accessToken: string }>("UserAndToken")
  .implement({
    fields: t => ({
      user: t.field({ type: User, resolve: (parent) => parent.user })
    })
  })

builder.mutationFields((t) => ({
  login: t.prismaField({
    description: "Login to time boxing platform.",
    type: UserAndToken,
    args: {
      email: t.arg({ type: "Email", required: true }),
      password: t.arg.string({ required: true }),
    },
    resolve: async (query, _, args) => {
      const user = await prisma.user.findUnique({
        where: { email: args.email },
      });

      if (
        !user ||
        !bcrypt.compare(args.password, user.password)
      ) {
        return Promise.reject(
          new GraphQLError(
            "Either email or password is incorrect."
          )
        );
      }

      try {
        const accessToken = generateAccessToken(user);
        const refreshToken = generateRefreshToken(user);

        cookies().set({
          name: "X-REFRESH-TOKEN",
          value: refreshToken,
          httpOnly: true,
          sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
          secure: process.env.NODE_ENV === "production",
          expires: new Date(Date.now() + ms("7d")), //7d
        });

        await prisma.session.create({
          data: {
            refreshToken,
            expiresAt: new Date(new Date().getDate() + 7),
            userId: user.id,
          },
        });

        // update user's last login.
        await prisma.user.update({
          where: { id: user.id },
          data: { lastLogin: new Date() },
        });

        return { user, accessToken };
      } catch (err) {
        console.log(err)
        return Promise.reject(
          new GraphQLError(
            "Unable to login. Try again later."
          )
        );
      }
    },
  }),
}));

问题分析

  1. 自定义类型字段缺失:定义的UserAndToken类型只实现了user字段,但返回的对象包含user和accessToken,GraphQL无法识别未定义的accessToken字段。
  2. bcrypt验证错误:bcrypt.compare是异步方法,原代码用同步调用会导致密码验证逻辑失效,永远返回false。
  3. 会话过期时间错误:new Date(new Date().getDate() + 7)只会修改日期部分,不会正确设置7天后的过期时间,应该用Date.now() + ms("7d")统一逻辑。

修复后的代码

import prisma from "@/prisma/client";
import { User as UserModel } from "@prisma/client";
import bcrypt from "bcrypt";
import { GraphQLError } from "graphql";
import ms from "ms";
import { cookies } from "next/headers";
import { builder } from "../builder";
import { User } from "../types/User";
import {
  generateAccessToken,
  generateRefreshToken,
} from "../utils/generateToken";

// 正确实现包含user和accessToken的自定义类型
const UserAndToken = builder
  .objectRef<{ user: UserModel, accessToken: string }>("UserAndToken")
  .implement({
    fields: t => ({
      user: t.field({ 
        type: User, 
        resolve: (parent) => parent.user 
      }),
      // 添加accessToken字段定义
      accessToken: t.string({
        resolve: (parent) => parent.accessToken
      })
    })
  })

builder.mutationFields((t) => ({
  login: t.prismaField({
    description: "Login to time boxing platform.",
    type: UserAndToken,
    args: {
      email: t.arg({ type: "Email", required: true }),
      password: t.arg.string({ required: true }),
    },
    resolve: async (query, _, args) => {
      const user = await prisma.user.findUnique({
        where: { email: args.email },
      });

      // 修复bcrypt异步调用问题
      if (!user || !(await bcrypt.compare(args.password, user.password))) {
        throw new GraphQLError("Either email or password is incorrect.");
      }

      try {
        const accessToken = generateAccessToken(user);
        const refreshToken = generateRefreshToken(user);

        cookies().set({
          name: "X-REFRESH-TOKEN",
          value: refreshToken,
          httpOnly: true,
          sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
          secure: process.env.NODE_ENV === "production",
          expires: new Date(Date.now() + ms("7d")),
        });

        await prisma.session.create({
          data: {
            refreshToken,
            // 修复会话过期时间计算
            expiresAt: new Date(Date.now() + ms("7d")),
            userId: user.id,
          },
        });

        await prisma.user.update({
          where: { id: user.id },
          data: { lastLogin: new Date() },
        });

        // 返回与自定义类型匹配的结构
        return { user, accessToken };
      } catch (err) {
        console.error(err);
        throw new GraphQLError("Unable to login. Try again later.");
      }
    },
  }),
}));

关键修复点

  • 给UserAndToken类型添加accessToken字符串字段,确保返回的对象结构与类型定义完全匹配。
  • 将bcrypt.compare改为异步调用(添加await),保证密码验证逻辑正确执行。
  • 统一会话过期时间的计算方式,避免日期设置错误。
  • 用throw替代Promise.reject,代码更简洁符合异步函数规范。

内容的提问来源于stack exchange,提问作者Jenny Patel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 00:59:58