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; } export interface EvaluatedDataSource { source: DataSourceSnapshot; rows: EvaluatedDataSourceRow[]; valid: boolean; } export type { DataSourceField, DataSourceRow, DataSourceSnapshot }; function assertUniqueFieldKeys(fields: readonly DataSourceField[]): void { const keys = new Set(); 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), ), }; }