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

Mongoose更新用户嵌套字段问题:address字段无法更新

问题:用户地址字段无法更新,测试提示updatedUser.personalDetails.address.zip为undefined

问题说明

我需要更新用户的height、dateOfBirth或address字段,测试显示height和dateOfBirth更新正常,但address无法更新,失败时提示updatedUser.personalDetails.address.zip为undefined。

相关代码

校验Schema

const updateProfileSchema = {
  body: {
    type: 'object',
    properties: {
      dateOfBirth: { type: 'string', format: 'date' },
      height: { type: 'number' },
      address: {
        type: 'object',
        properties: {
          street: { type: 'string' },
          city: { type: 'string' },
          state: { type: 'string' },
          zip: { type: 'number' },
          country: { type: 'string' },
        },
      },
    },
  },
};

MongoDB用户Schema

const userSchema = new Schema({
  name: { type: String, required: true },
  personalDetails: {
    height: { type: Number, min: 0 },
    dateOfBirth: { type: Date },
    address: {
      street: String,
      city: String,
      state: String,
      zip: Number,
      country: String,
    },
})

控制器代码

const updateProfile = async (request: any, reply: FastifyReply) => {
  const { userId }: { userId : string } = request.user;
  const {
    height,
    dateOfBirth,
    address = {},
  }: {
    height?: number;
    dateOfBirth?: string;
    address?: Record<string, any>;
  } = request.body;

  let user: userType | null;
  try {
    logger.info(`trying to get the user - ${userId}`);
    user = await User.findOne({ _id: userId});
  } catch (err) {
    logger.error(`can't get the user - ${userId}`, err);
    return await reply
      .status(500)
      .send({ message: 'Getting userfailed, please try again.' });
  }

  if (!user) {
    logger.error(`didn't find user by his id - ${userId}`);
    return await reply
      .status(404)
      .send({ message: 'Could not find user for provided id.' });
  }
  const updatedFields: Record<string, any> = {};

  for (const key in request.body) {
    const value = request.body[key];
    updatedFields[`personalDetails.${key}`] = value;
  }

  try {
    logger.info(`trying updating the user - ${userId}`);
    await User.updateOne(
      { _id: userId},
      { $set: updatedFields },
      {
        runValidators: true,
      },
    );
  } catch (err) {
    logger.error(`can't update the user - ${userId}`, err);
    return await reply
      .status(500)
      .send({ message: 'Editing user failed, please try again.' });
  }

  return await reply.status(200).send({
    message: 'user updated successfully',
  });
};

失败的Jest测试代码

test('should return 200 when sending address', async () => {
  const address = {
    zip: 29193,
  };

  const res = await supertest(app.server)
    .put(`/api/user/profile`)
    .set('Authorization', `Bearer ${token}`)
    .send(address);

  expect(res.status).toBe(200);
  const updatedUser: userType | null = await User.findOne({
    email: userDetails.email,
  });

  expect(updatedUser).toBeDefined();
  expect(updatedUser?.personalDetails?.address).toBeDefined();
  expect(updatedUser?.personalDetails?.address.zip).toBe(address.zip);
});

问题分析

1. 测试代码的错误

测试中发送的请求体是{zip: 29193},但根据校验Schema和控制器逻辑,更新address需要传入{address: {zip: 29193}}。当前请求体中的zip会被控制器错误地映射到personalDetails.zip,而MongoDB Schema中zip属于personalDetails.address的子字段,导致personalDetails.address并未被更新,测试时自然会显示address.zip为undefined。

2. 控制器逻辑的缺陷

即使请求体传入正确的{address: {zip: 29193}},当前控制器的逻辑会直接覆盖整个personalDetails.address对象。如果用户原本已有完整的地址信息,仅更新zip会导致其他地址字段丢失。正确的做法应该是支持嵌套字段的部分更新,生成personalDetails.address.zip这样的MongoDB路径,而不是替换整个address对象。

解决方案

修复测试代码

将请求体改为正确的嵌套结构:

test('should return 200 when sending address', async () => {
  const addressData = {
    zip: 29193,
  };

  const res = await supertest(app.server)
    .put(`/api/user/profile`)
    .set('Authorization', `Bearer ${token}`)
    .send({ address: addressData }); // 改为传入包含address键的对象

  expect(res.status).toBe(200);
  const updatedUser: userType | null = await User.findOne({
    email: userDetails.email,
  });

  expect(updatedUser).toBeDefined();
  expect(updatedUser?.personalDetails?.address).toBeDefined();
  expect(updatedUser?.personalDetails?.address.zip).toBe(addressData.zip);
});

优化控制器逻辑(支持嵌套字段部分更新)

修改控制器中生成updatedFields的逻辑,递归处理嵌套对象,生成正确的MongoDB路径:

const updateProfile = async (request: any, reply: FastifyReply) => {
  const { userId }: { userId : string } = request.user;

  let user: userType | null;
  try {
    logger.info(`trying to get the user - ${userId}`);
    user = await User.findOne({ _id: userId});
  } catch (err) {
    logger.error(`can't get the user - ${userId}`, err);
    return await reply
      .status(500)
      .send({ message: 'Getting user failed, please try again.' });
  }

  if (!user) {
    logger.error(`didn't find user by his id - ${userId}`);
    return await reply
      .status(404)
      .send({ message: 'Could not find user for provided id.' });
  }

  // 递归生成MongoDB更新路径
  const buildUpdateFields = (prefix: string, obj: Record<string, any>): Record<string, any> => {
    const fields: Record<string, any> = {};
    for (const key in obj) {
      const value = obj[key];
      if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
        Object.assign(fields, buildUpdateFields(`${prefix}.${key}`, value));
      } else {
        fields[`${prefix}.${key}`] = value;
      }
    }
    return fields;
  };

  const updatedFields = buildUpdateFields('personalDetails', request.body);

  // 处理dateOfBirth的类型转换
  if (updatedFields['personalDetails.dateOfBirth']) {
    updatedFields['personalDetails.dateOfBirth'] = new Date(updatedFields['personalDetails.dateOfBirth']);
  }

  try {
    logger.info(`trying updating the user - ${userId}`);
    await User.updateOne(
      { _id: userId},
      { $set: updatedFields },
      {
        runValidators: true,
      },
    );
  } catch (err) {
    logger.error(`can't update the user - ${userId}`, err);
    return await reply
      .status(500)
      .send({ message: 'Editing user failed, please try again.' });
  }

  return await reply.status(200).send({
    message: 'user updated successfully',
  });
};

说明

  • 修复测试代码后,请求体符合接口预期的结构,能正确触发address字段的更新。
  • 优化后的控制器逻辑支持嵌套字段的部分更新:比如传入{address: {zip: 29193}}时,会生成personalDetails.address.zip的更新路径,仅修改zip字段而保留其他地址信息;传入{height: 180, address: {city: "New York"}}时,也能同时更新多个字段。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 13:55:10