基于Sequelize findAndCountAll的客户端自定义返回字段筛选咨询
Sequelize分页查询实现客户端自定义返回字段的方案
核心实现逻辑
要实现客户端自主选择返回字段的能力,核心是动态构造Sequelize查询的attributes参数,全程通过白名单做安全校验,避免直接拼接客户端传入的字段带来安全风险,整体流程分为三步:
- 预先定义所有模型可对外暴露的字段白名单,拦截所有非授权的字段请求
- 解析客户端传入的逗号分隔字段参数,按所属模型做拆分归类
- 自动补全关联查询、分页count统计必需的主键、外键字段,避免查询失效
参数约定
优化客户端传参格式,通过点分路径区分字段所属的关联模型,避免字段归属歧义:
示例请求:
127.0.0.1:10000/attributesFilter?pageNo=1&pageSize=10&attributes=dob,firstName,User.locations.locationName
上述参数表示客户端需求:返回PersonalInfo表的dob、firstName字段,以及关联Location表的locationName字段
分步改造代码
- 预先定义字段白名单常量,作为所有可返回字段的校验依据,禁止白名单外的字段进入查询逻辑
// 可对外暴露的字段白名单 const ALLOWED_FIELDS = { PersonalInfo: ["id", "firstName", "middleName", "lastName", "dob"], User: ["id", "user"], Location: ["id", "locationName", "locationAddress"] }
- 编写动态属性构造工具函数,处理请求字段的解析、过滤、必需字段补全逻辑
/** * 解析客户端传入的属性参数,构造各模型的attributes配置 * @param {string} reqAttrStr 客户端传入的逗号分隔属性字符串 * @returns 各模型的动态属性配置 */ function buildQueryAttributes(reqAttrStr = '') { // 拆分客户端传入的字段列表,去除空值 const requestedFields = reqAttrStr.split(',').map(item => item.trim()).filter(Boolean) const attrConfig = { personalInfoAttrs: [], userAttrs: [], locationAttrs: [] } // 自动补全关联查询、count统计必需的主键/外键字段,保证查询逻辑正常 attrConfig.personalInfoAttrs.push("id") // 主表主键,保证distinct count统计准确 attrConfig.userAttrs.push("id", "personalInfoId") // 替换为实际关联外键字段 attrConfig.locationAttrs.push("id", "userId") // 替换为实际关联外键字段 requestedFields.forEach(fieldPath => { const pathNodes = fieldPath.split('.') // 主表PersonalInfo字段匹配 if (pathNodes.length === 1 && ALLOWED_FIELDS.PersonalInfo.includes(pathNodes[0])) { attrConfig.personalInfoAttrs.push(pathNodes[0]) } // 关联User表字段匹配 if (pathNodes[0] === 'User' && ALLOWED_FIELDS.User.includes(pathNodes[1])) { attrConfig.userAttrs.push(pathNodes[1]) } // 关联Location表字段匹配 if (pathNodes[0] === 'User' && pathNodes[1] === 'locations' && ALLOWED_FIELDS.Location.includes(pathNodes[2])) { attrConfig.locationAttrs.push(pathNodes[2]) } }) // 对各模型的属性数组去重 Object.keys(attrConfig).forEach(key => { attrConfig[key] = [...new Set(attrConfig[key])] }) return attrConfig }
- 替换原有findAndCountAll方法中写死的attributes配置,接入动态构造的参数
try { // 从请求query中获取客户端传入的属性参数 const { attributes: reqAttrStr } = req.query const dynamicAttrs = buildQueryAttributes(reqAttrStr) const { count, rows: usersPersonalInfo } = await PersonalInfo.findAndCountAll({ offset: (pageNo - 1) * pageSize, limit: pageSize, subQuery: false, distinct: true, where: { userId: { [Op.in]: allowedOrgUserIds }, deletedAt: null, }, // 替换原写死的主表属性数组 attributes: dynamicAttrs.personalInfoAttrs, include: [ { model: User, // 替换原写死的User表属性数组 attributes: dynamicAttrs.userAttrs, include:[ { model: Location, as:'locations', // 替换原写死的Location表属性数组 attributes: dynamicAttrs.locationAttrs }, ], }] }) success(res)({ providersList: usersPersonalInfo, total: count, pageNo, pageSize, }) } catch (error) { handleError(res)(error); }
关键注意事项
- 安全红线:禁止直接将客户端传入的字段字符串直接传入attributes参数,必须经过白名单校验,防止SQL注入、越权查询敏感字段
- 关联有效性:Sequelize识别关联关系依赖外键字段,无论客户端是否要求返回外键,都必须将关联链路的外键、父表主键自动补入attributes,否则会出现关联数据为空的问题
- 分页准确性:原有查询开启了
distinct: true配置,主表查询必须保留主键字段,否则count统计值会和实际返回行数存在偏差 - 兼容逻辑:如果客户端未传attributes参数,工具函数会自动返回所有白名单内的全量字段,完全兼容原有无字段筛选的调用逻辑,无需客户端强制适配
内容的提问来源于stack exchange,提问作者farzanmanafi
相关产品推荐
相关产品推荐

