kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
1,518 lines (1,513 loc) • 50.5 kB
JavaScript
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