├── .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 | [](https://www.npmjs.com/package/@calvin_von/axios-api-module)
11 | [](https://codecov.io/gh/CalvinVon/axios-api-module)
12 | [](https://github.com/CalvinVon/axios-api-module)
13 | 
14 | [](https://travis-ci.org/CalvinVon/axios-api-module)
15 | [](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 版本) [](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 | [](https://www.npmjs.com/package/@calvin_von/axios-api-module)
7 | [](https://codecov.io/gh/CalvinVon/axios-api-module)
8 | [](https://github.com/CalvinVon/axios-api-module)
9 | 
10 | [](https://travis-ci.org/CalvinVon/axios-api-module)
11 | [](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) [](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 | }
--------------------------------------------------------------------------------