refactor(api): extract report query engine
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import type { ColumnDef } from "./report-columns.js";
|
||||
import type { FilterInput, ReportGroupSummary } from "./report-query-config.js";
|
||||
|
||||
function parseFilterValue(def: ColumnDef | undefined, value: string): unknown {
|
||||
if (!def) {
|
||||
return value;
|
||||
}
|
||||
if (def.dataType === "number") {
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
if (def.dataType === "boolean") {
|
||||
return value === "true";
|
||||
}
|
||||
if (def.dataType === "date") {
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed.getTime();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function matchesInMemoryFilter(
|
||||
row: Record<string, unknown>,
|
||||
filter: FilterInput,
|
||||
columns: ColumnDef[],
|
||||
): boolean {
|
||||
const def = columns.find((column) => column.key === filter.field);
|
||||
if (!def) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rowValueRaw = row[filter.field];
|
||||
const rowValue = def.dataType === "date" && typeof rowValueRaw === "string"
|
||||
? new Date(rowValueRaw).getTime()
|
||||
: rowValueRaw;
|
||||
const parsedFilterValue = parseFilterValue(def, filter.value);
|
||||
|
||||
if (parsedFilterValue === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (filter.op) {
|
||||
case "eq":
|
||||
return rowValue === parsedFilterValue;
|
||||
case "neq":
|
||||
return rowValue !== parsedFilterValue;
|
||||
case "gt":
|
||||
return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue > parsedFilterValue;
|
||||
case "lt":
|
||||
return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue < parsedFilterValue;
|
||||
case "gte":
|
||||
return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue >= parsedFilterValue;
|
||||
case "lte":
|
||||
return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue <= parsedFilterValue;
|
||||
case "contains":
|
||||
return typeof rowValue === "string" && rowValue.toLowerCase().includes(filter.value.toLowerCase());
|
||||
case "in":
|
||||
return filter.value.split(",").map((value) => value.trim()).includes(String(rowValue ?? ""));
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function sortInMemoryRows(
|
||||
rows: Record<string, unknown>[],
|
||||
groupBy: string | undefined,
|
||||
sortBy: string | undefined,
|
||||
sortDir: "asc" | "desc",
|
||||
columns: ColumnDef[],
|
||||
): Record<string, unknown>[] {
|
||||
if (!groupBy && !sortBy) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
return [...rows].sort((left, right) => {
|
||||
if (groupBy) {
|
||||
const groupDef = columns.find((column) => column.key === groupBy);
|
||||
const groupComparison = compareRowValues(left[groupBy], right[groupBy], groupDef, "asc");
|
||||
if (groupComparison !== 0) {
|
||||
return groupComparison;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sortBy) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sortDef = columns.find((column) => column.key === sortBy);
|
||||
return compareRowValues(left[sortBy], right[sortBy], sortDef, sortDir);
|
||||
});
|
||||
}
|
||||
|
||||
function compareRowValues(
|
||||
leftValue: unknown,
|
||||
rightValue: unknown,
|
||||
def: ColumnDef | undefined,
|
||||
sortDir: "asc" | "desc",
|
||||
): number {
|
||||
if (leftValue == null && rightValue == null) {
|
||||
return 0;
|
||||
}
|
||||
if (leftValue == null) {
|
||||
return 1;
|
||||
}
|
||||
if (rightValue == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const direction = sortDir === "asc" ? 1 : -1;
|
||||
if (def?.dataType === "number") {
|
||||
return direction * (Number(leftValue) - Number(rightValue));
|
||||
}
|
||||
if (def?.dataType === "boolean") {
|
||||
return direction * (Number(Boolean(leftValue)) - Number(Boolean(rightValue)));
|
||||
}
|
||||
if (def?.dataType === "date") {
|
||||
return direction * (new Date(String(leftValue)).getTime() - new Date(String(rightValue)).getTime());
|
||||
}
|
||||
return direction * String(leftValue).localeCompare(String(rightValue), "de");
|
||||
}
|
||||
|
||||
export function pickColumns(row: Record<string, unknown>, columns: string[]): Record<string, unknown> {
|
||||
return Object.fromEntries(columns.map((column) => [column, row[column]]));
|
||||
}
|
||||
|
||||
export function buildReportGroups(
|
||||
rows: Record<string, unknown>[],
|
||||
groupBy: string | undefined,
|
||||
): ReportGroupSummary[] {
|
||||
if (!groupBy) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groups: ReportGroupSummary[] = [];
|
||||
let currentKey: string | null = null;
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
const rawValue = row[groupBy];
|
||||
const label = formatGroupValue(rawValue);
|
||||
const key = `${groupBy}:${label}`;
|
||||
|
||||
if (key !== currentKey) {
|
||||
groups.push({
|
||||
key,
|
||||
label,
|
||||
rowCount: 1,
|
||||
startIndex: index,
|
||||
});
|
||||
currentKey = key;
|
||||
return;
|
||||
}
|
||||
|
||||
groups[groups.length - 1]!.rowCount += 1;
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function formatGroupValue(value: unknown): string {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "No value";
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
Reference in New Issue
Block a user