Skip to main content
Home
This release is 3 versions behind 0.30.0 — the latest version of @kitsonk/kv-toolbox. Jump to latest

Built and signed on GitHub Actions

Utilities for working with Deno KV. Encrypted values, batching atomic transactions, handling blobs, querying/filtering, and more.

This package works with Node.js, Deno, Bun, Browsers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
a month ago (0.29.0)
Package root>query.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488
/** * Utilities for querying/filtering entries from a {@linkcode Deno.Kv} instance. * * @module */ import { keyToJSON, type KvKeyJSON, type KvValueJSON, toKey, toValue, valueToJSON } from "jsr:/@deno/kv-utils@^0.1.3/json"; import { equal } from "jsr:/@std/assert@~1/equal"; import { assert } from "jsr:/@std/assert@~1/assert"; import { type BlobMeta, list } from "./blob.ts"; import { keys, type KeyTree, tree, unique, uniqueCount, type UniqueCountElement } from "./keys.ts"; /** * Options which can be used when calling {@linkcode query}. */ export interface QueryOptions extends Deno.KvListOptions { /** * If `true`, only blob entries will be returned as a {@linkcode ReadableStream} of {@linkcode Uint8Array} chunks. */ stream?: boolean; /** * If `true`, only blob entries will be returned as a {@linkcode Blob} or {@linkcode File}. */ blob?: boolean; /** * If `true`, only blob entries will be returned as a {@linkcode Uint8Array}. */ bytes?: boolean; /** * If `true`, only blob entries will be returned as a {@linkcode BlobMeta} object. */ meta?: boolean; } /** * The supported operations for filtering entries. * * The following operations are supported: * * - `"<"` - less than * - `"<="` - less than or equal * - `"=="` - equal * - `">="` - greater than or equal * - `">"` - greater than * - `"!="` - not equal * - `"array-contains"` - array contains the value * - `"array-contains-any"` - array contains any of the values * - `"in"` - value is in the array of supplied values * - `"not-in"` - value is not in the array of supplied values * - `"matches"` - value matches the regular expression * - `"kind-of"` - value is of the specified kind */ export type Operation = | "<" | "<=" | "==" | ">=" | ">" | "!=" | "array-contains" | "array-contains-any" | "in" | "not-in" | "matches" | "kind-of"; type Mappable = Record<string, unknown> | Map<string, unknown>; /** * The different kinds of values that can be stored in a {@linkcode Deno.Kv}. */ export type Kinds = KvValueJSON["type"]; export interface QueryLike<T = unknown> { readonly selector: Deno.KvListSelector; get(): Deno.KvListIterator<T>; } /** * A representation of a {@linkcode Deno.KvListSelector} as a JSON object. */ export type KvListSelectorJSON = | { prefix: KvKeyJSON } | { prefix: KvKeyJSON; start: KvKeyJSON } | { prefix: KvKeyJSON; end: KvKeyJSON } | { start: KvKeyJSON; end: KvKeyJSON }; /** * A representation of an _and_ filter as a JSON object. */ export interface KvFilterAndJSON { kind: "and"; filters: KvFilterJSON[]; } /** * A representation of an _or_ filter as a JSON object. */ export interface KvFilterOrJSON { kind: "or"; filters: KvFilterJSON[]; } /** * A representation of a _where_ filter as a JSON object. */ export interface KvFilterWhereJSON { kind: "where"; property: string | string[]; operation: Operation; value: KvValueJSON; } /** * A representation of a _value_ filter as a JSON object. */ export interface KvFilterValueJSON { kind: "value"; operation: Operation; value: KvValueJSON; } /** * A representation of a filter as a JSON object. */ export type KvFilterJSON = | KvFilterAndJSON | KvFilterOrJSON | KvFilterWhereJSON | KvFilterValueJSON; /** * A representation of a query as a JSON object. */ export interface KvQueryJSON { selector: KvListSelectorJSON; options?: Deno.KvListOptions; filters: KvFilterJSON[]; } function getKind(value: unknown): Kinds { const type = typeof value; switch (type) { case "bigint": case "boolean": case "number": case "string": case "undefined": return type; case "object": if (Array.isArray(value)) { return "Array"; } if (value instanceof DataView) { return "DataView"; } if (ArrayBuffer.isView(value)) { if (value instanceof Int8Array) { return "Int8Array"; } if (value instanceof Uint8Array) { return "Uint8Array"; } if (value instanceof Uint8ClampedArray) { return "Uint8ClampedArray"; } if (value instanceof Int16Array) { return "Int16Array"; } if (value instanceof Uint16Array) { return "Uint16Array"; } if (value instanceof Int32Array) { return "Int32Array"; } if (value instanceof Uint32Array) { return "Uint32Array"; } if (value instanceof Float32Array) { return "Float32Array"; } if (value instanceof Float64Array) { return "Float64Array"; } if (value instanceof BigInt64Array) { return "BigInt64Array"; } if (value instanceof BigUint64Array) { return "BigUint64Array"; } } if (value instanceof ArrayBuffer) { return "ArrayBuffer"; } if (value instanceof Date) { return "Date"; } if ("Deno" in globalThis && value instanceof Deno.KvU64) { return "KvU64"; } if (value instanceof Error) { if (value instanceof EvalError) { return "EvalError"; } if (value instanceof RangeError) { return "RangeError"; } if (value instanceof ReferenceError) { return "ReferenceError"; } if (value instanceof SyntaxError) { return "SyntaxError"; } if (value instanceof TypeError) { return "TypeError"; } if (value instanceof URIError) { return "URIError"; } return "Error"; } if (value instanceof Map) { return "Map"; } if (value === null) { return "null"; } if (value instanceof RegExp) { return "RegExp"; } if (value instanceof Set) { return "Set"; } return "object"; default: return "undefined"; } } function getValue(obj: Mappable, key: string): unknown { if (obj instanceof Map) { return obj.get(key); } return obj[key]; } function hasProperty(obj: Mappable, key: string): boolean { if (obj instanceof Map) { return obj.has(key); } return key in obj; } function isOptionBlob(options: QueryOptions): boolean { return options.blob || options.stream || options.bytes || options.meta || false; } function isMappable(value: unknown): value is Mappable { return typeof value === "object" && value !== null; } function selectorToJSON(selector: Deno.KvListSelector): KvListSelectorJSON { if ("prefix" in selector) { if ("start" in selector) { return { prefix: keyToJSON(selector.prefix), start: keyToJSON(selector.start), }; } if ("end" in selector) { return { prefix: keyToJSON(selector.prefix), end: keyToJSON(selector.end), }; } return { prefix: keyToJSON(selector.prefix) }; } return { start: keyToJSON(selector.start), end: keyToJSON(selector.end) }; } function toSelector(json: KvListSelectorJSON): Deno.KvListSelector { if ("prefix" in json) { if ("start" in json) { return { prefix: toKey(json.prefix), start: toKey(json.start), }; } if ("end" in json) { return { prefix: toKey(json.prefix), end: toKey(json.end), }; } return { prefix: toKey(json.prefix) }; } return { start: toKey(json.start), end: toKey(json.end) }; } /** * A representation of a property path to a value in an object. This is used to * be able to query/filter entries based on nested properties. * * @example * * ```ts * import { PropertyPath } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const path = new PropertyPath("a", "b", "c"); * assert(path.exists({ a: { b: { c: 1 } } })); * assert(!path.exists({ a: { b: { d: 1 } } })); * assert(path.value({ a: { b: { c: 1 } } }) === 1); * ``` */ export class PropertyPath { #parts: string[]; constructor(...parts: string[]) { this.#parts = parts; } /** * Returns `true` if the property path exists in the object. */ exists(other: unknown): other is Mappable { let current = other; for (const part of this.#parts) { if (!isMappable(current)) { return false; } if (!hasProperty(current, part)) { return false; } current = getValue(current, part); } return true; } /** * Returns the value of the property represented by the path. If the property * does not exist, an error is thrown. */ value(other: Mappable): unknown { // deno-lint-ignore no-explicit-any let current: any = other; for (const part of this.#parts) { if (!isMappable(current)) { throw new TypeError("Value is not mappable"); } if (!hasProperty(current, part)) { throw new Error("Property does not exist"); } current = getValue(current, part); } return current; } /** * Convert the property path to a JSON array. */ toJSON(): string[] { return this.#parts; } /** * Create a property path from an array of parts. */ static from(parts: string[]): PropertyPath { return new PropertyPath(...parts); } } // deno-lint-ignore no-explicit-any function exec(other: any, operation: Operation, value: any | any[]): boolean { switch (operation) { case "<": return other < value; case "<=": return other <= value; case "!=": return !equal(other, value); case "==": return equal(other, value); case ">": return other > value; case ">=": return other >= value; case "array-contains": if (Array.isArray(other)) { return other.some((v) => equal(v, value)); } return false; case "array-contains-any": if (Array.isArray(other) && Array.isArray(value)) { return other.some((v) => value.some((w) => equal(v, w))); } return false; case "in": if (Array.isArray(value)) { return value.some((v) => equal(other, v)); } return false; case "not-in": if (Array.isArray(value)) { return !value.some((v) => equal(other, v)); } break; case "matches": if (typeof other === "string" && value instanceof RegExp) { return value.test(other); } break; case "kind-of": return getKind(other) === value; } return false; } /** * A filter instance which can be used to filter entries based on a set of * conditions. Users should use the static methods to create instances of this * class. * * @example Creating a filter based on a property value * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.where("age", "<=", 10); * assert(filter.test({ age: 10 })); * assert(!filter.test({ age: 11 })); * ``` * * @example Creating a filter based on a property value using a `PropertyPath` * * ```ts * import { Filter, PropertyPath } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.where(new PropertyPath("a", "b", "c"), "==", 1); * assert(filter.test({ a: { b: { c: 1 } } })); * assert(!filter.test({ a: { b: { c: 2 } } })); * assert(!filter.test({ a: { b: { d: 1 } } })); * ``` * * @example Creating a filter based on an _or_ condition * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.or( * Filter.where("age", "<", 10), * Filter.where("age", ">", 20), * ); * assert(filter.test({ age: 5 })); * assert(filter.test({ age: 25 })); * assert(!filter.test({ age: 15 })); * ``` * * @example Creating a filter based on an _and_ condition * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.and( * Filter.where("age", ">", 10), * Filter.where("age", "<", 20), * ); * assert(filter.test({ age: 15 })); * assert(!filter.test({ age: 10 })); * assert(!filter.test({ age: 20 })); * ``` */ export class Filter { #kind: "and" | "or" | "value" | "where"; #property?: string | PropertyPath; #filters: Filter[]; #operation?: Operation; #value?: unknown | unknown[]; private constructor(kind: "and" | "or", filters: Filter[]); private constructor( kind: "where", filters: undefined, property: string | PropertyPath, operation: Operation, value: unknown | unknown[], ); private constructor( kind: "value", filters: undefined, property: undefined, operation: Operation, value: unknown | unknown[], ); private constructor( kind: "and" | "or" | "value" | "where", filters: Filter[] = [], property?: string | PropertyPath, operation?: Operation, value?: unknown | unknown[], ) { this.#kind = kind; this.#filters = filters; this.#property = property; this.#operation = operation; this.#value = value; } /** * Test the value against the filter. */ test(value: unknown): boolean { switch (this.#kind) { case "and": return this.#filters.every((f) => f.test(value)); case "or": return this.#filters.some((f) => f.test(value)); case "where": assert(this.#property); assert(this.#operation); if (this.#property instanceof PropertyPath) { if (this.#property.exists(value)) { const propValue = this.#property.value(value); return exec(propValue, this.#operation, this.#value); } return false; } if (isMappable(value)) { if (hasProperty(value, this.#property)) { const propValue = getValue(value, this.#property); return exec(propValue, this.#operation, this.#value); } return false; } return false; case "value": assert(this.#operation); return exec(value, this.#operation, this.#value); } throw new TypeError("Invalid filter kind"); } /** * Convert the filter to a JSON object. */ toJSON(): KvFilterJSON { switch (this.#kind) { case "and": return { kind: "and", filters: this.#filters.map((f) => f.toJSON()), }; case "or": return { kind: "or", filters: this.#filters.map((f) => f.toJSON()), }; case "where": assert(this.#property); assert(this.#operation); return { kind: "where", property: this.#property instanceof PropertyPath ? this.#property.toJSON() : this.#property, operation: this.#operation, value: valueToJSON(this.#value), }; case "value": assert(this.#operation); return { kind: "value", operation: this.#operation, value: valueToJSON(this.#value), }; } } /** * Create a filter which will return `true` if all the filters return `true`. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.and( * Filter.where("age", ">", 10), * Filter.where("age", "<", 20), * ); * assert(filter.test({ age: 15 })); * assert(!filter.test({ age: 10 })); * assert(!filter.test({ age: 20 })); * ``` */ static and(...filters: Filter[]): Filter { return new Filter("and", filters); } /** * Create a filter which will return `true` if any of the filters return * `true`. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.or( * Filter.where("age", "<", 10), * Filter.where("age", ">", 20), * ); * assert(filter.test({ age: 5 })); * assert(filter.test({ age: 25 })); * assert(!filter.test({ age: 15 })); * ``` */ static or(...filters: Filter[]): Filter { return new Filter("or", filters); } /** * Create a filter which will return `true` if the value matches the * operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.value("==", 10); * assert(filter.test(10)); * assert(!filter.test(11)); * ``` */ static value(operation: "kind-of", value: Kinds): Filter; /** * Create a filter which will return `true` if the value matches the * operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.value("==", 10); * assert(filter.test(10)); * assert(!filter.test(11)); * ``` */ static value(operation: "matches", value: RegExp): Filter; /** * Create a filter which will return `true` if the value matches the * operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.value("==", 10); * assert(filter.test(10)); * assert(!filter.test(11)); * ``` */ static value( operation: "in" | "not-in" | "array-contains-any", value: unknown[], ): Filter; /** * Create a filter which will return `true` if the value matches the * operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.value("==", 10); * assert(filter.test(10)); * assert(!filter.test(11)); * ``` */ static value(operation: Operation, value: unknown): Filter; static value(operation: Operation, value: unknown | unknown[]): Filter { return new Filter("value", undefined, undefined, operation, value); } /** * Create a filter which will return `true` if the value of the property * matches the operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.where("age", "<=", 10); * assert(filter.test({ age: 10 })); * assert(!filter.test({ age: 11 })); * ``` */ static where( property: string | PropertyPath, operation: "kind-of", value: Kinds, ): Filter; /** * Create a filter which will return `true` if the value of the property * matches the operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.where("age", "<=", 10); * assert(filter.test({ age: 10 })); * assert(!filter.test({ age: 11 })); * ``` */ static where( property: string | PropertyPath, operation: "matches", value: RegExp, ): Filter; /** * Create a filter which will return `true` if the value of the property * matches the operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.where("age", "<=", 10); * assert(filter.test({ age: 10 })); * assert(!filter.test({ age: 11 })); * ``` */ static where( property: string | PropertyPath, operation: "in" | "not-in" | "array-contains-any", value: unknown[], ): Filter; /** * Create a filter which will return `true` if the value of the property * matches the operation and value. * * @example * * ```ts * import { Filter } from "@kitsonk/kv-toolbox/query"; * import { assert } from "@std/assert/assert"; * * const filter = Filter.where("age", "<=", 10); * assert(filter.test({ age: 10 })); * assert(!filter.test({ age: 11 })); * ``` */ static where( property: string | PropertyPath, operation: Operation, value: unknown, ): Filter; static where( property: string | PropertyPath, operation: Operation, value: unknown | unknown[], ): Filter { return new Filter("where", undefined, property, operation, value); } /** * Parse a filter from a JSON object. */ static parse(json: KvFilterJSON): Filter { switch (json.kind) { case "and": return Filter.and(...json.filters.map(Filter.parse)); case "or": return Filter.or(...json.filters.map(Filter.parse)); case "where": return Filter.where( Array.isArray(json.property) ? PropertyPath.from(json.property) : json.property, json.operation, toValue(json.value), ); case "value": return Filter.value(json.operation, toValue(json.value)); } } } const AsyncIterator = Object.getPrototypeOf(async function* () {}).constructor; class QueryListIterator<T = unknown> extends AsyncIterator implements Deno.KvListIterator<T> { #iterator: Deno.KvListIterator<T>; #count = 0; #limit?: number; #query: Filter[]; get cursor(): string { return this.#iterator.cursor; } constructor( iterator: Deno.KvListIterator<T>, query: Filter[], limit?: number, ) { super(); this.#iterator = iterator; this.#query = query; this.#limit = limit; } async next(): Promise<IteratorResult<Deno.KvEntry<T>, undefined>> { for await (const entry of this.#iterator) { if (this.#query.every((f) => f.test(entry.value))) { this.#count++; if (this.#limit && this.#count > this.#limit) { return { value: undefined, done: true }; } return { value: entry, done: false }; } } return { value: undefined, done: true }; } [Symbol.asyncIterator](): AsyncIterableIterator<Deno.KvEntry<T>> { return this; } } /** * Query instance for filtering entries from a {@linkcode Deno.Kv} instance. */ export class Query<T = unknown> implements QueryLike<T> { #kv: Deno.Kv; #limit?: number; #selector: Deno.KvListSelector; #options: QueryOptions; #query: Filter[] = []; /** * The selector that is used to query the entries. */ get selector(): Deno.KvListSelector { return { ...this.#selector }; } constructor( kv: Deno.Kv, selector: Deno.KvListSelector, options: QueryOptions = {}, ) { this.#kv = kv; this.#selector = selector; const { limit, ...rest } = options; this.#limit = limit; this.#options = rest; } /** * Resolves with an array of unique sub keys/prefixes for the provided prefix * along with the number of sub keys that match that prefix. The `count` * represents the number of sub keys, a value of `0` indicates that only the * exact key exists with no sub keys. * * This is useful when storing keys and values in a hierarchical/tree view, * where you are retrieving a list including counts and you want to know all * the unique _descendants_ of a key in order to be able to enumerate them. * * @example * * If you had the following keys stored in a datastore and the query matched * the keys: * * ``` * ["a", "b"] * ["a", "b", "c"] * ["a", "d", "e"] * ["a", "d", "f"] * ``` * * And you would get the following results when using `.counts()`: * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const kv = await Deno.openKv(); * console.log(await query(kv, { prefix: ["a"] }).counts()); * // { key: ["a", "b"], count: 1 } * // { key: ["a", "d"], count: 2 } * await kv.close(); * ``` */ counts(): Promise<UniqueCountElement[]> { return uniqueCount(this); } /** * Get the entries that match the query conditions. */ get(): Deno.KvListIterator<T> { const iterator = (isOptionBlob(this.#options) ? list(this.#kv, this.#selector, this.#options) : this.#kv.list<T>(this.#selector, this.#options)) as Deno.KvListIterator<T>; return new QueryListIterator<T>(iterator, this.#query, this.#limit); } /** * Set the limit of the number of entries to return. * * This will override any limit that may have been set in the options. */ limit(limit: number): this { this.#limit = limit; return this; } /** * Return an array of keys that match the query. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const kv = await Deno.openKv(); * console.log(await query(kv, { prefix: ["hello"] }).keys()); * await kv.close(); * ``` */ keys(): Promise<Deno.KvKey[]> { return keys(this); } /** * Query a Deno KV store for keys and resolve with any matching keys * organized into a tree structure. * * Each child node indicates if it also has a value and any children of that * node. * * @example * * If you had the following keys stored in a datastore and the query matched * the values of all the entries: * * ``` * ["a", "b"] * ["a", "b", "c"] * ["a", "d", "e"] * ["a", "d", "f"] * ``` * * And you would get the following results when using `.tree()`: * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const kv = await Deno.openKv(); * console.log(await query(kv, { prefix: ["a"] }).tree()); * // { * // prefix: ["a"], * // children: [ * // { * // part: "b", * // hasValue: true, * // children: [{ part: "c", hasValue: true }] * // }, { * // part: "d", * // children: [ * // { part: "e", hasValue: true }, * // { part: "f", hasValue: true } * // ] * // } * // ] * // } * await kv.close(); * ``` */ tree(): Promise<KeyTree> { return tree(this); } /** * Resolves with an array of unique sub keys/prefixes for the matched query. * * This is useful when storing keys and values in a hierarchical/tree view, * where you are retrieving a list and you want to know all the unique * _descendants_ of a key in order to be able to enumerate them. * * @example * * The following keys stored in a datastore that matched the query: * * ``` * ["a", "b"] * ["a", "b", "c"] * ["a", "d", "e"] * ["a", "d", "f"] * ``` * * The following results when using `.unique()`: * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const kv = await Deno.openKv(); * console.log(await query(kv, { prefix: ["a"] }).unique()); * // ["a", "b"] * // ["a", "d"] * await kv.close(); * ``` */ unique(): Promise<Deno.KvKey[]> { return unique(this); } /** * Add a filter to the query where the value of the entry matches the * operation and value. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .value("==", { age: 10 }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ value(operation: "kind-of", value: Kinds): this; /** * Add a filter to the query where the value of the entry matches the * operation and value. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .value("==", { age: 10 }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ value(operation: "matches", value: RegExp): this; /** * Add a filter to the query where the value of the entry matches the * operation and value. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .value("==", { age: 10 }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ value( operation: "in" | "not-in" | "array-contains-any", value: unknown[], ): this; /** * Add a filter to the query where the value of the entry matches the * operation and value. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .value("==", { age: 10 }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ value(operation: Operation, value: unknown): this; value(operation: Operation, value: unknown | unknown[]): this { this.#query.push(Filter.value(operation, value)); return this; } /** * Add a filter to the query. Only entries which values match all the filters * will be returned. * * @example * * ```ts * import { query, Filter } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where(Filter.and( * Filter.where("age", ">", 10), * Filter.where("age", "<", 20), * )) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ where(filter: Filter): this; /** * Add a property filter to the query. Only entries which values match the * filter will be returned. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where("age", "<=", 10) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ where( property: string | PropertyPath, operation: "kind-of", value: Kinds, ): this; /** * Add a property filter to the query. Only entries which values match the * filter will be returned. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where("age", "<=", 10) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ where( property: string | PropertyPath, operation: "matches", value: RegExp, ): this; /** * Add a property filter to the query. Only entries which values match the * filter will be returned. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where("age", "<=", 10) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ where( property: string | PropertyPath, operation: "in" | "not-in" | "array-contains-any", value: unknown[], ): this; /** * Add a property filter to the query. Only entries which values match the * filter will be returned. * * @example * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where("age", "<=", 10) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` */ where( property: string | PropertyPath, operation: Operation, value: unknown, ): this; where( propertyOrFilter: Filter | string | PropertyPath, operation?: Operation, value?: unknown | unknown[], ): this { if (propertyOrFilter instanceof Filter) { this.#query.push(propertyOrFilter); } else { assert(operation, "Operation is required"); this.#query.push(Filter.where(propertyOrFilter, operation, value)); } return this; } /** * Convert the query to a JSON object. */ toJSON(): KvQueryJSON { return { selector: selectorToJSON(this.#selector), options: this.#options, filters: this.#query.map((f) => f.toJSON()), }; } /** * Parse a query from an instance of {@linkcode Deno.Kv} and a JSON object. */ static parse<T = unknown>(kv: Deno.Kv, json: KvQueryJSON): Query<T> { const query = new Query<T>(kv, toSelector(json.selector), json.options); query.#query = json.filters.map(Filter.parse); return query; } } /** * Query/filter entries from a {@linkcode Deno.Kv} instance. * * The query instance can be used to filter entries based on a set of * conditions. Then the filtered entries can be retrieved using the `.get()` * method, which returns an async iterator that will yield the entries that * match the conditions. * * At a base level a query works like the `Deno.Kv.prototype.list()` method, but * with the added ability to filter entries based on the query conditions. * * @example Querying blob entries as `ReadableStream<Uint8Array>` values * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }, { stream: true }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @template T the type of the value stored in the {@linkcode Deno.Kv} instance * @param kv the target {@linkcode Deno.Kv} instance * @param selector the selector to use for selecting entries * @param options * @returns */ export function query( kv: Deno.Kv, selector: Deno.KvListSelector, options: QueryOptions & { stream: true }, ): Query<ReadableStream<Uint8Array>>; /** * Query/filter entries from a {@linkcode Deno.Kv} instance. * * The query instance can be used to filter entries based on a set of * conditions. Then the filtered entries can be retrieved using the `.get()` * method, which returns an async iterator that will yield the entries that * match the conditions. * * At a base level a query works like the `Deno.Kv.prototype.list()` method, but * with the added ability to filter entries based on the query conditions. * * @example Querying blob entries as `Blob` or `File` values * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }, { blob: true }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @template T the type of the value stored in the {@linkcode Deno.Kv} instance * @param kv the target {@linkcode Deno.Kv} instance * @param selector the selector to use for selecting entries * @param options * @returns */ export function query( kv: Deno.Kv, selector: Deno.KvListSelector, options: QueryOptions & { blob: true }, ): Query<Blob | File>; /** * Query/filter entries from a {@linkcode Deno.Kv} instance. * * The query instance can be used to filter entries based on a set of * conditions. Then the filtered entries can be retrieved using the `.get()` * method, which returns an async iterator that will yield the entries that * match the conditions. * * At a base level a query works like the `Deno.Kv.prototype.list()` method, but * with the added ability to filter entries based on the query conditions. * * @example Querying blob entries as `Uint8Array` values * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }, { bytes: true }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @template T the type of the value stored in the {@linkcode Deno.Kv} instance * @param kv the target {@linkcode Deno.Kv} instance * @param selector the selector to use for selecting entries * @param options * @returns */ export function query( kv: Deno.Kv, selector: Deno.KvListSelector, options: QueryOptions & { bytes: true }, ): Query<Uint8Array>; /** * Query/filter entries from a {@linkcode Deno.Kv} instance. * * The query instance can be used to filter entries based on a set of * conditions. Then the filtered entries can be retrieved using the `.get()` * method, which returns an async iterator that will yield the entries that * match the conditions. * * At a base level a query works like the `Deno.Kv.prototype.list()` method, but * with the added ability to filter entries based on the query conditions. * * @example Querying blob entries as `BlobMeta` values * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }, { meta: true }) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @template T the type of the value stored in the {@linkcode Deno.Kv} instance * @param kv the target {@linkcode Deno.Kv} instance * @param selector the selector to use for selecting entries * @param options * @returns */ export function query( kv: Deno.Kv, selector: Deno.KvListSelector, options: QueryOptions & { meta: true }, ): Query<BlobMeta>; /** * Query/filter entries from a {@linkcode Deno.Kv} instance. * * The query instance can be used to filter entries based on a set of * conditions. Then the filtered entries can be retrieved using the `.get()` * method, which returns an async iterator that will yield the entries that * match the conditions. * * At a base level a query works like the `Deno.Kv.prototype.list()` method, but * with the added ability to filter entries based on the query conditions. * * @example Filtering entries based on a property value * * ```ts * import { query } from "@kitsonk/kv-toolbox/query"; * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where("age", "<=", 10) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @example Filtering entries based on a property value using a `PropertyPath` * * ```ts * import { query, PropertyPath } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * // matches { a: { b: { c: 1 } } } * .where(new PropertyPath("a", "b", "c"), "==", 1) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @example Filtering entries based on an _or_ condition * * ```ts * import { query, Filter } from "@kitsonk/kv-toolbox/query"; * * const db = await Deno.openKv(); * const result = query(db, { prefix: [] }) * .where(Filter.or( * Filter.where("age", "<", 10), * Filter.where("age", ">", 20), * )) * .get(); * for await (const entry of result) { * console.log(entry); * } * db.close(); * ``` * * @template T the type of the value stored in the {@linkcode Deno.Kv} instance * @param kv the target {@linkcode Deno.Kv} instance * @param selector the selector to use for selecting entries * @param options * @returns */ export function query<T = unknown>(kv: Deno.Kv, selector: Deno.KvListSelector, options?: QueryOptions): Query<T>; export function query<T = unknown>(kv: Deno.Kv, selector: Deno.KvListSelector, options: QueryOptions = {}): Query<T> { return new Query(kv, selector, options); }