diff --git a/packages/vue-apollo-composable/README.md b/packages/vue-apollo-composable/README.md new file mode 100644 index 00000000..b2fade81 --- /dev/null +++ b/packages/vue-apollo-composable/README.md @@ -0,0 +1,3 @@ +# @vue/apollo-composable + +Experimental Apollo GraphQL functions for Vue Composition API diff --git a/packages/vue-apollo-composable/package.json b/packages/vue-apollo-composable/package.json new file mode 100644 index 00000000..4eb489f4 --- /dev/null +++ b/packages/vue-apollo-composable/package.json @@ -0,0 +1,46 @@ +{ + "name": "@vue/apollo-composable", + "version": "4.0.0-alpha.1", + "description": "Apollo GraphQL for Vue Composition API", + "repository": { + "type": "git", + "url": "git+https://github.com/Akryum/vue-apollo.git" + }, + "keywords": [ + "vue", + "apollo", + "graphql", + "composition" + ], + "author": "Guillaume Chau ", + "license": "MIT", + "bugs": { + "url": "https://github.com/Akryum/vue-apollo/issues" + }, + "homepage": "https://github.com/Akryum/vue-apollo#readme", + "publishConfig": { + "access": "public" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "yarn build --watch", + "build": "tsc --outDir dist -d", + "prepublishOnly": "yarn build" + }, + "peerDependencies": { + "@vue/composition-api": "^0.3.2", + "apollo-client": "^2.0.0", + "apollo-link": "^1.0.0", + "graphql": "^14.5.8", + "graphql-tag": "^2.0.0", + "vue": "^2.6.10" + }, + "devDependencies": { + "@vue/composition-api": "^0.3.2", + "typescript": "^3.7.2" + } +} \ No newline at end of file diff --git a/packages/vue-apollo-composable/src/index.ts b/packages/vue-apollo-composable/src/index.ts new file mode 100644 index 00000000..362987a5 --- /dev/null +++ b/packages/vue-apollo-composable/src/index.ts @@ -0,0 +1,4 @@ +export * from './useQuery' +export * from './useResult' +// export * from './useLoading' +export * from './useApolloClient' diff --git a/packages/vue-apollo-composable/src/useApolloClient.ts b/packages/vue-apollo-composable/src/useApolloClient.ts new file mode 100644 index 00000000..7adf6334 --- /dev/null +++ b/packages/vue-apollo-composable/src/useApolloClient.ts @@ -0,0 +1,38 @@ +import { inject } from '@vue/composition-api' +import ApolloClient from 'apollo-client' + +export const DefaultApolloClient = Symbol('default-apollo-client') +export const ApolloClients = Symbol('apollo-clients') + +export function useApolloClient (clientId: string = null) { + const providedApolloClients: { [key: string]: ApolloClient } = inject(ApolloClients, null) + const providedApolloClient: ApolloClient = inject(DefaultApolloClient, null) + + function resolveClient (clientId: string = null): ApolloClient { + let resolvedClient + if (clientId) { + if (!providedApolloClients) { + throw new Error(`No apolloClients injection found, tried to resolve '${clientId}' clientId`) + } + resolvedClient = providedApolloClients[clientId] + } else { + clientId = 'default' + if (providedApolloClients) { + resolvedClient = providedApolloClients.default + } else { + resolvedClient = providedApolloClient + } + } + if (!resolvedClient) { + throw new Error(`Apollo Client with id ${clientId} not found`) + } + return resolvedClient + } + + return { + resolveClient, + get client () { + return resolveClient(clientId) + } + } +} \ No newline at end of file diff --git a/packages/vue-apollo-composable/src/useQuery.ts b/packages/vue-apollo-composable/src/useQuery.ts new file mode 100644 index 00000000..1923c875 --- /dev/null +++ b/packages/vue-apollo-composable/src/useQuery.ts @@ -0,0 +1,213 @@ +import { ref, watch, onUnmounted, Ref, isRef } from '@vue/composition-api' +import Vue from 'vue' +import { DocumentNode } from 'graphql' +import ApolloClient, { OperationVariables, WatchQueryOptions, ObservableQuery, ApolloQueryResult, SubscribeToMoreOptions } from 'apollo-client' +import { Subscription } from 'apollo-client/util/Observable' +import { useApolloClient } from './useApolloClient' +import { ReactiveFunction } from './util/ReactiveFunction' +import { paramToRef } from './util/paramToRef' +import { paramToReactive } from './util/paramToReactive' +// import { trackQuery } from './util/loadingTracking' + +export interface UseQueryOptions< + TResult = any, + TVariables = OperationVariables +> extends Omit, 'query' | 'variables'> { + clientId?: string, + enabled?: boolean, +} + +export function useQuery< + TResult = any, + TVariables = OperationVariables, + TCacheShape = any +> ( + document: DocumentNode | Ref | ReactiveFunction, + variables: TVariables | Ref | ReactiveFunction = null, + options: UseQueryOptions | Ref> | ReactiveFunction> = {}, +) { + if (variables == null) variables = ref() + if (options == null) options = {} + const documentRef = paramToRef(document) + const variablesRef = paramToReactive(variables) + console.log(variablesRef) + const optionsRef = paramToReactive(options) + + // Result + /** + * Result from the query + */ + const result = ref() + const error = ref(null) + + // Loading + + /** + * Indicates if a network request is pending + */ + const loading = ref(false) + // trackQuery(loading) + + // Apollo Client + const { resolveClient } = useApolloClient() + + // Query + + let query: Ref> = ref() + let observer: Subscription + let started = false + + /** + * Starts watching the query + */ + function start () { + if (started) return + started = true + loading.value = true + + const client = resolveClient(currentOptions.value.clientId) + + query.value = client.watchQuery({ + query: currentDocument, + variables: currentVariables, + ...currentOptions.value, + }) + + observer = query.value.subscribe({ + next: onNextResult, + error: onError, + }) + + for (const subOptions of subscribeToMoreItems) { + subscribeToMore(subOptions) + } + } + + function onNextResult (queryResult: ApolloQueryResult) { + result.value = queryResult.data + loading.value = queryResult.loading + } + + function onError (queryError: any) { + error.value = queryError + } + + let onStopHandlers: (() => void)[] = [] + + /** + * Stop watching the query + */ + function stop () { + if (!started) return + started = false + loading.value = false + + onStopHandlers.forEach(handler => handler()) + onStopHandlers = [] + + if (query.value) { + query.value.stopPolling() + query.value = null + } + + if (observer) { + observer.unsubscribe() + observer = null + } + } + + // Restart + let restarting = false + /** + * Queue a restart of the query (on next tick) if it is already active + */ + function restart () { + if (!started || restarting) return + restarting = true + Vue.nextTick(() => { + if (started) { + stop() + start() + } + restarting = false + }) + } + + // Applying document + let currentDocument: DocumentNode + watch(documentRef, value => { + currentDocument = value + restart() + }) + + // Applying variables + let currentVariables: TVariables + watch(() => isRef(variablesRef) ? variablesRef.value : variablesRef, value => { + currentVariables = value + console.log('currentVariables', value) + restart() + }, { + deep: true, + }) + + // Applying options + const currentOptions = ref>() + watch(() => isRef(optionsRef) ? optionsRef.value : optionsRef, value => { + currentOptions.value = value + restart() + }, { + deep: true, + }) + + // Subscribe to more + + const subscribeToMoreItems = [] + + function subscribeToMore< + TSubscriptionVariables = OperationVariables, + TSubscriptionData = TResult + > (options: SubscribeToMoreOptions) { + subscribeToMoreItems.push(options) + addSubscribeToMore(options) + } + + function addSubscribeToMore (options: SubscribeToMoreOptions) { + if (!started) return + const unsubscribe = query.value.subscribeToMore(options) + onStopHandlers.push(unsubscribe) + } + + // Internal enabled returned to user + const enabled = ref(true) + + // Auto start & stop + watch( + () => enabled.value && + // Enabled option + (!currentOptions.value || currentOptions.value.enabled == null || currentOptions.value.enabled) + , value => { + if (value) { + start() + } else { + stop() + } + }) + + // Teardown + onUnmounted(stop) + + return { + result, + loading, + error, + enabled, + start, + stop, + restart, + document: documentRef, + variables: variablesRef, + options: optionsRef, + query, + subscribeToMore, + } +} diff --git a/packages/vue-apollo-composable/src/useResult.ts b/packages/vue-apollo-composable/src/useResult.ts new file mode 100644 index 00000000..3e41dbdb --- /dev/null +++ b/packages/vue-apollo-composable/src/useResult.ts @@ -0,0 +1,32 @@ +import { Ref, computed } from '@vue/composition-api' + +export function useResult< + TResult = any +> ( + result: Ref, + defaultValue: any = null, + pick: (data: TResult) => any = null, +) { + return computed(() => { + const value = result.value + if (value) { + if (pick) { + try { + return pick(value) + } catch (e) { + // Silent error + } + } else { + const keys = Object.keys(value) + if (keys.length === 1) { + // Automatically take the only key in result data + return value[keys[0]] + } else { + // Return entire result data + return value + } + } + } + return defaultValue + }) +} diff --git a/packages/vue-apollo-composable/src/util/ReactiveFunction.ts b/packages/vue-apollo-composable/src/util/ReactiveFunction.ts new file mode 100644 index 00000000..52784a11 --- /dev/null +++ b/packages/vue-apollo-composable/src/util/ReactiveFunction.ts @@ -0,0 +1 @@ +export type ReactiveFunction = () => TParam diff --git a/packages/vue-apollo-composable/src/util/paramToReactive.ts b/packages/vue-apollo-composable/src/util/paramToReactive.ts new file mode 100644 index 00000000..9424b118 --- /dev/null +++ b/packages/vue-apollo-composable/src/util/paramToReactive.ts @@ -0,0 +1,14 @@ +import { Ref, isRef, reactive, computed } from '@vue/composition-api' +import { ReactiveFunction } from './ReactiveFunction' + +export function paramToReactive (param: T | Ref | ReactiveFunction): T | Ref { + if (isRef(param)) { + return param + } else if (typeof param === 'function') { + return computed(param as ReactiveFunction) + } else if (param) { + return reactive(param) as T + } else { + return param + } +} \ No newline at end of file diff --git a/packages/vue-apollo-composable/src/util/paramToRef.ts b/packages/vue-apollo-composable/src/util/paramToRef.ts new file mode 100644 index 00000000..961e1682 --- /dev/null +++ b/packages/vue-apollo-composable/src/util/paramToRef.ts @@ -0,0 +1,12 @@ +import { Ref, isRef, computed, ref } from '@vue/composition-api' +import { ReactiveFunction } from './ReactiveFunction' + +export function paramToRef (param: T | Ref | ReactiveFunction): Ref { + if (isRef(param)) { + return param + } else if (typeof param === 'function') { + return computed(param as ReactiveFunction) + } else { + return ref(param) + } +} diff --git a/packages/vue-apollo-composable/tsconfig.json b/packages/vue-apollo-composable/tsconfig.json new file mode 100644 index 00000000..3fb6adbc --- /dev/null +++ b/packages/vue-apollo-composable/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "sourceMap": true, + }, + "include": [ + "src/**/*", + ], +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a25351de..3a6b1b69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2649,6 +2649,13 @@ source-map "~0.6.1" vue-template-es2015-compiler "^1.9.0" +"@vue/composition-api@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@vue/composition-api/-/composition-api-0.3.2.tgz#2d797028e489bf7812f08c7bb33ffd03ef23c617" + integrity sha512-fD4dn9cJX62QSP2TMFLXCOQOa+Bu2o7kWDjrU/FNLkNqPPcCKBLxCH/Lc+gNCRBKdEUGyI3arjAw7j0Yz1hnvw== + dependencies: + tslib "^1.9.3" + "@vue/eslint-config-standard@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@vue/eslint-config-standard/-/eslint-config-standard-4.0.0.tgz#6be447ee674e3b0f733c584098fd9a22e6d76fcd" @@ -14553,7 +14560,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.1.3: +typescript@^3.1.3, typescript@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==