├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SUMMARY.md ├── book.json ├── docs ├── README.md ├── adapters.md ├── basics │ ├── README.md │ ├── adapter.md │ ├── api-function.md │ ├── data-source.md │ └── fetch-api-call.md ├── contributing.md ├── recipes │ ├── README.md │ ├── api-enchantment.md │ ├── auto-invoke-methods.md │ ├── data-source-immutable.md │ ├── data-source-type-checking.md │ ├── default-response.md │ ├── file-upload.md │ ├── fixtures.md │ ├── generate-crud-methods.md │ ├── prepare-process.md │ └── validation.md └── testing.md ├── example ├── weather-axios-tcomb │ ├── index.js │ └── model.js └── weather-fetch-adapter │ └── index.js ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── bivrost-axios-adapter │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── adapter.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.js │ └── yarn.lock ├── bivrost-delay-adapter │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ └── adapter.js │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.js ├── bivrost-fetch-adapter │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.js │ └── yarn.lock ├── bivrost-local-storage-adapter │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.js ├── bivrost-save-blob-adapter │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.js │ └── yarn.lock ├── bivrost │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ │ ├── caches.js │ │ ├── classes.js │ │ ├── createRequestTemplate.js │ │ ├── http-bin-api.js │ │ ├── localstorage-mock.js │ │ ├── mockDataSource.js │ │ ├── promiseCache.js │ │ └── promiseDeduplicator.js │ ├── bivrost-3.2.4.tgz │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── data │ │ │ ├── cache.js │ │ │ └── source.js │ │ ├── http │ │ │ ├── api.js │ │ │ ├── clientRequest.js │ │ │ └── createRequestTemplate.js │ │ └── utils │ │ │ ├── mockDataSource.js │ │ │ ├── promiseCache.js │ │ │ └── promiseDeduplicator.js │ └── yarn.lock └── fetch-api-call │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── __tests__ │ └── index.js │ ├── fetch-api-call-3.2.4.tgz │ ├── node.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── createInterceptors.js │ ├── index.js │ ├── node.js │ └── setup.js │ └── yarn.lock └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | trim_trailing_whitespace=true 5 | insert_final_newline=true 6 | indent_style=space 7 | indent_size=2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /_book 5 | /npm-debug.log 6 | /node_modules 7 | /data 8 | /http 9 | /coverage 10 | /utils 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /npm-debug.log 5 | /node_modules 6 | /coverage 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - yarn add lerna 4 | - lerna bootstrap --npm-client=yarn 5 | - yarn 6 | 7 | node_js: 8 | - "4" 9 | - "5" 10 | - "6" 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "coverage/": true, 4 | "_book/": true, 5 | "packages/bivrost/data/": true, 6 | "packages/bivrost/http/": true, 7 | "packages/bivrost/utils/": true, 8 | 9 | "packages/bivrost-axios-adapter/coverage/": true, 10 | "packages/bivrost-axios-adapter/index.js": true, 11 | 12 | "packages/bivrost-delay-adapter/coverage/": true, 13 | "packages/bivrost-delay-adapter/index.js": true, 14 | 15 | "packages/bivrost-fetch-adapter/coverage/": true, 16 | "packages/bivrost-fetch-adapter/index.js": true, 17 | 18 | "packages/bivrost-local-storage-adapter/coverage/": true, 19 | "packages/bivrost-local-storage-adapter/index.js": true, 20 | 21 | "packages/bivrost-save-blob-adapter/coverage/": true, 22 | "packages/bivrost-save-blob-adapter/index.js": true, 23 | 24 | "packages/fetch-api-call/coverage/": true, 25 | "packages/fetch-api-call/index.js": true, 26 | "packages/fetch-api-call/createInterceptors.js": true, 27 | "packages/fetch-api-call/setup.js": true, 28 | "packages/fetch-api-call/node.js": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | [DOCS/CONTRIBUTING](https://tuchk4.github.io/bivrost/docs/contributing.html) 4 | 5 | --- 6 | 7 | ## Our Pledge 8 | 9 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 10 | 11 | ## Our Standards 12 | 13 | Examples of behavior that contributes to creating a positive environment include: 14 | 15 | * Using welcoming and inclusive language 16 | * Being respectful of differing viewpoints and experiences 17 | * Gracefully accepting constructive criticism 18 | * Focusing on what is best for the community 19 | * Showing empathy towards other community members 20 | 21 | Examples of unacceptable behavior by participants include: 22 | 23 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 24 | * Trolling, insulting/derogatory comments, and personal or political attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a professional setting 28 | 29 | ## Our Responsibilities 30 | 31 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 34 | 35 | ## Scope 36 | 37 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 38 | 39 | ## Enforcement 40 | 41 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at valeriy.sorokobatko@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 42 | 43 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 44 | 45 | ## Attribution 46 | 47 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/4/ 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Valerii Sorokobatko 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 | # [Bivrost](http://tuchk4.github.io/bivrost/) 2 | 3 | Bivrost allows to organize a simple interface to asynchronous APIs. 4 | 5 | [![build status](https://img.shields.io/travis/tuchk4/bivrost/master.svg?style=flat-square)](https://travis-ci.org/tuchk4/bivrost) 6 | [![npm version](https://img.shields.io/npm/v/bivrost.svg?style=flat-square)](https://www.npmjs.com/package/bivrost) 7 | 8 | # Fetch API Call 9 | 10 | Easiet way to use fetch with interceptros and awesome api endpoint declaration. 11 | [bivrost/packages/fetch-api-call](https://github.com/tuchk4/bivrost/tree/master/packages/fetch-api-call) 12 | 13 | ## Bivrost 14 | 15 | The main idea of Bivrost is grouping several API methods into data-sources. 16 | 17 | [Bivrost full documentation and recipes](https://tuchk4.github.io/bivrost/) 18 | 19 | ## Installation 20 | 21 | ``` 22 | yarn add bivrost 23 | ``` 24 | 25 | ## The gist 26 | 27 | That’s it! Create api function for github api. 28 | 29 | ```js 30 | import api from 'bivrost/http/api' 31 | import fetchAdapter from 'bivrost-fetch-adapter'; 32 | 33 | const githubApi = api({ 34 | protocol: 'https:' 35 | host: 'api.github.com', 36 | adapter: fetchAdapter() 37 | }); 38 | 39 | //define API method 40 | const repositoryList = githubApi('GET /users/:user/repos'), 41 | 42 | //call API method 43 | repositoryList({ user: 'tuchk4' }) 44 | .then(repositories => console.log(repositories)); 45 | ``` 46 | 47 | Create data source that contain few github api methods (get repositories list 48 | and get repository info) and its invoke chain. 49 | 50 | ```js 51 | import DataSource from 'bivrost/data/source'; 52 | import githubApi from './github-api'; 53 | import tcomb from 'tcomb'; 54 | 55 | class GihtubRepositories extends DataSource { 56 | // define invoke method chain. Default chain is - ['api', 'process'] 57 | static steps = ['api', 'immutable']; 58 | 59 | // "define "api" step 60 | static api = { 61 | repos: githubApi('GET /users/:user/repos'), 62 | repoInfo: githubApi('GET /repos/:user/:repository'), 63 | }; 64 | 65 | // step function will be executed for each method 66 | static immutable = response => Immutable.fromJSON(response); 67 | 68 | // define data source public methods that invokes steps methods 69 | getRepositories(user) { 70 | return this.invoke('repos', { 71 | user, 72 | }); 73 | } 74 | 75 | getRepositoryInfo(user, repository) { 76 | return this.invoke('repoInfo', { 77 | user, 78 | repository, 79 | }); 80 | } 81 | } 82 | ``` 83 | 84 | Extends GihtubRepositories and define username. Now all requests will be done 85 | for facebook's github group. 86 | 87 | ```js 88 | import GihtubRepositories from './github-repositories'; 89 | 90 | const FACEBOOK_GITHUB_ACCOUNT = 'facebook'; 91 | 92 | class FacebookRepositories extends GihtubRepositories { 93 | getRepositories() { 94 | return super.getRepositories(FACEBOOK_GITHUB_ACCOUNT); 95 | } 96 | 97 | getRepositoryInfo(repository) { 98 | return super.getRepositoryInfo(FACEBOOK_GITHUB_ACCOUNT, repository); 99 | } 100 | } 101 | ``` 102 | 103 | ### [Contributing](docs/contributing.md) 104 | 105 | Project is open for new ideas and features: 106 | 107 | * new adapters 108 | * new api functions 109 | * data source features 110 | * feedback is very matter 111 | 112 | --- 113 | 114 | ## Docs 115 | 116 | * [Basics](/docs/basics/README.md) 117 | * [Api function](/docs/basics/api-function.md) 118 | * [Adapter](/docs/basics/adapter.md) 119 | * [Data source](/docs/basics/data-source.md) 120 | * [Recipes](/docs/recipes/README.md) 121 | * [Api enchantment](/docs/recipes/api-enchantment.md) 122 | * [Generate DataSource invoke functions](/docs/recipes/data-source-auto-invoke.md) 123 | * [Data source immutable output](/docs/recipes/data-source-immutable.md) 124 | * [Data source type checking (tcomb)](/docs/recipes/data-source-type-checking.md) 125 | * [CRUD Data source](/docs/recipes/generate-crud-methods.md) 126 | * [File upload](/docs/recipes/file-upload.md) 127 | * [Fixtures](/docs/recipes/fixtures.md) 128 | * [Default api response](/docs/recipes/default-response.md) 129 | * [Prepare request and process response](/docs/recipes/prepare-process.md) 130 | * [Testing](/docs/testing.md) 131 | * [Contributing](docs/contributing.md) 132 | 133 | #### Adapters 134 | 135 | * [Fetch adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-fetch-adapter) 136 | * [Axios adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-axios-adapter) 137 | * [Delay adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-delay-adapter) 138 | * [Local storage adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-local-storage-adapter) 139 | * [Save blob adapter adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-save-blob-adapter) 140 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | docs/README.md -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": ">=3.2.1", 3 | "title": "Bivrost", 4 | "plugins": [ 5 | "edit-link", 6 | "prism", 7 | "-highlight", 8 | "github", 9 | "ga", 10 | "anker-enable" 11 | ], 12 | "pluginsConfig": { 13 | "edit-link": { 14 | "base": "https://github.com/tuchk4/bivrost/tree/master", 15 | "label": "Edit This Page" 16 | }, 17 | "github": { 18 | "url": "https://github.com/tuchk4/bivrost" 19 | }, 20 | "ga": { 21 | "token": "UA-89488143-1" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of content 2 | 3 | * [Basics](/docs/basics/README.md) 4 | * [Api Function](/docs/basics/api-function.md) 5 | * [Adapter](/docs/basics/adapter.md) 6 | * [Data Source](/docs/basics/data-source.md) 7 | * [Fetch API Call](/docs/basics/fetch-api-call.md) 8 | 9 | - [Bivrost at GitHub](https://github.com/tuchk4/bivrost) 10 | 11 | * [Recipes](/docs/recipes/README.md) 12 | * [Api Enchantment](/docs/recipes/api-enchantment.md) 13 | * [Auto Invoke Methods](/docs/recipes/auto-invoke-methods.md) 14 | * [Immutable Output](/docs/recipes/data-source-immutable.md) 15 | * [Type checking with tcomb](/docs/recipes/data-source-type-checking.md) 16 | * [CRUD Data Source](/docs/recipes/generate-crud-methods.md) 17 | * [File Upload](/docs/recipes/file-upload.md) 18 | * [Fixtures](/docs/recipes/fixtures.md) 19 | * [Default Api Response](/docs/recipes/default-response.md) 20 | * [Prepare Request and Process Response](/docs/recipes/prepare-process.md) 21 | * [Data Validation](/docs/recipes/validation.md) 22 | 23 | ### Adapters 24 | 25 | * [Adapeters List](/docs/adapters.md) 26 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 27 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 28 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 29 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 30 | * [Save Blob Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 31 | 32 | - [Testing](/docs/testing.md) 33 | 34 | * [Contributing](docs/contributing.md) 35 | -------------------------------------------------------------------------------- /docs/adapters.md: -------------------------------------------------------------------------------- 1 | # Adapters 2 | 3 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 4 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 5 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 6 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 7 | * [Save Blob Adapter Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 8 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # Bivrost basics 2 | 3 | * [Api function](/docs/basics/api-function.md) 4 | * [Adapter](/docs/basics/adapter.md) 5 | * [Data source](/docs/basics/data-source.md) 6 | * [Fetch API Call](/docs/basics/fetch-api-call.md) 7 | -------------------------------------------------------------------------------- /docs/basics/adapter.md: -------------------------------------------------------------------------------- 1 | # Adapter 2 | 3 | The main goal of Adapter is make some magic and return function that execute 4 | query according to passed url and Request object or enchant another Adapter. 5 | 6 | In most cases - adapter is using to adapt api function for used approaches or 7 | libraries (fetch, axios, rxjs, local storage) and execute query. 8 | 9 | Also adapter is using for enchanting other adapters - add additional delays for 10 | testing, log api calls, save request response as blob, filter params etc. For 11 | example there are 12 | [delayAdapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter), 13 | [saveBlobAdater](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter), 14 | [localStorage](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 15 | adapter are enchanters. 16 | 17 | ## 18 | 19 | ### [#](#adapter-list) Adapter list 20 | 21 | * [Fetch 22 | adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 23 | * [Axios 24 | adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 25 | * [Delay 26 | adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 27 | * [Local storage 28 | adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 29 | * [Save blob adapter 30 | adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 31 | 32 | ## 33 | 34 | ### [#](#under-the-hood) Under the hood 35 | 36 | _api_ function does not guarantee that XHR method will be called. It just call 37 | adapter with generated config. What magic will be done depends on used adapter. 38 | In case of _localStorageAdapter_ - it will save and get data from 39 | [localStorage](https://developer.mozilla.org/en/docs/Web/API/Window/localStorage). 40 | 41 | ```js 42 | import fetchAdapter from 'bivrost-fetch-adapter'; 43 | const fetch = fetchAdapter(options); 44 | ``` 45 | 46 | Options depends on the adapter. But in most cases options are: 47 | 48 | * _headers_ - requests headers. NOTE that headers could be also set with 49 | interceptors 50 | * _interceptors_ - adapter interceptors - request / response /error 51 | * [Axios Adapter 52 | Options](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter#adapter-options) 53 | * [Fetch Adapter 54 | Options](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter#configuration) 55 | 56 | Example: 57 | 58 | ```js 59 | import bivrostApi from 'bivrost/http/api' 60 | import fetchAdapter from 'bivrost-fetch-adapter'; 61 | 62 | const api = bivrostApi({ 63 | protocol: 'http:' 64 | host: 'localhost:3001', 65 | adapter: fetchAdapter() 66 | }); 67 | 68 | const createUser = api('POST /user?:hash&:version'); 69 | 70 | createUser({ 71 | name: 'john_doe', 72 | hash: 'eecab3', 73 | version: 'v1.2.0' 74 | }); 75 | ``` 76 | 77 | As the result _api_ function will call adapter with generated config and returns 78 | its response. 79 | 80 | ```json 81 | { 82 | "method": "POST", 83 | "path": "/user?hash=eecab3&version=v1.2.0", 84 | "body": { 85 | "name": "John Doe" 86 | } 87 | } 88 | ``` 89 | 90 | Next two example will produce same result: 91 | 92 | * Using api function: 93 | 94 | ```js 95 | import bivrostApi from 'bivrost/http/api' 96 | import fetchAdapter from 'bivrost-fetch-adapter'; 97 | 98 | const api = bivrostApi({ 99 | protocol: 'http:' 100 | host: 'localhost:3001', 101 | adapter: fetchAdapter() 102 | }); 103 | 104 | const createUser = api('POST /user?:hash&:version'); 105 | 106 | return createUser({ 107 | name: 'john_doe', 108 | hash: 'eecab3', 109 | version: 'v1.2.0' 110 | }); 111 | ``` 112 | 113 | * Using adapter directly: 114 | 115 | ```js 116 | import fetchAdapter from 'bivrost-fetch-adapter'; 117 | 118 | const fetch = fetchAdapter(); 119 | 120 | return fetch({ 121 | method: 'POST', 122 | path: 'http://localhost:3001/user?hash=eecab3&version=v1.2.0', 123 | body: { 124 | name: 'John Doe', 125 | }, 126 | }); 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/basics/api-function.md: -------------------------------------------------------------------------------- 1 | # Api function 2 | 3 | _Api_ is simple HTTP client wrapper that lets us define REST methods in single line of code. 4 | 5 | ```js 6 | import api from 'bivrost/http/api' 7 | import fetchAdapter from 'bivrost-fetch-adapter'; 8 | 9 | const githubApi = api({ 10 | protocol: 'https:' 11 | host: 'api.github.com', 12 | adapter: fetchAdapter() 13 | }); 14 | ``` 15 | 16 | Options: 17 | 18 | * _protocol_ - http protocol. Available values - **http:** an **https:** 19 | * _host_ - server hostname 20 | * _prefix_ - api url prefix. Useful when there are multiple api versions: 21 | * _/user_ 22 | * _/v1/user_ 23 | * _/v2/user_ 24 | 25 | Example: 26 | 27 | ```js 28 | // Create a new repository for the authenticated user 29 | const createRepository = githubApi('POST /user/repos'); 30 | 31 | createRepository({ 32 | name: 'My new repo', 33 | }).then(response => {}); 34 | 35 | // List all public repositories 36 | const getRepositoryList = githubApi('GET /repositories'); 37 | 38 | // Stringify url according to params 39 | getRepositoryList.stringify({ 40 | since: 364, 41 | }); // https://api.github.com/repositories?since=364 42 | 43 | getRepositoryList({ 44 | since: 364, // The integer ID of the last Repository that you've seen. 45 | }).then(response => {}); 46 | 47 | // List of users repositories 48 | const getRepositoryInfo = githubApi('GET /repos/:owner/:repo'); 49 | getUserRepositories({ 50 | owner: 'tuchk4', 51 | repo: 'bivrost', 52 | }).then(response => {}); 53 | ``` 54 | 55 | ## 56 | 57 | ### [#](#api-definition) Api url definition and placeholders 58 | 59 | * `PUT /user/:id` - _id_ parameter is required. _id_ will be removed from request body. All other parameters will be passed to request body. 60 | 61 | ```js 62 | const updateUser = api('PUT /user/:id'); 63 | updateUser({ 64 | id: 1, 65 | name: 'Valerii', 66 | }); 67 | 68 | // Make PUT request to /user/1 69 | // with request body 70 | // name=Valerii 71 | ``` 72 | 73 | * `POST /user?:hash&:version` - _hash_ and _version_ parameters are required. These parameters will be removed from request body and passed to query. All other parameters will be passed to request body. 74 | 75 | ```js 76 | const createUser = api('POST /user?:hash&:version&'); 77 | 78 | createUser({ 79 | name: 'John Doe', 80 | hash: 'eecab3', 81 | version: 'v1.2.0', 82 | }); 83 | 84 | // Make POST request to /user?hash=eecab3&version=v1.2.0 85 | // with request body 86 | // name=tuchk4 87 | ``` 88 | 89 | * `GET /users` - for _GET_ requests all parameters are passed to query. 90 | 91 | ```js 92 | const getUsers = api('GET /users'); 93 | 94 | getUsers({ 95 | group: 'admin', 96 | orderBy: 'loginDate', 97 | }); 98 | 99 | // Make GET request to /users?group=admin&orderBy=loginDate 100 | ``` 101 | 102 | ## 103 | 104 | ### [#](#adapters) Under the hood: adapters 105 | 106 | * [Adapters](/docs/basics/adapter.md) 107 | 108 | `POST /user?:hash&:version` does not guarantee that XHR POST method will be called. What will be done depends on used adapter - _api_ just calls adapter function with generated config. In case of _localStorage_ adapter - it will save or load data to localStorage. 109 | -------------------------------------------------------------------------------- /docs/basics/data-source.md: -------------------------------------------------------------------------------- 1 | # Data source 2 | 3 | A DataSource - is a group of several API methods and its configuration. Every method call passes with the configured steps. Result of each step will be passed as arguments to the next step and whole steps chain will be cached if cache is enabled. 4 | 5 | A DataSource provide methods for: 6 | 7 | * steps configuration 8 | * steps executing 9 | * cache manipulations 10 | * request deduplication 11 | 12 | ### 13 | 14 | ### [#](#this-gist) The gist 15 | 16 | ```js 17 | import DataSource from 'bivrost/data/source'; 18 | import bivrostApi from 'bivrost/http/api' 19 | import fetchAdapter from 'bivrost-fetch-adapter'; 20 | 21 | import { Record } from 'immutable'; 22 | 23 | const api = bivrostApi({ 24 | host: 'localhost', 25 | adapter: fetchAdapter() 26 | }); 27 | 28 | const User = Record({ 29 | id: null, 30 | name: '' 31 | }); 32 | 33 | class UsersDataSource extends DataSource { 34 | static steps = ['validate', 'api', 'model']; 35 | 36 | static cache = { 37 | loadAll: { 38 | enabled: true, 39 | isGlobal: true, 40 | ttl: 60000 41 | } 42 | }; 43 | 44 | static validate = { 45 | update: ({ id }) => { 46 | if (!id) { 47 | throw new Error('"ID" field is required for user update'); 48 | } 49 | } 50 | } 51 | 52 | static api = { 53 | loadAll: api('GET /users') 54 | update: api('PUT /user/:id') 55 | } 56 | 57 | static model = { 58 | update: User 59 | } 60 | 61 | update(user) { 62 | return this.invoke('update', user); 63 | } 64 | 65 | loadAll(filters) { 66 | return this.invoke('loadAll', filters); 67 | } 68 | } 69 | 70 | usersDataSource.loadAll({}); 71 | 72 | // if previous loadAll call IS NOT finished: 73 | // - will not trigger step chain because of deduplication 74 | usersDataSource.loadAll({}); 75 | 76 | // if previous loadAll call IS finished - will not trigger step chain. 77 | // - will not trigger step chain because of enabled cache 78 | usersDataSource.loadAll({}); 79 | ``` 80 | 81 | ### 82 | 83 | ### [#](#constructor) constructor({ headers, context, steps, options }) 84 | 85 | As constructor arguments (all arguments are optional): 86 | 87 | * _headers_ - additional headers 88 | * _steps_ - step sequence 89 | * _context_ - context object. Final context is calculated with: 90 | * DataSource context (passed as argument to DataSource constructor) 91 | * [invoke()](#invoke) context (passed as third param to invoke function) 92 | * _options_ - any data will be saved in data source instance 93 | 94 | ```js 95 | import DataSource from 'bivrost/data/source'; 96 | 97 | class AppDataSource extends DataSource { 98 | constructor() { 99 | super({ 100 | options: {}, 101 | steps: ['validate', 'serialize', 'api'], 102 | context: {}, 103 | headers: {}, 104 | }); 105 | } 106 | } 107 | ``` 108 | 109 | ### 110 | 111 | ### [#](#invoke) invoke(method: string, params: object, context: object) 112 | 113 | ```js 114 | DataSource.invoke((method: string), (params: object), (context: object)); 115 | ``` 116 | 117 | Now Bivrost generates [Auto Invoke Methods](/docs/recipes/auto-invoke-methods.md) 118 | 119 | Invoke is a data source's function that execute _method_ chain and pass _params_ as initial arguments and _context_ as second. 120 | 121 | Method's chain - sequence of steps where each step is a _function_ and its result is an argument for the next step (like lodash's flow). 122 | 123 | _context_ - is useful when you need to pass the same object to all steps. For example - dynamic _params_ validation or step's configuration. 124 | 125 | Steps sequence could configured as second argument to data source constructor or as property _steps_. If there is no method configuration at step - it will be skipped. 126 | 127 | Default steps sequence: 128 | 129 | * _prepare_ - used for request data transformation and serialization 130 | * _api_ - api call 131 | * _process_ - process the response 132 | 133 | ## 134 | 135 | ### [#](#invoke-steps) Invoke steps 136 | 137 | As property: 138 | 139 | ```js 140 | import DataSource from 'bivrost/data/source'; 141 | 142 | class AppDataSource extends DataSource { 143 | static steps = ['validate', 'serialize', 'api']; 144 | } 145 | ``` 146 | 147 | In this example - there are three steps for _invoke_ function - _validate_, _serialize_ and _api_. Steps sequence and naming - just a developer fantasy and depends on application architecture and requirements. 148 | 149 | ## 150 | 151 | ### [#](#step-configuration) Step configuration 152 | 153 | Step could be configured as _object_ or as _function_. 154 | 155 | * If step is configured as object: 156 | 157 | ```js 158 | class UserDataSource extends DataSource { 159 | static steps = ['api']; 160 | 161 | static api = { 162 | loadAll: api('GET /users'), 163 | }; 164 | 165 | loadAll() { 166 | return invoke('loadAll'); 167 | } 168 | } 169 | ``` 170 | 171 | * If step is configured as function - it will be executed for all methods: 172 | 173 | ```js 174 | class UserDataSource extends DataSource { 175 | static steps = ['api', 'immutable']; 176 | 177 | static immutable = response => Immutable.fromJSON(response); 178 | 179 | static api = { 180 | loadAll: api('GET /users'), 181 | }; 182 | 183 | loadAll(params) { 184 | return this.invoke('loadAll', params); 185 | } 186 | } 187 | ``` 188 | 189 | ## 190 | 191 | ### [#](#cache) Cache 192 | 193 | Define default cache config for all data source methods: 194 | 195 | ```js 196 | class UserDataSource extends DataSource { 197 | static defaultCache = { 198 | enabled: true, 199 | isGlobal: true, 200 | ttl: 60000, 201 | }; 202 | } 203 | ``` 204 | 205 | Define cache config for specific data source method: 206 | 207 | ```js 208 | class UserDataSource extends DataSource { 209 | static cache = { 210 | loadAll: { 211 | enabled: true, 212 | isGlobal: true, 213 | ttl: 60000, 214 | }, 215 | }; 216 | } 217 | ``` 218 | 219 | Configuration is almost same as invoke steps. Cache options: 220 | 221 | * _enabled: boolean_ - enable / disable cache for method 222 | * _isGlobal: boolean_ - enable / disable global method cache. If _true_ - cache will be shared between all data source instances. 223 | * _ttl: integer_ - cache lifetime in miliseconds 224 | 225 | Cache methods: 226 | 227 | * _getCacheKey(method: string, params: object)_ - Hook for cache key generating. By default cache key is `JSON.stringify(params)`` 228 | 229 | * _clearCache(method: string)_ - Clear method caches. If _method_ argument is not specified - clear all data source caches. 230 | 231 | ## 232 | 233 | ### [#](#debug-logs) Debug logs 234 | 235 | ```js 236 | const appDataSource = new AppDataSource(); 237 | appDataSource.enableDebugLogs(); 238 | appDataSource.disableDebugLogs(); 239 | ``` 240 | 241 | If logs are enabled - data source will post messages to console for each step with its parameters. [bows](https://www.npmjs.com/package/bows) is used for logging and thats why `localStorage.debug = true` should be set in your console to see messages. 242 | 243 | ![Bivrost logs](http://i.imgur.com/FOC5z5e.png) 244 | -------------------------------------------------------------------------------- /docs/basics/fetch-api-call.md: -------------------------------------------------------------------------------- 1 | # [Fetch API Call](https://github.com/tuchk4/bivrost/tree/master/packages/fetch-api-call) 2 | 3 | Awesome fetch api for browser and nodejs with interceptors easy way to declare 4 | api endpoints. Provide very easy way to setup api for different 5 | backends/services. 6 | 7 | ## [#](#advantages) Advantages 8 | 9 | * Support interceptors 10 | * Support multiple instances wiht its own interceptors 11 | * Easy way to define headers for specific requests 12 | * Declrative api end points 13 | 14 | ```js 15 | const login = api('POST /login'); 16 | login({}); 17 | 18 | const loadStatistics = api('GET /statistics'); 19 | loadStatistics({ 20 | filter: {}, 21 | }); 22 | 23 | const loadUser = api('GET /user/:id'); 24 | laodUser({ 25 | id: 1, 26 | }); 27 | ``` 28 | 29 | ### [#](#install) Install 30 | 31 | ``` 32 | yarn add fetch-api-call 33 | ``` 34 | 35 | ### [#](#api-declaration) Api Declaration 36 | 37 | ```js 38 | import fetchApiCall from 'fetch-api-call'; 39 | 40 | const { api } = fetchApiCall({ 41 | protocol: process.env.API_PROTOCOL, 42 | host: process.env.API_HOST, 43 | }); 44 | ``` 45 | 46 | Options: 47 | 48 | * _protocol_ - http protocol. Available values - **http:** an **https:** 49 | * _host_ - server hostname 50 | * _headers_ - requests headers. NOTE that headers could be also set with 51 | interceptors 52 | * _mode_ - more details at - 53 | https://developer.mozilla.org/en-US/docs/Web/API/Request/mode 54 | * _prefix_ - api url prefix. Useful when there are multiple api versions: 55 | * _/user_ 56 | * _/v1/user_ 57 | * _/v2/user_ 58 | 59 | **api** - function to define api methods in single line of code. 60 | 61 | ```js 62 | const apiCall = api('HTTP_METHOD /api/path/:with/:placeholders'); 63 | ``` 64 | 65 | _apiCall(params = {}, headers = {})_ - function takes two optional params: 66 | 67 | * _params_ - api params. Also is used for building final api entrypoint. More 68 | detauls see at 69 | [Api Url Definition and Placeholders](https://tuchk4.github.io/bivrost/docs/basics/api-function.html#api-definition). 70 | * _headers_ - certain request headers. 71 | 72 | Examples: 73 | 74 | ```js 75 | // user login api 76 | const login = api('POST /login'); 77 | login({ 78 | username, 79 | password, 80 | }); 81 | 82 | // user logout api 83 | const logout = api('POST /logout'); 84 | logout(); 85 | 86 | // get user api 87 | const getUser = api('GET /user/:id'); 88 | getUser({ 89 | id: 1, 90 | }); 91 | ``` 92 | 93 | ### [#](#interceptors) Interceptors 94 | 95 | ```js 96 | import fetchApiCall from 'fetch-api-call'; 97 | 98 | const { api, interceptors } = fetchApiCall({ 99 | protocol: process.env.API_PROTOCOL, 100 | host: process.env.API_HOST, 101 | }); 102 | ``` 103 | 104 | **interceptors** methods: 105 | 106 | * _addRequestInterceptor(request)_ 107 | * _addResponseInterceptor(response)_ 108 | * _addErrorInterceptor(error)_ 109 | 110 | Each methods return function to remove interceptor. 111 | 112 | Iterceptors will be called for all request done with _api_. 113 | 114 | ```js 115 | // user login api 116 | const login = api('POST /login'); 117 | let removeAcessTokenIterceptor = null; 118 | 119 | login({}).then(({ acessToken }) => { 120 | removeAcessTokenIterceptor = iterceptors.addRequestInterceptor(request => { 121 | request.headers.set('Acess-Token', acessToken); 122 | return request; 123 | }); 124 | }); 125 | 126 | // user logout api 127 | const logout = api('POST /logout'); 128 | logout().then(() => { 129 | removeAcessTokenIterceptor(); 130 | }); 131 | 132 | // get user api 133 | const getUser = api('GET /user/:id'); 134 | getUser({ 135 | id: 1, 136 | }); 137 | ``` 138 | 139 | ### [#](#request-headers) Request Headers 140 | 141 | This is useful when using `fetch-api-call` on NodeJS. In some cases it is not 142 | possible to use interceptors to set auth headers becasue it will work for all 143 | request. We should specify certain request with certain header. 144 | 145 | ```js 146 | const getStatistics = api('GET /statistics'); 147 | getStatistics( 148 | {}, 149 | { 150 | headers: { 151 | MyCustomHeader: '123zxc', 152 | }, 153 | } 154 | ); 155 | ``` 156 | 157 | ### [#](#multiple-api-instances) Multiple Api Instances 158 | 159 | Very useful if there is microservices backend architecture. 160 | 161 | ```js 162 | import fetchApiCall from 'fetch-api-call'; 163 | 164 | const Auth = fetchApiCall({ 165 | host: process.env.AUTH_API_HOST, 166 | }); 167 | 168 | const Users = fetchApiCall({ 169 | host: process.env.USERS_API_HOST, 170 | }); 171 | 172 | const Data = fetchApiCall({ 173 | host: process.env.DATA_API_HOST, 174 | prefix: 'v2', 175 | }); 176 | 177 | const login = Auth.api('POST /login').then(({ accessToken }) => { 178 | Users.interceptors.addRequest(res => { 179 | request.headers.set('Acess-Token', acessToken); 180 | return request; 181 | }); 182 | 183 | Data.interceptors.addRequest(res => { 184 | request.headers.set('Acess-Token', acessToken); 185 | return request; 186 | }); 187 | }); 188 | 189 | const loadStatisitcs = Data.api('GET /statisitcs'); 190 | loadStatisitcs({ 191 | filter: { 192 | //... 193 | }, 194 | }); 195 | ``` 196 | 197 | ### [#](#custom-setup) Custom Setup 198 | 199 | ```js 200 | import setup from 'fetch-api-call/setup'; 201 | import fetchAdapter from 'bivrost-fetch-adapter'; 202 | 203 | const createApi = setup({ 204 | headers: { 205 | 'Content-Type': 'application/json', 206 | }, 207 | mode: 'cors', 208 | adapter: fetchAdapter, 209 | interceptors: { 210 | resposne: res => res, 211 | request: req => req, 212 | error: err => err, 213 | }, 214 | }); 215 | 216 | const api = createApi({ 217 | protocol: process.env.API_PROTOCOL, 218 | host: process.env.API_HOST, 219 | }); 220 | 221 | const login = api('POST /login'); 222 | ``` 223 | 224 | Options: 225 | 226 | * _headers_ - default headers 227 | * _mode_ - default mode. More details at - 228 | https://developer.mozilla.org/en-US/docs/Web/API/Request/mode 229 | * _adapter_ - default HTTP adapter. See more at - 230 | https://tuchk4.github.io/bivrost/docs/adapters.html 231 | * _interceptors_ - initial interceptors. NOTE that this is NOT DEFAULT 232 | interceptors. 233 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Project is open for new ideas and features: 4 | 5 | * new adapters 6 | * new api functions 7 | * data source features 8 | * feedback is very matter 9 | 10 | ## Source code 11 | 12 | Source code is located in `/packages/${package}/src` directory. Before 13 | publishing to npm code is automatically transpiled using 14 | [babel](https://babeljs.io/) and npm `pre-publish` hook. 15 | 16 | ## Development 17 | 18 | Use `npm link` feature in your projects to get up to date local bivrost code and 19 | `yarn dev` to start watchers for code auto transpiling. 20 | 21 | ## Tests 22 | 23 | [jest](https://facebook.github.io/jest/) - painless javascript unit testing. 24 | 25 | * `yarn test` - execute tests + code coverage report 26 | * `yarn test-dev` start file system watchers 27 | 28 | ## Commands 29 | 30 | * `yarn run dev` - start watchers and transpile code with babel 31 | * `yarn run docs:watch` - start local server with 32 | [gitbook](https://toolchain.gitbook.com/) and generate html docs 33 | 34 | --- 35 | 36 | ## Our Pledge 37 | 38 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 39 | 40 | ## Our Standards 41 | 42 | Examples of behavior that contributes to creating a positive environment include: 43 | 44 | * Using welcoming and inclusive language 45 | * Being respectful of differing viewpoints and experiences 46 | * Gracefully accepting constructive criticism 47 | * Focusing on what is best for the community 48 | * Showing empathy towards other community members 49 | 50 | Examples of unacceptable behavior by participants include: 51 | 52 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 53 | * Trolling, insulting/derogatory comments, and personal or political attacks 54 | * Public or private harassment 55 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 56 | * Other conduct which could reasonably be considered inappropriate in a professional setting 57 | 58 | ## Our Responsibilities 59 | 60 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 63 | 64 | ## Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 67 | 68 | ## Enforcement 69 | 70 | The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 71 | 72 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 77 | 78 | [homepage]: http://contributor-covenant.org 79 | [version]: http://contributor-covenant.org/version/1/4/ 80 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Table of content 2 | 3 | * [Api enchantment](/docs/recipes/api-enchantment.md) 4 | * [Auto Invoke Methods](/docs/recipes/auto-invoke-methods.md) 5 | * [Data source immutable output](/docs/recipes/data-source-immutable.md) 6 | * [Data source type checking with tcomb](/docs/recipes/data-source-type-checking.md) 7 | * [CRUD Data source](/docs/recipes/generate-crud-methods.md) 8 | * [File upload](/docs/recipes/file-upload.md) 9 | * [Fixtures](/docs/recipes/fixtures.md) 10 | * [Default api response](/docs/recipes/default-response.md) 11 | * [Prepare request and process response](/docs/recipes/prepare-process.md) 12 | * [Data validation](/docs/recipes/validation.md) 13 | -------------------------------------------------------------------------------- /docs/recipes/api-enchantment.md: -------------------------------------------------------------------------------- 1 | # Api enchantment 2 | 3 | ## 4 | 5 | ### [#](#api-for-services) Create api for popular services 6 | 7 | For example twitter api: 8 | 9 | ```js 10 | // twitter-api.js 11 | import api from 'bivrost/http/api'; 12 | 13 | export default (authToken, config = {}) => { 14 | const adapter = fetchAdapter(); 15 | 16 | let twitterApi = api({ 17 | protocol: 'https:', 18 | host: 'api.twitter.com' 19 | prefix: '1.1', 20 | ...config, 21 | adapter: fetchAdapter({ 22 | headers: { 23 | ...config.headers, 24 | TwitterOAuth: authToken 25 | } 26 | }); 27 | }); 28 | 29 | // Returns the 20 most recent mentions (tweets containing a users’s @screen_name) 30 | // for the authenticating user. 31 | twitterApi.mentionsTimeline = apiInstance('GET /statuses/mentions_timeline'); 32 | 33 | // Returns a single Tweet, specified by the id parameter. 34 | twitterApi.show = apiInstance('GET /statuses/show/:id'); 35 | 36 | return twitterApi; 37 | } 38 | ``` 39 | 40 | Usage: 41 | 42 | ```js 43 | import createTwitterApi from 'twitter-api'; 44 | 45 | const twitterApi = createTwitterApi(AUTH_TOKEN); 46 | 47 | // manual call 48 | twitterApi('GET /statuses/mentions_timeline'); 49 | 50 | // shortcut call 51 | twitterApi.mentionsTimeline(); 52 | 53 | twitterApi.show({ 54 | id: TWEET_ID, 55 | }); 56 | ``` 57 | 58 | Same could be implemented for 59 | 60 | * Github - https://developer.github.com/v3/ 61 | * Open weather api - https://openweathermap.org/api 62 | * Slack chat - https://api.slack.com/ 63 | * Reddit - https://www.reddit.com/dev/api/ 64 | * Twitter - https://dev.twitter.com/rest/public 65 | * etc. 66 | 67 | ## 68 | 69 | ### [#](#group-api-backend) Group api by backend services 70 | 71 | Especially it is very useful for microservice architecture. 72 | 73 | * _src/data/api/auth.js_ - api for auth service service 74 | * _src/data/api/users.js_ - api for application users service 75 | * _src/data/api/notifications.js_ - api for notification service 76 | 77 | Also such api functions could be easily shared between other applications. 78 | -------------------------------------------------------------------------------- /docs/recipes/auto-invoke-methods.md: -------------------------------------------------------------------------------- 1 | # Auto Invoke Methods 2 | 3 | ```js 4 | import DataSource from 'bivrost/data/source'; 5 | 6 | class UsersDataSource extends DataSource { 7 | static api = { 8 | loadAll: api('GET /users'), 9 | load: api('GET /users/:id'), 10 | create: api('POST /users'), 11 | }; 12 | } 13 | 14 | const usersDataSource = new UsersDataSource(); 15 | 16 | // Generated methods: 17 | usersDataSource.invokeLoad({}); 18 | usersDataSource.invokeLoadAll({}); 19 | usersDataSource.invokeCreate({}); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/recipes/data-source-immutable.md: -------------------------------------------------------------------------------- 1 | # Data source immutable output 2 | 3 | [Immutable](https://facebook.github.io/immutable-js/) - Immutable.js provides 4 | many Persistent Immutable data structures including: List, Stack, Map, 5 | OrderedMap, Set, OrderedSet and Record. 6 | 7 | Make each methods response immutable: 8 | 9 | ```js 10 | class UserDataSource extends DataSource { 11 | static steps = ['api', 'immutable']; 12 | 13 | static immutable = response => Immutable.fromJSON(response); 14 | 15 | static api = { 16 | list: api('GET /users'), 17 | }; 18 | 19 | loadAll(params) { 20 | return this.invoke('list', params); 21 | } 22 | } 23 | ``` 24 | 25 | Use Immutable [Records](https://facebook.github.io/immutable-js/docs/#/Record) 26 | and [Maps](https://facebook.github.io/immutable-js/docs/#/Map): 27 | 28 | ```js 29 | import { Record, Map } from 'immutable'; 30 | 31 | const User = Record({ 32 | id: null, 33 | name: '', 34 | }); 35 | 36 | class UserDataSource extends DataSource { 37 | static steps = ['validate', 'api', 'model']; 38 | 39 | static api = { 40 | update: api('PUT /users/:id'), 41 | loadAll: api('GET /users'), 42 | }; 43 | 44 | static model = { 45 | update: response => new User(response), 46 | 47 | loadAll: response => { 48 | return response.reduce((map, user) => { 49 | map.set({ 50 | [user.id]: user, 51 | }); 52 | 53 | return map; 54 | }, Map()); 55 | }, 56 | }; 57 | 58 | update(user) { 59 | return this.invoke('update', user); 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/recipes/data-source-type-checking.md: -------------------------------------------------------------------------------- 1 | # Data source with type checking 2 | 3 | [tcomb](https://github.com/gcanti/tcomb) - is a library for Node.js and the 4 | browser which allows you to check the types of JavaScript values at runtime with 5 | a simple and concise syntax. It's great for Domain Driven Design and for adding 6 | safety to your internal code. 7 | 8 | Using _tcomb_ we could check _api_ input and output data in realtime and disable 9 | it for production env. 10 | 11 | ```js 12 | import DataSource from 'bivrost/data/source'; 13 | import tcomb from 'tcomb'; 14 | 15 | class UserDataSource extends DataSource { 16 | static steps = ['input', 'api', 'output']; 17 | 18 | static input = { 19 | user: tcomb.struct({ 20 | id: tcomb.Number, 21 | }), 22 | }; 23 | 24 | static output = { 25 | user: tcomb.struct({ 26 | id: tcomb.Number, 27 | name: tcomb.String, 28 | lastname: tcomb.maybe(tcomb.String), 29 | }), 30 | }; 31 | 32 | static api = { 33 | user: api('GET /user/:id'), 34 | }; 35 | 36 | loadUser({ id }) { 37 | return this.invoke('user', { 38 | id, 39 | }); 40 | } 41 | } 42 | 43 | const usersDataSource = new UserDataSource(); 44 | 45 | // OK 46 | usersDataSource.loadUser({ 47 | id: 1, 48 | }); 49 | 50 | // Throw exception that ID attribute must be a Number. 51 | usersDataSource.loadUser({ 52 | id: '1', 53 | }); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/recipes/default-response.md: -------------------------------------------------------------------------------- 1 | # Default api response 2 | 3 | NOTE: This is wrong to define default output because this is the server 4 | liability 5 | 6 | ```js 7 | class UsersDataSource extends DataSource { 8 | static steps = ['api', 'defaultResponse']; 9 | 10 | static api = { 11 | list: api('GET /users'), 12 | }; 13 | 14 | static defaultResponse = { 15 | list: users => (users ? users : []), 16 | }; 17 | 18 | loadAll(params) { 19 | return this.invoke('list', params); 20 | } 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/recipes/file-upload.md: -------------------------------------------------------------------------------- 1 | # File upload 2 | 3 | Use [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) to 4 | upload files. 5 | 6 | ```js 7 | import bivrostApi from 'bivrost/http/api'; 8 | import DataSource from 'bivrost/data/source'; 9 | import fetchAdapter from 'bivrost-fetch-adapter'; 10 | 11 | const api = bivrostApi({ 12 | host: 'localhost:3001', 13 | adapter: fetchAdapter(), 14 | }); 15 | 16 | class ImagesDataSource extends DataSource { 17 | static api = { 18 | save: api('POST /images', { 19 | headers: { 20 | 'content-type': 'application/x-www-form-urlencoded', 21 | }, 22 | }), 23 | }; 24 | 25 | saveImage(images) { 26 | return this.invoke('save', images); 27 | } 28 | } 29 | 30 | const formData = new FormData(); 31 | formData.append('image', imageUploadInput.files[0]); 32 | 33 | const imagesDataSource = new ImagesDataSource(); 34 | imagesDataSource.saveImage(formData).then(response => { 35 | console.log('images are saved'); 36 | }); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/recipes/fixtures.md: -------------------------------------------------------------------------------- 1 | # Fixtures 2 | 3 | Change invoke steps according to current env 4 | 5 | ```js 6 | import ALL_USERS_FIXTURE form 'fixtures/all-users'; 7 | 8 | class UserDataSource extends DataSource { 9 | 10 | static steps = process.env.NODE_ENV === 'development' 11 | ? ['fixture', 'immutable'] 12 | : ['api', 'immutable']; 13 | 14 | immutable = response => Immutable.fromJSON(response); 15 | 16 | static fixture = { 17 | loadAll: ALL_USERS_FIXTURE 18 | }; 19 | 20 | static api = { 21 | loadAll: api('GET /users') 22 | }; 23 | 24 | loadAll(params) { 25 | return this.invoke('loadAll', params); 26 | } 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/recipes/generate-crud-methods.md: -------------------------------------------------------------------------------- 1 | # Generate CRUD methods 2 | 3 | * [Create data source with CRUD method](#create-crud-ds) 4 | * [Extends CRUD data source](#extends-crud-ds) 5 | 6 | Crud data source factory: 7 | 8 | ```js 9 | import DataSource from 'bivrost/data/source'; 10 | 11 | export default (entity, api) => { 12 | const normalized = `${entity.toLowerCase()}s`; 13 | 14 | return class CrudDataSource extends DataSource { 15 | static api = { 16 | loadAll: api(`GET /${normalized}`), 17 | load: api(`GET /${normalized}/:id`), 18 | create: api(`POST /${normalized}`), 19 | update: api(`PUT /${normalized}/:id`), 20 | delete: api(`DELETE /${normalized}/:id`), 21 | }; 22 | 23 | load({ id }) { 24 | return this.invoke('load'); 25 | } 26 | 27 | loadAll({ filters }) { 28 | return this.invoke('loadAll', { filters }); 29 | } 30 | 31 | create({ user }) { 32 | return this.invoke('create', { user }); 33 | } 34 | 35 | update({ user }) { 36 | return this.invoke('update', { user }); 37 | } 38 | 39 | delete({ id }) { 40 | return this.invoke('delete', { id }); 41 | } 42 | }; 43 | }; 44 | ``` 45 | 46 | ## 47 | 48 | ### [#](#create-crud-ds) Create data source with CRUD methods 49 | 50 | ```js 51 | import bivrostApi from 'bivrost/http/api' 52 | import fetchAdapter from 'bivrost-fetch-adapter'; 53 | import createCrudDataSource from './create-crud-datasource.js'; 54 | 55 | const api = bivrostApi({ 56 | host: 'localhost', 57 | adapter: fetchAdapter() 58 | }); 59 | 60 | const UsersDataSource = createCrudDataSource('user', api) 61 | const users = new UsersDataSource(); 62 | 63 | users.loadAll(); // GET /users 64 | 65 | users.load({ 66 | id: 1 67 | }); // GET /users/1 68 | 69 | users.create({ 70 | name: 'John Doe' 71 | }); // POST /users 72 | 73 | 74 | users.update({ 75 | id: 1 76 | name: 'John Doe' 77 | }); // PUT /users/1 78 | 79 | users.delete({ 80 | id: 1 81 | }); // DELETE /users/1 82 | ``` 83 | 84 | ## 85 | 86 | ### [#](#extends-crud-ds) Extends CRUD data source 87 | 88 | ```js 89 | import tcomb from 'tcomb'; 90 | 91 | import bivrostApi from 'bivrost/http/api'; 92 | import fetchAdapter from 'bivrost-fetch-adapter'; 93 | import createCrudDataSource from './create-crud-datasource.js'; 94 | 95 | const api = bivrostApi({ 96 | host: 'localhost', 97 | adapter: fetchAdapter(), 98 | }); 99 | 100 | const UsersDataSource = createCrudDataSource('user', api); 101 | 102 | class EnchantedUsers extends UsersDataSource { 103 | static api = { 104 | ...UsersDataSource.api, 105 | ping: api('GET /users/:id/ping'), 106 | }; 107 | 108 | ping({ id }) { 109 | return this.invoke('ping', { id }); 110 | } 111 | 112 | create({ user }) { 113 | return super.create({ user }).then(user => { 114 | console.log('user was create'); 115 | 116 | return user; 117 | }); 118 | } 119 | } 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/recipes/prepare-process.md: -------------------------------------------------------------------------------- 1 | # Prepare request and process response 2 | 3 | Response processing in some cases are important to get more usable format 4 | according to current application. 5 | 6 | For example - auto add users avatars using http://placehold.it service. 7 | 8 | ```js 9 | class UsersDataSource extends DataSource { 10 | static steps = ['api', 'process']; 11 | 12 | static api = { 13 | list: api ('GET /users') 14 | }; 15 | 16 | static process = { 17 | list: users => users.map(user => { 18 | return { 19 | ...user, 20 | avatar: user.avatar ? user.avatar : 'http://placehold.it/30x30' 21 | } 22 | }); 23 | }; 24 | 25 | loadAll(params) { 26 | return this.invoke('list', params); 27 | } 28 | } 29 | ``` 30 | 31 | Prepare requests is very useful for requests params transformation 32 | (serialization): covert form camelCase to snake_case, rename some props, auto 33 | adding props etc. 34 | 35 | ```js 36 | class UsersDataSource extends DataSource { 37 | static steps = ['prepare', 'api', 'process']; 38 | 39 | static prepare = { 40 | list: params => ({ 41 | l: [params.limit, params.offset], 42 | o: params.order, 43 | }), 44 | }; 45 | 46 | static api = { 47 | list: api('GET /users'), 48 | }; 49 | 50 | loadAll(params) { 51 | return this.invoke('list', params); 52 | } 53 | } 54 | const usersDataSource = new UsersDataSource(); 55 | usersDataSource.loadAll({ 56 | limit: 100, 57 | offset: 2, 58 | order: 'id', 59 | }); // GET /users?l=100,2&o=id 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/recipes/validation.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | Throw error or return rejected promise at any step. There are some useful 4 | libraries for data validation: 5 | 6 | * [tcomb](https://github.com/gcanti/tcomb) - Type checking and DDD. There is 7 | additional example with tcomb - 8 | [Data source type checking with tcomb](/docs/recipes/data-source-type-checking.md) 9 | * [Joi](https://www.npmjs.com/package/joi) - Obejct schema validation 10 | * [Validator](https://github.com/chriso/validator.js) - String validation and 11 | sanitization 12 | 13 | ```js 14 | import ALL_USERS_FIXTURE form 'fixtures/all-users'; 15 | 16 | class UserDataSource extends DataSource { 17 | 18 | static steps = ['validation', 'api']; 19 | 20 | static validation = { 21 | create: user => { 22 | if (!user.id) { 23 | // throw new Error('"id" is required'); 24 | return Promise.reject('"id" is required'); 25 | } 26 | 27 | return user; 28 | } 29 | } 30 | 31 | static api = { 32 | loadAll: api('GET /users'), 33 | create: api('POST /users') 34 | }; 35 | 36 | loadAll(params) { 37 | return this.invoke('loadAll', params); 38 | } 39 | 40 | create(user) { 41 | return this.invoke('create', user); 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | ## Mock data source steps and methods 2 | 3 | Suggest to use [jest](https://facebook.github.io/jest/) for testing. It is 4 | really painless javascript unit testing library with code coverage and perfects 5 | mocks. 6 | 7 | Example data source: 8 | 9 | ```js 10 | export default class UsersDataSource extends DataSource { 11 | static steps = ['serialize', 'api']; 12 | 13 | static serialize = { 14 | loadAll: ({ groupId }) => ({ 15 | g: groupId, 16 | }), 17 | }; 18 | 19 | static api = { 20 | loadAll: api('GET /users'), 21 | }; 22 | 23 | loadAll(props) { 24 | return this.invoke(props); 25 | } 26 | } 27 | ``` 28 | 29 | Example data source test: 30 | 31 | ```js 32 | import UsersDataSource from 'data/sources/users'; 33 | import mockDataSource from 'bivrost/utils/mock-data-source'; 34 | 35 | describe('Datasource steps mock', () => { 36 | let ds = null; 37 | 38 | beforeEach(() => { 39 | ds = new UsersDataSource(); 40 | }); 41 | 42 | it('should mock ds steps', () => { 43 | const mocks = mockDataSource(ds, step => jest.fn(step)); 44 | 45 | ds 46 | .loadAll({ 47 | groupId: 5, 48 | }) 49 | .then(() => { 50 | expect(mocks.serialize.loadAll.mock.calls.length).toEqual(1); 51 | expect(mocks.serialize.loadAll.mock.calls[0][0]).toEqual({ 52 | groupId: 5, 53 | }); 54 | 55 | expect(mocks.api.loadAll.mock.calls[0][0]).toEqual({ 56 | g: 5, 57 | }); 58 | }); 59 | }); 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /example/weather-axios-tcomb/index.js: -------------------------------------------------------------------------------- 1 | import t from 'tcomb'; 2 | import axios from 'axios'; 3 | import axiosAdapter from 'bivrost-axios-adapter'; 4 | import api from 'bivrost/http/api'; 5 | import DataSource from 'bivrost/data/source'; 6 | import { TWeatherForecast, TWeatherCurrent } from './model'; 7 | 8 | //setup Api client 9 | const weatherApi = api({ 10 | base: 'http://api.openweathermap.org', //server 11 | prefix: '/data/2.5/', //path to API root 12 | adapter: axiosAdapter(axios), //HTTP client adapter 13 | }); 14 | 15 | class WeatherDataSource extends DataSource { 16 | steps = ['input', 'api', 'output']; 17 | 18 | cache = { 19 | dailyForecast: { 20 | enabled: true, //The results of `dailyForecast` method call will be cached 21 | ttl: 60 * 60 * 1000, //for an hour. 22 | isGlobal: true, //Share the same cache for all instances of WeatherDataSource. (default - no) 23 | }, 24 | current: { 25 | enabled: true, 26 | ttl: 15 * 60 * 1000, 27 | isGlobal: true, 28 | }, 29 | }; 30 | 31 | input = { 32 | dailyForecast: t.struct({ q: t.Str }), //input data is checked against tcomb structure 33 | current: t.struct({ q: t.Str }), 34 | }; 35 | 36 | api = { 37 | dailyForecast: weatherApi('GET /forecast/daily'), 38 | current: weatherApi('GET /weather'), 39 | }; 40 | 41 | output = { 42 | dailyForecast: TWeatherForecast, //output data is checked against tcomb structure 43 | current: TWeatherCurrent, 44 | }; 45 | 46 | dailyForecast(city) { 47 | return this.invoke('dailyForecast', { q: city }); 48 | } 49 | 50 | current(city) { 51 | return this.invoke('current', { q: city }); 52 | } 53 | } 54 | 55 | //create dataSource 56 | var weather = new WeatherDataSource(); 57 | 58 | //call datasource methods 59 | function printWeatherForecast() { 60 | return weather 61 | .dailyForecast('Kiev') 62 | .then(forecast => console.log('WEATHER FORECAST:', forecast)) 63 | .catch(error => console.error(error)); 64 | } 65 | 66 | function printCurrentWeather() { 67 | return weather 68 | .current('Kiev') 69 | .then(current => console.log('CURRENT WEATHER:', current)) 70 | .catch(error => console.error(error)); 71 | } 72 | 73 | printWeatherForecast() 74 | .then(printCurrentWeather) 75 | .catch(error => console.error(error)); 76 | -------------------------------------------------------------------------------- /example/weather-axios-tcomb/model.js: -------------------------------------------------------------------------------- 1 | import t from 'tcomb'; 2 | 3 | const MaybeNum = t.maybe(t.Num); 4 | 5 | const TWeatherInfo = t.struct({ 6 | id: t.Num, 7 | main: t.Str, 8 | description: t.Str, 9 | icon: t.Str, 10 | }); 11 | 12 | const TForecastItem = t.struct({ 13 | dt: t.Num, 14 | temp: t.struct({ 15 | day: MaybeNum, 16 | min: MaybeNum, 17 | max: MaybeNum, 18 | night: MaybeNum, 19 | eve: MaybeNum, 20 | morn: MaybeNum, 21 | }), 22 | pressure: MaybeNum, 23 | humidity: MaybeNum, 24 | weather: t.list(TWeatherInfo), 25 | speed: MaybeNum, 26 | deg: MaybeNum, 27 | clouds: MaybeNum, 28 | rain: MaybeNum, 29 | }); 30 | 31 | const TLatLon = t.struct({ 32 | lon: t.Num, 33 | lat: t.Num, 34 | }); 35 | 36 | const TCity = t.struct({ 37 | id: t.Num, 38 | name: t.Str, 39 | coord: TLatLon, 40 | country: t.Str, 41 | population: t.Num, 42 | }); 43 | 44 | export const TWeatherForecast = t.struct({ 45 | cod: t.Str, 46 | message: t.Num, 47 | city: TCity, 48 | cnt: t.Num, 49 | list: t.list(TForecastItem), 50 | }); 51 | 52 | export const TWeatherCurrent = t.struct({ 53 | coord: TLatLon, 54 | weather: t.list(TWeatherInfo), 55 | sys: t.struct({ 56 | message: t.Num, 57 | country: t.Str, 58 | sunrise: t.Num, 59 | sunset: t.Num, 60 | }), 61 | base: t.Str, 62 | 63 | main: t.struct({ 64 | temp: MaybeNum, 65 | temp_min: MaybeNum, 66 | temp_max: MaybeNum, 67 | pressure: MaybeNum, 68 | sea_level: MaybeNum, 69 | grnd_level: MaybeNum, 70 | humidity: MaybeNum, 71 | }), 72 | wind: t.Obj, 73 | clouds: t.Obj, 74 | rain: t.Obj, 75 | dt: t.Num, 76 | id: t.Num, 77 | name: t.Str, 78 | cod: t.Num, 79 | }); 80 | -------------------------------------------------------------------------------- /example/weather-fetch-adapter/index.js: -------------------------------------------------------------------------------- 1 | import fetchAdapter from 'bivrost-fetch-adapter'; // native browser's fetch method 2 | import api from 'bivrost/http/api'; 3 | import DataSource from 'bivrost/data/source'; 4 | 5 | //setup Api client 6 | const weatherApi = api({ 7 | base: 'http://api.openweathermap.org', //server 8 | prefix: '/data/2.5/', //path to API root 9 | adapter: fetchAdapter(), //HTTP client adapter 10 | }); 11 | 12 | class WeatherDataSource extends DataSource { 13 | steps = ['input', 'api', 'output']; 14 | 15 | cache = { 16 | dailyForecast: { 17 | enabled: true, //The results of `dailyForecast` method call will be cached 18 | ttl: 60 * 60 * 1000, //for an hour. 19 | isGlobal: true, //Share the same cache for all instances of WeatherDataSource. (default - no) 20 | }, 21 | current: { 22 | enabled: true, 23 | ttl: 15 * 60 * 1000, 24 | isGlobal: true, 25 | }, 26 | }; 27 | 28 | api = { 29 | dailyForecast: weatherApi('GET /forecast/daily'), 30 | current: weatherApi('GET /weather'), 31 | }; 32 | 33 | dailyForecast(city) { 34 | return this.invoke('dailyForecast', { q: city }); 35 | } 36 | 37 | current(city) { 38 | return this.invoke('current', { q: city }); 39 | } 40 | } 41 | 42 | //create dataSource 43 | var weather = new WeatherDataSource(); 44 | 45 | //call datasource methods 46 | function printWeatherForecast() { 47 | return weather 48 | .dailyForecast('Kiev') 49 | .then(forecast => console.log('WEATHER FORECAST:', forecast)) 50 | .catch(error => console.error(error)); 51 | } 52 | 53 | function printCurrentWeather() { 54 | return weather 55 | .current('Kiev') 56 | .then(current => console.log('CURRENT WEATHER:', current)) 57 | .catch(error => console.error(error)); 58 | } 59 | 60 | printWeatherForecast() 61 | .then(printCurrentWeather) 62 | .catch(error => console.error(error)); 63 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost", 3 | "version": "1.0.0", 4 | "description": 5 | "Bivrost allows to organize a simple interface to asynchronous APIs.", 6 | "keywords": ["data-source", "api", "fetch", "axios", "local-storage"], 7 | "author": "tuchk4 ", 8 | "scripts": { 9 | "dev": "lerna run dev --stream --parallel", 10 | "test": "jest", 11 | "test-dev": "jest --watch", 12 | "docs:clean": "rimraf _book", 13 | "docs:prepare": "gitbook install", 14 | "docs:build": 15 | "npm run docs:prepare && rm -rf _book && ./node_modules/.bin/gitbook build", 16 | "docs:watch": "npm run docs:prepare && ./node_modules/.bin/gitbook serve", 17 | "docs:publish": 18 | "npm run docs:build && cd _book && git init && git commit --allow-empty -m 'Update docs' && git checkout -b gh-pages && git add . && git commit -am 'Update docs' && git push git@github.com:tuchk4/bivrost gh-pages --force", 19 | "precommit": "lint-staged", 20 | "format": 21 | "prettier --trailing-comma es5 --single-quote --write 'packages/*/*.js' 'packages/*/!(node_modules|coverage)/**/*.js'" 22 | }, 23 | "jest": { 24 | "rootDir": "./packages", 25 | "setupFiles": ["./bivrost/__tests__/localstorage-mock.js"], 26 | "modulePathIgnorePatterns": [ 27 | "./bivrost/__tests__/http-bin-api.js", 28 | "./bivrost/__tests__/localstorage-mock.js" 29 | ] 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/tuchk4/bivrost.git" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.26.0", 37 | "babel-jest": "^21.2.0", 38 | "babel-polyfill": "^6.26.0", 39 | "babel-preset-es2015": "^6.3.13", 40 | "babel-preset-stage-0": "^6.3.13", 41 | "gitbook-cli": "^2.3.2", 42 | "gitbook-plugin-ga": "^2.0.0", 43 | "husky": "^0.14.3", 44 | "jest": "^21.2.1", 45 | "lerna": "^3.4.1", 46 | "lint-staged": "^5.0.0", 47 | "prettier": "^1.8.2" 48 | }, 49 | "license": "ISC", 50 | "bugs": { 51 | "url": "https://github.com/frankland/bivrost/issues" 52 | }, 53 | "homepage": "https://github.com/frankland/bivrost", 54 | "lint-staged": { 55 | "*.js": ["prettier --trailing-comma es5 --single-quote --write", "git add"] 56 | }, 57 | "dependencies": { 58 | "gitbook-plugin-anker-enable": "0.0.4", 59 | "gitbook-plugin-edit-link": "^2.0.2", 60 | "gitbook-plugin-github": "^2.0.0", 61 | "gitbook-plugin-prism": "^2.3.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /node_modules 5 | 6 | /coverage 7 | /index.js 8 | 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /__tests__ 5 | /npm-debug.log 6 | /node_modules 7 | /src 8 | /test 9 | /coverage 10 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/README.md: -------------------------------------------------------------------------------- 1 | # Bivrost axios adapter 2 | 3 | [![Build Status](https://api.travis-ci.org/tuchk4/bivrost-axios-adapter.svg?branch=master)](https://travis-ci.org/tuchk4/bivrost-axios-adapter) 4 | [![NPM Version](https://img.shields.io/npm/v/bivrost-axios-adapter.svg)](https://npmjs.org/package/bivrost-axios-adapter) 5 | 6 | Bivrost Adapter for [axios](https://github.com/mzabriskie/axios) function. 7 | 8 | ``` 9 | npm i --save axios bivrost-axios-adapter 10 | ``` 11 | 12 | ### Usage 13 | 14 | With [Bivrost](https://github.com/tuchk4/bivrost): 15 | 16 | ```js 17 | import DataSource from 'bivrost/data/source'; 18 | import bivrostApi from 'bivrost/http/api'; 19 | import axiosAdapter from 'bivrost-axios-adapter'; 20 | 21 | const api = bivrostApi({ 22 | host: 'localhost', 23 | adapter: axiosAdapter(), 24 | }); 25 | 26 | class UsersDataSource extends DataSource { 27 | static api = { 28 | loadAll: api('GET /users'), 29 | }; 30 | 31 | loadUsers(filters) { 32 | return this.invoke('loadAll', filters); 33 | } 34 | } 35 | ``` 36 | 37 | Direct calls: 38 | 39 | ```js 40 | import axiosAdapter from 'bivrost-axios-adapter'; 41 | 42 | const api = axiosAdapter({}); 43 | 44 | const options = { 45 | method: 'GET', 46 | query: { 47 | groupId: 10, 48 | }, 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | }; 53 | 54 | api('/users', options) 55 | .then(data => { 56 | // ... 57 | }) 58 | .catch(response => { 59 | // ... 60 | }); 61 | ``` 62 | 63 | ### Adapter options 64 | 65 | ```js 66 | import axiosAdapter from 'bivrost-axios-adapter'; 67 | 68 | const api = axiosAdapter({ 69 | // default options for each request with created adapter 70 | options: { 71 | withCredentials: false, 72 | xsrfCookieName: 'XSRF-TOKEN', 73 | }, 74 | }); 75 | ``` 76 | 77 | Available options: 78 | 79 | * transformRequest - allows changes to the request data before it is sent to the 80 | server 81 | * transformResponse - allows changes to the response data to be made before 82 | * headers - are custom headers to be sent 83 | * paramsSerializer - is an optional function in charge of serializing `params` 84 | * timeout - specifies the number of milliseconds before the request times out 85 | * withCredentials - indicates whether or not cross-site Access-Control requests 86 | * auth - indicates that HTTP Basic auth should be used, and supplies credentials 87 | * responseType - indicates the type of data that the server will respond with 88 | * xsrfCookieName - is the name of the cookie to use as a value for xsrf token 89 | * xsrfHeaderName - is the name of the http header that carries the xsrf token 90 | value 91 | 92 | More details - https://www.npmjs.com/package/axios#request-api 93 | 94 | ### Interceptors 95 | 96 | The main difference between axios-adapter interceptors and axios 97 | interceptorsis - is that interceptors could be specified for each adpater 98 | separately. 99 | 100 | ```js 101 | import axiosAdapter from 'bivrost-axios-adapter'; 102 | 103 | const api = axiosAdapter({ 104 | interceptors: { 105 | request: request => { 106 | // ... 107 | }, 108 | 109 | response: response => { 110 | // ... 111 | }, 112 | 113 | error: response => { 114 | // ... 115 | }, 116 | }, 117 | }); 118 | ``` 119 | 120 | Interceptor example: 121 | 122 | ```js 123 | import axiosAdapter from 'bivrost-axios-adapter'; 124 | 125 | const api = axiosAdapter({ 126 | interceptors: { 127 | response: httpResponse => httpResponse.data, 128 | error: httpErrorResponse => Promise.reject(httpErrorResponse), 129 | }, 130 | }); 131 | ``` 132 | 133 | --- 134 | 135 | [Bivrost](https://github.com/tuchk4/bivrost) allows to organize a simple 136 | interface to asyncronous APIs. 137 | 138 | #### Other adapters 139 | 140 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 141 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 142 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 143 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 144 | * [Save Blob Adapter Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 145 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/__tests__/adapter.js: -------------------------------------------------------------------------------- 1 | import axiosAdapter from '../src'; 2 | 3 | const httpbin = 'http://httpbin.org'; 4 | 5 | describe('Adapter', () => { 6 | const requestInterceptor = jest.fn(req => { 7 | return req; 8 | }); 9 | 10 | const responsetInterceptor = jest.fn(res => { 11 | return res; 12 | }); 13 | 14 | const axiosApi = axiosAdapter({ 15 | interceptors: { 16 | request: requestInterceptor, 17 | response: responsetInterceptor, 18 | }, 19 | }); 20 | 21 | const query = { 22 | foo: 'bar', 23 | }; 24 | 25 | it('GET request', () => { 26 | return axiosApi(`${httpbin}/get`, { 27 | method: 'GET', 28 | query: { 29 | ...query, 30 | }, 31 | }).then(response => { 32 | expect(response.data.args).toEqual({ 33 | ...query, 34 | }); 35 | 36 | expect(requestInterceptor.mock.calls.length).toEqual(1); 37 | expect(responsetInterceptor.mock.calls.length).toEqual(1); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-axios-adapter", 3 | "version": "3.0.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.17.1", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", 10 | "integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=", 11 | "requires": { 12 | "follow-redirects": "^1.2.5", 13 | "is-buffer": "^1.1.5" 14 | } 15 | }, 16 | "debug": { 17 | "version": "3.1.0", 18 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 19 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 20 | "requires": { 21 | "ms": "2.0.0" 22 | } 23 | }, 24 | "follow-redirects": { 25 | "version": "1.4.1", 26 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz", 27 | "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==", 28 | "requires": { 29 | "debug": "^3.1.0" 30 | } 31 | }, 32 | "is-buffer": { 33 | "version": "1.1.6", 34 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 35 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 36 | }, 37 | "ms": { 38 | "version": "2.0.0", 39 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 40 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-axios-adapter", 3 | "version": "3.2.2", 4 | "description": "Bivrost adapter for axios", 5 | "keywords": [ 6 | "bivrost", 7 | "data-source", 8 | "api", 9 | "axios", 10 | "local-storage", 11 | "fetch-api-call", 12 | "response", 13 | "request", 14 | "call steps" 15 | ], 16 | "main": "index.js", 17 | "scripts": { 18 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 19 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/tuchk4/bivrost.git" 24 | }, 25 | "author": "tuchk4 ", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/tuchk4/bivrost/issues" 29 | }, 30 | "homepage": "hhttps://github.com/tuchk4/bivrost", 31 | "dependencies": { 32 | "axios": "^0.17.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/src/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const DEFAULT_ADAPTER_OPTIONS = {}; 4 | 5 | const DEFAULT_ADAPTER_INTERCEPTORS = {}; 6 | 7 | export default function fetchAdapter({ interceptors = {}, ...options } = {}) { 8 | const adapterOptions = { 9 | ...DEFAULT_ADAPTER_OPTIONS, 10 | ...options, 11 | }; 12 | 13 | const adapterIntinterceptors = { 14 | ...DEFAULT_ADAPTER_INTERCEPTORS, 15 | ...interceptors, 16 | }; 17 | 18 | if (adapterIntinterceptors.request) { 19 | axios.interceptors.request.use(adapterIntinterceptors.request); 20 | } 21 | 22 | const responseInterceptors = []; 23 | if (adapterIntinterceptors.response) { 24 | responseInterceptors.push(adapterIntinterceptors.response); 25 | } 26 | 27 | if (adapterIntinterceptors.error) { 28 | if (!responseInterceptors.length) { 29 | responseInterceptors.push(res => res); 30 | } 31 | 32 | responseInterceptors.push(adapterIntinterceptors.error); 33 | } 34 | 35 | axios.interceptors.response.use(...responseInterceptors); 36 | 37 | return function(url, requestOptions = {}) { 38 | const config = { 39 | ...adapterOptions, 40 | ...requestOptions, 41 | headers: { 42 | ...(adapterOptions.headers || {}), 43 | ...(requestOptions.headers || {}), 44 | }, 45 | }; 46 | 47 | if (requestOptions.body) { 48 | config.data = requestOptions.body; 49 | } 50 | 51 | if (requestOptions.query) { 52 | config.params = requestOptions.query; 53 | } 54 | 55 | return axios({ 56 | ...config, 57 | url, 58 | method: requestOptions.method, 59 | }); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/bivrost-axios-adapter/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | axios@^0.17.1: 6 | version "0.17.1" 7 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" 8 | dependencies: 9 | follow-redirects "^1.2.5" 10 | is-buffer "^1.1.5" 11 | 12 | debug@^3.1.0: 13 | version "3.1.0" 14 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 15 | dependencies: 16 | ms "2.0.0" 17 | 18 | follow-redirects@^1.2.5: 19 | version "1.2.6" 20 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf" 21 | dependencies: 22 | debug "^3.1.0" 23 | 24 | is-buffer@^1.1.5: 25 | version "1.1.6" 26 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 27 | 28 | ms@2.0.0: 29 | version "2.0.0" 30 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 31 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | 5 | /node_modules 6 | /coverage 7 | /index.js 8 | 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /__tests__ 4 | /.idea 5 | /npm-debug.log 6 | /node_modules 7 | /src 8 | /coverage 9 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/README.md: -------------------------------------------------------------------------------- 1 | # Bivrost delay adapter 2 | 3 | Bivrost adapter enchantment to add request/respone delays for UI tests 4 | 5 | ``` 6 | yarn add bivrost-delay-adapter 7 | ``` 8 | 9 | ### Usage 10 | 11 | ```js 12 | import DataSource from 'bivrost/data/source'; 13 | import axiosAdapter from 'bivrost-axios-adapter'; 14 | import delayAdapter from 'bivrost-delay-adapter'; 15 | 16 | const delayedAxios = delayAdapter(axiosAdapter(), { 17 | request: 1000, // add 1sec delay before request 18 | response: 1000 // add 1sec delay after response 19 | error: 1000 // add 1sec delay after response with error 20 | }); 21 | 22 | const api = bivrostApi({ 23 | host: 'localhost', 24 | adapter: delayedAxios() 25 | }); 26 | 27 | class UsersDataSource extends DataSource { 28 | static api = { 29 | loadAll: api('GET /users') 30 | } 31 | 32 | loadUsers(filters) { 33 | return this.invoke('loadAll', filters); 34 | } 35 | } 36 | 37 | const usersDataSource = new UsersDataSource(); 38 | 39 | // Will load users with 1sec delay before request and 1sec delay after response 40 | usersDataSource.loadUsers().then(() => { 41 | console.log('done'); 42 | }); 43 | ``` 44 | 45 | --- 46 | 47 | [Bivrost](https://github.com/tuchk4/bivrost) allows to organize a simple 48 | interface to asyncronous APIs. 49 | 50 | #### Other adapters 51 | 52 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 53 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 54 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 55 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 56 | * [Save Blob Adapter Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 57 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/__tests__/adapter.js: -------------------------------------------------------------------------------- 1 | import delayAdapter from '../src'; 2 | 3 | jest.useFakeTimers(); 4 | 5 | describe('Delay adapter', () => { 6 | const resolveAdapter = jest.fn(() => Promise.resolve()); 7 | const rejectAdapter = jest.fn(() => Promise.reject()); 8 | 9 | const options = { 10 | all: { 11 | request: 100, 12 | response: 200, 13 | error: 300, 14 | }, 15 | withoutError: { 16 | request: 100, 17 | response: 200, 18 | }, 19 | onlyRequest: { 20 | request: 100, 21 | }, 22 | }; 23 | 24 | beforeEach(() => { 25 | setTimeout.mockClear(); 26 | }); 27 | 28 | it('resolve delay call', () => { 29 | const apiOptions = { 30 | ...options.all, 31 | }; 32 | 33 | const delayApi = delayAdapter(resolveAdapter, apiOptions); 34 | 35 | const p = delayApi().then(() => { 36 | expect(setTimeout.mock.calls.length).toBe(2); 37 | expect(setTimeout.mock.calls[1][1]).toBe(apiOptions.response); 38 | }); 39 | 40 | expect(setTimeout.mock.calls.length).toBe(1); 41 | expect(setTimeout.mock.calls[0][1]).toBe(apiOptions.request); 42 | 43 | // run request timer 44 | jest.runOnlyPendingTimers(); 45 | 46 | setImmediate(() => { 47 | // run response timer 48 | jest.runOnlyPendingTimers(); 49 | }); 50 | 51 | return p; 52 | }); 53 | 54 | it('reject delay call', () => { 55 | const apiOptions = { 56 | ...options.all, 57 | }; 58 | 59 | const delayApi = delayAdapter(rejectAdapter, apiOptions); 60 | 61 | const p = delayApi().then(() => { 62 | expect(setTimeout.mock.calls.length).toBe(2); 63 | expect(setTimeout.mock.calls[1][1]).toBe(apiOptions.error); 64 | }); 65 | 66 | expect(setTimeout.mock.calls.length).toBe(1); 67 | expect(setTimeout.mock.calls[0][1]).toBe(apiOptions.request); 68 | 69 | // run request timer 70 | jest.runAllTimers(); 71 | 72 | setImmediate(() => { 73 | // run response timer 74 | jest.runAllTimers(); 75 | }); 76 | 77 | return p; 78 | }); 79 | 80 | it('reject delay call without error timer', () => { 81 | const apiOptions = { 82 | ...options.withoutError, 83 | }; 84 | 85 | const delayApi = delayAdapter(rejectAdapter, apiOptions); 86 | 87 | const p = delayApi().then(() => { 88 | expect(setTimeout.mock.calls.length).toBe(2); 89 | expect(setTimeout.mock.calls[1][1]).toBe(apiOptions.response); 90 | }); 91 | 92 | expect(setTimeout.mock.calls.length).toBe(1); 93 | expect(setTimeout.mock.calls[0][1]).toBe(apiOptions.request); 94 | 95 | // run request timer 96 | jest.runOnlyPendingTimers(); 97 | 98 | setImmediate(() => { 99 | // run response timer 100 | jest.runOnlyPendingTimers(); 101 | }); 102 | 103 | return p; 104 | }); 105 | 106 | it('resolve delay call with only request timer', () => { 107 | const apiOptions = { 108 | ...options.onlyRequest, 109 | }; 110 | 111 | const delayApi = delayAdapter(rejectAdapter, apiOptions); 112 | 113 | const p = delayApi().then(() => { 114 | expect(setTimeout.mock.calls.length).toBe(1); 115 | }); 116 | 117 | expect(setTimeout.mock.calls.length).toBe(1); 118 | expect(setTimeout.mock.calls[0][1]).toBe(apiOptions.request); 119 | 120 | // run request timer 121 | jest.runOnlyPendingTimers(); 122 | 123 | return p; 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-delay-adapter", 3 | "version": "3.0.1", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-delay-adapter", 3 | "version": "3.2.2", 4 | "description": "Bivrost adapter enchantment to add request/respone delays for UI tests", 5 | "keywords": [ 6 | "bivrost", 7 | "data-source", 8 | "api", 9 | "fetch", 10 | "axios", 11 | "local-storage", 12 | "fetch-api-call", 13 | "response", 14 | "request", 15 | "call steps" 16 | ], 17 | "main": "index.js", 18 | "scripts": { 19 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 20 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/tuchk4/bivrost.git" 25 | }, 26 | "author": "tuchk4 ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/tuchk4/bivrost/issues" 30 | }, 31 | "homepage": "hhttps://github.com/tuchk4/bivrost" 32 | } 33 | -------------------------------------------------------------------------------- /packages/bivrost-delay-adapter/src/index.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_OPTIONS = { 2 | response: 0, 3 | request: 0, 4 | error: 0, 5 | }; 6 | 7 | export default function delayAdapter(adapter, options = {}) { 8 | const delayOptions = { 9 | ...DEFAULT_OPTIONS, 10 | ...options, 11 | }; 12 | 13 | const delayProxy = (isError, data) => { 14 | let delay = delayOptions.response; 15 | if (isError && delayOptions.error) { 16 | delay = delayOptions.error; 17 | } 18 | 19 | if (delay) { 20 | return new Promise(resolve => { 21 | setTimeout(() => { 22 | resolve(data); 23 | }, delay); 24 | }); 25 | } else { 26 | return data; 27 | } 28 | }; 29 | 30 | return function delayApi(...apiArguments) { 31 | const api = () => 32 | adapter(...apiArguments).then( 33 | delayProxy.bind(null, false), 34 | delayProxy.bind(null, true) 35 | ); 36 | 37 | if (delayOptions.request) { 38 | return new Promise(resolve => { 39 | setTimeout(() => { 40 | resolve(api()); 41 | }, delayOptions.request); 42 | }); 43 | } else { 44 | return api(); 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | 5 | /node_modules 6 | /index.js 7 | 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /__tests__ 4 | /.idea 5 | /npm-debug.log 6 | /node_modules 7 | /src 8 | /test 9 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/README.md: -------------------------------------------------------------------------------- 1 | # Bivrost fetch adapter 2 | 3 | Adapter for browser's native fetch function. 4 | 5 | ``` 6 | yarn add bivrost-fetch-adapter 7 | ``` 8 | 9 | ### Usage 10 | 11 | With [Bivrost](https://github.com/tuchk4/bivrost): 12 | 13 | ```js 14 | import DataSource from 'bivrost/data/source'; 15 | import bivrostApi from 'bivrost/http/api'; 16 | import fetchAdapter from 'bivrost-fetch-adapter'; 17 | 18 | const api = bivrostApi({ 19 | host: 'localhost', 20 | adapter: fetchAdapter(), 21 | }); 22 | 23 | class UsersDataSource extends DataSource { 24 | static api = { 25 | loadAll: api('GET /users'), 26 | }; 27 | 28 | loadUsers(filters) { 29 | return this.invoke('loadAll', filters); 30 | } 31 | } 32 | ``` 33 | 34 | Direct calls: 35 | 36 | ```js 37 | import fetchAdapter from 'bivrost-fetch-adapter'; 38 | 39 | const api = fetchAdapter(); 40 | 41 | const options = { 42 | method: 'GET', 43 | query: { 44 | groupId: 10, 45 | }, 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | }, 49 | }; 50 | 51 | api('/users', options) // /users?groupId=10 52 | .then(json => { 53 | // ... 54 | }) 55 | .catch(response => { 56 | // ... 57 | }); 58 | 59 | const options = { 60 | method: 'POST', 61 | body: { 62 | name: 'kek', 63 | }, 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | }, 67 | }; 68 | 69 | api('/user/1', options) 70 | .then(json => { 71 | // ... 72 | }) 73 | .catch(response => { 74 | // ... 75 | }); 76 | ``` 77 | 78 | ### Configuration 79 | 80 | ```js 81 | import fetchAdapter from 'bivrost-fetch-adapter'; 82 | 83 | const api = fetchAdapter({ 84 | // default options for each request with created adapter 85 | options: { 86 | mode: 'cors', 87 | redirect: 'follow', 88 | }, 89 | }); 90 | ``` 91 | 92 | Available options: 93 | 94 | * _headers_ - associated Headers object 95 | * _referrer_ - referrer of the request 96 | * _mode_ - cors, no-cors, same-origin 97 | * _credentials_ - should cookies go with the request? omit, same-origin 98 | * _redirect_ - follow, error, manual 99 | * _integrity_ - subresource integrity value 100 | * _cache_ - cache mode (default, reload, no-cache) 101 | 102 | ### Interceptors 103 | 104 | ```js 105 | import fetchAdapter from 'bivrost-fetch-adapter'; 106 | 107 | const api = fetchAdapter({ 108 | interceptors: { 109 | // takes Request instance as argument 110 | request: request => { 111 | // ... 112 | }, 113 | 114 | error: error => { 115 | // ... 116 | }, 117 | 118 | // takes Response instance as argument 119 | response: response => { 120 | // ... 121 | }, 122 | }, 123 | }); 124 | ``` 125 | 126 | * Request object documentation - 127 | https://developer.mozilla.org/en-US/docs/Web/API/Request 128 | * Response object documentation - 129 | https://developer.mozilla.org/en-US/docs/Web/API/Response 130 | 131 | NOTE: If there is a network error or another reason why the HTTP request 132 | couldn't be fulfilled, the fetch() promise will be rejected with a reference 133 | to that error. 134 | 135 | Interceptor example: 136 | 137 | ```js 138 | import fetchAdapter from 'bivrost-fetch-adapter'; 139 | 140 | const api = fetchAdapter({ 141 | interceptors: { 142 | request: request => { 143 | request.headers.set('Content-Type', 'application/json'); 144 | request.headers.set('access_token', Auth.accessToken); 145 | 146 | return request; 147 | }, 148 | 149 | error: error => Promise.reject(error); 150 | response: response => Promise.resolve(response) 151 | } 152 | }); 153 | ``` 154 | 155 | ### Fetch polyfill 156 | 157 | Github whatwg-fetch - https://github.com/github/fetch 158 | 159 | --- 160 | 161 | [Bivrost](https://github.com/tuchk4/bivrost) allows to organize a simple 162 | interface to asyncronous APIs. 163 | 164 | #### Other adapters 165 | 166 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 167 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 168 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 169 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 170 | * [Save Blob Adapter Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 171 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-fetch-adapter", 3 | "version": "3.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asynckit": { 8 | "version": "0.4.0", 9 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 10 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 11 | }, 12 | "combined-stream": { 13 | "version": "1.0.6", 14 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 15 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 16 | "requires": { 17 | "delayed-stream": "~1.0.0" 18 | } 19 | }, 20 | "delayed-stream": { 21 | "version": "1.0.0", 22 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 23 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 24 | }, 25 | "encoding": { 26 | "version": "0.1.12", 27 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 28 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 29 | "requires": { 30 | "iconv-lite": "~0.4.13" 31 | } 32 | }, 33 | "form-data": { 34 | "version": "2.3.2", 35 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 36 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 37 | "requires": { 38 | "asynckit": "^0.4.0", 39 | "combined-stream": "1.0.6", 40 | "mime-types": "^2.1.12" 41 | } 42 | }, 43 | "iconv-lite": { 44 | "version": "0.4.23", 45 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 46 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 47 | "requires": { 48 | "safer-buffer": ">= 2.1.2 < 3" 49 | } 50 | }, 51 | "is-stream": { 52 | "version": "1.1.0", 53 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 54 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 55 | }, 56 | "isomorphic-fetch": { 57 | "version": "2.2.1", 58 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 59 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 60 | "requires": { 61 | "node-fetch": "^1.0.1", 62 | "whatwg-fetch": ">=0.10.0" 63 | } 64 | }, 65 | "mime-db": { 66 | "version": "1.33.0", 67 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 68 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 69 | }, 70 | "mime-types": { 71 | "version": "2.1.18", 72 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 73 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 74 | "requires": { 75 | "mime-db": "~1.33.0" 76 | } 77 | }, 78 | "node-fetch": { 79 | "version": "1.7.3", 80 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 81 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 82 | "requires": { 83 | "encoding": "^0.1.11", 84 | "is-stream": "^1.0.1" 85 | } 86 | }, 87 | "qs": { 88 | "version": "6.5.2", 89 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 90 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 91 | }, 92 | "safer-buffer": { 93 | "version": "2.1.2", 94 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 95 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 96 | }, 97 | "whatwg-fetch": { 98 | "version": "2.0.4", 99 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", 100 | "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-fetch-adapter", 3 | "version": "3.2.2", 4 | "description": "Bivrost adapter using browser's native fetch function", 5 | "keywords": [ 6 | "bivrost", 7 | "data-source", 8 | "api", 9 | "fetch", 10 | "axios", 11 | "local-storage", 12 | "fetch-api-call", 13 | "response", 14 | "request", 15 | "call steps" 16 | ], 17 | "main": "index.js", 18 | "scripts": { 19 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 20 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/tuchk4/bivrost.git" 25 | }, 26 | "author": "tuchk4 ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/tuchk4/bivrost/issues" 30 | }, 31 | "homepage": "hhttps://github.com/tuchk4/bivrost", 32 | "dependencies": { 33 | "isomorphic-fetch": "2.2.1", 34 | "qs": "^6.5.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/src/index.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import 'isomorphic-fetch'; 3 | 4 | const DEFAULT_ADAPTER_OPTIONS = { 5 | queryFormat: { 6 | arrayFormat: 'brackets', 7 | }, 8 | }; 9 | 10 | const filterNullValues = obj => 11 | Object.keys(obj).reduce((map, key) => { 12 | if (obj[key]) { 13 | map[key] = obj[key]; 14 | } 15 | 16 | return map; 17 | }, {}); 18 | 19 | export default function fetchAdapter({ interceptors = {}, ...options } = {}) { 20 | const adapterOptions = { 21 | ...DEFAULT_ADAPTER_OPTIONS, 22 | ...options, 23 | }; 24 | 25 | return function(url, requestOptions = {}) { 26 | const config = { 27 | ...adapterOptions, 28 | ...requestOptions, 29 | headers: new Headers( 30 | filterNullValues({ 31 | ...(adapterOptions.headers || {}), 32 | ...(requestOptions.headers || {}), 33 | }) 34 | ), 35 | }; 36 | 37 | if (requestOptions.body) { 38 | if (requestOptions.body.forEach instanceof Function) { 39 | config.body = requestOptions.body; 40 | } else { 41 | config.body = JSON.stringify(requestOptions.body); 42 | } 43 | } 44 | 45 | let queryString = ''; 46 | if (requestOptions.query) { 47 | queryString = qs.stringify(requestOptions.query, config.queryFormat); 48 | } 49 | 50 | let request = new Request(`${url}${queryString ? `?${queryString}` : ''}`, { 51 | method: requestOptions.method, 52 | ...config, 53 | }); 54 | 55 | if (interceptors.request) { 56 | request = interceptors.request(request); 57 | } 58 | 59 | return fetch(request).then( 60 | response => { 61 | if (response.ok) { 62 | return interceptors.response 63 | ? interceptors.response(response) 64 | : response; 65 | } else { 66 | return interceptors.error 67 | ? interceptors.error(response) 68 | : Promise.reject(response); 69 | } 70 | }, 71 | error => { 72 | return interceptors.error 73 | ? interceptors.error(error) 74 | : Promise.reject(error); 75 | } 76 | ); 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /packages/bivrost-fetch-adapter/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | asynckit@^0.4.0: 6 | version "0.4.0" 7 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 8 | 9 | combined-stream@^1.0.5: 10 | version "1.0.5" 11 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 12 | dependencies: 13 | delayed-stream "~1.0.0" 14 | 15 | delayed-stream@~1.0.0: 16 | version "1.0.0" 17 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 18 | 19 | encoding@^0.1.11: 20 | version "0.1.12" 21 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 22 | dependencies: 23 | iconv-lite "~0.4.13" 24 | 25 | form-data@^2.3.1: 26 | version "2.3.1" 27 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" 28 | dependencies: 29 | asynckit "^0.4.0" 30 | combined-stream "^1.0.5" 31 | mime-types "^2.1.12" 32 | 33 | iconv-lite@~0.4.13: 34 | version "0.4.19" 35 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 36 | 37 | is-stream@^1.0.1: 38 | version "1.1.0" 39 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 40 | 41 | isomorphic-fetch@2.2.1: 42 | version "2.2.1" 43 | resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" 44 | dependencies: 45 | node-fetch "^1.0.1" 46 | whatwg-fetch ">=0.10.0" 47 | 48 | mime-db@~1.30.0: 49 | version "1.30.0" 50 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 51 | 52 | mime-types@^2.1.12: 53 | version "2.1.17" 54 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 55 | dependencies: 56 | mime-db "~1.30.0" 57 | 58 | node-fetch@^1.0.1: 59 | version "1.7.3" 60 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 61 | dependencies: 62 | encoding "^0.1.11" 63 | is-stream "^1.0.1" 64 | 65 | qs@^6.5.1: 66 | version "6.5.1" 67 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 68 | 69 | whatwg-fetch@>=0.10.0: 70 | version "2.0.3" 71 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" 72 | -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | 5 | /node_modules 6 | /index.js 7 | 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /__tests__ 5 | /npm-debug.log 6 | /node_modules 7 | /src 8 | /test 9 | -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/README.md: -------------------------------------------------------------------------------- 1 | # Bivrost localStorage adapter 2 | 3 | Bivrost adapter. Allows to work with localStorage as with REST backend 4 | 5 | ``` 6 | yarn add bivrost-local-storage-adapter 7 | ``` 8 | 9 | ## Usage 10 | 11 | With [Bivrost](https://github.com/tuchk4/bivrost): 12 | 13 | ```js 14 | import DataSource from 'bivrost/data/source'; 15 | import bivrostApi from 'bivrost/http/api'; 16 | import localStorageAdapter from 'bivrost-local-storage-adapter'; 17 | 18 | const api = bivrostApi({ 19 | adapter: localStorageAdapter({ 20 | namespace: 'my-application', 21 | }), 22 | }); 23 | 24 | class UsersDataSource extends DataSource { 25 | steps = ['api']; 26 | 27 | api = { 28 | loadAll: api('GET /users'), 29 | create: api('POST /users'), 30 | }; 31 | 32 | loadUsers() { 33 | return this.invoke('loadAll'); 34 | } 35 | 36 | createUser(user) { 37 | return this.invoke('create', user); 38 | } 39 | } 40 | ``` 41 | 42 | Direct calls: 43 | 44 | ```js 45 | import localStorageAdapter from 'bivrost-local-storage-adapter'; 46 | 47 | const localStorageAdapter = localStorageAdapter({ 48 | namespace: 'my-application', 49 | }); 50 | 51 | localStorageAdapter('/users/1', { 52 | method: 'POST', 53 | body: { 54 | name: 'John Doe', 55 | }, 56 | }).then(() => console.log('saved to localStorage')); 57 | 58 | localStorageAdapter('/users/1', { 59 | method: 'GET', 60 | }).then(user => { 61 | expect(user).toEqual({ 62 | name: 'John Doe', 63 | }); 64 | }); 65 | ``` 66 | 67 | --- 68 | 69 | [Bivrost](https://github.com/tuchk4/bivrost) allows to organize a simple 70 | interface to asyncronous APIs. 71 | 72 | #### Other adapters 73 | 74 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 75 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 76 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 77 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 78 | * [Save Blob Adapter Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 79 | -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-localstorage-adapter", 3 | "version": "3.0.1", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-localstorage-adapter", 3 | "version": "3.2.2", 4 | "description": "Bivrost adapter. Allows to work with localStorage as with REST backend", 5 | "keywords": [ 6 | "bivrost", 7 | "data-source", 8 | "api", 9 | "fetch", 10 | "axios", 11 | "local-storage", 12 | "fetch-api-call", 13 | "response", 14 | "request", 15 | "call steps" 16 | ], 17 | "main": "index.js", 18 | "scripts": { 19 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 20 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/tuchk4/bivrost.git" 25 | }, 26 | "author": "tuchk4 ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/tuchk4/bivrost/issues" 30 | }, 31 | "homepage": "hhttps://github.com/tuchk4/bivrost" 32 | } 33 | -------------------------------------------------------------------------------- /packages/bivrost-local-storage-adapter/src/index.js: -------------------------------------------------------------------------------- 1 | const load = namespace => { 2 | let data = localStorage.getItem(namespace); 3 | let processed = {}; 4 | 5 | if (data) { 6 | processed = JSON.parse(data); 7 | } 8 | 9 | return processed; 10 | }; 11 | 12 | const save = (namespace, data) => { 13 | var prepared = JSON.stringify(data); 14 | localStorage.setItem(namespace, prepared); 15 | }; 16 | 17 | export default function localStorageAdapter(config = {}) { 18 | const namespace = config.namespace; 19 | 20 | if (!namespace) { 21 | throw new Error( 22 | 'Bivrost localStorage adapter: namespace should be defined' 23 | ); 24 | } 25 | 26 | return function(url, requestOptions = {}) { 27 | let action = null; 28 | 29 | switch (requestOptions.method) { 30 | case 'GET': 31 | action = (id, query, body) => { 32 | return new Promise(resolve => { 33 | let db = load(namespace); 34 | 35 | resolve(db[id]); 36 | }); 37 | }; 38 | 39 | break; 40 | 41 | case 'POST' || 'PUT': 42 | action = (id, query, body) => { 43 | return new Promise(resolve => { 44 | let db = load(namespace); 45 | db[id] = body; 46 | save(namespace, db); 47 | 48 | resolve(db[id]); 49 | }); 50 | }; 51 | 52 | break; 53 | 54 | case 'DELETE': 55 | action = (id, query, body) => { 56 | return new Promise(resolve => { 57 | let db = load(namespace); 58 | 59 | delete db[id]; 60 | save(namespace, db); 61 | 62 | resolve(); 63 | }); 64 | }; 65 | 66 | break; 67 | 68 | default: 69 | throw new Error('method is not allowed'); 70 | } 71 | 72 | return action(url, requestOptions.query, requestOptions.body); 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | 5 | /node_modules 6 | /index.js 7 | 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /npm-debug.log 5 | /node_modules 6 | /src 7 | /test 8 | /__tests__ 9 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/README.md: -------------------------------------------------------------------------------- 1 | # Bivrost save blob adapter 2 | 3 | Bivrost adapter. Allows to save api resposne as blob. 4 | 5 | ``` 6 | yarn add bivrost-save-blob-adapter 7 | ``` 8 | 9 | ## Usage 10 | 11 | With [Bivrost](https://github.com/tuchk4/bivrost): 12 | 13 | ```js 14 | import DataSource from 'bivrost/data/source'; 15 | import bivrostApi from 'bivrost/http/api'; 16 | import axiosAdapter from 'bivrost-axios-adapter'; 17 | import saveBlobAdapter from 'bivrost-save-blob-adapter'; 18 | 19 | const api = bivrostApi({ 20 | host: 'localhost', 21 | adapter: saveBlobAdapter(axiosAdapter()), 22 | }); 23 | 24 | class UsersDataSource extends DataSource { 25 | steps = ['api']; 26 | 27 | api = { 28 | loadAll: api('GET /users'), 29 | }; 30 | 31 | saveUsersJSON(filters) { 32 | return this.invoke('loadAll', filters).then(download => { 33 | // call download function and generate filename 34 | download((url, params, response) => `users-${response.length}.json`); 35 | }); 36 | } 37 | } 38 | 39 | const usersDataSource = new UsersDataSource(); 40 | 41 | // Will save GET /users response as "users.json" file 42 | usersDataSource.saveUsersJSON().then(() => { 43 | console.log('file saved'); 44 | }); 45 | ``` 46 | 47 | Direct calls: 48 | 49 | ```js 50 | import axios from 'axios'; 51 | import axiosAdapter from 'bivrost-axios-adapter'; 52 | import saveBlobAdapter from 'bivrost-save-blob-adapter'; 53 | 54 | const axiosAdapter = axiosAdapter(axios); 55 | const saveBlob = saveBlobAdapter(axiosAdapter); 56 | 57 | const requestOptions = { 58 | method: 'GET', 59 | }; 60 | 61 | saveBlob('/report/exel', requestOptions).then(download => { 62 | download(() => 'exel.xls'); 63 | }); // browser will save and download response as file 64 | ``` 65 | 66 | --- 67 | 68 | [Bivrost](https://github.com/tuchk4/bivrost) allows to organize a simple 69 | interface to asyncronous APIs. 70 | 71 | #### Other adapters 72 | 73 | * [Fetch Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-fetch-adapter) 74 | * [Axios Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-axios-adapter) 75 | * [Delay Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-delay-adapter) 76 | * [LocalStorage Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-local-storage-adapter) 77 | * [Save Blob Adapter Adapter](https://github.com/tuchk4/bivrost/tree/master/packages/bivrost-save-blob-adapter) 78 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-save-blob-adapter", 3 | "version": "3.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "browser-filesaver": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/browser-filesaver/-/browser-filesaver-1.1.1.tgz", 10 | "integrity": "sha1-HDUbL1dvWwDx0p1EIcTAQo7uX6E=" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost-save-blob-adapter", 3 | "version": "3.2.2", 4 | "description": "Bivrost adapter. Allows to save api resposne as blob", 5 | "keywords": [ 6 | "bivrost", 7 | "data-source", 8 | "api", 9 | "fetch", 10 | "axios", 11 | "local-storage", 12 | "fetch-api-call", 13 | "response", 14 | "request", 15 | "call steps" 16 | ], 17 | "main": "index.js", 18 | "scripts": { 19 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 20 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/tuchk4/bivrost.git" 25 | }, 26 | "author": "tuchk4 ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/tuchk4/bivrost/issues" 30 | }, 31 | "homepage": "hhttps://github.com/tuchk4/bivrost", 32 | "dependencies": { 33 | "browser-filesaver": "^1.1.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/src/index.js: -------------------------------------------------------------------------------- 1 | import fileSaver from 'browser-filesaver'; 2 | 3 | const isFunction = func => 4 | func && {}.toString.call(func) === '[object Function]'; 5 | 6 | export default function saveBlobAdapter(adapter) { 7 | return (url, params) => { 8 | return adapter(url, params).then(response => { 9 | return getFileName => { 10 | const filename = getFileName(url, params, response); 11 | 12 | if (!filename) { 13 | throw new Error('Bivrost save blob adapter: Empty file name'); 14 | } 15 | 16 | const blob = new Blob([JSON.stringify(response)]); 17 | 18 | fileSaver.saveAs(blob, filename); 19 | 20 | return response; 21 | }; 22 | }); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/bivrost-save-blob-adapter/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | browser-filesaver@^1.1.1: 6 | version "1.1.1" 7 | resolved "https://registry.yarnpkg.com/browser-filesaver/-/browser-filesaver-1.1.1.tgz#1c351b2f576f5b00f1d29d4421c4c0428eee5fa1" 8 | -------------------------------------------------------------------------------- /packages/bivrost/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /_bookg 5 | /node_modules 6 | /data 7 | /http 8 | /coverage 9 | /utils 10 | 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | -------------------------------------------------------------------------------- /packages/bivrost/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /__tests__ 5 | /npm-debug.log 6 | /node_modules 7 | /coverage 8 | 9 | -------------------------------------------------------------------------------- /packages/bivrost/README.md: -------------------------------------------------------------------------------- 1 | # [Bivrost](http://tuchk4.github.io/bivrost/) 2 | 3 | Bivrost allows to organize a simple interface to asynchronous APIs. 4 | 5 | [![build status](https://img.shields.io/travis/tuchk4/bivrost/master.svg?style=flat-square)](https://travis-ci.org/tuchk4/bivrost) 6 | [![npm version](https://img.shields.io/npm/v/bivrost.svg?style=flat-square)](https://www.npmjs.com/package/bivrost) 7 | 8 | # Fetch API Call 9 | 10 | Easiet way to use fetch with interceptros and awesome api endpoint declaration. 11 | [bivrost/packages/fetch-api-call](https://github.com/tuchk4/bivrost/tree/master/packages/fetch-api-call) 12 | 13 | ## Bivrost 14 | 15 | The main idea of Bivrost is grouping several API methods into data-sources. 16 | 17 | [Bivrost full documentation and recipes](https://tuchk4.github.io/bivrost/) 18 | 19 | ## Installation 20 | 21 | ``` 22 | yarn add bivrost 23 | ``` 24 | 25 | ## The gist 26 | 27 | That’s it! Create api function for github api. 28 | 29 | ```js 30 | import api from 'bivrost/http/api' 31 | import fetchAdapter from 'bivrost-fetch-adapter'; 32 | 33 | const githubApi = api({ 34 | protocol: 'https:' 35 | host: 'api.github.com', 36 | adapter: fetchAdapter() 37 | }); 38 | 39 | //define API method 40 | const repositoryList = githubApi('GET /users/:user/repos'), 41 | 42 | //call API method 43 | repositoryList({ user: 'tuchk4' }) 44 | .then(repositories => console.log(repositories)); 45 | ``` 46 | 47 | Create data source that contain few github api methods (get repositories list 48 | and get repository info) and its invoke chain. 49 | 50 | ```js 51 | import DataSource from 'bivrost/data/source'; 52 | import githubApi from './github-api'; 53 | import tcomb from 'tcomb'; 54 | 55 | class GihtubRepositories extends DataSource { 56 | // define invoke method chain. Default chain is - ['api', 'process'] 57 | static steps = ['api', 'immutable']; 58 | 59 | // "define "api" step 60 | static api = { 61 | repos: githubApi('GET /users/:user/repos'), 62 | repoInfo: githubApi('GET /repos/:user/:repository'), 63 | }; 64 | 65 | // step function will be executed for each method 66 | static immutable = response => Immutable.fromJSON(response); 67 | 68 | // define data source public methods that invokes steps methods 69 | getRepositories(user) { 70 | return this.invoke('repos', { 71 | user, 72 | }); 73 | } 74 | 75 | getRepositoryInfo(user, repository) { 76 | return this.invoke('repoInfo', { 77 | user, 78 | repository, 79 | }); 80 | } 81 | } 82 | ``` 83 | 84 | Extends GihtubRepositories and define username. Now all requests will be done 85 | for facebook's github group. 86 | 87 | ```js 88 | import GihtubRepositories from './github-repositories'; 89 | 90 | const FACEBOOK_GITHUB_ACCOUNT = 'facebook'; 91 | 92 | class FacebookRepositories extends GihtubRepositories { 93 | getRepositories() { 94 | return super.getRepositories(FACEBOOK_GITHUB_ACCOUNT); 95 | } 96 | 97 | getRepositoryInfo(repository) { 98 | return super.getRepositoryInfo(FACEBOOK_GITHUB_ACCOUNT, repository); 99 | } 100 | } 101 | ``` 102 | 103 | ### [Contributing](docs/contributing.md) 104 | 105 | Project is open for new ideas and features: 106 | 107 | * new adapters 108 | * new api functions 109 | * data source features 110 | * feedback is very matter 111 | 112 | --- 113 | 114 | ## Docs 115 | 116 | * [Basics](/docs/basics/README.md) 117 | * [Api function](/docs/basics/api-function.md) 118 | * [Adapter](/docs/basics/adapter.md) 119 | * [Data source](/docs/basics/data-source.md) 120 | * [Recipes](/docs/recipes/README.md) 121 | * [Api enchantment](/docs/recipes/api-enchantment.md) 122 | * [Generate DataSource invoke functions](/docs/recipes/data-source-auto-invoke.md) 123 | * [Data source immutable output](/docs/recipes/data-source-immutable.md) 124 | * [Data source type checking (tcomb)](/docs/recipes/data-source-type-checking.md) 125 | * [CRUD Data source](/docs/recipes/generate-crud-methods.md) 126 | * [File upload](/docs/recipes/file-upload.md) 127 | * [Fixtures](/docs/recipes/fixtures.md) 128 | * [Default api response](/docs/recipes/default-response.md) 129 | * [Prepare request and process response](/docs/recipes/prepare-process.md) 130 | * [Testing](/docs/testing.md) 131 | * [Contributing](docs/contributing.md) 132 | 133 | #### Adapters 134 | 135 | * [Fetch adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-fetch-adapter) 136 | * [Axios adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-axios-adapter) 137 | * [Delay adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-delay-adapter) 138 | * [Local storage adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-local-storage-adapter) 139 | * [Save blob adapter adapter](https://github.com/tuchk4/bivrost/tree/master/packages/packages/bivrost-save-blob-adapter) 140 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/caches.js: -------------------------------------------------------------------------------- 1 | import DataSource from '../src/data/source'; 2 | 3 | describe('Cache', () => { 4 | it('default cache', () => { 5 | class DS1 extends DataSource { 6 | static defaultCache = { 7 | isGlobal: true, 8 | ttl: 1, 9 | }; 10 | 11 | static cache = { 12 | foo: { enabled: true }, 13 | }; 14 | } 15 | 16 | class DS2 extends DS1 { 17 | static defaultCache = { 18 | ttl: 10, 19 | }; 20 | } 21 | 22 | let ds2 = new DS2(); 23 | expect(ds2.getCache('foo').ttl).toBe(10); 24 | 25 | let ds1 = new DS1(); 26 | expect(ds1.getCache('foo').ttl).toBe(1); 27 | 28 | expect(ds1.constructor.defaultCache).toEqual({ 29 | ttl: 1, 30 | isGlobal: true, 31 | }); 32 | expect(DS1.defaultCache).toEqual({ 33 | ttl: 1, 34 | isGlobal: true, 35 | }); 36 | 37 | expect(ds2.constructor.defaultCache).toEqual({ 38 | ttl: 10, 39 | }); 40 | expect(DS2.defaultCache).toEqual({ 41 | ttl: 10, 42 | }); 43 | }); 44 | 45 | it('should be able to work per-class', () => { 46 | class DS extends DataSource { 47 | static cache = { 48 | foo: { 49 | isGlobal: true, 50 | enabled: true, 51 | }, 52 | }; 53 | } 54 | 55 | let ds0 = new DS(); 56 | let ds1 = new DS(); 57 | 58 | expect(ds0.getCache('foo')).toBe(ds1.getCache('foo')); 59 | }); 60 | 61 | it('should be able to work per-instance', () => { 62 | class DS extends DataSource { 63 | static cache = { 64 | foo: { 65 | isGlobal: false, 66 | enabled: true, 67 | }, 68 | }; 69 | } 70 | 71 | let ds0 = new DS(); 72 | let ds1 = new DS(); 73 | 74 | expect(ds0.getCache('foo')).not.toBe(ds1.getCache('foo')); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/classes.js: -------------------------------------------------------------------------------- 1 | import DataSource from '../src/data/source'; 2 | import httpBinApi from './http-bin-api'; 3 | 4 | function getClass() { 5 | class DS_A extends DataSource { 6 | static prepare = { 7 | post: req => ({ foo: req.Foo, bar: req.Bar }), 8 | get: req => ({ foo: req.Foo, bar: req.Bar }), 9 | }; 10 | 11 | static process = { 12 | post: res => JSON.parse(res.data.data), 13 | get: res => res.args, 14 | }; 15 | 16 | static cache = { 17 | get: { 18 | enabled: true, 19 | }, 20 | }; 21 | 22 | post(params) { 23 | return this.invoke('post', params); 24 | } 25 | 26 | get(params) { 27 | return this.invoke('get', params); 28 | } 29 | } 30 | 31 | class DS_B extends DS_A { 32 | highLevelMethod() { 33 | return this.post({ 34 | Foo: this.options.a, 35 | Bar: 200, 36 | }); 37 | } 38 | } 39 | 40 | class DS_C extends DS_B { 41 | static api = { 42 | post: httpBinApi('POST /post'), 43 | get: httpBinApi('GET /get'), 44 | }; 45 | } 46 | 47 | return DS_C; 48 | } 49 | 50 | describe('Classes', () => { 51 | it('should generate auto invoke methods', () => { 52 | const ds = new class extends DataSource { 53 | static api = { 54 | load_user: httpBinApi('POST /post'), 55 | }; 56 | }(); 57 | 58 | return ds 59 | .invokeLoadUser({ 60 | foo: 1, 61 | }) 62 | .then(res => { 63 | expect(res.data.json).toEqual({ 64 | foo: 1, 65 | }); 66 | }); 67 | }); 68 | 69 | it('should make request with empty obj', () => { 70 | const ds = new class extends DataSource { 71 | static api = { 72 | load_user: httpBinApi('POST /post'), 73 | }; 74 | }(); 75 | 76 | return ds.invokeLoadUser().then(res => { 77 | expect(res.data.json).toEqual({}); 78 | }); 79 | }); 80 | 81 | it('should pass context', () => { 82 | const processMock = jest.fn(props => ({ 83 | ...props.data.json, 84 | fromProcess: 3, 85 | })); 86 | 87 | const prepareMock = jest.fn(props => { 88 | return { 89 | ...props, 90 | fromPrepare: 2, 91 | }; 92 | }); 93 | 94 | const ds = new class extends DataSource { 95 | static prepare = { 96 | load_user: prepareMock, 97 | }; 98 | 99 | static process = { 100 | load_user: processMock, 101 | }; 102 | 103 | static api = { 104 | load_user: httpBinApi('POST /post'), 105 | }; 106 | }(); 107 | 108 | return ds.invokeLoadUser({ bar: 1 }, { foo: 1 }).then(res => { 109 | expect(res.fromProcess).toEqual(3); 110 | 111 | expect(prepareMock.mock.calls.length).toEqual(1); 112 | expect(processMock.mock.calls.length).toEqual(1); 113 | 114 | expect(prepareMock.mock.calls[0][1]).toEqual({ 115 | foo: 1, 116 | }); 117 | 118 | expect(processMock.mock.calls[0][1]).toEqual({ 119 | foo: 1, 120 | }); 121 | }); 122 | }); 123 | 124 | it('should done XHR request with correct result', () => { 125 | let DS = getClass(); 126 | 127 | let ds = new DS({ 128 | options: { 129 | a: 8889, 130 | }, 131 | }); 132 | 133 | return ds 134 | .highLevelMethod() 135 | .catch(e => e) 136 | .then(res => { 137 | expect(res).toEqual({ 138 | bar: 200, 139 | foo: 8889, 140 | }); 141 | }); 142 | }); 143 | 144 | it('should pass headers from context', () => { 145 | const loadUserMock = jest.fn(); 146 | 147 | const ds = new class extends DataSource { 148 | static api = { 149 | load_user: loadUserMock, 150 | }; 151 | }({ 152 | headers: { 153 | abc: 'zxc', 154 | }, 155 | }); 156 | 157 | return ds 158 | .invokeLoadUser({ 159 | foo: 1, 160 | }) 161 | .then(res => { 162 | expect(loadUserMock.mock.calls[0][0]).toEqual({ 163 | foo: 1, 164 | }); 165 | expect(loadUserMock.mock.calls[0][1]).toEqual({ 166 | headers: { 167 | abc: 'zxc', 168 | }, 169 | }); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/createRequestTemplate.js: -------------------------------------------------------------------------------- 1 | import createRequestTemplate from '../src/http/createRequestTemplate'; 2 | import api from '../src/http/api'; 3 | 4 | describe('Request template', () => { 5 | let getPOSTrequest = null; 6 | let getGETrequest = null; 7 | 8 | beforeEach(() => { 9 | getPOSTrequest = createRequestTemplate('POST /user/:id'); 10 | getGETrequest = createRequestTemplate('GET /user/:id'); 11 | }); 12 | 13 | it('stringify - 1', () => { 14 | const getUser = api({ 15 | host: 'localhost', 16 | })('GET /user/:id'); 17 | 18 | expect( 19 | getUser.stringify({ 20 | id: 1, 21 | }) 22 | ).toEqual('http://localhost/user/1'); 23 | }); 24 | 25 | it('stringify - 2', () => { 26 | const getUser = api({ 27 | host: 'localhost', 28 | })('GET /user/:id'); 29 | 30 | expect( 31 | getUser.stringify({ 32 | id: 1, 33 | group: 5, 34 | }) 35 | ).toEqual('http://localhost/user/1?group=5'); 36 | }); 37 | 38 | it('stringify - 3', () => { 39 | const getUser = api({ 40 | host: 'localhost', 41 | })('GET /user?:id&:group'); 42 | 43 | expect( 44 | getUser.stringify({ 45 | id: 1, 46 | group: 5, 47 | }) 48 | ).toEqual('http://localhost/user?id=1&group=5'); 49 | }); 50 | 51 | it('apply POST request', () => { 52 | const request = getPOSTrequest({ 53 | id: 1, 54 | name: 'Thor', 55 | }); 56 | 57 | expect(request).toEqual({ 58 | query: {}, 59 | path: '/user/1', 60 | method: 'POST', 61 | body: { 62 | name: 'Thor', 63 | }, 64 | }); 65 | }); 66 | 67 | it('apply GET request', () => { 68 | const request = getGETrequest({ 69 | id: 1, 70 | name: 'Thor', 71 | }); 72 | 73 | expect(request).toEqual({ 74 | query: { 75 | name: 'Thor', 76 | }, 77 | path: '/user/1', 78 | method: 'GET', 79 | }); 80 | }); 81 | 82 | it('should throw exception if not enough bound params', () => { 83 | expect(() => { 84 | getGETrequest({ 85 | name: 'Thor', 86 | }); 87 | }).toThrow(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/http-bin-api.js: -------------------------------------------------------------------------------- 1 | import axiosAdapter from 'bivrost-axios-adapter'; 2 | import api from '../src/http/api'; 3 | 4 | const httpBinApi = api({ 5 | protocol: 'https:', 6 | host: 'httpbin.org', 7 | adapter: axiosAdapter({ 8 | interceptors: { 9 | request: request => { 10 | return request; 11 | }, 12 | response: response => { 13 | return response; 14 | }, 15 | }, 16 | }), 17 | }); 18 | 19 | export default httpBinApi; 20 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/localstorage-mock.js: -------------------------------------------------------------------------------- 1 | // workaround for mocked window 2 | const localStorageMock = (function() { 3 | let store = {}; 4 | 5 | return { 6 | getItem: key => store[key] || null, 7 | setItem: (key, value) => { 8 | store[key] = value.toString(); 9 | }, 10 | clear: () => { 11 | store = {}; 12 | }, 13 | }; 14 | })(); 15 | 16 | Object.defineProperty(window, 'localStorage', { 17 | value: localStorageMock, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/mockDataSource.js: -------------------------------------------------------------------------------- 1 | import DataSource from '../src/data/source'; 2 | import mockDs from '../src/utils/mockDataSource'; 3 | 4 | class DS extends DataSource { 5 | static steps = ['serialize', 'api']; 6 | 7 | static serialize = { 8 | loadAll: ({ groupId }) => 9 | Promise.resolve({ 10 | g: groupId, 11 | }), 12 | }; 13 | 14 | static api = { 15 | loadAll: input => Promise.resolve(input), 16 | }; 17 | 18 | loadAll(props) { 19 | return this.invoke('loadAll', props); 20 | } 21 | } 22 | 23 | describe('Datasource steps mock', () => { 24 | let ds = null; 25 | 26 | beforeEach(() => { 27 | ds = new DS(); 28 | }); 29 | 30 | it('should mock ds steps', () => { 31 | const mocks = mockDs(ds, step => jest.fn(step)); 32 | 33 | ds 34 | .loadAll({ 35 | groupId: 5, 36 | }) 37 | .then(() => { 38 | expect(mocks.serialize.loadAll.mock.calls.length).toEqual(1); 39 | expect(mocks.serialize.loadAll.mock.calls[0][0]).toEqual({ 40 | groupId: 5, 41 | }); 42 | 43 | expect(mocks.api.loadAll.mock.calls[0][0]).toEqual({ 44 | g: 5, 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/promiseCache.js: -------------------------------------------------------------------------------- 1 | import promiseCache from '../src/utils/promiseCache'; 2 | import promiseDedup from '../src/utils/promiseDeduplicator'; 3 | 4 | import Cache from '../src/data/cache'; 5 | 6 | describe('Promise cache', () => { 7 | const promiseCreator = jest.fn(() => { 8 | return Promise.resolve('Odin'); 9 | }); 10 | 11 | it('should cached result as Promise', done => { 12 | const cache = new Cache(); 13 | const key = 123; 14 | 15 | const firstPromise = promiseCache(cache, key, promiseCreator); 16 | 17 | setImmediate(() => { 18 | const secondPromise = promiseCache(cache, key, promiseCreator); 19 | 20 | Promise.all([firstPromise, secondPromise]) 21 | .then(([firstResult, secondResult]) => { 22 | expect(firstResult).toEqual(secondResult); 23 | expect(promiseCreator.mock.calls.length).toBe(1); 24 | }) 25 | .then(done, done); 26 | }); 27 | }); 28 | 29 | it('should work with promise deduplicator', () => { 30 | const cache = new Cache(); 31 | const key = 123; 32 | 33 | const promiseCreator = jest.fn(() => { 34 | return Promise.resolve('Odin'); 35 | }); 36 | 37 | const dedupPromise = promiseDedup(key, promiseCreator); 38 | 39 | const firstPromise = promiseCache(cache, key, () => dedupPromise); 40 | const secondPromise = promiseCache(cache, key, () => dedupPromise); 41 | 42 | return Promise.all([firstPromise, secondPromise]).then( 43 | ([firstResult, secondResult]) => { 44 | expect(firstResult).toEqual(secondResult); 45 | expect(promiseCreator.mock.calls.length).toBe(1); 46 | } 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/bivrost/__tests__/promiseDeduplicator.js: -------------------------------------------------------------------------------- 1 | import promiseDedup from '../src/utils/promiseDeduplicator'; 2 | 3 | describe('Promise deduplicator', () => { 4 | const key1 = 123; 5 | const key2 = 456; 6 | 7 | const promiseCreator = () => { 8 | return Promise.resolve(); 9 | }; 10 | 11 | it('dedup promises with same kay should be cached', () => { 12 | const dedupPromise1 = promiseDedup(key1, promiseCreator); 13 | const dedupPromise2 = promiseDedup(key1, promiseCreator); 14 | 15 | expect(dedupPromise1).toBe(dedupPromise2); 16 | }); 17 | 18 | it('dedup promises wiht different keys should de different', () => { 19 | const dedupPromise1 = promiseDedup(key1, promiseCreator); 20 | const dedupPromise2 = promiseDedup(key2, promiseCreator); 21 | 22 | expect(dedupPromise1).not.toBe(dedupPromise2); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/bivrost/bivrost-3.2.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuchk4/bivrost/f6d6d5973bee77c02ee050dd34ed19a5f96b0a42/packages/bivrost/bivrost-3.2.4.tgz -------------------------------------------------------------------------------- /packages/bivrost/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost", 3 | "version": "3.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asynckit": { 8 | "version": "0.4.0", 9 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 10 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 11 | }, 12 | "axios": { 13 | "version": "0.17.1", 14 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", 15 | "integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=", 16 | "dev": true, 17 | "requires": { 18 | "follow-redirects": "^1.2.5", 19 | "is-buffer": "^1.1.5" 20 | } 21 | }, 22 | "bivrost-axios-adapter": { 23 | "version": "3.0.2", 24 | "resolved": "https://registry.npmjs.org/bivrost-axios-adapter/-/bivrost-axios-adapter-3.0.2.tgz", 25 | "integrity": "sha512-/G/XFZPhulpijAk4Wak1WufJFVX94L5kZhMigEnTK4r4N0ZOzMG5sAzPUVrPO/YGGCT9X0k6NM2QRvnWrgirFQ==", 26 | "dev": true, 27 | "requires": { 28 | "axios": "^0.17.1" 29 | } 30 | }, 31 | "combined-stream": { 32 | "version": "1.0.6", 33 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 34 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 35 | "requires": { 36 | "delayed-stream": "~1.0.0" 37 | } 38 | }, 39 | "debug": { 40 | "version": "3.1.0", 41 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 42 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 43 | "dev": true, 44 | "requires": { 45 | "ms": "2.0.0" 46 | } 47 | }, 48 | "delayed-stream": { 49 | "version": "1.0.0", 50 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 51 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 52 | }, 53 | "follow-redirects": { 54 | "version": "1.4.1", 55 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz", 56 | "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==", 57 | "dev": true, 58 | "requires": { 59 | "debug": "^3.1.0" 60 | } 61 | }, 62 | "form-data": { 63 | "version": "2.3.2", 64 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 65 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 66 | "requires": { 67 | "asynckit": "^0.4.0", 68 | "combined-stream": "1.0.6", 69 | "mime-types": "^2.1.12" 70 | } 71 | }, 72 | "is-buffer": { 73 | "version": "1.1.6", 74 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 75 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", 76 | "dev": true 77 | }, 78 | "lodash.camelcase": { 79 | "version": "4.3.0", 80 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 81 | "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" 82 | }, 83 | "mime-db": { 84 | "version": "1.33.0", 85 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 86 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 87 | }, 88 | "mime-types": { 89 | "version": "2.1.18", 90 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 91 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 92 | "requires": { 93 | "mime-db": "~1.33.0" 94 | } 95 | }, 96 | "ms": { 97 | "version": "2.0.0", 98 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 99 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 100 | "dev": true 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/bivrost/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bivrost", 3 | "version": "3.2.5", 4 | "main": "index.js", 5 | "description": 6 | "Bivrost allows to organize a simple interface to asynchronous APIs.", 7 | "keywords": [ 8 | "rest", 9 | "data-source", 10 | "api", 11 | "fetch", 12 | "axios", 13 | "local-storage", 14 | "fetch-api-call", 15 | "response", 16 | "request", 17 | "call steps" 18 | ], 19 | "scripts": { 20 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 21 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 22 | }, 23 | "devDependencies": { 24 | "bivrost-axios-adapter": "^3.2.2" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/tuchk4/bivrost.git" 29 | }, 30 | "author": "tuchk4 ", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/tuchk4/bivrost/issues" 34 | }, 35 | "homepage": "hhttps://github.com/tuchk4/bivrost", 36 | "dependencies": { 37 | "lodash.camelcase": "^4.3.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/bivrost/src/data/cache.js: -------------------------------------------------------------------------------- 1 | class CacheRecord { 2 | constructor(value, ttl) { 3 | this.value = value; 4 | this.exp = ttl + Date.now(); 5 | } 6 | 7 | isExpired(now = Date.now()) { 8 | return now > this.exp; 9 | } 10 | } 11 | 12 | export default class Cache { 13 | records = new Map(); 14 | 15 | constructor({ ttl = 60000 } = {}) { 16 | this.ttl = ttl; 17 | } 18 | 19 | get(id) { 20 | if (this.has(id)) { 21 | return this.records.get(id).value; 22 | } 23 | 24 | return null; 25 | } 26 | 27 | has(id) { 28 | let has = false; 29 | 30 | if (this.records.has(id)) { 31 | const record = this.records.get(id); 32 | 33 | if (record.isExpired()) { 34 | this.records.delete(id); 35 | has = false; 36 | } else { 37 | has = true; 38 | } 39 | } 40 | 41 | return has; 42 | } 43 | 44 | put(id, value, ttl = this.ttl) { 45 | const record = new CacheRecord(value, ttl); 46 | this.records.set(id, record); 47 | } 48 | 49 | clear(id) { 50 | if (id) { 51 | this.records.delete(id); 52 | } else { 53 | this.records = new Map(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/bivrost/src/data/source.js: -------------------------------------------------------------------------------- 1 | import camelCase from 'lodash.camelcase'; 2 | import promiseCache from '../utils/promiseCache'; 3 | import Cache from './cache'; 4 | 5 | const DEFAULT_STEPS = ['prepare', 'api', 'process']; 6 | const DEFAULT_METHOD_CACHE_CONFIG = { 7 | isGlobal: false, 8 | enabled: false, 9 | ttl: 60000, 10 | }; 11 | 12 | const isFunction = func => 13 | func && {}.toString.call(func) === '[object Function]'; 14 | 15 | const getMethodCacheName = (instance, method) => 16 | `${instance.constructor.uid}@${method}`; 17 | 18 | const buildCaches = instance => { 19 | const caches = new Map(); 20 | const cacheConfig = instance.constructor.cache || {}; 21 | const defaultCacheConfig = { 22 | ...DEFAULT_METHOD_CACHE_CONFIG, 23 | ...instance.constructor.defaultCache, 24 | }; 25 | 26 | for (let method of Object.keys(cacheConfig)) { 27 | const config = { 28 | ...defaultCacheConfig, 29 | ...cacheConfig[method], 30 | }; 31 | 32 | let cache = null; 33 | const cacheMehtodName = getMethodCacheName(instance, method); 34 | 35 | if (config.enabled) { 36 | if (config.isGlobal) { 37 | if (!instance.constructor.caches.hasOwnProperty(cacheMehtodName)) { 38 | instance.constructor.caches[cacheMehtodName] = new Cache(config); 39 | } 40 | cache = instance.constructor.caches[cacheMehtodName]; 41 | } else { 42 | cache = new Cache(config); 43 | } 44 | 45 | caches.set(cacheMehtodName, cache); 46 | } 47 | } 48 | 49 | return caches; 50 | }; 51 | 52 | const _steps = Symbol('steps'); 53 | const _caches = Symbol('caches'); 54 | const _debugLogs = Symbol('debug logs'); 55 | 56 | let uid = 1; 57 | 58 | export default class Source { 59 | static caches = []; 60 | 61 | constructor( 62 | { options = {}, steps = DEFAULT_STEPS, context = {}, headers = {} } = {} 63 | ) { 64 | if (!this.constructor.uid) { 65 | this.constructor.uid = uid++; 66 | } 67 | 68 | this.context = context; 69 | this.headers = headers; 70 | this.options = options; 71 | 72 | this[_steps] = this.constructor.steps || steps; 73 | this[_caches] = buildCaches(this); 74 | 75 | if (this.constructor.api) { 76 | for (const id of Object.keys(this.constructor.api)) { 77 | this[camelCase(`invoke ${id}`)] = this.invoke.bind(this, id); 78 | } 79 | } 80 | } 81 | 82 | getSteps() { 83 | return this[_steps]; 84 | } 85 | 86 | enableDebugLogs() { 87 | this[_debugLogs] = true; 88 | } 89 | 90 | disableDebugLogs() { 91 | this[_debugLogs] = false; 92 | } 93 | 94 | getCache(method) { 95 | const cacheMehtodName = getMethodCacheName(this, method); 96 | return this[_caches].get(cacheMehtodName); 97 | } 98 | 99 | invoke(method, params = {}, context = {}) { 100 | let log = null; 101 | 102 | if (this[_debugLogs]) { 103 | log = (...args) => { 104 | console.log('Bivrost --> ', ...args); 105 | }; 106 | 107 | log(`"${method}" call argumengts`, params, context); 108 | } 109 | 110 | const fn = (params, context) => { 111 | let stepsPromise = Promise.resolve(params); 112 | 113 | for (let stepId of this[_steps]) { 114 | let step = null; 115 | 116 | if (isFunction(stepId)) { 117 | step = stepId; 118 | } else { 119 | const stepConfig = this.constructor[stepId] || {}; 120 | step = isFunction(stepConfig) ? stepConfig : stepConfig[method]; 121 | 122 | if (!step) { 123 | continue; 124 | } 125 | } 126 | 127 | stepsPromise = stepsPromise.then(input => { 128 | if (log) { 129 | log(`"${stepId}" call`, input, context); 130 | } 131 | 132 | return Promise.resolve(null) 133 | .then(() => step(input, context)) 134 | .then(output => { 135 | if (log) { 136 | log(`"${stepId}" response`, output); 137 | } 138 | 139 | return output; 140 | }) 141 | .catch(error => { 142 | if (log) { 143 | log(`"${stepId}" error`, error); 144 | } 145 | 146 | return Promise.reject(error); 147 | }); 148 | }); 149 | } 150 | 151 | return stepsPromise; 152 | }; 153 | 154 | const cache = this.getCache(method); 155 | 156 | const methodContext = { 157 | ...this.context, 158 | ...context, 159 | }; 160 | 161 | const headers = { 162 | ...(this.context.headers || {}), 163 | ...(this.headers || {}), 164 | ...(context.headers || {}), 165 | }; 166 | 167 | if (Object.keys(headers).length) { 168 | methodContext.headers = headers; 169 | } 170 | 171 | if (!cache) { 172 | return fn(params, methodContext); 173 | } else { 174 | const key = this.getCacheKey(method, params, methodContext); 175 | 176 | if (log) { 177 | if (cache.has(key)) { 178 | log(`get from cache by key "${key}"`); 179 | } else { 180 | log('cache miss'); 181 | } 182 | } 183 | 184 | return promiseCache(cache, key, () => fn(params, methodContext)); 185 | } 186 | } 187 | 188 | getCacheKey(method, params = {}, context = {}) { 189 | return JSON.stringify(params); 190 | } 191 | 192 | clearCache(method, params) { 193 | let cacheKey = null; 194 | if (params) { 195 | cacheKey = this.getCacheKey(method, params); 196 | } 197 | 198 | let caches = method ? [this.getCache(method)] : this[_caches].values(); 199 | 200 | if (this[_debugLogs]) { 201 | if (cacheKey) { 202 | console.log(`Bivrost ---> clear cache for "${method}@${cacheKey}"`); 203 | } else { 204 | console.log(`Bivrost ---> clear all "${method}" caches`); 205 | } 206 | } 207 | 208 | for (let cache of caches) { 209 | if (cache) { 210 | cache.clear(cacheKey); 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /packages/bivrost/src/http/api.js: -------------------------------------------------------------------------------- 1 | import clientRequest from './clientRequest'; 2 | 3 | export const CLIENT_REQUEST_SETUP_ERROR = 'CLIENT_REQUEST_SETUP_ERROR'; 4 | 5 | function apiRequestTemplate(template, apiOptions, requestOptions) { 6 | const getRequestExecuteFunction = clientRequest(template, apiOptions); 7 | 8 | const apiRequest = function(params = {}, context = {}) { 9 | let error = null; 10 | let executeRequest = null; 11 | 12 | try { 13 | executeRequest = getRequestExecuteFunction(params); 14 | } catch (e) { 15 | error = { 16 | ok: false, 17 | message: e.message, 18 | type: CLIENT_REQUEST_SETUP_ERROR, 19 | error: e, 20 | }; 21 | } 22 | 23 | if (error) { 24 | return Promise.reject(error); 25 | } else { 26 | return executeRequest({ 27 | headers: { 28 | ...context.headers, 29 | ...requestOptions.headers, 30 | }, 31 | }); 32 | } 33 | }; 34 | 35 | apiRequest.displayName = `API: ${template}`; 36 | apiRequest.stringify = params => getRequestExecuteFunction.stringify(params); 37 | 38 | return apiRequest; 39 | } 40 | 41 | export default function api(apiOptions = {}) { 42 | return (template, requestOptions = {}) => 43 | apiRequestTemplate(template, apiOptions, requestOptions); 44 | } 45 | -------------------------------------------------------------------------------- /packages/bivrost/src/http/clientRequest.js: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import createRequestTemplate from './createRequestTemplate'; 3 | import promiseDeduplicator from '../utils/promiseDeduplicator'; 4 | 5 | const join = (...parts) => 6 | parts 7 | .join('/') 8 | .replace(/[\/]+/g, '/') 9 | .replace(/\/\?/g, '?') 10 | .replace(/\/\#/g, '#') 11 | .replace(/\:\//g, '://'); 12 | 13 | const buildUrl = (protocol = 'http:', host, prefix, path) => 14 | `${protocol}//${join(host, prefix, path)}`; 15 | 16 | export default function clientRequest(template, options = {}) { 17 | const getRequest = createRequestTemplate(template); 18 | 19 | const adapter = options.adapter; 20 | 21 | function getRequestExecuteFunction(params = {}) { 22 | const request = getRequest(params); 23 | const url = buildUrl( 24 | options.protocol, 25 | options.host, 26 | options.prefix, 27 | request.path 28 | ); 29 | 30 | return function executeRequest({ headers = {} } = {}) { 31 | const promiseCreator = () => 32 | adapter(url, { 33 | ...request, 34 | headers: { 35 | ...request.headers, 36 | ...headers, 37 | }, 38 | }); 39 | 40 | if (options.deduplicate && request.method === 'GET') { 41 | let dedupKey = JSON.stringify([request.method, url, request.query]); 42 | 43 | return promiseDeduplicator(dedupKey, promiseCreator); 44 | } else { 45 | return promiseCreator(); 46 | } 47 | }; 48 | } 49 | 50 | getRequestExecuteFunction.stringify = params => { 51 | const request = getRequest(params); 52 | 53 | const url = buildUrl( 54 | options.protocol, 55 | options.host, 56 | options.prefix, 57 | request.path 58 | ); 59 | 60 | const queryString = qs.stringify(request.query); 61 | return `${url}${queryString ? `?${queryString}` : ''}`; 62 | }; 63 | 64 | return getRequestExecuteFunction; 65 | } 66 | -------------------------------------------------------------------------------- /packages/bivrost/src/http/createRequestTemplate.js: -------------------------------------------------------------------------------- 1 | import Url from 'url'; 2 | 3 | const getUniqueBindings = (queryBindings, pathBindings) => 4 | new Set([...queryBindings, ...pathBindings]); 5 | 6 | const buildQuery = (queryBindings, queryDefaults, paramsMap) => { 7 | let query = {}; 8 | 9 | for (let def of queryDefaults) { 10 | query[def.key] = def.value; 11 | } 12 | 13 | for (let key of queryBindings) { 14 | if (paramsMap.has(key)) { 15 | query[key] = paramsMap.get(key); 16 | } 17 | } 18 | 19 | return query; 20 | }; 21 | 22 | const buildPath = (path, paramsMap) => 23 | path.replace(/:([\w\d]+)/g, (str, paramName) => { 24 | if (!paramsMap.has(paramName)) { 25 | throw new Error( 26 | `could not build path ("${path}") - param "${paramName}" does not exist` 27 | ); 28 | } 29 | 30 | return paramsMap.get(paramName); 31 | }); 32 | 33 | const buildUnboundParams = (exceptParamsSet, paramsMap) => { 34 | const newParams = {}; 35 | 36 | paramsMap.forEach((value, key) => { 37 | if (exceptParamsSet.has(key)) { 38 | return; 39 | } 40 | 41 | newParams[key] = value; 42 | }); 43 | 44 | return newParams; 45 | }; 46 | 47 | const fNot = f => a => !f(a); 48 | 49 | const getTemplateBinding = str => { 50 | let matches = str.match(/^:([\w\d]+)$/); 51 | return matches ? matches[1] : null; 52 | }; 53 | 54 | const extractQueryBindings = url => { 55 | return Object.keys(url.query) 56 | .map(getTemplateBinding) 57 | .filter(it => it !== null); 58 | }; 59 | 60 | const extractQueryDefaults = url => { 61 | return Object.keys(url.query) 62 | .filter(fNot(getTemplateBinding)) 63 | .map(it => [it, url.query[it]]); 64 | }; 65 | 66 | const extractPathBindings = url => { 67 | return url.pathname 68 | .split('/') 69 | .map(getTemplateBinding) 70 | .filter(it => it !== null); 71 | }; 72 | 73 | const extractMethodAndUrl = templateString => { 74 | const parts = templateString.match(/^([a-z]+)\s(.+)$/i); 75 | let method = 'GET'; 76 | let url = templateString; 77 | 78 | if (parts) { 79 | method = parts[1].toUpperCase(); 80 | url = parts[2]; 81 | } 82 | 83 | return [method, url]; 84 | }; 85 | 86 | const getParamsMap = params => { 87 | const paramsMap = new Map(); 88 | for (const key of Object.keys(params)) { 89 | paramsMap.set(key, params[key]); 90 | } 91 | 92 | return paramsMap; 93 | }; 94 | 95 | const parseRequestTemplate = templateString => { 96 | const [httpMethod, urlTemplate] = extractMethodAndUrl(templateString); 97 | 98 | const url = Url.parse(urlTemplate, true); 99 | const queryBindings = extractQueryBindings(url); 100 | const queryDefaults = extractQueryDefaults(url); 101 | const pathBindings = extractPathBindings(url); 102 | 103 | return { 104 | httpMethod, 105 | queryBindings, 106 | queryDefaults, 107 | pathBindings, 108 | path: url.pathname, 109 | }; 110 | }; 111 | 112 | const methodsWithBody = new Set(['POST', 'PUT', 'PATCH']); 113 | 114 | export default function getRequestTemplate(template) { 115 | const { 116 | httpMethod, 117 | queryBindings, 118 | queryDefaults, 119 | pathBindings, 120 | path, 121 | } = parseRequestTemplate(template); 122 | 123 | const uniqueBindings = getUniqueBindings(queryBindings, pathBindings); 124 | 125 | return function getRequest(params = {}) { 126 | const isIterable = params.forEach instanceof Function; 127 | 128 | let templateParams = null; 129 | 130 | if (isIterable) { 131 | params.forEach((value, key) => { 132 | templateParams[key] = value; 133 | }); 134 | } else { 135 | templateParams = params; 136 | } 137 | 138 | let paramsMap = getParamsMap(templateParams); 139 | 140 | let request = {}; 141 | 142 | let unboundQuery = {}; 143 | 144 | if (methodsWithBody.has(httpMethod)) { 145 | request.body = !isIterable 146 | ? buildUnboundParams(uniqueBindings, paramsMap) 147 | : params; 148 | } else { 149 | unboundQuery = !isIterable 150 | ? buildUnboundParams(uniqueBindings, paramsMap) 151 | : params; 152 | } 153 | 154 | return { 155 | ...request, 156 | query: { 157 | ...buildQuery(queryBindings, queryDefaults, paramsMap), 158 | ...unboundQuery, 159 | }, 160 | path: buildPath(path, paramsMap), 161 | method: httpMethod, 162 | }; 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /packages/bivrost/src/utils/mockDataSource.js: -------------------------------------------------------------------------------- 1 | const isFunction = func => 2 | func && {}.toString.call(func) === '[object Function]'; 3 | 4 | const mockDataSource = (ds, mockFn) => { 5 | const mocks = {}; 6 | 7 | for (let stepId of ds.getSteps()) { 8 | if (isFunction(stepId)) { 9 | ds.constructor[stepId] = mockFn(ds.constructor[stepId]); 10 | mocks[stepId] = ds.constructor[stepId]; 11 | } else { 12 | const stepConfig = ds.constructor[stepId] || {}; 13 | mocks[stepId] = {}; 14 | 15 | for (let methodId of Object.keys(stepConfig)) { 16 | stepConfig[methodId] = mockFn(stepConfig[methodId]); 17 | mocks[stepId][methodId] = stepConfig[methodId]; 18 | } 19 | } 20 | } 21 | 22 | return mocks; 23 | }; 24 | 25 | export default mockDataSource; 26 | -------------------------------------------------------------------------------- /packages/bivrost/src/utils/promiseCache.js: -------------------------------------------------------------------------------- 1 | export default function promiseCache(cache, key, fn, ttl) { 2 | return new Promise((resolve, reject) => { 3 | const previouslyCached = cache.get(key); 4 | 5 | if (null !== previouslyCached) { 6 | resolve(previouslyCached); 7 | return; 8 | } 9 | 10 | fn().then( 11 | result => { 12 | cache.put(key, result, ttl); 13 | resolve(result); 14 | }, 15 | err => { 16 | reject(err); 17 | } 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/bivrost/src/utils/promiseDeduplicator.js: -------------------------------------------------------------------------------- 1 | import Cache from '../data/cache'; 2 | 3 | /** 4 | * If the promise with given `key` is still running, it is returned instead of creating new one. 5 | * Useful for HTTP request deduplication. 6 | * 7 | * let promise1 = PromiseDeduplicator('http://example.com/', http.get.bind(http, 'http://example.com/')) 8 | * let promise2 = PromiseDeduplicator('http://example.com/', http.get.bind(http, 'http://example.com/')) 9 | * promise1 === promise2; 10 | */ 11 | const cacheMap = new Map(); 12 | 13 | export default function promiseDeduplicator(key, fnCreatePromise) { 14 | const cached = cacheMap.get(key); 15 | 16 | if (!cached) { 17 | const promise = fnCreatePromise().then( 18 | result => { 19 | cacheMap.delete(key); 20 | 21 | return result; 22 | }, 23 | error => { 24 | cacheMap.delete(key); 25 | 26 | return Promise.reject(error); 27 | } 28 | ); 29 | 30 | cacheMap.set(key, promise); 31 | return promise; 32 | } else { 33 | return cached; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/bivrost/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | asynckit@^0.4.0: 6 | version "0.4.0" 7 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 8 | 9 | combined-stream@1.0.6: 10 | version "1.0.6" 11 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" 12 | dependencies: 13 | delayed-stream "~1.0.0" 14 | 15 | delayed-stream@~1.0.0: 16 | version "1.0.0" 17 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 18 | 19 | form-data@^2.3.1: 20 | version "2.3.2" 21 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" 22 | dependencies: 23 | asynckit "^0.4.0" 24 | combined-stream "1.0.6" 25 | mime-types "^2.1.12" 26 | 27 | lodash.camelcase@^4.3.0: 28 | version "4.3.0" 29 | resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 30 | 31 | mime-db@~1.33.0: 32 | version "1.33.0" 33 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 34 | 35 | mime-types@^2.1.12: 36 | version "2.1.18" 37 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 38 | dependencies: 39 | mime-db "~1.33.0" 40 | -------------------------------------------------------------------------------- /packages/fetch-api-call/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /.idea 4 | /_book 5 | 6 | /node_modules 7 | /index.js 8 | /node.js 9 | /setup.js 10 | /createInterceptors.js 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* -------------------------------------------------------------------------------- /packages/fetch-api-call/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.git 3 | /__tests__ 4 | /.idea 5 | /npm-debug.log 6 | /node_modules 7 | /coverage 8 | /src 9 | 10 | -------------------------------------------------------------------------------- /packages/fetch-api-call/README.md: -------------------------------------------------------------------------------- 1 | # Fetch API Call 2 | 3 | Awesome fetch api for browser and nodejs with interceptors easy way to declare 4 | api endpoints. Provide very easy way to setup api for different 5 | backends/services. 6 | 7 | * [Advantages](#advantages) 8 | * [Api Declaration](#api-declaration) 9 | * [Interceptors](#interceptors) 10 | * [Request Headers](#request-headers) 11 | * [Custom Setup](#custom-setup) 12 | * [Bivrost Docs](http://tuchk4.github.io/bivrost/) 13 | 14 | --- 15 | 16 | ## Advantages 17 | 18 | * Support interceptors 19 | * Support multiple instances wiht its own interceptors 20 | * Easy way to define headers for specific requests 21 | * Declrative api end points 22 | 23 | ```js 24 | const login = api('POST /login'); 25 | login({}); 26 | 27 | const loadStatistics = api('GET /statistics'); 28 | loadStatistics({ 29 | filter: {}, 30 | }); 31 | 32 | const loadUser = api('GET /user/:id'); 33 | laodUser({ 34 | id: 1, 35 | }); 36 | 37 | 38 | // or even bind params to query 39 | const getUserDetails = api('POST /user/:id?:version'); 40 | getUser({ 41 | id: 1, 42 | data: {}, 43 | version: 2 44 | }); 45 | ``` 46 | 47 | ## Install 48 | 49 | ``` 50 | yarn add fetch-api-call 51 | ``` 52 | 53 | ## Api Declaration 54 | 55 | ```js 56 | import fetchApiCall from 'fetch-api-call'; 57 | 58 | const { api } = fetchApiCall({ 59 | protocol: process.env.API_PROTOCOL, 60 | host: process.env.API_HOST, 61 | }); 62 | ``` 63 | 64 | Options: 65 | 66 | * _protocol_ - http protocol. Available values - _http:_ an _https:_ 67 | * _host_ - server hostname 68 | * _headers_ - requests headers. NOTE that headers could be also set with 69 | interceptors 70 | * _mode_ - more details at - 71 | https://developer.mozilla.org/en-US/docs/Web/API/Request/mode 72 | * _prefix_ - api url prefix. Useful when there are multiple api versions: 73 | * _/user_ 74 | * _/v1/user_ 75 | * _/v2/user_ 76 | 77 | **api** - function to define api methods in single line of code. 78 | 79 | ```js 80 | const apiCall = api('HTTP_METHOD /api/path/:with/:placeholders'); 81 | ``` 82 | 83 | **apiCall(params = {}, headers = {})** - function takes two optional params: 84 | 85 | * _params_ - api params. Also is used for building final api entrypoint. More 86 | detauls see at 87 | [Api Url Definition and Placeholders](https://tuchk4.github.io/bivrost/docs/basics/api-function.html#api-definition). 88 | * _headers_ - certain request headers. 89 | 90 | Examples: 91 | 92 | ```js 93 | // user login api 94 | const login = api('POST /login'); 95 | login({ 96 | username, 97 | password, 98 | }); 99 | 100 | // user logout api 101 | const logout = api('POST /logout'); 102 | logout(); 103 | 104 | // get user api 105 | const getUser = api('GET /user/:id'); 106 | getUser({ 107 | id: 1, 108 | }); 109 | ``` 110 | 111 | ## Interceptors 112 | 113 | ```js 114 | import fetchApiCall from 'fetch-api-call'; 115 | 116 | const { api, interceptors } = fetchApiCall({ 117 | protocol: process.env.API_PROTOCOL, 118 | host: process.env.API_HOST, 119 | }); 120 | ``` 121 | 122 | **interceptors** methods: 123 | 124 | * _addRequestInterceptor(request)_ 125 | * _addResponseInterceptor(response)_ 126 | * _addErrorInterceptor(error)_ 127 | 128 | Each methods return function to remove interceptor. 129 | 130 | Iterceptors will be called for all request done with _api_. 131 | 132 | ```js 133 | const login = api('POST /login'); 134 | const logout = api('POST /logout'); 135 | 136 | let removeAcessTokenIterceptor = null; 137 | 138 | const { acessToken } = await login({}); 139 | 140 | removeAcessTokenIterceptor = iterceptors.addRequestInterceptor(request => { 141 | request.headers.set('Acess-Token', acessToken); 142 | return request; 143 | }); 144 | 145 | 146 | await logout(); 147 | removeAcessTokenIterceptor(); 148 | ``` 149 | 150 | ## Request Headers 151 | 152 | This is useful when using `fetch-api-call` on NodeJS. In some cases it is not 153 | possible to use interceptors to set auth headers becasue it will work for all 154 | request. We should specify certain request with certain header. 155 | 156 | ```js 157 | const getStatistics = api('GET /statistics'); 158 | getStatistics( 159 | {}, 160 | { 161 | headers: { 162 | MyCustomHeader: '123zxc', 163 | }, 164 | } 165 | ); 166 | ``` 167 | 168 | ## Multiple Api Instances 169 | 170 | Very useful if there is microservices backend architecture. 171 | 172 | ```js 173 | import fetchApiCall from 'fetch-api-call'; 174 | 175 | const Auth = fetchApiCall({ 176 | host: process.env.AUTH_API_HOST, 177 | }); 178 | 179 | const Users = fetchApiCall({ 180 | host: process.env.USERS_API_HOST, 181 | }); 182 | 183 | const Data = fetchApiCall({ 184 | host: process.env.DATA_API_HOST, 185 | prefix: 'v2', 186 | }); 187 | 188 | const login = Auth.api('POST /login').then(({ accessToken }) => { 189 | Users.interceptors.addRequest(res => { 190 | request.headers.set('Acess-Token', acessToken); 191 | return request; 192 | }); 193 | 194 | Data.interceptors.addRequest(res => { 195 | request.headers.set('Acess-Token', acessToken); 196 | return request; 197 | }); 198 | }); 199 | 200 | const loadStatisitcs = Data.api('GET /statisitcs'); 201 | loadStatisitcs({ 202 | filter: { 203 | //... 204 | }, 205 | }); 206 | ``` 207 | 208 | ## Custom Setup 209 | 210 | ```js 211 | import setup from 'fetch-api-call/setup'; 212 | import fetchAdapter from 'bivrost-fetch-adapter'; 213 | 214 | const createApi = setup({ 215 | headers: { 216 | 'Content-Type': 'application/json', 217 | }, 218 | mode: 'cors', 219 | adapter: fetchAdapter, 220 | interceptors: { 221 | resposne: res => res, 222 | request: req => req, 223 | error: err => err, 224 | }, 225 | deduplicate: false 226 | }); 227 | 228 | const api = createApi({ 229 | protocol: process.env.API_PROTOCOL, 230 | host: process.env.API_HOST, 231 | }); 232 | 233 | const login = api('POST /login'); 234 | ``` 235 | 236 | Options: 237 | 238 | * _headers_ - default headers 239 | * _mode_ - default mode. More details at - 240 | https://developer.mozilla.org/en-US/docs/Web/API/Request/mode 241 | * _adapter_ - default HTTP adapter. See more at - 242 | https://tuchk4.github.io/bivrost/docs/adapters.html 243 | * _interceptors_ - initial interceptors. NOTE that this is NOT DEFAULT 244 | interceptors. 245 | -------------------------------------------------------------------------------- /packages/fetch-api-call/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import fetchApiCall from '../src'; 2 | 3 | describe('fetchApiCall', () => { 4 | it('should process error', () => { 5 | const { api, interceptors } = fetchApiCall({ 6 | protocol: 'http:', 7 | host: 'httpbin.org', 8 | }); 9 | 10 | const badRequest = api('GET /status/:status'); 11 | const thenMock = jest.fn(); 12 | 13 | return badRequest({ 14 | status: 402, 15 | }) 16 | .then(thenMock) 17 | .catch(e => { 18 | expect(e).toEqual('Fuck you, pay me!'); 19 | }) 20 | .then(() => { 21 | expect(thenMock.mock.calls.length).toEqual(0); 22 | }); 23 | }); 24 | 25 | it('should process request options', () => { 26 | const { api, interceptors } = fetchApiCall({ 27 | protocol: 'http:', 28 | host: 'httpbin.org', 29 | request: { 30 | credentials: 'include', 31 | }, 32 | }); 33 | 34 | const request = api('GET /response-headers'); 35 | 36 | return request({ 37 | status: 200, 38 | }).then(r => { 39 | console.log(r); 40 | }); 41 | }); 42 | 43 | it('should pass post body', () => { 44 | const { api, interceptors } = fetchApiCall({ 45 | protocol: 'http:', 46 | host: 'httpbin.org', 47 | }); 48 | 49 | const login = api('POST /post'); 50 | const data = { foo: 'bar' }; 51 | 52 | return login(data).then(res => { 53 | expect(res.json).toEqual(data); 54 | }); 55 | }); 56 | 57 | it('should run call with custom headers 1', () => { 58 | const { api, interceptors } = fetchApiCall({ 59 | protocol: 'http:', 60 | host: 'httpbin.org', 61 | }); 62 | 63 | const withCustomHeaders = api('GET /headers'); 64 | 65 | return withCustomHeaders( 66 | {}, 67 | { 68 | headers: { 'x-with-custom-header': '123' }, 69 | } 70 | ).then(res => { 71 | expect(res.headers['X-With-Custom-Header']).toEqual('123'); 72 | }); 73 | }); 74 | 75 | it('should run call with custom headers 2', () => { 76 | const { api, interceptors } = fetchApiCall({ 77 | protocol: 'http:', 78 | host: 'httpbin.org', 79 | }); 80 | 81 | const withCustomHeaders = api('GET /headers', { 82 | headers: { 83 | 'x-with-custom-header-2': '345', 84 | }, 85 | }); 86 | 87 | return withCustomHeaders( 88 | {}, 89 | { 90 | headers: { 'x-with-custom-header': '123' }, 91 | } 92 | ).then(res => { 93 | expect(res.headers['X-With-Custom-Header']).toEqual('123'); 94 | expect(res.headers['X-With-Custom-Header-2']).toEqual('345'); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/fetch-api-call/fetch-api-call-3.2.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuchk4/bivrost/f6d6d5973bee77c02ee050dd34ed19a5f96b0a42/packages/fetch-api-call/fetch-api-call-3.2.4.tgz -------------------------------------------------------------------------------- /packages/fetch-api-call/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createApi = require('./index'); 4 | module.exports = createApi.default; 5 | -------------------------------------------------------------------------------- /packages/fetch-api-call/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-api-call", 3 | "version": "3.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asynckit": { 8 | "version": "0.4.0", 9 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 10 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 11 | }, 12 | "bivrost": { 13 | "version": "3.2.0", 14 | "resolved": "https://registry.npmjs.org/bivrost/-/bivrost-3.2.0.tgz", 15 | "integrity": "sha512-61YUHZd1QzAkqruJJBZFXEcn5zngs29ZOCA0ccgNXacwGTLIfMyzMvgJTKslDBu7RqbOb66pYbMJrKGQKL3aPA==", 16 | "requires": { 17 | "form-data": "^2.3.1", 18 | "lodash.camelcase": "^4.3.0" 19 | } 20 | }, 21 | "bivrost-fetch-adapter": { 22 | "version": "3.1.0", 23 | "resolved": "https://registry.npmjs.org/bivrost-fetch-adapter/-/bivrost-fetch-adapter-3.1.0.tgz", 24 | "integrity": "sha512-+6fgXUT30XYDn5H2T9SPu3sRKSRK9qTesw5dzXMDOTH3i6DsKEZ8+83ZohDz+8uK5YnQHd1meX2hM+5Bluso8g==", 25 | "requires": { 26 | "form-data": "^2.3.1", 27 | "isomorphic-fetch": "2.2.1", 28 | "qs": "^6.5.1" 29 | } 30 | }, 31 | "combined-stream": { 32 | "version": "1.0.6", 33 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 34 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 35 | "requires": { 36 | "delayed-stream": "~1.0.0" 37 | } 38 | }, 39 | "delayed-stream": { 40 | "version": "1.0.0", 41 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 42 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 43 | }, 44 | "encoding": { 45 | "version": "0.1.12", 46 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 47 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 48 | "requires": { 49 | "iconv-lite": "~0.4.13" 50 | } 51 | }, 52 | "form-data": { 53 | "version": "2.3.2", 54 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 55 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 56 | "requires": { 57 | "asynckit": "^0.4.0", 58 | "combined-stream": "1.0.6", 59 | "mime-types": "^2.1.12" 60 | } 61 | }, 62 | "iconv-lite": { 63 | "version": "0.4.23", 64 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 65 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 66 | "requires": { 67 | "safer-buffer": ">= 2.1.2 < 3" 68 | } 69 | }, 70 | "is-stream": { 71 | "version": "1.1.0", 72 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 73 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 74 | }, 75 | "isomorphic-fetch": { 76 | "version": "2.2.1", 77 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 78 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 79 | "requires": { 80 | "node-fetch": "^1.0.1", 81 | "whatwg-fetch": ">=0.10.0" 82 | } 83 | }, 84 | "lodash.camelcase": { 85 | "version": "4.3.0", 86 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 87 | "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" 88 | }, 89 | "mime-db": { 90 | "version": "1.33.0", 91 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 92 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 93 | }, 94 | "mime-types": { 95 | "version": "2.1.18", 96 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 97 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 98 | "requires": { 99 | "mime-db": "~1.33.0" 100 | } 101 | }, 102 | "node-fetch": { 103 | "version": "1.7.3", 104 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 105 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 106 | "requires": { 107 | "encoding": "^0.1.11", 108 | "is-stream": "^1.0.1" 109 | } 110 | }, 111 | "qs": { 112 | "version": "6.5.2", 113 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 114 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 115 | }, 116 | "regenerator-runtime": { 117 | "version": "0.11.1", 118 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", 119 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" 120 | }, 121 | "safer-buffer": { 122 | "version": "2.1.2", 123 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 124 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 125 | }, 126 | "whatwg-fetch": { 127 | "version": "2.0.4", 128 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", 129 | "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/fetch-api-call/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-api-call", 3 | "version": "3.2.5", 4 | "description": "Fetch api call supporting url params / interceptors / awesome api setup", 5 | "keywords": [ 6 | "rest", 7 | "bivrost", 8 | "data-source", 9 | "api", 10 | "fetch", 11 | "axios", 12 | "local-storage", 13 | "fetch-api-call", 14 | "response", 15 | "request", 16 | "call steps" 17 | ], 18 | "browser": "index.js", 19 | "main": "node.js", 20 | "scripts": { 21 | "prepublish": "../../node_modules/.bin/babel -d ./ ./src", 22 | "dev": "../../node_modules/.bin/babel -d ./ ./src --watch" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/tuchk4/bivrost.git" 27 | }, 28 | "author": "tuchk4 ", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/tuchk4/bivrost/issues" 32 | }, 33 | "homepage": "hhttps://github.com/tuchk4/bivrost", 34 | "dependencies": { 35 | "bivrost": "^3.2.5", 36 | "bivrost-fetch-adapter": "^3.2.2", 37 | "regenerator-runtime": "^0.11.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/fetch-api-call/src/createInterceptors.js: -------------------------------------------------------------------------------- 1 | export default (defaultInterceptors = {}) => { 2 | const interceptors = { 3 | error: new Set(), 4 | request: new Set(), 5 | response: new Set(), 6 | }; 7 | 8 | const api = { 9 | // ----- 10 | request: request => 11 | [...interceptors.request].reduce((request, cb) => cb(request), request), 12 | 13 | // ----- 14 | error: error => 15 | [...interceptors.error].reduce( 16 | (error, cb) => error.then(err => cb(err)), 17 | Promise.resolve(error) 18 | ), 19 | 20 | // ----- 21 | response: response => 22 | [...interceptors.response].reduce((response, cb) => { 23 | return response.then(res => cb(res)); 24 | }, Promise.resolve(response)), 25 | 26 | // ----- 27 | // ----- 28 | 29 | addErrorInterceptor: cb => { 30 | interceptors.error.add(cb); 31 | return () => interceptors.error.delete(cb); 32 | }, 33 | addRequestInterceptor: cb => { 34 | interceptors.request.add(cb); 35 | return () => interceptors.request.delete(cb); 36 | }, 37 | addResponseInterceptor: cb => { 38 | interceptors.response.add(cb); 39 | return () => interceptors.response.delete(cb); 40 | }, 41 | }; 42 | 43 | if (defaultInterceptors.request) { 44 | api.addRequestInterceptor(defaultInterceptors.request); 45 | } 46 | 47 | if (defaultInterceptors.error) { 48 | api.addErrorInterceptor(defaultInterceptors.error); 49 | } 50 | 51 | if (defaultInterceptors.response) { 52 | api.addResponseInterceptor(defaultInterceptors.response); 53 | } 54 | 55 | return api; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/fetch-api-call/src/index.js: -------------------------------------------------------------------------------- 1 | import regeneratorRuntime from 'regenerator-runtime'; 2 | import fetchAdapter from 'bivrost-fetch-adapter'; 3 | import setup from './setup'; 4 | 5 | function processResponse(response) { 6 | const headers = response && response.headers; 7 | 8 | if (headers) { 9 | const contentType = headers.get('content-type'); 10 | 11 | if (contentType && contentType.indexOf('application/json') !== -1) { 12 | return response.json(); 13 | } else if (contentType && contentType.indexOf('image') !== -1) { 14 | return response.blob(); 15 | } else { 16 | return response.text(); 17 | } 18 | } 19 | 20 | return response; 21 | } 22 | 23 | export default setup({ 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | mode: 'cors', 28 | adapter: fetchAdapter, 29 | interceptors: { 30 | error: async error => await processResponse(error), 31 | response: async response => await processResponse(response), 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/fetch-api-call/src/node.js: -------------------------------------------------------------------------------- 1 | const createApi = require('./index'); 2 | module.exports = createApi.default; 3 | -------------------------------------------------------------------------------- /packages/fetch-api-call/src/setup.js: -------------------------------------------------------------------------------- 1 | import regeneratorRuntime from 'regenerator-runtime'; 2 | import api from 'bivrost/http/api'; 3 | 4 | import createInterceptors from './createInterceptors'; 5 | export default ({ adapter, ...defaults }) => ({ 6 | headers, 7 | mode, 8 | // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request 9 | request = { 10 | ...defaults.request, 11 | ...request, 12 | }, 13 | ...options 14 | }) => { 15 | const interceptors = createInterceptors(defaults.interceptors); 16 | return { 17 | interceptors, 18 | api: api({ 19 | adapter: adapter({ 20 | mode: mode ? mode : defaults.mode, 21 | headers: headers ? headers : defaults.headers, 22 | interceptors: { 23 | request: request => interceptors.request(request), 24 | error: async error => { 25 | throw await interceptors.error(error); 26 | }, 27 | response: async response => await interceptors.response(response), 28 | }, 29 | ...request, 30 | }), 31 | deduplicate: defaults.hasOwnProperty('deduplicate') 32 | ? defaults.deduplicate 33 | : true, 34 | ...options, 35 | }), 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/fetch-api-call/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | regenerator-runtime@^0.11.0: 6 | version "0.11.1" 7 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" 8 | --------------------------------------------------------------------------------