UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

1,518 lines (1,513 loc) 50.5 kB
import { compareValues, convexToJson, jsonToConvex } from "convex/values"; //#region src/orm/filter-expression.ts /** * Unique symbol for FilterExpression brand * Prevents structural typing - only expressions created by factory functions are valid */ const FilterExpressionBrand = Symbol("FilterExpression"); /** * Create a field reference * Used internally by operator functions */ function fieldRef(fieldName) { return { __brand: "FieldReference", fieldName }; } /** * Type guard for FieldReference */ function isFieldReference(value) { return value && typeof value === "object" && value.__brand === "FieldReference"; } /** * Create a column wrapper * Used internally by query builder's _createColumnProxies */ function column(builder, columnName) { return { builder, columnName }; } function resolveColumn(col) { if (col && typeof col === "object" && "columnName" in col) return col; if (isFieldReference(col)) return { builder: col, columnName: col.fieldName }; const builder = col; const columnName = builder?.config?.name; if (!columnName) throw new Error("Column builder is missing a column name"); return column(builder, columnName); } /** * Internal binary expression implementation * Private class - only accessible via factory functions */ var BinaryExpressionImpl = class { [FilterExpressionBrand] = true; type = "binary"; constructor(operator, operands) { this.operator = operator; this.operands = operands; } accept(visitor) { return visitor.visitBinary(this); } }; /** * Internal logical expression implementation * Private class - only accessible via factory functions */ var LogicalExpressionImpl = class { [FilterExpressionBrand] = true; type = "logical"; constructor(operator, operands) { this.operator = operator; this.operands = operands; } accept(visitor) { return visitor.visitLogical(this); } }; /** * Internal unary expression implementation * Private class - only accessible via factory functions */ var UnaryExpressionImpl = class { [FilterExpressionBrand] = true; type = "unary"; constructor(operator, operands) { this.operator = operator; this.operands = operands; } accept(visitor) { return visitor.visitUnary(this); } }; /** * Equality operator: field == value * * @example * const filter = eq(cols.name, 'Alice'); */ function eq(col, value) { return new BinaryExpressionImpl("eq", [fieldRef(resolveColumn(col).columnName), value]); } /** * Not equal operator: field != value */ function ne(col, value) { return new BinaryExpressionImpl("ne", [fieldRef(resolveColumn(col).columnName), value]); } /** * Greater than operator: field > value */ function gt(col, value) { return new BinaryExpressionImpl("gt", [fieldRef(resolveColumn(col).columnName), value]); } /** * Greater than or equal operator: field >= value */ function gte(col, value) { return new BinaryExpressionImpl("gte", [fieldRef(resolveColumn(col).columnName), value]); } /** * Less than operator: field < value */ function lt(col, value) { return new BinaryExpressionImpl("lt", [fieldRef(resolveColumn(col).columnName), value]); } /** * Less than or equal operator: field <= value */ function lte(col, value) { return new BinaryExpressionImpl("lte", [fieldRef(resolveColumn(col).columnName), value]); } /** * Between operator: field BETWEEN min AND max (inclusive) * * Sugar for and(gte(field, min), lte(field, max)). */ function between(col, min, max) { return and(gte(col, min), lte(col, max)); } /** * Not between operator: field < min OR field > max * * Sugar for or(lt(field, min), gt(field, max)). */ function notBetween(col, min, max) { return or(lt(col, min), gt(col, max)); } /** * LIKE operator: SQL-style pattern matching with % wildcards * Note: Implemented as post-filter (Convex has no native LIKE) * * @example * const users = await db.query.users.findMany({ * where: like(users.name, '%alice%'), * }); */ function like(col, pattern) { return new BinaryExpressionImpl("like", [fieldRef(resolveColumn(col).columnName), pattern]); } /** * ILIKE operator: Case-insensitive LIKE * Note: Implemented as post-filter (Convex has no native LIKE) * * @example * const users = await db.query.users.findMany({ * where: ilike(users.name, '%ALICE%'), * }); */ function ilike(col, pattern) { return new BinaryExpressionImpl("ilike", [fieldRef(resolveColumn(col).columnName), pattern]); } /** * NOT LIKE operator: Negated LIKE pattern */ function notLike(col, pattern) { return new BinaryExpressionImpl("notLike", [fieldRef(resolveColumn(col).columnName), pattern]); } /** * NOT ILIKE operator: Negated case-insensitive LIKE */ function notIlike(col, pattern) { return new BinaryExpressionImpl("notIlike", [fieldRef(resolveColumn(col).columnName), pattern]); } /** * startsWith operator: Check if string starts with prefix * Optimized for prefix matching * * @example * const users = await db.query.users.findMany({ * where: startsWith(users.email, 'admin@'), * }); */ function startsWith(col, prefix) { return new BinaryExpressionImpl("startsWith", [fieldRef(resolveColumn(col).columnName), prefix]); } /** * endsWith operator: Check if string ends with suffix * * @example * const users = await db.query.users.findMany({ * where: endsWith(users.email, '@example.com'), * }); */ function endsWith(col, suffix) { return new BinaryExpressionImpl("endsWith", [fieldRef(resolveColumn(col).columnName), suffix]); } /** * contains operator: Check if string contains substring * Can use search index for optimization when available * * @example * const posts = await db.query.posts.findMany({ * where: contains(posts.title, 'javascript'), * }); */ function contains(col, substring) { return new BinaryExpressionImpl("contains", [fieldRef(resolveColumn(col).columnName), substring]); } /** * Logical AND: all expressions must be true * Filters out undefined expressions (following Drizzle pattern) * * @example * const filter = and( * eq(fieldRef('age'), 25), * eq(fieldRef('name'), 'Alice') * ); */ function and(...expressions) { const defined = expressions.filter((expr) => expr !== void 0); if (defined.length === 0) return; if (defined.length === 1) return defined[0]; return new LogicalExpressionImpl("and", defined); } /** * Logical OR: at least one expression must be true * Filters out undefined expressions (following Drizzle pattern) * * @example * const filter = or( * eq(fieldRef('status'), 'active'), * eq(fieldRef('status'), 'pending') * ); */ function or(...expressions) { const defined = expressions.filter((expr) => expr !== void 0); if (defined.length === 0) return; if (defined.length === 1) return defined[0]; return new LogicalExpressionImpl("or", defined); } /** * Logical NOT: negates expression * * @example * const filter = not(eq(fieldRef('isDeleted'), true)); */ function not(expression) { return new UnaryExpressionImpl("not", [expression]); } /** * Array membership operator: field IN array * * @example * const filter = inArray(cols.status, ['active', 'pending']); */ function inArray(col, values) { return new BinaryExpressionImpl("inArray", [fieldRef(resolveColumn(col).columnName), values]); } /** * Array exclusion operator: field NOT IN array * Validates array is non-empty at construction time * * @example * const filter = notInArray(cols.role, ['admin', 'moderator']); */ function notInArray(col, values) { if (!Array.isArray(values) || values.length === 0) throw new Error("notInArray requires a non-empty array of values"); return new BinaryExpressionImpl("notInArray", [fieldRef(resolveColumn(col).columnName), values]); } /** * Array contains operator: field @> array */ function arrayContains(col, values) { return new BinaryExpressionImpl("arrayContains", [fieldRef(resolveColumn(col).columnName), values]); } /** * Array contained operator: field <@ array */ function arrayContained(col, values) { return new BinaryExpressionImpl("arrayContained", [fieldRef(resolveColumn(col).columnName), values]); } /** * Array overlaps operator: field && array */ function arrayOverlaps(col, values) { return new BinaryExpressionImpl("arrayOverlaps", [fieldRef(resolveColumn(col).columnName), values]); } /** * Null check operator: field IS NULL * Type validation: Only works with nullable fields * * @example * const filter = isNull(cols.deletedAt); */ function isNull(col) { return new UnaryExpressionImpl("isNull", [fieldRef(resolveColumn(col).columnName)]); } /** * Not null check operator: field IS NOT NULL * Type validation: Only works with nullable fields * * @example * const filter = isNotNull(cols.deletedAt); */ function isNotNull(col) { return new UnaryExpressionImpl("isNotNull", [fieldRef(resolveColumn(col).columnName)]); } //#endregion //#region src/orm/unset-token.ts const unsetToken = Symbol.for("kitcn/orm/unsetToken"); function isUnsetToken(value) { return value === unsetToken; } //#endregion //#region src/orm/stream.ts function makeExclusive(boundType) { if (boundType === "gt" || boundType === "gte") return "gt"; return "lt"; } /** Split a range query between two index keys into a series of range queries * that should be executed in sequence. This is necessary because Convex only * supports range queries of the form * q.eq("f1", v).eq("f2", v).lt("f3", v).gt("f3", v). * i.e. all fields must be equal except for the last field, which can have * two inequalities. * * For example, the range from >[1, 2, 3] to <=[1, 3, 2] would be split into * the following queries: * 1. q.eq("f1", 1).eq("f2", 2).gt("f3", 3) * 2. q.eq("f1", 1).gt("f2", 2).lt("f2", 3) * 3. q.eq("f1", 1).eq("f2", 3).lte("f3", 2) */ function splitRange(indexFields, order, startBound, endBound, startBoundType, endBoundType) { const commonPrefix = []; let workingIndexFields = indexFields.slice(); let workingStartBound = startBound.slice(); let workingEndBound = endBound.slice(); let workingStartBoundType = startBoundType; let workingEndBoundType = endBoundType; while (workingStartBound.length > 0 && workingEndBound.length > 0 && compareValues(workingStartBound[0], workingEndBound[0]) === 0) { const indexField = workingIndexFields[0]; workingIndexFields = workingIndexFields.slice(1); const eqBound = workingStartBound[0]; workingStartBound = workingStartBound.slice(1); workingEndBound = workingEndBound.slice(1); commonPrefix.push([ "eq", indexField, eqBound ]); } const makeCompare = (boundType, key) => { const range = commonPrefix.slice(); let i = 0; for (; i < key.length - 1; i++) range.push([ "eq", workingIndexFields[i], key[i] ]); if (i < key.length) range.push([ boundType, workingIndexFields[i], key[i] ]); return range; }; const startRanges = []; while (workingStartBound.length > 1) { startRanges.push(makeCompare(workingStartBoundType, workingStartBound)); workingStartBoundType = makeExclusive(workingStartBoundType); workingStartBound = workingStartBound.slice(0, -1); } const endRanges = []; while (workingEndBound.length > 1) { endRanges.push(makeCompare(workingEndBoundType, workingEndBound)); workingEndBoundType = makeExclusive(workingEndBoundType); workingEndBound = workingEndBound.slice(0, -1); } endRanges.reverse(); let middleRange; if (workingEndBound.length === 0) middleRange = makeCompare(workingStartBoundType, workingStartBound); else if (workingStartBound.length === 0) middleRange = makeCompare(workingEndBoundType, workingEndBound); else { const startValue = workingStartBound[0]; const endValue = workingEndBound[0]; middleRange = commonPrefix.slice(); middleRange.push([ workingStartBoundType, workingIndexFields[0], startValue ]); middleRange.push([ workingEndBoundType, workingIndexFields[0], endValue ]); } const ranges = [ ...startRanges, middleRange, ...endRanges ]; if (order === "desc") ranges.reverse(); return ranges; } function rangeToQuery(range) { return (q) => { let query = q; for (const [boundType, field, value] of range) query = query[boundType](field, value); return query; }; } /** * Get the ordered list of fields for a given table's index based on the schema. * * - For "by_creation_time", returns ["_creationTime", "_id"]. * - For "by_id", returns ["_id"]. * - Otherwise, looks up the named index in the schema and returns its fields * followed by ["_creationTime", "_id"]. * e.g. for an index defined like `.index("abc", ["a", "b"])`, * returns ["a", "b", "_creationTime", "_id"]. */ function getIndexFields(table, index, schema) { const indexDescriptor = String(index ?? "by_creation_time"); if (indexDescriptor === "by_creation_time") return ["_creationTime", "_id"]; if (indexDescriptor === "by_id") return ["_id"]; if (!schema) throw new Error("schema is required to infer index fields"); const indexInfo = schema.tables[table].indexes.find((index) => index.indexDescriptor === indexDescriptor); if (!indexInfo) throw new Error(`Index ${indexDescriptor} not found in table ${table}`); const fields = indexInfo.fields.slice(); fields.push("_creationTime"); fields.push("_id"); return fields; } function getIndexKey(doc, indexFields) { const key = []; for (const field of indexFields) { let obj = doc; for (const subfield of field.split(".")) obj = obj[subfield]; key.push(obj); } return key; } /** * A "stream" is an async iterable of query results, ordered by an index on a table. * * Use it as you would use `ctx.db`. * If using pagination in a reactive query, see the warnings on the `paginator` * function. TL;DR: you need to pass in `endCursor` to prevent holes or overlaps * between pages. * * Once you have a stream, you can use `mergeStreams` or `filterStream` to make * more streams. Then use `queryStream` to convert it into an OrderedQuery, * so you can call `.paginate()`, `.collect()`, etc. */ function stream(db, schema) { return new StreamDatabaseReader(db, schema); } /** * A "QueryStream" is an async iterable of query results, ordered by indexed fields. */ var QueryStream = class { /** * Create a new stream with a TypeScript filter applied. * * This is similar to `db.query(tableName).filter(predicate)`, but it's more * general because it can call arbitrary TypeScript code, including more * database queries. * * All documents filtered out are still considered "read" from the database; * they are just excluded from the output stream. * * In contrast to `filter` from legacy helper filtering, this filterWith * is applied *before* any pagination. That means if the filter excludes a lot * of documents, the `.paginate()` method will read a lot of documents until * it gets as many documents as it wants. If you run into issues with reading * too much data, you can pass `maxScan` to `paginate()`. */ filterWith(predicate) { const order = this.getOrder(); return new FlatMapStream(this, async (doc) => { return new SingletonStream(await predicate(doc) ? doc : null, order, [], [], []); }, []); } /** * Create a new stream where each element is the result of applying the mapper * function to the elements of the original stream. * * Similar to how [1, 2, 3].map(x => x * 2) => [2, 4, 6] */ map(mapper) { const order = this.getOrder(); return new FlatMapStream(this, async (doc) => { return new SingletonStream(await mapper(doc), order, [], [], []); }, []); } /** * Similar to flatMap on an array, but iterate over a stream, and the for each * element, iterate over the stream created by the mapper function. * * Ordered by the original stream order, then the mapped stream. Similar to * how ["a", "b"].flatMap(letter => [letter, letter]) => ["a", "a", "b", "b"] * * @param mapper A function that takes a document and returns a new stream. * @param mappedIndexFields The index fields of the streams created by mapper. * @returns A stream of documents returned by the mapper streams, * grouped by the documents in the original stream. */ flatMap(mapper, mappedIndexFields) { normalizeIndexFields(mappedIndexFields); return new FlatMapStream(this, mapper, mappedIndexFields); } /** * Get the first item from the original stream for each distinct value of the * selected index fields. * * e.g. if the stream has an equality filter on `a`, and index fields `[a, b, c]`, * we can do `stream.distinct(["b"])` to get a stream of the first item for * each distinct value of `b`. * Similarly, you could do `stream.distinct(["a", "b"])` with the same result, * or `stream.distinct(["a", "b", "c"])` to get the original stream. * * This stream efficiently skips past items with the same value for the selected * distinct index fields. * * This can be used to perform a loose index scan. */ distinct(distinctIndexFields) { return new DistinctStream(this, distinctIndexFields); } filter(_predicate) { throw new Error("Cannot call .filter() directly on a query stream. Use .filterWith() for filtering or .collect() if you want to convert the stream to an array first."); } async paginate(opts) { if (opts.limit === 0) { if (opts.cursor === null) throw new Error(".paginate called with cursor of null and 0 for limit. This is not supported, as null is not a valid continueCursor. Advice: avoid calling paginate entirely in these cases."); return { page: [], isDone: false, continueCursor: opts.cursor }; } const order = this.getOrder(); let newStartKey = { key: [], inclusive: true }; if (opts.cursor !== null) newStartKey = { key: deserializeCursor(opts.cursor), inclusive: false }; let newEndKey = { key: [], inclusive: true }; const maxRowsToRead = opts.maxScan; const softMaxRowsToRead = opts.limit + 1; let maxRows = opts.limit; if (opts.endCursor) { newEndKey = { key: deserializeCursor(opts.endCursor), inclusive: true }; maxRows = void 0; } const newLowerBound = order === "asc" ? newStartKey : newEndKey; const newUpperBound = order === "asc" ? newEndKey : newStartKey; const narrowStream = this.narrow({ lowerBound: newLowerBound.key, lowerBoundInclusive: newLowerBound.inclusive, upperBound: newUpperBound.key, upperBoundInclusive: newUpperBound.inclusive }); const page = []; const indexKeys = []; let hasMore = opts.endCursor && opts.endCursor !== "[]"; let continueCursor = opts.endCursor ?? "[]"; for await (const [doc, indexKey] of narrowStream.iterWithKeys()) { if (doc !== null) page.push(doc); indexKeys.push(indexKey); if (maxRows !== void 0 && page.length >= maxRows || maxRowsToRead !== void 0 && indexKeys.length >= maxRowsToRead) { hasMore = true; continueCursor = serializeCursor(indexKey); break; } } let pageStatus; let splitCursor; if (indexKeys.length === maxRowsToRead) { pageStatus = "SplitRequired"; splitCursor = indexKeys[Math.floor((indexKeys.length - 1) / 2)]; } else if (indexKeys.length >= softMaxRowsToRead) { pageStatus = "SplitRecommended"; splitCursor = indexKeys[Math.floor((indexKeys.length - 1) / 2)]; } return { page, isDone: !hasMore, continueCursor, pageStatus, splitCursor: splitCursor ? serializeCursor(splitCursor) : void 0 }; } async collect() { return await this.take(Number.POSITIVE_INFINITY); } async take(n) { const results = []; for await (const [doc, _] of this.iterWithKeys()) { if (doc === null) continue; results.push(doc); if (results.length === n) break; } return results; } async unique() { const docs = await this.take(2); if (docs.length === 2) throw new Error("Query is not unique"); return docs[0] ?? null; } async uniqueOrThrow() { const doc = await this.unique(); if (doc === null) throw new Error("Query returned no results"); return doc; } async first() { return (await this.take(1))[0] ?? null; } async firstOrThrow() { const doc = await this.first(); if (doc === null) throw new Error("Query returned no results"); return doc; } [Symbol.asyncIterator]() { const iterator = this.iterWithKeys()[Symbol.asyncIterator](); return { async next() { const result = await iterator.next(); if (result.done) return { done: true, value: void 0 }; return { done: false, value: result.value[0] }; } }; } }; var StreamDatabaseReader = class { system; db; schema; constructor(db, schema) { this.db = db; this.schema = schema; this.system = db.system; } query(tableName) { return new StreamQueryInitializer(this, tableName); } get(_id) { throw new Error("get() not supported for `paginator`"); } normalizeId(_tableName, _id) { throw new Error("normalizeId() not supported for `paginator`."); } }; var StreamableQuery = class extends QueryStream {}; var StreamQueryInitializer = class extends StreamableQuery { parent; table; constructor(parent, table) { super(); this.parent = parent; this.table = table; } fullTableScan() { return this.withIndex("by_creation_time"); } withIndex(indexName, indexRange) { const q = new ReflectIndexRange(getIndexFields(this.table, indexName, this.parent.schema)); if (indexRange) indexRange(q); return new StreamQuery(this, indexName, q, indexRange); } withSearchIndex(_indexName, _searchFilter) { throw new Error("Cannot paginate withSearchIndex"); } inner() { return this.fullTableScan(); } order(order) { return this.inner().order(order); } reflect() { return this.inner().reflect(); } iterWithKeys() { return this.inner().iterWithKeys(); } getOrder() { return this.inner().getOrder(); } getEqualityIndexFilter() { return this.inner().getEqualityIndexFilter(); } getIndexFields() { return this.inner().getIndexFields(); } narrow(indexBounds) { return this.inner().narrow(indexBounds); } }; var StreamQuery = class extends StreamableQuery { parent; index; q; indexRange; constructor(parent, index, q, indexRange) { super(); this.parent = parent; this.index = index; this.q = q; this.indexRange = indexRange; } order(order) { return new OrderedStreamQuery(this, order); } inner() { return this.order("asc"); } reflect() { return this.inner().reflect(); } iterWithKeys() { return this.inner().iterWithKeys(); } getOrder() { return this.inner().getOrder(); } getEqualityIndexFilter() { return this.inner().getEqualityIndexFilter(); } getIndexFields() { return this.inner().getIndexFields(); } narrow(indexBounds) { return this.inner().narrow(indexBounds); } }; var OrderedStreamQuery = class extends StreamableQuery { parent; order; constructor(parent, order) { super(); this.parent = parent; this.order = order; } reflect() { return { db: this.parent.parent.parent.db, schema: this.parent.parent.parent.schema, table: this.parent.parent.table, index: this.parent.index, indexFields: this.parent.q.indexFields, order: this.order, bounds: { lowerBound: this.parent.q.lowerBoundIndexKey ?? [], lowerBoundInclusive: this.parent.q.lowerBoundInclusive, upperBound: this.parent.q.upperBoundIndexKey ?? [], upperBoundInclusive: this.parent.q.upperBoundInclusive }, indexRange: this.parent.indexRange }; } /** * inner() is as if you had used ctx.db to construct the query. */ inner() { const { db, table, index, order, indexRange } = this.reflect(); return db.query(table).withIndex(index, indexRange).order(order); } iterWithKeys() { const { indexFields } = this.reflect(); const iterable = this.inner(); return { [Symbol.asyncIterator]() { const iterator = iterable[Symbol.asyncIterator](); return { async next() { const result = await iterator.next(); if (result.done) return { done: true, value: void 0 }; return { done: false, value: [result.value, getIndexKey(result.value, indexFields)] }; } }; } }; } getOrder() { return this.order; } getEqualityIndexFilter() { return this.parent.q.equalityIndexFilter; } getIndexFields() { return this.parent.q.indexFields; } narrow(indexBounds) { const { db, table, index, order, bounds, schema } = this.reflect(); let maxLowerBound = bounds.lowerBound; let maxLowerBoundInclusive = bounds.lowerBoundInclusive; if (compareKeys({ value: indexBounds.lowerBound, kind: indexBounds.lowerBoundInclusive ? "predecessor" : "successor" }, { value: bounds.lowerBound, kind: bounds.lowerBoundInclusive ? "predecessor" : "successor" }) > 0) { maxLowerBound = indexBounds.lowerBound; maxLowerBoundInclusive = indexBounds.lowerBoundInclusive; } let minUpperBound = bounds.upperBound; let minUpperBoundInclusive = bounds.upperBoundInclusive; if (compareKeys({ value: indexBounds.upperBound, kind: indexBounds.upperBoundInclusive ? "successor" : "predecessor" }, { value: bounds.upperBound, kind: bounds.upperBoundInclusive ? "successor" : "predecessor" }) < 0) { minUpperBound = indexBounds.upperBound; minUpperBoundInclusive = indexBounds.upperBoundInclusive; } return streamIndexRange(db, schema, table, index, { lowerBound: maxLowerBound, lowerBoundInclusive: maxLowerBoundInclusive, upperBound: minUpperBound, upperBoundInclusive: minUpperBoundInclusive }, order); } }; /** * Create a stream of documents using the given index and bounds. */ function streamIndexRange(db, schema, table, index, bounds, order) { return new ConcatStreams(...splitRange(getIndexFields(table, index, schema), order, bounds.lowerBound, bounds.upperBound, bounds.lowerBoundInclusive ? "gte" : "gt", bounds.upperBoundInclusive ? "lte" : "lt").map((splitBound) => stream(db, schema).query(table).withIndex(index, rangeToQuery(splitBound)).order(order))); } var ReflectIndexRange = class { #hasSuffix = false; lowerBoundIndexKey = void 0; lowerBoundInclusive = true; upperBoundIndexKey = void 0; upperBoundInclusive = true; equalityIndexFilter = []; indexFields; constructor(indexFields) { this.indexFields = indexFields; } eq(field, value) { if (!this.#canLowerBound(field) || !this.#canUpperBound(field)) throw new Error(`Cannot use eq on field '${field}'`); this.lowerBoundIndexKey = this.lowerBoundIndexKey ?? []; this.lowerBoundIndexKey.push(value); this.upperBoundIndexKey = this.upperBoundIndexKey ?? []; this.upperBoundIndexKey.push(value); this.equalityIndexFilter.push(value); return this; } lt(field, value) { if (!this.#canUpperBound(field)) throw new Error(`Cannot use lt on field '${field}'`); this.upperBoundIndexKey = this.upperBoundIndexKey ?? []; this.upperBoundIndexKey.push(value); this.upperBoundInclusive = false; this.#hasSuffix = true; return this; } lte(field, value) { if (!this.#canUpperBound(field)) throw new Error(`Cannot use lte on field '${field}'`); this.upperBoundIndexKey = this.upperBoundIndexKey ?? []; this.upperBoundIndexKey.push(value); this.#hasSuffix = true; return this; } gt(field, value) { if (!this.#canLowerBound(field)) throw new Error(`Cannot use gt on field '${field}'`); this.lowerBoundIndexKey = this.lowerBoundIndexKey ?? []; this.lowerBoundIndexKey.push(value); this.lowerBoundInclusive = false; this.#hasSuffix = true; return this; } gte(field, value) { if (!this.#canLowerBound(field)) throw new Error(`Cannot use gte on field '${field}'`); this.lowerBoundIndexKey = this.lowerBoundIndexKey ?? []; this.lowerBoundIndexKey.push(value); this.#hasSuffix = true; return this; } #canLowerBound(field) { const currentLowerBoundLength = this.lowerBoundIndexKey?.length ?? 0; const currentUpperBoundLength = this.upperBoundIndexKey?.length ?? 0; if (currentLowerBoundLength > currentUpperBoundLength) return false; if (currentLowerBoundLength === currentUpperBoundLength && this.#hasSuffix) return false; return currentLowerBoundLength < this.indexFields.length && this.indexFields[currentLowerBoundLength] === field; } #canUpperBound(field) { const currentLowerBoundLength = this.lowerBoundIndexKey?.length ?? 0; const currentUpperBoundLength = this.upperBoundIndexKey?.length ?? 0; if (currentUpperBoundLength > currentLowerBoundLength) return false; if (currentLowerBoundLength === currentUpperBoundLength && this.#hasSuffix) return false; return currentUpperBoundLength < this.indexFields.length && this.indexFields[currentUpperBoundLength] === field; } }; /** * Merge multiple streams, provided in any order, into a single stream. * * The streams will be merged into a stream of documents ordered by the index keys, * i.e. by "author" (then by the implicit "_creationTime"). * * e.g. ```ts * mergedStream([ * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user3")), * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user1")), * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user2")), * ], ["author"]) * ``` * * returns a stream of messages for user1, then user2, then user3. * * You can also use `orderByIndexFields` to change the indexed fields before merging, which changes the order of the merged stream. * This only works if the streams are already ordered by `orderByIndexFields`, * which happens if each does a .eq(field, value) on all index fields before `orderByIndexFields`. * * e.g. if the "by_author" index is defined as being ordered by ["author", "_creationTime"], * and each query does an equality lookup on "author", each individual query before merging is in fact ordered by "_creationTime". * * e.g. ```ts * mergedStream([ * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user3")), * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user1")), * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user2")), * ], ["_creationTime"]) * ``` * * This returns a stream of messages from all three users, sorted by creation time. */ function mergedStream(streams, orderByIndexFields) { return new MergedStream(streams, orderByIndexFields); } var MergedStream = class MergedStream extends QueryStream { #order; #streams; #equalityIndexFilter; #indexFields; constructor(streams, orderByIndexFields) { super(); if (streams.length === 0) throw new Error("Cannot union empty array of streams"); this.#order = allSame(streams.map((stream) => stream.getOrder()), "Cannot merge streams with different orders"); this.#streams = streams.map((stream) => new OrderByStream(stream, orderByIndexFields)); this.#indexFields = allSame(this.#streams.map((stream) => stream.getIndexFields()), "Cannot merge streams with different index fields. Consider using .orderBy()"); this.#equalityIndexFilter = commonPrefix(this.#streams.map((stream) => stream.getEqualityIndexFilter())); } iterWithKeys() { const iterables = this.#streams.map((stream) => stream.iterWithKeys()); const comparisonInversion = this.#order === "asc" ? 1 : -1; return { [Symbol.asyncIterator]() { const iterators = iterables.map((iterable) => iterable[Symbol.asyncIterator]()); const results = Array.from({ length: iterators.length }, () => ({ done: false, value: void 0 })); return { async next() { await Promise.all(iterators.map(async (iterator, i) => { if (!results[i].done && !results[i].value) results[i] = await iterator.next(); })); let minIndexKeyAndIndex; for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.done || !result.value) continue; const [_, resultIndexKey] = result.value; if (minIndexKeyAndIndex === void 0) { minIndexKeyAndIndex = [resultIndexKey, i]; continue; } const [prevMin, _prevMinIndex] = minIndexKeyAndIndex; if (compareKeys({ value: resultIndexKey, kind: "exact" }, { value: prevMin, kind: "exact" }) * comparisonInversion < 0) minIndexKeyAndIndex = [resultIndexKey, i]; } if (minIndexKeyAndIndex === void 0) return { done: true, value: void 0 }; const [_, minIndex] = minIndexKeyAndIndex; const result = results[minIndex].value; results[minIndex].value = void 0; return { done: false, value: result }; } }; } }; } getOrder() { return this.#order; } getEqualityIndexFilter() { return this.#equalityIndexFilter; } getIndexFields() { return this.#indexFields; } narrow(indexBounds) { return new MergedStream(this.#streams.map((stream) => stream.narrow(indexBounds)), this.#indexFields); } }; function allSame(values, errorMessage) { const first = values[0]; for (const value of values) if (compareValues(value, first)) throw new Error(errorMessage); return first; } function commonPrefix(values) { let commonPrefix = values[0]; for (const value of values) for (let i = 0; i < commonPrefix.length; i++) if (i >= value.length || compareValues(commonPrefix[i], value[i])) { commonPrefix = commonPrefix.slice(0, i); break; } return commonPrefix; } /** * Concatenate multiple streams into a single stream. * This assumes that the streams correspond to disjoint index ranges, * and are provided in the same order as the index ranges. * * e.g. ```ts * new ConcatStreams( * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user1")), * stream(db, schema).query("messages").withIndex("by_author", q => q.eq("author", "user2")), * ) * ``` * * is valid, but if the stream arguments were reversed, or the queries were * `.order("desc")`, it would be invalid. * * It's not recommended to use `ConcatStreams` directly, since it has the same * behavior as `MergedStream`, but with fewer runtime checks. */ var ConcatStreams = class ConcatStreams extends QueryStream { #order; #streams; #equalityIndexFilter; #indexFields; constructor(...streams) { super(); this.#streams = streams; if (streams.length === 0) throw new Error("Cannot concat empty array of streams"); this.#order = allSame(streams.map((stream) => stream.getOrder()), "Cannot concat streams with different orders. Consider using .orderBy()"); this.#indexFields = allSame(streams.map((stream) => stream.getIndexFields()), "Cannot concat streams with different index fields. Consider using .orderBy()"); this.#equalityIndexFilter = commonPrefix(streams.map((stream) => stream.getEqualityIndexFilter())); } iterWithKeys() { const iterables = this.#streams.map((stream) => stream.iterWithKeys()); const comparisonInversion = this.#order === "asc" ? 1 : -1; let previousIndexKey; return { [Symbol.asyncIterator]() { const iterators = iterables.map((iterable) => iterable[Symbol.asyncIterator]()); return { async next() { while (iterators.length > 0) { const result = await iterators[0].next(); if (result.done) iterators.shift(); else { const [_, indexKey] = result.value; if (previousIndexKey !== void 0 && compareKeys({ value: previousIndexKey, kind: "exact" }, { value: indexKey, kind: "exact" }) * comparisonInversion > 0) throw new Error(`ConcatStreams in wrong order: ${JSON.stringify(previousIndexKey)}, ${JSON.stringify(indexKey)}`); previousIndexKey = indexKey; return result; } } return { done: true, value: void 0 }; } }; } }; } getOrder() { return this.#order; } getEqualityIndexFilter() { return this.#equalityIndexFilter; } getIndexFields() { return this.#indexFields; } narrow(indexBounds) { return new ConcatStreams(...this.#streams.map((stream) => stream.narrow(indexBounds))); } }; var FlatMapStreamIterator = class { #outerStream; #outerIterator; #currentOuterItem = null; #mapper; #mappedIndexFields; constructor(outerStream, mapper, mappedIndexFields) { this.#outerIterator = outerStream.iterWithKeys()[Symbol.asyncIterator](); this.#outerStream = outerStream; this.#mapper = mapper; this.#mappedIndexFields = mappedIndexFields; } singletonSkipInnerStream() { const indexKey = this.#mappedIndexFields.map(() => null); return new SingletonStream(null, this.#outerStream.getOrder(), this.#mappedIndexFields, indexKey, indexKey); } async setCurrentOuterItem(item) { const [t, indexKey] = item; let innerStream; if (t === null) innerStream = this.singletonSkipInnerStream(); else { innerStream = await this.#mapper(t); if (!equalIndexFields(innerStream.getIndexFields(), this.#mappedIndexFields)) throw new Error(`FlatMapStream: inner stream has different index fields than expected: ${JSON.stringify(innerStream.getIndexFields())} vs ${JSON.stringify(this.#mappedIndexFields)}`); if (innerStream.getOrder() !== this.#outerStream.getOrder()) throw new Error(`FlatMapStream: inner stream has different order than outer stream: ${innerStream.getOrder()} vs ${this.#outerStream.getOrder()}`); } this.#currentOuterItem = { t, indexKey, innerIterator: innerStream.iterWithKeys()[Symbol.asyncIterator](), count: 0 }; } async next() { if (this.#currentOuterItem === null) { const result = await this.#outerIterator.next(); if (result.done) return { done: true, value: void 0 }; await this.setCurrentOuterItem(result.value); return await this.next(); } const result = await this.#currentOuterItem.innerIterator.next(); if (result.done) { if (this.#currentOuterItem.count > 0) this.#currentOuterItem = null; else this.#currentOuterItem.innerIterator = this.singletonSkipInnerStream().iterWithKeys()[Symbol.asyncIterator](); return await this.next(); } const [u, indexKey] = result.value; this.#currentOuterItem.count++; return { done: false, value: [u, [...this.#currentOuterItem.indexKey, ...indexKey]] }; } }; var FlatMapStream = class FlatMapStream extends QueryStream { #stream; #mapper; #mappedIndexFields; constructor(stream, mapper, mappedIndexFields) { super(); this.#stream = stream; this.#mapper = mapper; this.#mappedIndexFields = mappedIndexFields; } iterWithKeys() { const outerStream = this.#stream; const mapper = this.#mapper; const mappedIndexFields = this.#mappedIndexFields; return { [Symbol.asyncIterator]() { return new FlatMapStreamIterator(outerStream, mapper, mappedIndexFields); } }; } getOrder() { return this.#stream.getOrder(); } getEqualityIndexFilter() { return this.#stream.getEqualityIndexFilter(); } getIndexFields() { return [...this.#stream.getIndexFields(), ...this.#mappedIndexFields]; } narrow(indexBounds) { const outerLength = this.#stream.getIndexFields().length; const outerLowerBound = indexBounds.lowerBound.slice(0, outerLength); const outerUpperBound = indexBounds.upperBound.slice(0, outerLength); const innerLowerBound = indexBounds.lowerBound.slice(outerLength); const innerUpperBound = indexBounds.upperBound.slice(outerLength); const outerIndexBounds = { lowerBound: outerLowerBound, lowerBoundInclusive: innerLowerBound.length === 0 ? indexBounds.lowerBoundInclusive : true, upperBound: outerUpperBound, upperBoundInclusive: innerUpperBound.length === 0 ? indexBounds.upperBoundInclusive : true }; const innerIndexBounds = { lowerBound: innerLowerBound, lowerBoundInclusive: innerLowerBound.length === 0 ? true : indexBounds.lowerBoundInclusive, upperBound: innerUpperBound, upperBoundInclusive: innerUpperBound.length === 0 ? true : indexBounds.upperBoundInclusive }; return new FlatMapStream(this.#stream.narrow(outerIndexBounds), async (t) => { return (await this.#mapper(t)).narrow(innerIndexBounds); }, this.#mappedIndexFields); } }; var SingletonStream = class SingletonStream extends QueryStream { #value; #order; #indexFields; #indexKey; #equalityIndexFilter; constructor(value, order, indexFields, indexKey, equalityIndexFilter) { super(); this.#value = value; this.#order = order; this.#indexFields = indexFields; this.#indexKey = indexKey; this.#equalityIndexFilter = equalityIndexFilter; if (indexKey.length !== indexFields.length) throw new Error(`indexKey must have the same length as indexFields: ${JSON.stringify(indexKey)} vs ${JSON.stringify(indexFields)}`); } iterWithKeys() { const value = this.#value; const indexKey = this.#indexKey; return { [Symbol.asyncIterator]() { let sent = false; return { async next() { if (sent) return { done: true, value: void 0 }; sent = true; return { done: false, value: [value, indexKey] }; } }; } }; } getOrder() { return this.#order; } getIndexFields() { return this.#indexFields; } getEqualityIndexFilter() { return this.#equalityIndexFilter; } narrow(indexBounds) { const compareLowerBound = compareKeys({ value: indexBounds.lowerBound, kind: indexBounds.lowerBoundInclusive ? "exact" : "successor" }, { value: this.#indexKey, kind: "exact" }); const compareUpperBound = compareKeys({ value: this.#indexKey, kind: "exact" }, { value: indexBounds.upperBound, kind: indexBounds.upperBoundInclusive ? "exact" : "predecessor" }); if (compareLowerBound <= 0 && compareUpperBound <= 0) return new SingletonStream(this.#value, this.#order, this.#indexFields, this.#indexKey, this.#equalityIndexFilter); return new EmptyStream(this.#order, this.#indexFields); } }; /** * This is a completely empty stream that yields no values, and in particular * does not count towards maxScan. * Compare to SingletonStream(null, ...), which yields no values but does count * towards maxScan. */ var EmptyStream = class extends QueryStream { #order; #indexFields; constructor(order, indexFields) { super(); this.#order = order; this.#indexFields = indexFields; } iterWithKeys() { return { [Symbol.asyncIterator]() { return { async next() { return { done: true, value: void 0 }; } }; } }; } getOrder() { return this.#order; } getIndexFields() { return this.#indexFields; } getEqualityIndexFilter() { return []; } narrow(_indexBounds) { return this; } }; function normalizeIndexFields(indexFields) { if (!indexFields.includes("_creationTime") && (indexFields.length !== 1 || indexFields[0] !== "_id")) indexFields.push("_creationTime"); if (!indexFields.includes("_id")) indexFields.push("_id"); } function* getOrderingIndexFields(stream) { const streamEqualityIndexLength = stream.getEqualityIndexFilter().length; const streamIndexFields = stream.getIndexFields(); for (let i = 0; i <= streamEqualityIndexLength; i++) yield streamIndexFields.slice(i); } var OrderByStream = class OrderByStream extends QueryStream { #staticFilter; #stream; #indexFields; constructor(stream, indexFields) { super(); this.#stream = stream; this.#indexFields = indexFields; normalizeIndexFields(this.#indexFields); const streamIndexFields = stream.getIndexFields(); if (!Array.from(getOrderingIndexFields(stream)).some((orderingIndexFields) => equalIndexFields(orderingIndexFields, indexFields))) throw new Error(`indexFields must be some sequence of fields the stream is ordered by: ${JSON.stringify(indexFields)}, ${JSON.stringify(streamIndexFields)} (${stream.getEqualityIndexFilter().length} equality fields)`); this.#staticFilter = stream.getEqualityIndexFilter().slice(0, streamIndexFields.length - indexFields.length); } getOrder() { return this.#stream.getOrder(); } getEqualityIndexFilter() { return this.#stream.getEqualityIndexFilter().slice(this.#staticFilter.length); } getIndexFields() { return this.#indexFields; } iterWithKeys() { const iterable = this.#stream.iterWithKeys(); const staticFilter = this.#staticFilter; return { [Symbol.asyncIterator]() { const iterator = iterable[Symbol.asyncIterator](); return { async next() { const result = await iterator.next(); if (result.done) return result; const [doc, indexKey] = result.value; return { done: false, value: [doc, indexKey.slice(staticFilter.length)] }; } }; } }; } narrow(indexBounds) { return new OrderByStream(this.#stream.narrow({ lowerBound: [...this.#staticFilter, ...indexBounds.lowerBound], lowerBoundInclusive: indexBounds.lowerBoundInclusive, upperBound: [...this.#staticFilter, ...indexBounds.upperBound], upperBoundInclusive: indexBounds.upperBoundInclusive }), this.#indexFields); } }; var DistinctStream = class DistinctStream extends QueryStream { #distinctIndexFieldsLength; #stream; #distinctIndexFields; constructor(stream, distinctIndexFields) { super(); this.#stream = stream; this.#distinctIndexFields = distinctIndexFields; let distinctIndexFieldsLength; for (const orderingIndexFields of getOrderingIndexFields(stream)) if (equalIndexFields(orderingIndexFields.slice(0, distinctIndexFields.length), distinctIndexFields)) { distinctIndexFieldsLength = stream.getIndexFields().length - orderingIndexFields.length + distinctIndexFields.length; break; } if (distinctIndexFieldsLength === void 0) throw new Error(`distinctIndexFields must be a prefix of the stream's ordering index fields: ${JSON.stringify(distinctIndexFields)}, ${JSON.stringify(stream.getIndexFields())} (${stream.getEqualityIndexFilter().length} equality fields)`); this.#distinctIndexFieldsLength = distinctIndexFieldsLength; } iterWithKeys() { const stream = this.#stream; const distinctIndexFieldsLength = this.#distinctIndexFieldsLength; return { [Symbol.asyncIterator]() { let currentStream = stream; let currentIterator = currentStream.iterWithKeys()[Symbol.asyncIterator](); return { async next() { const result = await currentIterator.next(); if (result.done) return { done: true, value: void 0 }; const [doc, indexKey] = result.value; if (doc === null) return { done: false, value: [null, indexKey] }; const distinctIndexKey = indexKey.slice(0, distinctIndexFieldsLength); if (stream.getOrder() === "asc") currentStream = currentStream.narrow({ lowerBound: distinctIndexKey, lowerBoundInclusive: false, upperBound: [], upperBoundInclusive: true }); else currentStream = currentStream.narrow({ lowerBound: [], lowerBoundInclusive: true, upperBound: distinctIndexKey, upperBoundInclusive: false }); currentIterator = currentStream.iterWithKeys()[Symbol.asyncIterator](); return result; } }; } }; } narrow(indexBounds) { const indexBoundsPrefix = { ...indexBounds, lowerBound: indexBounds.lowerBound.slice(0, this.#distinctIndexFieldsLength), upperBound: indexBounds.upperBound.slice(0, this.#distinctIndexFieldsLength) }; return new DistinctStream(this.#stream.narrow(indexBoundsPrefix), this.#distinctIndexFields); } getOrder() { return this.#stream.getOrder(); } getIndexFields() { return this.#stream.getIndexFields(); } getEqualityIndexFilter() { return this.#stream.getEqualityIndexFilter(); } }; function equalIndexFields(indexFields1, indexFields2) { if (indexFields1.length !== indexFields2.length) return false; for (let i = 0; i < indexFields1.length; i++) if (indexFields1[i] !== indexFields2[i]) return false; return true; } function getValueAtIndex(v, index) { if (index >= v.length) return; return { kind: "found", value: v[index] }; } function compareDanglingSuffix(shorterKeyKind, longerKeyKind, shorterKey, longerKey) { if (shorterKeyKind === "exact" && longerKeyKind === "exact") throw new Error(`Exact keys are not the same length: ${JSON.stringify(shorterKey.value)}, ${JSON.stringify(longerKey.value)}`); if (shorterKeyKind === "exact") throw new Error(`Exact key is shorter than prefix: ${JSON.stringify(shorterKey.value)}, ${JSON.stringify(longerKey.value)}`); if (shorterKeyKind === "predecessor" && longerKeyKind === "successor") return -1; if (shorterKeyKind === "successor" && longerKeyKind === "predecessor") return 1; if (shorterKeyKind === "predecessor" && longerKeyKind === "predecessor") return -1; if (shorterKeyKind === "successor" && longerKeyKind === "successor") return 1; if (shorterKeyKind === "predecessor" && longerKeyKind === "exact") return -1; if (shorterKeyKind === "successor" && longerKeyKind === "exact") return 1; throw new Error(`Unexpected key kinds: ${shorterKeyKind}, ${longerKeyKind}`); } function compareKeys(key1, key2) { let i = 0; while (i < Math.max(key1.value.length, key2.value.length)) { const v1 = getValueAtIndex(key1.value, i); const v2 = getValueAtIndex(key2.value, i); if (v1 === void 0) return compareDanglingSuffix(key1.kind, key2.kind, key1, key2); if (v2 === void 0) return -1 * compareDanglingSuffix(key2.kind, key1.kind, key2, key1); const result = compareValues(v1.value, v2.value); if (result !== 0) return result; i++; } if (key1.kind === key2.kind) return 0; if (key1.kind === "exact") { if (key2.kind === "successor") return -1; return 1; } if (key1.kind === "predecessor") return -1; if (key1.kind === "successor") return 1; throw new Error(`Unexpected key kind: ${key1.kind}`); } function serializeCursor(key) { return JSON.stringify(convexToJson(key.map((v) => v === void 0 ? "undefined" : typeof v === "string" && v.endsWith("undefined") ? `_${v}` : v))); } function deserializeCursor(cursor) { let parsed; try { parsed = JSON.parse(cursor); } catch { throw new Error("Invalid pagination cursor for stream-backed pagination. Use the continueCursor returned by the same findMany query shape."); } return jsonToConvex(parsed).map((v) => { if (typeof v === "string") { if (v === "undefined") return; if (v.endsWith("undefined")) return v.slice(1); } return v; }); } //#endregion //#region src/orm/query-context.ts async function getByIdWithOrmQueryFallback(ctx, tableName, id) { const ormTableQuery = ctx.orm?.query?.[tableName]; if (ormTableQuery?.findFirst) return await ormTableQuery.findFirst({ where: { id } }); return await ctx.db.get(id); } //#endregion export { ne as A, inArray as C, like as D, isNull as E, notLike as F, or as I, startsWith as L, notBetween as M, notIlike a