├── .babelrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── VueChimeraLogo.png ├── VueChimeraLogo.svg ├── build ├── rollup.config.base.js ├── rollup.config.browser.js ├── rollup.config.es.js └── rollup.config.umd.js ├── docs ├── .vuepress │ ├── components │ │ ├── cache-example.vue │ │ ├── demo │ │ │ ├── auto-refresh.vue │ │ │ ├── post-request.vue │ │ │ ├── reactive-get.vue │ │ │ └── simple-get.vue │ │ ├── events-example.vue │ │ └── reactive-endpoint-example.vue │ ├── config.js │ ├── enhanceApp.js │ ├── public │ │ └── favicon.png │ └── styles │ │ └── index.styl ├── README.md ├── api │ └── README.md ├── demo.md └── guide │ ├── cache.md │ ├── cancel.md │ ├── chimera.md │ ├── endpoint.md │ ├── events.md │ ├── examples.md │ ├── getting-started.md │ ├── installation.md │ ├── mixin.md │ ├── reactive-endpoints.md │ └── ssr.md ├── jest.config.js ├── nuxt ├── index.js └── plugin.js ├── package.json ├── ship.config.js ├── src ├── Endpoint.js ├── NullEndpoint.js ├── VueChimera.js ├── cache │ ├── MemoryCache.js │ └── StorageCache.js ├── components │ └── ChimeraEndpoint.js ├── events.js ├── http │ └── axiosAdapter.js ├── index.es.js ├── index.js ├── mixin.js └── utils.js ├── ssr └── index.js ├── tests ├── mocks │ └── axios.mock.js ├── ssr.test.js └── unit │ ├── axios.test.js │ ├── cache.test.js │ ├── chimera.test.js │ ├── component.test.js │ ├── endpoint.test.js │ ├── event.test.js │ ├── index.js │ ├── plugin.test.js │ └── reactivity.test.js ├── todo ├── types ├── endpoint.d.ts ├── index.d.ts └── vue.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false 5 | }] 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ], 10 | "env": { 11 | "test": { 12 | "presets": [ 13 | ["@babel/preset-env"] 14 | ], 15 | "plugins": ["@babel/plugin-transform-runtime"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | 7 | defaults: &defaults 8 | working_directory: ~/repo 9 | docker: 10 | # specify the version you desire here 11 | - image: circleci/node:12-browsers 12 | 13 | jobs: 14 | setup: 15 | <<: *defaults 16 | steps: 17 | - checkout 18 | 19 | # Download and cache dependencies 20 | - restore_cache: 21 | keys: 22 | - v1-dependencies-{{ checksum "package.json" }} 23 | # fallback to using the latest cache if no exact match is found 24 | - v1-dependencies- 25 | 26 | - run: yarn install 27 | 28 | - save_cache: 29 | paths: 30 | - node_modules 31 | key: v1-dependencies-{{ checksum "package.json" }} 32 | - persist_to_workspace: 33 | root: ~/repo 34 | paths: . 35 | 36 | lint: 37 | <<: *defaults 38 | steps: 39 | - attach_workspace: 40 | at: ~/repo 41 | - run: yarn lint 42 | 43 | build: 44 | <<: *defaults 45 | steps: 46 | - attach_workspace: 47 | at: ~/repo 48 | - run: yarn build 49 | 50 | # run tests! 51 | test: 52 | <<: *defaults 53 | steps: 54 | - attach_workspace: 55 | at: ~/repo 56 | - run: yarn test && yarn report-coverage 57 | 58 | deploy: 59 | <<: *defaults 60 | steps: 61 | - attach_workspace: 62 | at: ~/repo 63 | - run: yarn release:trigger 64 | 65 | workflows: 66 | version: 2 67 | setup-lint-build-test: 68 | jobs: 69 | - setup 70 | - lint: 71 | requires: 72 | - setup 73 | - test: 74 | requires: 75 | - setup 76 | - build: 77 | requires: 78 | - setup 79 | - deploy: 80 | requires: 81 | - test 82 | - build 83 | - lint 84 | filters: 85 | tags: 86 | only: /^v.*/ 87 | branches: 88 | ignore: /.*/ 89 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | coverage/* 3 | example/**/* 4 | node_modules/**/* 5 | nuxt/* 6 | !docs/.vuepress 7 | docs/.vuepress/dist 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": ["standard", "plugin:vue/recommended"], 3 | "plugins": [ 4 | 'vue' 5 | ], 6 | "rules": { 7 | "no-console": ["error"], 8 | "no-alert": ["error"], 9 | "no-debugger": ["error"] 10 | }, 11 | overrides: [ 12 | { 13 | files: [ 14 | 'tests/**/*.js', 15 | ], 16 | env: { 17 | jest: true 18 | }, 19 | } 20 | ] 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /.idea 4 | coverage 5 | yarn-error.log 6 | dist 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.swp 4 | *.yml 5 | coverage 6 | docs/_book 7 | config 8 | dist/*.map 9 | node_modules 10 | example 11 | test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.2](https://github.com/chimera-js/vue-chimera/compare/v3.0.1...v3.0.2) (2020-03-20) 2 | 3 | ## Fixes 4 | Fix serverside polling (interval) 5 | 6 | ### Bug Fixes 7 | 8 | * docs build ([ca354b9](https://github.com/chimera-js/vue-chimera/commit/ca354b9d084a71a9ea5b41a76fd7a29fa67850f0)) 9 | * remove setInterval from serverside ([66dd90a](https://github.com/chimera-js/vue-chimera/commit/66dd90a65240f49f1648edaf80ac2f465685e3c4)) 10 | 11 | 12 | 13 | ## [3.0.1](https://github.com/chimera-js/vue-chimera/compare/v3.0.0...v3.0.1) (2020-03-20) 14 | 15 | ## Fixes 16 | Fix passing axios as function 17 | Fix send extra params without parameter 18 | 19 | # [3.0.0](https://github.com/chimera-js/vue-chimera/compare/v2.4.3...v3.0.0) (2020-03-18) 20 | 21 | ## Features 22 | 23 | Prefetch reactive endpoints 24 | 25 | Mixin strategy and inheritance 26 | 27 | SSR compatibility 28 | 29 | ## Refactors 30 | 31 | 'Resource' renamed to 'Endpoint' 32 | 33 | Refactored and improved reactivity 34 | 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Vue-Chimera 2 | 3 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. 4 | 2. Run `npm install` to install the dependencies. 5 | 6 | > _Note that both **npm** and **yarn** have been seen to miss installing dependencies. To remedy that, you can either delete the `node_modules` folder in your example app and install again or do a local install of the missing dependencies._ 7 | 8 | ## Running test suites 9 | 10 | ```sh 11 | yarn test 12 | ``` 13 | 14 | ### Pull Requests 15 | 16 | * Fill in [the required template](PULL_REQUEST_TEMPLATE.md) 17 | * Do not include issue numbers in the PR title 18 | * Include screenshots and animated GIFs in your pull request whenever possible. 19 | * Follow the [JavaScript](#javascript-styleguide) and [CoffeeScript](#coffeescript-styleguide) styleguides. 20 | * Include thoughtfully-worded, well-structured [Jasmine](https://jasmine.github.io/) specs in the `./spec` folder. Run them using `atom --test spec`. See the [Specs Styleguide](#specs-styleguide) below. 21 | * Document new code based on the [Documentation Styleguide](#documentation-styleguide) 22 | * End all files with a newline 23 | * [Avoid platform-dependent code](https://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/) 24 | * Place requires in the following order: 25 | * Built in Node Modules (such as `path`) 26 | * Built in Atom and Electron Modules (such as `atom`, `remote`) 27 | * Local Modules (using relative paths) 28 | * Place class properties in the following order: 29 | * Class methods and properties (methods starting with a `@` in CoffeeScript or `static` in JavaScript) 30 | * Instance methods and properties 31 | 32 | ### JavaScript Styleguide 33 | 34 | All JavaScript must adhere to [JavaScript Standard Style](https://standardjs.com/). 35 | 36 | * Prefer the object spread operator (`{...anotherObj}`) to `Object.assign()` 37 | * Inline `export`s with expressions whenever possible 38 | 39 | ```js 40 | // Use this: 41 | export default class ClassName { 42 | 43 | } 44 | 45 | // Instead of: 46 | class ClassName { 47 | 48 | } 49 | export default ClassName 50 | ``` 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Sasan Farrokh ([@SasanFarrokh](https://github.com/SasanFarrokh)) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |

Vue Chimera

7 | 8 | [![vuejs](https://img.shields.io/badge/vue.js-2.x-green.svg)](https://vuejs.org) 9 | [![circle ci](https://img.shields.io/circleci/project/github/chimera-js/vue-chimera/master.svg)](https://circleci.com/gh/chimera-js/vue-chimera) 10 | [![npm version](https://img.shields.io/npm/v/vue-chimera.svg)](https://www.npmjs.org/package/vue-chimera) 11 | [![npm downloads](https://img.shields.io/npm/dt/vue-chimera.svg)](http://npm-stat.com/charts.html?package=vue-chimera) 12 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/vue-chimera/3.0.2)](https://bundlephobia.com/result?p=vue-chimera@^3.0.0) 13 | [![codecov](https://codecov.io/gh/chimera-js/vue-chimera/branch/master/graph/badge.svg)](https://codecov.io/gh/chimera-js/vue-chimera) 14 | 15 | VueJS RESTful client with reactive features. 16 | Vue-Chimera is based on [axios](https://github.com/axios/axios) http client library. 17 | 18 | Overview of features: 19 | - Loading flags 20 | - Binding vue instances to API endpoints 21 | - Reactive endpoints and auto request based on vue instance data 22 | - Auto refreshing data 23 | - Serverside prefetching (Nuxt.js compatible) 24 | - Request cancellation 25 | - Cancel all pending requests on vue instance destroy (like route changes) 26 | - Events 27 | - Lightweight 28 | 29 | ## Demo 30 | 31 | [Demo](https://vue-chimera.netlify.com/demo) 32 | 33 | ## Documents 34 | 35 | [Full Documentation](https://vue-chimera.netlify.com) 36 | 37 | ## Installing 38 | 39 | Using npm: 40 | 41 | ```bash 42 | $ npm install vue-chimera 43 | or 44 | $ yarn add vue-chimera 45 | ``` 46 | 47 | Using cdn: 48 | ```html 49 | 50 | ``` 51 | 52 | ## Getting started 53 | 54 | To add **vue-chimera** to your Vue you must use it as a plugin: 55 | *ECMAScript 6* 56 | ```javascript 57 | import Vue from 'vue' 58 | import VueChimera from 'vue-chimera' 59 | 60 | Vue.use(VueChimera) 61 | 62 | ``` 63 | 64 | ## Using with Nuxt.js 65 | You can use Vue-Chimera with nuxtjs to use it's SSR features so you can easily prefetch the data. 66 | ```javascript 67 | // nuxt.config.js 68 | 69 | module.exports = { 70 | 71 | modules: [ 72 | 'vue-chimera/nuxt' 73 | ], 74 | 75 | chimera: { 76 | // Enables server side prefetch on endpoints which has `auto` property 77 | // true: fetched on server 78 | // false: fetched on client 79 | // 'override': fetched on server and client (overrided by client) 80 | prefetch: true, 81 | 82 | prefetchTimeout: 2000 // Server side timeout for prefetch 83 | } 84 | 85 | } 86 | ``` 87 | 88 | ## Maintainer 89 |

90 | 91 |

92 | 93 | ## Contribution 94 | All PRs are welcome. 95 | Thanks. 96 | 97 | ## License 98 | [MIT](https://github.com/chimera-js/vue-chimera/blob/master/LICENSE.MD) 99 | -------------------------------------------------------------------------------- /VueChimeraLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chimera-js/vue-chimera/cc7cf33922c09ca8763cabab00bf171142c17137/VueChimeraLogo.png -------------------------------------------------------------------------------- /VueChimeraLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /build/rollup.config.base.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import cjs from 'rollup-plugin-commonjs' 3 | import resolve from 'rollup-plugin-node-resolve' 4 | 5 | export default { 6 | input: 'src/index.js', 7 | plugins: [ 8 | resolve({ 9 | mainFields: ['jsnext', 'main', 'browser'] 10 | }), 11 | cjs({ 12 | exclude: ['src/*', 'src/components/*'] 13 | }), 14 | babel({ 15 | exclude: 'node_modules/**' 16 | }) 17 | ], 18 | external: ['axios'] 19 | } 20 | -------------------------------------------------------------------------------- /build/rollup.config.browser.js: -------------------------------------------------------------------------------- 1 | import base from './rollup.config.base' 2 | import { terser } from 'rollup-plugin-terser' 3 | 4 | const config = Object.assign({}, base, { 5 | output: { 6 | file: 'dist/vue-chimera.min.js', 7 | format: 'iife', 8 | name: 'VueChimera', 9 | globals: { 10 | axios: 'axios' 11 | } 12 | } 13 | }) 14 | 15 | config.plugins.push(terser()) 16 | 17 | export default config 18 | -------------------------------------------------------------------------------- /build/rollup.config.es.js: -------------------------------------------------------------------------------- 1 | import base from './rollup.config.base' 2 | 3 | export default Object.assign({}, base, { 4 | input: 'src/index.es.js', 5 | output: { 6 | file: 'dist/vue-chimera.es.js', 7 | format: 'es', 8 | name: 'VueChimera' 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /build/rollup.config.umd.js: -------------------------------------------------------------------------------- 1 | import base from './rollup.config.base' 2 | import { terser } from 'rollup-plugin-terser' 3 | 4 | const config = Object.assign({}, base, { 5 | output: { 6 | file: 'dist/vue-chimera.umd.js', 7 | format: 'umd', 8 | name: 'VueChimera', 9 | globals: { 10 | axios: 'axios' 11 | }, 12 | exports: 'named' 13 | } 14 | }) 15 | 16 | config.plugins.push(terser()) 17 | 18 | export default config 19 | -------------------------------------------------------------------------------- /docs/.vuepress/components/cache-example.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | -------------------------------------------------------------------------------- /docs/.vuepress/components/demo/auto-refresh.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /docs/.vuepress/components/demo/post-request.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | -------------------------------------------------------------------------------- /docs/.vuepress/components/demo/reactive-get.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /docs/.vuepress/components/demo/simple-get.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /docs/.vuepress/components/events-example.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 52 | -------------------------------------------------------------------------------- /docs/.vuepress/components/reactive-endpoint-example.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: '/', 3 | serviceWorker: true, 4 | head: [ 5 | ['link', { rel: 'icon', href: '/favicon.png' }] 6 | ], 7 | title: 'Vue Chimera', 8 | plugins: { 9 | '@vuepress/pwa': { 10 | serviceWorker: true, 11 | updatePopup: { 12 | '/': { 13 | message: 'New content is available.', 14 | buttonText: 'Refresh' 15 | } 16 | } 17 | } 18 | }, 19 | themeConfig: { 20 | repo: 'chimera-js/vue-chimera', 21 | docsDir: 'docs', 22 | // editLinks: true, 23 | selectText: 'Languages', 24 | label: 'English', 25 | lastUpdated: 'Last Updated', 26 | smoothScroll: true, 27 | nav: [ 28 | { text: 'Guide', link: '/guide/installation' }, 29 | { text: 'API Reference', link: '/api/' }, 30 | { text: 'Demo', link: '/demo' } 31 | ], 32 | sidebarDepth: 1, 33 | sidebar: { 34 | '/guide/': [ 35 | { 36 | title: 'Introduction', 37 | collapsable: false, 38 | children: [ 39 | 'installation', 40 | 'getting-started', 41 | 'endpoint', 42 | 'reactive-endpoints', 43 | 'chimera' 44 | ] 45 | }, 46 | { 47 | title: 'Topics', 48 | collapsable: false, 49 | children: [ 50 | 'events', 51 | 'cache', 52 | 'ssr', 53 | 'cancel', 54 | 'mixin' 55 | ] 56 | }, 57 | 'examples' 58 | ] 59 | } 60 | }, 61 | chainWebpack: (config, isServer) => { 62 | return config.resolve.alias.set('vue-chimera$', '../../src/index.es.js') 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChimera from '../../src/index' 3 | 4 | Vue.use(VueChimera, { 5 | baseURL: 'https://jsonplaceholder.typicode.com', 6 | prefetch: false 7 | }) 8 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chimera-js/vue-chimera/cc7cf33922c09ca8763cabab00bf171142c17137/docs/.vuepress/public/favicon.png -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | from { 3 | transform rotate(0deg) 4 | } 5 | to { 6 | transform rotate(360deg) 7 | } 8 | } 9 | body { 10 | line-height 2em 11 | } 12 | table { 13 | line-height: 1.9em !important 14 | } 15 | .example-box { 16 | padding 20px; 17 | border 1px solid #e0e0e0 18 | } 19 | .center { 20 | text-align center 21 | } 22 | .center-block { 23 | display block 24 | margin 0 auto 25 | } 26 | 27 | .spinner { 28 | border 2px solid #3b8070 29 | border-top-color transparent 30 | height 20px; 31 | width 20px 32 | display inline-block 33 | vertical-align middle 34 | border-radius 50% 35 | animation spin 0.4s linear infinite 36 | } 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /favicon.png 4 | tagline: Connect websites to REST apis easy! 5 | actionText: Get Started → 6 | actionLink: /guide/installation 7 | features: 8 | 9 | - title: Declarative REST endpoints 10 | details: Auto fetching apis, loading flags, status codes and errors 11 | - title: Reactive RESTful endpoints 12 | details: Automatic reloading apis on parameter changes 13 | - title: Server side fetch 14 | details: No xhr on pages, Execute your api on the server before rendering 15 | --- 16 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # Soon... 2 | -------------------------------------------------------------------------------- /docs/demo.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Vue Chimera

4 |

Easiest way to interact with RESTful APIs

5 |
6 | ---- 7 | 8 | ### Simple GET 9 | 10 | <<< @/docs/.vuepress/components/demo/simple-get.vue 11 | 12 | 13 | 14 | ### Reactive GET 15 | 16 | <<< @/docs/.vuepress/components/demo/reactive-get.vue 17 | 18 | 19 | 20 | ### Auto refreshing (polling) 21 | 22 | <<< @/docs/.vuepress/components/demo/auto-refresh.vue 23 | 24 | 25 | 26 | ### POST requests (manual sending) 27 | 28 | <<< @/docs/.vuepress/components/demo/post-request.vue 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/guide/cache.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | You can use `cache` property to leverage cache feature on endpoints 3 | 4 | Endpoints using cache should have unique `key` property 5 | 6 | Current cache implementations are: 7 | - Memory Cache 8 | - Storage Cache 9 | 10 | <<< @/docs/.vuepress/components/cache-example.vue 11 | 12 | 13 | 14 | ## Cache busting 15 | You can use `.reload(true)` or `.fetch(true)` to ignore cache 16 | 17 | ```javascript 18 | export default { 19 | methods: { 20 | forceReload() { 21 | this.users.reload(true) 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | Also to delete cache you can call `.deleteCache()` on endpoint. But be ware on reactive endpoints 28 | this only removes current endpoint cache, you can use `.cache.clear()` to completely remove cache keys 29 | from a cache store. 30 | 31 | ## Custom cache implementation 32 | You can easily extend `MemoryCache` or `StorageCache` classes to implement your own rules. 33 | (LRU cache, ...) 34 | -------------------------------------------------------------------------------- /docs/guide/cancel.md: -------------------------------------------------------------------------------- 1 | # Cancellation 2 | Endpoints current pending situation can be easily canceled by calling `.cancel()` method 3 | 4 | By default, on vue instance `beforeDestroy` hook, all pending endpoints will be canceled. 5 | 6 | ```javascript 7 | export default { 8 | 9 | chimera: { 10 | users: '/users', 11 | on: { 12 | cancel () { 13 | console.log('Canceled') 14 | } 15 | } 16 | }, 17 | 18 | methods: { 19 | cancelUser () { 20 | this.users.cancel() 21 | } 22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/guide/chimera.md: -------------------------------------------------------------------------------- 1 | # Chimera Instance 2 | Chimera instance manages all endpoints. 3 | - Send endpoint requests that has `auto` property true 4 | - Cancel endpoint requests on vue component destroy 5 | - Instantiate endpoints based on defaults set on it 6 | - Update [reactive endpoints](/guide/reactive-endpoints) 7 | - Provide some globals like `$loading`, `$axios`, `$cancelAll()` 8 | 9 | ## Endpoint defaults 10 | To set some defaults to endpoint definitions use `$options` property 11 | on `chimera` option: 12 | 13 | ```javascript 14 | export default { 15 | chimera: { 16 | $options: { 17 | // Pass an axios instance with it's defaults 18 | axios: axios.create(), 19 | 20 | // Or 21 | baseURL: 'http://my-api.com/api/', 22 | headers: { 23 | 'X-Sample-Header': 'Header' 24 | }, 25 | }, 26 | 27 | posts: { 28 | url: '/posts' 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | _Note :_ Chimera options can also be set globally on **plugin** options. 35 | See [Installation](/guide/installation) 36 | 37 | ## Access current vue instance 38 | Sometimes to define the defaults for endpoints, You need to access 39 | current vue instance (usually with `this`) 40 | 41 | Simply you can use a function that returns these options: 42 | 43 | ```javascript 44 | export default { 45 | // Chimera as a function 46 | chimera () { 47 | return { 48 | $options: { 49 | // Pass injected axios as http client 50 | axios: this.$axios, 51 | }, 52 | 53 | // Use route parameters 54 | post: { 55 | url: '/posts/' + this.$route.params.id 56 | } 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ## Global chimera properties and methods 63 | There are some useful global props prepending with $. 64 | This is the reason why you cannot define endpoint with name starting with $ 65 | 66 | - `$chimera.$loading` 67 | 68 | Global loading indicator, returns `true` if there's any endpoint in loading state 69 | 70 | - `$chimera.$cancelAll()` 71 | 72 | Global method that cancels all pending requests 73 | -------------------------------------------------------------------------------- /docs/guide/endpoint.md: -------------------------------------------------------------------------------- 1 | # Endpoints 2 | 3 | Endpoints are base class objects that uses 4 | [axios](https://github.com/axios/axios#request-config) 5 | to call endpoints, then results and errors are stored inside them. 6 | 7 | ## Defining Endpoints 8 | To define an endpoint, just add `chimera` property to root of your vue component instance, 9 | then add your endpoint definitions to chimera object. 10 | 11 | Chimera properties will be automatically converted to 12 | [Endpoint](https://github.com/chimera-js/vue-chimera/blob/master/src/Endpoint.js) 13 | object 14 | 15 | 16 | ```javascript 17 | export default { 18 | 19 | // Chimera property contains all restful endpoints 20 | chimera: { 21 | 22 | // Here you can define your restful endpoints and api endpoints 23 | products: '/products', 24 | 25 | // a sample POST request 26 | sendProduct: { 27 | url: '/products', 28 | method: 'POST', 29 | 30 | // POST body 31 | params: { 32 | title: 'My Product', 33 | description: 'Vue Chimera is awesome...' 34 | }, 35 | auto: false 36 | } 37 | 38 | // endpoint keys cannot start with $ (dollor sign) 39 | }, 40 | 41 | data() { 42 | return { ... } 43 | } 44 | 45 | } 46 | ``` 47 | 48 | Endpoint definition can be anything coompatible with 49 | [axios configuration](https://github.com/axios/axios#request-config) 50 | format: 51 | * **string**: A simple string for simple GET requests 52 | * **Object**: For complex endpoints like: POST, PATCH, with Parameters, with Headers 53 | * **Endpoint**: An instance of 54 | [Endpoint](https://github.com/chimera-js/vue-chimera/blob/master/src/Endpoint.js) (manually instantiate Endpoint) 55 | * **Function**: for reactive endpoints [Reactive-Endpoints](#reactive-endpoints) (explained later) 56 | 57 | ## Endpoint options 58 | 59 | | Properties | Type | Default value | Description | 60 | | ---------- | ----- | ------------- | ----------- | 61 | | key | String | null | Unique key that identifies an endpoint, used for caching and server side fetching purpose.
_We recommend to always set it on every endpoint_ | | 62 | | url | String | | Endpoint url 63 | | baseURL | String | | Endpoint base url like: `http://example.com/api/` 64 | | method | String | GET | Endpoint method: (POST/GET) 65 | | headers | Object | {} | Request headers 66 | | params | Object | {} | Request parameters (Query string for GET / Body Data for POST/PATCH/DELETE) 67 | | auto | Boolean/String | 'get' | A boolean flag that indicates endpoint should be fetched on instantiation or reactive changes.
If it's a string, fetches endpoints with same request method | 68 | | transformer| Function/Object | null | Transform response or error results to something else | 69 | | interval | Number | undefined | A number in miliseconds to auto refresh an api 70 | | debounce | Number | 50 | A number in miliseconds that prevent duplicate api calls during this time, can be set to `false` to disable debouncing. | 71 | | cache | Cache | null | Cache strategy object. [More info](/guide/cache) 72 | | timeout | Number | 0 | Request timeout 73 | | axios | Axios/Object | null | Axios instance to send requests 74 | | on | Object | null | Sets event listeners [Events](/guide/events) 75 | | prefetch | Boolean | Equals to `auto` if not set | A boolean flag that indicates endpoint should be fetched on server. [More Info](/guide/ssr) | 76 | | prefetchTimeout | Number | 4000 | A number in milliseconds that indicates how much should wait for server to fetch endpoints | 77 | 78 | \* options is the same axios configuration. Any other 79 | [axios configuration](https://github.com/axios/axios#request-config) 80 | can also be set. 81 | 82 | _Note_ : All the endpoint options can have some defaults, like `baseURL` or `headers`. 83 | To set global defaults we can use plugin options or set `$options` on a 84 | [Chimera instance](/guide/chimera) 85 | 86 | ## Sending endpoint request 87 | Component endpoints can be accessed via `$chimera` property injected to vue instance, 88 | and also for simplicity if there is no props or data conflicting endpoint name, 89 | endpoints can directly accessed via it's name. 90 | 91 | ```javascript 92 | export default { 93 | chimera: { 94 | posts: '/posts' 95 | }, 96 | mounted () { 97 | this.$chimera.posts === this.posts // True 98 | } 99 | } 100 | ``` 101 | 102 | Endpoints are automatically loaded on component create 103 | if method is GET, but can be overrided with 104 | setting `auto` is set to `true` or `false` 105 | 106 | ```javascript 107 | export default { 108 | chimera: { 109 | users: '/users', // By default GET requests has `auto` 110 | usersNotAuto: { 111 | url: '/users', 112 | auto: false, // Disable auto on GET 113 | }, 114 | newUser: { // POST/PUT/PATCH/DELETE requests are manual. `auto`: false 115 | url: '/users/new', 116 | method: 'POST', 117 | params: { name: 'John Doe' } 118 | }, 119 | newUserAuto: { // But can explicitly set to auto 120 | url: '/users/new', 121 | method: 'POST', 122 | params: { name: 'John Doe' }, 123 | auto: true, 124 | } 125 | }, 126 | method: { 127 | submit () { 128 | this.newUser.fetch() // calling POST request 129 | this.usersNotAuto.fetch() // fetching 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | Or they can be manually fetched via calling `.fetch()` or `.reload()` 136 | method on the endpoint instance 137 | 138 | ## Using endpoints 139 | Endpoints have props and methods that can be used to send requests and extract the results 140 | 141 | ### Properties 142 | | Property | Type | Initial Value | Description 143 | | -------- | :-----: | :-------------: | ----------- 144 | | data | Object/Array | null | Endpoint response JSON object returned from server when request is successful.
_Might be transformed through transformers_ 145 | | loading | Boolean | false | Indicates the endpoint is in loading state 146 | | error | Object/Array/String | null | Error json object or string returned from server when request failed 147 | | lastLoaded | Date | null | Date object from last time endpoint successfully loaded (null if not loaded yet) 148 | | status | number | null | Endpoint response status 149 | | headers | Object | {} | Endpoint response/error headers 150 | | ---- | ---- | ---- | ---- 151 | | params | Object | | Parameters passed to endpoint on definition 152 | | url | String | | Url passed to endpoint on definition 153 | | method | String | | Request method (GET/POST/...) passed to endpoint on definition 154 | | looping | Boolean | | Boolean indicates that endpoint is looping (`interval` option) 155 | 156 | ### Methods 157 | 158 | | Method | Return type | Description 159 | | ---------- | ----------- | ----------- 160 | | fetch(force, extraOptions) | Promise | Fetches the endpoint from server.
`force`: True for cache busting,
`extraOptions` Merged into current endpoint axios config for extra options & params 161 | | reload(force) | Promise | Same as fetch, (debounced) 162 | | send(extraParams) | Promise | Send request but with extra parameters in body or query string 163 | | on(event, handler)| | Sets an event listener. [Events](#events) 164 | | cancel() | void | Interupts request 165 | | startInterval() | void | Manually starts interval (auto refresh) 166 | | stopInterval() | void | Manually stops interval 167 | 168 | ### Using Inside template 169 | - `data` is the final json result of our restful endpoint 170 | - `loading` is a boolean flag, identifies the endpoint is loading. 171 | - `error` is a final json result of error response. 172 | You can read other endpoint property and methods [here](#endpoint-properties-and-methods). 173 | 174 | ```html 175 | 186 | 187 | 194 | ``` 195 | 196 | ### Using Programmatically 197 | 198 | ```javascript 199 | export default { 200 | chimera: { 201 | users: '/users' 202 | }, 203 | computed: { 204 | users () { 205 | const users = this.$chimera.users.data 206 | if (!Array.isArray(users)) return [] 207 | return users 208 | } 209 | }, 210 | mounted () { 211 | setTimeout(() => { 212 | this.$chimera.users.reload() // or users.reload() 213 | }, 15000) 214 | }, 215 | } 216 | ``` 217 | 218 | ## Auto refresh endpoints 219 | You can use `interval` property to auto refresh endpoints after time 220 | ```javascript 221 | export default { 222 | chimera: { 223 | users: { 224 | url: '/users', 225 | interval: 10 * 1000 // 10 seconds 226 | } 227 | }, 228 | methods: { 229 | // Manual start and stop 230 | stopInterval () { 231 | this.users.startInterval(10000) 232 | console.log(this.users.looping) // true 233 | setTimeout(() => { 234 | this.users.stopInterval() 235 | console.log(this.users.looping) // false 236 | }, 500000) 237 | } 238 | } 239 | } 240 | ``` 241 | -------------------------------------------------------------------------------- /docs/guide/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | You can also bind events to endpoints to have more control over REST communications 4 | 5 | - **success**: emits when endpoint request successfully sent 6 | - **error**: emits when endpoint gets error 7 | - **cancel**: emits when endpoint canceled (error event not emitted on cancellation) 8 | - **loading**: emits when endpoint gets to loading 9 | - **timeout**: emits when endpoint gets timeout error 10 | 11 | ## Attach event listeners 12 | You can add listeners on chimera property inside `$options` or directly on endpoint definition 13 | 14 | <<< @/docs/.vuepress/components/events-example.vue 15 | 16 | 17 | 18 | ## Attach programmatically 19 | Use `on` method to attach events to endpoints 20 | 21 | ```javascript 22 | export default { 23 | 24 | mounted () { 25 | this.$chimera.users.on('success', () => { 26 | console.log('!') 27 | }) 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/guide/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | With vue chimera we can easily bind rest apis to our vue components. 4 | 5 | After installation, every component can use these features through `chimera` special option. 6 | 7 | ### Basic example 8 | We are going to show list of posts on this api: 9 | [https://jsonplaceholder.typicode.com/posts](https://jsonplaceholder.typicode.com/posts) 10 | 11 | ```html 12 | 27 | 28 | 37 | ``` 38 | 39 | Simple as that. No promise handling, loading and error indicators so far. 40 | 41 | 42 | ### Next sections 43 | You might now ask, what if there are more complicated API calls, 44 | like variable parameters or url, POST calls, paginations, response headers and... 45 | 46 | Vue chimera comes with solution to most rest api concerns. 47 | 48 | We introduce more awesome features in next sections. 49 | 50 | Thanks for reading. 51 | 52 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | vue-chimera is available on npm 4 | 5 | ## Using npm/yarn 6 | 7 | ```bash 8 | $ npm install --save vue-chimera 9 | # or 10 | $ yarn add vue-chimera 11 | ``` 12 | 13 | 14 | Then install plugin into Vue: 15 | 16 | ```javascript 17 | import Vue from 'vue' 18 | import VueChimera from "vue-chimera" 19 | 20 | Vue.use(VueChimera, { 21 | // Chimera default options 22 | }) 23 | ``` 24 | 25 | ## Using Nuxt.js 26 | 27 | Simply after adding installing `vue-chimera` package, add `vue-chimera/nuxt` 28 | to modules in `nuxt.config.js` 29 | ```javascript 30 | // nuxt.config.js 31 | module.exports = { 32 | modules: [ 33 | 'vue-chimera/nuxt' 34 | ], 35 | chimera: { 36 | axios: axios.create(), 37 | 38 | // Integrate with @nuxt/axios 39 | axios () { 40 | return this.$axios 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | If your using 47 | [@nuxt/axios](https://axios.nuxtjs.org/) 48 | look sample above. 49 | 50 | ## Using Script tag (CDN) 51 | ```html 52 | 53 | 54 | 57 | ``` 58 | If you use old browser style and `Vue` is publicly exposed as global variable 59 | just add `dist/vue-chimera.min.js` script tag to your HTML file and everything would be fine to go on. 60 | -------------------------------------------------------------------------------- /docs/guide/mixin.md: -------------------------------------------------------------------------------- 1 | # Mixin 2 | You can mix chimera property using mixins. 3 | 4 | All endpoints with same name gets override like vue data merging strategy. 5 | `$options` property gets merged. 6 | 7 | Event listeners, headers, params will be merged. 8 | 9 | ```javascript 10 | const myMixin = { 11 | chimera: { 12 | $options: { 13 | headers: { 14 | 'X-Custom-Header': 'xxx' 15 | } 16 | }, 17 | user: '/user', 18 | post: '/posts', 19 | } 20 | } 21 | export default { 22 | 23 | mixins: [ 24 | myMixin 25 | ], 26 | 27 | // You can mix chimera functions 28 | chimera () { 29 | return { 30 | $options: { 31 | // Headers are merged with mixin 32 | headers: { 33 | 'X-Custom-Header-2': '...' 34 | } 35 | }, 36 | // User endpoint completely gets override 37 | user: '/user?query=2' 38 | } 39 | }, 40 | 41 | 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/guide/reactive-endpoints.md: -------------------------------------------------------------------------------- 1 | # Reactive Endpoints 2 | 3 | ## How it works? 4 | Like computed properties endpoints can be reactive functions, when any parameter inside the functions 5 | changes, endpoint regenerated. 6 | 7 | Also if `endpoint.auto` evaluates to `true`, request will be sent automatically. 8 | 9 | ```javascript 10 | let app = new Vue({ 11 | 12 | data() { 13 | return { 14 | selectedUserId: 1, 15 | postId: 2 16 | } 17 | }, 18 | 19 | chimera: { 20 | user() { 21 | return `/api/v1/users/${this.selectedUserId}` 22 | }, 23 | post() { 24 | return { 25 | url: '/api/v1/posts', 26 | params: { 27 | postId: this.postId 28 | }, 29 | method: 'post', 30 | auto: true, 31 | // if you want use cache or ssr features define a unique key based on it's parameters 32 | key: 'post-' + this.postId, 33 | } 34 | }, 35 | } 36 | 37 | }) 38 | ``` 39 | 40 | _Any Endpoint parameters, headers, url, auto, ... can change_ 41 | 42 | ## Example 43 | 44 | <<< @/docs/.vuepress/components/reactive-endpoint-example.vue 45 | 46 | 47 | 48 | ## keepData property 49 | By default, response data will be kept between reactive changes. 50 | 51 | Set `keepData` to `false` to remove all status, data, and error between reactive changes 52 | -------------------------------------------------------------------------------- /docs/guide/ssr.md: -------------------------------------------------------------------------------- 1 | # Server-Side Rendering 2 | 3 | ::: warning 4 | **Requires Vue 2.6+ with `serverPrefetch` support** 5 | ::: 6 | 7 | If you have server side rendering on your Vue app you can easily prefetch all endpoints 8 | on any component (even nested components) 9 | on server side, So there will be no loading and flashes in contents. 10 | 11 | How awesome is that? 12 | 13 | ## Using Nuxt.js 14 | If you're using [Nuxt.js](https://nuxtjs.org/), You can easily add `vue-chimera/nuxt` to 15 | nuxt.config.js modules. SSR prefetch enabled by default. 16 | ```javascript 17 | // nuxt.config.js 18 | module.exports = { 19 | 20 | modules: [ 21 | 'vue-chimera/nuxt' 22 | ], 23 | chimera: { 24 | prefetch: true, // Set to false to disable prefetch 25 | prefetchTimeout: 4000, // Maximum number in milliseconds to wait on server to prefetch 26 | } 27 | 28 | } 29 | ``` 30 | 31 | ::: tip 32 | You can set `prefetch` to 'override' in order to prefetch on server and fetch it again on client 33 | ::: 34 | 35 | ## Using custom server 36 | If you configured SSR on your own custom application server, 37 | just before rendering app, attach `vue-chimera/ssr` to context with a custom name. 38 | 39 | Then pass the property name on the context to VueChimera plugin 40 | ```javascript 41 | // Example express server 42 | app.get('/*', (req, res) => { 43 | const VueChimeraSSR = require('vue-chimera/ssr') 44 | res.setHeader('Content-Type', 'text/html') 45 | const context = { 46 | req, 47 | url: req.url, 48 | chimera: VueChimeraSSR.getStates() 49 | } 50 | 51 | renderer.renderToString(context, (err, renderedHtml) => { 52 | let html = renderedHtml 53 | 54 | if (err) { 55 | res.status(500) 56 | } else { 57 | res.status(200) 58 | } 59 | 60 | res.send(html) 61 | }) 62 | }) 63 | ``` 64 | 65 | Attach context to window in render: 66 | ```html 67 | 68 | 69 | 70 | 71 | {{{ renderState({ contextKey: 'chimera', windowKey: '__CHIMERA__' }) }}} 72 | 73 | 74 | ``` 75 | 76 | Pass context to VuePlugin: 77 | 78 | ```javascript 79 | import Vue from 'vue' 80 | import VueChimera from 'vue-chimera' 81 | 82 | Vue.use(VueChimera, { 83 | ssrContext: '__CHIMERA__' 84 | }) 85 | ``` 86 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testRegex: 'tests/.*\\.test.js$', 3 | moduleFileExtensions: ['js', 'json', 'json'], 4 | transform: { 5 | '^.+\\.js$': 'babel-jest', 6 | '^.+\\.vue$': 'vue-jest' 7 | }, 8 | transformIgnorePatterns: [ 9 | '/node_modules/' 10 | ], 11 | setupFiles: ['/tests/unit/index.js'], 12 | collectCoverage: true, 13 | collectCoverageFrom: [ 14 | 'src/**/*.{js,vue}' 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /nuxt/index.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | module.exports = function nuxtChimeraModule (moduleOptions) { 4 | 5 | const options = Object.assign({ 6 | ssrContext: '__NUXT__.chimera', 7 | }, this.options.chimera, moduleOptions) 8 | 9 | this.addPlugin({ 10 | src: resolve(__dirname, 'plugin.js'), 11 | fileName: 'vue-chimera.js', 12 | options 13 | }) 14 | 15 | } 16 | 17 | module.exports.meta = require('../package.json') 18 | -------------------------------------------------------------------------------- /nuxt/plugin.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChimera from 'vue-chimera' 3 | 4 | Vue.use(VueChimera, <%= serialize(options, null, 2) %>) 5 | 6 | export default function ({ beforeNuxtRender, app }) { 7 | if (process.server) { 8 | const ChimeraSSR = require('vue-chimera/ssr') 9 | beforeNuxtRender(({ nuxtState }) => { 10 | nuxtState.chimera = ChimeraSSR.getStates() 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chimera", 3 | "version": "3.0.2", 4 | "description": "VueJS RESTful client with reactive endpoints and features", 5 | "main": "dist/vue-chimera.umd.js", 6 | "module": "dist/vue-chimera.es.js", 7 | "unpkg": "dist/vue-chimera.min.js", 8 | "typings": "types/index.d.ts", 9 | "scripts": { 10 | "build": "yarn build:browser && yarn build:es && yarn build:umd", 11 | "build:browser": "rollup --config build/rollup.config.browser.js", 12 | "build:es": "rollup --config build/rollup.config.es.js", 13 | "build:umd": "rollup --config build/rollup.config.umd.js", 14 | "lint": "eslint . --ext .js,.vue", 15 | "test": "yarn test:unit", 16 | "test:unit": "jest", 17 | "report-coverage": "codecov", 18 | "docs:dev": "vuepress dev docs", 19 | "docs:build": "vuepress build docs", 20 | "release:prepare": "shipjs prepare", 21 | "release:trigger": "shipjs trigger" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/chimera-js/vue-chimera.git" 26 | }, 27 | "keywords": [ 28 | "vuejs", 29 | "awesome-vue", 30 | "restful", 31 | "rest", 32 | "client", 33 | "http", 34 | "vue-rest", 35 | "chimera", 36 | "vue-chimera" 37 | ], 38 | "author": "Sasan Farrokh", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/chimera-js/vue-chimera/issues" 42 | }, 43 | "homepage": "https://github.com/chimera-js/vue-chimera#readme", 44 | "dependencies": { 45 | "axios": ">=0.15.0", 46 | "p-debounce": "^1.0.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.8.6", 50 | "@babel/preset-env": "^7.8.6", 51 | "@vue/server-test-utils": "^1.0.0-beta.31", 52 | "@vue/test-utils": "^1.0.0-beta.31", 53 | "babel-jest": "^25.1.0", 54 | "codecov": "^3.6.5", 55 | "eslint": "^5.4.0", 56 | "eslint-config-standard": "^12.0.0", 57 | "eslint-plugin-import": "^2.14.0", 58 | "eslint-plugin-node": "^7.0.1", 59 | "eslint-plugin-promise": "^4.0.0", 60 | "eslint-plugin-standard": "^4.0.0", 61 | "eslint-plugin-vue": "^6.2.2", 62 | "jest": "^25.1.0", 63 | "node-fetch": "^2.6.0", 64 | "rollup": "^1.32.1", 65 | "rollup-plugin-babel": "^4.0.2", 66 | "rollup-plugin-commonjs": "^10.1.0", 67 | "rollup-plugin-node-resolve": "^5.2.0", 68 | "rollup-plugin-terser": "^5.3.0", 69 | "shipjs": "0.18.0", 70 | "sinon": "^6.1.4", 71 | "vue": "^2.6.11", 72 | "vue-jest": "^4.0.0-beta.2", 73 | "vue-server-renderer": "^2.6.11", 74 | "vue-template-compiler": "^2.6.11", 75 | "vuepress": "^1.3.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ship.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | afterPublish ({ exec }) { 3 | exec('yarn report-coverage') 4 | }, 5 | testCommandBeforeRelease: ({ isYarn }) => isYarn ? 'yarn test' : 'npm run test' 6 | } 7 | -------------------------------------------------------------------------------- /src/Endpoint.js: -------------------------------------------------------------------------------- 1 | import pDebounce from 'p-debounce' 2 | import * as events from './events' 3 | import { isPlainObject, mergeExistingKeys, noopReturn, warn } from './utils' 4 | import axiosAdapter from './http/axiosAdapter' 5 | 6 | const INITIAL_RESPONSE = { 7 | status: null, 8 | data: null, 9 | headers: undefined, 10 | error: null, 11 | lastLoaded: undefined 12 | } 13 | 14 | export default class Endpoint { 15 | constructor (opts, initial) { 16 | if (typeof opts === 'string') opts = { url: opts, key: opts } 17 | 18 | if (!opts) { 19 | warn('Invalid options', opts) 20 | throw new Error('[Chimera]: invalid options') 21 | } 22 | 23 | opts = this.options ? this.constructor.applyDefaults(this.options, opts) : opts 24 | 25 | let { 26 | debounce, 27 | transformer, 28 | interval, 29 | headers, 30 | on: listeners, 31 | auto, 32 | prefetch, 33 | ...options 34 | } = opts 35 | 36 | options.method = (options.method || 'get').toLowerCase() 37 | 38 | this.fetchDebounced = debounce !== false 39 | ? pDebounce(this.fetch.bind(this), debounce || 50, { leading: true }) 40 | : this.fetch 41 | 42 | // Set Transformers 43 | this.setTransformer(transformer) 44 | 45 | this.prefetched = false 46 | this.loading = false 47 | 48 | // Set Events 49 | this.listeners = Object.create(null) 50 | if (isPlainObject(listeners)) { 51 | for (const key in listeners) { 52 | this.on(key, listeners[key]) 53 | } 54 | } 55 | 56 | Object.assign(this, options) 57 | this.requestHeaders = headers 58 | 59 | // Handle type on auto 60 | this.auto = typeof auto === 'string' ? auto.toLowerCase() === options.method : !!auto 61 | this.prefetch = prefetch != null ? prefetch : this.auto 62 | 63 | Object.assign(this, INITIAL_RESPONSE, initial || {}) 64 | 65 | interval && this.startInterval(interval) 66 | } 67 | 68 | setTransformer (transformer) { 69 | if (typeof transformer === 'function') { 70 | this.responseTransformer = transformer 71 | this.errorTransformer = transformer 72 | } else if (isPlainObject(transformer)) { 73 | const { response, error } = transformer 74 | this.responseTransformer = response || noopReturn 75 | this.errorTransformer = error || noopReturn 76 | } else { 77 | this.responseTransformer = noopReturn 78 | this.errorTransformer = noopReturn 79 | } 80 | } 81 | 82 | on (event, handler) { 83 | this.listeners[event] = (this.listeners[event] || []).concat(handler) 84 | return this 85 | } 86 | 87 | emit (event) { 88 | (this.listeners[event] || []).forEach(handler => { 89 | handler(this, event) 90 | }) 91 | } 92 | 93 | fetch (force, extraOptions) { 94 | if (this.cache && !force) { 95 | let cacheValue = this.getCache() 96 | if (cacheValue) { 97 | this.setResponse(cacheValue, true) 98 | return Promise.resolve(cacheValue) 99 | } 100 | } 101 | let request = this 102 | if (isPlainObject(extraOptions)) { 103 | request = Object.create(this) 104 | // Merge extra options 105 | if (extraOptions.params) extraOptions.params = Object.assign({}, request.params, extraOptions.params) 106 | if (extraOptions.headers) extraOptions.requestHeaders = Object.assign({}, request.requestHeaders, extraOptions.headers) 107 | Object.assign(request, extraOptions) 108 | } 109 | this.loading = true 110 | this.emit(events.LOADING) 111 | return this.http.request(request).then(res => { 112 | this.loading = false 113 | this.setResponse(res, true) 114 | this.setCache(res) 115 | this.emit(events.SUCCESS) 116 | return res 117 | }).catch(err => { 118 | this.loading = false 119 | this.setResponse(err, false) 120 | if (this.http.isCancelError(err)) return 121 | if (this.http.isTimeoutError) { 122 | this.emit(events.TIMEOUT) 123 | } 124 | this.emit(events.ERROR) 125 | 126 | throw err 127 | }) 128 | } 129 | 130 | reload (force) { 131 | return this.fetchDebounced(force) 132 | } 133 | 134 | send (params = {}) { 135 | return this.fetch(true, { params }) 136 | } 137 | 138 | cancel (silent) { 139 | if (this.loading) { 140 | this.http.cancel(this) 141 | !silent && this.emit(events.CANCEL) 142 | } 143 | } 144 | 145 | getCacheKey () { 146 | if (this.key) return this.key 147 | /* istanbul ignore next */ 148 | throw new Error('[Chimera]: cannot use cache without "key" property') 149 | } 150 | 151 | getCache () { 152 | return this.cache ? this.cache.getItem(this.getCacheKey()) : undefined 153 | } 154 | 155 | setCache (value) { 156 | this.cache && this.cache.setItem(this.getCacheKey(), value) 157 | } 158 | 159 | deleteCache () { 160 | this.cache && this.cache.removeItem(this.getCacheKey()) 161 | } 162 | 163 | setResponse (res, success) { 164 | res = res || {} 165 | this.status = res.status 166 | this.data = success ? this.responseTransformer(res.data, this) : null 167 | this.error = !success ? this.errorTransformer(res.data, this) : null 168 | 169 | this.headers = !this.light ? res.headers || {} : undefined 170 | this.lastLoaded = !this.light ? new Date() : undefined 171 | } 172 | 173 | startInterval (ms) { 174 | /* istanbul ignore if */ 175 | if (typeof ms !== 'number') throw new Error('[Chimera]: interval should be number') 176 | /* istanbul ignore if */ 177 | if (typeof process !== 'undefined' && process.server) return 178 | 179 | this._interval = ms 180 | this.stopInterval() 181 | this._interval_id = typeof window !== 'undefined' && setInterval(() => { 182 | this.cancel() 183 | this.reload(true) 184 | }, this._interval) 185 | } 186 | 187 | stopInterval () { 188 | if (this._interval_id) { 189 | clearInterval(this._interval_id) 190 | this._interval_id = null 191 | this._interval = false 192 | } 193 | } 194 | 195 | get looping () { 196 | return !!this._interval 197 | } 198 | 199 | get response () { 200 | return mergeExistingKeys(INITIAL_RESPONSE, this) 201 | } 202 | 203 | toString () { 204 | return JSON.stringify(this.response) 205 | } 206 | 207 | static applyDefaults (base, options) { 208 | if (!base) return options 209 | options = { ...options } 210 | const strats = this.optionMergeStrategies 211 | Object.keys(base).forEach(key => { 212 | options[key] = (key in options && strats[key]) 213 | ? strats[key](base[key], options[key]) 214 | : (key in options ? options[key] : base[key]) 215 | }) 216 | return options 217 | } 218 | } 219 | 220 | Endpoint.prototype.http = axiosAdapter 221 | 222 | const strats = Endpoint.optionMergeStrategies = {} 223 | 224 | strats.headers = strats.params = strats.transformers = function (base, opts) { 225 | if (isPlainObject(base) && isPlainObject(opts)) { 226 | return { 227 | ...base, 228 | ...opts 229 | } 230 | } 231 | return opts === undefined ? base : opts 232 | } 233 | strats.on = function (fromVal, toVal) { 234 | const value = { ...(fromVal || {}) } 235 | Object.entries(toVal || {}).forEach(([event, handlers]) => { 236 | const h = value[event] 237 | value[event] = h ? [].concat(h).concat(handlers) : handlers 238 | }) 239 | return value 240 | } 241 | -------------------------------------------------------------------------------- /src/NullEndpoint.js: -------------------------------------------------------------------------------- 1 | import Endpoint from './Endpoint' 2 | 3 | export default class NullEndpoint extends Endpoint { 4 | constructor () { 5 | super({}) 6 | } 7 | fetch (force) { 8 | return Promise.reject(new Error('[Chimera]: Fetching null endpoint')) 9 | } 10 | cancel () {} 11 | } 12 | -------------------------------------------------------------------------------- /src/VueChimera.js: -------------------------------------------------------------------------------- 1 | import BaseEndpoint from './Endpoint' 2 | import NullEndpoint from './NullEndpoint' 3 | import { isPlainObject, getServerContext, warn, removeUndefined } from './utils' 4 | import { createAxios } from './http/axiosAdapter' 5 | 6 | const shouldAutoFetch = r => r.auto && (!r.prefetched || r.prefetch === 'override') 7 | 8 | export default class VueChimera { 9 | constructor (vm, { ...endpoints }, options) { 10 | this._vm = vm 11 | this._watchers = [] 12 | 13 | let { deep, ssrContext, axios, ...endpointOptions } = options || {} 14 | const LocalEndpoint = this.LocalEndpoint = class Endpoint extends BaseEndpoint {} 15 | bindVmToEvents(endpointOptions, this._vm) 16 | LocalEndpoint.prototype.options = LocalEndpoint.applyDefaults(LocalEndpoint.prototype.options, endpointOptions) 17 | Object.assign(this, removeUndefined({ deep, ssrContext, axios })) 18 | LocalEndpoint.prototype.axios = createAxios(typeof this.axios === 'function' && !this.axios.request ? this.axios.call(vm) : this.axios) 19 | 20 | this._ssrContext = getServerContext(this.ssrContext) 21 | this._server = vm.$isServer 22 | const watchOption = { 23 | immediate: true, 24 | deep: this._deep, 25 | sync: true 26 | } 27 | 28 | for (let key in endpoints) { 29 | if (key.charAt(0) === '$') { 30 | delete endpoints[key] 31 | continue 32 | } 33 | 34 | let r = endpoints[key] 35 | if (typeof r === 'function') { 36 | this._watchers.push([ 37 | () => r.call(vm), 38 | (t, f) => this.updateEndpoint(key, t, f), 39 | watchOption 40 | ]) 41 | } else { 42 | r = endpoints[key] = this.endpointFrom(r) 43 | if (!this._server) { 44 | shouldAutoFetch(r) && r.reload() 45 | } 46 | } 47 | } 48 | 49 | Object.defineProperty(endpoints, '$cancelAll', { value: () => this.cancelAll() }) 50 | Object.defineProperty(endpoints, '$loading', { get () { return !!Object.values(this).find(el => !!el.loading) } }) 51 | this.endpoints = endpoints 52 | } 53 | 54 | init () { 55 | this._watchers = this._watchers.map(w => this._vm.$watch(...w)) 56 | } 57 | 58 | initServer () { 59 | this._vm.$_chimeraPromises = [] 60 | Object.values(this.endpoints).forEach(endpoint => { 61 | if (endpoint.auto && endpoint.prefetch) { 62 | /* istanbul ignore if */ 63 | if (!endpoint.key) { 64 | warn('used prefetch with no key associated with endpoint!') 65 | return 66 | } 67 | this._vm.$_chimeraPromises.push(endpoint.fetch(true, { timeout: endpoint.prefetchTimeout }).then(() => endpoint).catch(() => null)) 68 | } 69 | }) 70 | } 71 | 72 | updateEndpoint (key, newValue, oldValue) { 73 | const oldEndpoint = this.endpoints[key] 74 | const newEndpoint = this.endpointFrom(newValue, oldEndpoint && oldEndpoint.keepData ? oldEndpoint.response : null) 75 | 76 | if (oldValue && oldEndpoint) { 77 | oldEndpoint.stopInterval() 78 | oldEndpoint.cancel(true) 79 | newEndpoint.lastLoaded = oldEndpoint.lastLoaded 80 | } 81 | 82 | if (!this._server) { 83 | if (shouldAutoFetch(newEndpoint)) newEndpoint.reload() 84 | } 85 | this._vm.$set(this.endpoints, key, newEndpoint) 86 | } 87 | 88 | endpointFrom (value, initial) { 89 | if (value == null) return new NullEndpoint() 90 | if (typeof value === 'string') value = { url: value } 91 | 92 | bindVmToEvents(value, this._vm) 93 | 94 | const endpoint = new (this.LocalEndpoint || BaseEndpoint)(value, initial) 95 | 96 | if (!this._server && !initial && endpoint.key && endpoint.prefetch && this._ssrContext) { 97 | initial = this._ssrContext[value.key] 98 | if (initial) initial.prefetched = true 99 | Object.assign(endpoint, initial) 100 | } 101 | return endpoint 102 | } 103 | 104 | cancelAll () { 105 | Object.values(this.endpoints).forEach(r => { 106 | r.cancel() 107 | }) 108 | } 109 | 110 | destroy () { 111 | const vm = this._vm 112 | 113 | this.cancelAll() 114 | Object.values(this.endpoints).forEach(r => { 115 | r.stopInterval() 116 | }) 117 | delete vm._chimera 118 | } 119 | } 120 | 121 | function bindVmToEvents (value, vm) { 122 | if (isPlainObject(value.on)) { 123 | const bindVm = (handler) => { 124 | if (typeof handler === 'function') { 125 | handler = handler.bind(vm) 126 | } 127 | if (typeof handler === 'string') handler = vm[handler] 128 | return handler 129 | } 130 | Object.entries(value.on).forEach(([event, handlers]) => { 131 | value.on[event] = (Array.isArray(handlers) ? handlers.map(bindVm) : bindVm(handlers)) 132 | }) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/cache/MemoryCache.js: -------------------------------------------------------------------------------- 1 | module.exports = class MemoryCache { 2 | constructor (expiration) { 3 | this.expiration = expiration || 1000 * 60 4 | this._store = {} 5 | } 6 | 7 | /** 8 | * 9 | * @param key Key for the cache 10 | * @param value Value for cache persistence 11 | * @param expiration Expiration time in milliseconds 12 | */ 13 | setItem (key, value, expiration) { 14 | this._store[key] = { 15 | expiration: Date.now() + (expiration || this.expiration), 16 | value 17 | } 18 | } 19 | 20 | /** 21 | * If Cache exists return the Parsed Value, If Not returns {null} 22 | * 23 | * @param key 24 | */ 25 | getItem (key) { 26 | let item = this._store[key] 27 | 28 | if (item && item.value && Date.now() <= item.expiration) { 29 | return item.value 30 | } 31 | 32 | this.removeItem(key) 33 | return null 34 | } 35 | 36 | removeItem (key) { 37 | delete this._store[key] 38 | } 39 | 40 | keys () { 41 | return Object.keys(this._store) 42 | } 43 | 44 | all () { 45 | return this.keys().reduce((obj, str) => { 46 | obj[str] = this._store[str] 47 | return obj 48 | }, {}) 49 | } 50 | 51 | length () { 52 | return this.keys().length 53 | } 54 | 55 | clear () { 56 | this._store = {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cache/StorageCache.js: -------------------------------------------------------------------------------- 1 | const MemoryCache = require('./MemoryCache') 2 | 3 | module.exports = class StorageCache extends MemoryCache { 4 | constructor (key, expiration, sessionStorage = false) { 5 | super(expiration) 6 | this.key = key 7 | 8 | const storage = sessionStorage ? 'sessionStorage' : 'localStorage' 9 | /* istanbul ignore if */ 10 | if (typeof window === 'undefined' || !window[storage]) { 11 | throw Error(`StorageCache: ${storage} is not available.`) 12 | } else { 13 | this.storage = window[storage] 14 | } 15 | 16 | try { 17 | this._store = JSON.parse(this.storage.getItem(key)) || {} 18 | } catch (e) { 19 | this.clear() 20 | this._store = {} 21 | } 22 | } 23 | 24 | setItem (key, value, expiration) { 25 | super.setItem(key, value, expiration) 26 | this.storage.setItem(this.key, JSON.stringify(this._store)) 27 | } 28 | 29 | clear () { 30 | this.storage.removeItem(this.key) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ChimeraEndpoint.js: -------------------------------------------------------------------------------- 1 | import NullEndpoint from '../NullEndpoint' 2 | import Endpoint from '../Endpoint' 3 | import VueChimera from '../VueChimera' 4 | import { getServerContext } from '../utils' 5 | 6 | export default { 7 | 8 | inheritAttrs: false, 9 | 10 | props: { 11 | options: { 12 | type: [Object, String] 13 | }, 14 | tag: { 15 | type: String, 16 | default: null 17 | }, 18 | ssrContext: { 19 | type: String, 20 | default: null 21 | } 22 | }, 23 | 24 | data () { 25 | return { 26 | endpoint: this.getEndpoint() 27 | } 28 | }, 29 | 30 | render (h) { 31 | let result = this.$scopedSlots.default(this.endpoint) 32 | if (Array.isArray(result)) { 33 | result = result.concat(this.$slots.default) 34 | } else { 35 | result = [result].concat(this.$slots.default) 36 | } 37 | return this.tag ? h(this.tag, result) : result[0] 38 | }, 39 | 40 | created () { 41 | const ep = this.endpoint 42 | if (this.$isServer && ep.key) { 43 | this.$_chimeraPromises = [ep.fetch(true).then(() => ep).catch(() => null)] 44 | } 45 | }, 46 | 47 | mounted () { 48 | const ep = this.endpoint 49 | if (ep.auto && (!ep.data || ep.prefetch === 'override')) { 50 | ep.reload() 51 | } 52 | }, 53 | 54 | methods: { 55 | getEndpoint () { 56 | let value = this.options 57 | if (value == null) return new NullEndpoint() 58 | if (typeof value === 'string') value = { url: value } 59 | 60 | const endpoint = new Endpoint(value) 61 | endpoint.emit = ev => { 62 | Endpoint.prototype.emit.call(endpoint, ev) 63 | this.$emit(ev, endpoint) 64 | } 65 | 66 | this._ssrContext = getServerContext(this.ssrContext || VueChimera.prototype.ssrContext) 67 | if (!this._server && endpoint.key && endpoint.prefetch && this._ssrContext) { 68 | const initial = this._ssrContext[endpoint.key] 69 | if (initial) initial.prefetched = true 70 | Object.assign(endpoint, initial) 71 | } 72 | 73 | return endpoint 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | export const SUCCESS = 'success' 2 | export const ERROR = 'error' 3 | export const CANCEL = 'cancel' 4 | export const LOADING = 'loading' 5 | export const TIMEOUT = 'timeout' 6 | -------------------------------------------------------------------------------- /src/http/axiosAdapter.js: -------------------------------------------------------------------------------- 1 | import Axios, { CancelToken } from 'axios' 2 | import { isPlainObject, removeUndefined } from '../utils' 3 | 4 | export function createAxios (config) { 5 | if (typeof config === 'function') { 6 | if (typeof config.request === 'function') return config 7 | return config() 8 | } 9 | if (isPlainObject(config)) { 10 | return Axios.create(config) 11 | } 12 | return Axios 13 | } 14 | 15 | export default { 16 | request (endpoint) { 17 | const axios = createAxios(endpoint.axios) 18 | const request = (({ 19 | url, 20 | method, 21 | baseURL, 22 | requestHeaders: headers, 23 | timeout 24 | }) => ({ 25 | url, 26 | method, 27 | baseURL, 28 | headers, 29 | timeout 30 | }))(endpoint) 31 | 32 | removeUndefined(request) 33 | 34 | request[(endpoint.method || 'get') !== 'get' ? 'data' : 'params'] = endpoint.params 35 | request.cancelToken = new CancelToken(c => { 36 | endpoint._canceler = c 37 | }) 38 | return axios.request(request).catch(err => { 39 | throw Object.assign(err, err.response) 40 | }) 41 | }, 42 | cancel (endpoint) { 43 | if (typeof endpoint._canceler === 'function') endpoint._canceler() 44 | endpoint._canceler = null 45 | }, 46 | isCancelError (err) { 47 | return Axios.isCancel(err) 48 | }, 49 | isTimeoutError (err) { 50 | return err.message && !err.response && err.message.indexOf('timeout') !== -1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.es.js: -------------------------------------------------------------------------------- 1 | export * from './events' 2 | export { default as MemoryCache } from './cache/MemoryCache' 3 | export { default as StorageCache } from './cache/StorageCache' 4 | export { default as Endpoint } from './Endpoint' 5 | 6 | export { default } from './index' 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import mixin from './mixin' 2 | import VueChimera from './VueChimera' 3 | import ChimeraEndpoint from './components/ChimeraEndpoint' 4 | import Endpoint from './Endpoint' 5 | 6 | const DEFAULT_OPTIONS = { 7 | cache: null, 8 | debounce: 50, 9 | deep: true, 10 | keepData: true, 11 | auto: 'get', // false, true, '%METHOD%', 12 | prefetch: true, 13 | prefetchTimeout: 4000, 14 | transformer: null, 15 | ssrContext: null 16 | } 17 | 18 | export function install (Vue, options = {}) { 19 | options = Object.assign({}, DEFAULT_OPTIONS, options) 20 | 21 | Vue.mixin(mixin) 22 | Vue.component('chimera-endpoint', ChimeraEndpoint) 23 | 24 | const { deep, ssrContext, axios, ...endpointOptions } = options 25 | Endpoint.prototype.options = endpointOptions 26 | Object.assign(VueChimera.prototype, { 27 | deep, 28 | ssrContext, 29 | axios 30 | }) 31 | 32 | // const merge = Vue.config.optionMergeStrategies.methods 33 | Vue.config.optionMergeStrategies.chimera = function (toVal, fromVal, vm) { 34 | if (!toVal) return fromVal 35 | if (!fromVal) return toVal 36 | 37 | if (typeof fromVal === 'function') fromVal = fromVal.call(vm) 38 | if (typeof toVal === 'function') toVal = toVal.call(vm) 39 | 40 | return Object.assign({}, toVal, fromVal, toVal.$options && fromVal.$options ? { 41 | $options: Endpoint.applyDefaults(toVal.$options, fromVal.$options) 42 | } : {}) 43 | } 44 | } 45 | 46 | export default install 47 | export { Endpoint, ChimeraEndpoint } 48 | -------------------------------------------------------------------------------- /src/mixin.js: -------------------------------------------------------------------------------- 1 | import VueChimera from './VueChimera' 2 | import { hasKey, isPlainObject } from './utils' 3 | 4 | export default { 5 | beforeCreate () { 6 | const vmOptions = this.$options 7 | let chimera 8 | 9 | // Stop if instance doesn't have chimera or already initialized 10 | /* istanbul ignore if */ 11 | if (!vmOptions.chimera || vmOptions._chimera) return 12 | 13 | if (typeof vmOptions.chimera === 'function') { 14 | // Initialize with function 15 | vmOptions.chimera = vmOptions.chimera.call(this) 16 | } 17 | /* istanbul ignore else */ 18 | if (isPlainObject(vmOptions.chimera)) { 19 | const { $options, ...endpoints } = vmOptions.chimera 20 | chimera = new VueChimera(this, endpoints, $options) 21 | } else { 22 | throw new Error('[Chimera]: chimera options should be an object or a function that returns object') 23 | } 24 | 25 | if (!Object.prototype.hasOwnProperty.call(this, '$chimera')) { 26 | Object.defineProperty(this, '$chimera', { 27 | get: () => chimera.endpoints 28 | }) 29 | } 30 | Object.keys(chimera.endpoints).forEach(key => { 31 | if (!(hasKey(vmOptions.computeds, key) || hasKey(vmOptions.props, key) || hasKey(vmOptions.methods, key))) { 32 | Object.defineProperty(this, key, { 33 | get: () => this.$chimera[key], 34 | enumerable: true, 35 | configurable: true 36 | }) 37 | } 38 | }) 39 | this._chimera = chimera 40 | }, 41 | 42 | data () { 43 | /* istanbul ignore if */ 44 | if (!this._chimera) return {} 45 | return { 46 | $chimera: this._chimera.endpoints 47 | } 48 | }, 49 | 50 | created () { 51 | /* istanbul ignore if */ 52 | if (!this._chimera) return 53 | this._chimera.init() 54 | this.$isServer && this._chimera.initServer() 55 | }, 56 | 57 | beforeDestroy () { 58 | /* istanbul ignore if */ 59 | if (!this._chimera) return 60 | this._chimera.destroy() 61 | }, 62 | 63 | serverPrefetch (...args) { 64 | /* istanbul ignore if */ 65 | if (!this.$_chimeraPromises) return 66 | const ChimeraSSR = require('../ssr/index') 67 | return Promise.all(this.$_chimeraPromises).then(results => { 68 | results.forEach(endpoint => { 69 | endpoint && ChimeraSSR.addEndpoint(endpoint) 70 | }) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function isPlainObject (value) { 2 | return typeof value === 'object' && value && Object.prototype.toString(value) === '[object Object]' 3 | } 4 | 5 | export function mergeExistingKeys (...obj) { 6 | let o = Object.assign({}, ...obj) 7 | return Object.keys(obj[0]).reduce((carry, item) => { 8 | carry[item] = o[item] 9 | return carry 10 | }, {}) 11 | } 12 | 13 | export const hasKey = (obj, key) => key in (obj || {}) 14 | 15 | export function removeUndefined (obj) { 16 | Object.keys(obj).forEach(key => { 17 | if (obj[key] === undefined) delete obj[key] 18 | }) 19 | return obj 20 | } 21 | 22 | export function getServerContext (contextString) { 23 | try { 24 | let context = window 25 | const keys = contextString.split('.') 26 | keys.forEach(key => { 27 | context = context[key] 28 | }) 29 | return context 30 | } catch (e) {} 31 | return null 32 | } 33 | 34 | export function noopReturn (arg) { return arg } 35 | 36 | export function warn (arg, ...args) { 37 | // eslint-disable-next-line no-console 38 | console.warn('[Chimera]: ' + arg, ...args) 39 | } 40 | -------------------------------------------------------------------------------- /ssr/index.js: -------------------------------------------------------------------------------- 1 | const results = {} 2 | 3 | exports.addEndpoint = function (r) { 4 | results[r.key] = r.response 5 | } 6 | 7 | exports.getStates = function () { 8 | return results 9 | } 10 | 11 | exports.serializeStates = function () { 12 | return JSON.stringify(results) 13 | } 14 | 15 | exports.exportStates = function (attachTo, globalName) { 16 | return `${attachTo}.${globalName} = ${exports.serializeStates()};` 17 | } 18 | -------------------------------------------------------------------------------- /tests/mocks/axios.mock.js: -------------------------------------------------------------------------------- 1 | module.exports = (fn) => { 2 | const axiosMock = jest.fn(request => Promise.resolve(fn ? fn(request) : request)) 3 | axiosMock.request = axiosMock 4 | return axiosMock 5 | } 6 | -------------------------------------------------------------------------------- /tests/ssr.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@vue/server-test-utils' 2 | import { createLocalVue } from '@vue/test-utils' 3 | import VueChimera from '../src/index' 4 | import ssr from '../ssr/index' 5 | 6 | let localVue 7 | 8 | beforeEach(() => { 9 | localVue = createLocalVue() 10 | localVue.mixin({ 11 | beforeCreate () { 12 | Object.defineProperty(this, '$isServer', { 13 | get () { 14 | return true 15 | } 16 | }) 17 | } 18 | }) 19 | localVue.use(VueChimera, { 20 | prefetch: true, 21 | ssrContext: '__CONTEXT__.chimera' 22 | }) 23 | }) 24 | 25 | describe('test-server-side-rendering', function () { 26 | it('should render correctly', async function () { 27 | await render({ 28 | name: 'ssr-component', 29 | render (h) { 30 | return h('span', {}, [this.$chimera.users.loading ? 't' : 'f']) 31 | }, 32 | chimera: { 33 | users: { 34 | url: 'test', 35 | key: 'test', 36 | http: { 37 | request: () => Promise.resolve({ data: { test: 1 } }) 38 | } 39 | } 40 | } 41 | }, { 42 | localVue 43 | }) 44 | 45 | expect(ssr.getStates().test.data).toEqual({ test: 1 }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/unit/axios.test.js: -------------------------------------------------------------------------------- 1 | import axiosAdapter from '../../src/http/axiosAdapter' 2 | import Axios from 'axios' 3 | 4 | describe('test-axios-adapter', function () { 5 | it('should instantiate axios correctly', function () { 6 | const spy = jest.spyOn(Axios, 'create') 7 | axiosAdapter.request({ 8 | method: 'get', 9 | axios: { 10 | baseURL: 'http://test.test' 11 | } 12 | }) 13 | expect(spy).toBeCalledTimes(1) 14 | }) 15 | it('should send params in data', async function () { 16 | let axiosConfig 17 | const params = { test: 1 } 18 | await axiosAdapter.request({ 19 | method: 'post', 20 | params, 21 | axios: () => ({ 22 | request: async options => { 23 | axiosConfig = options 24 | } 25 | }) 26 | }) 27 | expect(axiosConfig.data).toEqual(params) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unit/cache.test.js: -------------------------------------------------------------------------------- 1 | import Endpoint from '../../src/Endpoint' 2 | import MemoryCache from '../../src/cache/MemoryCache' 3 | import StorageCache from '../../src/cache/StorageCache' 4 | 5 | let axiosResponse, axiosMock 6 | beforeEach(() => { 7 | axiosResponse = { 8 | data: { test: 1 }, 9 | headers: {}, 10 | status: 200 11 | } 12 | axiosMock = jest.fn(() => Promise.resolve(axiosResponse)) 13 | axiosMock.request = axiosMock 14 | }) 15 | 16 | describe('test-memory-cache', function () { 17 | it('should use cache', async function () { 18 | const memoryCache = new MemoryCache(1000) 19 | const endpoint = new Endpoint({ 20 | url: '/users', 21 | key: 'users', 22 | auto: false, 23 | cache: memoryCache 24 | }) 25 | 26 | endpoint.http = axiosMock 27 | const setSpy = jest.spyOn(memoryCache, 'setItem') 28 | const getSpy = jest.spyOn(memoryCache, 'getItem') 29 | const removeSpy = jest.spyOn(memoryCache, 'removeItem') 30 | 31 | await endpoint.fetch() 32 | expect(axiosMock).toBeCalled() 33 | expect(setSpy).toBeCalledWith(endpoint.getCacheKey(), axiosResponse) 34 | expect(endpoint.data).toEqual(axiosResponse.data) 35 | 36 | await endpoint.fetch() 37 | expect(axiosMock).toBeCalledTimes(1) 38 | expect(getSpy).toReturnWith(axiosResponse) 39 | 40 | endpoint.deleteCache() 41 | expect(removeSpy).toBeCalled() 42 | await endpoint.fetch() 43 | expect(axiosMock).toBeCalledTimes(2) 44 | 45 | expect(memoryCache.keys()).toEqual([endpoint.getCacheKey()]) 46 | expect(memoryCache.length()).toEqual(1) 47 | expect(memoryCache.all()[endpoint.getCacheKey()]).toHaveProperty('value', axiosResponse) 48 | 49 | memoryCache.clear() 50 | expect(memoryCache.all()).toEqual({}) 51 | }) 52 | }) 53 | 54 | describe('test-storage-cache', function () { 55 | beforeAll(() => { 56 | global.window.localStorage = new MemoryCache() 57 | }) 58 | it('should work', async function () { 59 | const storageCache = new StorageCache('key') 60 | const endpoint = new Endpoint({ 61 | url: '/users', 62 | key: 'users', 63 | auto: false, 64 | cache: storageCache 65 | }) 66 | 67 | endpoint.http = axiosMock 68 | 69 | await endpoint.fetch() 70 | expect(axiosMock).toBeCalledTimes(1) 71 | expect(endpoint.data).toEqual(axiosResponse.data) 72 | 73 | await endpoint.fetch() 74 | expect(axiosMock).toBeCalledTimes(1) 75 | 76 | const newEndpoint = new Endpoint({ 77 | url: '/users', 78 | key: 'users', 79 | auto: false, 80 | axios: axiosMock, 81 | cache: new StorageCache('key') 82 | }) 83 | await newEndpoint.fetch() 84 | expect(axiosMock).toBeCalledTimes(1) 85 | expect(endpoint.data).toEqual(axiosResponse.data) 86 | }) 87 | 88 | it('should not raise error', function () { 89 | window.localStorage.setItem('key', '{BAD JSON') 90 | const storageCache = new StorageCache('key') 91 | expect(storageCache.all()).toEqual({}) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /tests/unit/chimera.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChimera from '../../src/VueChimera' 3 | import Endpoint from '../../src/Endpoint' 4 | import NullEndpoint from '../../src/NullEndpoint' 5 | 6 | global.window = { 7 | __STATE__: { 8 | chimera: {} 9 | } 10 | } 11 | 12 | Vue.config.devtools = false 13 | Vue.config.productionTip = false 14 | 15 | const chimeraFactory = function (endpoints, vm, options) { 16 | vm = vm || new Vue() 17 | return new VueChimera(vm, endpoints, { 18 | deep: true, 19 | ssrContext: '__STATE__.chimera', 20 | ...options 21 | }) 22 | } 23 | 24 | const axiosMock = () => { 25 | const axiosResponse = { 26 | data: { test: 1 }, 27 | headers: {}, 28 | status: 200 29 | } 30 | const axiosMock = jest.fn(() => Promise.resolve(axiosResponse)) 31 | axiosMock.request = axiosMock 32 | return axiosMock 33 | } 34 | 35 | describe('test-vue-chimera', function () { 36 | it('should instantiate null endpoint', function () { 37 | const { endpoints } = chimeraFactory({ 38 | n: null 39 | }) 40 | 41 | expect(endpoints.n).toBeInstanceOf(NullEndpoint) 42 | }) 43 | 44 | it('should bind vm to listeners', function () { 45 | let self, endpoint 46 | const spy = jest.fn() 47 | const chimera = chimeraFactory({ 48 | test: { 49 | url: '/test', 50 | auto: false, 51 | on: { 52 | test (newEndpoint) { 53 | self = this 54 | endpoint = newEndpoint 55 | }, 56 | event: 'spy' 57 | } 58 | } 59 | }, new Vue({ 60 | methods: { 61 | spy 62 | } 63 | })) 64 | 65 | chimera.endpoints.test.emit('test') 66 | chimera.endpoints.test.emit('event') 67 | expect(self).toBe(chimera._vm) 68 | expect(endpoint).toBe(chimera.endpoints.test) 69 | expect(spy).toBeCalled() 70 | }) 71 | 72 | it('should cancel all endpoints', function () { 73 | const chimera = chimeraFactory({ 74 | test: '/1', 75 | test2: '/2' 76 | }) 77 | 78 | const spy = jest.spyOn(Endpoint.prototype, 'cancel') 79 | 80 | chimera.cancelAll() 81 | chimera.endpoints.$cancelAll() 82 | expect(spy).toBeCalledTimes(4) 83 | }) 84 | 85 | it('should work with $loading', async function () { 86 | const vm = new Vue() 87 | const chimera = new VueChimera(vm, { 88 | test: '/test', 89 | test2: '/test2' 90 | }, { 91 | axios: axiosMock() 92 | }) 93 | 94 | const p = chimera.endpoints.test.reload() 95 | expect(chimera.endpoints.$loading).toBeTruthy() 96 | await p 97 | expect(chimera.endpoints.$loading).toBeFalsy() 98 | }) 99 | 100 | it('should start interval', async function () { 101 | jest.useFakeTimers() 102 | const chimera = chimeraFactory({ 103 | test: { 104 | url: 'interval', 105 | interval: 1000, 106 | axios: axiosMock() 107 | } 108 | }) 109 | 110 | const endpoints = chimera.endpoints 111 | const spy = jest.spyOn(endpoints.test, 'reload') 112 | expect(endpoints.test.looping).toBeTruthy() 113 | 114 | jest.runOnlyPendingTimers() 115 | expect(spy).toBeCalledTimes(1) 116 | 117 | jest.runOnlyPendingTimers() 118 | expect(spy).toBeCalledTimes(2) 119 | 120 | chimera.endpoints.test.stopInterval() 121 | 122 | jest.runOnlyPendingTimers() 123 | expect(spy).toBeCalledTimes(2) 124 | }) 125 | 126 | it('should apply ssr context', function () { 127 | const vm = new Vue() 128 | const context = window.__CONTEXT__ = { 129 | test: { 130 | data: { __test__: 1 } 131 | } 132 | } 133 | const chimera = new VueChimera(vm, { 134 | test: { 135 | url: 'test', 136 | key: 'test', 137 | prefetch: true 138 | } 139 | }, { 140 | ssrContext: '__CONTEXT__' 141 | }) 142 | 143 | expect(chimera.endpoints.test.data).toEqual(context.test.data) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /tests/unit/component.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import ChimeraEndpoint from '../../src/components/ChimeraEndpoint' 3 | import NullEndpoint from '../../src/NullEndpoint' 4 | import Endpoint from '../../src/Endpoint' 5 | 6 | Endpoint.prototype.auto = false 7 | 8 | const mountEndpoint = (options, slot, props) => mount(ChimeraEndpoint, { 9 | propsData: { 10 | options, 11 | ...props 12 | }, 13 | scopedSlots: { 14 | default: slot || '

{{props.data}}

' 15 | } 16 | }) 17 | 18 | describe('test-chimera-endpoint-component', function () { 19 | it('should instantiate string', function () { 20 | const wrapper = mountEndpoint('/test') 21 | expect(wrapper.vm.endpoint.url).toEqual('/test') 22 | }) 23 | it('should instantiate null', function () { 24 | const wrapper = mountEndpoint(null) 25 | expect(wrapper.vm.endpoint).toBeInstanceOf(NullEndpoint) 26 | }) 27 | 28 | it('should have multiple slot', function () { 29 | const wrapper = mountEndpoint('/test', '

{{props.data}}

', { tag: 'div' }) 30 | expect(wrapper.html()).toBeTruthy() 31 | }) 32 | 33 | it('should instantiate endpoint', async function () { 34 | const wrapper = mountEndpoint({ 35 | url: 'test', 36 | auto: true, 37 | key: 'test', 38 | http: { 39 | request: () => Promise.resolve({ status: 200, data: { test: '__TEST__' } }) 40 | } 41 | }) 42 | 43 | const endpoint = wrapper.vm.endpoint 44 | await (new Promise(resolve => { 45 | endpoint.on('success', resolve) 46 | })) 47 | expect(endpoint.url).toBe('test') 48 | expect(endpoint.auto).toBeTruthy() 49 | expect(endpoint.baseURL).toBeUndefined() 50 | 51 | expect(wrapper.isVueInstance()).toBeTruthy() 52 | expect(wrapper.text().includes('__TEST__')).toBeTruthy() 53 | }) 54 | 55 | it('should use ssr context', async function () { 56 | window.context = { 57 | test: { 58 | data: { 59 | test: '__TEST__' 60 | } 61 | } 62 | } 63 | const wrapper = mountEndpoint({ 64 | url: 'test', 65 | auto: true, 66 | key: 'test' 67 | }, null, { 68 | ssrContext: 'context' 69 | }) 70 | 71 | const endpoint = wrapper.vm.endpoint 72 | expect(endpoint.auto).toBeTruthy() 73 | expect(endpoint.prefetch).toBeTruthy() 74 | expect(wrapper.text().includes('__TEST__')).toBeTruthy() 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/unit/endpoint.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import Endpoint from '../../src/Endpoint' 3 | import NullEndpoint from '../../src/NullEndpoint' 4 | import * as events from '../../src/events' 5 | import axios from 'axios' 6 | import { isPlainObject } from '../../src/utils' 7 | import axiosMock from '../mocks/axios.mock' 8 | 9 | let server 10 | let endpoint 11 | let client 12 | 13 | beforeAll(() => { 14 | server = sinon.createFakeServer() 15 | server.autoRespond = true 16 | client = axios.create() 17 | }) 18 | afterAll(() => { 19 | server.restore() 20 | }) 21 | 22 | beforeEach(() => { 23 | endpoint = new Endpoint({ 24 | url: '/users', 25 | auto: false, 26 | axios: client 27 | }) 28 | }) 29 | 30 | describe('test-instantiation', function () { 31 | it('should instantiate Endpoint from string', function () { 32 | let r = new Endpoint('/users') 33 | expect(r).toBeInstanceOf(Endpoint) 34 | expect(r.method.toLowerCase()).toBe('get') 35 | expect(r.url).toBe('/users') 36 | }) 37 | 38 | it('should instantiate Endpoint from object', function () { 39 | let tr = (v) => v 40 | let r = new Endpoint({ 41 | url: '/u', 42 | auto: false, 43 | method: 'POST', 44 | debounce: false, 45 | transformer: { 46 | response: tr, 47 | error: tr 48 | } 49 | }) 50 | expect(r).toBeInstanceOf(Endpoint) 51 | expect(r.method.toLowerCase()).toBe('post') 52 | expect(r.url).toBe('/u') 53 | expect(r.responseTransformer).toBe(tr) 54 | expect(r.errorTransformer).toBe(tr) 55 | expect(r.fetch === r.fetchDebounced).toBeTruthy() 56 | }) 57 | 58 | it('should be null endpoint', function () { 59 | expect(() => new Endpoint(null)).toThrow() 60 | 61 | let r = new NullEndpoint() 62 | 63 | expect(r.fetch()).rejects.toBeInstanceOf(Error) 64 | }) 65 | 66 | it('should have initial data', function () { 67 | const data = {} 68 | let r = new Endpoint('/users', { data }) 69 | expect(data === r.data).toBeTruthy() 70 | }) 71 | }) 72 | 73 | describe('test-execution', function () { 74 | let data = [{ id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }] 75 | let headers = { 'content-type': 'application/json', 'x-my-custom-header': 'my-custom-value' } 76 | 77 | it('should recieve status 200', function (done) { 78 | server.respondWith('GET', '/users', [ 79 | 200, 80 | headers, 81 | JSON.stringify(data) 82 | ]) 83 | 84 | expect(endpoint.loading).toBeFalsy() 85 | 86 | endpoint.fetch().then(() => { 87 | expect(endpoint.status).toBe(200) 88 | expect(endpoint.data).toEqual(data) 89 | expect(endpoint.headers).toEqual(headers) 90 | expect(endpoint.loading).toBeFalsy() 91 | expect(endpoint.lastLoaded).toBeInstanceOf(Date) 92 | done() 93 | }).catch(err => { 94 | expect(endpoint.loading).toBeFalsy() 95 | done(err) 96 | }) 97 | 98 | expect(endpoint.loading).toBeTruthy() 99 | }) 100 | 101 | it('should receive status 501', function (done) { 102 | server.respondWith('GET', '/users', [ 103 | 501, 104 | { 'Content-Type': 'application/json' }, 105 | JSON.stringify(data) 106 | ]) 107 | 108 | expect(endpoint.loading).toBeFalsy() 109 | endpoint.send().then(done).catch(() => { 110 | expect(endpoint.loading).toBeFalsy() 111 | expect(endpoint.status).toBe(501) 112 | expect(endpoint.error).toEqual(data) 113 | done() 114 | }) 115 | 116 | expect(endpoint.loading).toBeTruthy() 117 | }) 118 | 119 | it('should send with extra', async function () { 120 | endpoint.axios = axiosMock() 121 | endpoint.params = { 122 | a: 1, 123 | b: 2 124 | } 125 | 126 | await endpoint.send({ b: 3 }) 127 | 128 | expect(endpoint.axios.mock.calls[0][0].params).toEqual({ 129 | a: 1, 130 | b: 3 131 | }) 132 | }) 133 | 134 | it('should send headers', async function () { 135 | const headers = { 'X-Test': 'TEST' } 136 | const axios = axiosMock() 137 | let endpoint = new Endpoint({ 138 | url: '/users', 139 | headers, 140 | axios 141 | }) 142 | await endpoint.fetch() 143 | expect(axios.mock.calls[0][0].headers).toEqual(headers) 144 | 145 | await endpoint.fetch(true, { 146 | headers: { 'X-Test2': 'TEST2' } 147 | }) 148 | expect(axios.mock.calls[1][0].headers).toEqual({ 'X-Test': 'TEST', 'X-Test2': 'TEST2' }) 149 | }) 150 | }) 151 | 152 | describe('test-transformers', function () { 153 | let data = [{ id: 1, name: 'User 1' }, { id: 2, name: 'User 2' }] 154 | let tr = res => res.map(val => val.id) 155 | 156 | it('should work with single function', function () { 157 | let endpoint = new Endpoint({ 158 | transformer: tr 159 | }) 160 | expect(endpoint.responseTransformer === endpoint.errorTransformer).toBeTruthy() 161 | expect(endpoint.responseTransformer === tr).toBeTruthy() 162 | }) 163 | 164 | it('should transform response', function (done) { 165 | server.respondWith('GET', '/users', [ 166 | 200, 167 | { 'Content-Type': 'application/json' }, 168 | JSON.stringify(data) 169 | ]) 170 | 171 | endpoint.setTransformer({ response: tr }) 172 | 173 | endpoint.fetch().then(() => { 174 | expect(endpoint.data).toEqual(tr(data)) 175 | done() 176 | }).catch(err => done(err)) 177 | }) 178 | 179 | it('should transform error', function (done) { 180 | server.respondWith('GET', '/users', [ 181 | 500, 182 | { 'Content-Type': 'application/json' }, 183 | JSON.stringify(data) 184 | ]) 185 | 186 | endpoint.setTransformer({ error: tr }) 187 | 188 | endpoint.fetch() 189 | .then(done) 190 | .catch(() => { 191 | expect(endpoint.error).toEqual(tr(data)) 192 | done() 193 | }) 194 | }) 195 | }) 196 | 197 | describe('test-cancellation', function () { 198 | it('should cancel the request', function (done) { 199 | server.respondWith('GET', '/users', [ 200 | 200, 201 | { 'Content-Type': 'application/json' }, 202 | JSON.stringify({}) 203 | ]) 204 | 205 | endpoint.on(events.CANCEL, function () { 206 | expect(endpoint.data).toBeNull() 207 | done() 208 | }) 209 | expect(endpoint.loading).toBeFalsy() 210 | endpoint.fetch() 211 | expect(endpoint.loading).toBeTruthy() 212 | endpoint.cancel() 213 | }) 214 | }) 215 | 216 | describe('test-misc', function () { 217 | it('should serialize', function () { 218 | expect(isPlainObject(endpoint.response)).toBeTruthy() 219 | expect(endpoint.toString()).toEqual(JSON.stringify(endpoint.response)) 220 | }) 221 | it('should be light', async function () { 222 | let endpoint = new Endpoint({ 223 | url: 'test', 224 | light: true 225 | }) 226 | endpoint.http = { 227 | request (request, endpoint) { 228 | return Promise.resolve({ 229 | data: {}, 230 | headers: {}, 231 | status: 200 232 | }) 233 | } 234 | } 235 | 236 | await endpoint.fetch() 237 | 238 | const obj = JSON.parse(JSON.stringify(endpoint.response)) 239 | expect(obj).not.toHaveProperty('lastLoaded') 240 | expect(obj).not.toHaveProperty('headers') 241 | }) 242 | }) 243 | -------------------------------------------------------------------------------- /tests/unit/event.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChimera from '../../src/index' 3 | import * as events from '../../src/events' 4 | import Endpoint from '../../src/Endpoint' 5 | 6 | let server 7 | let httpMock = { 8 | request: () => new Promise((resolve, reject) => { 9 | server = { resolve, reject } 10 | }), 11 | isCancelError (err) { 12 | return !!err.cancel 13 | } 14 | } 15 | beforeEach(() => { 16 | server = null 17 | }) 18 | 19 | describe('test-events', function () { 20 | let endpoint 21 | beforeEach(() => { 22 | endpoint = new Endpoint({ 23 | test: '/test', 24 | auto: false, 25 | http: httpMock 26 | }) 27 | }) 28 | it('should broadcast success event', function (done) { 29 | const spies = new Array(5).fill(1).map(() => jest.fn()) 30 | spies.forEach(spy => { 31 | endpoint.on(events.SUCCESS, spy) 32 | }) 33 | 34 | endpoint.on(events.SUCCESS, endpoint => { 35 | expect(endpoint.data).toEqual({ test: 'test' }) 36 | }) 37 | 38 | endpoint.fetch().then(() => { 39 | spies.forEach(spy => expect(spy).toBeCalledTimes(1)) 40 | done() 41 | }).catch(done) 42 | 43 | server.resolve({ status: 200, data: { test: 'test' } }) 44 | }) 45 | 46 | it('should broadcast error event', function (done) { 47 | endpoint.fetch().catch(() => {}) 48 | 49 | server.reject({ status: 500, data: { test: 'test' } }) 50 | 51 | endpoint.on(events.ERROR, endpoint => { 52 | expect(endpoint.error).toEqual({ test: 'test' }) 53 | done() 54 | }) 55 | }) 56 | 57 | it('should broadcast loading event', function (done) { 58 | endpoint.on(events.LOADING, endpoint => { 59 | expect(endpoint.loading).toBeTruthy() 60 | server && server.resolve({}) 61 | done() 62 | }) 63 | 64 | endpoint.fetch() 65 | }) 66 | 67 | it('should broadcast cancel event', function (done) { 68 | const spy = jest.fn() 69 | endpoint = new Endpoint({ 70 | url: '/users', 71 | auto: false, 72 | on: { 73 | [events.CANCEL] () { 74 | done() 75 | } 76 | } 77 | }) 78 | 79 | endpoint.on(events.ERROR, spy) 80 | endpoint.on(events.SUCCESS, spy) 81 | 82 | endpoint.fetch() 83 | endpoint.cancel() 84 | expect(spy).not.toBeCalled() 85 | }) 86 | }) 87 | 88 | describe('test-listener-inheritance', function () { 89 | it('should inherit all listeners', function (done) { 90 | const pluginSpy = jest.fn().mockName('pluginSpy') 91 | const optionsSpy = jest.fn().mockName('optionsSpy') 92 | const mixinSpy = jest.fn().mockName('mixinSpy') 93 | const endpointSpy = jest.fn().mockName('endpointSpy') 94 | 95 | Vue.use(VueChimera, { 96 | on: { 97 | [events.SUCCESS]: pluginSpy 98 | } 99 | }) 100 | 101 | let vm = new Vue({ 102 | mixins: [ 103 | { 104 | chimera: { 105 | $options: { 106 | on: { 107 | [events.SUCCESS]: mixinSpy 108 | } 109 | } 110 | } 111 | } 112 | ], 113 | chimera: { 114 | $options: { 115 | on: { 116 | [events.SUCCESS]: optionsSpy 117 | } 118 | }, 119 | test: { 120 | url: '/', 121 | auto: false, 122 | http: httpMock, 123 | on: { 124 | [events.SUCCESS]: endpointSpy 125 | } 126 | } 127 | } 128 | }) 129 | 130 | expect(Object.values(vm.test.listeners.success)).toHaveLength(4) 131 | 132 | vm.test.fetch().then(() => { 133 | expect(endpointSpy).toBeCalledTimes(1) 134 | expect(optionsSpy).toBeCalledTimes(1) 135 | expect(pluginSpy).toBeCalledTimes(1) 136 | expect(mixinSpy).toBeCalledTimes(1) 137 | 138 | done() 139 | }).catch(done) 140 | 141 | server.resolve({ data: 's', status: 200 }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /tests/unit/index.js: -------------------------------------------------------------------------------- 1 | global.console.warn = jest.fn() 2 | -------------------------------------------------------------------------------- /tests/unit/plugin.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChimera from '../../src/index' 3 | import install from '../../src/index.es' 4 | 5 | describe('test-import', function () { 6 | it('should be a vue plugin', function () { 7 | expect(typeof VueChimera).toBe('function') 8 | expect(typeof install).toBe('function') 9 | }) 10 | }) 11 | 12 | describe('test-mixin', function () { 13 | let mixinOptions, baseOptions, LocalVue 14 | beforeEach(() => { 15 | LocalVue = Vue 16 | LocalVue.use(VueChimera, { 17 | auto: false, 18 | headers: { 19 | 'X-Test-1': '1' 20 | }, 21 | params: { 22 | a: 1 23 | } 24 | }) 25 | mixinOptions = { 26 | $options: { 27 | headers: { 'X-Test-2': '2' }, 28 | params: { b: 1 } 29 | }, 30 | test: { 31 | url: '/a' 32 | } 33 | } 34 | baseOptions = { 35 | $options: { 36 | baseURL: 's', 37 | headers: { 'X-Test-3': '3' }, 38 | params: { 39 | a: 2 40 | } 41 | }, 42 | test: { 43 | url: '/b', 44 | headers: { 'X-Test-4': '4' } 45 | } 46 | } 47 | }) 48 | it('should inherit options', function () { 49 | const vm = new LocalVue({ 50 | mixins: [ 51 | { chimera: mixinOptions } 52 | ], 53 | chimera: baseOptions 54 | }) 55 | 56 | expect(vm.$chimera.test === vm.test).toBeTruthy() 57 | expect(vm.test.headers).not.toEqual(mixinOptions.$options.headers) 58 | expect(vm.test.requestHeaders).toEqual({ 59 | 'X-Test-1': '1', 60 | 'X-Test-2': '2', 61 | 'X-Test-3': '3', 62 | 'X-Test-4': '4' 63 | }) 64 | expect(vm.test.url).toEqual('/b') 65 | expect(vm.test.baseURL).toEqual('s') 66 | expect(vm.test.params).toEqual({ a: 2, b: 1 }) 67 | }) 68 | 69 | it('should inherit options with functions', function () { 70 | const vm = new LocalVue({ 71 | mixins: [ 72 | { chimera: () => mixinOptions } 73 | ], 74 | chimera () { 75 | return { 76 | ...baseOptions, 77 | noParams: { 78 | params: null 79 | } 80 | } 81 | } 82 | }) 83 | 84 | expect(vm.test.url).toEqual('/b') 85 | expect(vm.test.requestHeaders).toEqual({ 86 | 'X-Test-1': '1', 87 | 'X-Test-2': '2', 88 | 'X-Test-3': '3', 89 | 'X-Test-4': '4' 90 | }) 91 | expect(vm.test.baseURL).toEqual('s') 92 | expect(vm.test.params).toEqual({ a: 2, b: 1 }) 93 | expect(vm.noParams.params).toBeNull() 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /tests/unit/reactivity.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueChimera from '../../src/index.js' 3 | import Endpoint from '../../src/Endpoint' 4 | import sinon from 'sinon' 5 | import Axios from 'axios' 6 | 7 | Vue.use(VueChimera, { axios: Axios }) 8 | Vue.config.devtools = false 9 | Vue.config.productionTip = false 10 | 11 | describe('vue-test-reactivity', function () { 12 | let server, responseData 13 | beforeEach(() => { 14 | responseData = [{ id: 1, name: 'chimera1' }, { id: 2, name: 'chimera2' }] 15 | server = sinon.createFakeServer() 16 | }) 17 | 18 | afterEach(() => { 19 | server.restore() 20 | }) 21 | 22 | describe('test-chimera-reactivity', function () { 23 | it('should react to chimera endpoint changes', async function () { 24 | let app = new Vue({ 25 | chimera: { 26 | users: '/users' 27 | } 28 | }) 29 | const watchers = [jest.fn()] 30 | app.$watch('users', watchers[0], { deep: true }) 31 | 32 | let watcherKeys = Object.keys(app.users.response).concat(['loading']) 33 | watcherKeys.forEach(key => { 34 | if (key === 'error') return 35 | watchers.push(jest.fn()) 36 | app.$watch('users.' + key, watchers[watchers.length - 1]) 37 | }) 38 | 39 | const p = app.$chimera.users.fetch() 40 | 41 | setTimeout(() => { 42 | server.respond([200, { 'Content-Type': 'application/json' }, JSON.stringify(responseData)]) 43 | }, 100) 44 | 45 | await p 46 | await app.$nextTick() 47 | 48 | watchers.forEach((w) => { 49 | expect(w).toBeCalled() 50 | }) 51 | expect(app.users.data).toEqual(responseData) 52 | expect(app.users.status).toBe(200) 53 | expect(app.users.lastLoaded).toBeInstanceOf(Date) 54 | expect(app.users.lastLoaded.getTime() - Date.now()).toBeLessThan(1000) 55 | }) 56 | }) 57 | 58 | describe('test-reactive-endpoints', function () { 59 | it('should react to changes', async function () { 60 | const watcher = jest.fn() 61 | let app = new Vue({ 62 | chimera: { 63 | $options: { 64 | deep: false 65 | }, 66 | users () { 67 | return { 68 | url: '/users/' + this.id, 69 | params: this.params, 70 | auto: this.auto 71 | } 72 | } 73 | }, 74 | data () { 75 | return { 76 | id: 1, 77 | auto: false, 78 | params: { 79 | page: 2 80 | } 81 | } 82 | }, 83 | watch: { 84 | users: watcher 85 | } 86 | }) 87 | const fetchSpy = jest.spyOn(Endpoint.prototype, 'fetch') 88 | expect(app._chimera.deep).toBe(false) 89 | expect(app.users.url).toBe('/users/1') 90 | expect(app.users.params).toEqual({ page: 2 }) 91 | 92 | expect(watcher).not.toBeCalled() 93 | 94 | app.id = 2 95 | expect(app.users.url).toBe('/users/2') 96 | await app.$nextTick() 97 | expect(watcher).toBeCalledTimes(1) 98 | 99 | app.params.page = 3 100 | await app.$nextTick() 101 | expect(watcher).toBeCalledTimes(1) 102 | 103 | app.params = { page: 5 } 104 | expect(app.users.params).toEqual({ page: 5 }) 105 | await app.$nextTick() 106 | expect(watcher).toBeCalledTimes(2) 107 | 108 | expect(fetchSpy).not.toBeCalled() 109 | app.auto = true 110 | await app.$nextTick() 111 | expect(watcher).toBeCalledTimes(3) 112 | expect(fetchSpy).toBeCalled() 113 | }) 114 | }) 115 | 116 | describe('test-function-init', function () { 117 | it('should initialized with a function', function () { 118 | const app = new Vue({ 119 | chimera () { 120 | return { 121 | $options: { 122 | auto: false 123 | }, 124 | $users: '/users', 125 | users: '/users' 126 | } 127 | } 128 | }) 129 | expect(app._chimera.constructor.name).toBe('VueChimera') 130 | expect(app.$chimera.users.constructor.name).toBe('Endpoint') 131 | 132 | expect(app.$chimera.$users).toBeUndefined() 133 | expect(app.users.auto).toBeFalsy() 134 | }) 135 | 136 | it('should destroy', async function () { 137 | const app = new Vue({ 138 | chimera () { 139 | return { 140 | $options: { 141 | auto: false 142 | }, 143 | $users: '/users', 144 | users: '/users' 145 | } 146 | } 147 | }) 148 | 149 | const cancel = jest.spyOn(app._chimera, 'cancelAll') 150 | const destroy = jest.spyOn(app._chimera, 'destroy') 151 | 152 | app.$destroy() 153 | 154 | await app.$nextTick() 155 | 156 | expect(cancel).toBeCalledTimes(1) 157 | expect(destroy).toBeCalledTimes(1) 158 | }) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /todo: -------------------------------------------------------------------------------- 1 | Document 2 | Vue CLI plugin 3 | 4 | Fetch 5 | ESlint plugin 6 | Load More 7 | Short polling 8 | Plugin able 9 | 10 | runtime add event to chimera 11 | runtime add options to chimera 12 | 13 | 14 | // Light Resources 15 | //Merging Strategy 16 | //Components 17 | // Typescript support 18 | // Test 19 | -------------------------------------------------------------------------------- /types/endpoint.d.ts: -------------------------------------------------------------------------------- 1 | export interface CacheInterface { 2 | setItem: (key: string, value: object) => void 3 | getItem: (key: string) => object 4 | removeItem: (key: string) => void 5 | keys: () => Array 6 | all: () => object 7 | length: () => number 8 | clear: () => void 9 | } 10 | 11 | export type TransformerDef = (res: any) => any 12 | 13 | export type Dictionary = { [key: string]: string } 14 | 15 | export type EventHandler = ((endpoint: Endpoint) => void) | string 16 | 17 | export type EndpointDef = { 18 | url: string, 19 | method?: string, 20 | params?: any, 21 | baseURL?: string, 22 | headers?: Dictionary, 23 | on: { 24 | [key: string]: EventHandler | EventHandler[] 25 | }, 26 | debounce?: boolean | number, 27 | interval?: number | boolean, 28 | timeout?: number, 29 | transformer?: TransformerDef | { response: TransformerDef, error: TransformerDef }, 30 | auto?: boolean, 31 | prefetch?: boolean, 32 | prefetchTimeout?: number, 33 | keepData?: boolean, 34 | cache?: CacheInterface, 35 | axios?: object | (() => object), 36 | 37 | [key: string]: any 38 | } | string 39 | 40 | type Response = { 41 | status: number | null, 42 | data: any, 43 | headers: Dictionary | null, 44 | error: object | string | null, 45 | lastLoaded: Date | null 46 | } 47 | 48 | type Request = { 49 | url: string, 50 | baseURL: string | null, 51 | method: 'get' | 'post' | 'delete' | 'patch' | 'put', 52 | params: object | null, 53 | timeout: number, 54 | headers: Dictionary | null 55 | } 56 | 57 | export interface Endpoint { 58 | data: any, 59 | headers: Dictionary | null, 60 | params: object | null, 61 | loading: boolean, 62 | prefetched: boolean, 63 | 64 | looping: boolean, 65 | 66 | fetch (force?: boolean, extraOptions?: Partial): Promise 67 | 68 | on (event: string, handler: (endpoint: Endpoint) => void): this 69 | 70 | emit (event: string) : void 71 | 72 | reload() : Promise 73 | 74 | cancel() : void 75 | 76 | request: Request, 77 | 78 | response: Response, 79 | 80 | [key: string]: any 81 | } 82 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import './vue' 2 | import { PluginFunction } from 'vue' 3 | 4 | declare const install : PluginFunction<{}>; 5 | 6 | export default install 7 | -------------------------------------------------------------------------------- /types/vue.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { EndpointDef, Endpoint } from './endpoint' 3 | 4 | declare module 'vue/types/options' { 5 | interface ComponentOptions { 6 | chimera?: { 7 | $options?: object, 8 | } & { 9 | [key: string]: EndpointDef | (() => EndpointDef) 10 | } 11 | } 12 | } 13 | 14 | declare module 'vue/types/vue' { 15 | interface Vue { 16 | readonly $chimera: { 17 | $loading: Boolean, 18 | $cancelAll: () => void, 19 | } & { 20 | [key: string]: Endpoint 21 | }, 22 | } 23 | } 24 | --------------------------------------------------------------------------------