From 78734ba91be84644b7f4f27b3ddfcdb5632637d6 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:57:32 +0000 Subject: [PATCH 1/6] test: assert findOne returns single object in angular-db (issue #1261) Co-Authored-By: Claude Opus 4.6 --- .../tests/inject-live-query.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index 2f4db01ca..1175c20a5 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -752,6 +752,40 @@ describe(`injectLiveQuery`, () => { }) }) + it(`should return a single object for findOne query`, async () => { + await TestBed.runInInjectionContext(async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-angular`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const { state, data } = injectLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + + await waitForAngularUpdate() + + expect(state().size).toBe(1) + expect(state().get(`3`)).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + // findOne should return a single object, not an array + expect(Array.isArray(data())).toBe(false) + expect(data()).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + }) + }) + describe(`eager execution during sync`, () => { it(`should show state while isLoading is true during sync`, async () => { await TestBed.runInInjectionContext(async () => { From 0689ccc8ab91a6199665847e016370031fd6dde4 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 19 Feb 2026 15:19:24 +0100 Subject: [PATCH 2/6] test: add type assertions for injectLiveQuery findOne in angular-db Adds inject-live-query.test-d.ts with expectTypeOf assertions verifying that injectLiveQuery with findOne() types data as Signal and regular queries type data as Signal>. Co-Authored-By: Claude Opus 4.6 --- .../tests/inject-live-query.test-d.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 packages/angular-db/tests/inject-live-query.test-d.ts diff --git a/packages/angular-db/tests/inject-live-query.test-d.ts b/packages/angular-db/tests/inject-live-query.test-d.ts new file mode 100644 index 000000000..667e11314 --- /dev/null +++ b/packages/angular-db/tests/inject-live-query.test-d.ts @@ -0,0 +1,138 @@ +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 { injectLiveQuery } from '../src/index' +import type { SingleResult } from '../../db/src/types' + +type Person = { + id: string + name: string + age: number + email: string + isActive: boolean + team: string +} + +describe(`injectLiveQuery type assertions`, () => { + it(`should type findOne query builder to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-angular`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const { data } = injectLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + + // findOne returns a single result or undefined + expectTypeOf(data()).toEqualTypeOf() + }) + + it(`should type findOne config object to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-config-angular`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const { data } = injectLiveQuery({ + params: () => ({ id: `3` }), + query: ({ params, q }) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, params.id)) + .findOne(), + }) + + // findOne returns a single result or undefined + expectTypeOf(data()).toEqualTypeOf() + }) + + it(`should type findOne collection using liveQueryCollectionOptions to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-options-angular`, + 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 } = injectLiveQuery(liveQueryCollection) + + // findOne returns a single result or undefined + expectTypeOf(data()).toEqualTypeOf() + }) + + it(`should type findOne collection using createLiveQueryCollection to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-create-angular`, + 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 } = injectLiveQuery(liveQueryCollection) + + // findOne returns a single result or undefined + expectTypeOf(data()).toEqualTypeOf() + }) + + it(`should type regular query to return an array`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-array-angular`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const { data } = injectLiveQuery((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()).toEqualTypeOf>() + }) +}) From a137fcaa65bbd9cf8c0a9c9142eb33ddc855902e Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 19 Feb 2026 15:35:16 +0100 Subject: [PATCH 3/6] test: fix type errors in inject-live-query test helpers - Add NonSingleResult to createMockCollection return type since mock collections are never singleResult - Add non-null assertions for collection() signal access in tests Co-Authored-By: Claude Opus 4.6 --- .../tests/inject-live-query.test.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index 1175c20a5..81fbb12a9 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -14,6 +14,7 @@ import type { CollectionStatus, Context, LiveQueryCollectionConfig, + NonSingleResult, QueryBuilder, } from '@tanstack/db' @@ -66,12 +67,13 @@ async function waitForAngularUpdate() { function createMockCollection( initial: Array> = [], initialStatus: CollectionStatus = `ready`, -): Collection> & { - __setStatus: (s: CollectionStatus) => void - __replaceAll: (rows: Array>) => void - __upsert: (row: T & Record<`id`, K>) => void - __delete: (key: K) => void -} { +): Collection> & + NonSingleResult & { + __setStatus: (s: CollectionStatus) => void + __replaceAll: (rows: Array>) => void + __upsert: (row: T & Record<`id`, K>) => void + __delete: (key: K) => void + } { const map = new Map() for (const r of initial) { map.set(r.id, r) @@ -532,7 +534,7 @@ describe(`injectLiveQuery`, () => { await waitForAngularUpdate() - expect(res.collection().id).toEqual(expect.any(String)) + expect(res.collection()!.id).toEqual(expect.any(String)) expect(res.status()).toBe(`ready`) expect(Array.isArray(res.data())).toBe(true) expect(res.state() instanceof Map).toBe(true) @@ -565,7 +567,7 @@ describe(`injectLiveQuery`, () => { const res = injectLiveQuery(config) await waitForAngularUpdate() - expect(res.collection().id).toEqual(expect.any(String)) + expect(res.collection()!.id).toEqual(expect.any(String)) expect(res.isReady()).toBe(true) }) }) @@ -611,7 +613,7 @@ describe(`injectLiveQuery`, () => { await waitForAngularUpdate() - expect(res.collection().id).toEqual(expect.any(String)) + expect(res.collection()!.id).toEqual(expect.any(String)) expect(res.status()).toBe(`ready`) expect(Array.isArray(res.data())).toBe(true) expect(res.state() instanceof Map).toBe(true) @@ -829,7 +831,7 @@ describe(`injectLiveQuery`, () => { }) // Start the live query sync manually - liveQueryCollection().preload() + liveQueryCollection()!.preload() await waitForAngularUpdate() @@ -941,7 +943,7 @@ describe(`injectLiveQuery`, () => { }) // Start the live query sync manually - liveQueryCollection().preload() + liveQueryCollection()!.preload() await waitForAngularUpdate() @@ -1041,7 +1043,7 @@ describe(`injectLiveQuery`, () => { }) // Start the live query sync manually - liveQueryCollection().preload() + liveQueryCollection()!.preload() await waitForAngularUpdate() From e5c47ba8a3edeba92651e5b7364feb005397848e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:59:32 +0000 Subject: [PATCH 4/6] fix: return single object from findOne in angular-db instead of array (issue #1261) Co-Authored-By: Claude Opus 4.6 --- packages/angular-db/src/index.ts | 89 ++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index e96a70de2..ae2735d47 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -10,12 +10,16 @@ import { BaseQueryBuilder, createLiveQueryCollection } from '@tanstack/db' import type { ChangeMessage, Collection, + CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, + InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, + NonSingleResult, QueryBuilder, + SingleResult, } from '@tanstack/db' import type { Signal } from '@angular/core' @@ -24,16 +28,14 @@ import type { Signal } from '@angular/core' * Contains reactive signals for the query state and data. */ export interface InjectLiveQueryResult< - TResult extends object = any, - TKey extends string | number = string | number, - TUtils extends Record = {}, + TContext extends Context, > { /** A signal containing the complete state map of results keyed by their ID */ - state: Signal> - /** A signal containing the results as an array */ - data: Signal> + state: Signal>> + /** A signal containing the results as an array, or single result for findOne queries */ + data: Signal> /** A signal containing the underlying collection instance (null for disabled queries) */ - collection: Signal | null> + collection: Signal, string | number, {}> | null> /** A signal containing the current status of the collection */ status: Signal /** A signal indicating whether the collection is currently loading */ @@ -48,6 +50,38 @@ export interface InjectLiveQueryResult< isCleanedUp: Signal } +export interface InjectLiveQueryResultWithCollection< + TResult extends object = any, + TKey extends string | number = string | number, + TUtils extends Record = {}, +> { + state: Signal> + data: Signal> + collection: Signal | null> + status: Signal + isLoading: Signal + isReady: Signal + isIdle: Signal + isError: Signal + isCleanedUp: Signal +} + +export interface InjectLiveQueryResultWithSingleResultCollection< + TResult extends object = any, + TKey extends string | number = string | number, + TUtils extends Record = {}, +> { + state: Signal> + data: Signal + collection: Signal<(Collection & SingleResult) | null> + status: Signal + isLoading: Signal + isReady: Signal + isIdle: Signal + isError: Signal + isCleanedUp: Signal +} + export function injectLiveQuery< TContext extends Context, TParams extends any, @@ -57,7 +91,7 @@ export function injectLiveQuery< params: TParams q: InitialQueryBuilder }) => QueryBuilder -}): InjectLiveQueryResult> +}): InjectLiveQueryResult export function injectLiveQuery< TContext extends Context, TParams extends any, @@ -67,25 +101,34 @@ export function injectLiveQuery< params: TParams q: InitialQueryBuilder }) => QueryBuilder | undefined | null -}): InjectLiveQueryResult> +}): InjectLiveQueryResult export function injectLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, -): InjectLiveQueryResult> +): InjectLiveQueryResult export function injectLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, -): InjectLiveQueryResult> +): InjectLiveQueryResult export function injectLiveQuery( config: LiveQueryCollectionConfig, -): InjectLiveQueryResult> +): InjectLiveQueryResult +// Pre-created collection without singleResult +export function injectLiveQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + liveQueryCollection: Collection & NonSingleResult, +): InjectLiveQueryResultWithCollection +// Pre-created collection with singleResult export function injectLiveQuery< TResult extends object, TKey extends string | number, TUtils extends Record, >( - liveQueryCollection: Collection, -): InjectLiveQueryResult + liveQueryCollection: Collection & SingleResult, +): InjectLiveQueryResultWithSingleResultCollection export function injectLiveQuery(opts: any) { assertInInjectionContext(injectLiveQuery) const destroyRef = inject(DestroyRef) @@ -156,11 +199,23 @@ export function injectLiveQuery(opts: any) { }) const state = signal(new Map()) - const data = signal>([]) + const internalData = signal>([]) const status = signal( collection() ? `idle` : `disabled`, ) + // Returns single item for singleResult collections, array otherwise + const data = computed(() => { + const currentCollection = collection() + if (!currentCollection) { + return internalData() + } + const config = (currentCollection).config as + | CollectionConfigSingleRowOption + | undefined + return config?.singleResult ? internalData()[0] : internalData() + }) + const syncDataFromCollection = ( currentCollection: Collection, ) => { @@ -168,7 +223,7 @@ export function injectLiveQuery(opts: any) { const newData = Array.from(currentCollection.values()) state.set(newState) - data.set(newData) + internalData.set(newData) status.set(currentCollection.status) } @@ -185,7 +240,7 @@ export function injectLiveQuery(opts: any) { if (!currentCollection) { status.set(`disabled` as const) state.set(new Map()) - data.set([]) + internalData.set([]) cleanup() return } From 31bb75cbfd51c6b6f957d84f06032385e7f8dee7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:02:07 +0000 Subject: [PATCH 5/6] ci: apply automated fixes --- packages/angular-db/src/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/angular-db/src/index.ts b/packages/angular-db/src/index.ts index ae2735d47..329d08800 100644 --- a/packages/angular-db/src/index.ts +++ b/packages/angular-db/src/index.ts @@ -27,15 +27,17 @@ import type { Signal } from '@angular/core' * The result of calling `injectLiveQuery`. * Contains reactive signals for the query state and data. */ -export interface InjectLiveQueryResult< - TContext extends Context, -> { +export interface InjectLiveQueryResult { /** A signal containing the complete state map of results keyed by their ID */ state: Signal>> /** A signal containing the results as an array, or single result for findOne queries */ data: Signal> /** A signal containing the underlying collection instance (null for disabled queries) */ - collection: Signal, string | number, {}> | null> + collection: Signal, + string | number, + {} + > | null> /** A signal containing the current status of the collection */ status: Signal /** A signal indicating whether the collection is currently loading */ @@ -210,7 +212,7 @@ export function injectLiveQuery(opts: any) { if (!currentCollection) { return internalData() } - const config = (currentCollection).config as + const config = currentCollection.config as | CollectionConfigSingleRowOption | undefined return config?.singleResult ? internalData()[0] : internalData() From 60c11ddd17370d824cc004f40451eed7f921c9b3 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 19 Feb 2026 15:40:59 +0100 Subject: [PATCH 6/6] chore: add changeset for angular-db findOne fix Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-angular-findone.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-angular-findone.md diff --git a/.changeset/fix-angular-findone.md b/.changeset/fix-angular-findone.md new file mode 100644 index 000000000..f27f35f12 --- /dev/null +++ b/.changeset/fix-angular-findone.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-db': patch +--- + +Fix `injectLiveQuery` with `findOne()` returning an array instead of a single object, and add proper type overloads so TypeScript correctly infers `Signal` for `findOne()` queries