@urql/vue
Version:
A highly customizable and versatile GraphQL client for vue
735 lines (707 loc) • 21.3 kB
JavaScript
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