diff --git a/.changeset/svelte-findone-support.md b/.changeset/svelte-findone-support.md new file mode 100644 index 000000000..0dffe35c6 --- /dev/null +++ b/.changeset/svelte-findone-support.md @@ -0,0 +1,26 @@ +--- +'@tanstack/svelte-db': patch +--- + +Add `findOne()` / `SingleResult` support to `useLiveQuery` hook. + +When using `.findOne()` in a query, the `data` property is now correctly typed as `T | undefined` instead of `Array`, matching the React implementation. + +**Example:** + +```ts +const query = useLiveQuery((q) => + q + .from({ users: usersCollection }) + .where(({ users }) => eq(users.id, userId)) + .findOne(), +) + +// query.data is now typed as User | undefined (not User[]) +``` + +This works with all query patterns: + +- Query functions: `useLiveQuery((q) => q.from(...).findOne())` +- Config objects: `useLiveQuery({ query: (q) => q.from(...).findOne() })` +- Pre-created collections with `SingleResult` diff --git a/docs/framework/svelte/reference/functions/useLiveQuery.md b/docs/framework/svelte/reference/functions/useLiveQuery.md index 835bd781b..74b6534f4 100644 --- a/docs/framework/svelte/reference/functions/useLiveQuery.md +++ b/docs/framework/svelte/reference/functions/useLiveQuery.md @@ -8,10 +8,10 @@ title: useLiveQuery ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>; +function useLiveQuery(queryFn, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, InferResultType>; ``` -Defined in: [useLiveQuery.svelte.ts:155](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L155) +Defined in: [useLiveQuery.svelte.ts:160](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L160) Create a live query using a query function @@ -37,7 +37,7 @@ Array of reactive dependencies that trigger query re-execution when changed ### Returns -[`UseLiveQueryReturn`](../interfaces/UseLiveQueryReturn.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\> +[`UseLiveQueryReturn`](../interfaces/UseLiveQueryReturn.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}, `InferResultType`\<`TContext`\>\> Reactive object with query data, state, and status information @@ -134,10 +134,10 @@ const todosQuery = useLiveQuery((q) => ## Call Signature ```ts -function useLiveQuery(queryFn, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>; +function useLiveQuery(queryFn, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, InferResultType | undefined>; ``` -Defined in: [useLiveQuery.svelte.ts:161](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L161) +Defined in: [useLiveQuery.svelte.ts:166](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L166) Create a live query using a query function @@ -163,7 +163,7 @@ Array of reactive dependencies that trigger query re-execution when changed ### Returns -[`UseLiveQueryReturn`](../interfaces/UseLiveQueryReturn.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\> +[`UseLiveQueryReturn`](../interfaces/UseLiveQueryReturn.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}, `InferResultType`\<`TContext`\> \| `undefined`\> Reactive object with query data, state, and status information @@ -260,10 +260,10 @@ const todosQuery = useLiveQuery((q) => ## Call Signature ```ts -function useLiveQuery(config, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }>; +function useLiveQuery(config, deps?): UseLiveQueryReturn<{ [K in string | number | symbol]: (TContext["result"] extends object ? any[any] : TContext["hasJoins"] extends true ? TContext["schema"] : TContext["schema"][TContext["fromSourceName"]])[K] }, InferResultType>; ``` -Defined in: [useLiveQuery.svelte.ts:206](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L206) +Defined in: [useLiveQuery.svelte.ts:214](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L214) Create a live query using configuration object @@ -289,7 +289,7 @@ Array of reactive dependencies that trigger query re-execution when changed ### Returns -[`UseLiveQueryReturn`](../interfaces/UseLiveQueryReturn.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}\> +[`UseLiveQueryReturn`](../interfaces/UseLiveQueryReturn.md)\<\{ \[K in string \| number \| symbol\]: (TContext\["result"\] extends object ? any\[any\] : TContext\["hasJoins"\] extends true ? TContext\["schema"\] : TContext\["schema"\]\[TContext\["fromSourceName"\]\])\[K\] \}, `InferResultType`\<`TContext`\>\> Reactive object with query data, state, and status information @@ -333,10 +333,10 @@ const itemsQuery = useLiveQuery({ ## Call Signature ```ts -function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithCollection; +function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithCollection; ``` -Defined in: [useLiveQuery.svelte.ts:255](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L255) +Defined in: [useLiveQuery.svelte.ts:263](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L263) Subscribe to an existing query collection (can be reactive) @@ -358,13 +358,13 @@ Subscribe to an existing query collection (can be reactive) #### liveQueryCollection -`MaybeGetter`\<`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\>\> +`MaybeGetter`\<`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `NonSingleResult`\> Pre-created query collection to subscribe to (can be a getter) ### Returns -[`UseLiveQueryReturnWithCollection`](../interfaces/UseLiveQueryReturnWithCollection.md)\<`TResult`, `TKey`, `TUtils`\> +[`UseLiveQueryReturnWithCollection`](../interfaces/UseLiveQueryReturnWithCollection.md)\<`TResult`, `TKey`, `TUtils`, `TResult`[]\> Reactive object with query data, state, and status information @@ -412,3 +412,129 @@ const queryResult = useLiveQuery(sharedQuery) // {/each} // {/if} ``` + +## Call Signature + +```ts +function useLiveQuery(liveQueryCollection): UseLiveQueryReturnWithCollection; +``` + +Defined in: [useLiveQuery.svelte.ts:274](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L274) + +Create a live query using a query function + +### Type Parameters + +#### TResult + +`TResult` *extends* `object` + +#### TKey + +`TKey` *extends* `string` \| `number` + +#### TUtils + +`TUtils` *extends* `Record`\<`string`, `any`\> + +### Parameters + +#### liveQueryCollection + +`MaybeGetter`\<`Collection`\<`TResult`, `TKey`, `TUtils`, `StandardSchemaV1`\<`unknown`, `unknown`\>, `TResult`\> & `SingleResult`\> + +### Returns + +[`UseLiveQueryReturnWithCollection`](../interfaces/UseLiveQueryReturnWithCollection.md)\<`TResult`, `TKey`, `TUtils`, `TResult` \| `undefined`\> + +Reactive object with query data, state, and status information + +### Remarks + +**IMPORTANT - Destructuring in Svelte 5:** +Direct destructuring breaks reactivity. To destructure, wrap with `$derived`: + +❌ **Incorrect** - Loses reactivity: +```ts +const { data, isLoading } = useLiveQuery(...) +``` + +✅ **Correct** - Maintains reactivity: +```ts +// Option 1: Use dot notation (recommended) +const query = useLiveQuery(...) +// Access: query.data, query.isLoading + +// Option 2: Wrap with $derived for destructuring +const query = useLiveQuery(...) +const { data, isLoading } = $derived(query) +``` + +This is a fundamental Svelte 5 limitation, not a library bug. See: +https://github.com/sveltejs/svelte/issues/11002 + +### Examples + +```ts +// Basic query with object syntax (recommended pattern) +const todosQuery = useLiveQuery((q) => + q.from({ todos: todosCollection }) + .where(({ todos }) => eq(todos.completed, false)) + .select(({ todos }) => ({ id: todos.id, text: todos.text })) +) +// Access via: todosQuery.data, todosQuery.isLoading, etc. +``` + +```ts +// With reactive dependencies +let minPriority = $state(5) +const todosQuery = useLiveQuery( + (q) => q.from({ todos: todosCollection }) + .where(({ todos }) => gt(todos.priority, minPriority)), + [() => minPriority] // Re-run when minPriority changes +) +``` + +```ts +// Destructuring with $derived (if needed) +const query = useLiveQuery((q) => + q.from({ todos: todosCollection }) +) +const { data, isLoading, isError } = $derived(query) +// Now data, isLoading, and isError maintain reactivity +``` + +```ts +// Join pattern +const issuesQuery = useLiveQuery((q) => + q.from({ issues: issueCollection }) + .join({ persons: personCollection }, ({ issues, persons }) => + eq(issues.userId, persons.id) + ) + .select(({ issues, persons }) => ({ + id: issues.id, + title: issues.title, + userName: persons.name + })) +) +``` + +```ts +// Handle loading and error states in template +const todosQuery = useLiveQuery((q) => + q.from({ todos: todoCollection }) +) + +// In template: +// {#if todosQuery.isLoading} +//
Loading...
+// {:else if todosQuery.isError} +//
Error: {todosQuery.status}
+// {:else} +//
    +// {#each todosQuery.data as todo (todo.id)} +//
  • {todo.text}
  • +// {/each} +//
+// {/if} +``` diff --git a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md index ee969a2eb..714736f4b 100644 --- a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md +++ b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturn.md @@ -3,9 +3,9 @@ id: UseLiveQueryReturn title: UseLiveQueryReturn --- -# Interface: UseLiveQueryReturn\ +# Interface: UseLiveQueryReturn\ -Defined in: [useLiveQuery.svelte.ts:29](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L29) +Defined in: [useLiveQuery.svelte.ts:33](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L33) Return type for useLiveQuery hook @@ -15,6 +15,10 @@ Return type for useLiveQuery hook `T` *extends* `object` +### TData + +`TData` = `T`[] + ## Properties ### collection @@ -24,7 +28,7 @@ collection: Collection; ``` -Defined in: [useLiveQuery.svelte.ts:32](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L32) +Defined in: [useLiveQuery.svelte.ts:36](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L36) The underlying query collection instance @@ -33,12 +37,12 @@ The underlying query collection instance ### data ```ts -data: T[]; +data: TData; ``` -Defined in: [useLiveQuery.svelte.ts:31](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L31) +Defined in: [useLiveQuery.svelte.ts:35](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L35) -Reactive array of query results in order +Reactive array of query results in order, or single item when using findOne() *** @@ -48,7 +52,7 @@ Reactive array of query results in order isCleanedUp: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:38](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L38) +Defined in: [useLiveQuery.svelte.ts:42](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L42) True when query has been cleaned up @@ -60,7 +64,7 @@ True when query has been cleaned up isError: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:37](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L37) +Defined in: [useLiveQuery.svelte.ts:41](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L41) True when query encountered an error @@ -72,7 +76,7 @@ True when query encountered an error isIdle: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:36](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L36) +Defined in: [useLiveQuery.svelte.ts:40](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L40) True when query hasn't started yet @@ -84,7 +88,7 @@ True when query hasn't started yet isLoading: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:34](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L34) +Defined in: [useLiveQuery.svelte.ts:38](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L38) True while initial query data is loading @@ -96,7 +100,7 @@ True while initial query data is loading isReady: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:35](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L35) +Defined in: [useLiveQuery.svelte.ts:39](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L39) True when query has received first data and is ready @@ -108,7 +112,7 @@ True when query has received first data and is ready state: Map; ``` -Defined in: [useLiveQuery.svelte.ts:30](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L30) +Defined in: [useLiveQuery.svelte.ts:34](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L34) Reactive Map of query results (key → item) @@ -120,6 +124,6 @@ Reactive Map of query results (key → item) status: CollectionStatus; ``` -Defined in: [useLiveQuery.svelte.ts:33](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L33) +Defined in: [useLiveQuery.svelte.ts:37](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L37) Current query status diff --git a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md index 9d921fcf9..0d62c7ae8 100644 --- a/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md +++ b/docs/framework/svelte/reference/interfaces/UseLiveQueryReturnWithCollection.md @@ -3,9 +3,9 @@ id: UseLiveQueryReturnWithCollection title: UseLiveQueryReturnWithCollection --- -# Interface: UseLiveQueryReturnWithCollection\ +# Interface: UseLiveQueryReturnWithCollection\ -Defined in: [useLiveQuery.svelte.ts:41](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L41) +Defined in: [useLiveQuery.svelte.ts:45](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L45) ## Type Parameters @@ -21,6 +21,10 @@ Defined in: [useLiveQuery.svelte.ts:41](https://github.com/TanStack/db/blob/main `TUtils` *extends* `Record`\<`string`, `any`\> +### TData + +`TData` = `T`[] + ## Properties ### collection @@ -29,17 +33,17 @@ Defined in: [useLiveQuery.svelte.ts:41](https://github.com/TanStack/db/blob/main collection: Collection; ``` -Defined in: [useLiveQuery.svelte.ts:48](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L48) +Defined in: [useLiveQuery.svelte.ts:53](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L53) *** ### data ```ts -data: T[]; +data: TData; ``` -Defined in: [useLiveQuery.svelte.ts:47](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L47) +Defined in: [useLiveQuery.svelte.ts:52](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L52) *** @@ -49,7 +53,7 @@ Defined in: [useLiveQuery.svelte.ts:47](https://github.com/TanStack/db/blob/main isCleanedUp: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:54](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L54) +Defined in: [useLiveQuery.svelte.ts:59](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L59) *** @@ -59,7 +63,7 @@ Defined in: [useLiveQuery.svelte.ts:54](https://github.com/TanStack/db/blob/main isError: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:53](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L53) +Defined in: [useLiveQuery.svelte.ts:58](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L58) *** @@ -69,7 +73,7 @@ Defined in: [useLiveQuery.svelte.ts:53](https://github.com/TanStack/db/blob/main isIdle: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:52](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L52) +Defined in: [useLiveQuery.svelte.ts:57](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L57) *** @@ -79,7 +83,7 @@ Defined in: [useLiveQuery.svelte.ts:52](https://github.com/TanStack/db/blob/main isLoading: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:50](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L50) +Defined in: [useLiveQuery.svelte.ts:55](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L55) *** @@ -89,7 +93,7 @@ Defined in: [useLiveQuery.svelte.ts:50](https://github.com/TanStack/db/blob/main isReady: boolean; ``` -Defined in: [useLiveQuery.svelte.ts:51](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L51) +Defined in: [useLiveQuery.svelte.ts:56](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L56) *** @@ -99,7 +103,7 @@ Defined in: [useLiveQuery.svelte.ts:51](https://github.com/TanStack/db/blob/main state: Map; ``` -Defined in: [useLiveQuery.svelte.ts:46](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L46) +Defined in: [useLiveQuery.svelte.ts:51](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L51) *** @@ -109,4 +113,4 @@ Defined in: [useLiveQuery.svelte.ts:46](https://github.com/TanStack/db/blob/main status: CollectionStatus; ``` -Defined in: [useLiveQuery.svelte.ts:49](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L49) +Defined in: [useLiveQuery.svelte.ts:54](https://github.com/TanStack/db/blob/main/packages/svelte-db/src/useLiveQuery.svelte.ts#L54) diff --git a/packages/svelte-db/src/useLiveQuery.svelte.ts b/packages/svelte-db/src/useLiveQuery.svelte.ts index 436112b5f..cd82a59fa 100644 --- a/packages/svelte-db/src/useLiveQuery.svelte.ts +++ b/packages/svelte-db/src/useLiveQuery.svelte.ts @@ -6,18 +6,22 @@ import { BaseQueryBuilder, createLiveQueryCollection } from '@tanstack/db' import type { ChangeMessage, Collection, + CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, + InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, + NonSingleResult, QueryBuilder, + SingleResult, } from '@tanstack/db' /** * 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 item when using findOne() * @property collection - The underlying query collection instance * @property status - Current query status * @property isLoading - True while initial query data is loading @@ -26,9 +30,9 @@ import type { * @property isError - True when query encountered an error * @property isCleanedUp - True when query has been cleaned up */ -export interface UseLiveQueryReturn { +export interface UseLiveQueryReturn> { state: Map - data: Array + data: TData collection: Collection status: CollectionStatus isLoading: boolean @@ -42,9 +46,10 @@ export interface UseLiveQueryReturnWithCollection< T extends object, TKey extends string | number, TUtils extends Record, + TData = Array, > { state: Map - data: Array + data: TData collection: Collection status: CollectionStatus isLoading: boolean @@ -155,7 +160,7 @@ function toValue(value: MaybeGetter): T { export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, deps?: Array<() => unknown>, -): UseLiveQueryReturn> +): UseLiveQueryReturn, InferResultType> // Overload 1b: Accept query function that can return undefined/null export function useLiveQuery( @@ -163,7 +168,10 @@ export function useLiveQuery( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, deps?: Array<() => unknown>, -): UseLiveQueryReturn> +): UseLiveQueryReturn< + GetResult, + InferResultType | undefined +> /** * Create a live query using configuration object @@ -206,7 +214,7 @@ export function useLiveQuery( export function useLiveQuery( config: LiveQueryCollectionConfig, deps?: Array<() => unknown>, -): UseLiveQueryReturn> +): UseLiveQueryReturn, InferResultType> /** * Subscribe to an existing query collection (can be reactive) @@ -251,14 +259,27 @@ export function useLiveQuery( * // {/each} * // {/if} */ -// Overload 3: Accept pre-created live query collection (can be reactive) +// Overload 3: Accept pre-created live query collection WITHOUT SingleResult (returns array) export function useLiveQuery< TResult extends object, TKey extends string | number, TUtils extends Record, >( - liveQueryCollection: MaybeGetter>, -): UseLiveQueryReturnWithCollection + liveQueryCollection: MaybeGetter< + Collection & NonSingleResult + >, +): UseLiveQueryReturnWithCollection> + +// Overload 4: Accept pre-created live query collection WITH SingleResult (returns single item) +export function useLiveQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + liveQueryCollection: MaybeGetter< + Collection & SingleResult + >, +): UseLiveQueryReturnWithCollection // Implementation export function useLiveQuery( @@ -438,6 +459,18 @@ export function useLiveQuery( return state }, get data() { + const currentCollection = collection + if (currentCollection) { + const config = + currentCollection.config as CollectionConfigSingleRowOption< + any, + any, + any + > + if (config.singleResult) { + return internalData[0] + } + } return internalData }, get collection() { diff --git a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts index edf7eb79c..cb16e8579 100644 --- a/packages/svelte-db/tests/useLiveQuery.svelte.test.ts +++ b/packages/svelte-db/tests/useLiveQuery.svelte.test.ts @@ -1852,4 +1852,221 @@ describe(`Query Collections`, () => { }) }) }) + + describe(`findOne() - single result queries`, () => { + it(`should return a single row when using findOne() with query function`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-1`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + const query = useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + + flushSync() + + // State should still contain the item as a Map entry + expect(query.state.size).toBe(1) + expect(query.state.get(`3`)).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + // Data should be a single object, not an array + expect(query.data).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + expect(Array.isArray(query.data)).toBe(false) + }) + }) + + it(`should return a single row when using findOne() with config object`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-2`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + const query = useLiveQuery({ + query: (q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + }) + + flushSync() + + expect(query.state.size).toBe(1) + expect(query.data).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + expect(Array.isArray(query.data)).toBe(false) + }) + }) + + it(`should return a single row with pre-created collection using findOne()`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-3`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + const liveQueryCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + }) + + const query = useLiveQuery(liveQueryCollection) + + flushSync() + + expect(query.state.size).toBe(1) + expect(query.data).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + expect(Array.isArray(query.data)).toBe(false) + }) + }) + + it(`should return undefined when findOne() matches no rows`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-empty`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + const query = useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `999`)) // Non-existent ID + .findOne(), + ) + + flushSync() + + expect(query.state.size).toBe(0) + expect(query.data).toBeUndefined() + }) + }) + + it(`should reactively update single result when data changes`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-reactive`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + const query = useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + + flushSync() + + expect(query.data).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + // Update the person + collection.utils.begin() + collection.utils.write({ + type: `update`, + value: { + id: `3`, + name: `John Smith Updated`, + age: 36, + email: `john.smith@example.com`, + isActive: true, + team: `team1`, + }, + }) + collection.utils.commit() + + flushSync() + + expect(query.data).toMatchObject({ + id: `3`, + name: `John Smith Updated`, + age: 36, + }) + expect(Array.isArray(query.data)).toBe(false) + }) + }) + + it(`should transition from single result to undefined when item is deleted`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-delete`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + cleanup = $effect.root(() => { + const query = useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + + flushSync() + + expect(query.data).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + // Delete the person + collection.utils.begin() + collection.utils.write({ + type: `delete`, + value: { + id: `3`, + name: `John Smith`, + age: 35, + email: `john.smith@example.com`, + isActive: true, + team: `team1`, + }, + }) + collection.utils.commit() + + flushSync() + + expect(query.data).toBeUndefined() + expect(query.state.size).toBe(0) + }) + }) + }) })