├── .browserslistrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_EN.md ├── babel.config.js ├── dist └── axios-api-module.min.js ├── es ├── api-module.js ├── context.js └── index.js ├── index.d.ts ├── lib ├── api-module.js ├── context.js └── index.js ├── package-lock.json ├── package.json ├── src ├── api-module.js ├── context.js └── index.js ├── test ├── constructor.spec.js ├── context.spec.js ├── method.spec.js ├── middleware.spec.js ├── request.spec.js └── utils.js └── webpack.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output/ 3 | coverage/ 4 | .vscode/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | example/ 3 | node_modules/ 4 | test/ 5 | .nyc_output/ 6 | coverage/ 7 | .vscode/ 8 | webpack.config.js 9 | babel.config.js 10 | .travis.yml 11 | .browserslistrc -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | node_js 3 | node_js: 4 | - "8" 5 | install: 6 | - npm install 7 | - npm install -g codecov 8 | script: 9 | - npm run coverage:unit 10 | - codecov -t $CODECOV_TOKEN 11 | 12 | branches: 13 | only: 14 | - master -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0](https://github.com/CalvinVon/axios-api-module/compare/v1.6.0...v2.0.0) (2019-11-18) 2 | 3 | ### BREAKING CHANGES 4 | 5 | * **postRequestMiddleWare:** `postRequestMiddleWare` parameters has changed. 6 | 7 | To migrate the code follow the example below: 8 | 9 | Before: 10 | 11 | ```js 12 | apiMod.registerPostRequestMiddleWare(apiMetas, res, next) { 13 | next(res); 14 | } 15 | ``` 16 | 17 | After: 18 | 19 | ```js 20 | apiMod.registerPostRequestMiddleWare(apiMetas, { data, response }, next) { 21 | console.log(data); 22 | next(response); 23 | } 24 | ``` 25 | 26 | 27 | 28 | # v1.6.0 [(2019-10-28)](https://github.com/CalvinVon/axios-api-module/compare/v1.6.0...v1.5.2) 29 | ### Features 30 | - exports origin meta data by adding [meta property](./README.md##Send-Requests) to request. 31 | 32 | ## v1.5.2 [(2019-10-3)](https://github.com/CalvinVon/axios-api-module/compare/v1.5.1...v1.5.2) 33 | - update dependencies and fix other potential security vulnerabilities 34 | 35 | ## v1.5.1 [(2019-6-13)](https://github.com/CalvinVon/axios-api-module/compare/v1.5.0...v1.5.1) 36 | ### Bug fixes 37 | - `postRequestMiddle` typo error 38 | 39 | # v1.5.0 [(2019-6-13)](https://github.com/CalvinVon/axios-api-module/compare/v1.4.1...v1.5.0) 40 | ### Features 41 | - add `globalPostRequestMiddleWare` static method 42 | - add `registerPostRequestMiddleWare` instance method 43 | 44 | ## v1.4.1 [(2019-5-31)](https://github.com/CalvinVon/axios-api-module/compare/v1.4.0...v1.4.1) 45 | ### Bug Fixes 46 | - fix the bug that the `data` option would be *parsed* first before processing in `foreRequestHook` function. 47 | 48 | # v1.4.0 [(2019-5-29)](https://github.com/CalvinVon/axios-api-module/compare/v1.3.2release...v1.4.0) 49 | 50 | ### BREAKING CHANGES 51 | - **ApiModule#`globalForeRequestMiddleWare`:** method name changed from `registerForeRequestMiddleWare` 52 | - **ApiModule#`globalFallbackMiddleWare`:** method name changed from `registerFallbackMiddleWare` 53 | - **registerFallbackMiddleWare(fallbackHook):** change the second parameters to an object which contains `error` and `data` fields. 54 | - **`axios` dependence:** you need to install `axios` dependence by `npm i axios -S` additional, cause you can have better control of dependency version. 55 | 56 | ### Bug fixes 57 | - **`getAxios`:** now return an axios instance by `axios.create(config)` 58 | 59 | ### Features 60 | - adding unit test and coverage test. 61 | - better error tips when you passing invalid values to `apiMetas` or registering middlewares. 62 | 63 | ## v1.3.2 [(2019-4-2)](https://github.com/CalvinVon/axios-api-module/compare/v1.3.0...v1.3.2release) 64 | ### Bug fixes 65 | - fix default error handler would still be invoked when fallbackMiddle has been registered already. 66 | 67 | # v1.3.0 [(2019-3-19)](https://github.com/CalvinVon/axios-api-module/compare/v1.2.0...v1.3.0) 68 | ### Features 69 | - **`baseConfig`:** add baseConfig for creating axios by default configuration. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Calvin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API 设计已经过时,本模块不再维护! 2 | 3 | # The API design is outdated, this module is no longer maintained! 4 | 5 | # axios-api-module 6 | 一个专注于业务并基于 [axios](https://github.com/axios/axios) 的模块化封装模块。 7 | 8 | 尝试一下带有模块化文件分割的 webpack 工程化[例子](https://stackblitz.com/edit/test-axios-api-module) 9 | 10 | [![version](https://img.shields.io/npm/v/@calvin_von/axios-api-module.svg)](https://www.npmjs.com/package/@calvin_von/axios-api-module) 11 | [![codecov](https://codecov.io/gh/CalvinVon/axios-api-module/branch/master/graph/badge.svg)](https://codecov.io/gh/CalvinVon/axios-api-module) 12 | [![](https://img.shields.io/npm/dt/@calvin_von/axios-api-module.svg)](https://github.com/CalvinVon/axios-api-module) 13 | ![npm bundle size (scoped)](https://img.shields.io/bundlephobia/min/@calvin_von/axios-api-module) 14 | [![Build Status](https://travis-ci.org/CalvinVon/axios-api-module.svg?branch=master)](https://travis-ci.org/CalvinVon/axios-api-module) 15 | [![dependencies](https://img.shields.io/david/CalvinVon/axios-api-module.svg)](https://www.npmjs.com/package/@calvin_von/axios-api-module) 16 | 17 | [中文文档](./README.md) 18 | | 19 | [English Doc](/README_EN.md) 20 | 21 | # 目录 22 | - [快速上手](#快速上手) 23 | - [安装](#安装) 24 | - [典型用法](#典型用法) 25 | - [定义请求接口](#定义请求接口) 26 | - [单个命名空间](#单个命名空间) 27 | - [启用模块化命名空间](#启用模块化命名空间) 28 | - [发送请求](#发送请求) 29 | - [设置中间件](#设置中间件) 30 | - [中间件定义](#中间件定义) 31 | - [为每一个实例设置中间件](#为每一个实例设置中间件) 32 | - [全局中间件](#全局中间件) 33 | - [设置 axios 拦截器](#设置-axios-拦截器) 34 | - [导出 axios 实例](#导出-axios-实例) 35 | - [执行顺序](#执行顺序) 36 | - [设置拦截器](#设置拦截器) 37 | - [选项](#选项) 38 | - [baseConfig 选项](#baseConfig-选项) 39 | - [module 选项](#module-选项) 40 | - [API 手册](#API-手册) 41 | - [类 `ApiModule`](#类-`ApiModule`) 42 | - [静态方法](#静态方法) 43 | - [globalBefore](#globalBefore) 44 | - [globalAfter](#globalAfter) 45 | - [globalCatch](#globalCatch) 46 | - [实例方法](#实例方法) 47 | - [#useBefore](#useBefore) 48 | - [#useAfter](#useAfter) 49 | - [#useCatch](#useCatch) 50 | - [#getInstance](#getInstance) 51 | - [#getAxios](#getAxios) 52 | - [#generateCancellationSource](#generateCancellationSource) 53 | - [类 `Context`](#类-`Context`) 54 | - [只读成员](#只读成员) 55 | - [metadata](#metadata) 56 | - [metadataKeys](#metadataKeys) 57 | - [method](#method) 58 | - [baseURL](#baseURL) 59 | - [url](#url) 60 | - [data](#data) 61 | - [response](#response) 62 | - [responseError](#responseError) 63 | - [实例方法](#实例方法) 64 | - [setData](#setData) 65 | - [setResponse](#setResponse) 66 | - [setError](#setError) 67 | - [setAxiosOptions](#setAxiosOptions) 68 | - [版本变更记录](#版本变更记录) 69 | - [许可证](#许可证) 70 | 71 | # 快速上手 72 | ## 安装 73 | 使用 npm 安装 74 | > **注意**:axios 库并不会包含在发布包中,你需要单独安装 axios 依赖 75 | 76 | ```bash 77 | npm i axios @calvin_von/axios-api-module -S 78 | ``` 79 | 或者使用 yarn 安装: 80 | ```bash 81 | yarn add axios @calvin_von/axios-api-module 82 | ``` 83 | 84 | 或者直接 CDN 方式引入: 85 | ```html 86 | 87 | 88 | 89 | 90 | ``` 91 | 92 | > 为什么?这样设计便可使用户自由选择适合的 axios 版本(请遵循 [semver](https://semver.org/) 版本规则,现在支持 0.x 版本) [![axios version](https://img.shields.io/npm/v/axios?label=axios)](https://www.npmjs.org/package/axios) 93 | 94 | --- 95 | 96 | ## 典型用法 97 | 98 | ```js 99 | import ApiModule from "@calvin_von/axios-api-module"; 100 | // 或者 CDN 导入 101 | // var ApiModule = window['ApiModule']; 102 | 103 | // 创建一个模块化命名空间的实例 104 | const apiMod = new ApiModule({ 105 | baseConfig: { 106 | baseURL: 'http://api.yourdomain.com', 107 | headers: { 108 | 'Content-Type': 'application/json; charset=UTF-8' 109 | }, 110 | withCredentials: true, 111 | timeout: 60000 112 | }, 113 | module: true, 114 | metadatas: { 115 | main: { 116 | getList: { 117 | url: '/api/list/', 118 | method: 'get', 119 | // 添加其他自定义字段 120 | name: 'GetMainList' 121 | } 122 | }, 123 | user: { 124 | getInfo: { 125 | method: 'get' 126 | // 支持多种路径参数定义方式 127 | url: '/api/user/{uid}/info' 128 | // url: '/api/user/:uid/info' 129 | } 130 | } 131 | } 132 | }); 133 | 134 | // 拿到转换之后的请求实例 135 | const apiMapper = apiMod.getInstance(); 136 | apiMapper.$module === apiMod; // true 137 | 138 | // 发送请求 139 | // apiMapper 由传入的 metadatas 选项映射 140 | apiMapper.main.getList({ query: { pageSize: 10, pageNum: 1 } }); 141 | apiMapper.user.getInfo({ params: { uid: 88 } }); 142 | ``` 143 | 144 | --- 145 | 146 | ## 定义请求接口 147 | 你需要将接口组织成一个对象(或者由多个命名空间的对象)传入 `metadatas` 选项中 148 | 149 | - ### 单个命名空间 150 | 当接口数目不多,或者希望实例化多个时,**将 `module` 设置成 `false` 或空值**,`ApiModule` 会采用单个命名空间 151 | ```js 152 | const apiModule = new ApiModule({ 153 | module: false, 154 | metadatas: { 155 | requestA: { url: '/path/to/a', method: 'get' }, 156 | requestB: { url: '/path/to/b', method: 'post' }, 157 | } 158 | // other options... 159 | }); 160 | ``` 161 | 使用 [`#getInstance`](#getInstance) 方法来获得转换之后的请求集合对象 162 | ```js 163 | const apiMapper = apiModule.getInstance(); 164 | apiMapper 165 | .requestA({ query: { a: 'b' } }) 166 | .then(data => {...}) 167 | .catch(error => {...}) 168 | ``` 169 | 170 | - ### 启用模块化命名空间 171 | **将 `module` 设置为 `true` 时**, `ApiModule` 会启用多个命名空间 172 | ```js 173 | const apiModule = new ApiModule({ 174 | module: true, 175 | metadatas: { 176 | moduleA: { 177 | request: { url: '/module/a/request', method: 'get' }, 178 | }, 179 | moduleB: { 180 | request: { url: '/module/b/request', method: 'post' }, 181 | } 182 | } 183 | // other options... 184 | }); 185 | 186 | const apiMapper = apiModule.getInstance(); 187 | apiMapper 188 | .moduleA 189 | .request({ query: { module: 'a' } }) 190 | .then(data => {...}) 191 | .catch(error => {...}) 192 | 193 | apiMapper 194 | .moduleB 195 | .request({ body: { module: 'b' } }) 196 | .then(data => {...}) 197 | .catch(error => {...}) 198 | ``` 199 | 200 | --- 201 | 202 | ## 发送请求 203 | 使用 [ApiModule#getInstance](#getInstance) 方法来获得转换之后的请求集合对象,然后你需要像这样来发送一个请求: 204 | ```js 205 | Request({ query: {...}, body: {...}, params: {...} }, opt?) 206 | ``` 207 | 208 | - **query**: 209 | 与请求一起发送的 URL 参数。必须是一个普通对象或 `URLSearchParams` 对象。在 axios 上 [查看 params 选项](https://github.com/axios/axios#request-config) 210 | 211 | - **params**: 212 | 支持动态 URL 参数 (用法类似于 vue-router 的 [动态匹配](https://router.vuejs.org/guide/essentials/dynamic-matching.html)) 213 | 214 | - **body**: 215 | 要作为请求体正文发送的数据。在 axios 上 [查看 data 选项](https://github.com/axios/axios#request-config) 216 | 217 | - **opt**: 218 | 提供更多 axios 原始请求配置。在 axios 上 [查看 Request Config](https://github.com/axios/axios#request-config) 219 | 220 | ```js 221 | const request = apiMapper.user.getInfo; 222 | 223 | // *可以配置 context 参数 224 | console.log(request.context); 225 | 226 | // axios origin request options 227 | const config = { /* Axios Request Config */ }; 228 | const requestData = { 229 | params: { 230 | uid: this.uid 231 | }, 232 | query: { 233 | ts: Date.now() 234 | } 235 | }; 236 | 237 | // 发送请求 238 | request(requestData, config) 239 | .then(data => {...}) 240 | .catch(error => {...}) 241 | 242 | // 与下列直接使用 axios 的代码执行效果一致 243 | axios.get(`/api/user/${this.uid}/info`, { 244 | query: { 245 | ts: Date.now() 246 | } 247 | }); 248 | ``` 249 | 250 | --- 251 | 252 | ## 设置中间件 253 | `ApiModule` 拥有中间件机制,围绕请求的**请求前**、**请求后**和**请求失败**阶段设计了更细粒度的统一控制,以帮助开发者更好地组织代码 254 | 255 | 推荐的方式是,在定义接口的 *metadata* 中定义自定义字段,然后在对应的中间件内获取并执行一定操作。 256 | 257 | 下面是一个在发起请求前添加用户信息参数并在请求成功后预处理数据的例子: 258 | ```js 259 | const userId = getUserIdSomehow(); 260 | const userToken = getUserTokenSomehow(); 261 | 262 | apiModule.useBefore((context, next) => { 263 | const { appendUserId, /** 其他自定义字段 */ } = context.metadata; 264 | 265 | if (appendUserId) { 266 | const data = context.data || {}; 267 | if (data.query) { 268 | data.query.uid = userId; 269 | } 270 | context.setData(data); 271 | context.setAxiosOptions({ 272 | headers: { 273 | 'Authorization': token 274 | } 275 | }); 276 | } 277 | 278 | next(); // next 函数必须要被调用 279 | }); 280 | 281 | apiModule.useAfter((context, next) => { 282 | const responseData = context.response; 283 | const { preProcessor, /** 其他自定义字段 */ } = context.metadata; 284 | if (preProcessor) { 285 | try { 286 | context.setResponse(preProcessor(responseData)); 287 | } catch (e) { 288 | console.error(e); 289 | } 290 | } 291 | 292 | next(); 293 | }); 294 | ``` 295 | 296 | 297 | > 事实上,`ApiModule` 的设计初衷,是避免编写重复臃肿的代码,从而分离出业务代码。 298 | 299 | > 而且 `ApiModule` 将 `axios` 提供的**拦截器**视为封装浏览器请求的“底层”层面事务,抽离出中间件模式来处理**业务层面**的事务,你可以把每一个接口定义当做是数据源服务(就像是 Angular 里面的“Service”概念),你可以做一些与页面无关的操作,故称之为“*一个专注于业务的封装模块*”。 300 | 301 | 302 | ### 中间件定义 303 | - 类型: `(context, next) => null` 304 | - 参数: 305 | 306 | 每个中间件均包含两个参数: 307 | - `context` 308 | 309 | - 类型:[Context](#类-`Context`) 310 | - 描述:提供一系列方法来修改包括请求的参数、响应的数据、错误数据以及请求的 axios 选项,并提供一系列请求相关的只读参数。 311 | 312 | - `next` 313 | - 类型:`(error?: object|string|Error) => null` 314 | - 描述: 315 | - 每个中间件必须调用 `next` 函数来进入到下一步。 316 | - 传入错误参数将导致请求失败(在前置中间件将不会发送真实请求且直接导致请求 `rejected`)。 317 | - 使用 [Context#setError](#setError) 方法传入错误参数和在 `next` 函数传入的参数行为一致。 318 | 319 | 320 | ### 为每一个实例设置中间件 321 | 多个 `ApiModule` 实例之间不互相影响,**实例单独设置的中间件会覆盖全局设置的中间件** 322 | 323 | - 设置请求前置中间件:[ApiModule#useBefore](#useBefore) 324 | - 设置请求后置中间件:[ApiModule#useAfter](#useAfter) 325 | - 设置请求失败中间件:[ApiModule#useCatch](#useCatch) 326 | 327 | 328 | ### 全局中间件 329 | 设置全局中间件,将会**影响之后所有**创建的 `ApiModule` 实例 330 | 331 | - 设置请求前置中间件:[ApiModule.globalBefore](#globalBefore) 332 | - 设置请求后置中间件:[ApiModule.globalAfter](#globalAfter) 333 | - 设置请求失败中间件:[ApiModule.globalCatch](#globalCatch) 334 | 335 | 336 | --- 337 | 338 | ## 设置 axios 拦截器 339 | 你仍然可以设置 axios 的拦截器,使用 `ApiModule` 并不会影响到原来的拦截器用法 340 | 341 | ### 导出 axios 实例 342 | 你可以使用 [ApiModule#getAxios](#getAxios) 方法导出 axios 实例来设置拦截器 343 | 344 | 345 | ### 执行顺序 346 | > 理清 `axios 拦截器` 和 `ApiModule 中间件` 之间的执行顺序 347 | > 1. 请求前置中间件 348 | > 2. axios 请求拦截器 349 | > 3. axios 响应拦截器 350 | > 4. 请求后置或者失败中间件 351 | 352 | 可以看出,对于我们的业务 `axios` 的执行更加的“底层”一些,所以我们建议**业务相关**的代码放在中间件中实现,而拦截器*仅仅来判断请求发送成功与否或者实现一些协议、框架相关的事务*。 353 | 354 | 355 | ### 设置拦截器 356 | ```js 357 | const axiosInstance = apiMod.getAxios(); 358 | 359 | axiosInstance.interceptors.request.use( 360 | function (config) { 361 | return config; 362 | }, 363 | function (error) { 364 | return Promise.reject(error); 365 | } 366 | ); 367 | 368 | axiosInstance.interceptors.response.use( 369 | function (response) { 370 | if (response.data.status === 200) { 371 | return response.data; 372 | } 373 | return Promise.reject(new Error(response.msg)); 374 | }, 375 | function (error) { 376 | return Promise.reject(error); 377 | } 378 | ); 379 | ``` 380 | 381 | # 选项 382 | ```js 383 | const apiMod = new ApiModule({ 384 | baseConfig: { /*...*/ }, // Object, axios 请求的选项参数 385 | module: true, // Boolean, 是否启用模块化命名空间 386 | console: true, // Boolean, 是否启用请求失败日志 387 | metadatas: { 388 | main: { // 命名空间 389 | getList: { 390 | method: 'get', // 请求方式 "get" | "post" | "patch" | "delete" | "put" | "head" 391 | url: '/api/user/list' // 请求路径 392 | } 393 | } 394 | } 395 | }); 396 | ``` 397 | --- 398 | 399 | ## baseConfig 选项 400 | 401 | 设置 axios 的请求选项参数。查看 [Axios 文档 (#Request Config)](https://github.com/axios/axios#request-config) 402 | 403 | 404 | ## module 选项 405 | 406 | 是否启用命名空间,[了解更多](#定义请求接口)。 407 | 408 | > 在使用 Vue.js 的一个例子: 409 | 你可以创建多个 `ApiModule` 的实例, 尤其是当 `module` 选项置为 `false` 值时 410 | 411 | ```js 412 | Vue.prototype.$foregroundApi = foregroundApis; 413 | Vue.prototype.$backgroundApi = backgroundApis; 414 | ``` 415 | 416 | # API 手册 417 | ## 类 `ApiModule` 418 | ### 静态方法 419 | ### globalBefore 420 | 设置请求前置中间件,和 [#useBefore](#useBefore) 定义一致,但会被实例方法覆盖,且会影响生成的全部 `ApiModule` 实例 421 | 422 | ### globalAfter 423 | 设置请求后置中间件,和 [#useAfter](#useAfter) 定义一致,但会被实例方法覆盖,且会影响生成的全部 `ApiModule` 实例 424 | 425 | ### globalCatch 426 | 设置请求失败中间件,和 [#useCatch](#useCatch) 定义一致,但会被实例方法覆盖,且会影响生成的全部 `ApiModule` 实例 427 | 428 | ## 实例方法 429 | ### #useBefore 430 | - 参数: `foreRequestHook: (context, next) => null)` 查看 [中间件定义](#中间件定义) 431 | - 描述 432 | 433 | 传入的**前置中间件**会**在每个请求前被调用**,可使用且有效的 `context` 方法如下: 434 | - [context#setData](#setData) 设置请求数据 435 | - [context#setError](#setError) 设置请求错误 436 | - [context#setAxiosOptions](#setAxiosOptions) 设置请求的 axios 选项 437 | 438 | 若在此时设置错误参数,则会导致真实请求不会被发送,直接进入请求失败阶段 439 | 440 | ### #useAfter 441 | - 参数: `postRequestHook: (context, next) => null)` 查看 [中间件定义](#中间件定义) 442 | - 描述 443 | 444 | 传入的**后置中间件**会**在每个请求成功后被调用**,可使用且有效的 `context` 方法如下: 445 | - [context#setResponse](#setData) 设置请求响应 446 | - [context#setError](#setError) 设置请求错误 447 | 448 | 若在此时设置错误参数,即使请求成功,该请求也将进入请求失败阶段 449 | 450 | ### #useCatch 451 | - 参数: `fallbackHook: (context, next) => null)` 查看 [中间件定义](#中间件定义) 452 | - 描述 453 | 454 | 传入的**失败中间件**会**在每个请求失败(或者设定错误)后被调用**,可使用且有效的 `context` 方法如下: 455 | - [context#setError](#setError) 设置请求错误 456 | 457 | 若在此时设置错误参数,会覆盖原始的错误值 458 | 459 | ### #getInstance 460 | - 返回:`TransformedRequestMapper | { [namespace: string]: TransformedRequestMapper, $module?: ApiModule };` 461 | - 描述:获取到映射后的请求集合对象 462 | ```js 463 | const apiModule = new ApiModule({ /*...*/ }); 464 | const apiMapper = apiModule.getInstance(); 465 | 466 | apiMapper.xxx({ /* `query`, `body`, `params` data here */ }, { /* Axios Request Config */ }); 467 | ``` 468 | 469 | ### #getAxios 470 | - 返回:`AxiosInstance` 471 | - 描述: 获取设置完 `baseConfig` 过后的 axios 实例 472 | ```js 473 | const apiModule = new ApiModule({ /*...*/ }); 474 | const axios = apiModule.getAxios(); 475 | 476 | axios.get('/other/path', { /* Axios Request Config */ }); 477 | ``` 478 | 479 | ### #generateCancellationSource 480 | - 返回:`CancelTokenSource` 481 | - 描述:生成 axios `Cancellation` source. 482 | 483 | 你可以直接使用 axios 的 `HTTP cancellation`, 查看([axios#cancellation 的文档](https://github.com/axios/axios#cancellation)) 484 | ```js 485 | import axios from 'axios'; 486 | 487 | const CancelToken = axios.CancelToken; 488 | const source = CancelToken.source(); 489 | 490 | ... 491 | ``` 492 | 493 | 或者调用 `ApiModule#generateCancellationSource()` 494 | ```js 495 | ... 496 | 497 | const api = apiMod.getInstance(); 498 | const cancelSourceA = api.$module.generateCancellationSource(); 499 | const cancelSourceB = api.$module.generateCancellationSource(); 500 | 501 | // 发送请求 502 | const requestA = api.test({ 503 | query: { 504 | a: 123 505 | }, 506 | }, { 507 | cancelToken: cancelSourceA.token 508 | }); 509 | 510 | const requestB = api.test({ 511 | query: { 512 | b: 321 513 | }, 514 | }, { 515 | cancelToken: cancelSourceB.token 516 | }); 517 | 518 | cancelSourceA.cancel('用户主动取消'); 519 | 520 | // requestA 将会是 rejected 状态,错误原因是 `用户主动取消` 521 | // requestB 正常发送! 522 | ``` 523 | --- 524 | 525 | ## 类 `Context` 526 | 527 | ### 只读成员 528 | ### metadata 529 | 当前请求设置的 metadata 元数据(的拷贝),即修改该只读值并不会影响该接口定义的元数据 530 | 531 | ### metadataKeys 532 | 当前请求对应的 metadata 元数据的对象路径数组,例如请求 `apiMapper.moduleA.interfaceB` 方法对应的是 `['moduleA', 'interfaceB']`。 533 | 534 | 在开发环境中实用。 535 | 536 | ### method 537 | 当前请求的请求方法 538 | 539 | ### baseURL 540 | 当前请求的 baseURL 541 | 542 | ### url 543 | 当前请求的完整请求 url,为 baseURL 和 解析过的 metadata.url 的组合 544 | 545 | ### data 546 | 当前请求的请求参数,类型[查看详情](#发送请求): 547 | - data.query?: object 请求的 `URLSearchParams` 查询参数 548 | - data.params?: object 请求的动态 URL 参数。支持 `/:id` 和 `/{id}` 定义法 549 | - data.body?: object 请求的请求体数据 550 | - 添加其他用户自定义字段,可在中间件中访问到 551 | 552 | ### response 553 | 当前请求的响应数据 554 | 555 | ### responseError 556 | 当前请求的响应错误数据,或者是手动设置的错误数据,存在该值**不代表请求一定失败** 557 | 558 | ### axiosOptions 559 | 当前请求即将使用的 `axios` 选项参数,将会由请求传入的第二个 `opt` 参数和 `context#setAxiosOptions` 合并得到 560 | 561 | ### 实例方法 562 | ### setData 563 | 设置请求传入的请求参数([查看详情](#发送请求)),将覆盖传入数据以达到改写请求数据的目的 564 | 565 | ### setResponse 566 | 设置请求的响应数据,将覆盖原来的响应以达到改写请求成功数据的目的 567 | 568 | ### setError 569 | 设置请求失败数据,无论请求是否成功,均会返回失败 570 | 571 | ### setAxiosOptions 572 | 设置 `axios` 请求的选项,但会和请求方法中传入的 `axios` 选项**合并**,**且优先级没有请求方法中传入的参数高** 573 | 574 | --- 575 | 576 | # 版本变更记录 577 | [版本变更记录](./CHANGELOG.md) 578 | 579 | # 许可证 580 | [MIT 许可证](./LICENSE) 581 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # axios-api-module 2 | A business-focused modular encapsulate module based on axios. 3 | 4 | Try this webpack project [example](https://stackblitz.com/edit/test-axios-api-module) with modular file splitting. 5 | 6 | [![version](https://img.shields.io/npm/v/@calvin_von/axios-api-module.svg)](https://www.npmjs.com/package/@calvin_von/axios-api-module) 7 | [![codecov](https://codecov.io/gh/CalvinVon/axios-api-module/branch/master/graph/badge.svg)](https://codecov.io/gh/CalvinVon/axios-api-module) 8 | [![](https://img.shields.io/npm/dt/@calvin_von/axios-api-module.svg)](https://github.com/CalvinVon/axios-api-module) 9 | ![npm bundle size (scoped)](https://img.shields.io/bundlephobia/min/@calvin_von/axios-api-module) 10 | [![Build Status](https://travis-ci.org/CalvinVon/axios-api-module.svg?branch=master)](https://travis-ci.org/CalvinVon/axios-api-module) 11 | [![dependencies](https://img.shields.io/david/CalvinVon/axios-api-module.svg)](https://www.npmjs.com/package/@calvin_von/axios-api-module) 12 | 13 | [中文文档](./README.md) 14 | | 15 | [English Doc](/README_EN.md) 16 | 17 | # Table of contents 18 | - [Getting Started](#Getting-Started) 19 | - [Install](#Install) 20 | - [Typical Usage](#Typical-Usage) 21 | - [Define request interface](#define-request-interface) 22 | - [Single Namespace](#Single-Namespace) 23 | - [Enable Modular Namespace](#Enable-Modular-Namespace) 24 | - [Send Requests](#Send-Requests) 25 | - [Set Middlewares](#Set-Middlewares) 26 | - [Middleware Definition](#Middleware-Definition) 27 | - [Set middlewares for each instance](#Set-middlewares-for-each-instance) 28 | - [Set global middlewares](#Set-global-Middlewares) 29 | - [Set axios interceptor](#Set-axios-Interceptor) 30 | - [Export axios instance](#Export-axios-Instance) 31 | - [Execution order](#execution-order) 32 | - [Set Interceptor](#Set-Interceptor) 33 | - [Options](#Options) 34 | - [`baseConfig` option](#`baseConfig`-option) 35 | - [`module` option](#`module`-option) 36 | - [API Reference](#API-Reference) 37 | - [class `ApiModule`](#class-`ApiModule`) 38 | - [Static Method](#Static-Method) 39 | - [globalBefore](#globalBefore) 40 | - [globalAfter](#globalAfter) 41 | - [globalCatch](#globalCatch) 42 | - [Instance Method](#Instance-Method) 43 | - [#useBefore](#useBefore) 44 | - [#useAfter](#useAfter) 45 | - [#useCatch](#useCatch) 46 | - [#getInstance](#getInstance) 47 | - [#getAxios](#getAxios) 48 | - [#generateCancellationSource](#generateCancellationSource) 49 | - [class `Context`](#class-`Context`) 50 | - [Read-only Members](#Read-only-Members) 51 | - [metadata](#metadata) 52 | - [method](#method) 53 | - [baseURL](#baseURL) 54 | - [url](#url) 55 | - [data](#data) 56 | - [response](#response) 57 | - [responseError](#responseError) 58 | - [Instance Method](#Instance-Method) 59 | - [setData](#setData) 60 | - [setResponse](#setResponse) 61 | - [setError](#setError) 62 | - [setAxiosOptions](#setAxiosOptions) 63 | - [CHANGELOG](#CHANGELOG) 64 | - [LICENSE](#LICENSE) 65 | 66 | # Getting Started 67 | ### Install 68 | You can install the library via npm. 69 | > **Note**: the axios library is not included in the package, you need to install the axios dependency separately 70 | ```bash 71 | npm i axios @calvin_von/axios-api-module -S 72 | ``` 73 | or via yarn: 74 | ```bash 75 | yarn add axios @calvin_von/axios-api-module 76 | ``` 77 | 78 | or via CDN 79 | ```html 80 | 81 | 82 | 83 | 84 | ``` 85 | > Why? This design allows users to freely choose the appropriate axios version (please follow the [semver](https://semver.org/) version rule, and now we supports 0.x versions) [![Axios version](https://img.shields.io/npm/v/axios?label=axios)](https://www.npmjs.org/package/axios) 86 | 87 | --- 88 | 89 | ### Typical Usage 90 | 91 | ```js 92 | // You should import axios at first 93 | import axios from 'axios'; 94 | 95 | import ApiModule from "@calvin_von/axios-api-module"; 96 | // or CDN import 97 | // var ApiModule = window['ApiModule']; 98 | 99 | // create a modular namespace ApiModule instance 100 | const apiMod = new ApiModule({ 101 | baseConfig: { 102 | baseURL: 'http://api.yourdomain.com', 103 | headers: { 104 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' 105 | }, 106 | withCredentials: true, 107 | timeout: 60000 108 | }, 109 | module: true, 110 | metadatas: { 111 | main: { 112 | getList: { 113 | url: '/api/list/', 114 | method: 'get', 115 | // Add another custom fields 116 | name: 'GetMainList' 117 | } 118 | }, 119 | user: { 120 | getInfo: { 121 | // support multiple params definitions 122 | // url: '/api/user/:uid/info', 123 | url: '/api/user/{uid}/info', 124 | method: 'get' 125 | name: 'getUserInfo', 126 | } 127 | } 128 | } 129 | }); 130 | 131 | 132 | // get the converted request instance 133 | const apiMapper = apiMod.getInstance(); 134 | apiMapper.$module === apiMod; // true 135 | 136 | // send request 137 | // apiMapper is mapped by the passed metadatas option 138 | apiMapper.main.getList({ query: { pageSize: 10, pageNum: 1 } }); 139 | apiMapper.user.getInfo({ params: { uid: 88 } }); 140 | ``` 141 | 142 | --- 143 | 144 | 145 | ## Define request interface 146 | You need to organize the interface into an object (or objects from multiple namespaces) and pass it into the metadatas option. 147 | 148 | - ### Single namespace 149 | When the number of interfaces is not large, or if you want to instantiate more than one, **set `module` to `false` or empty value**, `ApiModule` will adopt a single namespace 150 | ```js 151 | const apiModule = new ApiModule({ 152 | module: false, 153 | metadatas: { 154 | requestA: { url: '/path/to/a', method: 'get' }, 155 | requestB: { url: '/path/to/b', method: 'post' }, 156 | } 157 | // other options... 158 | }); 159 | ``` 160 | Use the [`#getInstance`](#getInstance) method to get the request collection object after conversion 161 | 162 | ```js 163 | const apiMapper = apiModule.getInstance(); 164 | apiMapper 165 | .requestA({ query: { a: 'b' } }) 166 | .then(data => {...}) 167 | .catch(error => {...}) 168 | ``` 169 | 170 | - ### Enable Modular Namespace 171 | **When `module` is set to `true`**, `ApiModule` will enable multiple namespaces 172 | ```js 173 | const apiModule = new ApiModule({ 174 | module: true, 175 | metadatas: { 176 | moduleA: { 177 | request: { url: '/module/a/request', method: 'get' }, 178 | }, 179 | moduleB: { 180 | request: { url: '/module/b/request', method: 'post' }, 181 | } 182 | } 183 | // other options... 184 | }); 185 | 186 | const apiMapper = apiModule.getInstance(); 187 | apiMapper 188 | .moduleA 189 | .request({ query: { module: 'a' } }) 190 | .then(data => {...}) 191 | .catch(error => {...}) 192 | 193 | apiMapper 194 | .moduleB 195 | .request({ body: { module: 'b' } }) 196 | .then(data => {...}) 197 | .catch(error => {...}) 198 | ``` 199 | 200 | --- 201 | 202 | 203 | ## Send Requests 204 | To send request, you need to use the [ApiModule#getInstance](#getInstance) method to get the converted request collection object, then just like this: 205 | ```js 206 | Request({ query: {...}, body: {...}, params: {...} }, opt?) 207 | ``` 208 | 209 | - **query**: The URL parameters to be sent with the request, must be a plain object or an URLSearchParams object. [axios params option](https://github.com/axios/axios#request-config) 210 | 211 | - **params**: Support dynamic url params(usage likes [vue-router dynamic matching](https://router.vuejs.org/guide/essentials/dynamic-matching.html)) 212 | 213 | - **body**: The data to be sent as the request body. [axios data option](https://github.com/axios/axios#request-config) 214 | 215 | - **opt**: More original request configs available. [Request Config](https://github.com/axios/axios#request-config) 216 | 217 | ```js 218 | const request = apiMapper.user.getInfo; 219 | 220 | // *configurable context parameter 221 | console.log(request.context); 222 | 223 | // axios origin request options 224 | const config = { /* Axios Request Config */ }; 225 | const requestData = { 226 | params: { 227 | uid: this.uid 228 | }, 229 | query: { 230 | ts: Date.now() 231 | } 232 | }; 233 | 234 | // is equal to 235 | axios.get(`/api/user/${this.uid}/info`, { 236 | query: { 237 | ts: Date.now() 238 | } 239 | }); 240 | ``` 241 | 242 | In addition, each converted request has a `context` parameter, which is convenient for setting various parameters of the request outside the middleware 243 | ```js 244 | const context = Request.context; 245 | context.setAxoisOptions({ ... }); 246 | ``` 247 | 248 | --- 249 | 250 | 251 | ## Set Intercepters 252 | `ApiModule` has a middleware mechanism, designed more fine-grained unified control around the requested **before request**, **post request**, and**request failed** stages to help developers better organize code 253 | 254 | The recommended way is to define a custom field in the *metadata* that defines the interface, and then get and perform a certain operation in the corresponding middleware. 255 | 256 | The following is an example of adding user information parameters before making a request and preprocessing the data after the request is successful: 257 | 258 | ```js 259 | const userId = getUserIdSomehow(); 260 | const userToken = getUserTokenSomehow(); 261 | 262 | apiModule.useBefore((context, next) => { 263 | const { appendUserId, /** other custom fields */ } = context.metadata; 264 | 265 | if (appendUserId) { 266 | const data = context.data || {}; 267 | if (data.query) { 268 | data.query.uid = userId; 269 | } 270 | context.setData(data); 271 | context.setAxiosOptions({ 272 | headers: { 273 | 'Authorization': token 274 | } 275 | }); 276 | } 277 | 278 | next(); // next must be called 279 | }); 280 | 281 | apiModule.useAfter((context, next) => { 282 | const responseData = context.response; 283 | const { preProcessor, /** other custom fields */ } = context.metadata; 284 | if (preProcessor) { 285 | try { 286 | context.setResponse(preProcessor(responseData)); 287 | } catch (e) { 288 | console.error(e); 289 | } 290 | } 291 | 292 | next(); 293 | }); 294 | ``` 295 | 296 | > In fact, `ApiModule` was originally designed to avoid writing bloated code repeatedly, thereby separating business code. 297 | 298 | > Moreover, `ApiModule` regards the interceptor provided by the axios as the "low-level" level affairs that encapsulates the browser request, also, `ApiModule` designs the middleware pattern to handle the "**business level**" affairs. In fact, you can put each interface definition is treated as a data source service (something like the "Service" concept in Angular), and you can do some operations that are not related to the page, so it is called "*a business-focused packaging module*". 299 | 300 | ### Middleware definition 301 | - Type: `(context, next) => null` 302 | - Parameters: 303 | 304 | Each middleware contains two parameters: 305 | - `context` 306 | - Type: [Context](#class-Context) 307 | - Description: Provides a series of methods to modify request parameters, response data, error data, and request axios options, and provides a series of request-related read-only parameters. 308 | 309 | - `next` 310 | - Type: `(error?: object | string | Error) => null` 311 | - Description: 312 | - Each middleware must call the `next` function to proceed to the next step. 313 | - Passing the error parameters will cause the request to fail (the browser will not send a real request and will directly cause the request to be rejected in the fore-request middleware). 314 | - Passing the error parameters using the [Context#setError](#setError) method behaves the same as the parameters passed in the `next` function. 315 | 316 | ### Set middlewares for each instance 317 | Multiple `ApiModule` instances do not affect each other. **Middleware set separately by the instance will override globally set middleware** 318 | 319 | - Set the fore-request middleware: [ApiModule#useBefore](#useBefore) 320 | - Set the post-request middleware: [ApiModule#useAfter](#useAfter) 321 | - Set the request failed middleware: [ApiModule#useCatch](#useCatch) 322 | 323 | 324 | ### Global middlewares 325 | Setting the global middlewares will affect all `ApiModule` instances created later 326 | 327 | - Set the fore-request middleware: [ApiModule.globalBefore](#globalBefore) 328 | - Set the post-request middleware: [ApiModule.globalAfter](#globalAfter) 329 | - Set the request failed middleware: [ApiModule.globalCatch](#globalCatch) 330 | 331 | --- 332 | 333 | ## Setting up axios interceptor 334 | You can still set axios interceptors. Using `ApiModule` will not affect the original interceptor usage. 335 | 336 | ### Export axios instance 337 | You can use the [ApiModule#getAxios](#getAxios) method to export the `axios` instance to set the interceptor 338 | 339 | 340 | ### Execution order 341 | 342 | > Execution order between `axios intercepters` and `ApiModule middlewares` 343 | > 1. fore-request middleware 344 | > 2. axios request intercepter 345 | > 3. axios response intercepter 346 | > 4. post-request or fallback middleware 347 | 348 | It can be seen that the execution of our business `axios` is more "underlying", so we recommend that **business-related** code be implemented in the middleware, and the interceptor *is only to determine whether the request is sent successfully or implements some protocol and framework related affairs*. 349 | 350 | 351 | ### Set interceptor 352 | 353 | ```js 354 | const axiosInstance = apiMod.getAxios(); 355 | 356 | axiosInstance.interceptors.request.use( 357 | function (config) { 358 | return config; 359 | }, 360 | function (error) { 361 | return Promise.reject(error); 362 | } 363 | ); 364 | 365 | axiosInstance.interceptors.response.use( 366 | function (response) { 367 | if (response.data.status === 200) { 368 | return response.data; 369 | } 370 | return Promise.reject(new Error(response.msg)); 371 | }, 372 | function (error) { 373 | return Promise.reject(error); 374 | } 375 | ); 376 | ``` 377 | 378 | # Options 379 | ```js 380 | const apiMod = new ApiModule({ 381 | baseConfig: { /*...*/ }, // Object, axios request config 382 | module: true, // Boolean, whether modular namespace 383 | console: true, // Boolean, switch log on off 384 | metadatas: { 385 | main: { // namespace module 386 | getList: { 387 | method: 'get', // request method "get" | "post" | "patch" | "delete" | "put" | "head" 388 | url: '/api/user/list' 389 | } 390 | } 391 | } 392 | }); 393 | ``` 394 | --- 395 | ## `baseConfig` option 396 | 397 | Set base axios request config for single api module. 398 | 399 | > More details about baseConfig, see [Axios Doc(#Request Config)](https://github.com/axios/axios#request-config) 400 | 401 | 402 | ## `module` option 403 | 404 | Whether enable modular namespaces. [Learn more](#define-request-interface). 405 | 406 | > Example in Vue.js: 407 | You can create multiple instance, typically when `module` option set to `false` 408 | 409 | ```js 410 | Vue.prototype.$foregroundApi = foregroundApis; 411 | Vue.prototype.$backgroundApi = backgroundApis; 412 | ``` 413 | 414 | --- 415 | 416 | # API Reference 417 | ## class `ApiModule` 418 | ## Static Method 419 | 420 | ### globalBefore 421 | Set the **fore-request middleware**, which is consistent with the definition of [#useBefore](#useBefore), but will be overridden by the instance method and will affect all the `ApiModule` instances 422 | 423 | ### globalAfter 424 | Set the **post-request middleware**, which is consistent with the definition of [#useAfter](#useAfter), but will be overridden by the instance method and will affect all the `ApiModule` instances 425 | 426 | ### globalCatch 427 | Set the **request failed middleware**, which is consistent with the definition of [#useCatch](#useCatch), but will be overridden by the instance method and will affect all the `ApiModule` instances 428 | 429 | ## Instance Method 430 | ### #useBefore 431 | - parameters: `foreRequestHook: (context, next) => null)`. Learn more about the [Middleware Definition](#Middleware-Definition) 432 | - description: 433 | 434 | The passed **fore-request middleware** will be called before every request. The available and effective `context` methods are as follows: 435 | - [context#setData](#setData) 436 | - [context#setError](#setError) 437 | - [context#setAxiosOptions](#setAxiosOptions) 438 | 439 | If the wrong parameters are set at this time, the real request will not be sent, and the request will directly enter the failure stage. 440 | 441 | ### #useAfter 442 | - parameters: `postRequestHook: (context, next) => null)`. Learn more about the [Middleware Definition](#Middleware-Definition) 443 | - description: 444 | 445 | The passed **post-request middleware** will be called after every request is successful. The available and effective `context` methods are as follows: 446 | - [context#setResponse](#setData) 447 | - [context#setError](#setError) 448 | 449 | If error parameters are set at this time, even if the request is successful, the request will enter the request failure stage 450 | 451 | ### #useCatch 452 | - parameters: `fallbackHook: (context, next) => null)`. Learn more about the [Middleware Definition](#Middleware-Definition) 453 | - description: 454 | 455 | The passed **request failed middleware** will be called after each request fails (or is set incorrectly). The available and effective `context` methods are as follows: 456 | - [context#setError](#setError) 457 | 458 | If an error parameter is set at this time, the original error value will be overwritten 459 | 460 | ### #getInstance 461 | - return: `TransformedRequestMapper | { [namespace: string]: TransformedRequestMapper, $module?: ApiModule };` 462 | - description: Get the mapped request collection object 463 | ```js 464 | const apiModule = new ApiModule({ /*...*/ }); 465 | const apiMapper = apiModule.getInstance(); 466 | 467 | apiMapper.xxx({ /* `query`, `body`, `params` data here */ }, { /* Axios Request Config */ }); 468 | ``` 469 | 470 | ### #getAxios 471 | - return: `AxiosInstance` 472 | - description: Get the axios instance that after setted 473 | ```js 474 | const apiModule = new ApiModule({ /*...*/ }); 475 | const axios = apiModule.getAxios(); 476 | 477 | axios.get('/other/path', { /* Axios Request Config */ }); 478 | ``` 479 | 480 | 481 | ### `generateCancellationSource()` 482 | - return: `CancelTokenSource` 483 | - description: Generate axios `Cancellation` source. 484 | 485 | You can use axios `cancellation`, ([docs about axios#cancellation](https://github.com/axios/axios#cancellation)) 486 | ```js 487 | import axios from 'axios'; 488 | 489 | const CancelToken = axios.CancelToken; 490 | const source = CancelToken.source(); 491 | 492 | ... 493 | ``` 494 | 495 | or just use `#generateCancellationSource()` 496 | ```js 497 | ... 498 | 499 | const api = apiMod.getInstance(); 500 | const cancelSourceA = api.$module.generateCancellationSource(); 501 | const cancelSourceB = api.$module.generateCancellationSource(); 502 | 503 | // send a request 504 | const requestA = api.test({ 505 | query: { 506 | a: 123 507 | }, 508 | }, { 509 | cancelToken: cancelSourceA.token 510 | }); 511 | 512 | const requestB = api.test({ 513 | query: { 514 | b: 321 515 | }, 516 | }, { 517 | cancelToken: cancelSourceB.token 518 | }); 519 | 520 | cancelSourceA.cancel('Canceled by the user'); 521 | 522 | // requestA would be rejected by reason `Canceled by the user` 523 | // requestB ok! 524 | ``` 525 | 526 | --- 527 | ## class `Context` 528 | 529 | ### Read-only members 530 | ### metadata 531 | The copy of the metadata for the current request, that is, modifying the read-only value will not affect the original metadata 532 | 533 | ### metadataKeys 534 | The object keys path array of metadata corresponding to the current request, for example, the request `apiMapper.moduleA.interfaceB` method corresponds to `['moduleA','interfaceB']`. 535 | 536 | Would be useful in the development environment. 537 | 538 | ### method 539 | Request method for the current request 540 | 541 | ### baseURL 542 | The baseURL of the current request 543 | 544 | ### url 545 | The full request url path of the current request, a combination of `baseURL` and parsed `metadata.url` 546 | 547 | ### data 548 | Request parameters for the current request, [see details](#Send-Requests): 549 | - data.query?: object. `URLSearchParams` query parameter for object request 550 | - data.params?: object. The dynamic URL parameters for the object request. Supports `/:id` and `/{id}` definitions 551 | - data.body?: object. request body data 552 | - Add other user-custom fields, which can be accessed in middlewares 553 | 554 | ### response 555 | Response data for the current request 556 | 557 | ### responseError 558 | The current request's response error data, or manually set error data, the existence of this value **does not mean that the request must be failed** 559 | 560 | ### axiosOptions 561 | The `axios` option parameter to be used in the current request will be obtained by combining the second `opt` parameter and `context#setAxiosOptions` passed in the request 562 | 563 | ### instance method 564 | ### setData 565 | Set the request parameters of the incoming request ([View Details](#Send-Requests)), which will overwrite the incoming data to achieve the purpose of overwriting the requested data 566 | 567 | ### setResponse 568 | Set the requested response data, which will overwrite the original response to achieve the purpose of overwriting the successful data of the request 569 | 570 | ### setError 571 | Set the request failure data, whether the request is successful or not, it will return failure 572 | 573 | ### setAxiosOptions 574 | Set options for the `axios` request, but will be **merged** with the `axios` option passed in the request method, and **the priority is not higher than the parameters passed in the request method** 575 | 576 | --- 577 | 578 | # CHANGELOG 579 | [CHANGELOG](./CHANGELOG.md) 580 | 581 | # LICENSE 582 | [MIT LICENSE](./LICENSE) 583 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "es": { 4 | presets: [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | useBuiltIns: false, 9 | modules: false 10 | } 11 | ] 12 | ] 13 | }, 14 | "umd": { 15 | presets: [ 16 | [ 17 | "@babel/preset-env", 18 | { 19 | useBuiltIns: false, 20 | modules: "umd" 21 | } 22 | ] 23 | ], 24 | plugins: [ 25 | "add-module-exports" 26 | ] 27 | }, 28 | "test": { 29 | presets: [ 30 | [ 31 | "@babel/preset-env" 32 | ] 33 | ] 34 | } 35 | }, 36 | plugins: [ 37 | "@babel/plugin-transform-runtime", 38 | "@babel/plugin-proposal-class-properties" 39 | ] 40 | } -------------------------------------------------------------------------------- /dist/axios-api-module.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * axios-api-module.js v3.1.1 3 | * (c) 2020 Calvin Von 4 | * Released under the MIT License. 5 | */ 6 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("axios")):"function"==typeof define&&define.amd?define(["axios"],t):"object"==typeof exports?exports.ApiModule=t(require("axios")):e.ApiModule=t(e.axios)}("undefined"!=typeof self?self:this,(function(e){return r={},t.m=o=[function(e,t){e.exports=function(e){return e&&e.__esModule?e:{default:e}}},function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t){function o(e,t){for(var o=0;o0&&void 0!==arguments[0]?arguments[0]:{};(0,n.default)(this,e),(0,i.default)(this,"options",{}),(0,i.default)(this,"apiMapper",void 0),(0,i.default)(this,"foreRequestHook",void 0),(0,i.default)(this,"postRequestHook",void 0),(0,i.default)(this,"fallbackHook",void 0);var r=o.metadatas,a=void 0===r?{}:r,u=o.module,l=o.console,c=void 0===l||l,f=o.baseConfig,d=void 0===f?{}:f;this.options={axios:s.default.create(d),metadatas:a,module:u,console:c,baseConfig:d},this.apiMapper={},u?Object.keys(a).forEach((function(e){t.apiMapper[e]=t._proxyable(a[e],e)})):this.apiMapper=this._proxyable(a),Object.defineProperty(this.apiMapper,"$module",{configurable:!1,enumerable:!1,writable:!1,value:this})}return(0,a.default)(e,[{key:"useBefore",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c;this.foreRequestHook=e}},{key:"useAfter",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c;this.postRequestHook=e}},{key:"useCatch",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c;this.fallbackHook=e}},{key:"generateCancellationSource",value:function(){return s.default.CancelToken.source()}},{key:"getAxios",value:function(){return this.options.axios}},{key:"getInstance",value:function(){return this.apiMapper}},{key:"foreRequestMiddleWare",value:function t(o,r){var n=this.foreRequestHook||e.foreRequestHook||c;if("function"==typeof n)try{n.call(this,o,r)}catch(t){console.error("[ApiModule] An error occurred in foreRequestMiddleWare: ",t),r()}else console.warn("[ApiModule] foreRequestMiddleWare: ".concat(n," is not a valid foreRequestHook function")),r()}},{key:"postRequestMiddleWare",value:function t(o,r){var n=this.postRequestHook||e.postRequestHook||c;if("function"==typeof n)try{n.call(this,o,r)}catch(t){console.error("[ApiModule] An error occurred in postRequestMiddleWare: ",t),r()}else console.warn("[ApiModule] postRequestMiddleWare: ".concat(n," is not a valid foreRequestHook function")),r()}},{key:"fallbackMiddleWare",value:function(t,o){var r=this,n=function(){if(r.options.console){var e=t.metadata,n=e.method,i=e.url,s="[ApiModule] [".concat(n.toUpperCase()," ").concat(i,"] failed with ").concat(a.message);console.error(new Error(s))}o()},a=t.responseError,i=this.fallbackHook||e.fallbackHook||n;if("function"==typeof i)try{i.call(this,t,o)}catch(a){console.error("[ApiModule] An error occurred in fallbackMiddleWare: ",a),o()}else console.warn("[ApiModule] fallbackMiddleWare: ".concat(i," is not a valid fallbackHook function")),n()}},{key:"_proxyable",value:function(e,t){var o={};for(var r in e)e.hasOwnProperty(r)&&(o[r]=this._proxyApiMetadata(e,r,t));return o}},{key:"_proxyApiMetadata",value:function(e,t,o){var r=this,n=e[t];if("[object Object]"!==Object.prototype.toString.call(n))throw new TypeError("[ApiModule] api metadata [".concat(t,"] is not an object"));var a=new u.default(n,this.options);if(a._metadataKeys=[o,t].filter(Boolean),!a.url||!a.method)throw console.warn("[ApiModule] check your api metadata for [".concat(t,"]: "),n),new Error("[ApiModule] api metadata [".concat(t,"]: 'method' or 'url' value not found"));var i=function(e){var t=e||a.responseError;return!!t&&(t instanceof Error?a.setError(t):"string"==typeof t?a.setError(new Error(t)):a.setError(t),!0)},s=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return a.setError(null).setResponse(null).setAxiosOptions({}).setData(e)._setRequestOptions(t),new Promise((function(e,t){r.foreRequestMiddleWare(a,(function(o){if(i(o))r.fallbackMiddleWare(a,(function(){t(a.responseError)}));else{var n=a.data||{},s=n.query,u=void 0===s?{}:s,l=n.body,c=void 0===l?{}:l,f=Object.assign({},{method:a.method.toLowerCase(),url:a.url,params:u,data:c},a.axiosOptions);r.options.axios(f).then((function(t){a.setResponse(t),r.postRequestMiddleWare(a,(function(t){if(i(t))throw a.responseError;e(a.response)}))})).catch((function(e){i(e),r.fallbackMiddleWare(a,(function(e){i(e),t(a.responseError)}))}))}}))}))};return s.context=a,s}}],[{key:"globalBefore",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c;e.foreRequestHook=t}},{key:"globalAfter",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c;e.postRequestHook=t}},{key:"globalCatch",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c;e.fallbackHook=t}}]),e}();r.default=f,(0,i.default)(f,"foreRequestHook",void 0),(0,i.default)(f,"postRequestHook",void 0),(0,i.default)(f,"fallbackHook",void 0),e.exports=t.default})?r.apply(t,n):r)||(e.exports=a)},function(t,o){t.exports=e},function(e,t,o){var r,n,a;n=[t,o(1),o(2),o(3)],void 0===(a="function"==typeof(r=function(r,n,a,i){"use strict";var s=o(0);function u(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,r)}return o}Object.defineProperty(r,"__esModule",{value:!0}),r.default=void 0,n=s(n),a=s(a),i=s(i);var l=function(){function e(t,o){(0,n.default)(this,e),(0,i.default)(this,"_options",void 0),(0,i.default)(this,"_metadata",void 0),(0,i.default)(this,"_metadataKeys",""),(0,i.default)(this,"_data",null),(0,i.default)(this,"_response",null),(0,i.default)(this,"_responseError",null),(0,i.default)(this,"_reqAxiosOpts",{}),(0,i.default)(this,"_metaAxiosOpts",{}),this._metadata=t,this._options=o}return(0,a.default)(e,[{key:"setData",value:function(e){return this._data=e,this}},{key:"setResponse",value:function(e){return this._response=e,this}},{key:"setError",value:function(e){return this._responseError=e,this}},{key:"_setRequestOptions",value:function(e){return c(e)?this._reqAxiosOpts=e:console.error("[ApiModule] the request parameter, the parameter `".concat(e,"` is not an object")),this}},{key:"setAxiosOptions",value:function(e){return c(e)?this._metaAxiosOpts=e:console.error("[ApiModule] configure axios options error, the parameter `".concat(e,"` is not an object")),this}},{key:"metadata",get:function(){return function(e){for(var t=1;t 0 && arguments[0] !== undefined ? arguments[0] : {}; 36 | 37 | _classCallCheck(this, ApiModule); 38 | 39 | _defineProperty(this, "options", {}); 40 | 41 | _defineProperty(this, "apiMapper", void 0); 42 | 43 | _defineProperty(this, "foreRequestHook", void 0); 44 | 45 | _defineProperty(this, "postRequestHook", void 0); 46 | 47 | _defineProperty(this, "fallbackHook", void 0); 48 | 49 | var _config$metadatas = config.metadatas, 50 | metadatas = _config$metadatas === void 0 ? {} : _config$metadatas, 51 | modularNsp = config.module, 52 | _config$console = config.console, 53 | useConsole = _config$console === void 0 ? true : _config$console, 54 | _config$baseConfig = config.baseConfig, 55 | baseConfig = _config$baseConfig === void 0 ? {} : _config$baseConfig; 56 | this.options = { 57 | axios: axios.create(baseConfig), 58 | metadatas: metadatas, 59 | module: modularNsp, 60 | console: useConsole, 61 | baseConfig: baseConfig 62 | }; 63 | this.apiMapper = {}; 64 | 65 | if (modularNsp) { 66 | // moduled namespace 67 | Object.keys(metadatas).forEach(function (apiName) { 68 | _this.apiMapper[apiName] = _this._proxyable(metadatas[apiName], apiName); 69 | }); 70 | } else { 71 | // single module 72 | this.apiMapper = this._proxyable(metadatas); 73 | } 74 | 75 | Object.defineProperty(this.apiMapper, '$module', { 76 | configurable: false, 77 | enumerable: false, 78 | writable: false, 79 | value: this 80 | }); 81 | } 82 | /** 83 | * Register fore-request middleWare globally (for all instances) 84 | * @param {Function} foreRequestHook(context, next) 85 | */ 86 | 87 | 88 | _createClass(ApiModule, [{ 89 | key: "useBefore", 90 | 91 | /** 92 | * Registe Fore-Request MiddleWare 93 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 94 | */ 95 | value: function useBefore() { 96 | var foreRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 97 | this.foreRequestHook = foreRequestHook; 98 | } 99 | /** 100 | * Registe Post-Request MiddleWare 101 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 102 | */ 103 | 104 | }, { 105 | key: "useAfter", 106 | value: function useAfter() { 107 | var postRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 108 | this.postRequestHook = postRequestHook; 109 | } 110 | /** 111 | * Registe Fallback MiddleWare 112 | * @param {Function} fallbackHook(apiMeta, data = {}, next) 113 | */ 114 | 115 | }, { 116 | key: "useCatch", 117 | value: function useCatch() { 118 | var fallbackHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 119 | this.fallbackHook = fallbackHook; 120 | } 121 | /** 122 | * get axios cancellation source for cancel api 123 | * @returns {CancelTokenSource} 124 | */ 125 | 126 | }, { 127 | key: "generateCancellationSource", 128 | value: function generateCancellationSource() { 129 | return axios.CancelToken.source(); 130 | } 131 | /** 132 | * @returns {Axios} get instance of Axios 133 | */ 134 | 135 | }, { 136 | key: "getAxios", 137 | value: function getAxios() { 138 | return this.options.axios; 139 | } 140 | /** 141 | * @returns {Object} get instance of api metadata mapper 142 | */ 143 | 144 | }, { 145 | key: "getInstance", 146 | value: function getInstance() { 147 | return this.apiMapper; 148 | } 149 | /** 150 | * fore-request middleware 151 | * @param {Context} context 152 | * @param {Function} next 153 | */ 154 | 155 | }, { 156 | key: "foreRequestMiddleWare", 157 | value: function foreRequestMiddleWare(context, next) { 158 | var hookFunction = this.foreRequestHook || ApiModule.foreRequestHook || defaultMiddleware; 159 | 160 | if (typeof hookFunction === 'function') { 161 | try { 162 | hookFunction.call(this, context, next); 163 | } catch (error) { 164 | console.error('[ApiModule] An error occurred in foreRequestMiddleWare: ', error); 165 | next(); 166 | } 167 | } else { 168 | console.warn("[ApiModule] foreRequestMiddleWare: ".concat(hookFunction, " is not a valid foreRequestHook function")); 169 | next(); 170 | } 171 | } 172 | /** 173 | * post-request middleware 174 | * @param {Context} context 175 | * @param {Function} next 176 | */ 177 | 178 | }, { 179 | key: "postRequestMiddleWare", 180 | value: function postRequestMiddleWare(context, next) { 181 | var hookFunction = this.postRequestHook || ApiModule.postRequestHook || defaultMiddleware; 182 | 183 | if (typeof hookFunction === 'function') { 184 | try { 185 | hookFunction.call(this, context, next); 186 | } catch (error) { 187 | console.error('[ApiModule] An error occurred in postRequestMiddleWare: ', error); 188 | next(); 189 | } 190 | } else { 191 | console.warn("[ApiModule] postRequestMiddleWare: ".concat(hookFunction, " is not a valid foreRequestHook function")); 192 | next(); 193 | } 194 | } 195 | /** 196 | * fallback middleWare 197 | * @param {Context} context 198 | * @param {Function} next 199 | */ 200 | 201 | }, { 202 | key: "fallbackMiddleWare", 203 | value: function fallbackMiddleWare(context, next) { 204 | var _this2 = this; 205 | 206 | var defaultErrorHandler = function defaultErrorHandler() { 207 | if (_this2.options.console) { 208 | var _context$metadata = context.metadata, 209 | method = _context$metadata.method, 210 | url = _context$metadata.url; 211 | var msg = "[ApiModule] [".concat(method.toUpperCase(), " ").concat(url, "] failed with ").concat(error.message); 212 | console.error(new Error(msg)); 213 | } 214 | 215 | next(); 216 | }; 217 | 218 | var error = context.responseError; 219 | var hookFunction = this.fallbackHook || ApiModule.fallbackHook || defaultErrorHandler; 220 | 221 | if (typeof hookFunction === 'function') { 222 | try { 223 | hookFunction.call(this, context, next); 224 | } catch (error) { 225 | console.error('[ApiModule] An error occurred in fallbackMiddleWare: ', error); 226 | next(); 227 | } 228 | } else { 229 | console.warn("[ApiModule] fallbackMiddleWare: ".concat(hookFunction, " is not a valid fallbackHook function")); 230 | defaultErrorHandler(); 231 | } 232 | } // tranfer single module api meta info to request 233 | 234 | }, { 235 | key: "_proxyable", 236 | value: function _proxyable(target, apiName) { 237 | var _target = {}; 238 | 239 | for (var key in target) { 240 | if (target.hasOwnProperty(key)) { 241 | _target[key] = this._proxyApiMetadata(target, key, apiName); 242 | } 243 | } 244 | 245 | return _target; 246 | } // map api meta to to request 247 | 248 | }, { 249 | key: "_proxyApiMetadata", 250 | value: function _proxyApiMetadata(target, key, parentKey) { 251 | var _this3 = this; 252 | 253 | var metadata = target[key]; 254 | 255 | if (Object.prototype.toString.call(metadata) !== '[object Object]') { 256 | throw new TypeError("[ApiModule] api metadata [".concat(key, "] is not an object")); 257 | } 258 | 259 | var context = new Context(metadata, this.options); 260 | context._metadataKeys = [parentKey, key].filter(Boolean); 261 | 262 | if (!context.url || !context.method) { 263 | console.warn("[ApiModule] check your api metadata for [".concat(key, "]: "), metadata); 264 | throw new Error("[ApiModule] api metadata [".concat(key, "]: 'method' or 'url' value not found")); 265 | } 266 | /** 267 | * Collect errors and set errors uniformly. Returns if there is an error 268 | * @param {Error|any} err 269 | * @return {Boolean} 270 | */ 271 | 272 | 273 | var handleResponseError = function handleResponseError(err) { 274 | var error = err || context.responseError; 275 | if (!error) return false; 276 | 277 | if (error instanceof Error) { 278 | context.setError(error); 279 | } else if (typeof error === 'string') { 280 | context.setError(new Error(error)); 281 | } else { 282 | context.setError(error); 283 | } 284 | 285 | return true; 286 | }; 287 | 288 | var request = function request(data) { 289 | var opt = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 290 | 291 | context.setError(null).setResponse(null).setAxiosOptions({}).setData(data)._setRequestOptions(opt); 292 | 293 | return new Promise(function (resolve, reject) { 294 | _this3.foreRequestMiddleWare(context, function (err) { 295 | if (handleResponseError(err)) { 296 | _this3.fallbackMiddleWare(context, function () { 297 | reject(context.responseError); 298 | }); 299 | } else { 300 | var _ref = context.data || {}, 301 | _ref$query = _ref.query, 302 | query = _ref$query === void 0 ? {} : _ref$query, 303 | _ref$body = _ref.body, 304 | body = _ref$body === void 0 ? {} : _ref$body; 305 | 306 | var config = Object.assign({}, { 307 | method: context.method.toLowerCase(), 308 | url: context.url, 309 | params: query, 310 | data: body 311 | }, context.axiosOptions); 312 | 313 | _this3.options.axios(config).then(function (res) { 314 | context.setResponse(res); 315 | 316 | _this3.postRequestMiddleWare(context, function (err) { 317 | if (handleResponseError(err)) { 318 | throw context.responseError; 319 | } 320 | 321 | resolve(context.response); 322 | }); 323 | }).catch(function (error) { 324 | handleResponseError(error); 325 | 326 | _this3.fallbackMiddleWare(context, function (err) { 327 | handleResponseError(err); 328 | reject(context.responseError); 329 | }); 330 | }); 331 | } 332 | }); 333 | }); 334 | }; 335 | 336 | request.context = context; 337 | return request; 338 | } 339 | }], [{ 340 | key: "globalBefore", 341 | value: function globalBefore() { 342 | var foreRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 343 | ApiModule.foreRequestHook = foreRequestHook; 344 | } 345 | /** 346 | * Register post-request middleware globally (for all instances) 347 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 348 | */ 349 | 350 | }, { 351 | key: "globalAfter", 352 | value: function globalAfter() { 353 | var postRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 354 | ApiModule.postRequestHook = postRequestHook; 355 | } 356 | /** 357 | * Register fallback MiddleWare Globally (For All Instance) 358 | * @param {Function} fallbackHook(apiMeta, data = {}, next) 359 | */ 360 | 361 | }, { 362 | key: "globalCatch", 363 | value: function globalCatch() { 364 | var fallbackHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 365 | ApiModule.fallbackHook = fallbackHook; 366 | } 367 | }]); 368 | 369 | return ApiModule; 370 | }(); 371 | 372 | _defineProperty(ApiModule, "foreRequestHook", void 0); 373 | 374 | _defineProperty(ApiModule, "postRequestHook", void 0); 375 | 376 | _defineProperty(ApiModule, "fallbackHook", void 0); 377 | 378 | export { ApiModule as default }; -------------------------------------------------------------------------------- /es/context.js: -------------------------------------------------------------------------------- 1 | import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; 2 | import _createClass from "@babel/runtime/helpers/createClass"; 3 | import _defineProperty from "@babel/runtime/helpers/defineProperty"; 4 | 5 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 6 | 7 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 8 | 9 | var Context = /*#__PURE__*/function () { 10 | // Private members 11 | function Context(apiMetadata, options) { 12 | _classCallCheck(this, Context); 13 | 14 | _defineProperty(this, "_options", void 0); 15 | 16 | _defineProperty(this, "_metadata", void 0); 17 | 18 | _defineProperty(this, "_metadataKeys", ''); 19 | 20 | _defineProperty(this, "_data", null); 21 | 22 | _defineProperty(this, "_response", null); 23 | 24 | _defineProperty(this, "_responseError", null); 25 | 26 | _defineProperty(this, "_reqAxiosOpts", {}); 27 | 28 | _defineProperty(this, "_metaAxiosOpts", {}); 29 | 30 | this._metadata = apiMetadata; 31 | this._options = options; 32 | } 33 | /** 34 | * set request data 35 | * @param {any} data 36 | * @return {Context} 37 | */ 38 | 39 | 40 | _createClass(Context, [{ 41 | key: "setData", 42 | value: function setData(data) { 43 | this._data = data; 44 | return this; 45 | } 46 | /** 47 | * set response data 48 | * @param {any} response 49 | * @return {Context} 50 | */ 51 | 52 | }, { 53 | key: "setResponse", 54 | value: function setResponse(response) { 55 | this._response = response; 56 | return this; 57 | } 58 | /** 59 | * set response error 60 | * @param {any} error 61 | * @return {Context} 62 | */ 63 | 64 | }, { 65 | key: "setError", 66 | value: function setError(error) { 67 | this._responseError = error; 68 | return this; 69 | } 70 | /** 71 | * set single axios request options 72 | * @param {AxiosOptions} axiosOptions 73 | * @private 74 | */ 75 | 76 | }, { 77 | key: "_setRequestOptions", 78 | value: function _setRequestOptions(axiosOptions) { 79 | if (isObject(axiosOptions)) { 80 | this._reqAxiosOpts = axiosOptions; 81 | } else { 82 | console.error("[ApiModule] the request parameter, the parameter `".concat(axiosOptions, "` is not an object")); 83 | } 84 | 85 | return this; 86 | } 87 | /** 88 | * set axios options (Designed for invocation in middleware) 89 | * @param {*} axiosOptions 90 | * @public 91 | */ 92 | 93 | }, { 94 | key: "setAxiosOptions", 95 | value: function setAxiosOptions(axiosOptions) { 96 | if (isObject(axiosOptions)) { 97 | this._metaAxiosOpts = axiosOptions; 98 | } else { 99 | console.error("[ApiModule] configure axios options error, the parameter `".concat(axiosOptions, "` is not an object")); 100 | } 101 | 102 | return this; 103 | } 104 | }, { 105 | key: "metadata", 106 | get: function get() { 107 | return _objectSpread({}, this._metadata); 108 | } 109 | }, { 110 | key: "metadataKeys", 111 | get: function get() { 112 | return this._metadataKeys; 113 | } 114 | }, { 115 | key: "method", 116 | get: function get() { 117 | return this._metadata.method; 118 | } 119 | }, { 120 | key: "baseURL", 121 | get: function get() { 122 | return this.axiosOptions.baseURL || ''; 123 | } 124 | }, { 125 | key: "url", 126 | get: function get() { 127 | var url = this._metadata.url; 128 | 129 | var _ref = this.data || {}, 130 | params = _ref.params; 131 | 132 | if (isObject(params)) { 133 | // handle api like /a/:id/b/{param} 134 | return this.baseURL + url.replace(/\B(?::(\w+)|{(\w+)})/g, function () { 135 | return params[(arguments.length <= 1 ? undefined : arguments[1]) || (arguments.length <= 2 ? undefined : arguments[2])]; 136 | }); 137 | } else { 138 | return this.baseURL + url; 139 | } 140 | } 141 | }, { 142 | key: "data", 143 | get: function get() { 144 | return this._data; 145 | } 146 | }, { 147 | key: "response", 148 | get: function get() { 149 | return this._response; 150 | } 151 | }, { 152 | key: "responseError", 153 | get: function get() { 154 | return this._responseError; 155 | } 156 | }, { 157 | key: "axiosOptions", 158 | get: function get() { 159 | // request axios options > metadata axios options 160 | var _reqAxiosOpts = this._reqAxiosOpts, 161 | _metaAxiosOpts = this._metaAxiosOpts; 162 | return Object.assign({}, _metaAxiosOpts, _reqAxiosOpts); 163 | } 164 | }]); 165 | 166 | return Context; 167 | }(); 168 | 169 | export { Context as default }; 170 | 171 | function isObject(target) { 172 | return Object.prototype.toString.call(target) === '[object Object]'; 173 | } -------------------------------------------------------------------------------- /es/index.js: -------------------------------------------------------------------------------- 1 | import ApiModule from "./api-module"; 2 | export default ApiModule; -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosInstance, CancelTokenSource } from "axios"; 2 | 3 | export interface ApiModuleConfig { 4 | metadatas: ApiMetadataMapper | { [namespace: string]: ApiMetadataMapper }; 5 | module?: Boolean; 6 | console?: Boolean; 7 | baseConfig?: AxiosRequestConfig; 8 | } 9 | 10 | /** 11 | * Fore-request middleware 12 | */ 13 | export type ForeRequestHook = ( 14 | context: Context, 15 | next: (error?: any) => null 16 | ) => void; 17 | 18 | /** 19 | * Post-request middleware 20 | */ 21 | export type PostRequestHook = ( 22 | context: Context, 23 | next: (error?: any) => null 24 | ) => void; 25 | 26 | /** 27 | * fallback middleware 28 | */ 29 | export type FallbackHook = ( 30 | context: Context, 31 | next: (error?: any) => null 32 | ) => void; 33 | 34 | export interface ApiMetadataMapper { 35 | [metadataName: string]: ApiMetadata; 36 | } 37 | 38 | export interface ApiMetadata { 39 | method: "get" | "post" | "patch" | "delete" | "put" | "head"; 40 | url: string; 41 | [field: string]: any 42 | } 43 | 44 | export interface TransformedRequestData { 45 | query?: Object; 46 | params?: Object; 47 | body?: Object; 48 | } 49 | 50 | export type TransformedRequest = ( 51 | data?: TransformedRequestData, 52 | opt?: AxiosRequestConfig 53 | ) => Promise; 54 | 55 | export interface TransformedRequestMapper { 56 | [requestName: string]: TransformedRequest; 57 | } 58 | 59 | export interface ApiModuleOptions { 60 | axios: AxiosInstance; 61 | metadatas: ApiMetadataMapper | { [namespace: string]: ApiMetadataMapper }; 62 | module: boolean; 63 | console: boolean; 64 | baseConfig: AxiosRequestConfig; 65 | } 66 | 67 | export declare class ApiModule { 68 | constructor(config: ApiModuleConfig); 69 | 70 | options: ApiModuleOptions; 71 | 72 | /** 73 | * Register fore-request middleWare globally (for all instances) 74 | */ 75 | static globalBefore(foreRequestHook: ForeRequestHook): void; 76 | 77 | /** 78 | * Register post-request middleware globally (for all instances) 79 | */ 80 | static globalAfter(postRequestHook: PostRequestHook): void; 81 | 82 | /** 83 | * Register fallback middleware globally (for all instances) 84 | */ 85 | static globalCatch(fallbackHook: FallbackHook): void; 86 | 87 | 88 | /** 89 | * Registe fore-request middleware 90 | */ 91 | useBefore(foreRequestHook: ForeRequestHook): void; 92 | 93 | /** 94 | * Registe post-request middleware 95 | */ 96 | useAfter(postRequestHook: PostRequestHook): void; 97 | 98 | /** 99 | * Registe fallback-request middleware 100 | */ 101 | useCatch(fallbackHook: FallbackHook): void; 102 | 103 | /** 104 | * Get the instance of api metadatas mapper 105 | */ 106 | getInstance(): 107 | { 108 | $module: ApiModule; 109 | [requestName: string]: TransformedRequest; 110 | } 111 | | 112 | { 113 | $module: ApiModule; 114 | [namespace: string]: TransformedRequestMapper 115 | }; 116 | 117 | /** 118 | * Get the instance of Axios 119 | */ 120 | getAxios(): AxiosInstance; 121 | 122 | /** 123 | * Get axios cancellation source 124 | */ 125 | generateCancellationSource(): CancelTokenSource; 126 | } 127 | 128 | export declare class Context { 129 | /** 130 | * Set request data 131 | */ 132 | setData(data: TransformedRequestData): Context; 133 | 134 | /** 135 | * Set response data 136 | */ 137 | setResponse(response: any): Context; 138 | 139 | /** 140 | * Set request error or response error data 141 | */ 142 | setError(error: string | Error): Context; 143 | 144 | /** 145 | * Set axios request config 146 | */ 147 | setAxiosOptions(options: AxiosRequestConfig): Context; 148 | 149 | readonly metadata: ApiMetadata; 150 | readonly metadataKeys: string[]; 151 | readonly method: string; 152 | readonly baseURL: string; 153 | /** 154 | * Parsed url 155 | */ 156 | readonly url: string; 157 | 158 | /** 159 | * Request data 160 | */ 161 | readonly data: TransformedRequestData; 162 | 163 | /** 164 | * Response data 165 | */ 166 | readonly response: any; 167 | 168 | /** 169 | * Response error 170 | */ 171 | readonly responseError: any; 172 | readonly axiosOptions: AxiosRequestConfig | object; 173 | } 174 | 175 | 176 | export default ApiModule; 177 | -------------------------------------------------------------------------------- /lib/api-module.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(["exports", "@babel/runtime/helpers/classCallCheck", "@babel/runtime/helpers/createClass", "@babel/runtime/helpers/defineProperty", "axios", "./context"], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(exports, require("@babel/runtime/helpers/classCallCheck"), require("@babel/runtime/helpers/createClass"), require("@babel/runtime/helpers/defineProperty"), require("axios"), require("./context")); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, global.classCallCheck, global.createClass, global.defineProperty, global.axios, global.context); 11 | global.apiModule = mod.exports; 12 | } 13 | })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports, _classCallCheck2, _createClass2, _defineProperty2, _axios, _context) { 14 | "use strict"; 15 | 16 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 17 | 18 | Object.defineProperty(_exports, "__esModule", { 19 | value: true 20 | }); 21 | _exports.default = void 0; 22 | _classCallCheck2 = _interopRequireDefault(_classCallCheck2); 23 | _createClass2 = _interopRequireDefault(_createClass2); 24 | _defineProperty2 = _interopRequireDefault(_defineProperty2); 25 | _axios = _interopRequireDefault(_axios); 26 | _context = _interopRequireDefault(_context); 27 | 28 | var defaultMiddleware = function defaultMiddleware(context, next) { 29 | return next(context.responseError); 30 | }; 31 | /** 32 | * Api Module class 33 | * 34 | * @static {Function} foreRequestHook 35 | * @static {Function} postRequestHook 36 | * @static {Function} fallbackHook 37 | * 38 | * @member {Object} options 39 | * @member {Function} foreRequestHook 40 | * @member {Function} postRequestHook 41 | * @member {Function} fallbackHook 42 | * 43 | * @method useBefore(hook) 44 | * @method useAfter(hook) 45 | * @method useCatch(hook) 46 | * @method getAxios() 47 | * @method getInstance() 48 | * @method generateCancellationSource() get axios cancellation source for cancel api 49 | */ 50 | 51 | 52 | var ApiModule = /*#__PURE__*/function () { 53 | function ApiModule() { 54 | var _this = this; 55 | 56 | var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 57 | (0, _classCallCheck2.default)(this, ApiModule); 58 | (0, _defineProperty2.default)(this, "options", {}); 59 | (0, _defineProperty2.default)(this, "apiMapper", void 0); 60 | (0, _defineProperty2.default)(this, "foreRequestHook", void 0); 61 | (0, _defineProperty2.default)(this, "postRequestHook", void 0); 62 | (0, _defineProperty2.default)(this, "fallbackHook", void 0); 63 | var _config$metadatas = config.metadatas, 64 | metadatas = _config$metadatas === void 0 ? {} : _config$metadatas, 65 | modularNsp = config.module, 66 | _config$console = config.console, 67 | useConsole = _config$console === void 0 ? true : _config$console, 68 | _config$baseConfig = config.baseConfig, 69 | baseConfig = _config$baseConfig === void 0 ? {} : _config$baseConfig; 70 | this.options = { 71 | axios: _axios.default.create(baseConfig), 72 | metadatas: metadatas, 73 | module: modularNsp, 74 | console: useConsole, 75 | baseConfig: baseConfig 76 | }; 77 | this.apiMapper = {}; 78 | 79 | if (modularNsp) { 80 | // moduled namespace 81 | Object.keys(metadatas).forEach(function (apiName) { 82 | _this.apiMapper[apiName] = _this._proxyable(metadatas[apiName], apiName); 83 | }); 84 | } else { 85 | // single module 86 | this.apiMapper = this._proxyable(metadatas); 87 | } 88 | 89 | Object.defineProperty(this.apiMapper, '$module', { 90 | configurable: false, 91 | enumerable: false, 92 | writable: false, 93 | value: this 94 | }); 95 | } 96 | /** 97 | * Register fore-request middleWare globally (for all instances) 98 | * @param {Function} foreRequestHook(context, next) 99 | */ 100 | 101 | 102 | (0, _createClass2.default)(ApiModule, [{ 103 | key: "useBefore", 104 | 105 | /** 106 | * Registe Fore-Request MiddleWare 107 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 108 | */ 109 | value: function useBefore() { 110 | var foreRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 111 | this.foreRequestHook = foreRequestHook; 112 | } 113 | /** 114 | * Registe Post-Request MiddleWare 115 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 116 | */ 117 | 118 | }, { 119 | key: "useAfter", 120 | value: function useAfter() { 121 | var postRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 122 | this.postRequestHook = postRequestHook; 123 | } 124 | /** 125 | * Registe Fallback MiddleWare 126 | * @param {Function} fallbackHook(apiMeta, data = {}, next) 127 | */ 128 | 129 | }, { 130 | key: "useCatch", 131 | value: function useCatch() { 132 | var fallbackHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 133 | this.fallbackHook = fallbackHook; 134 | } 135 | /** 136 | * get axios cancellation source for cancel api 137 | * @returns {CancelTokenSource} 138 | */ 139 | 140 | }, { 141 | key: "generateCancellationSource", 142 | value: function generateCancellationSource() { 143 | return _axios.default.CancelToken.source(); 144 | } 145 | /** 146 | * @returns {Axios} get instance of Axios 147 | */ 148 | 149 | }, { 150 | key: "getAxios", 151 | value: function getAxios() { 152 | return this.options.axios; 153 | } 154 | /** 155 | * @returns {Object} get instance of api metadata mapper 156 | */ 157 | 158 | }, { 159 | key: "getInstance", 160 | value: function getInstance() { 161 | return this.apiMapper; 162 | } 163 | /** 164 | * fore-request middleware 165 | * @param {Context} context 166 | * @param {Function} next 167 | */ 168 | 169 | }, { 170 | key: "foreRequestMiddleWare", 171 | value: function foreRequestMiddleWare(context, next) { 172 | var hookFunction = this.foreRequestHook || ApiModule.foreRequestHook || defaultMiddleware; 173 | 174 | if (typeof hookFunction === 'function') { 175 | try { 176 | hookFunction.call(this, context, next); 177 | } catch (error) { 178 | console.error('[ApiModule] An error occurred in foreRequestMiddleWare: ', error); 179 | next(); 180 | } 181 | } else { 182 | console.warn("[ApiModule] foreRequestMiddleWare: ".concat(hookFunction, " is not a valid foreRequestHook function")); 183 | next(); 184 | } 185 | } 186 | /** 187 | * post-request middleware 188 | * @param {Context} context 189 | * @param {Function} next 190 | */ 191 | 192 | }, { 193 | key: "postRequestMiddleWare", 194 | value: function postRequestMiddleWare(context, next) { 195 | var hookFunction = this.postRequestHook || ApiModule.postRequestHook || defaultMiddleware; 196 | 197 | if (typeof hookFunction === 'function') { 198 | try { 199 | hookFunction.call(this, context, next); 200 | } catch (error) { 201 | console.error('[ApiModule] An error occurred in postRequestMiddleWare: ', error); 202 | next(); 203 | } 204 | } else { 205 | console.warn("[ApiModule] postRequestMiddleWare: ".concat(hookFunction, " is not a valid foreRequestHook function")); 206 | next(); 207 | } 208 | } 209 | /** 210 | * fallback middleWare 211 | * @param {Context} context 212 | * @param {Function} next 213 | */ 214 | 215 | }, { 216 | key: "fallbackMiddleWare", 217 | value: function fallbackMiddleWare(context, next) { 218 | var _this2 = this; 219 | 220 | var defaultErrorHandler = function defaultErrorHandler() { 221 | if (_this2.options.console) { 222 | var _context$metadata = context.metadata, 223 | method = _context$metadata.method, 224 | url = _context$metadata.url; 225 | var msg = "[ApiModule] [".concat(method.toUpperCase(), " ").concat(url, "] failed with ").concat(error.message); 226 | console.error(new Error(msg)); 227 | } 228 | 229 | next(); 230 | }; 231 | 232 | var error = context.responseError; 233 | var hookFunction = this.fallbackHook || ApiModule.fallbackHook || defaultErrorHandler; 234 | 235 | if (typeof hookFunction === 'function') { 236 | try { 237 | hookFunction.call(this, context, next); 238 | } catch (error) { 239 | console.error('[ApiModule] An error occurred in fallbackMiddleWare: ', error); 240 | next(); 241 | } 242 | } else { 243 | console.warn("[ApiModule] fallbackMiddleWare: ".concat(hookFunction, " is not a valid fallbackHook function")); 244 | defaultErrorHandler(); 245 | } 246 | } // tranfer single module api meta info to request 247 | 248 | }, { 249 | key: "_proxyable", 250 | value: function _proxyable(target, apiName) { 251 | var _target = {}; 252 | 253 | for (var key in target) { 254 | if (target.hasOwnProperty(key)) { 255 | _target[key] = this._proxyApiMetadata(target, key, apiName); 256 | } 257 | } 258 | 259 | return _target; 260 | } // map api meta to to request 261 | 262 | }, { 263 | key: "_proxyApiMetadata", 264 | value: function _proxyApiMetadata(target, key, parentKey) { 265 | var _this3 = this; 266 | 267 | var metadata = target[key]; 268 | 269 | if (Object.prototype.toString.call(metadata) !== '[object Object]') { 270 | throw new TypeError("[ApiModule] api metadata [".concat(key, "] is not an object")); 271 | } 272 | 273 | var context = new _context.default(metadata, this.options); 274 | context._metadataKeys = [parentKey, key].filter(Boolean); 275 | 276 | if (!context.url || !context.method) { 277 | console.warn("[ApiModule] check your api metadata for [".concat(key, "]: "), metadata); 278 | throw new Error("[ApiModule] api metadata [".concat(key, "]: 'method' or 'url' value not found")); 279 | } 280 | /** 281 | * Collect errors and set errors uniformly. Returns if there is an error 282 | * @param {Error|any} err 283 | * @return {Boolean} 284 | */ 285 | 286 | 287 | var handleResponseError = function handleResponseError(err) { 288 | var error = err || context.responseError; 289 | if (!error) return false; 290 | 291 | if (error instanceof Error) { 292 | context.setError(error); 293 | } else if (typeof error === 'string') { 294 | context.setError(new Error(error)); 295 | } else { 296 | context.setError(error); 297 | } 298 | 299 | return true; 300 | }; 301 | 302 | var request = function request(data) { 303 | var opt = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 304 | 305 | context.setError(null).setResponse(null).setAxiosOptions({}).setData(data)._setRequestOptions(opt); 306 | 307 | return new Promise(function (resolve, reject) { 308 | _this3.foreRequestMiddleWare(context, function (err) { 309 | if (handleResponseError(err)) { 310 | _this3.fallbackMiddleWare(context, function () { 311 | reject(context.responseError); 312 | }); 313 | } else { 314 | var _ref = context.data || {}, 315 | _ref$query = _ref.query, 316 | query = _ref$query === void 0 ? {} : _ref$query, 317 | _ref$body = _ref.body, 318 | body = _ref$body === void 0 ? {} : _ref$body; 319 | 320 | var config = Object.assign({}, { 321 | method: context.method.toLowerCase(), 322 | url: context.url, 323 | params: query, 324 | data: body 325 | }, context.axiosOptions); 326 | 327 | _this3.options.axios(config).then(function (res) { 328 | context.setResponse(res); 329 | 330 | _this3.postRequestMiddleWare(context, function (err) { 331 | if (handleResponseError(err)) { 332 | throw context.responseError; 333 | } 334 | 335 | resolve(context.response); 336 | }); 337 | }).catch(function (error) { 338 | handleResponseError(error); 339 | 340 | _this3.fallbackMiddleWare(context, function (err) { 341 | handleResponseError(err); 342 | reject(context.responseError); 343 | }); 344 | }); 345 | } 346 | }); 347 | }); 348 | }; 349 | 350 | request.context = context; 351 | return request; 352 | } 353 | }], [{ 354 | key: "globalBefore", 355 | value: function globalBefore() { 356 | var foreRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 357 | ApiModule.foreRequestHook = foreRequestHook; 358 | } 359 | /** 360 | * Register post-request middleware globally (for all instances) 361 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 362 | */ 363 | 364 | }, { 365 | key: "globalAfter", 366 | value: function globalAfter() { 367 | var postRequestHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 368 | ApiModule.postRequestHook = postRequestHook; 369 | } 370 | /** 371 | * Register fallback MiddleWare Globally (For All Instance) 372 | * @param {Function} fallbackHook(apiMeta, data = {}, next) 373 | */ 374 | 375 | }, { 376 | key: "globalCatch", 377 | value: function globalCatch() { 378 | var fallbackHook = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultMiddleware; 379 | ApiModule.fallbackHook = fallbackHook; 380 | } 381 | }]); 382 | return ApiModule; 383 | }(); 384 | 385 | _exports.default = ApiModule; 386 | (0, _defineProperty2.default)(ApiModule, "foreRequestHook", void 0); 387 | (0, _defineProperty2.default)(ApiModule, "postRequestHook", void 0); 388 | (0, _defineProperty2.default)(ApiModule, "fallbackHook", void 0); 389 | module.exports = exports.default; 390 | }); -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(["exports", "@babel/runtime/helpers/classCallCheck", "@babel/runtime/helpers/createClass", "@babel/runtime/helpers/defineProperty"], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(exports, require("@babel/runtime/helpers/classCallCheck"), require("@babel/runtime/helpers/createClass"), require("@babel/runtime/helpers/defineProperty")); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, global.classCallCheck, global.createClass, global.defineProperty); 11 | global.context = mod.exports; 12 | } 13 | })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports, _classCallCheck2, _createClass2, _defineProperty2) { 14 | "use strict"; 15 | 16 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 17 | 18 | Object.defineProperty(_exports, "__esModule", { 19 | value: true 20 | }); 21 | _exports.default = void 0; 22 | _classCallCheck2 = _interopRequireDefault(_classCallCheck2); 23 | _createClass2 = _interopRequireDefault(_createClass2); 24 | _defineProperty2 = _interopRequireDefault(_defineProperty2); 25 | 26 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 27 | 28 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 29 | 30 | var Context = /*#__PURE__*/function () { 31 | // Private members 32 | function Context(apiMetadata, options) { 33 | (0, _classCallCheck2.default)(this, Context); 34 | (0, _defineProperty2.default)(this, "_options", void 0); 35 | (0, _defineProperty2.default)(this, "_metadata", void 0); 36 | (0, _defineProperty2.default)(this, "_metadataKeys", ''); 37 | (0, _defineProperty2.default)(this, "_data", null); 38 | (0, _defineProperty2.default)(this, "_response", null); 39 | (0, _defineProperty2.default)(this, "_responseError", null); 40 | (0, _defineProperty2.default)(this, "_reqAxiosOpts", {}); 41 | (0, _defineProperty2.default)(this, "_metaAxiosOpts", {}); 42 | this._metadata = apiMetadata; 43 | this._options = options; 44 | } 45 | /** 46 | * set request data 47 | * @param {any} data 48 | * @return {Context} 49 | */ 50 | 51 | 52 | (0, _createClass2.default)(Context, [{ 53 | key: "setData", 54 | value: function setData(data) { 55 | this._data = data; 56 | return this; 57 | } 58 | /** 59 | * set response data 60 | * @param {any} response 61 | * @return {Context} 62 | */ 63 | 64 | }, { 65 | key: "setResponse", 66 | value: function setResponse(response) { 67 | this._response = response; 68 | return this; 69 | } 70 | /** 71 | * set response error 72 | * @param {any} error 73 | * @return {Context} 74 | */ 75 | 76 | }, { 77 | key: "setError", 78 | value: function setError(error) { 79 | this._responseError = error; 80 | return this; 81 | } 82 | /** 83 | * set single axios request options 84 | * @param {AxiosOptions} axiosOptions 85 | * @private 86 | */ 87 | 88 | }, { 89 | key: "_setRequestOptions", 90 | value: function _setRequestOptions(axiosOptions) { 91 | if (isObject(axiosOptions)) { 92 | this._reqAxiosOpts = axiosOptions; 93 | } else { 94 | console.error("[ApiModule] the request parameter, the parameter `".concat(axiosOptions, "` is not an object")); 95 | } 96 | 97 | return this; 98 | } 99 | /** 100 | * set axios options (Designed for invocation in middleware) 101 | * @param {*} axiosOptions 102 | * @public 103 | */ 104 | 105 | }, { 106 | key: "setAxiosOptions", 107 | value: function setAxiosOptions(axiosOptions) { 108 | if (isObject(axiosOptions)) { 109 | this._metaAxiosOpts = axiosOptions; 110 | } else { 111 | console.error("[ApiModule] configure axios options error, the parameter `".concat(axiosOptions, "` is not an object")); 112 | } 113 | 114 | return this; 115 | } 116 | }, { 117 | key: "metadata", 118 | get: function get() { 119 | return _objectSpread({}, this._metadata); 120 | } 121 | }, { 122 | key: "metadataKeys", 123 | get: function get() { 124 | return this._metadataKeys; 125 | } 126 | }, { 127 | key: "method", 128 | get: function get() { 129 | return this._metadata.method; 130 | } 131 | }, { 132 | key: "baseURL", 133 | get: function get() { 134 | return this.axiosOptions.baseURL || ''; 135 | } 136 | }, { 137 | key: "url", 138 | get: function get() { 139 | var url = this._metadata.url; 140 | 141 | var _ref = this.data || {}, 142 | params = _ref.params; 143 | 144 | if (isObject(params)) { 145 | // handle api like /a/:id/b/{param} 146 | return this.baseURL + url.replace(/\B(?::(\w+)|{(\w+)})/g, function () { 147 | return params[(arguments.length <= 1 ? undefined : arguments[1]) || (arguments.length <= 2 ? undefined : arguments[2])]; 148 | }); 149 | } else { 150 | return this.baseURL + url; 151 | } 152 | } 153 | }, { 154 | key: "data", 155 | get: function get() { 156 | return this._data; 157 | } 158 | }, { 159 | key: "response", 160 | get: function get() { 161 | return this._response; 162 | } 163 | }, { 164 | key: "responseError", 165 | get: function get() { 166 | return this._responseError; 167 | } 168 | }, { 169 | key: "axiosOptions", 170 | get: function get() { 171 | // request axios options > metadata axios options 172 | var _reqAxiosOpts = this._reqAxiosOpts, 173 | _metaAxiosOpts = this._metaAxiosOpts; 174 | return Object.assign({}, _metaAxiosOpts, _reqAxiosOpts); 175 | } 176 | }]); 177 | return Context; 178 | }(); 179 | 180 | _exports.default = Context; 181 | 182 | function isObject(target) { 183 | return Object.prototype.toString.call(target) === '[object Object]'; 184 | } 185 | 186 | module.exports = exports.default; 187 | }); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(["exports", "./api-module"], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(exports, require("./api-module")); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports, global.apiModule); 11 | global.index = mod.exports; 12 | } 13 | })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports, _apiModule) { 14 | "use strict"; 15 | 16 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 17 | 18 | Object.defineProperty(_exports, "__esModule", { 19 | value: true 20 | }); 21 | _exports.default = void 0; 22 | _apiModule = _interopRequireDefault(_apiModule); 23 | var _default = _apiModule.default; 24 | _exports.default = _default; 25 | module.exports = exports.default; 26 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@calvin_von/axios-api-module", 3 | "version": "3.1.1", 4 | "description": "Encapsulated api module based on axios", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "typings": "./index.d.ts", 8 | "scripts": { 9 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 10 | "build:lib": "cross-env BABEL_ENV=umd babel src --out-dir lib", 11 | "build:dist": "cross-env BABEL_ENV=umd webpack --config webpack.config.js", 12 | "test:unit": "cross-env BABEL_ENV=test mocha --require @babel/register", 13 | "coverage:unit": "cross-env BABEL_ENV=test nyc mocha --require @babel/register", 14 | "prepublish": "npm run build:es && npm run build:lib && npm run build:dist && npm run coverage:unit" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/CalvinVon/axios-api-module.git" 19 | }, 20 | "keywords": [ 21 | "axios", 22 | "api", 23 | "module" 24 | ], 25 | "author": "calvin_von", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/CalvinVon/axios-api-module/issues" 29 | }, 30 | "homepage": "https://github.com/CalvinVon/axios-api-module#readme", 31 | "devDependencies": { 32 | "@babel/cli": "^7.10.1", 33 | "@babel/core": "^7.10.2", 34 | "@babel/plugin-proposal-class-properties": "^7.10.1", 35 | "@babel/plugin-transform-runtime": "^7.10.1", 36 | "@babel/preset-env": "^7.10.2", 37 | "@babel/register": "^7.10.1", 38 | "@babel/runtime": "^7.10.2", 39 | "axios": "^0.19.2", 40 | "babel-loader": "^8.1.0", 41 | "babel-plugin-add-module-exports": "^1.0.2", 42 | "chai": "^4.2.0", 43 | "cross-env": "^7.0.2", 44 | "istanbul": "^0.4.5", 45 | "mocha": "^7.2.0", 46 | "nyc": "^15.1.0", 47 | "uglifyjs-webpack-plugin": "^2.2.0", 48 | "webpack": "^4.43.0", 49 | "webpack-cli": "^3.3.11" 50 | }, 51 | "nyc": { 52 | "check-coverage": true, 53 | "per-file": true, 54 | "statements": 95, 55 | "lines": 95, 56 | "functions": 95, 57 | "branches": 95, 58 | "include": [ 59 | "src/*.js" 60 | ], 61 | "exclude": [ 62 | "node_modules/", 63 | "test/", 64 | "lib/", 65 | "es/", 66 | "dist/", 67 | "example/" 68 | ], 69 | "reporter": [ 70 | "lcov", 71 | "text", 72 | "text-summary" 73 | ], 74 | "extension": [ 75 | ".js" 76 | ], 77 | "cache": false 78 | }, 79 | "dependencies": {} 80 | } 81 | -------------------------------------------------------------------------------- /src/api-module.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Context from './context'; 3 | 4 | const defaultMiddleware = (context, next) => next(context.responseError); 5 | 6 | /** 7 | * Api Module class 8 | * 9 | * @static {Function} foreRequestHook 10 | * @static {Function} postRequestHook 11 | * @static {Function} fallbackHook 12 | * 13 | * @member {Object} options 14 | * @member {Function} foreRequestHook 15 | * @member {Function} postRequestHook 16 | * @member {Function} fallbackHook 17 | * 18 | * @method useBefore(hook) 19 | * @method useAfter(hook) 20 | * @method useCatch(hook) 21 | * @method getAxios() 22 | * @method getInstance() 23 | * @method generateCancellationSource() get axios cancellation source for cancel api 24 | */ 25 | export default class ApiModule { 26 | 27 | static foreRequestHook; 28 | static postRequestHook; 29 | static fallbackHook; 30 | 31 | options = {}; 32 | apiMapper; 33 | foreRequestHook; 34 | postRequestHook; 35 | fallbackHook; 36 | 37 | 38 | constructor(config = {}) { 39 | const { 40 | metadatas = {}, 41 | module: modularNsp, 42 | console: useConsole = true, 43 | baseConfig = {}, 44 | } = config; 45 | 46 | this.options = { 47 | axios: axios.create(baseConfig), 48 | metadatas, 49 | module: modularNsp, 50 | console: useConsole, 51 | baseConfig 52 | }; 53 | 54 | this.apiMapper = {}; 55 | 56 | if (modularNsp) { 57 | // moduled namespace 58 | Object.keys(metadatas).forEach(apiName => { 59 | this.apiMapper[apiName] = this._proxyable(metadatas[apiName], apiName); 60 | }); 61 | } 62 | else { 63 | // single module 64 | this.apiMapper = this._proxyable(metadatas); 65 | } 66 | 67 | Object.defineProperty(this.apiMapper, '$module', { 68 | configurable: false, 69 | enumerable: false, 70 | writable: false, 71 | value: this 72 | }); 73 | } 74 | 75 | 76 | /** 77 | * Register fore-request middleWare globally (for all instances) 78 | * @param {Function} foreRequestHook(context, next) 79 | */ 80 | static globalBefore(foreRequestHook = defaultMiddleware) { 81 | ApiModule.foreRequestHook = foreRequestHook; 82 | } 83 | 84 | /** 85 | * Register post-request middleware globally (for all instances) 86 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 87 | */ 88 | static globalAfter(postRequestHook = defaultMiddleware) { 89 | ApiModule.postRequestHook = postRequestHook; 90 | } 91 | 92 | /** 93 | * Register fallback MiddleWare Globally (For All Instance) 94 | * @param {Function} fallbackHook(apiMeta, data = {}, next) 95 | */ 96 | static globalCatch(fallbackHook = defaultMiddleware) { 97 | ApiModule.fallbackHook = fallbackHook; 98 | } 99 | 100 | /** 101 | * Registe Fore-Request MiddleWare 102 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 103 | */ 104 | useBefore(foreRequestHook = defaultMiddleware) { 105 | this.foreRequestHook = foreRequestHook; 106 | } 107 | 108 | /** 109 | * Registe Post-Request MiddleWare 110 | * @param {Function} foreRequestHook(apiMeta, data = {}, next) 111 | */ 112 | useAfter(postRequestHook = defaultMiddleware) { 113 | this.postRequestHook = postRequestHook; 114 | } 115 | 116 | /** 117 | * Registe Fallback MiddleWare 118 | * @param {Function} fallbackHook(apiMeta, data = {}, next) 119 | */ 120 | useCatch(fallbackHook = defaultMiddleware) { 121 | this.fallbackHook = fallbackHook; 122 | } 123 | 124 | /** 125 | * get axios cancellation source for cancel api 126 | * @returns {CancelTokenSource} 127 | */ 128 | generateCancellationSource() { 129 | return axios.CancelToken.source(); 130 | } 131 | 132 | 133 | /** 134 | * @returns {Axios} get instance of Axios 135 | */ 136 | getAxios() { 137 | return this.options.axios; 138 | } 139 | 140 | /** 141 | * @returns {Object} get instance of api metadata mapper 142 | */ 143 | getInstance() { 144 | return this.apiMapper; 145 | } 146 | 147 | 148 | /** 149 | * fore-request middleware 150 | * @param {Context} context 151 | * @param {Function} next 152 | */ 153 | foreRequestMiddleWare(context, next) { 154 | const hookFunction = this.foreRequestHook || ApiModule.foreRequestHook || defaultMiddleware; 155 | if (typeof hookFunction === 'function') { 156 | try { 157 | hookFunction.call(this, context, next); 158 | } catch (error) { 159 | console.error('[ApiModule] An error occurred in foreRequestMiddleWare: ', error); 160 | next(); 161 | } 162 | } 163 | else { 164 | console.warn(`[ApiModule] foreRequestMiddleWare: ${hookFunction} is not a valid foreRequestHook function`); 165 | next(); 166 | } 167 | } 168 | 169 | /** 170 | * post-request middleware 171 | * @param {Context} context 172 | * @param {Function} next 173 | */ 174 | postRequestMiddleWare(context, next) { 175 | const hookFunction = this.postRequestHook || ApiModule.postRequestHook || defaultMiddleware; 176 | if (typeof hookFunction === 'function') { 177 | try { 178 | hookFunction.call(this, context, next); 179 | } catch (error) { 180 | console.error('[ApiModule] An error occurred in postRequestMiddleWare: ', error); 181 | next(); 182 | } 183 | } 184 | else { 185 | console.warn(`[ApiModule] postRequestMiddleWare: ${hookFunction} is not a valid foreRequestHook function`); 186 | next(); 187 | } 188 | } 189 | 190 | /** 191 | * fallback middleWare 192 | * @param {Context} context 193 | * @param {Function} next 194 | */ 195 | fallbackMiddleWare(context, next) { 196 | const defaultErrorHandler = () => { 197 | if (this.options.console) { 198 | const { 199 | method, 200 | url 201 | } = context.metadata; 202 | const msg = `[ApiModule] [${method.toUpperCase()} ${url}] failed with ${error.message}`; 203 | console.error(new Error(msg)); 204 | } 205 | 206 | next(); 207 | }; 208 | const error = context.responseError; 209 | const hookFunction = this.fallbackHook || ApiModule.fallbackHook || defaultErrorHandler; 210 | 211 | if (typeof hookFunction === 'function') { 212 | try { 213 | hookFunction.call(this, context, next); 214 | } catch (error) { 215 | console.error('[ApiModule] An error occurred in fallbackMiddleWare: ', error); 216 | next(); 217 | } 218 | } 219 | else { 220 | console.warn(`[ApiModule] fallbackMiddleWare: ${hookFunction} is not a valid fallbackHook function`); 221 | defaultErrorHandler(); 222 | } 223 | } 224 | 225 | // tranfer single module api meta info to request 226 | _proxyable(target, apiName) { 227 | const _target = {}; 228 | for (const key in target) { 229 | if (target.hasOwnProperty(key)) { 230 | _target[key] = this._proxyApiMetadata(target, key, apiName); 231 | } 232 | } 233 | return _target; 234 | } 235 | 236 | // map api meta to to request 237 | _proxyApiMetadata(target, key, parentKey) { 238 | const metadata = target[key]; 239 | if (Object.prototype.toString.call(metadata) !== '[object Object]') { 240 | throw new TypeError(`[ApiModule] api metadata [${key}] is not an object`); 241 | } 242 | 243 | const context = new Context(metadata, this.options); 244 | context._metadataKeys = [parentKey, key].filter(Boolean); 245 | 246 | if (!context.url || !context.method) { 247 | console.warn(`[ApiModule] check your api metadata for [${key}]: `, metadata); 248 | throw new Error(`[ApiModule] api metadata [${key}]: 'method' or 'url' value not found`); 249 | } 250 | 251 | /** 252 | * Collect errors and set errors uniformly. Returns if there is an error 253 | * @param {Error|any} err 254 | * @return {Boolean} 255 | */ 256 | const handleResponseError = err => { 257 | const error = err || context.responseError; 258 | if (!error) return false; 259 | 260 | if (error instanceof Error) { 261 | context.setError(error); 262 | } 263 | else if (typeof error === 'string') { 264 | context.setError(new Error(error)); 265 | } 266 | else { 267 | context.setError(error); 268 | } 269 | return true; 270 | }; 271 | 272 | 273 | const request = (data, opt = {}) => { 274 | context 275 | .setError(null) 276 | .setResponse(null) 277 | .setAxiosOptions({}) 278 | 279 | .setData(data) 280 | ._setRequestOptions(opt); 281 | 282 | return new Promise((resolve, reject) => { 283 | this.foreRequestMiddleWare(context, err => { 284 | if (handleResponseError(err)) { 285 | this.fallbackMiddleWare(context, () => { 286 | reject(context.responseError); 287 | }); 288 | } 289 | else { 290 | const { 291 | query = {}, 292 | body = {} 293 | } = context.data || {}; 294 | 295 | const config = Object.assign( 296 | {}, 297 | { 298 | method: context.method.toLowerCase(), 299 | url: context.url, 300 | params: query, 301 | data: body, 302 | }, 303 | context.axiosOptions 304 | ); 305 | 306 | this.options.axios(config) 307 | .then(res => { 308 | context.setResponse(res); 309 | this.postRequestMiddleWare(context, err => { 310 | if (handleResponseError(err)) { 311 | throw context.responseError; 312 | } 313 | resolve(context.response); 314 | }); 315 | }) 316 | .catch(error => { 317 | handleResponseError(error); 318 | this.fallbackMiddleWare(context, err => { 319 | handleResponseError(err); 320 | reject(context.responseError); 321 | }); 322 | }); 323 | } 324 | }) 325 | }) 326 | }; 327 | 328 | request.context = context; 329 | return request; 330 | } 331 | } -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | export default class Context { 2 | 3 | // Private members 4 | _options; 5 | _metadata; 6 | _metadataKeys = ''; 7 | _data = null; 8 | _response = null; 9 | _responseError = null; 10 | _reqAxiosOpts = {}; 11 | _metaAxiosOpts = {}; 12 | 13 | constructor(apiMetadata, options) { 14 | this._metadata = apiMetadata; 15 | this._options = options; 16 | } 17 | 18 | 19 | /** 20 | * set request data 21 | * @param {any} data 22 | * @return {Context} 23 | */ 24 | setData(data) { 25 | this._data = data; 26 | return this; 27 | } 28 | 29 | /** 30 | * set response data 31 | * @param {any} response 32 | * @return {Context} 33 | */ 34 | setResponse(response) { 35 | this._response = response; 36 | return this; 37 | } 38 | 39 | /** 40 | * set response error 41 | * @param {any} error 42 | * @return {Context} 43 | */ 44 | setError(error) { 45 | this._responseError = error; 46 | return this; 47 | } 48 | 49 | /** 50 | * set single axios request options 51 | * @param {AxiosOptions} axiosOptions 52 | * @private 53 | */ 54 | _setRequestOptions(axiosOptions) { 55 | if (isObject(axiosOptions)) { 56 | this._reqAxiosOpts = axiosOptions; 57 | } 58 | else { 59 | console.error(`[ApiModule] the request parameter, the parameter \`${axiosOptions}\` is not an object`); 60 | } 61 | return this; 62 | } 63 | 64 | 65 | /** 66 | * set axios options (Designed for invocation in middleware) 67 | * @param {*} axiosOptions 68 | * @public 69 | */ 70 | setAxiosOptions(axiosOptions) { 71 | if (isObject(axiosOptions)) { 72 | this._metaAxiosOpts = axiosOptions; 73 | } 74 | else { 75 | console.error(`[ApiModule] configure axios options error, the parameter \`${axiosOptions}\` is not an object`); 76 | } 77 | return this; 78 | } 79 | 80 | get metadata() { 81 | return { ...this._metadata }; 82 | } 83 | 84 | get metadataKeys() { 85 | return this._metadataKeys; 86 | } 87 | 88 | get method() { 89 | return this._metadata.method; 90 | } 91 | 92 | get baseURL() { 93 | return this.axiosOptions.baseURL || ''; 94 | } 95 | 96 | get url() { 97 | const { url } = this._metadata; 98 | const { params } = this.data || {}; 99 | 100 | if (isObject(params)) { 101 | // handle api like /a/:id/b/{param} 102 | return this.baseURL + url 103 | .replace(/\B(?::(\w+)|{(\w+)})/g, (...args) => { 104 | return params[args[1] || args[2]]; 105 | }); 106 | } 107 | else { 108 | return this.baseURL + url; 109 | } 110 | } 111 | 112 | get data() { 113 | return this._data; 114 | } 115 | 116 | get response() { 117 | return this._response; 118 | } 119 | 120 | get responseError() { 121 | return this._responseError; 122 | } 123 | 124 | get axiosOptions() { 125 | // request axios options > metadata axios options 126 | const { _reqAxiosOpts, _metaAxiosOpts } = this; 127 | return Object.assign({}, _metaAxiosOpts, _reqAxiosOpts); 128 | } 129 | } 130 | 131 | 132 | function isObject(target) { 133 | return Object.prototype.toString.call(target) === '[object Object]'; 134 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ApiModule from "./api-module"; 2 | 3 | export default ApiModule; -------------------------------------------------------------------------------- /test/constructor.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import ApiModule from '../src'; 3 | 4 | describe('baseConfig', () => { 5 | 6 | it('options of instance contain original config', () => { 7 | const config = { 8 | baseConfig: { 9 | baseURL: 'http://api.yourdomain.com', 10 | headers: { 11 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 12 | 'X-test-header': 'api-module' 13 | }, 14 | withCredentials: true, 15 | timeout: 60000 16 | }, 17 | console: false, 18 | module: false, 19 | metadatas: {} 20 | }; 21 | const apiModule = new ApiModule(config); 22 | expect(apiModule.options).to.be.contain(config); 23 | }); 24 | 25 | it('api mapper contains \'$module\' property that refs to ApiModule instance', () => { 26 | const apiModule = new ApiModule(); 27 | const apiMapper = apiModule.getInstance(); 28 | expect(apiMapper).to.has.ownProperty('$module'); 29 | expect(apiMapper['$module']).to.be.equal(apiModule); 30 | }); 31 | 32 | it('no modular namespace api metas', () => { 33 | const apiModule = new ApiModule({ 34 | module: false, 35 | metadatas: { 36 | test: { 37 | url: '/api/test', 38 | method: 'get' 39 | } 40 | } 41 | }); 42 | 43 | const apiMapper = apiModule.getInstance(); 44 | expect(apiMapper).to.have.all.keys('test'); 45 | }); 46 | 47 | it('multiple modular namespaces api metas', () => { 48 | const apiModule = new ApiModule({ 49 | module: true, 50 | metadatas: { 51 | main: { 52 | test: { 53 | url: '/api/test', 54 | method: 'get' 55 | } 56 | }, 57 | sub: { 58 | subTest: { 59 | url: '/sub/test', 60 | method: 'get' 61 | } 62 | } 63 | } 64 | }); 65 | 66 | const apiMapper = apiModule.getInstance(); 67 | expect(apiMapper).to.have.all.keys('main', 'sub').but.not.have.all.keys('test', 'subTest'); 68 | }); 69 | 70 | it('metadatas passing empty meta value should throw error', () => { 71 | const produceEmptyMeta = () => { 72 | new ApiModule({ 73 | module: false, 74 | metadatas: { 75 | test: {}, 76 | } 77 | }); 78 | }; 79 | const produceNullMeta = () => { 80 | new ApiModule({ 81 | module: false, 82 | metadatas: { 83 | other: null, 84 | } 85 | }); 86 | }; 87 | const produceUndefinedMeta = () => { 88 | new ApiModule({ 89 | module: false, 90 | metadatas: { 91 | another: undefined 92 | } 93 | }); 94 | }; 95 | 96 | expect(produceEmptyMeta).to.throw(Error, /api metadata \[(\w+)\]: 'method' or 'url' value not found/i); 97 | expect(produceNullMeta).to.throw(TypeError, /api metadata \[(\w+)\] is not an object/i); 98 | expect(produceUndefinedMeta).to.throw(TypeError, /api metadata \[(\w+)\] is not an object/i); 99 | }); 100 | 101 | it('one instance will return same instance of axios', () => { 102 | const apiModule = new ApiModule({ 103 | api: {} 104 | }); 105 | 106 | expect(apiModule.getAxios()).to.be.equal(apiModule.getAxios()); 107 | }); 108 | 109 | }); -------------------------------------------------------------------------------- /test/context.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import ApiModule from '../src'; 3 | 4 | 5 | 6 | describe('context.metadataKeys', () => { 7 | it('single module', () => { 8 | const request = new ApiModule({ 9 | metadatas: { 10 | interfaceA: { 11 | name: 'interfaceA', 12 | method: 'GET', 13 | url: '/test' 14 | } 15 | }, 16 | module: false 17 | }).getInstance().interfaceA; 18 | 19 | expect(request.context.metadataKeys).to.be.deep.equal(['interfaceA']); 20 | }); 21 | 22 | it('multiple module', () => { 23 | const request = new ApiModule({ 24 | metadatas: { 25 | modA: { 26 | interface: { 27 | name: 'modA', 28 | method: 'GET', 29 | url: '/test' 30 | } 31 | } 32 | }, 33 | module: true 34 | }).getInstance().modA.interface; 35 | 36 | expect(request.context.metadataKeys).to.be.deep.equal(['modA', 'interface']); 37 | }); 38 | }); 39 | 40 | describe('context should be reset in second calls', () => { 41 | 42 | it('data, error, response', () => { 43 | const apiMod = new ApiModule({ 44 | metadatas: { 45 | interfaceA: { 46 | name: 'interfaceA', 47 | method: 'GET', 48 | url: '/test' 49 | } 50 | }, 51 | module: false 52 | }); 53 | apiMod.useBefore((context, next) => { 54 | // set error on purpose 55 | context.setError('I am an Error occurred before real request'); 56 | context.setResponse({ data: 123 }); 57 | context.setAxiosOptions({ headers: { 'x-app': 1 } }); 58 | next(); 59 | }); 60 | const request = apiMod.getInstance().interfaceA; 61 | 62 | // first request 63 | request(); 64 | 65 | let collector = {}; 66 | apiMod.useBefore((context, next) => { 67 | collector = { 68 | data: context.data, 69 | response: context.response, 70 | responseError: context.responseError, 71 | axiosOptions: context.axiosOptions, 72 | } 73 | next(); 74 | }); 75 | 76 | const secondData = {}; 77 | request(secondData); 78 | 79 | expect(collector.data).to.be.equal(secondData); 80 | expect(collector.response).to.be.equal(null); 81 | expect(collector.responseError).to.be.equal(null); 82 | expect(collector.axiosOptions).to.be.deep.equal({}); 83 | 84 | }); 85 | }); -------------------------------------------------------------------------------- /test/method.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import utils from './utils'; 3 | import ApiModule from '../src'; 4 | 5 | 6 | function cleanHooks() { 7 | ApiModule.foreRequestHook = null; 8 | ApiModule.postRequestHook = null; 9 | ApiModule.fallbackHook = null; 10 | } 11 | 12 | 13 | describe('useBefore methods', () => { 14 | let server; 15 | let testMetadata, 16 | testData, 17 | apiModule, 18 | apiMapper; 19 | 20 | before('Setup server', done => { 21 | server = utils.createServer(7788); 22 | server.on('listening', () => { 23 | done(); 24 | }); 25 | }); 26 | 27 | after('Stop and clean server', done => { 28 | server.on('close', () => { 29 | server = null; 30 | done(); 31 | }); 32 | 33 | cleanHooks(); 34 | server.close(); 35 | }); 36 | 37 | beforeEach(() => { 38 | testMetadata = { 39 | url: '/api/test', 40 | method: 'get', 41 | name: 'test middleware methods' 42 | }; 43 | testData = { 44 | query: { 45 | a: 1, 46 | b: 2 47 | }, 48 | body: { 49 | c: 11, 50 | d: 22 51 | } 52 | }; 53 | apiModule = new ApiModule({ 54 | baseConfig: { 55 | baseURL: 'http://localhost:7788' 56 | }, 57 | module: false, 58 | metadatas: { 59 | test: testMetadata 60 | } 61 | }); 62 | 63 | apiMapper = apiModule.getInstance(); 64 | }); 65 | 66 | afterEach(() => { 67 | cleanHooks(); 68 | }); 69 | 70 | 71 | it('static method globalBefore', async () => { 72 | ApiModule.globalBefore((context, next) => { 73 | expect(context.metadata).to.be.not.equal(testMetadata); 74 | expect(context.metadata).to.be.eql(testMetadata); 75 | expect(context.data).to.be.equal(testData); 76 | next(); 77 | }); 78 | 79 | await apiMapper.test(testData); 80 | }); 81 | 82 | it('instance method useBefore', async () => { 83 | apiModule.useBefore((context, next) => { 84 | expect(context.metadata).to.be.not.equal(testMetadata); 85 | expect(context.metadata).to.be.eql(testMetadata); 86 | expect(context.data).to.be.equal(testData); 87 | next(); 88 | }); 89 | 90 | await apiMapper.test(testData); 91 | }); 92 | 93 | it('instance method would override static method', async () => { 94 | apiModule.useBefore((context, next) => { 95 | expect(context).to.be.ok; 96 | next(); 97 | }); 98 | ApiModule.globalBefore((context, next) => { 99 | next(); 100 | }); 101 | 102 | await apiMapper.test(testData); 103 | }); 104 | 105 | 106 | it('static before method passing `null` would not throw an error', async () => { 107 | ApiModule.globalBefore(null); 108 | await apiMapper.test(testData); 109 | }); 110 | 111 | it('static before method passing `123` would not throw an error', async () => { 112 | ApiModule.globalBefore(123); 113 | await apiMapper.test(testData); 114 | }); 115 | 116 | it('static before method passing undefined would not throw an error', async () => { 117 | ApiModule.globalBefore(); 118 | await apiMapper.test(testData); 119 | }); 120 | 121 | it('instance before method passing undefined would not throw an error', async () => { 122 | apiModule.useBefore(); 123 | await apiMapper.test(testData); 124 | }); 125 | 126 | it('passed some error then reject the request', (done) => { 127 | apiModule.useBefore((context, next) => { 128 | next(new Error('some thing happened')); 129 | }); 130 | 131 | apiMapper.test(testData) 132 | .catch(err => { 133 | done(); 134 | }) 135 | }) 136 | }); 137 | 138 | describe('useAfter methods', () => { 139 | let server; 140 | let testMetadata, 141 | testData, 142 | apiModule, 143 | apiMapper; 144 | 145 | before('Setup server', done => { 146 | server = utils.createServer(7788, (req, res) => { 147 | let rawData = ''; 148 | req.on('data', chunk => rawData += chunk); 149 | req.on('end', () => { 150 | const data = JSON.parse(rawData); 151 | res.writeHead(200, { 'Content-Type': 'application/json' }); 152 | res.end(JSON.stringify({ code: 200, data })); 153 | }); 154 | 155 | // prevent default response action 156 | return true; 157 | }); 158 | server.on('listening', () => { 159 | done(); 160 | }); 161 | }); 162 | 163 | after('Stop and clean server', done => { 164 | server.on('close', () => { 165 | server = null; 166 | done(); 167 | }); 168 | server.close(); 169 | cleanHooks(); 170 | }); 171 | 172 | beforeEach('Setup ApiModule', () => { 173 | testMetadata = { 174 | url: '/api/test', 175 | method: 'get', 176 | }; 177 | testData = { 178 | query: { 179 | a: 1, 180 | b: 2 181 | }, 182 | body: { 183 | c: 11, 184 | d: 22 185 | } 186 | }; 187 | apiModule = new ApiModule({ 188 | baseConfig: { 189 | baseURL: 'http://localhost:7788' 190 | }, 191 | module: false, 192 | metadatas: { 193 | test: testMetadata 194 | } 195 | }); 196 | 197 | apiMapper = apiModule.getInstance(); 198 | apiModule.getAxios().interceptors.response.use(response => { 199 | return response.data.data; 200 | }); 201 | }); 202 | 203 | afterEach('Clean ApiModule', () => { 204 | cleanHooks(); 205 | }); 206 | 207 | it('static after method globalAfter', async () => { 208 | ApiModule.globalAfter((context, next) => { 209 | expect(context.metadata).to.be.not.equal(testMetadata); 210 | expect(context.metadata).to.be.eql(testMetadata); 211 | next(); 212 | }); 213 | 214 | const res = await apiMapper.test(testData); 215 | expect(res).to.be.eql(testData.body); 216 | }); 217 | 218 | it('instance after method useAfter', async () => { 219 | apiModule.useAfter((context, next) => { 220 | expect(context.metadata).to.be.eql(testMetadata); 221 | next(); 222 | }); 223 | 224 | const res = await apiMapper.test(testData); 225 | expect(res).to.be.eql(testData.body); 226 | }); 227 | 228 | it('instance after method would override static method', async () => { 229 | apiModule.useAfter((context, next) => { 230 | next(); 231 | }); 232 | ApiModule.globalAfter((context, next) => { 233 | next(new Error('It should not go here.')); 234 | }); 235 | 236 | const res = await apiMapper.test(testData); 237 | expect(res).to.be.eql(testData.body); 238 | }); 239 | 240 | 241 | it('static after method passing `null` would not throw an errors', async () => { 242 | ApiModule.globalAfter(null); 243 | await apiMapper.test(testData); 244 | }); 245 | 246 | it('static after method passing `123` would not throw an error', async () => { 247 | ApiModule.globalAfter(123); 248 | await apiMapper.test(testData); 249 | }); 250 | 251 | it('static after method passing undefined would not throw an error', async () => { 252 | ApiModule.globalAfter(); 253 | await apiMapper.test(testData); 254 | }); 255 | 256 | it('instance after method passing undefined would not throw an error', async () => { 257 | apiModule.useAfter(); 258 | await apiMapper.test(testData); 259 | }); 260 | 261 | }); 262 | 263 | describe('useCatch methods', () => { 264 | let server; 265 | let testMetadata, 266 | testData, 267 | apiModule, 268 | apiMapper; 269 | 270 | 271 | before('Setup server', done => { 272 | server = utils.createServer(7788); 273 | server.on('listening', () => { 274 | done(); 275 | }); 276 | }); 277 | 278 | after('Stop and clean server', done => { 279 | server.on('close', () => { 280 | server = null; 281 | done(); 282 | }); 283 | server.close(); 284 | cleanHooks(); 285 | }); 286 | 287 | 288 | beforeEach('Setup ApiModule', () => { 289 | testMetadata = { 290 | url: '/api/test', 291 | method: 'get', 292 | name: 'test middleware methods' 293 | }; 294 | testData = { 295 | query: { 296 | a: 1, 297 | b: 2 298 | }, 299 | body: { 300 | c: 11, 301 | d: 22 302 | } 303 | }; 304 | apiModule = new ApiModule({ 305 | baseConfig: { 306 | // a typo error on purpose 307 | baseURL: 'http://localhost:7789', 308 | timeout: 0 309 | }, 310 | module: false, 311 | console: true, 312 | metadatas: { 313 | test: testMetadata 314 | } 315 | }); 316 | 317 | apiMapper = apiModule.getInstance(); 318 | }); 319 | 320 | afterEach('Clean ApiModule', () => { 321 | cleanHooks(); 322 | }); 323 | 324 | it('static catch method useCatch', async () => { 325 | const middlewareError = new Error('I made a mistake'); 326 | ApiModule.globalBefore((context, next) => { 327 | // context.setError(); 328 | next(middlewareError); 329 | }); 330 | ApiModule.globalCatch((context, next) => { 331 | expect(context.metadata).to.be.deep.equal(testMetadata); 332 | expect(context.data).to.be.deep.equal(testData); 333 | expect(context.responseError).to.be.equal(middlewareError); 334 | next(); 335 | }); 336 | 337 | try { 338 | await apiMapper.test(testData); 339 | } catch (error) { 340 | expect(error).to.be.equal(middlewareError); 341 | } 342 | }); 343 | 344 | it('instance catch method useCatch', async () => { 345 | const middlewareError = new Error('I made a mistake'); 346 | apiModule.useBefore((context, next) => { 347 | // context.setError(); 348 | next(middlewareError); 349 | }); 350 | apiModule.useCatch((context, next) => { 351 | expect(context.metadata).to.be.deep.equal(testMetadata); 352 | expect(context.data).to.be.deep.equal(testData); 353 | expect(context.responseError).to.be.equal(middlewareError); 354 | next(); 355 | }); 356 | 357 | try { 358 | await apiMapper.test(testData); 359 | } catch (error) { 360 | expect(error).to.be.equal(middlewareError); 361 | } 362 | }); 363 | 364 | 365 | it('instance catch method would override static method', async () => { 366 | const error = new Error('A mistake'); 367 | const anthorError = new Error('Anthor mistake'); 368 | 369 | apiModule.useBefore((context, next) => { 370 | next(error); 371 | }); 372 | apiModule.useCatch((context, next) => { 373 | expect(context.responseError).to.be.equal(error); 374 | context.setError(anthorError); 375 | next(); 376 | }); 377 | ApiModule.globalCatch((context, next) => { 378 | throw 'It should not go here'; 379 | next(); 380 | }); 381 | 382 | try { 383 | await apiMapper.test(testData); 384 | } catch (err) { 385 | expect(err).to.be.equal(anthorError); 386 | expect(err).to.be.not.equal(error); 387 | } 388 | }); 389 | 390 | it('static catch method passing `null` would not throw an error', async () => { 391 | ApiModule.globalCatch(null); 392 | apiModule.useBefore((context, next) => { 393 | next('error'); 394 | }); 395 | try { 396 | apiMapper.test(testData) 397 | expect(1).to.be.ok; 398 | } catch (error) { 399 | throw 'It should not go here'; 400 | } 401 | }); 402 | 403 | it('static catch method passing `123` would not throw an error', async () => { 404 | ApiModule.globalCatch(123); 405 | apiModule.useBefore((context, next) => { 406 | next('error'); 407 | }); 408 | try { 409 | apiMapper.test(testData) 410 | expect(1).to.be.ok; 411 | } catch (error) { 412 | throw 'It should not go here'; 413 | } 414 | }); 415 | 416 | it('static catch method passing undefined would not throw an error', async () => { 417 | ApiModule.globalCatch(); 418 | apiModule.useBefore((context, next) => { 419 | next('error'); 420 | }); 421 | try { 422 | apiMapper.test(testData) 423 | expect(1).to.be.ok; 424 | } catch (error) { 425 | throw 'It should not go here'; 426 | } 427 | }); 428 | 429 | it('instance catch method passing undefined would not throw an error', async () => { 430 | apiModule.useCatch(); 431 | expect(true).to.be.ok; 432 | }); 433 | 434 | }); -------------------------------------------------------------------------------- /test/middleware.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import utils from './utils'; 3 | import ApiModule from '../src'; 4 | 5 | describe('foreRequestMiddleware', () => { 6 | let server, 7 | apiModule; 8 | before('Setup server', done => { 9 | server = utils.createBounceServer(7788); 10 | server.on('listening', () => { 11 | done(); 12 | }); 13 | }); 14 | 15 | after('Stop and clean server', done => { 16 | server.on('close', () => { 17 | server = null; 18 | done(); 19 | }); 20 | server.close(); 21 | }); 22 | 23 | beforeEach(() => { 24 | apiModule = new ApiModule({ 25 | baseConfig: { 26 | baseURL: 'http://localhost:7788' 27 | }, 28 | metadatas: { 29 | test: { 30 | url: '/', 31 | method: 'post' 32 | }, 33 | testParams: { 34 | url: '/user/:id/{time}', 35 | method: 'get' 36 | } 37 | }, 38 | module: false 39 | }); 40 | }); 41 | 42 | afterEach(() => { 43 | apiModule.foreRequestHook = null; 44 | apiModule.postRequestHook = null; 45 | apiModule.fallbackHook = null; 46 | }); 47 | 48 | 49 | it('foreRequestMiddleWare set another data', async () => { 50 | const data = { body: { a: 1, b: 2 } }; 51 | const anotherData = { body: { c: 3, d: 4 } }; 52 | apiModule.useBefore((context, next) => { 53 | expect(context.responseError).to.be.equal(null); 54 | expect(context.data).to.be.equal(data); 55 | context.setData(anotherData); 56 | expect(context.data).to.be.equal(anotherData); 57 | next(); 58 | }); 59 | 60 | 61 | try { 62 | const res = await apiModule.getInstance().test(data); 63 | expect(res.data).to.be.not.deep.equal(data.body); 64 | expect(res.data).to.be.deep.equal(anotherData.body); 65 | } catch (err) { 66 | throw 'It should not go here' 67 | } 68 | }); 69 | 70 | it('request was success, but fore-request middleware set an error', async () => { 71 | const error = new Error('I am an error'); 72 | 73 | apiModule.useBefore((context, next) => { 74 | expect(context.responseError).to.be.equal(null); 75 | context.setError(error); 76 | expect(context.responseError).to.be.equal(error); 77 | next(); 78 | }); 79 | 80 | 81 | try { 82 | await apiModule.getInstance().test(); 83 | throw 'It should not go here'; 84 | } catch (err) { 85 | expect(err).to.be.equal(error); 86 | } 87 | }); 88 | 89 | it('the request was successful, but fore-request middleware passes an error in the `next` function', async () => { 90 | const error = new Error('I am an error'); 91 | 92 | apiModule.useBefore((context, next) => { 93 | expect(context.responseError).to.be.equal(null); 94 | next(error); 95 | expect(context.responseError).to.be.equal(error); 96 | }); 97 | 98 | 99 | try { 100 | await apiModule.getInstance().test(); 101 | throw 'It should not go here'; 102 | } catch (err) { 103 | expect(err).to.be.equal(error); 104 | } 105 | }); 106 | 107 | it('`next` function parameters will override response error', async () => { 108 | const error = new Error('An error'); 109 | const anotherError = 'An error'; 110 | 111 | apiModule.useBefore((context, next) => { 112 | expect(context.responseError).to.be.equal(null); 113 | context.setError(error); 114 | expect(context.responseError).to.be.equal(error); 115 | next(anotherError); 116 | }); 117 | 118 | try { 119 | await apiModule.getInstance().test(); 120 | } catch (err) { 121 | expect(err).to.be.not.equal(error); 122 | expect(err).to.be.match(new RegExp(anotherError)); 123 | } 124 | 125 | }); 126 | 127 | it('fore-request middleware set a new axios options', done => { 128 | const token = 'Basic ' + Buffer.from('I am a token').toString('base64'); 129 | apiModule.useBefore((context, next) => { 130 | context.setAxiosOptions({ 131 | baseURL: 'http://localhost:8877', 132 | headers: { 133 | 'authorization': token 134 | } 135 | }); 136 | next(); 137 | }); 138 | const server = utils.createServer(8877, (req, res) => { 139 | const authHeader = req.headers['authorization']; 140 | expect(authHeader).to.be.equal(token); 141 | 142 | server.on('close', () => { 143 | done(); 144 | }); 145 | server.close(); 146 | }); 147 | 148 | server.on('listening', async () => { 149 | try { 150 | await apiModule.getInstance().test(null); 151 | } catch (error) { 152 | throw 'It should not go here'; 153 | } 154 | }); 155 | }); 156 | 157 | it('fore-request middleware set axios options would be override by request axios options', async () => { 158 | apiModule.useBefore((context, next) => { 159 | context.setAxiosOptions({ 160 | baseURL: 'http://localhost:8877', 161 | headers: { 162 | 'x-custom-header': 'I am custom header' 163 | } 164 | }); 165 | next(); 166 | }); 167 | 168 | try { 169 | const res = await apiModule.getInstance().test(null, { 170 | baseURL: 'http://localhost:7788' 171 | }); 172 | 173 | expect(res).to.be.ok; 174 | expect(res.config.baseURL).to.be.equal('http://localhost:7788'); 175 | expect(res.config.headers['x-custom-header']).to.be.equal('I am custom header'); 176 | } catch (error) { 177 | console.error(error); 178 | throw 'It should not go here'; 179 | } 180 | }); 181 | 182 | it('context.baseURL and context.url', async () => { 183 | const baseURL = 'http://localhost:7788/api'; 184 | const id = 123; 185 | const now = Date.now(); 186 | 187 | apiModule.useBefore((context, next) => { 188 | expect(context.baseURL).to.be.equal(baseURL); 189 | expect(context.url).to.be.equal(`${baseURL}/user/${id}/${now}`); 190 | next(); 191 | }); 192 | 193 | const request = apiModule.getInstance().testParams; 194 | 195 | try { 196 | const res = await request( 197 | { 198 | params: { 199 | id, 200 | time: now 201 | } 202 | }, 203 | { 204 | baseURL 205 | } 206 | ); 207 | 208 | expect(res).to.be.ok; 209 | expect(res.config.url).to.be.equal(request.context.url); 210 | expect(res.config.baseURL).to.be.equal(request.context.baseURL); 211 | } catch (error) { 212 | console.log(error); 213 | throw 'It should not go here'; 214 | } 215 | }); 216 | 217 | 218 | it('fore-request middleware set illegal axios options', async () => { 219 | utils.overrideConsole(); 220 | 221 | apiModule.useBefore((context, next) => { 222 | const produceError = () => { 223 | context.setAxiosOptions(null); 224 | }; 225 | expect(produceError).to.throw(/configure axios options error/); 226 | next(); 227 | }); 228 | 229 | try { 230 | await apiModule.getInstance().test(); 231 | } catch (error) { 232 | console.error(error); 233 | } 234 | 235 | 236 | utils.recoverConsole(); 237 | }); 238 | 239 | it('error occurred in fore-request middleware', async () => { 240 | utils.overrideConsole(); 241 | 242 | apiModule.useBefore((context, next) => { 243 | throw 'I am an Error'; 244 | next(); 245 | }); 246 | 247 | try { 248 | await apiModule.getInstance().test(); 249 | } catch (error) { 250 | expect(error).to.be.match(/An error occurred in foreRequestMiddleWare/); 251 | } 252 | 253 | utils.recoverConsole(); 254 | }); 255 | 256 | it('error occurred in fore-request middleware, but request would send normally', async () => { 257 | apiModule.useBefore((context, next) => { 258 | throw 'I am an Error'; 259 | next(); 260 | }); 261 | 262 | try { 263 | const res = await apiModule.getInstance().test(); 264 | expect(res).to.be.ok; 265 | } catch (error) { 266 | throw 'It should not go here'; 267 | } 268 | }); 269 | }); 270 | 271 | describe('postRequestMiddleware', () => { 272 | let server, 273 | apiModule; 274 | before('Setup server', done => { 275 | server = utils.createServer(7788); 276 | server.on('listening', () => { 277 | done(); 278 | }); 279 | }); 280 | 281 | after('Stop and clean server', done => { 282 | server.on('close', () => { 283 | server = null; 284 | done(); 285 | }); 286 | server.close(); 287 | }); 288 | 289 | beforeEach(() => { 290 | apiModule = new ApiModule({ 291 | baseConfig: { 292 | baseURL: 'http://localhost:7788' 293 | }, 294 | metadatas: { 295 | test: { 296 | url: '/', 297 | method: 'get' 298 | } 299 | }, 300 | module: false 301 | }); 302 | }); 303 | 304 | afterEach(() => { 305 | apiModule.foreRequestHook = null; 306 | apiModule.postRequestHook = null; 307 | apiModule.fallbackHook = null; 308 | }); 309 | 310 | 311 | it('the request was successful, postRequestMiddleWare set another response', async () => { 312 | const anotherResponse = { code: 0, list: [] }; 313 | apiModule.useAfter((context, next) => { 314 | expect(context.responseError).to.be.equal(null); 315 | expect(context.response.data).to.be.deep.equal(utils.defaultServerRespose); 316 | context.response.data = anotherResponse; 317 | context.setResponse(context.response); 318 | expect(context.response.data).to.be.equal(anotherResponse); 319 | next(); 320 | }); 321 | 322 | 323 | try { 324 | const data = await apiModule.getInstance().test(); 325 | expect(data.data).to.be.not.equal(utils.defaultServerRespose); 326 | expect(data.data).to.be.equal(anotherResponse); 327 | } catch (err) { 328 | throw 'It should not go here' 329 | } 330 | }); 331 | it('the request was successful, post-request middleware passes an error in the `next` function', async () => { 332 | const error = new Error('I am an error'); 333 | 334 | apiModule.useAfter((context, next) => { 335 | expect(context.responseError).to.be.equal(null); 336 | expect(context.response.data).to.be.deep.equal(utils.defaultServerRespose); 337 | next(error); 338 | expect(context.responseError).to.be.equal(error); 339 | }); 340 | 341 | 342 | try { 343 | await apiModule.getInstance().test(); 344 | throw 'It should not go here'; 345 | } catch (err) { 346 | expect(err).to.be.equal(error); 347 | } 348 | }); 349 | 350 | it('error occurred in post-request middleware', async () => { 351 | try { 352 | apiModule.useAfter((context, next) => { 353 | throw 'I am an Error'; 354 | next(); 355 | }); 356 | await apiModule.getInstance().test(); 357 | } catch (error) { 358 | expect(error).to.be.match(/An error occurred in postRequestMiddleWare/); 359 | } 360 | }); 361 | 362 | it('error occurred in post-request middleware, but request would send normally', async () => { 363 | apiModule.useAfter((context, next) => { 364 | throw 'I am an Error'; 365 | next(); 366 | }); 367 | 368 | try { 369 | const res = await apiModule.getInstance().test(); 370 | expect(res).to.be.ok; 371 | } catch (error) { 372 | throw 'It should not go here'; 373 | } 374 | }); 375 | }); 376 | 377 | 378 | describe('fallbackMiddleware', () => { 379 | let server, 380 | apiModule; 381 | before('Setup server', done => { 382 | server = utils.createBounceServer(7788); 383 | server.on('listening', () => { 384 | done(); 385 | }); 386 | }); 387 | 388 | after('Stop and clean server', done => { 389 | server.on('close', () => { 390 | server = null; 391 | done(); 392 | }); 393 | server.close(); 394 | }); 395 | 396 | beforeEach(() => { 397 | apiModule = new ApiModule({ 398 | baseConfig: { 399 | baseURL: 'http://localhost:7788', 400 | timeout: 500 401 | }, 402 | metadatas: { 403 | test: { 404 | url: '/', 405 | method: 'post' 406 | } 407 | }, 408 | module: false, 409 | console: true 410 | }); 411 | }); 412 | 413 | afterEach(() => { 414 | apiModule.foreRequestHook = null; 415 | apiModule.postRequestHook = null; 416 | apiModule.fallbackHook = null; 417 | }); 418 | 419 | 420 | it('fallback middleware set another error', async () => { 421 | const error = new Error('I am an error'); 422 | const serverResponse = { code: 500, message: 'Internal server error' }; 423 | apiModule.getAxios().interceptors.response.use(response => { 424 | if (response.data.code === 200) { 425 | return response.data.data; 426 | } 427 | else { 428 | return Promise.reject(response.data); 429 | } 430 | }); 431 | apiModule.useCatch((context, next) => { 432 | expect(context.responseError).to.be.not.equal(null); 433 | expect(context.responseError).to.be.deep.equal(serverResponse); 434 | context.setError(error); 435 | next(); 436 | }); 437 | 438 | 439 | try { 440 | await apiModule.getInstance().test({ body: serverResponse }); 441 | throw 'It should not go here'; 442 | } catch (err) { 443 | expect(err).to.be.not.equal(serverResponse); 444 | expect(err).to.be.equal(error); 445 | } 446 | }); 447 | 448 | it('fallback middleware `next` function parameters will override response error', async () => { 449 | const error = new Error('I am an error'); 450 | const anotherError = 'I am an error'; 451 | 452 | apiModule.getAxios().interceptors.response.use(response => { 453 | if (response.data.code === 200) { 454 | return response.data.data; 455 | } 456 | else { 457 | return Promise.reject(response.data); 458 | } 459 | }); 460 | 461 | apiModule.useCatch((context, next) => { 462 | expect(context.responseError).to.be.not.equal(null); 463 | context.setError(error); 464 | next(anotherError); 465 | }); 466 | 467 | 468 | try { 469 | await apiModule.getInstance().test({ body: { code: 500, message: 'Internal server error' } }); 470 | throw 'It should not go here'; 471 | } catch (err) { 472 | expect(err).to.be.not.equal(error); 473 | expect(err).to.be.an('Error'); 474 | expect(err).to.be.not.a('String'); 475 | expect(err).to.be.match(new RegExp(anotherError)); 476 | } 477 | }); 478 | 479 | it('request was success, and fallback middleware will not be called', async () => { 480 | 481 | apiModule.useCatch((context, next) => { 482 | throw 'It should not go here'; 483 | next(); 484 | }); 485 | 486 | try { 487 | const data = await apiModule.getInstance().test(); 488 | expect(data).to.be.ok; 489 | } catch (err) { 490 | throw 'It should not go here'; 491 | } 492 | }); 493 | 494 | it('error occurred in fallback middleware', async () => { 495 | try { 496 | apiModule.useBefore((context, next) => { 497 | context.setError('I am an error'); 498 | next(); 499 | }); 500 | apiModule.useCatch((context, next) => { 501 | throw 'I am an Error'; 502 | next(); 503 | }); 504 | await apiModule.getInstance().test(); 505 | throw 'It should not go here'; 506 | } catch (error) { 507 | console.log(error); 508 | expect(error).to.be.match(/I am an error/); 509 | } 510 | }); 511 | 512 | it('error occurred in fallback middleware, but request would send normally', async () => { 513 | 514 | try { 515 | apiModule.useCatch((context, next) => { 516 | throw 'I am an Error'; 517 | next(); 518 | }); 519 | const res = await apiModule.getInstance().test(); 520 | expect(res).to.be.ok; 521 | } catch (error) { 522 | throw 'It should not go here'; 523 | } 524 | }); 525 | 526 | 527 | it('default error handler', async () => { 528 | utils.overrideConsole(); 529 | apiModule.useCatch(null); 530 | 531 | try { 532 | const request = apiModule.getInstance().test; 533 | request.context.setAxiosOptions({ 534 | // typo on purpose 535 | baseURL: 'http://localhost:8877' 536 | }); 537 | request(); 538 | } catch (error) { 539 | console.log('error: ', error); 540 | expect(error).to.be.match(/\[ApiModule\] \[post \/\] failed with /); 541 | } 542 | 543 | utils.recoverConsole(); 544 | }); 545 | }); -------------------------------------------------------------------------------- /test/request.spec.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import ApiModule from '../src'; 3 | import utils from './utils'; 4 | 5 | 6 | describe('request by baseConfig', () => { 7 | 8 | let config, apiModule, apiMapper; 9 | 10 | beforeEach('Setup ApiModule', () => { 11 | config = { 12 | baseURL: 'http://localhost:7788' 13 | }; 14 | apiModule = new ApiModule({ 15 | baseConfig: config, 16 | module: false, 17 | metadatas: { 18 | test: { 19 | url: '/api/test', 20 | method: 'post' 21 | } 22 | } 23 | }); 24 | 25 | apiMapper = apiModule.getInstance(); 26 | }); 27 | 28 | it('axios should get be configured right', () => { 29 | const axios = apiModule.getAxios(); 30 | expect(axios.defaults).to.have.contain(config); 31 | }); 32 | 33 | it('server should get right data package', done => { 34 | const postData = { 35 | body: { 36 | a: 1, 37 | b: 2 38 | } 39 | }; 40 | const server = utils.createServer(7788, req => { 41 | let rawData = ''; 42 | req.on('data', chunk => rawData += chunk); 43 | req.on('end', () => { 44 | server.on('close', () => { 45 | done(); 46 | }); 47 | server.close(); 48 | const data = JSON.parse(rawData); 49 | expect(data).to.be.deep.equal(postData.body); 50 | }); 51 | }); 52 | server.on('listening', () => { 53 | apiMapper.test(postData); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('api metadata mapper', () => { 59 | 60 | beforeEach(() => { 61 | utils.overrideConsole(); 62 | }); 63 | 64 | afterEach(() => { 65 | utils.recoverConsole(); 66 | }); 67 | 68 | it('api request mapper set non-object options', () => { 69 | const apiModule = new ApiModule({ 70 | module: false, 71 | metadatas: { 72 | test: { 73 | url: '/test', 74 | method: 'get' 75 | } 76 | } 77 | }); 78 | 79 | expect(() => apiModule.getInstance().test(null, null)).to.throw(/the request parameter/); 80 | }); 81 | it('test path parameter mode url', done => { 82 | const apiModule = new ApiModule({ 83 | baseConfig: { 84 | baseURL: 'http://localhost:7788' 85 | }, 86 | module: false, 87 | metadatas: { 88 | test: { 89 | url: '/api/{id}/:time/info', 90 | method: 'post' 91 | } 92 | } 93 | }); 94 | 95 | const apiMapper = apiModule.getInstance(); 96 | const id = 123; 97 | const time = Date.now(); 98 | 99 | const server = utils.createServer(7788, req => { 100 | server.on('close', () => { 101 | done(); 102 | }); 103 | console.log(req.url); 104 | expect(req.url).to.be.equal(`/api/${id}/${time}/info?o=calvin&v=von`); 105 | server.close(); 106 | }); 107 | server.on('listening', () => { 108 | 109 | apiMapper.test({ 110 | params: { 111 | id, 112 | time 113 | }, 114 | query: { 115 | o: 'calvin', 116 | v: 'von' 117 | } 118 | }); 119 | }) 120 | }); 121 | 122 | it('api request mapper options', done => { 123 | try { 124 | const apiModule = new ApiModule({ 125 | baseConfig: { 126 | baseURL: 'http://localhost:7788' 127 | }, 128 | module: false, 129 | metadatas: { 130 | test: { 131 | url: '/api/info', 132 | method: 'post' 133 | } 134 | } 135 | }); 136 | const apiMapper = apiModule.getInstance(); 137 | 138 | const server = utils.createServer(7788, req => { 139 | let rawData = ''; 140 | req.on('data', chunk => rawData += chunk); 141 | req.on('end', () => { 142 | server.on('close', () => { 143 | done(); 144 | }); 145 | expect(rawData).to.be.equal("{\"a\":1,\"b\":2}"); 146 | server.close(); 147 | }); 148 | }); 149 | 150 | server.on('listening', () => { 151 | apiMapper.test({ 152 | body: { 153 | a: 1, 154 | b: 2 155 | } 156 | }, { 157 | headers: { 158 | 'Content-Type': 'application/x-www-form-urlencoded' 159 | }, 160 | baseURL: 'http://localhost:7788' 161 | }); 162 | }); 163 | } catch (error) { 164 | console.log(error) 165 | } 166 | }); 167 | }); 168 | 169 | describe('cancellation', () => { 170 | let server; 171 | before('Setup server', done => { 172 | server = utils.createServer(7788); 173 | server.on('listening', done); 174 | }); 175 | 176 | after('Stop and clean server', done => { 177 | server.on('close', () => { 178 | done(); 179 | }); 180 | server.close(); 181 | server = null; 182 | }); 183 | 184 | it('request cancellation', async () => { 185 | const apiModule = new ApiModule({ 186 | baseConfig: { 187 | baseURL: 'http://localhost:7788', 188 | timeout: 10000 189 | }, 190 | module: false, 191 | metadatas: { 192 | test: { 193 | url: '/api/info', 194 | method: 'get' 195 | } 196 | } 197 | }); 198 | 199 | const apiMapper = apiModule.getInstance(); 200 | const cancelSource = apiModule.generateCancellationSource(); 201 | 202 | try { 203 | cancelSource.cancel('Canceled by the user'); 204 | await apiMapper.test( 205 | null, 206 | { 207 | cancelToken: cancelSource.token 208 | } 209 | ); 210 | 211 | } catch (error) { 212 | expect(error.message).to.be.equal('Canceled by the user'); 213 | } 214 | }); 215 | }); -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | const originConsole = { 4 | log: console.log, 5 | warn: console.warn, 6 | error: console.error, 7 | }; 8 | 9 | export default { 10 | /** 11 | * @param {Number} port 12 | * @param {Function} callback stop default response action when return true 13 | */ 14 | createServer(port, callback = new Function()) { 15 | return http.createServer((req, res) => { 16 | if (callback(req, res)) { 17 | return; 18 | } 19 | res.writeHead(200, { 'Content-Type': 'application/json' }); 20 | res.end(JSON.stringify(this.defaultServerRespose)); 21 | }).listen(port); 22 | }, 23 | 24 | 25 | /** 26 | * Respond to the data that request 27 | * @param {Number} port 28 | */ 29 | createBounceServer(port) { 30 | return http.createServer((req, res) => { 31 | let rawData = ''; 32 | req.on('data', chunk => rawData += chunk); 33 | req.on('end', () => { 34 | const data = JSON.parse(rawData); 35 | res.writeHead(200, { 'Content-Type': 'application/json' }); 36 | res.end(JSON.stringify(data)); 37 | }); 38 | }).listen(port); 39 | }, 40 | 41 | defaultServerRespose: { 42 | code: 200, 43 | msg: 'success' 44 | }, 45 | 46 | overrideConsole() { 47 | console.warn = txt => { 48 | throw txt; 49 | } 50 | console.error = txt => { 51 | throw txt; 52 | } 53 | }, 54 | 55 | recoverConsole() { 56 | console.log = originConsole.log; 57 | console.warn = originConsole.warn; 58 | console.error = originConsole.error; 59 | } 60 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | entry: { 8 | 'axios-api-module': './src/index.js' 9 | }, 10 | externals: ['axios'], 11 | output: { 12 | path: path.resolve(__dirname, './dist'), 13 | filename: '[name].min.js', 14 | libraryTarget: 'umd', 15 | library: 'ApiModule', 16 | globalObject: 'typeof self !== \'undefined\' ? self : this', 17 | }, 18 | module: { 19 | rules: [{ 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | loader: "babel-loader" 23 | }] 24 | }, 25 | plugins: [ 26 | new webpack.BannerPlugin({ 27 | banner: `axios-api-module.js v${require('./package.json').version}\n(c) ${new Date().getFullYear()} Calvin Von\nReleased under the MIT License.` 28 | }), 29 | new UglifyJsPlugin({ 30 | uglifyOptions: { 31 | compress: {}, 32 | warnings: false 33 | }, 34 | parallel: true 35 | }) 36 | ] 37 | } --------------------------------------------------------------------------------