@tanstack/optimistic
Version:
Core optimistic updates library
308 lines (307 loc) • 9.53 kB
JavaScript
class BaseQueryBuilder {
/**
* Create a new QueryBuilder instance.
*/
constructor(query = {}) {
this.query = {};
this.query = query;
}
/**
* Specify the collection to query from.
* This is the first method that must be called in the chain.
*
* @param collection The collection name to query from
* @param as Optional alias for the collection
* @returns A new QueryBuilder with the from clause set
*/
from(collection, as) {
if (typeof collection === `object` && collection !== null) {
return this.fromCollectionRef(collection);
} else if (typeof collection === `string`) {
return this.fromInputReference(
collection,
as
);
} else {
throw new Error(`Invalid collection type`);
}
}
fromCollectionRef(collectionRef) {
var _a;
const keys = Object.keys(collectionRef);
if (keys.length !== 1) {
throw new Error(`Expected exactly one key`);
}
const key = keys[0];
const collection = collectionRef[key];
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.from = key;
(_a = newBuilder.query).collections ?? (_a.collections = {});
newBuilder.query.collections[key] = collection;
return newBuilder;
}
fromInputReference(collection, as) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.from = collection;
if (as) {
newBuilder.query.as = as;
}
return newBuilder;
}
/**
* Specify what columns to select.
* Overwrites any previous select clause.
*
* @param selects The columns to select
* @returns A new QueryBuilder with the select clause set
*/
select(...selects) {
const validatedSelects = selects.map((select) => {
if (typeof select === `object` && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
select !== null && !Array.isArray(select)) {
const result = {};
for (const [key, value] of Object.entries(select)) {
if (typeof value === `object` && value !== null && !Array.isArray(value)) {
const keys = Object.keys(value);
if (keys.length === 1) {
const funcName = keys[0];
const allowedFunctions = [
`SUM`,
`COUNT`,
`AVG`,
`MIN`,
`MAX`,
`DATE`,
`JSON_EXTRACT`,
`JSON_EXTRACT_PATH`,
`UPPER`,
`LOWER`,
`COALESCE`,
`CONCAT`,
`LENGTH`,
`ORDER_INDEX`
];
if (!allowedFunctions.includes(funcName)) {
console.warn(
`Unsupported function: ${funcName}. Expected one of: ${allowedFunctions.join(`, `)}`
);
}
}
}
result[key] = value;
}
return result;
}
return select;
});
const newBuilder = new BaseQueryBuilder(
this.query
);
newBuilder.query.select = validatedSelects;
return newBuilder;
}
/**
* Add a where clause to filter the results.
* Can be called multiple times to add AND conditions.
*
* @param leftOrCondition The left operand or complete condition
* @param operator Optional comparison operator
* @param right Optional right operand
* @returns A new QueryBuilder with the where clause added
*/
where(leftOrCondition, operator, right) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
let condition;
if (operator !== void 0 && right !== void 0) {
condition = [leftOrCondition, operator, right];
} else {
condition = leftOrCondition;
}
if (!newBuilder.query.where) {
newBuilder.query.where = condition;
} else {
const andArray = [newBuilder.query.where, `and`, condition];
newBuilder.query.where = andArray;
}
return newBuilder;
}
/**
* Add a having clause to filter the grouped results.
* Can be called multiple times to add AND conditions.
*
* @param leftOrCondition The left operand or complete condition
* @param operator Optional comparison operator
* @param right Optional right operand
* @returns A new QueryBuilder with the having clause added
*/
having(leftOrCondition, operator, right) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
let condition;
if (operator !== void 0 && right !== void 0) {
condition = [leftOrCondition, operator, right];
} else {
condition = leftOrCondition;
}
if (!newBuilder.query.having) {
newBuilder.query.having = condition;
} else {
const andArray = [newBuilder.query.having, `and`, condition];
newBuilder.query.having = andArray;
}
return newBuilder;
}
join(joinClause) {
if (typeof joinClause.from === `object` && joinClause.from !== null) {
return this.joinCollectionRef(
joinClause
);
} else {
return this.joinInputReference(
joinClause
);
}
}
joinCollectionRef(joinClause) {
var _a;
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
const keys = Object.keys(joinClause.from);
if (keys.length !== 1) {
throw new Error(`Expected exactly one key in CollectionRef`);
}
const key = keys[0];
const collection = joinClause.from[key];
if (!collection) {
throw new Error(`Collection not found for key: ${key}`);
}
const joinClauseCopy = {
type: joinClause.type,
from: key,
on: joinClause.on,
where: joinClause.where
};
if (!newBuilder.query.join) {
newBuilder.query.join = [joinClauseCopy];
} else {
newBuilder.query.join = [...newBuilder.query.join, joinClauseCopy];
}
(_a = newBuilder.query).collections ?? (_a.collections = {});
newBuilder.query.collections[key] = collection;
return newBuilder;
}
joinInputReference(joinClause) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
const joinClauseCopy = { ...joinClause };
if (!newBuilder.query.join) {
newBuilder.query.join = [joinClauseCopy];
} else {
newBuilder.query.join = [...newBuilder.query.join, joinClauseCopy];
}
joinClause.as ?? joinClause.from;
return newBuilder;
}
/**
* Add an orderBy clause to sort the results.
* Overwrites any previous orderBy clause.
*
* @param orderBy The order specification
* @returns A new QueryBuilder with the orderBy clause set
*/
orderBy(orderBy) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.orderBy = orderBy;
return newBuilder;
}
/**
* Set a limit on the number of results returned.
*
* @param limit Maximum number of results to return
* @returns A new QueryBuilder with the limit set
*/
limit(limit) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.limit = limit;
return newBuilder;
}
/**
* Set an offset to skip a number of results.
*
* @param offset Number of results to skip
* @returns A new QueryBuilder with the offset set
*/
offset(offset) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.offset = offset;
return newBuilder;
}
/**
* Specify which column(s) to use as keys in the output keyed stream.
*
* @param keyBy The column(s) to use as keys
* @returns A new QueryBuilder with the keyBy clause set
*/
keyBy(keyBy) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.keyBy = keyBy;
return newBuilder;
}
/**
* Add a groupBy clause to group the results by one or more columns.
*
* @param groupBy The column(s) to group by
* @returns A new QueryBuilder with the groupBy clause set
*/
groupBy(groupBy) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
newBuilder.query.groupBy = groupBy;
return newBuilder;
}
/**
* Define a Common Table Expression (CTE) that can be referenced in the main query.
* This allows referencing the CTE by name in subsequent from/join clauses.
*
* @param name The name of the CTE
* @param queryBuilderCallback A function that builds the CTE query
* @returns A new QueryBuilder with the CTE added
*/
with(name, queryBuilderCallback) {
const newBuilder = new BaseQueryBuilder();
Object.assign(newBuilder.query, this.query);
const cteBuilder = new BaseQueryBuilder();
const cteQueryBuilder = queryBuilderCallback(
cteBuilder
);
const cteQuery = cteQueryBuilder._query;
const withQuery = {
...cteQuery,
as: name
};
if (!newBuilder.query.with) {
newBuilder.query.with = [withQuery];
} else {
newBuilder.query.with = [...newBuilder.query.with, withQuery];
}
return newBuilder;
}
get _query() {
return this.query;
}
}
function queryBuilder() {
return new BaseQueryBuilder();
}
export {
BaseQueryBuilder,
queryBuilder
};
//# sourceMappingURL=query-builder.js.map