Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-cooks-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-db': patch
---

Fix type of findOne queries in Vue such that they type to a singular result instead of an array of results.
64 changes: 53 additions & 11 deletions packages/vue-db/src/useLiveQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ import { createLiveQueryCollection } from '@tanstack/db'
import type {
ChangeMessage,
Collection,
CollectionConfigSingleRowOption,
CollectionStatus,
Context,
GetResult,
InferResultType,
InitialQueryBuilder,
LiveQueryCollectionConfig,
NonSingleResult,
QueryBuilder,
SingleResult,
} from '@tanstack/db'
import type { ComputedRef, MaybeRefOrGetter } from 'vue'

/**
* Return type for useLiveQuery hook
* @property state - Reactive Map of query results (key → item)
* @property data - Reactive array of query results in order
* @property data - Reactive array of query results in order, or single result for findOne queries
* @property collection - The underlying query collection instance
* @property status - Current query status
* @property isLoading - True while initial query data is loading
Expand All @@ -33,10 +37,10 @@ import type { ComputedRef, MaybeRefOrGetter } from 'vue'
* @property isError - True when query encountered an error
* @property isCleanedUp - True when query has been cleaned up
*/
export interface UseLiveQueryReturn<T extends object> {
state: ComputedRef<Map<string | number, T>>
data: ComputedRef<Array<T>>
collection: ComputedRef<Collection<T, string | number, {}>>
export interface UseLiveQueryReturn<TContext extends Context> {
state: ComputedRef<Map<string | number, GetResult<TContext>>>
data: ComputedRef<InferResultType<TContext>>
collection: ComputedRef<Collection<GetResult<TContext>, string | number, {}>>
status: ComputedRef<CollectionStatus>
isLoading: ComputedRef<boolean>
isReady: ComputedRef<boolean>
Expand All @@ -61,6 +65,22 @@ export interface UseLiveQueryReturnWithCollection<
isCleanedUp: ComputedRef<boolean>
}

export interface UseLiveQueryReturnWithSingleResultCollection<
T extends object,
TKey extends string | number,
TUtils extends Record<string, any>,
> {
state: ComputedRef<Map<TKey, T>>
data: ComputedRef<T | undefined>
collection: ComputedRef<Collection<T, TKey, TUtils> & SingleResult>
status: ComputedRef<CollectionStatus>
isLoading: ComputedRef<boolean>
isReady: ComputedRef<boolean>
isIdle: ComputedRef<boolean>
isError: ComputedRef<boolean>
isCleanedUp: ComputedRef<boolean>
}

/**
* Create a live query using a query function
* @param queryFn - Query function that defines what data to fetch
Expand Down Expand Up @@ -114,15 +134,15 @@ export interface UseLiveQueryReturnWithCollection<
export function useLiveQuery<TContext extends Context>(
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
deps?: Array<MaybeRefOrGetter<unknown>>,
): UseLiveQueryReturn<GetResult<TContext>>
): UseLiveQueryReturn<TContext>

// Overload 1b: Accept query function that can return undefined/null
export function useLiveQuery<TContext extends Context>(
queryFn: (
q: InitialQueryBuilder,
) => QueryBuilder<TContext> | undefined | null,
deps?: Array<MaybeRefOrGetter<unknown>>,
): UseLiveQueryReturn<GetResult<TContext>>
): UseLiveQueryReturn<TContext>

/**
* Create a live query using configuration object
Expand Down Expand Up @@ -160,7 +180,7 @@ export function useLiveQuery<TContext extends Context>(
export function useLiveQuery<TContext extends Context>(
config: LiveQueryCollectionConfig<TContext>,
deps?: Array<MaybeRefOrGetter<unknown>>,
): UseLiveQueryReturn<GetResult<TContext>>
): UseLiveQueryReturn<TContext>

/**
* Subscribe to an existing query collection (can be reactive)
Expand Down Expand Up @@ -201,15 +221,28 @@ export function useLiveQuery<TContext extends Context>(
* // <Item v-for="item in data" :key="item.id" v-bind="item" />
* // </div>
*/
// Overload 3: Accept pre-created live query collection (can be reactive)
// Overload 3: Accept pre-created live query collection (can be reactive) - non-single result
export function useLiveQuery<
TResult extends object,
TKey extends string | number,
TUtils extends Record<string, any>,
>(
liveQueryCollection: MaybeRefOrGetter<Collection<TResult, TKey, TUtils>>,
liveQueryCollection: MaybeRefOrGetter<
Collection<TResult, TKey, TUtils> & NonSingleResult
>,
): UseLiveQueryReturnWithCollection<TResult, TKey, TUtils>

// Overload 4: Accept pre-created live query collection with singleResult: true
export function useLiveQuery<
TResult extends object,
TKey extends string | number,
TUtils extends Record<string, any>,
>(
liveQueryCollection: MaybeRefOrGetter<
Collection<TResult, TKey, TUtils> & SingleResult
>,
): UseLiveQueryReturnWithSingleResultCollection<TResult, TKey, TUtils>

// Implementation
export function useLiveQuery(
configOrQueryOrCollection: any,
Expand Down Expand Up @@ -294,7 +327,16 @@ export function useLiveQuery(
const internalData = reactive<Array<any>>([])

// Computed wrapper for the data to match expected return type
const data = computed(() => internalData)
// Returns single item for singleResult collections, array otherwise
const data = computed(() => {
const currentCollection = collection.value
if (!currentCollection) {
return internalData
}
const config: CollectionConfigSingleRowOption<any, any, any> =
currentCollection.config
return config.singleResult ? internalData[0] : internalData
})

// Track collection status reactively
const status = ref(
Expand Down
137 changes: 137 additions & 0 deletions packages/vue-db/tests/useLiveQuery.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expectTypeOf, it } from 'vitest'
import { createCollection } from '../../db/src/collection/index'
import { mockSyncCollectionOptions } from '../../db/tests/utils'
import {
createLiveQueryCollection,
eq,
liveQueryCollectionOptions,
} from '../../db/src/query/index'
import { useLiveQuery } from '../src/useLiveQuery'
import type { SingleResult } from '../../db/src/types'

type Person = {
id: string
name: string
age: number
email: string
isActive: boolean
team: string
}

describe(`useLiveQuery type assertions`, () => {
it(`should type findOne query builder to return a single row`, () => {
const collection = createCollection(
mockSyncCollectionOptions<Person>({
id: `test-persons-findone-vue`,
getKey: (person: Person) => person.id,
initialData: [],
}),
)

const { data } = useLiveQuery((q) =>
q
.from({ collection })
.where(({ collection: c }) => eq(c.id, `3`))
.findOne(),
)

// BUG: Currently returns ComputedRef<Array<Person>> but should be ComputedRef<Person | undefined>
expectTypeOf(data.value).toEqualTypeOf<Person | undefined>()
})

it(`should type findOne config object to return a single row`, () => {
const collection = createCollection(
mockSyncCollectionOptions<Person>({
id: `test-persons-findone-config-vue`,
getKey: (person: Person) => person.id,
initialData: [],
}),
)

const { data } = useLiveQuery({
query: (q) =>
q
.from({ collection })
.where(({ collection: c }) => eq(c.id, `3`))
.findOne(),
})

// BUG: Currently returns ComputedRef<Array<Person>> but should be ComputedRef<Person | undefined>
expectTypeOf(data.value).toEqualTypeOf<Person | undefined>()
})

it(`should type findOne collection using liveQueryCollectionOptions to return a single row`, () => {
const collection = createCollection(
mockSyncCollectionOptions<Person>({
id: `test-persons-findone-options-vue`,
getKey: (person: Person) => person.id,
initialData: [],
}),
)

const options = liveQueryCollectionOptions({
query: (q) =>
q
.from({ collection })
.where(({ collection: c }) => eq(c.id, `3`))
.findOne(),
})

const liveQueryCollection = createCollection(options)

expectTypeOf(liveQueryCollection).toExtend<SingleResult>()

const { data } = useLiveQuery(liveQueryCollection)

// BUG: Currently returns ComputedRef<Array<Person>> but should be ComputedRef<Person | undefined>
expectTypeOf(data.value).toEqualTypeOf<Person | undefined>()
})

it(`should type findOne collection using createLiveQueryCollection to return a single row`, () => {
const collection = createCollection(
mockSyncCollectionOptions<Person>({
id: `test-persons-findone-create-vue`,
getKey: (person: Person) => person.id,
initialData: [],
}),
)

const liveQueryCollection = createLiveQueryCollection({
query: (q) =>
q
.from({ collection })
.where(({ collection: c }) => eq(c.id, `3`))
.findOne(),
})

expectTypeOf(liveQueryCollection).toExtend<SingleResult>()

const { data } = useLiveQuery(liveQueryCollection)

// BUG: Currently returns ComputedRef<Array<Person>> but should be ComputedRef<Person | undefined>
expectTypeOf(data.value).toEqualTypeOf<Person | undefined>()
})

it(`should type regular query to return an array`, () => {
const collection = createCollection(
mockSyncCollectionOptions<Person>({
id: `test-persons-array-vue`,
getKey: (person: Person) => person.id,
initialData: [],
}),
)

const { data } = useLiveQuery((q) =>
q
.from({ collection })
.where(({ collection: c }) => eq(c.isActive, true))
.select(({ collection: c }) => ({
id: c.id,
name: c.name,
})),
)

// Regular queries should return an array
expectTypeOf(data.value).toEqualTypeOf<Array<{ id: string; name: string }>>()
})
})
8 changes: 4 additions & 4 deletions packages/vue-db/tests/useLiveQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ describe(`Query Collections`, () => {
.select(({ issues, persons }) => ({
id: issues.id,
title: issues.title,
name: persons.name,
name: persons?.name,
})),
)

Expand Down Expand Up @@ -590,7 +590,7 @@ describe(`Query Collections`, () => {
.select(({ issues, persons }) => ({
id: issues.id,
title: issues.title,
name: persons.name,
name: persons?.name,
})),
)

Expand Down Expand Up @@ -1162,7 +1162,7 @@ describe(`Query Collections`, () => {
.select(({ issues, persons }) => ({
id: issues.id,
title: issues.title,
name: persons.name,
name: persons?.name,
})),
)

Expand Down Expand Up @@ -1689,7 +1689,7 @@ describe(`Query Collections`, () => {
.select(({ issues, persons }) => ({
id: issues.id,
title: issues.title,
userName: persons.name,
userName: persons?.name,
})),
)

Expand Down
Loading