diff --git a/src/collection.ts b/src/collection.ts index b31e9ed..83ed789 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -209,8 +209,8 @@ export class Collection { */ public findFirst( predicate?: - | ((query: Query>) => Query) - | Query>, + | ((query: Query>>) => Query) + | Query>>, options?: StrictOptions, ): Strict extends true ? RecordType> @@ -248,8 +248,8 @@ export class Collection { */ public findMany( predicate?: - | ((query: Query>) => Query) - | Query>, + | ((query: Query>>) => Query) + | Query>>, options?: PaginationOptions & SortOptions & StrictOptions, ): Array>> { const query = @@ -291,8 +291,8 @@ export class Collection { */ public async update( predicate: - | ((query: Query>) => Query) - | Query> + | ((query: Query>>) => Query) + | Query>> | RecordType>, options: UpdateOptions> & StrictOptions, @@ -340,8 +340,8 @@ export class Collection { */ public async updateMany( predicate: - | ((query: Query>) => Query) - | Query>, + | ((query: Query>>) => Query) + | Query>>, options: UpdateOptions> & SortOptions & StrictOptions, @@ -380,8 +380,8 @@ export class Collection { */ public delete( predicate: - | ((query: Query>) => Query) - | Query> + | ((query: Query>>) => Query) + | Query>> | RecordType>, options?: StrictOptions, ): Strict extends true @@ -415,8 +415,8 @@ export class Collection { */ public deleteMany( predicate: - | ((query: Query>) => Query) - | Query>, + | ((query: Query>>) => Query) + | Query>>, options?: SortOptions & StrictOptions, ): Array>> { /** @@ -618,7 +618,7 @@ export class Collection { } *#query( - query: Query>, + query: Query>>, options: PaginationOptions = { take: Infinity }, ): Generator< RecordType> | undefined, @@ -668,7 +668,7 @@ export class Collection { for (let i = start; i !== end; i += delta) { const record = this.#records[i] - if (query.test(record)) { + if (record != null && query.test(record)) { if (skip != null) { if (skipped < skip) { skipped++ diff --git a/src/query.ts b/src/query.ts index 86e3a8f..cd8c1f0 100644 --- a/src/query.ts +++ b/src/query.ts @@ -20,7 +20,7 @@ export type Condition = } : never -export type PredicateFunction = (value: T) => boolean +export type PredicateFunction = (value: T) => unknown export class Query { #predicate?: PredicateFunction @@ -33,19 +33,23 @@ export class Query { return !!this.#predicate?.(value) } - public where(condition: Condition) { + public where(condition: Condition | PredicateFunction) { return new Query( Query.#and(this.#predicate, Query.#normalize(condition)), ) } - public and(...conditions: Array | Condition>) { + public and( + ...conditions: Array | Condition | PredicateFunction> + ) { return new Query( Query.#and(this.#predicate, ...conditions.map(Query.#normalize)), ) } - public or(...conditions: Array | Condition>) { + public or( + ...conditions: Array | Condition | PredicateFunction> + ) { return new Query( Query.#or(this.#predicate, ...conditions.map(Query.#normalize)), ) diff --git a/tests/find-first.test.ts b/tests/find-first.test.ts index 1ea8d50..cf57be6 100644 --- a/tests/find-first.test.ts +++ b/tests/find-first.test.ts @@ -159,3 +159,17 @@ it('queries by nullable properties', async () => { users.findFirst((q) => q.where({ organizationId: (id) => id !== null })), ).toEqual({ id: 2, organizationId: 5 }) }) + +it('supports top-level record-based predicate via `q.where`', async () => { + const users = new Collection({ + schema: z.object({ id: z.number(), name: z.string() }), + }) + + const john = await users.create({ id: 1, name: 'John' }) + await users.create({ id: 2, name: 'Kate' }) + + expect(users.findFirst((q) => q.where((user) => user.id === 1))).toEqual(john) + expect( + users.findFirst((q) => q.where((user) => user.id === 123)), + ).toBeUndefined() +}) diff --git a/tests/find-many.test.ts b/tests/find-many.test.ts index 7a93fae..5a87512 100644 --- a/tests/find-many.test.ts +++ b/tests/find-many.test.ts @@ -107,3 +107,17 @@ it('queries by nullable properties', async () => { users.findMany((q) => q.where({ organizationId: (id) => id !== null })), ).toEqual([{ id: 2, organizationId: 5 }]) }) + +it('supports top-level record-based predicate via `q.where`', async () => { + const users = new Collection({ + schema: z.object({ id: z.number(), name: z.string() }), + }) + + const john = await users.create({ id: 1, name: 'John' }) + const johnatan = await users.create({ id: 2, name: 'Johnatan' }) + + expect( + users.findMany((q) => q.where((user) => user.name.startsWith('John'))), + ).toEqual([john, johnatan]) + expect(users.findMany((q) => q.where((user) => user.id > 3))).toEqual([]) +}) diff --git a/tests/types/find-first.test-d.ts b/tests/types/find-first.test-d.ts index d6aad4c..fa090c4 100644 --- a/tests/types/find-first.test-d.ts +++ b/tests/types/find-first.test-d.ts @@ -1,5 +1,5 @@ -import { Collection, type RecordType } from '#/src/collection.js' import { z } from 'zod' +import { Collection, type RecordType } from '#/src/collection.js' it('does not require a query argument', () => { const users = new Collection({ @@ -69,3 +69,17 @@ it('supports a strict mode', () => { .toHaveProperty('strict') .toEqualTypeOf() }) + +it('supports top-level record-based predicate via `q.where`', () => { + const users = new Collection({ + schema: z.object({ id: z.number(), name: z.string() }), + }) + + users.findFirst((q) => + q.where((user) => { + expectTypeOf(user).toEqualTypeOf< + RecordType<{ id: number; name: string }> + >() + }), + ) +})