diff --git a/.changeset/ninety-cooks-lick.md b/.changeset/ninety-cooks-lick.md new file mode 100644 index 000000000..1512371ea --- /dev/null +++ b/.changeset/ninety-cooks-lick.md @@ -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. diff --git a/packages/vue-db/src/useLiveQuery.ts b/packages/vue-db/src/useLiveQuery.ts index bdb035c27..76e181664 100644 --- a/packages/vue-db/src/useLiveQuery.ts +++ b/packages/vue-db/src/useLiveQuery.ts @@ -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 @@ -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 { - state: ComputedRef> - data: ComputedRef> - collection: ComputedRef> +export interface UseLiveQueryReturn { + state: ComputedRef>> + data: ComputedRef> + collection: ComputedRef, string | number, {}>> status: ComputedRef isLoading: ComputedRef isReady: ComputedRef @@ -61,6 +65,22 @@ export interface UseLiveQueryReturnWithCollection< isCleanedUp: ComputedRef } +export interface UseLiveQueryReturnWithSingleResultCollection< + T extends object, + TKey extends string | number, + TUtils extends Record, +> { + state: ComputedRef> + data: ComputedRef + collection: ComputedRef & SingleResult> + status: ComputedRef + isLoading: ComputedRef + isReady: ComputedRef + isIdle: ComputedRef + isError: ComputedRef + isCleanedUp: ComputedRef +} + /** * Create a live query using a query function * @param queryFn - Query function that defines what data to fetch @@ -114,7 +134,7 @@ export interface UseLiveQueryReturnWithCollection< export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, deps?: Array>, -): UseLiveQueryReturn> +): UseLiveQueryReturn // Overload 1b: Accept query function that can return undefined/null export function useLiveQuery( @@ -122,7 +142,7 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, deps?: Array>, -): UseLiveQueryReturn> +): UseLiveQueryReturn /** * Create a live query using configuration object @@ -160,7 +180,7 @@ export function useLiveQuery( export function useLiveQuery( config: LiveQueryCollectionConfig, deps?: Array>, -): UseLiveQueryReturn> +): UseLiveQueryReturn /** * Subscribe to an existing query collection (can be reactive) @@ -201,15 +221,28 @@ export function useLiveQuery( * // * // */ -// 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, >( - liveQueryCollection: MaybeRefOrGetter>, + liveQueryCollection: MaybeRefOrGetter< + Collection & NonSingleResult + >, ): UseLiveQueryReturnWithCollection +// Overload 4: Accept pre-created live query collection with singleResult: true +export function useLiveQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + liveQueryCollection: MaybeRefOrGetter< + Collection & SingleResult + >, +): UseLiveQueryReturnWithSingleResultCollection + // Implementation export function useLiveQuery( configOrQueryOrCollection: any, @@ -294,7 +327,16 @@ export function useLiveQuery( const internalData = reactive>([]) // 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 = + currentCollection.config + return config.singleResult ? internalData[0] : internalData + }) // Track collection status reactively const status = ref( diff --git a/packages/vue-db/tests/useLiveQuery.test-d.ts b/packages/vue-db/tests/useLiveQuery.test-d.ts new file mode 100644 index 000000000..514a2b913 --- /dev/null +++ b/packages/vue-db/tests/useLiveQuery.test-d.ts @@ -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({ + 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> but should be ComputedRef + expectTypeOf(data.value).toEqualTypeOf() + }) + + it(`should type findOne config object to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + 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> but should be ComputedRef + expectTypeOf(data.value).toEqualTypeOf() + }) + + it(`should type findOne collection using liveQueryCollectionOptions to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + 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() + + const { data } = useLiveQuery(liveQueryCollection) + + // BUG: Currently returns ComputedRef> but should be ComputedRef + expectTypeOf(data.value).toEqualTypeOf() + }) + + it(`should type findOne collection using createLiveQueryCollection to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + 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() + + const { data } = useLiveQuery(liveQueryCollection) + + // BUG: Currently returns ComputedRef> but should be ComputedRef + expectTypeOf(data.value).toEqualTypeOf() + }) + + it(`should type regular query to return an array`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + 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>() + }) +}) diff --git a/packages/vue-db/tests/useLiveQuery.test.ts b/packages/vue-db/tests/useLiveQuery.test.ts index 57b8ae57b..634c5ff8d 100644 --- a/packages/vue-db/tests/useLiveQuery.test.ts +++ b/packages/vue-db/tests/useLiveQuery.test.ts @@ -301,7 +301,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons?.name, })), ) @@ -590,7 +590,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons?.name, })), ) @@ -1162,7 +1162,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - name: persons.name, + name: persons?.name, })), ) @@ -1689,7 +1689,7 @@ describe(`Query Collections`, () => { .select(({ issues, persons }) => ({ id: issues.id, title: issues.title, - userName: persons.name, + userName: persons?.name, })), )