agnostic-query
Version:
Type-safe fluent builder for portable query schemas. Runtime-agnostic, database-agnostic — the same QuerySchema drives Drizzle, Kysely, db0, or raw SQL.
108 lines (107 loc) • 3.35 kB
JavaScript
//#region src/kysely/fromKysely.ts
const opReverseMap = {
"=": "=",
">": ">",
">=": ">=",
"<": "<",
"<=": "<=",
like: "like",
ilike: "ilike",
in: "in"
};
const parseRawSqlField = (sql) => {
const rootMatch = sql.match(/^"((?:[^"]|"")*)"(.*)$/);
if (!rootMatch) return ["unknown_path"];
const root = rootMatch[1].replace(/""/g, "\"");
const tail = rootMatch[2];
if (!tail) return [root];
if (/^\[\d+\]/.test(tail)) return [root, ...[...tail.matchAll(/\[(\d+)\]/g)].map((m) => Number(m[1]) - 1)];
const segments = [];
for (const m of tail.matchAll(/->>?(?:'((?:[^']|'')*)'|(\d+))/g)) if (m[1] !== void 0) segments.push(m[1].replace(/''/g, "'"));
else if (m[2] !== void 0) segments.push(Number(m[2]));
if (segments.length === 0) return ["unknown_path"];
return [root, ...segments];
};
const parseFieldFromNode = (node) => {
if (node?.kind === "ReferenceNode" && node.column?.kind === "ColumnNode") return [node.column.column.name];
if (node?.kind === "RawNode" && node.sqlFragments.length === 1) return parseRawSqlField(node.sqlFragments[0]);
return ["unknown_path"];
};
const parseWhere = (node) => {
if (!node) return;
if (node.kind === "AndNode" || node.kind === "OrNode") {
const conditions = [parseWhere(node.left), parseWhere(node.right)].filter((c) => !!c);
if (conditions.length === 0) return;
if (conditions.length === 1) return conditions[0];
return {
op: node.kind === "AndNode" ? "and" : "or",
conditions
};
}
if (node.kind === "ParensNode") return parseWhere(node.node);
if (node.kind === "UnaryOperationNode" && node.operator?.operator === "not") {
const inner = parseWhere(node.operand);
if (!inner) return;
return {
op: "not",
condition: inner
};
}
if (node.kind === "BinaryOperationNode") {
const field = parseFieldFromNode(node.leftOperand);
if (field.length === 0) return;
if (!node.operator?.operator) return;
if (node.operator?.operator === "in") return {
field,
op: "in",
values: node.rightOperand?.values ?? []
};
return {
field,
op: opReverseMap[node.operator.operator],
value: node.rightOperand?.value
};
}
};
const flattenLogic = (where) => {
if (where.op === "and" || where.op === "or") {
const flat = [];
for (const c of where.conditions) {
const flattened = flattenLogic(c);
if (flattened.op === where.op) flat.push(...flattened.conditions);
else flat.push(flattened);
}
return {
op: where.op,
conditions: flat
};
}
if (where.op === "not") return {
op: "not",
condition: flattenLogic(where.condition)
};
return where;
};
const fromKysely = (queryBuilder) => {
const node = queryBuilder.toOperationNode();
const result = {};
if (node.limit) {
const limitValue = node.limit.limit.value;
if (typeof limitValue === "number") result.limit = limitValue;
}
if (node.offset) {
const offsetValue = node.offset.offset.value;
if (typeof offsetValue === "number") result.offset = offsetValue;
}
if (node.orderBy && node.orderBy.items.length > 0) result.orderBy = node.orderBy.items.map((item) => {
return {
field: parseFieldFromNode(item.orderBy),
direction: item.direction?.sqlFragments?.[0] === "desc" ? "desc" : "asc"
};
});
const parsed = parseWhere(node.where?.where);
if (parsed) result.where = flattenLogic(parsed);
return result;
};
//#endregion
export { fromKysely };