/** * Defines a function `filter` that wraps a query, attaching a * JavaScript/TypeScript function that filters results just like * `db.query(...).filter(...)` but with more generality. * */ import type { DocumentByInfo, GenericTableInfo, PaginationOptions, QueryInitializer, PaginationResult, FilterBuilder, Expression, OrderedQuery, IndexRange, IndexRangeBuilder, Indexes, NamedIndex, NamedSearchIndex, Query, SearchFilterBuilder, SearchIndexes, } from "convex/server"; import { SearchFilter } from "convex/server"; async function asyncFilter( arr: T[], predicate: (d: T) => Promise | boolean, ): Promise { const results = await Promise.all(arr.map(predicate)); return arr.filter((_v, index) => results[index]); } class QueryWithFilter< T extends GenericTableInfo, > implements QueryInitializer { // q actually is only guaranteed to implement OrderedQuery, // but we forward all QueryInitializer methods to it and if they fail they fail. q: QueryInitializer; p: Predicate; iterator?: AsyncIterator; constructor(q: OrderedQuery, p: Predicate) { this.q = q as QueryInitializer; this.p = p; } filter(predicate: (q: FilterBuilder) => Expression): this { // eslint-disable-next-line @convex-dev/no-filter-in-query return new QueryWithFilter(this.q.filter(predicate), this.p) as this; } order(order: "asc" | "desc"): QueryWithFilter { return new QueryWithFilter(this.q.order(order), this.p); } async paginate( paginationOpts: PaginationOptions, ): Promise>> { const result = await this.q.paginate(paginationOpts); return { ...result, page: await asyncFilter(result.page, this.p) }; } async collect(): Promise[]> { const results = await this.q.collect(); return await asyncFilter(results, this.p); } async take(n: number): Promise[]> { const results: DocumentByInfo[] = []; for await (const result of this) { results.push(result); if (results.length >= n) { break; } } return results; } async first(): Promise | null> { for await (const result of this) { return result; } return null; } async unique(): Promise | null> { let uniqueResult: DocumentByInfo | null = null; for await (const result of this) { if (uniqueResult === null) { uniqueResult = result; } else { throw new Error( `unique() query returned more than one result: [${uniqueResult._id}, ${result._id}, ...]`, ); } } return uniqueResult; } [Symbol.asyncIterator](): AsyncIterator, any, undefined> { this.iterator = this.q[Symbol.asyncIterator](); return this; } async next(): Promise> { for (;;) { const { value, done } = await this.iterator!.next(); if (value && (await this.p(value))) { return { value, done }; } if (done) { return { value: null, done: true }; } } } return() { return this.iterator!.return!(); } // Implement the remainder of QueryInitializer. fullTableScan(): QueryWithFilter { return new QueryWithFilter(this.q.fullTableScan(), this.p); } withIndex>( indexName: IndexName, indexRange?: | (( q: IndexRangeBuilder, NamedIndex, 0>, ) => IndexRange) | undefined, ): Query { return new QueryWithFilter(this.q.withIndex(indexName, indexRange), this.p); } withSearchIndex>( indexName: IndexName, searchFilter: ( q: SearchFilterBuilder, NamedSearchIndex>, ) => SearchFilter, ): OrderedQuery { return new QueryWithFilter( this.q.withSearchIndex(indexName, searchFilter), this.p, ); } } export type Predicate = ( doc: DocumentByInfo, ) => Promise | boolean; type QueryTableInfo = Q extends OrderedQuery ? T : never; /** * Applies a filter to a database query, just like `.filter((q) => ...)` but * supporting arbitrary JavaScript/TypeScript. * Performance is roughly the same as `.filter((q) => ...)`. If you want better * performance, use an index to narrow down the results before filtering. * * Examples: * * // Full table scan, filtered to short messages. * return await filter( * ctx.db.query("messages"), * async (message) => message.body.length < 10, * ).collect(); * * // Short messages by author, paginated. * return await filter( * ctx.db.query("messages").withIndex("by_author", q=>q.eq("author", args.author)), * async (message) => message.body.length < 10, * ).paginate(args.paginationOpts); * * // Same behavior as above: Short messages by author, paginated. * // Note the filter can wrap any part of the query pipeline, and it is applied * // at the end. This is how RowLevelSecurity works. * const shortMessages = await filter( * ctx.db.query("messages"), * async (message) => message.body.length < 10, * ); * return await shortMessages * .withIndex("by_author", q=>q.eq("author", args.author)) * .paginate(args.paginationOpts); * * // Also works with `order()`, `take()`, `unique()`, and `first()`. * return await filter( * ctx.db.query("messages").order("desc"), * async (message) => message.body.length < 10, * ).first(); * * @param query The query to filter. * @param predicate Async function to run on each document before it is yielded * from the query pipeline. * @returns A new query with the filter applied. */ export function filter>( query: Q, predicate: Predicate>, ): Q { return new QueryWithFilter>( query as unknown as OrderedQuery>, predicate, ) as any as Q; }