UNPKG

@convex-dev/aggregate

Version:

Convex component to calculate counts and sums of values for efficient aggregation.

143 lines (129 loc) 3.89 kB
/** * The Aggregate API uses keys and IDs, where the keys are for sorting and * IDs are for tie-breaking uniqueness. The component's BTree API uses * positions, which are unique keys. */ import type { Value } from "convex/values"; import type { Key } from "../component/btree.js"; export type Bound<K extends Key, ID extends string> = { key: K; id?: ID; inclusive: boolean; }; export type SideBounds<K extends Key, ID extends string> = { lower?: Bound<K, ID>; upper?: Bound<K, ID>; }; // e.g. TuplePrefix<[string, number]> = [] | [string] | [string, number] export type TuplePrefix< K extends unknown[], P extends unknown[] = [], > = P["length"] extends K["length"] ? P : P | TuplePrefix<K, [...P, K[P["length"]]]>; export type Bounds<K extends Key, ID extends string> = | SideBounds<K, ID> | (K extends unknown[] ? { prefix: TuplePrefix<Extract<K, unknown[]>> } : never) | { eq: K; }; // IDs are strings so in the Convex ordering, null < IDs < arrays. const BEFORE_ALL_IDS = null; const AFTER_ALL_IDS: never[] = []; // First a key, which is exploded with explodeKey. // Then the ID, or BEFORE_ALL_IDS or AFTER_ALL_IDS. // Then a value to be inclusive or exclusive. export type Position = [Key, string | null | never[], "" | null | never[]]; function explodeKey<K extends Key>(key: K): Key { if (Array.isArray(key)) { const exploded = [""]; for (const item of key) { exploded.push(item); exploded.push(""); } return exploded; } return key; } function implodeKey(k: Key): Key { if (Array.isArray(k)) { const imploded: Value[] = []; for (let i = 1; i < k.length; i += 2) { imploded.push(k[i]); } return imploded; } return k; } export function keyToPosition<K extends Key, ID extends string>( key: K, id: ID, ): Position { return [explodeKey(key), id, ""]; } export function positionToKey<K extends Key, ID extends string>( position: Position, ): { key: K; id: ID } { return { key: implodeKey(position[0]) as K, id: position[1] as ID }; } export function boundsToPositions<K extends Key, ID extends string>( bounds?: Bounds<K, ID>, ): { k1?: Position; k2?: Position } { if (bounds === undefined) { return {}; } if ("eq" in bounds) { return { k1: boundToPosition("lower", { key: bounds.eq, inclusive: true }), k2: boundToPosition("upper", { key: bounds.eq, inclusive: true }), }; } if ("prefix" in bounds) { const prefix: Key[] = bounds.prefix; const exploded: Key = []; for (const item of prefix) { exploded.push(""); exploded.push(item); } return { k1: [exploded.concat([BEFORE_ALL_IDS]), BEFORE_ALL_IDS, BEFORE_ALL_IDS], k2: [exploded.concat([AFTER_ALL_IDS]), AFTER_ALL_IDS, AFTER_ALL_IDS], }; } return { k1: boundToPosition("lower", bounds.lower), k2: boundToPosition("upper", bounds.upper), }; } export function boundToPosition<K extends Key, ID extends string>( direction: "lower" | "upper", bound: Bound<K, ID>, ): Position; export function boundToPosition(direction: "lower" | "upper"): undefined; export function boundToPosition<K extends Key, ID extends string>( direction: "lower" | "upper", bound?: Bound<K, ID>, ): Position | undefined; export function boundToPosition<K extends Key, ID extends string>( direction: "lower" | "upper", bound?: Bound<K, ID>, ): Position | undefined { if (bound === undefined) { return undefined; } if (direction === "lower") { return [ explodeKey(bound.key), bound.id ?? (bound.inclusive ? BEFORE_ALL_IDS : AFTER_ALL_IDS), bound.inclusive ? BEFORE_ALL_IDS : AFTER_ALL_IDS, ]; } else { return [ explodeKey(bound.key), bound.id ?? (bound.inclusive ? AFTER_ALL_IDS : BEFORE_ALL_IDS), bound.inclusive ? AFTER_ALL_IDS : BEFORE_ALL_IDS, ]; } }