Skip to content

Commit

Permalink
feat: useQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Chau committed Nov 28, 2019
1 parent c0d2a87 commit 377f421
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 1 deletion.
3 changes: 3 additions & 0 deletions packages/vue-apollo-composable/README.md
@@ -0,0 +1,3 @@
# @vue/apollo-composable

Experimental Apollo GraphQL functions for Vue Composition API
46 changes: 46 additions & 0 deletions 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 <guillaume.b.chau@gmail.com>",
"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"
}
}
4 changes: 4 additions & 0 deletions packages/vue-apollo-composable/src/index.ts
@@ -0,0 +1,4 @@
export * from './useQuery'
export * from './useResult'
// export * from './useLoading'
export * from './useApolloClient'
38 changes: 38 additions & 0 deletions 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<TCacheShape = any> (clientId: string = null) {
const providedApolloClients: { [key: string]: ApolloClient<TCacheShape> } = inject(ApolloClients, null)
const providedApolloClient: ApolloClient<TCacheShape> = inject(DefaultApolloClient, null)

function resolveClient (clientId: string = null): ApolloClient<TCacheShape> {
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)
}
}
}
213 changes: 213 additions & 0 deletions 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<WatchQueryOptions<TVariables>, 'query' | 'variables'> {
clientId?: string,
enabled?: boolean,
}

export function useQuery<
TResult = any,
TVariables = OperationVariables,
TCacheShape = any
> (
document: DocumentNode | Ref<DocumentNode> | ReactiveFunction<DocumentNode>,
variables: TVariables | Ref<TVariables> | ReactiveFunction<TVariables> = null,
options: UseQueryOptions<TResult, TVariables> | Ref<UseQueryOptions<TResult, TVariables>> | ReactiveFunction<UseQueryOptions<TResult, TVariables>> = {},
) {
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<TResult>()
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<ObservableQuery<TResult, TVariables>> = 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<TResult, TVariables>({
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<TResult>) {
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<UseQueryOptions<TResult, TVariables>>()
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<TResult, TSubscriptionVariables, TSubscriptionData>) {
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,
}
}
32 changes: 32 additions & 0 deletions 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<TResult>,
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
})
}
@@ -0,0 +1 @@
export type ReactiveFunction<TParam> = () => TParam
14 changes: 14 additions & 0 deletions 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<T> (param: T | Ref<T> | ReactiveFunction<T>): T | Ref<T> {
if (isRef(param)) {
return param
} else if (typeof param === 'function') {
return computed(param as ReactiveFunction<T>)
} else if (param) {
return reactive(param) as T
} else {
return param
}
}
12 changes: 12 additions & 0 deletions 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<T> (param: T | Ref<T> | ReactiveFunction<T>): Ref<T> {
if (isRef(param)) {
return param
} else if (typeof param === 'function') {
return computed(param as ReactiveFunction<T>)
} else {
return ref(param)
}
}
10 changes: 10 additions & 0 deletions packages/vue-apollo-composable/tsconfig.json
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": true,
},
"include": [
"src/**/*",
],
}

0 comments on commit 377f421

Please sign in to comment.