UNPKG

@urql/vue

Version:

A highly customizable and versatile GraphQL client for vue

735 lines (707 loc) 21.3 kB
Object.defineProperty(exports, '__esModule', { value: true }); var core = require('@urql/core'); var vue = require('vue'); var wonka = require('wonka'); var clientsPerInstance = new WeakMap(); /** Provides a {@link Client} to a component’s children. * * @param opts - {@link ClientOptions}, a {@link Client}, or a reactive ref object of a `Client`. * * @remarks * `provideClient` provides a {@link Client} to `@urql/vue`’s GraphQL * functions in children components. * * Hint: GraphQL functions and {@link useClient} will see the * provided `Client`, even if `provideClient` has been called * in the same component’s `setup` function. * * @example * ```ts * import { provideClient } from '@urql/vue'; * // All of `@urql/core` is also re-exported by `@urql/vue`: * import { Client, cacheExchange, fetchExchange } from '@urql/core'; * * export default { * setup() { * provideClient(new Client({ * url: 'https://API', * exchanges: [cacheExchange, fetchExchange], * })); * }, * }; * ``` */ function provideClient(opts) { var client; if (!vue.isRef(opts)) { client = vue.shallowRef(opts instanceof core.Client ? opts : new core.Client(opts)); } else { client = opts; } var instance = vue.getCurrentInstance(); if (instance) { clientsPerInstance.set(instance, client); } vue.provide('$urql', client); return client.value; } /** Provides a {@link Client} to a Vue app. * * @param app - the Vue {@link App} * @param opts - {@link ClientOptions}, a {@link Client}, or a reactive ref object of a `Client`. * * @remarks * `install` provides a {@link Client} to `@urql/vue`’s GraphQL * functions in a Vue app. * * @example * ```ts * import * as urql from '@urql/vue'; * // All of `@urql/core` is also re-exported by `@urql/vue`: * import { cacheExchange, fetchExchange } from '@urql/core'; * * import { createApp } from 'vue'; * import Root from './App.vue'; * * const app = createApp(Root); * app.use(urql, { * url: 'http://localhost:3000/graphql', * exchanges: [cacheExchange, fetchExchange], * }); * ``` */ function install(app, opts) { var client; if (!vue.isRef(opts)) { client = vue.shallowRef(opts instanceof core.Client ? opts : new core.Client(opts)); } else { client = opts; } app.provide('$urql', client); } /** Returns a provided reactive ref object of a {@link Client}. * * @remarks * `useClient` may be called in Vue `setup` functions to retrieve a * reactive rev object of a {@link Client} that’s previously been * provided with {@link provideClient} in the current or a parent’s * `setup` function. * * @throws * In development, if `useClient` is called outside of a Vue `setup` * function or no {@link Client} was provided, an error will be thrown. */ function useClient() { var instance = vue.getCurrentInstance(); if (process.env.NODE_ENV !== 'production' && !instance) { throw new Error('use* functions may only be called during the `setup()` or other lifecycle hooks.'); } var client = vue.inject('$urql'); if (!client && instance) { client = clientsPerInstance.get(instance); } if (process.env.NODE_ENV !== 'production' && !client) { throw new Error('No urql Client was provided. Did you forget to install the plugin or call `provideClient` in a parent?'); } return client; } var unwrap = maybeRef => typeof maybeRef === 'function' ? maybeRef() : maybeRef != null && vue.isRef(maybeRef) ? maybeRef.value : maybeRef; var isPlainObject = value => { if (typeof value !== 'object' || value === null) return false; return value.constructor && Object.getPrototypeOf(value).constructor === Object.prototype.constructor; }; var isArray = Array.isArray; var unwrapDeeply = input => { input = vue.isRef(input) ? input.value : input; if (typeof input === 'function') { return unwrapDeeply(input()); } if (input && typeof input === 'object') { if (isArray(input)) { var length = input.length; var out = new Array(length); var i = 0; for (; i < length; i++) { out[i] = unwrapDeeply(input[i]); } return out; } else if (isPlainObject(input)) { var keys = Object.keys(input); var _length = keys.length; var _i = 0; var key; var _out = {}; for (; _i < _length; _i++) { key = keys[_i]; _out[key] = unwrapDeeply(input[key]); } return _out; } } return input; }; var createRequestWithArgs = args => { return core.createRequest(unwrap(args.query), unwrapDeeply(args.variables)); }; var useRequestState = () => { var hasNext = vue.ref(false); var stale = vue.ref(false); var fetching = vue.ref(false); var error = vue.shallowRef(); var operation = vue.shallowRef(); var extensions = vue.shallowRef(); return { hasNext, stale, fetching, error, operation, extensions }; }; function useClientState(args, client, method) { var source = vue.shallowRef(); var isPaused = vue.isRef(args.pause) ? args.pause : typeof args.pause === 'function' ? vue.computed(args.pause) : vue.ref(!!args.pause); var request = vue.computed(() => createRequestWithArgs(args)); var requestOptions = vue.computed(() => { return 'requestPolicy' in args ? { requestPolicy: unwrap(args.requestPolicy), ...unwrap(args.context) } : { ...unwrap(args.context) }; }); var pause = () => { if (!vue.isReadonly(isPaused)) { isPaused.value = true; } }; var resume = () => { if (!vue.isReadonly(isPaused)) { isPaused.value = false; } }; var executeRaw = opts => { return client.value[method](request.value, { ...requestOptions.value, ...opts }); }; var execute = opts => { source.value = executeRaw(opts); }; // it's important to use `watchEffect()` here instead of `watch()` // because it listening for reactive variables inside `executeRaw()` function var teardown = vue.watchEffect(() => { source.value = !isPaused.value ? executeRaw() : undefined; }); return { source, isPaused, pause, resume, execute, teardown }; } /* eslint-disable react-hooks/rules-of-hooks */ /** Input arguments for the {@link useQuery} function. * * @param query - The GraphQL query that `useQuery` executes. * @param variables - The variables for the GraphQL query that `useQuery` executes. */ /** State of the current query, your {@link useQuery} function is executing. * * @remarks * `UseQueryState` is returned by {@link useQuery} and * gives you the updating {@link OperationResult} of * GraphQL queries. * * Each value that is part of the result is wrapped in a reactive ref * and updates as results come in. * * Hint: Even when the query and variables update, the previous state of * the last result is preserved, which allows you to display the * previous state, while implementing a loading indicator separately. */ /** Return value of {@link useQuery}, which is an awaitable {@link UseQueryState}. * * @remarks * {@link useQuery} returns a {@link UseQueryState} but may also be * awaited inside a Vue `async setup()` function. If it’s awaited * the query is executed before resolving. */ /** Function to run a GraphQL query and get reactive GraphQL results. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} object. * * @remarks * `useQuery` allows GraphQL queries to be defined and executed inside * Vue `setup` functions. * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the * provided {@link Client}. * * The returned result’s reactive values update when the `Client` has * new results for the query, and changes when your input `args` change. * * Additionally, `useQuery` may also be awaited inside an `async setup()` * function to use Vue’s Suspense feature. * * @see {@link https://urql.dev/goto/docs/basics/vue#queries} for `useQuery` docs. * * @example * ```ts * import { gql, useQuery } from '@urql/vue'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * export default { * setup() { * const result = useQuery({ query: TodosQuery }); * return { data: result.data }; * }, * }; * ``` */ function useQuery(args) { return callUseQuery(args); } function callUseQuery(args, client = useClient(), stops) { var data = vue.shallowRef(); var { fetching, operation, extensions, stale, error, hasNext } = useRequestState(); var { isPaused, source, pause, resume, execute, teardown } = useClientState(args, client, 'executeQuery'); var teardownQuery = vue.watchEffect(onInvalidate => { if (source.value) { fetching.value = true; stale.value = false; onInvalidate(wonka.subscribe(res => { data.value = res.data; stale.value = !!res.stale; fetching.value = false; error.value = res.error; operation.value = res.operation; extensions.value = res.extensions; hasNext.value = res.hasNext; })(wonka.onEnd(() => { fetching.value = false; stale.value = false; hasNext.value = false; })(source.value)).unsubscribe); } else { fetching.value = false; stale.value = false; hasNext.value = false; } }, { // NOTE: This part of the query pipeline is only initialised once and will need // to do so synchronously flush: 'sync' }); stops && stops.push(teardown, teardownQuery); var then = (onFulfilled, onRejected) => { var sub; var promise = new Promise(resolve => { if (!source.value) { return resolve(state); } var hasResult = false; sub = wonka.subscribe(() => { if (!state.fetching.value && !state.stale.value) { if (sub) sub.unsubscribe(); hasResult = true; resolve(state); } })(source.value); if (hasResult) sub.unsubscribe(); }); return promise.then(onFulfilled, onRejected); }; var state = { data, stale, error, operation, extensions, fetching, isPaused, hasNext, pause, resume, executeQuery(opts) { execute(opts); return { ...state, then }; } }; return { ...state, then }; } /* eslint-disable react-hooks/rules-of-hooks */ /** State of the last mutation executed by {@link useMutation}. * * @remarks * `UseMutationResponse` is returned by {@link useMutation} and * gives you the {@link OperationResult} of the last executed mutation, * and a {@link UseMutationResponse.executeMutation} method to * start mutations. * * Even if the mutation document passed to {@link useMutation} changes, * the state isn’t reset, so you can keep displaying the previous result. */ /** Function to create a GraphQL mutation, run by passing variables to {@link UseMutationResponse.executeMutation} * * @param query - a GraphQL mutation document which `useMutation` will execute. * @returns a {@link UseMutationResponse} object. * * @remarks * `useMutation` allows GraphQL mutations to be defined inside Vue `setup` functions, * and keeps its state after the mutation is started. Mutations can be started by calling * {@link UseMutationResponse.executeMutation} with variables. * * The returned result updates when a mutation is executed and keeps * track of the last mutation result. * * @see {@link https://urql.dev/goto/docs/basics/vue#mutations} for `useMutation` docs. * * @example * ```ts * import { gql, useMutation } from '@urql/vue'; * * const UpdateTodo = gql` * mutation ($id: ID!, $title: String!) { * updateTodo(id: $id, title: $title) { * id, title * } * } * `; * * export default { * setup() { * const result = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await result.executeMutation({ id, title }); * }; * // ... * }, * }; * ``` */ function useMutation(query) { return callUseMutation(query); } function callUseMutation(query, client = useClient()) { var data = vue.shallowRef(); var { fetching, operation, extensions, stale, error, hasNext } = useRequestState(); return { data, stale, fetching, error, operation, extensions, hasNext, executeMutation(variables, context) { fetching.value = true; return wonka.toPromise(wonka.take(1)(wonka.filter(result => !result.hasNext)(wonka.onPush(result => { data.value = result.data; stale.value = result.stale; fetching.value = false; error.value = result.error; operation.value = result.operation; extensions.value = result.extensions; hasNext.value = result.hasNext; })(client.value.executeMutation(createRequestWithArgs({ query, variables }), context || {}))))); } }; } /* eslint-disable react-hooks/rules-of-hooks */ /** Input arguments for the {@link useSubscription} function. * * @param query - The GraphQL subscription document that `useSubscription` executes. * @param variables - The variables for the GraphQL subscription that `useSubscription` executes. */ /** Combines previous data with an incoming subscription result’s data. * * @remarks * A `SubscriptionHandler` may be passed to {@link useSubscription} to * aggregate subscription results into a combined {@link UseSubscriptionResponse.data} * value. * * This is useful when a subscription event delivers a single item, while * you’d like to display a list of events. * * @example * ```ts * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const result = useSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * ``` */ /** A {@link SubscriptionHandler} or a reactive ref of one. */ /** State of the current query, your {@link useSubscription} function is executing. * * @remarks * `UseSubscriptionResponse` is returned by {@link useSubscription} and * gives you the updating {@link OperationResult} of GraphQL subscriptions. * * Each value that is part of the result is wrapped in a reactive ref * and updates as results come in. * * Hint: Even when the query and variables update, the prior state of * the last result is preserved. */ /** Function to run a GraphQL subscription and get reactive GraphQL results. * * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link UseSubscriptionResponse} object. * * @remarks * `useSubscription` allows GraphQL subscriptions to be defined and executed inside * Vue `setup` functions. * Given {@link UseSubscriptionArgs.query}, it executes the GraphQL subscription with the * provided {@link Client}. * * The returned result updates when the `Client` has new results * for the subscription, and `data` is updated with the result’s data * or with the `data` that a `handler` returns. * * @example * ```ts * import { gql, useSubscription } from '@urql/vue'; * * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * export default { * setup() { * const result = useSubscription( * { query: NotificationsSubscription }, * function combineNotifications(notifications = [], data) { * return [...notifications, data.newNotification]; * }, * ); * // ... * }, * }; * ``` */ function useSubscription(args, handler) { return callUseSubscription(args, handler); } function callUseSubscription(args, handler, client = useClient(), stops) { var data = vue.shallowRef(); var { fetching, operation, extensions, stale, error } = useRequestState(); var { isPaused, source, pause, resume, execute, teardown } = useClientState(args, client, 'executeSubscription'); var teardownSubscription = vue.watchEffect(onInvalidate => { if (source.value) { fetching.value = true; onInvalidate(wonka.subscribe(result => { fetching.value = true; error.value = result.error; extensions.value = result.extensions; stale.value = !!result.stale; operation.value = result.operation; if (result.data != null && handler) { var cb = vue.isRef(handler) ? handler.value : handler; if (typeof cb === 'function') { data.value = cb(data.value, result.data); return; } } data.value = result.data; })(wonka.onEnd(() => { fetching.value = false; })(source.value)).unsubscribe); } else { fetching.value = false; } }); stops && stops.push(teardown, teardownSubscription); var state = { data, stale, error, operation, extensions, fetching, isPaused, pause, resume, executeSubscription(opts) { execute(opts); return state; } }; return state; } /** Handle to create GraphQL operations outside of Vue’s `setup` functions. * * @remarks * The `ClientHandle` object is created inside a Vue `setup` function but * allows its methods to be called outside of `setup` functions, delaying * the creation of GraphQL operations, as an alternative to pausing queries * or subscriptions. * * This is also important when chaining multiple functions inside an * `async setup()` function. * * Hint: If you only need a single, non-updating result and want to execute * queries programmatically, it may be easier to call the {@link Client.query} * method. */ /** Creates a {@link ClientHandle} inside a Vue `setup` function. * * @remarks * `useClientHandle` creates and returns a {@link ClientHandle} * when called in a Vue `setup` function, which allows queries, * mutations, and subscriptions to be created _outside_ of * `setup` functions. * * This is also important when chaining multiple functions inside an * `async setup()` function. * * {@link useQuery} and other GraphQL functions must usually * be created in Vue `setup` functions so they can stop GraphQL * operations when your component unmounts. However, while they * queries and subscriptions can be paused, sometimes it’s easier * to delay the creation of their response objects. * * * @example * ```ts * import { ref, computed } from 'vue'; * import { gql, useClientHandle } from '@urql/vue'; * * export default { * async setup() { * const handle = useClientHandle(); * * const pokemons = await handle.useQuery({ * query: gql`{ pokemons(limit: 10) { id, name } }`, * }); * * const index = ref(0); * * // The `handle` allows another `useQuery` call to now be setup again * const pokemon = await handle.useQuery({ * query: gql` * query ($id: ID!) { * pokemon(id: $id) { id, name } * } * `, * variables: computed(() => ({ * id: pokemons.data.value.pokemons[index.value].id, * }), * }); * } * }; * ``` */ function useClientHandle() { var client = useClient(); var stops = []; vue.onBeforeUnmount(() => { var stop; while (stop = stops.shift()) stop(); }); var handle = { client: client.value, useQuery(args) { return callUseQuery(args, client, stops); }, useSubscription(args, handler) { return callUseSubscription(args, handler, client, stops); }, useMutation(query) { return callUseMutation(query, client); } }; if (process.env.NODE_ENV !== 'production') { vue.onMounted(() => { Object.assign(handle, { useQuery(args) { if (process.env.NODE_ENV !== 'production' && !vue.getCurrentInstance()) { throw new Error('`handle.useQuery()` should only be called in the `setup()` or a lifecycle hook.'); } return callUseQuery(args, client, stops); }, useSubscription(args, handler) { if (process.env.NODE_ENV !== 'production' && !vue.getCurrentInstance()) { throw new Error('`handle.useSubscription()` should only be called in the `setup()` or a lifecycle hook.'); } return callUseSubscription(args, handler, client, stops); } }); }); } return handle; } exports["default"] = install; exports.install = install; exports.provideClient = provideClient; exports.useClientHandle = useClientHandle; exports.useMutation = useMutation; exports.useQuery = useQuery; exports.useSubscription = useSubscription; Object.keys(core).forEach(function (k) { if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, { enumerable: true, get: function () { return core[k]; } }); }); //# sourceMappingURL=urql-vue.js.map