Files
buzz-sheet/lib/content/data-source.ts
T

126 lines
3.3 KiB
TypeScript

import * as z from "zod";
import { formulaEngine, type FormulaError } from "@/lib/formula";
import type {
DataSourceField,
DataSourceRow,
DataSourceSnapshot,
} from "./types";
const fieldKeySchema = z
.string()
.min(1)
.max(80)
.regex(
/^[\p{L}_][\p{L}\p{M}\p{N}_]*$/u,
"Field keys may contain letters, marks, numbers, and underscores.",
);
export const dataSourceFieldSchema = z.strictObject({
key: fieldKeySchema,
label: z.string().trim().min(1).max(120),
type: z.enum(["text", "number", "badge", "media"]),
formula: z.string().trim().max(500).nullable().optional(),
displayPrecision: z.number().int().min(0).max(20).optional(),
});
export const dataSourceRowSchema = z.strictObject({
id: z.string().uuid(),
values: z.record(z.string(), z.string().nullable()),
hidden: z.boolean().optional(),
});
export const dataSourceSnapshotSchema = z.strictObject({
id: z.string().uuid(),
name: z.string().trim().min(1).max(160),
version: z.number().int().positive(),
fields: z.array(dataSourceFieldSchema).min(1).max(80),
rows: z.array(dataSourceRowSchema).max(2_000),
});
export interface EvaluatedDataSourceCell {
raw: string | null;
canonical: string | null;
display: string | null;
error?: FormulaError;
}
export interface EvaluatedDataSourceRow {
id: string;
hidden: boolean;
cells: Record<string, EvaluatedDataSourceCell>;
}
export interface EvaluatedDataSource {
source: DataSourceSnapshot;
rows: EvaluatedDataSourceRow[];
valid: boolean;
}
export type { DataSourceField, DataSourceRow, DataSourceSnapshot };
function assertUniqueFieldKeys(fields: readonly DataSourceField[]): void {
const keys = new Set<string>();
for (const field of fields) {
if (keys.has(field.key)) {
throw new Error(`Duplicate data-source field key: ${field.key}`);
}
keys.add(field.key);
}
}
export function evaluateDataSource(
input: DataSourceSnapshot,
): EvaluatedDataSource {
const source = dataSourceSnapshotSchema.parse(input);
assertUniqueFieldKeys(source.fields);
const rows = source.rows.map((row: DataSourceRow): EvaluatedDataSourceRow => {
const numericFields = source.fields.filter(
(field) => field.type === "number" || field.formula,
);
const evaluation = formulaEngine.evaluate(
numericFields.map((field) => ({
key: field.key,
value: row.values[field.key],
formula: field.formula,
displayPrecision: field.displayPrecision,
})),
);
const cells = Object.fromEntries(
source.fields.map((field) => {
const raw = row.values[field.key] ?? null;
if (field.type !== "number" && !field.formula) {
return [
field.key,
{ raw, canonical: raw, display: raw } satisfies EvaluatedDataSourceCell,
];
}
const calculated = evaluation.fields[field.key];
return [
field.key,
{
raw,
canonical: calculated?.canonical ?? null,
display: calculated?.display ?? null,
error: calculated?.error,
} satisfies EvaluatedDataSourceCell,
];
}),
);
return { id: row.id, hidden: row.hidden ?? false, cells };
});
return {
source,
rows,
valid: rows.every((row) =>
Object.values(row.cells).every((cell) => !cell.error),
),
};
}