Skip to content

Commit

Permalink
feat(query): defineQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Mar 27, 2024
1 parent 4e7ed71 commit e0f7768
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 54 deletions.
78 changes: 78 additions & 0 deletions src/define-query.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { enableAutoUnmount, mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { ref } from 'vue'
import { QueryPlugin } from './query-plugin'
import { defineQuery } from './define-query'
import { useQuery } from './use-query'

describe('defineQuery', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
})

enableAutoUnmount(afterEach)

it('reuses the query in multiple places', async () => {
const useTodoList = defineQuery({
key: ['todos'],
query: async () => [{ id: 1 }],
})

let returendValues!: ReturnType<typeof useTodoList>
mount(
{
setup() {
returendValues = useTodoList()
return { ...returendValues }
},
template: `<div></div>`,
},
{
global: {
plugins: [createPinia(), QueryPlugin],
},
},
)

const { data } = useTodoList()
expect(data).toBe(useTodoList().data)
expect(data).toBe(returendValues.data)
})

it('reuses the query in multiple places with a setup function', async () => {
const useTodoList = defineQuery(() => {
const todoFilter = ref<'all' | 'finished' | 'unfinished'>('all')
const { data, ...rest } = useQuery({
key: ['todos', { filter: todoFilter.value }],
query: async () => [{ id: 1 }],
})
return { ...rest, todoList: data, todoFilter }
})

let returendValues!: ReturnType<typeof useTodoList>
mount(
{
setup() {
returendValues = useTodoList()
return { ...returendValues }
},
template: `<div></div>`,
},
{
global: {
plugins: [createPinia(), QueryPlugin],
},
},
)

const { todoList, todoFilter } = useTodoList()
expect(todoList).toBe(useTodoList().todoList)
expect(todoList).toBe(returendValues.todoList)
expect(todoFilter).toBe(useTodoList().todoFilter)
expect(todoFilter).toBe(returendValues.todoFilter)
})
})
35 changes: 35 additions & 0 deletions src/define-query.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expectTypeOf, it } from 'vitest'
import { defineQuery } from './define-query'
import { useQuery } from './use-query'

it('can define a query with an options object', () => {
const useMyMutation = defineQuery({
key: ['todos'],
query: async () => [{ id: 1 }],
})

const { data, refresh } = useMyMutation()

expectTypeOf(data.value).toEqualTypeOf<{ id: number }[] | undefined>()
expectTypeOf(refresh()).toEqualTypeOf<Promise<{ id: number }[]>>()
})

it('can define a query with a function', () => {
const useMyQuery = defineQuery(() => {
return {
foo: 'bar',
...useQuery({
key: ['todos'],
query: async () => [{ id: 1 }],
}),
}
})

const { data, refresh, foo } = useMyQuery()

expectTypeOf(data.value).toEqualTypeOf<{ id: number }[] | undefined>()
expectTypeOf(refresh()).toEqualTypeOf<Promise<{ id: number }[]>>()
expectTypeOf(foo).toEqualTypeOf<string>()
})

it.todo('can type the error', () => {})
52 changes: 52 additions & 0 deletions src/define-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { UseQueryOptions } from './query-options'
import { useQueryCache } from './query-store'
import type { ErrorDefault } from './types-extension'
import type { UseQueryReturn } from './use-query'
import { useQuery } from './use-query'

/**
* Define a query with the given options. Similar to `useQuery(options)` but allows you to reuse the query in multiple
* places.
*
* @param options - the options to define the query
* @example
* ```ts
* const useTodoList = defineQuery({
* key: ['todos'],
* query: () => fetch('/api/todos', { method: 'GET' }),
* })
* ```
*/
export function defineQuery<
TResult,
TError = ErrorDefault,
>(
options: UseQueryOptions<TResult, TError>,
): () => UseQueryReturn<TResult, TError>

