如何在Mongoose虚拟属性/中间件中执行异步函数生成S3签名URL?
如何在Mongoose中自动为文档生成异步签名的S3图片URL
问题背景
我将图片存储在S3中,仅在MongoDB里保存图片的imageKey。查询MongoDB文档时,需要把这个key转换为可访问的签名URL,原本打算用Mongoose的虚拟属性实现,代码如下:
MySchema.virtual('imageUrl').get(async function(this: MyDoc) { if (!this.imageKey) { return null; } const readUrl = await awsPreSignReadUrl(this.imageKey); return readUrl; });
但Mongoose的虚拟属性不支持异步逻辑。我知道可以改用实例方法手动调用,但需要让这个URL生成逻辑自动适配find、findOne、aggregate、populate等所有查询操作,求一个通用的自动解决方案。
可行解决方案
方法1:利用Mongoose查询Post钩子批量处理
通过Mongoose的post钩子,在查询完成后自动为文档添加签名URL,覆盖find、findOne、findById等常用查询:
// 为多个查询类型注册post钩子 MySchema.post(['find', 'findOne', 'findById'], async function(docs) { // 统一处理单个文档和文档数组的情况 const targetDocs = Array.isArray(docs) ? docs : [docs]; // 批量生成签名URL,提升性能 const urlPromises = targetDocs.map(doc => doc.imageKey ? awsPreSignReadUrl(doc.imageKey) : Promise.resolve(null) ); const signedUrls = await Promise.all(urlPromises); // 为每个文档添加imageUrl字段,并确保转JSON时保留 targetDocs.forEach((doc, index) => { doc.imageUrl = signedUrls[index]; // 重写toJSON方法,确保输出包含imageUrl doc.toJSON = function() { const obj = this.toObject(); obj.imageUrl = this.imageUrl; return obj; }; }); });
注意聚合查询的处理:聚合操作返回的是普通JS对象而非Mongoose文档实例,需要单独处理结果:
// 执行聚合查询后手动处理结果 const aggResults = await MyModel.aggregate([/* 你的聚合管道逻辑 */]); const urlPromises = aggResults.map(item => item.imageKey ? awsPreSignReadUrl(item.imageKey) : Promise.resolve(null) ); const signedUrls = await Promise.all(urlPromises); aggResults.forEach((item, index) => { item.imageUrl = signedUrls[index]; });
方法2:封装通用转换函数,重写Model查询方法
创建一个通用的转换函数,然后重写Model的查询方法,让所有查询自动调用这个函数:
// 通用函数:为文档/文档数组添加imageUrl async function attachImageUrl(target) { const isArray = Array.isArray(target); const items = isArray ? target : [target]; const urlPromises = items.map(item => item.imageKey ? awsPreSignReadUrl(item.imageKey) : Promise.resolve(null) ); const signedUrls = await Promise.all(urlPromises); items.forEach((item, index) => { item.imageUrl = signedUrls[index]; }); return isArray ? items : items[0]; } // 重写Model的find方法 const originalFind = MyModel.find; MyModel.find = function(...args) { return originalFind.apply(this, args).then(docs => attachImageUrl(docs)); }; // 同理重写其他查询方法 const originalFindOne = MyModel.findOne; MyModel.findOne = function(...args) { return originalFindOne.apply(this, args).then(doc => attachImageUrl(doc)); }; const originalAggregate = MyModel.aggregate; MyModel.aggregate = function(...args) { return originalAggregate.apply(this, args).then(results => attachImageUrl(results)); };
这样所有查询操作都会自动返回带imageUrl的结果,无需手动调用转换函数。
方法3:结合文档初始化钩子与toJSON转换
利用Mongoose的post('init')钩子,在文档从数据库加载完成后异步生成签名URL,再通过toJSON配置确保输出包含该字段:
// 文档初始化时生成签名URL MySchema.post('init', async function(doc) { if (doc.imageKey) { doc.imageUrl = await awsPreSignReadUrl(doc.imageKey); } }); // 配置toJSON,确保转换时包含imageUrl MySchema.set('toJSON', { transform: function(doc, ret) { ret.imageUrl = doc.imageUrl; delete ret.imageKey; // 可选:如果不需要返回原始key可以删除 return ret; } });
这个方法对find、findOne等返回Mongoose文档的查询有效,但聚合结果仍需单独处理。
内容的提问来源于stack exchange,提问作者Pelayo Martinez
相关产品推荐
相关产品推荐

