├── .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 | [](https://vuejs.org)
9 | [](https://circleci.com/gh/chimera-js/vue-chimera)
10 | [](https://www.npmjs.org/package/vue-chimera)
11 | [](http://npm-stat.com/charts.html?package=vue-chimera)
12 | [](https://bundlephobia.com/result?p=vue-chimera@^3.0.0)
13 | [](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 |
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 |
2 |
3 | {{ user.baseURL }}{{ user.url }}
4 |
5 |
9 |
10 | {{ user.data.name }}
11 | Loading...
12 |
13 | Error: {{ user.status }} {{ user.error }}
14 |
15 |
16 |
17 |
44 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/demo/auto-refresh.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | {{ endpoint.data.name }}
9 |
10 |
11 | Last time loaded: {{ endpoint.lastLoaded.toTimeString() }}
12 |
13 |
14 |
15 |
25 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/demo/post-request.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | {{ newUser.data }}
9 |
10 |
11 |
14 |
15 |
16 |
17 |
32 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/demo/reactive-get.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | User ID:
7 |
8 |
12 |
13 | {{ reactiveGet.data.name }}
14 |
15 |
16 |
17 |
18 |
19 |
33 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/demo/simple-get.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | {{ simpleGet.data.name }}
9 |
10 |
11 |
14 |
15 |
16 |
17 |
24 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/events-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
8 | {{ user.name }}
9 |
10 |
11 |
Loading...
14 |
15 |
19 | {{ ev }} {{ i+1 }}
20 |
21 |
22 |
23 |
24 |
25 |
52 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/reactive-endpoint-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ user.baseURL }}{{ user.url }}
4 |
5 |
9 |
10 |
{{ user.data.name }}
11 |
Loading...
12 |
13 |
Error: {{ user.status }} {{ user.error }}
14 |
15 |
16 | Keep data
20 |
21 |
22 |
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 |
176 |
177 |
178 |
179 | -
180 | {{ user.name }}
181 |
182 |
183 |
Loading...
184 |
185 |
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 |
13 |
14 |
15 |
16 |
Loading...
17 |
18 |
19 |
20 | - {{ post.title }}
21 |
22 |
23 |
24 |
{{ posts.error.message }}
25 |
26 |
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 |
--------------------------------------------------------------------------------