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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/server/setup.ts:
--------------------------------------------------------------------------------
1 | import { mockServer, MockList } from 'graphql-tools';
2 | import schema from './schema';
3 |
4 | const server = mockServer(schema, {
5 | Post: () => ({
6 | id: () =>
7 | Math.random()
8 | .toString(36)
9 | .substring(7),
10 | title: () => 'Hello World',
11 | slug: () => 'hello-world'
12 | }),
13 | Query: () => ({
14 | posts: () => new MockList(5),
15 | post: (_: any, { id }: any) => {
16 | return {
17 | id,
18 | title: `Hello World: ${id}`,
19 | slug: 'hello-world'
20 | };
21 | }
22 | }),
23 | Mutation: () => ({
24 | likePost: () => ({
25 | success: true,
26 | code: '200',
27 | message: 'Operation successful'
28 | })
29 | })
30 | });
31 |
32 | beforeEach(() => {
33 | const fetchController = {
34 | simulateNetworkError: false
35 | };
36 |
37 | (global as any).fetchController = fetchController;
38 |
39 | // setup a fetch mock
40 | (global as any).fetch = jest.fn(async function mockedAPI(url: string, opts: RequestInit) {
41 | if (fetchController.simulateNetworkError) {
42 | throw new Error('Network Error');
43 | }
44 |
45 | const body = JSON.parse(opts.body as string);
46 | const res = await server.query(body.query, body.variables);
47 |
48 | return Promise.resolve({
49 | json() {
50 | return res;
51 | }
52 | });
53 | });
54 |
55 | (global as any).sleep = (time: number) =>
56 | new Promise(resolve => {
57 | setTimeout(resolve, time);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { DocumentNode, print } from 'graphql';
2 | import stringify from 'fast-json-stable-stringify';
3 | import Vue from 'vue';
4 | import { Operation } from './types';
5 |
6 | /**
7 | * Normalizes a list of variable objects.
8 | */
9 | export function normalizeVariables(...variables: object[]) {
10 | let normalized;
11 | const length = variables.length;
12 | for (let i = 0; i < length; i++) {
13 | if (!normalized) {
14 | normalized = {};
15 | }
16 |
17 | normalized = { ...normalized, ...variables[i] };
18 | }
19 |
20 | return normalized;
21 | }
22 |
23 | /**
24 | * Normalizes a query string or object to a string.
25 | */
26 | export function normalizeQuery(query: string | DocumentNode): string | null {
27 | if (typeof query === 'string') {
28 | return query;
29 | }
30 |
31 | if (query.loc) {
32 | return print(query);
33 | }
34 |
35 | return null;
36 | }
37 |
38 | export function hash(x: string) {
39 | let h, i, l;
40 | for (h = 5381 | 0, i = 0, l = x.length | 0; i < l; i++) {
41 | h = (h << 5) + h + x.charCodeAt(i);
42 | }
43 |
44 | return h >>> 0;
45 | }
46 |
47 | export function getQueryKey(operation: Operation) {
48 | const variables = operation.variables ? stringify(operation.variables) : '';
49 | const query = normalizeQuery(operation.query);
50 |
51 | return hash(`${query}${variables}`);
52 | }
53 |
54 | export function normalizeChildren(context: Vue, slotProps: any) {
55 | if (context.$scopedSlots.default) {
56 | return context.$scopedSlots.default(slotProps) || [];
57 | }
58 |
59 | return context.$slots.default || [];
60 | }
61 |
--------------------------------------------------------------------------------
/src/Mutation.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VueConstructor } from 'vue';
2 | import { VqlClient } from './client';
3 | import { normalizeChildren } from './utils';
4 |
5 | type withVqlClient = VueConstructor<
6 | Vue & {
7 | $vql: VqlClient;
8 | }
9 | >;
10 |
11 | function componentData() {
12 | const data: any = null;
13 | const errors: any = null;
14 |
15 | return {
16 | data,
17 | errors,
18 | fetching: false,
19 | done: false
20 | };
21 | }
22 |
23 | export const Mutation = (Vue as withVqlClient).extend({
24 | name: 'Mutation',
25 | inject: ['$vql'],
26 | props: {
27 | query: {
28 | type: [String, Object],
29 | required: true
30 | }
31 | },
32 | data: componentData,
33 | methods: {
34 | async mutate(vars: object = {}) {
35 | if (!this.$vql) {
36 | throw new Error('Could not find the VQL client, did you install the plugin correctly?');
37 | }
38 |
39 | try {
40 | this.data = null;
41 | this.errors = null;
42 | this.fetching = true;
43 | this.done = false;
44 | const { data, errors } = await this.$vql.executeMutation({
45 | query: this.query,
46 | variables: vars || undefined
47 | });
48 |
49 | this.data = data;
50 | this.errors = errors;
51 | this.done = true;
52 | } catch (err) {
53 | this.errors = [err];
54 | this.data = null;
55 | this.done = false;
56 | } finally {
57 | this.fetching = false;
58 | }
59 | }
60 | },
61 | render(h) {
62 | const children = normalizeChildren(this, {
63 | data: this.data,
64 | errors: this.errors,
65 | fetching: this.fetching,
66 | done: this.done,
67 | execute: this.mutate
68 | });
69 |
70 | if (!children.length) {
71 | return h();
72 | }
73 |
74 | return children.length === 1 ? children[0] : h('span', children);
75 | }
76 | });
77 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Vue-gql
3 | lang: en-US
4 | home: true
5 | heroImage: /logo.png
6 | actionText: Get Started →
7 | actionLink: ./guide/
8 | features:
9 | - title: Declarative
10 | details: Use minimal Vue.js components to work with GraphQL
11 | - title: Fast
12 | details: A lightweight footprint.
13 | - title: Caching
14 | details: Reasonable caching behavior out of the box.
15 | - title: TypeScript
16 | details: Everything is written in TypeScript.
17 | footer: MIT Licensed | Copyright © 2019-present Baianat
18 | description: A small and fast GraphQL client for Vue.js
19 | meta:
20 | - name: og:title
21 | content: Vue-gql
22 | - name: og:description
23 | content: A small and fast GraphQL client for Vue.js
24 | ---
25 |
26 | # Quick Setup
27 |
28 | ## install
29 |
30 | ```bash
31 | # install with yarn
32 | yarn add vue-gql graphql
33 |
34 | # install with npm
35 | npm install vue-gql graphql
36 | ```
37 |
38 | ## Use
39 |
40 | In your entry file, import the required modules:
41 |
42 | ```js
43 | import Vue from 'vue';
44 | import { createClient, withProvider } from 'vue-gql';
45 | import App from './App.vue';
46 |
47 | const client = createClient({
48 | url: '/graphql' // Your endpoint
49 | });
50 |
51 | // use this instead of your App
52 | const AppWithClient = withProvider(App, client);
53 |
54 | new Vue({
55 | render: h => h(AppWithClient)
56 | }).mount('#app');
57 | ```
58 |
59 | Now you can use the `Query` component to run GQL queries:
60 |
61 | ```vue
62 |
63 |
64 |
65 |
66 | - {{ post.title }}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
82 | ```
83 |
--------------------------------------------------------------------------------
/src/Subscription.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VueConstructor } from 'vue';
2 | import { VqlClient } from './client';
3 | import { normalizeChildren } from './utils';
4 | import { Unsub } from './types';
5 |
6 | function componentData() {
7 | const data: any = null;
8 | const errors: any = null;
9 |
10 | return {
11 | data,
12 | errors,
13 | fetching: false
14 | };
15 | }
16 |
17 | type withVqlClient = VueConstructor<
18 | Vue & {
19 | _cachedVars?: number;
20 | $vql: VqlClient;
21 | $observer?: Unsub;
22 | }
23 | >;
24 |
25 | export const Subscription = (Vue as withVqlClient).extend({
26 | name: 'Subscription',
27 | inject: ['$vql'],
28 | props: {
29 | query: {
30 | type: [String, Object],
31 | required: true
32 | },
33 | variables: {
34 | type: Object,
35 | default: null
36 | },
37 | pause: {
38 | type: Boolean,
39 | default: false
40 | }
41 | },
42 | data: componentData,
43 | mounted() {
44 | if (!this.$vql) {
45 | throw new Error('Cannot detect Client Provider');
46 | }
47 |
48 | const self = this;
49 | this.$observer = this.$vql
50 | .executeSubscription({
51 | query: this.query,
52 | variables: this.variables
53 | })
54 | .subscribe({
55 | next(result) {
56 | self.data = result.data;
57 | self.errors = result.errors;
58 | },
59 | complete() {},
60 | error(err) {
61 | self.data = undefined;
62 | self.errors = [err];
63 | }
64 | });
65 | },
66 | beforeDestroy() {
67 | if (this.$observer) {
68 | this.$observer.unsubscribe();
69 | }
70 | },
71 | render(h) {
72 | const children = normalizeChildren(this, {
73 | data: this.data,
74 | errors: this.errors,
75 | fetching: this.fetching
76 | });
77 |
78 | if (!children.length) {
79 | return h();
80 | }
81 |
82 | return children.length === 1 ? children[0] : h('span', children);
83 | }
84 | });
85 |
--------------------------------------------------------------------------------
/docs/guide/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | lang: en-US
4 | meta:
5 | - name: og:title
6 | content: Introduction | Vue-gql
7 | ---
8 |
9 | # Introduction
10 |
11 | Vue-gql is a minimal [GraphQL](https://graphql.org/) client for Vue.js, exposing components to build highly customizable GraphQL projects. You can use this in small projects or large complex applications.
12 |
13 | We use GraphQL In most of our apps we build at Baianat, but more often than not we end up only using the bare-bones **ApolloLink** without the extra whistles provided by the **ApolloClient**, or we use `fetch` to run our GraphQL queries as we like to handle caching and persisting on our own. Also we would like to use [Vuex](https://vuex.vuejs.org/) in some of the queries, but due to **ApolloClient** having its own immutable store, we cannot use both side-by side and one makes the other redundant.
14 |
15 | To solve this, we needed a bare-bones GraphQL client for Vue.js, but with small quality of life defaults out of the box, like caching. Keeping it simple means it gets to be flexible and lightweight, and can be scaled to handle more complex challenges.
16 |
17 | This library is inspired by [URQL](https://github.com/FormidableLabs/urql).
18 |
19 | ## Features
20 |
21 | - Very small bundle size.
22 | - API is exposed as minimal Vue components that do most of the work for you.
23 | - Query caching by default with sensible configurable policies: `cache-first`, `network-only`, `cache-and-network`.
24 | - SSR support.
25 | - TypeScript friendly as its written in pure TypeScript.
26 |
27 | ## Compatibility
28 |
29 | This library relies on the `fetch` web API to run queries, you can use `unfetch` (client-side) or `node-fetch` (server-side) to use as a polyfill.
30 |
31 | ## Alternatives
32 |
33 | ### [VueApollo](https://github.com/Akryum/vue-apollo)
34 |
35 | **VueApollo** Is probably the most complete Vue GraphQL client out there, like **vue-gql** it exposes components to work with queries and mutations. It builds upon the **ApolloClient** ecosystem. Use it if you find **vue-gql** lacking for your use-case.
36 |
--------------------------------------------------------------------------------
/test/mutation.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 | import flushPromises from 'flush-promises';
3 | import { Mutation, createClient, Provider } from '../src/index';
4 |
5 | test('runs mutations', async () => {
6 | const client = createClient({
7 | url: 'https://test.baianat.com/graphql'
8 | });
9 |
10 | const wrapper = mount(
11 | {
12 | data: () => ({
13 | client
14 | }),
15 | components: {
16 | Mutation,
17 | Provider
18 | },
19 | template: `
20 |
21 |
22 |
23 |
24 |
25 | {{ data.likePost.message }}
26 |
27 |
28 |
29 |
30 |
31 |
32 | `
33 | },
34 | { sync: false }
35 | );
36 |
37 | await flushPromises();
38 | expect(fetch).toHaveBeenCalledTimes(0);
39 |
40 | wrapper.find('button').trigger('click');
41 | await flushPromises();
42 | expect(fetch).toHaveBeenCalledTimes(1);
43 | expect(wrapper.find('p').text()).toBe('Operation successful');
44 | });
45 |
46 | test('handles errors', async () => {
47 | const client = createClient({
48 | url: 'https://test.baianat.com/graphql'
49 | });
50 |
51 | (global as any).fetchController.simulateNetworkError = true;
52 |
53 | const wrapper = mount(
54 | {
55 | data: () => ({
56 | client
57 | }),
58 | components: {
59 | Mutation,
60 | Provider
61 | },
62 | template: `
63 |
64 |
65 |
66 |
67 |
68 | {{ errors[0].message }}
69 |
70 |
71 |
72 |
73 |
74 |
75 | `
76 | },
77 | { sync: false }
78 | );
79 |
80 | wrapper.find('button').trigger('click');
81 | await flushPromises();
82 | expect(wrapper.find('p').text()).toBe('Network Error');
83 | });
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-gql
2 |
3 | A small and fast GraphQL client for Vue.js.
4 |
5 |
6 |
7 | [](https://codecov.io/gh/baianat/vue-gql)
8 | [](https://travis-ci.org/baianat/vue-gql)
9 | [](https://bundlephobia.com/result?p=vue-gql@0.1.0)
10 |
11 |
12 |
13 | ## Features
14 |
15 | - 📦 **Minimal:** Its all you need to query GQL APIs.
16 | - 🦐 **Tiny:** Very small footprint.
17 | - 🗄 **Caching:** Simple and convenient query caching by default.
18 | - 💪 **TypeScript**: Written in Typescript.
19 | - 💚 Minimal Vue.js Components.
20 |
21 | ## Documentation
22 |
23 | You can find the full [documentation here](https://baianat.github.io/vue-gql)
24 |
25 | ## Quick Start
26 |
27 | First install `vue-gql`:
28 |
29 | ```bash
30 | yarn add vue-gql graphql
31 |
32 | # or npm
33 |
34 | npm install vue-gql graphql --save
35 | ```
36 |
37 | Setup the GraphQL client/endpoint:
38 |
39 | ```js
40 | import Vue from 'vue';
41 | import { withProvider, createClient } from 'vue-gql';
42 | import App from './App.vue'; // Your App Component
43 |
44 | const client = createClient({
45 | url: 'http://localhost:3002/graphql'
46 | });
47 |
48 | // Wrap your app component with the provider component.
49 | const AppWithGQL = withProvider(App, client);
50 |
51 | new Vue({
52 | render: h => h(AppWithGQL)
53 | }).$mount('#app');
54 | ```
55 |
56 | Now you can use the `Query` and `Mutation` components to run queries:
57 |
58 | ```vue
59 |
60 |
61 | Is Fetching ...
62 |
63 | {{ data }}
64 |
65 |
66 |
67 |
68 |
77 | ```
78 |
79 | You can do a lot more, `vue-gql` makes frequent tasks such as re-fetching, caching, mutation responses, error handling, subscriptions a breeze. Consult the documentation for more use-cases and examples.
80 |
81 | ## Compatibility
82 |
83 | This library relies on the `fetch` web API to run queries, you can use [`unfetch`](https://github.com/developit/unfetch) (client-side) or [`node-fetch`](https://www.npmjs.com/package/node-fetch) (server-side) to use as a polyfill.
84 |
85 | ## Examples
86 |
87 | SOON
88 |
89 | ## License
90 |
91 | MIT
92 |
--------------------------------------------------------------------------------
/docs/guide/client.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 | To start querying GraphQL endpoints, you need to setup a client for that endpoint. **vue-gql** exposes a `createClient` function that allows you to create GraphQL clients for your endpoints.
4 |
5 | ```js
6 | import { createClient } from 'vue-gql';
7 |
8 | const client = createClient({
9 | url: '/graphql' // your endpoint.
10 | });
11 | ```
12 |
13 | After you've created a client, you need to **provide** the client instance to your app, you can do this via two ways.
14 |
15 | ## Provider component
16 |
17 | **vue-gql** exports a `Provider` component that accepts a single prop, the `client` created by `createClient` function.
18 |
19 | ### SFC
20 |
21 | ```vue
22 |
23 |
24 |
25 |
26 |
27 |
28 |
44 | ```
45 |
46 | ### JSX
47 |
48 | This can be much easier if you are using JSX:
49 |
50 | ```jsx
51 | import { Provider, createClient } from 'vue-gql';
52 |
53 | const client = createClient({
54 | url: '/graphql'
55 | });
56 |
57 | return new Vue({
58 | el: '#app',
59 | render() {
60 | return (
61 |
62 |
63 |
64 | );
65 | }
66 | });
67 | ```
68 |
69 | :::tip
70 | The **Provider** component is **renderless** by default, meaning it will not render any extra HTML other than its slot, but only when exactly one child is present, if multiple children exist inside its slot it will render a `span`.
71 | :::
72 |
73 | ### Multiple Providers
74 |
75 | While uncommon, there is no limitations on how many endpoints you can use within your app, you can use as many provider as you like and that allows you to query different GraphQL APIs within the same app without hassle.
76 |
77 | ```vue
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | ```
86 |
87 | ## withProvider function
88 |
89 | **vue-gql** exposes a `withProvider` function that takes a Vue component and returns the same component wrapped by the `Provider` component, it is very handy to use in JS components and render functions.
90 |
91 | ```js
92 | import Vue from 'vue';
93 | import { createClient, withProvider } from 'vue-gql';
94 | import App from './App.vue';
95 |
96 | const client = createClient({
97 | url: '/graphql' // Your endpoint
98 | });
99 |
100 | // use this instead of your App
101 | const AppWithClient = withProvider(App, client);
102 |
103 | new Vue({
104 | // Render the wrapped version instead.
105 | render: h => h(AppWithClient)
106 | }).mount('#app');
107 | ```
108 |
109 | ## Next Steps
110 |
111 | Now that you have successfully setup the GraphQL client, you can use [Query](./queries.md) and [Mutation](./mutations.md) components to execute GraphQL queries.
112 |
--------------------------------------------------------------------------------
/src/Query.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VueConstructor } from 'vue';
2 | import stringify from 'fast-json-stable-stringify';
3 | import { CachePolicy } from './types';
4 | import { VqlClient } from './client';
5 | import { normalizeVariables, normalizeChildren, hash } from './utils';
6 |
7 | type withVqlClient = VueConstructor<
8 | Vue & {
9 | _cachedVars?: number;
10 | $vql: VqlClient;
11 | }
12 | >;
13 |
14 | function componentData() {
15 | const data: any = null;
16 | const errors: any = null;
17 |
18 | return {
19 | data,
20 | errors,
21 | fetching: false,
22 | done: false
23 | };
24 | }
25 |
26 | export const Query = (Vue as withVqlClient).extend({
27 | name: 'Query',
28 | inject: ['$vql'],
29 | props: {
30 | query: {
31 | type: [String, Object],
32 | required: true
33 | },
34 | variables: {
35 | type: Object,
36 | default: null
37 | },
38 | cachePolicy: {
39 | type: String,
40 | default: undefined,
41 | validator(value) {
42 | const isValid = [undefined, 'cache-and-network', 'network-only', 'cache-first'].indexOf(value) !== -1;
43 |
44 | return isValid;
45 | }
46 | },
47 | pause: {
48 | type: Boolean,
49 | default: false
50 | }
51 | },
52 | data: componentData,
53 | serverPrefetch() {
54 | // fetch it on the server-side.
55 | return (this as any).fetch();
56 | },
57 | watch: {
58 | variables: {
59 | deep: true,
60 | handler(value) {
61 | if (this.pause) {
62 | return;
63 | }
64 |
65 | const id = hash(stringify(value));
66 | if (id === this._cachedVars) {
67 | return;
68 | }
69 |
70 | this._cachedVars = id;
71 | // tslint:disable-next-line: no-floating-promises
72 | this.fetch();
73 | }
74 | }
75 | },
76 | methods: {
77 | async fetch(vars?: object, cachePolicy?: CachePolicy) {
78 | if (!this.$vql) {
79 | throw new Error('Could not detect Client Provider');
80 | }
81 |
82 | try {
83 | this.fetching = true;
84 | const { data, errors } = await this.$vql.executeQuery({
85 | query: this.query,
86 | variables: normalizeVariables(this.variables, vars || {}),
87 | cachePolicy: cachePolicy || (this.cachePolicy as CachePolicy)
88 | });
89 |
90 | this.data = data;
91 | this.errors = errors;
92 | } catch (err) {
93 | this.errors = [err];
94 | this.data = null;
95 | } finally {
96 | this.done = true;
97 | this.fetching = false;
98 | }
99 | }
100 | },
101 | mounted() {
102 | // fetch it on client side if it was not already.
103 | if (!this.data) {
104 | // tslint:disable-next-line: no-floating-promises
105 | this.fetch();
106 | }
107 | },
108 | render(h) {
109 | const children = normalizeChildren(this, {
110 | data: this.data,
111 | errors: this.errors,
112 | fetching: this.fetching,
113 | done: this.done,
114 | execute: ({ cachePolicy }: { cachePolicy?: CachePolicy } = {}) => this.fetch({}, cachePolicy)
115 | });
116 |
117 | if (!children.length) {
118 | return h();
119 | }
120 |
121 | return children.length === 1 ? children[0] : h('span', children);
122 | }
123 | });
124 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-gql",
3 | "version": "0.2.3",
4 | "description": "A small and fast GraphQL client for Vue.js",
5 | "module": "dist/vql.esm.js",
6 | "unpkg": "dist/vql.js",
7 | "main": "dist/vql.js",
8 | "types": "dist/types/src",
9 | "scripts": {
10 | "docs:dev": "vuepress dev docs",
11 | "docs:build": "vuepress build docs",
12 | "docs:deploy": "./scripts/deploy.sh",
13 | "test": "jest",
14 | "test:cover": "jest --coverage",
15 | "lint": "eslint . '**/*.{js,jsx,ts,tsx}' --fix",
16 | "build": "node scripts/build.js && npm run ts:defs",
17 | "ts:defs": "tsc --emitDeclarationOnly"
18 | },
19 | "author": "Abdelrahman Awad ",
20 | "license": "MIT",
21 | "devDependencies": {
22 | "@babel/core": "^7.5.4",
23 | "@babel/plugin-transform-runtime": "^7.5.0",
24 | "@babel/preset-env": "^7.5.4",
25 | "@commitlint/cli": "^8.0.0",
26 | "@types/fast-json-stable-stringify": "^2.0.0",
27 | "@types/graphql": "^14.2.2",
28 | "@types/jest": "^24.0.15",
29 | "@typescript-eslint/eslint-plugin": "^1.12.0",
30 | "@typescript-eslint/parser": "^1.12.0",
31 | "@vue/test-utils": "^1.0.0-beta.29",
32 | "babel-core": "^7.0.0-bridge.0",
33 | "babel-jest": "^24.8.0",
34 | "bundlesize": "^0.18.0",
35 | "chalk": "^2.4.2",
36 | "eslint": "^6.0.1",
37 | "eslint-config-prettier": "^6.0.0",
38 | "eslint-config-standard": "^13.0.1",
39 | "eslint-plugin-import": "^2.18.0",
40 | "eslint-plugin-jest": "^22.7.2",
41 | "eslint-plugin-node": "^9.1.0",
42 | "eslint-plugin-prettier": "^3.1.0",
43 | "eslint-plugin-promise": "^4.2.1",
44 | "eslint-plugin-standard": "^4.0.0",
45 | "filesize": "^4.1.2",
46 | "flush-promises": "^1.0.2",
47 | "graphql": "^14.4.2",
48 | "graphql-tools": "^4.0.5",
49 | "gzip-size": "^5.1.1",
50 | "husky": "^3.0.0",
51 | "jest": "^24.8.0",
52 | "lint-staged": "^9.2.0",
53 | "mkdirp": "^0.5.1",
54 | "node-fetch": "^2.6.0",
55 | "prettier": "^1.18.2",
56 | "rollup": "^1.16.7",
57 | "rollup-plugin-commonjs": "^10.0.1",
58 | "rollup-plugin-node-resolve": "^5.2.0",
59 | "rollup-plugin-replace": "^2.2.0",
60 | "rollup-plugin-typescript2": "^0.22.0",
61 | "ts-jest": "^24.0.2",
62 | "typescript": "^3.5.3",
63 | "uglify-js": "^3.6.0",
64 | "vue": "^2.6.10",
65 | "vue-jest": "^3.0.4",
66 | "vue-template-compiler": "^2.6.10",
67 | "vuepress": "^1.0.2"
68 | },
69 | "husky": {
70 | "hooks": {
71 | "pre-commit": "lint-staged",
72 | "commit-msg": "commitlint --edit -E HUSKY_GIT_PARAMS"
73 | }
74 | },
75 | "files": [
76 | "dist/*.js",
77 | "dist/locale/*.js",
78 | "dist/types/**/*.d.ts"
79 | ],
80 | "bundlesize": [
81 | {
82 | "path": "./dist/*.min.js",
83 | "maxSize": "10 kB"
84 | }
85 | ],
86 | "eslintIgnore": [
87 | "locale",
88 | "dist",
89 | "scripts"
90 | ],
91 | "lint-staged": {
92 | "*.ts": [
93 | "eslint --fix",
94 | "prettier --write",
95 | "git add",
96 | "jest --maxWorkers=1 --bail --findRelatedTests"
97 | ],
98 | "*.js": [
99 | "git add"
100 | ]
101 | },
102 | "dependencies": {
103 | "fast-json-stable-stringify": "^2.0.0"
104 | },
105 | "peerDependencies": {
106 | "vue": "^2.5.18",
107 | "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/scripts/config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const { rollup } = require('rollup');
4 | const chalk = require('chalk');
5 | const uglify = require('uglify-js');
6 | const gzipSize = require('gzip-size');
7 | const filesize = require('filesize');
8 | const typescript = require('rollup-plugin-typescript2');
9 | const resolve = require('rollup-plugin-node-resolve');
10 | const commonjs = require('rollup-plugin-commonjs');
11 | const replace = require('rollup-plugin-replace');
12 | const version = process.env.VERSION || require('../package.json').version;
13 |
14 | const commons = {
15 | banner: `/**
16 | * vql v${version}
17 | * (c) ${new Date().getFullYear()} Baianat
18 | * @license MIT
19 | */`,
20 | outputFolder: path.join(__dirname, '..', 'dist'),
21 | uglifyOptions: {
22 | compress: true,
23 | mangle: true
24 | }
25 | };
26 |
27 | const paths = {
28 | dist: commons.outputFolder
29 | };
30 |
31 | const utils = {
32 | stats({ path, code }) {
33 | const { size } = fs.statSync(path);
34 | const gzipped = gzipSize.sync(code);
35 |
36 | return `| Size: ${filesize(size)} | Gzip: ${filesize(gzipped)}`;
37 | },
38 | async writeBundle({ input, output }, fileName, minify = false) {
39 | const bundle = await rollup(input);
40 | const {
41 | output: [{ code }]
42 | } = await bundle.generate(output);
43 |
44 | let outputPath = path.join(paths.dist, fileName);
45 | fs.writeFileSync(outputPath, code);
46 | let stats = this.stats({ code, path: outputPath });
47 | // eslint-disable-next-line
48 | console.log(`${chalk.green('Output File:')} ${fileName} ${stats}`);
49 |
50 | if (minify) {
51 | let minifiedFileName = fileName.replace('.js', '') + '.min.js';
52 | outputPath = path.join(paths.dist, minifiedFileName);
53 | fs.writeFileSync(outputPath, uglify.minify(code, commons.uglifyOptions).code);
54 | stats = this.stats({ code, path: outputPath });
55 | // eslint-disable-next-line
56 | console.log(`${chalk.green('Output File:')} ${minifiedFileName} ${stats}`);
57 | }
58 |
59 | return true;
60 | }
61 | };
62 |
63 | const builds = {
64 | umd: {
65 | input: 'src/index.ts',
66 | format: 'umd',
67 | name: 'VueGql',
68 | env: 'production'
69 | },
70 | esm: {
71 | input: 'src/index.ts',
72 | format: 'es'
73 | }
74 | };
75 |
76 | function genConfig(options) {
77 | const config = {
78 | input: {
79 | input: options.input,
80 | external: ['vue', 'fast-json-stable-stringify', 'graphql'],
81 | plugins: [
82 | typescript({ useTsconfigDeclarationDir: true }),
83 | replace({ __VERSION__: version }),
84 | resolve(),
85 | commonjs()
86 | ]
87 | },
88 | output: {
89 | banner: commons.banner,
90 | format: options.format,
91 | name: options.name,
92 | globals: {
93 | vue: 'Vue'
94 | }
95 | }
96 | };
97 |
98 | if (options.env) {
99 | config.input.plugins.unshift(
100 | replace({
101 | 'process.env.NODE_ENV': JSON.stringify(options.env)
102 | })
103 | );
104 | }
105 |
106 | return config;
107 | }
108 |
109 | const configs = Object.keys(builds).reduce((prev, key) => {
110 | prev[key] = genConfig(builds[key]);
111 |
112 | return prev;
113 | }, {});
114 |
115 | module.exports = {
116 | configs,
117 | utils,
118 | uglifyOptions: commons.uglifyOptions,
119 | paths
120 | };
121 |
--------------------------------------------------------------------------------
/docs/guide/subscriptions.md:
--------------------------------------------------------------------------------
1 | # Subscriptions
2 |
3 | `vue-gql` handles subscriptions with the `Subscription` component in the same way as the `Query` component.
4 |
5 | To add support for subscriptions you need to pass a `subscriptionForwarder` function to the `createClient` function, which in turn will call your subscription client. The `subscriptionForwarder` expects an object that follows the [observable spec](https://github.com/tc39/proposal-observable) to be returned.
6 |
7 | The following example uses `apollo-server` with the `subscriptions-transport-ws` package:
8 |
9 | ```js
10 | import { createClient } from 'vue-gql';
11 | import { SubscriptionClient } from 'subscriptions-transport-ws';
12 |
13 | const subscriptionClient = new SubscriptionClient('ws://localhost:4001/graphql', {});
14 |
15 | const client = createClient({
16 | url: 'http://localhost:4000/graphql',
17 | subscriptionForwarder: op => subscriptionClient.request(op)
18 | });
19 | ```
20 |
21 | Once you've setup the `subscriptionForwarder` function, you can now use the `Subscription` component in the same way as the `Query` component.
22 |
23 | The `Subscription` component exposes `data`, `error` on the slot props.
24 |
25 | ```vue{2,4,8,12}
26 |
27 |
28 | {{ data }}
29 |
30 |
31 |
32 |
52 | ```
53 |
54 | The `data` prop will be updated whenever a new value is received from the subscription.
55 |
56 | ## Using Subscriptions
57 |
58 | Having a subscription component printing the data probably isn't that useful, for example in a chat app you would append new messages to the old ones to do that you need refactor your code to do the following:
59 |
60 | - Have the `Subscription` component pass the `data` to a child component `Chatbox`.
61 | - The `Chatbox` component would watch the `data` received and append them to existing messages.
62 |
63 | Here is a minimal example:
64 |
65 | **Chatbox.vue**
66 |
67 | ```vue
68 |
69 |
70 | {{ message }}
71 |
72 |
73 |
74 |
88 | ```
89 |
90 | And in the parent component:
91 |
92 | ```vue
93 |
94 |
95 |
96 |
97 |
98 |
99 |
121 | ```
122 |
--------------------------------------------------------------------------------
/docs/guide/mutations.md:
--------------------------------------------------------------------------------
1 | # Mutations
2 |
3 | **vue-gql** exposes a **Mutation** component that is very similar to the **[Query](./queries.md)** component but with few distinct differences:
4 |
5 | - The mutation component **does not** have a `variables` prop.
6 | - The mutation component **does not** run automatically, you have to explicitly call `execute`.
7 | - Cache policies do not apply to mutations as mutations represent real-time actions and will always use `network-only` policy.
8 |
9 | :::tip
10 | The **Mutation** component is **renderless** by default, meaning it will not render any extra HTML other than its slot, but only when exactly one child is present, if multiple children exist inside its slot it will render a `span`.
11 | :::
12 |
13 | ```vue{3,4,5,6,10,11}
14 |
15 |
16 |
20 |
21 | {{ data.likePost.message }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
37 | ```
38 |
39 | ## Passing Variables
40 |
41 | Since the **Mutation** component does not accept `variables` you can pass them to the `execute` method instead:
42 |
43 | ```vue{3,8}
44 |
48 |
49 | {{ data.likePost.message }}
50 |
51 |
52 |
53 | ```
54 |
55 | Usually you would wrap your `forms` with the **Mutation** component and handle submits by executing the mutation.
56 |
57 | ## Slot Props
58 |
59 | ### fetching
60 |
61 | The **Mutation** slot props contain more useful information that you can use to build better experience for your users, for example you can use the `fetching` slot prop to display a loading indicator while the form submits.
62 |
63 | ```vue{3,5}
64 |
68 |
69 |
70 |
71 | {{ data.likePost.message }}
72 |
73 |
74 |
75 |
76 | ```
77 |
78 | ### done
79 |
80 | The `done` slot prop is a boolean that indicates that the query has been completed.
81 |
82 | ### errors
83 |
84 | The `errors` slot prop contains all errors encountered when running the query.
85 |
86 | ```vue{3,6}
87 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | ```
98 |
99 | ### execute
100 |
101 | Like you previously saw, the `execute` slot prop is a function that executes the mutation, it accepts the variables object if specified, and unlike the same slot prop in the **Query** component it does not affect caching.
102 |
103 | ```vue{3,9}
104 |
108 |
109 |
110 | - {{ post.title }}
111 |
112 |
113 |
114 |
115 | ```
116 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import { makeCache } from './cache';
2 | import { OperationResult, CachePolicy, Operation, ObservableLike } from './types';
3 | import { normalizeQuery } from './utils';
4 |
5 | type Fetcher = typeof fetch;
6 |
7 | type FetchOptions = Omit;
8 |
9 | interface CachedOperation extends Operation {
10 | cachePolicy?: CachePolicy;
11 | }
12 |
13 | interface GraphQLRequestContext {
14 | fetchOptions?: FetchOptions;
15 | }
16 |
17 | type ContextFactory = () => GraphQLRequestContext;
18 |
19 | type SubscriptionForwarder = (operation: Operation) => ObservableLike;
20 |
21 | interface VqlClientOptions {
22 | url: string;
23 | fetch?: Fetcher;
24 | context?: ContextFactory;
25 | cachePolicy?: CachePolicy;
26 | subscriptionForwarder?: SubscriptionForwarder;
27 | }
28 |
29 | function resolveGlobalFetch(): Fetcher | undefined {
30 | if (typeof window !== 'undefined' && 'fetch' in window) {
31 | return window.fetch.bind(window);
32 | }
33 |
34 | if (typeof global !== 'undefined' && 'fetch' in global) {
35 | return (global as any).fetch;
36 | }
37 |
38 | return undefined;
39 | }
40 |
41 | function makeFetchOptions({ query, variables }: Operation, opts: FetchOptions) {
42 | const normalizedQuery = normalizeQuery(query);
43 | if (!normalizedQuery) {
44 | throw new Error('A query must be provided.');
45 | }
46 |
47 | return {
48 | method: 'POST',
49 | body: JSON.stringify({ query: normalizedQuery, variables }),
50 | ...opts,
51 | headers: {
52 | 'content-type': 'application/json',
53 | ...opts.headers
54 | }
55 | };
56 | }
57 |
58 | interface VqlClientOptionsWithFetcher extends VqlClientOptions {
59 | fetch: Fetcher;
60 | }
61 |
62 | export class VqlClient {
63 | private url: string;
64 |
65 | private fetch: Fetcher;
66 |
67 | private defaultCachePolicy: CachePolicy;
68 |
69 | private context?: ContextFactory;
70 |
71 | private cache = makeCache();
72 |
73 | private subscriptionForwarder?: SubscriptionForwarder;
74 |
75 | public constructor(opts: VqlClientOptionsWithFetcher) {
76 | this.url = opts.url;
77 | this.fetch = opts.fetch;
78 | this.context = opts.context;
79 | this.defaultCachePolicy = opts.cachePolicy || 'cache-first';
80 | this.subscriptionForwarder = opts.subscriptionForwarder;
81 | }
82 |
83 | public async executeQuery(operation: CachedOperation): Promise {
84 | const fetchOptions = this.context ? this.context().fetchOptions : {};
85 | const opts = makeFetchOptions(operation, fetchOptions || {});
86 | const policy = operation.cachePolicy || this.defaultCachePolicy;
87 | const cachedResult = this.cache.getCachedResult(operation);
88 | if (policy === 'cache-first' && cachedResult) {
89 | return cachedResult;
90 | }
91 |
92 | const lazyFetch = () =>
93 | this.fetch(this.url, opts)
94 | .then(response => response.json())
95 | .then(result => {
96 | if (policy !== 'network-only') {
97 | this.cache.afterQuery(operation, result);
98 | }
99 |
100 | return result;
101 | });
102 |
103 | if (policy === 'cache-and-network' && cachedResult) {
104 | lazyFetch();
105 |
106 | return cachedResult;
107 | }
108 |
109 | return lazyFetch();
110 | }
111 |
112 | public async executeMutation(operation: Operation): Promise {
113 | const fetchOptions = this.context ? this.context().fetchOptions : {};
114 | const opts = makeFetchOptions(operation, fetchOptions || {});
115 |
116 | return this.fetch(this.url, opts).then(response => response.json());
117 | }
118 |
119 | public executeSubscription(operation: Operation) {
120 | if (!this.subscriptionForwarder) {
121 | throw new Error('No subscription forwarder was set.');
122 | }
123 |
124 | return this.subscriptionForwarder(operation);
125 | }
126 | }
127 |
128 | export function createClient(opts: VqlClientOptions) {
129 | opts.fetch = opts.fetch || resolveGlobalFetch();
130 | if (!opts.fetch) {
131 | throw new Error('Could not resolve a fetch() method, you should provide one.');
132 | }
133 |
134 | return new VqlClient(opts as VqlClientOptionsWithFetcher);
135 | }
136 |
--------------------------------------------------------------------------------
/test/subscription.ts:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils';
2 | import flushPromises from 'flush-promises';
3 | import { Subscription, createClient, Provider } from '../src/index';
4 |
5 | const Vue = createLocalVue();
6 | Vue.component('Subscription', Subscription);
7 |
8 | function makeObservable(throws = false) {
9 | let interval: any;
10 | let counter = 0;
11 | const observable = {
12 | subscribe: function({ next, error }: { error: Function; next: Function }) {
13 | interval = setInterval(() => {
14 | if (throws) {
15 | error(new Error('oops!'));
16 | return;
17 | }
18 |
19 | next({ data: { message: 'New message', id: counter++ } });
20 | }, 100);
21 |
22 | afterAll(() => {
23 | clearTimeout(interval);
24 | });
25 |
26 | return {
27 | unsubscribe() {
28 | clearTimeout(interval);
29 | }
30 | };
31 | }
32 | };
33 |
34 | return observable;
35 | }
36 |
37 | test('Handles subscriptions', async () => {
38 | const client = createClient({
39 | url: 'https://test.baianat.com/graphql',
40 | subscriptionForwarder: () => {
41 | return makeObservable();
42 | }
43 | });
44 |
45 | const wrapper = mount(
46 | {
47 | data: () => ({
48 | client
49 | }),
50 | components: {
51 | Subscription,
52 | Provider,
53 | Child: {
54 | props: ['newMessages'],
55 | data: () => ({ messages: [] }),
56 | watch: {
57 | newMessages(this: any, message: object) {
58 | this.messages.push(message);
59 | }
60 | },
61 | template: `
62 |
63 | - {{ msg.id }}
64 |
65 | `
66 | }
67 | },
68 | template: `
69 |
70 |
71 |
72 |
73 |
74 | `
75 | },
76 | { sync: false }
77 | );
78 |
79 | await (global as any).sleep(510);
80 | await flushPromises();
81 | expect(wrapper.findAll('li')).toHaveLength(5);
82 | wrapper.destroy();
83 | });
84 |
85 | test('Handles observer errors', async () => {
86 | const client = createClient({
87 | url: 'https://test.baianat.com/graphql',
88 | subscriptionForwarder: () => {
89 | return makeObservable(true);
90 | }
91 | });
92 |
93 | const wrapper = mount(
94 | {
95 | data: () => ({
96 | client
97 | }),
98 | components: {
99 | Subscription,
100 | Provider
101 | },
102 | template: `
103 |
104 |
105 |
106 | {{ errors[0].message }}
107 |
108 |
109 |
110 | `
111 | },
112 | { sync: false }
113 | );
114 |
115 | await (global as any).sleep(150);
116 | await flushPromises();
117 | expect(wrapper.find('p').text()).toBe('oops!');
118 | wrapper.destroy();
119 | });
120 |
121 | test('renders a span if multiple root is found', async () => {
122 | const client = createClient({
123 | url: 'https://test.baianat.com/graphql',
124 | subscriptionForwarder: () => {
125 | return makeObservable(true);
126 | }
127 | });
128 |
129 | const wrapper = mount(
130 | {
131 | data: () => ({
132 | client
133 | }),
134 | components: {
135 | Subscription,
136 | Provider
137 | },
138 | template: `
139 |
140 |
141 | {{ data }}
142 | {{ data }}
143 |
144 |
145 | `
146 | },
147 | { sync: false }
148 | );
149 |
150 | await flushPromises();
151 | expect(wrapper.findAll('span')).toHaveLength(3);
152 | wrapper.destroy();
153 | });
154 |
155 | test('Fails if provider was not resolved', async () => {
156 | expect(() => {
157 | mount(
158 | {
159 | components: {
160 | Subscription
161 | },
162 | template: `
163 |
164 | {{ data }}
165 |
166 | `
167 | },
168 | { sync: false }
169 | );
170 | }).toThrow(/Client Provider/);
171 | });
172 |
--------------------------------------------------------------------------------
/docs/guide/queries.md:
--------------------------------------------------------------------------------
1 | # Queries
2 |
3 | You can query GraphQL APIs with the **Query** component after you've setup the [GraphQL Client](./client.md).
4 |
5 | The **Query** component uses slots and [scoped slots](https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots) to provide the query state to the slot template.
6 |
7 | To run a query, the **Query** component takes a required `query` prop that can be either a `string` containing the query or a `DocumentNode` loaded by `graphql-tag/loader` from `.graphql` files.
8 |
9 | :::tip
10 | The **Query** component is **renderless** by default, meaning it will not render any extra HTML other than its slot, but only when exactly one child is present, if multiple children exist inside its slot it will render a `span`.
11 | :::
12 |
13 | ```vue
14 |
15 |
16 |
17 |
18 | {{ todo.text }}
19 |
20 |
21 |
22 |
23 |
24 |
33 | ```
34 |
35 | By default the query will run on the server-side if applicable (via `serverPrefetch`) or on mounted (client-side) if it didn't already.
36 |
37 | :::tip
38 | The examples from now on will omit much of the boilerplate and will only use the `Query` component to demonstrate its uses clearly.
39 | :::
40 |
41 | ## [graphql-tag](https://github.com/apollographql/graphql-tag)
42 |
43 | You can use `graphql-tag` to compile your queries or load them with the `graphql-tag/loader`.
44 |
45 | ```
46 | // in script
47 | const todos = gql`
48 | todos {
49 | id
50 | text
51 | }
52 | `;
53 |
54 | // in template
55 |
56 |
57 | {{ todo.text }}
58 |
59 |
60 | ```
61 |
62 | Here we are using `require` with the `graphql-tag/loader`:
63 |
64 | ```vue{1}
65 |
66 |
67 | {{ todo.text }}
68 |
69 |
70 | ```
71 |
72 | ## Variables
73 |
74 | You can provide variables to your queries using the `variables` optional prop, which is an object containing the variables you would normally send to a GraphQL request.
75 |
76 | ```vue{2}
77 |
78 |
79 |
80 | {{ todo.text }}
81 |
82 |
83 |
84 |
85 |
101 | ```
102 |
103 | ## Slot Props
104 |
105 | ### fetching
106 |
107 | The **Query** slot props contain more useful information that you can use to build better experience for your users, for example you can use the `fetching` slot prop to display a loading indicator.
108 |
109 | ```vue{1,3}
110 |
111 |
112 |
113 |
114 |
115 | {{ todo.text }}
116 |
117 |
118 |
119 | ```
120 |
121 | ### done
122 |
123 | The `done` slot prop is a boolean that indicates that the query has been completed.
124 |
125 | ### errors
126 |
127 | The `errors` slot prop contains all errors encountered when running the query.
128 |
129 | ```vue{1,3}
130 |
131 |
132 |
133 |
134 |
135 | {{ todo.text }}
136 |
137 |
138 |
139 | ```
140 |
141 | ### execute
142 |
143 | Sometimes you want to re-fetch the query or run it after some action, the `execute` slot prop is a function that re-runs the query. This example executes the query after the button has been clicked, note that the query is still fetched initially.
144 |
145 | ```vue{1,6}
146 |
147 |
148 |
149 | - {{ post.title }}
150 |
151 |
152 |
153 |
154 | ```
155 |
156 | ## Caching
157 |
158 | Unique queries are cached in memory, the uniqueness here is an id calculated by the query body, and its variables. Meaning if the same query is run with the same variables it will be fetched from the cache by default and will not hit the network. **Cache is deleted after the user closes/refreshes the page.**
159 |
160 | By default the client uses `cache-first` policy to handle queries, the full list of available policies are:
161 |
162 | - `cache-first`: If found in cache return it, otherwise fetch it from the network.
163 | - `network-only`: Always fetch from the network and do not cache it.
164 | - `cache-and-network`: If found in cache return it, but fetch the fresh value and cache it for next time, if not found in cache it will fetch it from network and cache it.
165 |
166 | You can force the **Query** component to fetch using any of the policies mentioned, you can do this by passing a `cachePolicy` option to the `execute` slot prop:
167 |
168 | ```vue{6}
169 |
170 |
171 |
172 | - {{ post.title }}
173 |
174 |
175 |
176 |
177 | ```
178 |
179 | :::tip
180 | Calling `execute` with a different cache policy will not change the default policy, the policy you specify will always be used for the next request upon calling `execute`.
181 | :::
182 |
183 | ### Setting default cache policy
184 |
185 | You can set the default policy when you are [providing the GraphQL client](./client.md) by passing `cachePolicy` option to the `createClient` function.
186 |
187 | ```js{3}
188 | const client = createClient({
189 | url: '/graphql', // Your endpoint
190 | cachePolicy: 'network-only'
191 | });
192 | ```
193 |
194 | This will make all the **Query** components under the **Provider** tree use the `network-only` policy by default, you can still override with the `execute` slot prop.
195 |
196 | ### Cache Prop
197 |
198 | You could also pass the `cachePolicy` prop to the `Query` component to set its default caching policy explicitly.
199 |
200 | ```vue{3}
201 |
206 |
207 |
208 | - {{ post.title }}
209 |
210 |
211 |
212 | ```
213 |
214 | ## Watching Variables
215 |
216 | Often you want to re-fetch the query when a variable changes, this is done for you by default as long as the query uses `variables` prop.
217 |
218 | ```vue{3}
219 |
224 |
225 | {{ data.post.title }}
226 |
227 |
228 | ```
229 |
230 | :::tip
231 | This examples re-runs the query whenever the `id` changes, the results of re-fetched queries follows the configured cache-policy.
232 | :::
233 |
234 | ### Disabling variable watching
235 |
236 | You can disable the mentioned behavior by setting `pause` prop to `true`.
237 |
238 | ```vue{4}
239 |
245 |
246 | {{ data.post.title }}
247 |
248 |
249 | ```
250 |
--------------------------------------------------------------------------------
/test/query.ts:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils';
2 | import flushPromises from 'flush-promises';
3 | import { withProvider, Query, createClient, Provider } from '../src/index';
4 | import App from './App.vue';
5 |
6 | const Vue = createLocalVue();
7 | Vue.component('Query', Query);
8 |
9 | test('executes queries on mounted', async () => {
10 | const client = createClient({
11 | url: 'https://test.baianat.com/graphql'
12 | });
13 |
14 | const AppWithGQL = withProvider(App, client);
15 |
16 | const wrapper = mount(AppWithGQL, { sync: false, localVue: Vue });
17 | await flushPromises();
18 | expect(wrapper.findAll('li').length).toBe(5);
19 | });
20 |
21 | test('caches queries by default', async () => {
22 | const client = createClient({
23 | url: 'https://test.baianat.com/graphql'
24 | });
25 |
26 | const wrapper = mount(
27 | {
28 | data: () => ({
29 | client
30 | }),
31 | components: {
32 | Query,
33 | Provider
34 | },
35 | template: `
36 |
37 |
38 |
39 |
40 |
41 |
42 | - {{ post.title }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | `
51 | },
52 | { sync: false }
53 | );
54 |
55 | await flushPromises();
56 | expect(fetch).toHaveBeenCalledTimes(1);
57 |
58 | wrapper.find('button').trigger('click');
59 | await flushPromises();
60 | // cache was used.
61 | expect(fetch).toHaveBeenCalledTimes(1);
62 | });
63 |
64 | test('cache policy can be overridden with execute function', async () => {
65 | const client = createClient({
66 | url: 'https://test.baianat.com/graphql'
67 | });
68 |
69 | const wrapper = mount(
70 | {
71 | data: () => ({
72 | client
73 | }),
74 | components: {
75 | Query,
76 | Provider
77 | },
78 | template: `
79 |
80 |
81 |
82 |
83 |
84 |
85 | - {{ post.title }}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | `
94 | },
95 | { sync: false }
96 | );
97 |
98 | await flushPromises();
99 | expect(fetch).toHaveBeenCalledTimes(1);
100 |
101 | wrapper.find('button').trigger('click');
102 | await flushPromises();
103 | // fetch was triggered a second time.
104 | expect(fetch).toHaveBeenCalledTimes(2);
105 | });
106 |
107 | test('cache policy can be overridden with cachePolicy prop', async () => {
108 | const client = createClient({
109 | url: 'https://test.baianat.com/graphql'
110 | });
111 |
112 | const wrapper = mount(
113 | {
114 | data: () => ({
115 | client
116 | }),
117 | components: {
118 | Query,
119 | Provider
120 | },
121 | template: `
122 |
123 |
124 |
125 |
126 |
127 |
128 | - {{ post.title }}
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | `
137 | },
138 | { sync: false }
139 | );
140 |
141 | await flushPromises();
142 | expect(fetch).toHaveBeenCalledTimes(1);
143 |
144 | wrapper.find('button').trigger('click');
145 | await flushPromises();
146 | // fetch was triggered a second time.
147 | expect(fetch).toHaveBeenCalledTimes(2);
148 | });
149 |
150 | test('variables are watched by default', async () => {
151 | const client = createClient({
152 | url: 'https://test.baianat.com/graphql'
153 | });
154 |
155 | const wrapper = mount(
156 | {
157 | data: () => ({
158 | client,
159 | id: 12
160 | }),
161 | components: {
162 | Query,
163 | Provider
164 | },
165 | template: `
166 |
167 |
168 |
169 |
170 |
171 | {{ data.post.title }}
172 |
173 |
174 |
175 |
176 |
177 | `
178 | },
179 | { sync: false }
180 | );
181 |
182 | await flushPromises();
183 | expect(fetch).toHaveBeenCalledTimes(1);
184 | expect(wrapper.find('h1').text()).toContain('12');
185 | wrapper.setData({
186 | id: 13
187 | });
188 | await flushPromises();
189 | // fetch was triggered a second time, due to variable change.
190 | expect(fetch).toHaveBeenCalledTimes(2);
191 | expect(wrapper.find('h1').text()).toContain('13');
192 | });
193 |
194 | test('variables watcher can be disabled', async () => {
195 | const client = createClient({
196 | url: 'https://test.baianat.com/graphql'
197 | });
198 |
199 | const wrapper = mount(
200 | {
201 | data: () => ({
202 | client,
203 | id: 12
204 | }),
205 | components: {
206 | Query,
207 | Provider
208 | },
209 | template: `
210 |
211 |
212 |
213 |
214 |
215 | {{ data.post.title }}
216 |
217 |
218 |
219 |
220 |
221 | `
222 | },
223 | { sync: false }
224 | );
225 |
226 | await flushPromises();
227 | expect(fetch).toHaveBeenCalledTimes(1);
228 | expect(wrapper.find('h1').text()).toContain('12');
229 | wrapper.setData({
230 | id: 13
231 | });
232 | await flushPromises();
233 | expect(fetch).toHaveBeenCalledTimes(1);
234 | expect(wrapper.find('h1').text()).toContain('12');
235 | });
236 |
237 | test('variables prop arrangement does not trigger queries', async () => {
238 | const client = createClient({
239 | url: 'https://test.baianat.com/graphql'
240 | });
241 |
242 | const wrapper = mount(
243 | {
244 | data: () => ({
245 | client,
246 | vars: {
247 | id: 12,
248 | type: 'test'
249 | }
250 | }),
251 | components: {
252 | Query,
253 | Provider
254 | },
255 | template: `
256 |
257 |
258 |
259 |
260 |
261 | {{ data.post.title }}
262 |
263 |
264 |
265 |
266 |
267 | `
268 | },
269 | { sync: false }
270 | );
271 |
272 | await flushPromises();
273 | expect(fetch).toHaveBeenCalledTimes(1);
274 | expect(wrapper.find('h1').text()).toContain('12');
275 | (wrapper.vm as any).vars = {
276 | type: 'test',
277 | id: 12
278 | };
279 | await flushPromises();
280 | expect(fetch).toHaveBeenCalledTimes(1);
281 |
282 | (wrapper.vm as any).vars.id = 13;
283 | await flushPromises();
284 | // fetch was triggered a second time, due to variable change.
285 | expect(fetch).toHaveBeenCalledTimes(2);
286 | expect(wrapper.find('h1').text()).toContain('13');
287 | });
288 |
289 | test('Handles query errors', async () => {
290 | const client = createClient({
291 | url: 'https://test.baianat.com/graphql'
292 | });
293 |
294 | const wrapper = mount(
295 | {
296 | data: () => ({
297 | client
298 | }),
299 | components: {
300 | Query,
301 | Provider
302 | },
303 | template: `
304 |
305 |
306 |
307 |
308 | - {{ post.title }}
309 |
310 | {{ errors[0].message }}
311 |
312 |
313 |
314 | `
315 | },
316 | { sync: false }
317 | );
318 |
319 | await flushPromises();
320 | expect(wrapper.find('#error').text()).toMatch(/Cannot query field/);
321 | });
322 |
323 | test('Handles external errors', async () => {
324 | const client = createClient({
325 | url: 'https://test.baianat.com/graphql'
326 | });
327 |
328 | (global as any).fetchController.simulateNetworkError = true;
329 |
330 | const wrapper = mount(
331 | {
332 | data: () => ({
333 | client
334 | }),
335 | components: {
336 | Query,
337 | Provider
338 | },
339 | template: `
340 |
341 |
342 |
343 |
344 | - {{ post.title }}
345 |
346 | {{ errors[0].message }}
347 |
348 |
349 |
350 | `
351 | },
352 | { sync: false }
353 | );
354 |
355 | await flushPromises();
356 | expect(wrapper.find('#error').text()).toMatch(/Network Error/);
357 | });
358 |
--------------------------------------------------------------------------------