/**
* Define a query with a setup function. Allows to return arbitrary values from the query function, create contextual
* refs, rename the returned values, etc.
*
* @param setup - a function to setup the query
* @example
* ```ts
* const useFilteredTodos = defineQuery(() => {
* const todoFilter = ref<'all' | 'finished' | 'unfinished'>('all')
* const { data, ...rest } = useQuery({
* key: ['todos', { filter: todoFilter.value }],
* query: () =>
* fetch(`/api/todos?filter=${todoFilter.value}`, { method: 'GET' }),
* })
* // expose the todoFilter ref and rename data for convenience
* return { ...rest, todoList: data, todoFilter }
* })
* ```
*/
export function defineQuery<T>(setup: () => T): () => T
export function defineQuery(
optionsOrSetup: UseQueryOptions | (() => unknown),
): () => unknown {
const setupFn = typeof optionsOrSetup === 'function' ? optionsOrSetup : () => useQuery(optionsOrSetup)
return () => useQueryCache().ensureDefinedQuery(setupFn)
}
4 changes: 1 addition & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ export {
export { defineMutation } from './define-mutation'

export { useQuery, type UseQueryReturn } from './use-query'
export { defineQuery } from './define-query'

// export { type UseQueryKeyList } from './query-keys'

export {
// TODO: figure out if worth compared to `defineQuery()`
// queryOptions,
// type InferUseQueryKeyData,
type UseQueryKey,
type UseQueryOptions,
type UseQueryOptionsWithDefaults,
Expand Down
44 changes: 0 additions & 44 deletions src/query-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,11 @@ import type { ErrorDefault } from './types-extension'
*/
export type _RefetchOnControl = boolean | 'always'

/**
* Symbol used to attach a data type to a `UseQueryKey`. It's never actually used, it's just for types.
*/
const DATA_TYPE_SYMBOL = Symbol()

/**
* Key used to identify a query. Always an array.
*/
export type UseQueryKey = Array<EntryNodeKey | _ObjectFlat>

/**
* Key used to identify a query with a specific data type.
* @internal
*/
export type _UseQueryKeyWithDataType<T> = UseQueryKey & {
/**
* Attach a data type to a key to infer the type of the data solely from the key.
* @see {@link InferUseQueryKeyData}
*
* @internal
*/
[DATA_TYPE_SYMBOL]: T
}

// export type UseQueryKey<T = unknown> = Array<EntryNodeKey | _ObjectFlat> & {
// /**
// * Attach a data type to a key to infer the type of the data solely from the key.
// * @see {@link InferUseQueryKeyData}
// *
// * @internal
// */
// [DATA_TYPE_SYMBOL]?: T
// }

/**
* Infer the data type from a `UseQueryKey` if possible. Falls back to `unknown`.
*/
export type InferUseQueryKeyData<Key> =
Key extends Record<typeof DATA_TYPE_SYMBOL, infer T> ? T : unknown

/**
* Context object passed to the `query` function of `useQuery()`.
* @see {@link UseQueryOptions}
Expand Down Expand Up @@ -137,15 +102,6 @@ export interface UseQueryOptions<TResult = unknown, TError = ErrorDefault> {
refetchOnReconnect?: _RefetchOnControl
}

// NOTE: helper to type the options. Still not used
export const queryOptions: <Options extends UseQueryOptions>(
options: Options,
) => Options & {
key: Options['key'] & {
[DATA_TYPE_SYMBOL]: Options extends UseQueryOptions<infer T> ? T : unknown
}
} = (options) => options as any

/**
* Default options for `useQuery()`. Modifying this object will affect all the queries that don't override these
*/
Expand Down
9 changes: 9 additions & 0 deletions src/query-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ export const useQueryCache = defineStore(QUERY_STORE_ID, () => {
// this allows use to attach reactive effects to the scope later on
const scope = getCurrentScope()!

const defineQueryMap = new WeakMap<() => unknown, any>()
function ensureDefinedQuery<T>(fn: () => T): T {
if (!defineQueryMap.has(fn)) {
defineQueryMap.set(fn, scope.run(fn)!)
}
return defineQueryMap.get(fn)!
}

function ensureEntry<TResult = unknown, TError = ErrorDefault>(
keyRaw: UseQueryKey,
options: UseQueryOptionsWithDefaults<TResult, TError>,
Expand Down Expand Up @@ -354,6 +362,7 @@ export const useQueryCache = defineStore(QUERY_STORE_ID, () => {
: undefined,

ensureEntry,
ensureDefinedQuery,
invalidateEntry,
setQueryData,
getQueryData,
Expand Down
13 changes: 13 additions & 0 deletions src/use-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,19 @@ describe('useQuery', () => {
await runTimers()
expect(query).toHaveBeenCalledTimes(2)
})

it.todo('can avoid throwing', async () => {
const { wrapper, query } = mountSimple({
staleTime: 0,
})

await runTimers()
expect(wrapper.vm.error).toBeNull()

query.mockRejectedValueOnce(new Error('ko'))

await expect(wrapper.vm.refetch()).resolves.toBeUndefined()
})
})

describe('shared state', () => {
Expand Down
7 changes: 0 additions & 7 deletions src/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,6 @@ export function useQuery<TResult, TError = ErrorDefault>(
return queryReturn
}

// TODO: createQuery with access to other properties as arguments for advanced (maybe even recommended) usage
function _defineQuery<TResult, TError = ErrorDefault>(
setup: () => UseQueryOptions<TResult, TError>,
) {
return () => useQuery<TResult, TError>(setup())
}

/**
* Unwraps a key from `options.key` while checking for properties any problematic dependencies. Should be used in DEV
* only.
Expand Down

0 comments on commit e0f7768

Please sign in to comment.