UNPKG

@convex-dev/geospatial

Version:
153 lines (140 loc) 4.88 kB
import type { Rectangle } from "../component/types.js"; import { GeospatialDocument, FilterValue, FilterObject } from "./index.js"; /** * A query for keys within a given shape. */ export interface GeospatialQuery<Doc extends GeospatialDocument> { /** * The shape to query. */ shape: QueryShape; /** * An optional filter expression to apply to the query. */ filter?: (q: GeospatialFilterBuilder<Doc>) => GeospatialFilterExpression<Doc>; /** * An optional limit on the number of results to return (default: 64). */ limit?: number; } interface GeospatialFilterBuilder<Doc extends GeospatialDocument> { /** * Require that a match's field equal a particular value. All conditions are ANDed together, so call * `.eq()` multiple times to further filter the set of matching documents. * * @param field The filter field. * @param value The value to match against. */ eq<FieldName extends keyof Doc["filterKeys"] & string>( field: FieldName, value: FilterValue<Doc, FieldName>, ): GeospatialFilterBuilder<Doc>; /** * Require that a match's field equal any of the provided values. This OR condition applies in addition * to other calls to `.eq()`. There can be at most one `.in()` call in a filter expression. * * @param field The filter field. * @param values The values to match against. */ in<FieldName extends keyof Doc["filterKeys"] & string>( field: FieldName, values: FilterValue<Doc, FieldName>[], ): GeospatialFilterBuilderAfterIn<Doc>; /** * Require that a match's sort key be greater than or equal to the provided value. * * @param value The inclusive lower bound on the sort key. */ gte(field: "sortKey", value: number): GeospatialFilterBuilder<Doc>; /** * Require that a match's sort key be less than the provided value. * * @param value The exclusive upper bound on the sort key. */ lt(field: "sortKey", value: number): GeospatialFilterBuilder<Doc>; } interface GeospatialFilterBuilderAfterIn<Doc extends GeospatialDocument> { /** * Require that a match's field equal a particular value. All conditions are ANDed together, so call * `.eq()` multiple times to further filter the set of matching documents. * * @param field The filter field. * @param value The value to match against. */ eq<FieldName extends keyof Doc["filterKeys"] & string>( field: FieldName, value: FilterValue<Doc, FieldName>, ): GeospatialFilterBuilderAfterIn<Doc>; /** * Require that a match's sort key be greater than or equal to the provided value. * * @param value The inclusive lower bound on the sort key. */ gte(field: "sortKey", value: number): GeospatialFilterBuilderAfterIn<Doc>; /** * Require that a match's sort key be less than the provided value. * * @param value The exclusive upper bound on the sort key. */ lt(field: "sortKey", value: number): GeospatialFilterBuilderAfterIn<Doc>; } type GeospatialFilterExpression<Doc extends GeospatialDocument> = | GeospatialFilterBuilder<Doc> | GeospatialFilterBuilderAfterIn<Doc>; export class FilterBuilderImpl<Doc extends GeospatialDocument> { filterConditions: FilterObject<Doc>[] = []; interval?: { startInclusive?: number; endExclusive?: number }; inDefined: boolean = false; eq<FieldName extends keyof Doc["filterKeys"] & string>( field: FieldName, value: FilterValue<Doc, FieldName>, ): FilterBuilderImpl<Doc> { this.filterConditions.push({ filterKey: field, filterValue: value, occur: "must", }); return this; } in<FieldName extends keyof Doc["filterKeys"] & string>( field: FieldName, values: FilterValue<Doc, FieldName>[], ): FilterBuilderImpl<Doc> { if (this.inDefined) { throw new Error("Invalid query: Can't have multiple `in` clauses."); } this.inDefined = true; for (const value of values) { this.filterConditions.push({ filterKey: field, filterValue: value, occur: "should", }); } return this; } gte(field: "sortKey", value: number): FilterBuilderImpl<Doc> { if (!this.interval) { this.interval = { startInclusive: value }; } else if (!this.interval.startInclusive) { this.interval.startInclusive = value; } else { this.interval.startInclusive = Math.max( this.interval.startInclusive, value, ); } return this; } lt(field: "sortKey", value: number): FilterBuilderImpl<Doc> { if (!this.interval) { this.interval = { endExclusive: value }; } else if (!this.interval.endExclusive) { this.interval.endExclusive = value; } else { this.interval.endExclusive = Math.min(this.interval.endExclusive, value); } return this; } } export type QueryShape = { type: "rectangle"; rectangle: Rectangle };