import { useMemo, useState, type ChangeEvent } from "react"; import { triggerBlobDownload } from "@add-ideas/toolbox-helpers"; import { generateFixtures, modelJson, parseFixtureModel, serializeDataset, validateModel, type Distribution, type FixtureField, type FixtureModel, type FixtureType, type GeneratedDataset, type ModelFormat, type OutputFormat, } from "../fixture/model"; const SAMPLE_SQL = `CREATE TABLE users ( id INTEGER PRIMARY KEY, email VARCHAR(80) NOT NULL UNIQUE, display_name VARCHAR(48) NOT NULL, active BOOLEAN NOT NULL, created_at TIMESTAMP NOT NULL ); CREATE TABLE orders ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), amount DECIMAL(10,2) NOT NULL, ordered_at TIMESTAMP NOT NULL );`; const initialParsed = parseFixtureModel(SAMPLE_SQL, "sql"); const initialDataset = generateFixtures(initialParsed.model, { seed: "fixture-tools-demo", rows: 8, boundaryPercent: 10, invalidPercent: 0, }); type View = "model" | "data" | "coverage" | "violations"; const TYPES: FixtureType[] = [ "string", "integer", "number", "boolean", "date", "datetime", "email", "uuid", "enum", "foreignKey", ]; const DISTRIBUTIONS: Distribution[] = [ "uniform", "sequential", "normal", "constant", ]; function download(text: string, filename: string, mediaType: string) { triggerBlobDownload(new Blob([text], { type: mediaType }), filename); } function replaceField( model: FixtureModel, tableIndex: number, fieldIndex: number, changes: Partial, ): FixtureModel { return { tables: model.tables.map((table, currentTable) => currentTable === tableIndex ? { ...table, fields: table.fields.map((field, currentField) => currentField === fieldIndex ? { ...field, ...changes } : field, ), } : table, ), }; } function FieldEditor({ field, onChange, onRemove, }: { field: FixtureField; onChange: (changes: Partial) => void; onRemove: () => void; }) { return ( onChange({ name: event.target.value })} /> {field.type === "foreignKey" ? ( onChange({ reference: event.target.value })} /> ) : field.type === "enum" ? ( onChange({ values: event.target.value .split(",") .map((value) => value.trim()) .filter(Boolean), }) } /> {field.distribution === "constant" ? ( ) : null} ) : field.distribution === "constant" ? ( field.type === "boolean" ? ( ) : ( onChange({ constant: event.target.value === "" ? undefined : field.type === "integer" || field.type === "number" ? Number(event.target.value) : event.target.value, }) } /> ) ) : field.type === "integer" || field.type === "number" ? ( onChange({ minimum: event.target.value ? Number(event.target.value) : undefined, }) } /> onChange({ maximum: event.target.value ? Number(event.target.value) : undefined, }) } /> ) : ( onChange({ minLength: event.target.value ? Number(event.target.value) : undefined, }) } /> onChange({ maxLength: event.target.value ? Number(event.target.value) : undefined, }) } /> )} ); } export function Workbench() { const [source, setSource] = useState(SAMPLE_SQL); const [sourceName, setSourceName] = useState("shop.sql"); const [inputFormat, setInputFormat] = useState("sql"); const [model, setModel] = useState(initialParsed.model); const [notices, setNotices] = useState(initialParsed.notices); const [dataset, setDataset] = useState(initialDataset); const [seed, setSeed] = useState("fixture-tools-demo"); const [rows, setRows] = useState(8); const [boundaryPercent, setBoundaryPercent] = useState(10); const [invalidPercent, setInvalidPercent] = useState(0); const [providerProfile, setProviderProfile] = useState("auto"); const [selectedTable, setSelectedTable] = useState(model.tables[0]!.name); const [outputFormat, setOutputFormat] = useState("json"); const [view, setView] = useState("data"); const [error, setError] = useState(); const tableIndex = Math.max( 0, model.tables.findIndex((table) => table.name === selectedTable), ); const table = model.tables[tableIndex]!; const records = dataset.tables[selectedTable] ?? []; const output = useMemo(() => { try { return { text: serializeDataset(dataset, outputFormat, selectedTable) }; } catch (reason) { return { text: "", error: reason instanceof Error ? reason.message : "Output failed.", }; } }, [dataset, outputFormat, selectedTable]); const parseSource = () => { try { const parsed = parseFixtureModel(source, inputFormat); setModel(parsed.model); setNotices(parsed.notices); setSelectedTable(parsed.model.tables[0]!.name); setError(undefined); setView("model"); } catch (reason) { setError( reason instanceof Error ? reason.message : "The model could not be parsed.", ); } }; const generate = () => { try { const validated = validateModel(structuredClone(model)); const next = generateFixtures(validated, { seed, rows, boundaryPercent, invalidPercent, providerProfile, }); setModel(validated); setDataset(next); setError(undefined); setView("data"); } catch (reason) { setError(reason instanceof Error ? reason.message : "Generation failed."); } }; const openFile = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; if (file.size > 2 * 1024 * 1024) { setError("Model file exceeds 2 MiB."); return; } const extension = file.name.split(".").at(-1)?.toLowerCase(); const format: ModelFormat = file.name .toLowerCase() .endsWith(".fixture.json") ? "fixture-model" : extension === "sql" ? "sql" : extension === "xsd" ? "xsd" : extension === "csv" ? "csv" : "json-schema"; setSource(await file.text()); setSourceName(file.name); setInputFormat(format); setError(undefined); }; const updateCurrentField = ( fieldIndex: number, changes: Partial, ) => setModel((current) => replaceField(current, tableIndex, fieldIndex, changes), ); const extension = outputFormat === "json" ? "json" : outputFormat === "ndjson" ? "ndjson" : outputFormat === "xml" ? "xml" : outputFormat === "sql" ? "sql" : "csv"; return (

Deterministic synthetic-data laboratory

Fixture Tools

Turn local schemas into replayable, privacy-safe datasets—with foreign keys, boundary values and intentionally invalid cases kept explicit.

Seeded · local only

Model source

{sourceName} · the last valid model remains active after an error