Skip to content

Commit c4c2399

Browse files
add findOne() to query builder (#440)
* add findOne() to query builder * move utility type to db/types * fix utility type * add single support to aditional useLiveQuery overloads * Add support for specifying `.findOne()` outside of a query builder * WIP addressing of the review * fix for review * remove unused offset * restore text comments * restore text comments * disable limit=1 for future enforcing --------- Co-authored-by: Sam Willis <[email protected]>
1 parent 748b2b2 commit c4c2399

File tree

15 files changed

+441
-24
lines changed

15 files changed

+441
-24
lines changed

docs/reference/classes/basequerybuilder.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,32 @@ query
200200

201201
***
202202

203+
### findOne()
204+
205+
```ts
206+
findOne(): QueryBuilder<TContext>
207+
```
208+
209+
Specify that the query should return a single row as `data` and not an array.
210+
211+
#### Returns
212+
213+
[`QueryBuilder`](../../type-aliases/querybuilder.md)\<`TContext`\>
214+
215+
A QueryBuilder with single return enabled
216+
217+
#### Example
218+
219+
```ts
220+
// Get an user by ID
221+
query
222+
.from({ users: usersCollection })
223+
.where(({users}) => eq(users.id, 1))
224+
.findOne()
225+
```
226+
227+
***
228+
203229
### from()
204230

205231
```ts

packages/db/src/collection/index.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import type {
2424
InferSchemaInput,
2525
InferSchemaOutput,
2626
InsertConfig,
27+
NonSingleResult,
2728
OperationConfig,
29+
SingleResult,
2830
SubscribeChangesOptions,
2931
Transaction as TransactionType,
3032
UtilsRecord,
@@ -50,6 +52,7 @@ export interface Collection<
5052
TInsertInput extends object = T,
5153
> extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
5254
readonly utils: TUtils
55+
readonly singleResult?: true
5356
}
5457

5558
/**
@@ -132,8 +135,22 @@ export function createCollection<
132135
options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
133136
schema: T
134137
utils?: TUtils
135-
}
136-
): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>>
138+
} & NonSingleResult
139+
): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
140+
NonSingleResult
141+
142+
// Overload for when schema is provided and singleResult is true
143+
export function createCollection<
144+
T extends StandardSchemaV1,
145+
TKey extends string | number = string | number,
146+
TUtils extends UtilsRecord = {},
147+
>(
148+
options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
149+
schema: T
150+
utils?: TUtils
151+
} & SingleResult
152+
): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &
153+
SingleResult
137154

138155
// Overload for when no schema is provided
139156
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
@@ -145,8 +162,21 @@ export function createCollection<
145162
options: CollectionConfig<T, TKey, never> & {
146163
schema?: never // prohibit schema if an explicit type is provided
147164
utils?: TUtils
148-
}
149-
): Collection<T, TKey, TUtils, never, T>
165+
} & NonSingleResult
166+
): Collection<T, TKey, TUtils, never, T> & NonSingleResult
167+
168+
// Overload for when no schema is provided and singleResult is true
169+
// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config
170+
export function createCollection<
171+
T extends object,
172+
TKey extends string | number = string | number,
173+
TUtils extends UtilsRecord = {},
174+
>(
175+
options: CollectionConfig<T, TKey, never> & {
176+
schema?: never // prohibit schema if an explicit type is provided
177+
utils?: TUtils
178+
} & SingleResult
179+
): Collection<T, TKey, TUtils, never, T> & SingleResult
150180

151181
// Implementation
152182
export function createCollection(

packages/db/src/query/builder/index.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
SubQueryMustHaveFromClauseError,
1717
} from "../../errors.js"
1818
import { createRefProxy, toExpression } from "./ref-proxy.js"
19-
import type { NamespacedRow } from "../../types.js"
19+
import type { NamespacedRow, SingleResult } from "../../types.js"
2020
import type {
2121
Aggregate,
2222
BasicExpression,
@@ -615,6 +615,28 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
615615
}) as any
616616
}
617617

618+
/**
619+
* Specify that the query should return a single result
620+
* @returns A QueryBuilder that returns the first result
621+
*
622+
* @example
623+
* ```ts
624+
* // Get the user matching the query
625+
* query
626+
* .from({ users: usersCollection })
627+
* .where(({users}) => eq(users.id, 1))
628+
* .findOne()
629+
*```
630+
*/
631+
findOne(): QueryBuilder<TContext & SingleResult> {
632+
return new BaseQueryBuilder({
633+
...this.query,
634+
// TODO: enforcing return only one result with also a default orderBy if none is specified
635+
// limit: 1,
636+
singleResult: true,
637+
})
638+
}
639+
618640
// Helper methods
619641
private _getCurrentAliases(): Array<string> {
620642
const aliases: Array<string> = []
@@ -817,4 +839,10 @@ export type ExtractContext<T> =
817839
: never
818840

819841
// Export the types from types.ts for convenience
820-
export type { Context, Source, GetResult, RefLeaf as Ref } from "./types.js"
842+
export type {
843+
Context,
844+
Source,
845+
GetResult,
846+
RefLeaf as Ref,
847+
InferResultType,
848+
} from "./types.js"

packages/db/src/query/builder/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CollectionImpl } from "../../collection/index.js"
2+
import type { SingleResult } from "../../types.js"
23
import type {
34
Aggregate,
45
BasicExpression,
@@ -47,6 +48,8 @@ export interface Context {
4748
>
4849
// The result type after select (if select has been called)
4950
result?: any
51+
// Single result only (if findOne has been called)
52+
singleResult?: boolean
5053
}
5154

5255
/**
@@ -571,6 +574,7 @@ export type MergeContextWithJoinType<
571574
[K in keyof TNewSchema & string]: TJoinType
572575
}
573576
result: TContext[`result`]
577+
singleResult: TContext[`singleResult`] extends true ? true : false
574578
}
575579

576580
/**
@@ -621,6 +625,14 @@ export type ApplyJoinOptionalityToMergedSchema<
621625
TNewSchema[K]
622626
}
623627

628+
/**
629+
* Utility type to infer the query result size (single row or an array)
630+
*/
631+
export type InferResultType<TContext extends Context> =
632+
TContext extends SingleResult
633+
? GetResult<TContext> | undefined
634+
: Array<GetResult<TContext>>
635+
624636
/**
625637
* GetResult - Determines the final result type of a query
626638
*

packages/db/src/query/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
type Context,
1010
type Source,
1111
type GetResult,
12+
type InferResultType,
1213
} from "./builder/index.js"
1314

1415
// Expression functions exports

packages/db/src/query/ir.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface QueryIR {
1717
limit?: Limit
1818
offset?: Offset
1919
distinct?: true
20+
singleResult?: true
2021

2122
// Functional variants
2223
fnSelect?: (row: NamespacedRow) => any

packages/db/src/query/live-query-collection.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,29 @@ import { CollectionConfigBuilder } from "./live/collection-config-builder.js"
33
import type { LiveQueryCollectionConfig } from "./live/types.js"
44
import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
55
import type { Collection } from "../collection/index.js"
6-
import type { CollectionConfig, UtilsRecord } from "../types.js"
6+
import type {
7+
CollectionConfig,
8+
CollectionConfigSingleRowOption,
9+
NonSingleResult,
10+
SingleResult,
11+
UtilsRecord,
12+
} from "../types.js"
713
import type { Context, GetResult } from "./builder/types.js"
814

15+
type CollectionConfigForContext<
16+
TContext extends Context,
17+
TResult extends object,
18+
> = TContext extends SingleResult
19+
? CollectionConfigSingleRowOption<TResult> & SingleResult
20+
: CollectionConfigSingleRowOption<TResult> & NonSingleResult
21+
22+
type CollectionForContext<
23+
TContext extends Context,
24+
TResult extends object,
25+
> = TContext extends SingleResult
26+
? Collection<TResult> & SingleResult
27+
: Collection<TResult> & NonSingleResult
28+
929
/**
1030
* Creates live query collection options for use with createCollection
1131
*
@@ -35,12 +55,15 @@ export function liveQueryCollectionOptions<
3555
TResult extends object = GetResult<TContext>,
3656
>(
3757
config: LiveQueryCollectionConfig<TContext, TResult>
38-
): CollectionConfig<TResult> {
58+
): CollectionConfigForContext<TContext, TResult> {
3959
const collectionConfigBuilder = new CollectionConfigBuilder<
4060
TContext,
4161
TResult
4262
>(config)
43-
return collectionConfigBuilder.getConfig()
63+
return collectionConfigBuilder.getConfig() as CollectionConfigForContext<
64+
TContext,
65+
TResult
66+
>
4467
}
4568

4669
/**
@@ -83,7 +106,7 @@ export function createLiveQueryCollection<
83106
TResult extends object = GetResult<TContext>,
84107
>(
85108
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
86-
): Collection<TResult, string | number, {}>
109+
): CollectionForContext<TContext, TResult>
87110

88111
// Overload 2: Accept full config object with optional utilities
89112
export function createLiveQueryCollection<
@@ -92,7 +115,7 @@ export function createLiveQueryCollection<
92115
TUtils extends UtilsRecord = {},
93116
>(
94117
config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
95-
): Collection<TResult, string | number, TUtils>
118+
): CollectionForContext<TContext, TResult>
96119

97120
// Implementation
98121
export function createLiveQueryCollection<
@@ -103,7 +126,7 @@ export function createLiveQueryCollection<
103126
configOrQuery:
104127
| (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
105128
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
106-
): Collection<TResult, string | number, TUtils> {
129+
): CollectionForContext<TContext, TResult> {
107130
// Determine if the argument is a function (query) or a config object
108131
if (typeof configOrQuery === `function`) {
109132
// Simple query function case
@@ -113,7 +136,10 @@ export function createLiveQueryCollection<
113136
) => QueryBuilder<TContext>,
114137
}
115138
const options = liveQueryCollectionOptions<TContext, TResult>(config)
116-
return bridgeToCreateCollection(options)
139+
return bridgeToCreateCollection(options) as CollectionForContext<
140+
TContext,
141+
TResult
142+
>
117143
} else {
118144
// Config object case
119145
const config = configOrQuery as LiveQueryCollectionConfig<
@@ -124,7 +150,7 @@ export function createLiveQueryCollection<
124150
return bridgeToCreateCollection({
125151
...options,
126152
utils: config.utils,
127-
})
153+
}) as CollectionForContext<TContext, TResult>
128154
}
129155
}
130156

packages/db/src/query/live/collection-config-builder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { RootStreamBuilder } from "@tanstack/db-ivm"
77
import type { OrderByOptimizationInfo } from "../compiler/order-by.js"
88
import type { Collection } from "../../collection/index.js"
99
import type {
10-
CollectionConfig,
10+
CollectionConfigSingleRowOption,
1111
KeyedStream,
1212
ResultStream,
1313
SyncConfig,
@@ -79,7 +79,7 @@ export class CollectionConfigBuilder<
7979
this.compileBasePipeline()
8080
}
8181

82-
getConfig(): CollectionConfig<TResult> {
82+
getConfig(): CollectionConfigSingleRowOption<TResult> {
8383
return {
8484
id: this.id,
8585
getKey:
@@ -93,6 +93,7 @@ export class CollectionConfigBuilder<
9393
onUpdate: this.config.onUpdate,
9494
onDelete: this.config.onDelete,
9595
startSync: this.config.startSync,
96+
singleResult: this.query.singleResult,
9697
}
9798
}
9899

packages/db/src/query/live/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@ export interface LiveQueryCollectionConfig<
9090
* GC time for the collection
9191
*/
9292
gcTime?: number
93+
94+
/**
95+
* If enabled the collection will return a single object instead of an array
96+
*/
97+
singleResult?: true
9398
}

packages/db/src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,28 @@ export interface CollectionConfig<
503503
sync: SyncConfig<T, TKey>
504504
}
505505

506+
export type SingleResult = {
507+
singleResult: true
508+
}
509+
510+
export type NonSingleResult = {
511+
singleResult?: never
512+
}
513+
514+
export type MaybeSingleResult = {
515+
/**
516+
* If enabled the collection will return a single object instead of an array
517+
*/
518+
singleResult?: true
519+
}
520+
521+
// Only used for live query collections
522+
export type CollectionConfigSingleRowOption<
523+
T extends object = Record<string, unknown>,
524+
TKey extends string | number = string | number,
525+
TSchema extends StandardSchemaV1 = never,
526+
> = CollectionConfig<T, TKey, TSchema> & MaybeSingleResult
527+
506528
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
507529
ChangeMessage<T>
508530
>

0 commit comments

Comments
 (0)