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
相关产品推荐
相关产品推荐

