资讯动态

Reaction API 的 api-utils 工具包:GraphQL 过滤、聚合分页与 URL 处理的四大核心函数实战指南

发布时间:2026/10/14 2:44:19 来源:尧图企业网站定制
后端电商【免费下载链接】reactionProject has been discontinued ////// Mailchimp Open Commerce is an API-first, headless commerce platform built using Node.js, React, GraphQL. Deployed via Docker and Kubernetes.项目地址https://gitcode.com/gh_mirrors/re/reaction点击查看免费下载导读reactioncommerce/api-utils是 Mailchimp Open CommerceReactionGraphQL API 的底层工具包为各个 API 插件products、catalogs、fulfillment、tags 等提供开箱即用的通用函数。本文以 api-utils 官方文档 为主线结合 源码实现 与真实插件调用链系统讲解generateFilterQueryGraphQL 条件转 MongoDB 查询、getAbsoluteUrlURL 拼接、getPaginatedResponseFromAggregate聚合管道分页与tagsForCatalogProducts产品标签映射四个核心函数的输入格式、返回结构、边界行为与底层原理帮助你在自定义插件或扩展 GraphQL resolver 时直接复用这些能力。包概览与安装引入api-utils的定位非常纯粹为 Reaction API 提供一系列与业务无关的通用工具函数。它的包清单见 packages/api-utils/package.json当前版本依赖simpl-schema、lodash、graphql-relay、accounting-js、envalid等库自身不带任何 Meteor 运行时是一个标准的 Node.js ES Module 包type: module要求 Node 14.18.1。安装npm install reactioncommerce/api-utils官方文档特别提醒请确保使用 NPM 5。该包以 ES Moduleimport方式对外暴露导入时按需引用具体函数文件即可import functionName from reactioncommerce/api-utils/functionName.js;值得注意的是包内不推荐从入口文件整体导入——packages/api-utils/index.js 会打印警告Please import modules directly。这是刻意的设计通过 exports 字段 将./*映射到./lib/*、./graphql/映射到./lib/graphql/从而允许按文件路径精确加载配合sideEffects: false可以让打包器更好地做 tree-shaking。函数清单完整函数索引见 docs 目录索引包括 4 个重点讲解的实用函数以及encodeOpaqueId/decodeOpaqueId不透明 ID 编解码、getSlug字符串转 slug、formatMoney金额格式化、hashToken令牌哈希、applyPaginationToMongoCursor等大量基础设施均可按需直接导入。generateFilterQuery把 GraphQL 过滤条件编译成 MongoDB 查询设计思路generateFilterQuery解决的是一个非常典型的 GraphQL 服务端问题客户端在查询参数里提交结构化的过滤条件而不是原始 MongoDB 语法服务端需要安全地把它们转换成合法的 MongoDB selector 再交给collection.find()。它期望的输入正是 GraphQL Schema 中定义的FilterConditionsInput类型其完整定义见 api-plugin-products 的 Schemaproducts查询的conditions参数即使用此类型。FilterConditionsInput 输入格式官方文档给出的类型定义如下这是理解整个函数行为的基石input FilterConditionsInput { all: [ConditionsArray] any: [ConditionsArray] } input ConditionsArray { all: [SingleConditionInput] any: [SingleConditionInput] } input SingleConditionInput { booleanValue: Boolean caseSensitive: Boolean dateValue: DateTime floatArrayValue: [Float] floatValue: Float integerArrayValue: [Int] integerValue: Int key: String! logicalNot: Boolean relationalOperator: RelationalOperatorTypes! stringArrayValue: [String] stringValue: String } enum RelationalOperatorTypes{ beginsWith endsWith eq gt gte in lt lte ne nin regex }结构上它有严格的三层嵌套顶层FilterConditionsInput只能有all或any两个键值是ConditionsArray数组中间层每个ConditionsArray元素再包含一层all或any值是SingleConditionInput数组原子层SingleConditionInput描述单个过滤条件其中key要过滤的字段名与relationalOperator关系运算符枚举是必填项并且必须在 8 个值字段stringValue、integerValue、floatValue、booleanValue、dateValue、stringArrayValue、integerArrayValue、floatArrayValue中恰好提供其中一个外加两个可选布尔开关caseSensitiveregex 是否区分大小写与logicalNot是否取反。语义映射规则非常直观all等价于 MongoDB 的$andany等价于$or。这一映射在源码中由keyMap显式定义const keyMap { all: $and, any: $or };官方示例单条件查询import generateFilterQuery from reactioncommerce/api-utils/generateFilterQuery.js; const conditions { all: [ { any: [ { key: handle, stringValue: mens-waterproof-outdoor-rain-jacket, relationalOperator: eq, logicalNot: false } ] } ] }; const { filterQuery } generateFilterQuery(context, Product, conditions, shopId); return Products.find(filterQuery);因为只有单个条件最外层用all还是any结果没有区别。调用签名是generateFilterQuery(context, collectionName, conditions, shopId)context携带 per-request 状态尤其是context.simpleSchemas里的集合 Schema 与context.collectionscollectionName是集合名如ProductshopId会作为额外的等值条件注入最终 selector见 generateQuery 实现。完整校验链三层防御从 generateFilterQuery.js 的实现看函数在真正生成查询前会做三层校验这也是把它直接暴露给客户端输入仍然安全的原因结构校验用ConditionsArraySchema.validate(conditions)基于 simpl-schema 定义见 源码 L81-L96检查嵌套结构、字段类型与枚举值合法性字段元数据校验通过 collectCollectionFields 读取context.simpleSchemas[collectionName]合并后的 Schema收集每个字段的类型如SimpleSchema.String、SimpleSchema.Number再调用validateConditions校验key必须存在于集合字段中否则抛出Invalid key: ${key}值字段必须与字段类型匹配String 字段必须给stringValue/stringArrayValueBoolean 字段必须给booleanValue等8 个值字段中只能提供一个值多给抛Only one value must be provided...关系运算符必须与字段类型兼容具体矩阵定义在validCombos见 源码 L98-L123字符串支持eq/ne/in/nin/regex/beginsWith/endsWith数字与整数支持eq/ne/gt/gte/lt/lte/in/nin数组支持in/nin/eq/ne布尔支持eq/ne日期支持eq/ne/gt/gte/lt/lte数组值不能为空Array value cannot be empty。运算符映射simpleConditionToQuery把枚举运算符翻译成 MongoDB 查询运算符。其中regex、beginsWith、endsWith会编译为$regexbeginsWith加^前缀、endsWith加$后缀并且默认不区分大小写$options: i只有显式caseSensitive: true时才置空logicalNot会把整个子查询包进$not见 源码 L343-L414。进阶示例多条件组合当需要同时满足多组条件时顶层all$and与内层any$or组合出完整的布尔表达式。下面的条件等价于handle 精确匹配 或 title 以 men 开头且_id属于两个 ID 之一、isDeleted为 false、workflow.status为 new且price.min 19.99 且 type 为 simpleimport generateFilterQuery from reactioncommerce/api-utils/generateFilterQuery.js; const conditions { all: [ { any: [ { key: handle, stringValue: mens-waterproof-outdoor-rain-jacket, relationalOperator: eq, logicalNot: false }, { key: title, stringValue: men, relationalOperator: beginsWith, logicalNot: false, caseSensitive: false } ] }, { all: [ { key: _id, stringArrayValue: [DZwLHk4EAzitRni8F, Hn4BRaBvLkYffMq36], relationalOperator: in, logicalNot: false }, { key: isDeleted, booleanValue: false, relationalOperator: eq, logicalNot: false }, { key: workflow.status, stringValue: new, relationalOperator: eq, logicalNot: false } ] }, { all: [ { key: price.min, floatValue: 19.99, relationalOperator: gte, logicalNot: false }, { key: type, stringValue: simple, relationalOperator: eq, logicalNot: false } ] } ] }; const { filterQuery } generateFilterQuery(context, Product, conditions, shopId); return Products.find(filterQuery);最终生成的 MongoDB selector 形态与 generateFilterQuery.test.js 中断言的一致{ $and: [ { $or: [ { handle: { $eq: mens-waterproof-outdoor-rain-jacket } }, { title: { $regex: ^men, $options: i } } ] }, { $and: [ { _id: { $in: [DZwLHk4EAzitRni8F, Hn4BRaBvLkYffMq36] } }, { isDeleted: { $eq: false } }, { workflow.status: { $eq: new } } ] }, { $and: [ { price.min: { $gte: 19.99 } }, { type: { $eq: simple } } ] } ], shopId: SHOP123 }在真实插件中的用法generateFilterQuery是通用的——任何定义了 simple-schema 的集合都能用。以 api-plugin-products 的 filterProducts.js 为例它的完整调用链是GraphQL resolver →filterProducts(context, conditions, shopId)→ 先做权限校验validatePermissions(reaction:legacy:products, read, ...)→generateFilterQuery(context, Product, conditions, shopId)→Products.find(filterQuery)。products 插件 README 中还有针对单条件、双条件、多条件的完整图解见 api-plugin-products/README.md。getAbsoluteUrl安全拼接根地址与相对路径函数行为getAbsoluteUrl(rootUrl, path)接收一个根 URL 和一个相对 URL把它们合并成完整地址。它的两个关键容错设计在官方文档中写得非常清楚传入的path可以是undefined——此时只返回根 URL 本身传入的path也可以是已经完整的 URL——此时原样返回不做任何拼接。官方示例const rootUrl http://localhost:3000/; const path /media/test.jpg; const url getAbsoluteUrl(rootUrl, path); // http://localhost:3000/media/test.jpg源码与边界行为从 getAbsoluteUrl.js 的实现可以看出它用截取path前 8 个字符并转小写的方式判断是否为绝对地址const pathStart path.slice(0, 8).toLowerCase(); if (pathStart.startsWith(https://) || pathStart.startsWith(http://) || pathStart.startsWith(//)) { return path; }也就是说以下三类输入会被视为已经是绝对地址而直接返回https://开头、http://开头、以及//开头的协议相对地址。其余情况会剥掉path最前面的/然后直接追加到rootUrl后面——这意味着rootUrl末尾是否有/都不影响结果。测试用例getAbsoluteUrl.test.js覆盖了空 path、带前导斜杠、已含 rootUrl、第三方https://服务、协议相对 URL以及一个有趣的特例path 为http.jpg时前 8 个字符为http.jpg不含://不会误判为绝对地址而是正确拼接成http://localhost:3000/http.jpg。在插件中的真实应用getAbsoluteUrl被广泛用于把数据库里存储的相对媒体路径转换成对外可访问的完整 URL例如api-plugin-tags 的 heroMediaUrl resolver先判断url是否以http开头不是则调用context.getAbsoluteUrl(url)补全api-plugin-shops 的 brandAssets、api-plugin-orders 的 imageURLs 等也都在做同样的处理。它通常作为context上的方法注入见 api-core 的 ReactionAPICore.js这样各插件可以无感知地使用同一个根地址配置。getPaginatedResponseFromAggregate聚合管道的 Relay 风格分页函数定位在 Reaction API 中返回多个文档的 resolver 需要以 Relay Connection 的形态输出结果。当数据源是聚合管道而不是简单查询时getPaginatedResponseFromAggregate(collection, pipeline, args, options)负责往管道里追加排序、分页skip/limit与统计相关的 stage并返回标准化结果。参数与返回结构官方文档给出如下用法import getPaginatedResponseFromAggregate from reactioncommerce/api-utils/getPaginatedResponseFromAggregate.js; // the MongoDB collection you want to run your aggregation on const collection getYourMongoDBCollectionSomehow(); const pipeline [ { // for example, heres a $lookup operator $lookup: { // some options } }, // but you can add, // any, // other, // operators ]; const args { // connection arguments from your GraphQL query or mutation // supported: // offset: Int, // first: Int, // last: Int, // sortBy: String (the name of the MongoDB field you want to sort results by) // sortOrder: String (desc for descending, or asc for ascending) }; const options { // supported: // includeHasNextPage: Boolean, // includeHasPreviousPage: Boolean, // includeTotalCount: Boolean }; const results await getPaginatedResponseFromAggregate(collection, pipeline, args, options);返回对象结构{ pipeline: Array, // 传入的管道 函数追加的修改$sort、$limit、$skip、$group 等 nodes: Array, // 返回的条目 pageInfo: { hasNextPage: Boolean, // 仅在 options 请求时返回 hasPreviousPage: Boolean, // 仅在 options 请求时返回 startCursor: String, // nodes 中第一个条目的 _id endCursor: String // nodes 中最后一个条目的 _id }, totalCount: Int // 仅在 options 请求时返回 }源码级原理从 getPaginatedResponseFromAggregate.js 的实现可以看到几个关键设计after/before不受支持传入这两个连接参数会直接抛错Connection args after and before arent supported by getPaginatedResponseFromAggregate测试见 getPaginatedResponseFromAggregate.test.js。聚合分页只支持offset与first/last两种模式默认分页大小 20无论first、last还是offset模式未显式给出first时都会应用DEFAULT_LIMIT 20见 applyPaginationToMongoAggregate.js 与 applyOffsetPaginationToMongoAggregate.jsfirst与last互斥同时传入会抛Request either first or last but not bothoffset与last也互斥totalCount 的巧妙计算由于聚合管道没有.count()实现会先追加$group$addToSet: $$ROOT$sum: 1把全部结果收进单文档并计数再$unwind还原、$set注入totalCount字段、$replaceRoot恢复根文档——整个过程见 源码 L40-L80。因此当includeTotalCount为false时可以省掉这批 stage排序通过 getMongoSort.js 把sortOrder: asc/desc映射为1/-1且总是附带_id作为次级排序键以保证次序稳定cursor 模式对应物如果数据源是普通查询而不是聚合应改用 getPaginatedResponse基于 Mongo Cursor支持after/before。在插件中的真实应用以 api-plugin-catalogs 的 vendors 查询 为例可以看到完整的 resolver 集成模式先由 queries/vendors.js 构造一个按product.vendor做$group的管道然后在 resolver 里调用return getPaginatedResponseFromAggregate(collection, pipeline, { ...connectionArgs, sortBy: name }, { includeHasNextPage: wasFieldRequested(pageInfo.hasNextPage, info), includeHasPreviousPage: wasFieldRequested(pageInfo.hasPreviousPage, info), includeTotalCount: wasFieldRequested(totalCount, info) });wasFieldRequested同样来自 api-utils让函数只在客户端真正请求了某个字段时才去计算它从而避免多余的数据库命令——这是该包在性能细节上的典型设计。类似用法还出现在 fulfillment 插件的 fulfillmentMethods resolver 和 flatRateFulfillmentMethods resolver 中。tagsForCatalogProducts把 CatalogProduct 的 tagIds 映射为标签名函数功能tagsForCatalogProducts(Tags, catalogProducts)接收一个Tags集合实例与一组 Catalog 集合中的CatalogItemProduct对象返回每个产品对应的标签名数组。官方示例数据集// Tags [ { _id: 1, name: ONE }, { _id: 2, name: TWO }, { _id: 3, name: THREE } ] // CatalogProducts [ { product: { productId: 1, tagIds: [1, 2] } }, { product: { productId: 2, tagIds: [3] } } ]用法与返回结果// Tags 是 Tags Mongo.Collection // catalogProducts 是 Catalog 集合中 CatalogItemProduct 的数组 const result await tagsForCatalogProducts(Tags, catalogProducts); // 返回 [ { productId: 1, tags: [ONE, TWO] }, { productId: 2, tags: [THREE] } ]实现细节从 tagsForCatalogProducts.js 的实现看它先遍历catalogProducts收集所有product.tagIds去重成一个数组然后一次Tags.find({ _id: { $in: tagIds } }).toArray()批量取回标签文档最后再遍历产品、按tagIds的顺序把_id替换成name。因此若传入的catalogProducts为空数组直接返回[]不做任何数据库查询测试覆盖于 tagsForCatalogProducts.test.js某个tagIds中的 ID 在 Tags 集合中找不到时对应位置返回null保持数组顺序与tagIds一致只进行一次查询复杂度与产品数量线性相关适合在批量场景下避免 N1 查询。在插件中的真实应用该函数被多个插件在扩展公共订单上下文时复用例如 api-plugin-fulfillment 的 extendCommonOrder.js、api-plugin-shipments 的 extendCommonOrder.js 与 api-plugin-surcharges 的 extendCommonOrder.js——它们在把订单数据传给 fulfillment/shipment/surcharge 提供方之前先通过tagsForCatalogProducts把产品 ID 与标签名配对供下游业务逻辑使用。测试保障与发布机制每个核心函数都配有完整的 Jest 测试generateFilterQuery.test.js用 mock 的collectCollectionFields模拟 Product 集合字段表逐条断言单条件、双条件、多条件生成的 selector 与期望逐字相等toStrictEqualgetAbsoluteUrl.test.js覆盖空 path、带斜杠、已是绝对地址、第三方服务、协议相对地址等边界getPaginatedResponseFromAggregate.test.js验证after/before抛错、管道追加的$group/$unwind/$set/$replaceRoot/$sort/$limitstage、空结果时totalCount为 0 等行为tagsForCatalogProducts.test.js验证标签映射与空数组短路。测试框架配置在 packages/api-utils/jest.config.cjspackage.json的 scripts 提供了npm test与npm run test:watch。发布机制方面官方文档说明该 NPM 包会在每次 push 到trunk分支时自动发布因此要求开发者遵循规范的 Git commit message 格式semantic-release 风格以便版本号自动递增并自动生成 release notes。总结api-utils的价值在于把GraphQL 与 MongoDB 之间的翻译层沉淀成可复用的纯函数generateFilterQuery负责安全地把声明式条件编译为 MongoDB selector 并附带完整校验getPaginatedResponseFromAggregate负责给聚合管道注入排序与分页并输出 Relay Connection 标准结构getAbsoluteUrl与tagsForCatalogProducts则解决 URL 归一化和关联数据批量映射两个高频小问题。理解它们的输入契约与实现细节后你在扩展 Reaction 插件、编写自定义 resolver 时就可以直接引用对应函数保持与官方插件一致的行为与性能特征。更完整的函数索引与用法可继续查阅 packages/api-utils/docs。赞分享后端电商【免费下载链接】reactionProject has been discontinued ////// Mailchimp Open Commerce is an API-first, headless commerce platform built using Node.js, React, GraphQL. Deployed via Docker and Kubernetes.项目地址https://gitcode.com/gh_mirrors/re/reaction点击查看免费下载相关推荐Elementor elementor/utils 包解析统一错误处理 API 与跨包工具函数实战指南Elementor elementor/utils 包解析统一错误处理 API 与跨包工具函数实战指南 本文以 Elementor 开源仓库中的 packaCMS前端后端低代码Whoops API 完全指南核心类、内置处理器与工具函数的实战详解Whoops API 完全指南核心类、内置处理器与工具函数的实战详解 Whoops 是 PHP 生态中广受欢迎的异常与错误处理库它以一套清晰的对象模型 R开发工具后端基于 Reaction Commerce api-utils 包Filter 过滤、Opaque ID、getSlug 与 Mock Collection 的版本演进与源码级解读基于 Reaction Commerce api utils 包Filter 过滤、Opaque ID、getSlug 与 Mock Collection 的后端电商上一篇detectron2Vim/det训练指南自定义训练循环、Trainer 抽象与指标日志全解析下一篇告别焦虑等待3分钟掌握Elsevier期刊审稿状态自动追踪神器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