@frt-platform/report-core
v1.5.1
Published
Core report template engine: schema, validation, normalization, and migration.
Readme
@frt-platform/report-core
Core engine for defining, validating, migrating, diffing, and serializing dynamic report templates.
This package is:
- 🧠 Framework-agnostic
- 🧩 UI-agnostic
- 🔒 Safe by design (Zod-based)
- 🧱 The foundation of the FRT incident & reporting platform
It contains no React, no database code, and no styling.
You can use it in Node, Next.js, backend services, or custom form engines.
✨ Features
📄 Template Schema
Sections → fields
Field IDs, labels, descriptions, placeholders
Built-in field types:
shortText,longTextnumberdatecheckboxsingleSelect,multiSelectrepeatGroup(nested fieldsets)
🎛 Field Constraints
- min/max length (
shortText,longText) - min/max value (
number) - min/max selections (
multiSelect) - allowed options
- checkbox required semantics
- default values
🔀 Conditional Logic
visibleIfrequiredIf- Supports:
equals,any,all,not - Fully integrated into validation.
🔥 Validation Engine
Build response schema dynamically with conditions
DX helper:
buildResponseSchema(template)(no response needed)Throwing API:
validateReportResponse()Non-throwing API:
validateReportResponseDetailed()Rich
ResponseFieldErrorobjects with:- section title
- field label
- error code
- full message
- nested repeatGroup row context
🔄 Template Migration & Normalization
- Legacy format migration (
fields → sections) - Automatic ID normalization & uniqueness enforcement
- Stable
"lowercase_with_underscores"-style IDs - Fallback IDs for missing values:
section_1,field_1, etc.
🔍 Schema Diffing
Detect:
- Added / removed / reordered sections
- Added / removed / reordered fields
- Modified fields
- Nested diffs inside repeat groups
📦 JSON Schema Export
Export a template as a valid JSON Schema (2020-12 draft)
Includes vendor extensions:
x-frt-visibleIfx-frt-requiredIfx-frt-minIf/x-frt-maxIffor repeatGroup- placeholders
Useful for OpenAPI, Postman, or other backend runtimes.
🏗 Field Registry
Extend the system at runtime:
- Add custom types (
richText,fileUpload, etc.) - Override validation logic
- Provide metadata for UI packages
- Unknown/unregistered field types safely fall back to
z.any()so they never break the core engine.
🧬 Type Inference
Get a fully typed response type from a template:
type MyResponse = InferResponse<typeof template>;🧾 Serialization Helpers
Deterministic JSON output with sorting options:
serializeReportTemplateSchema(template, {
pretty: true,
sortSectionsById: true,
sortFieldsById: true,
});Perfect for Git diffs and storage.
📦 Installation
npm install @frt-platform/report-core zodzod is a peer dependency.
🧹 Parsing & Normalization
The core exposes helpers for safely parsing, migrating, and normalizing templates.
import {
parseReportTemplateSchema,
parseReportTemplateSchemaFromString,
} from "@frt-platform/report-core";
// From a raw JS object (e.g. loaded from DB, file, etc.)
const template = parseReportTemplateSchema(rawTemplateObject);
// From a JSON string
const templateFromString = parseReportTemplateSchemaFromString(jsonString);Under the hood, parseReportTemplateSchema does:
Legacy migration
Accepts both the new
{ sections: [...] }format and legacy flat:{ version: 1, fields: [ /* ... */ ] }Legacy
fieldsare automatically wrapped into a singlesection_1.
Schema validation
- Uses a Zod validator for
ReportTemplateSchemato ensure the template is structurally valid.
- Uses a Zod validator for
Normalization
Section and field IDs are normalized to lowercase with spaces → underscores:
"My Field!"→"my_field"
Invalid characters are stripped (only
[a-z0-9_-]are kept).IDs are made unique per namespace by appending
-1,-2, … as needed:my_field,my_field-1,my_field-2, …
Missing IDs get deterministic fallbacks:
- Sections:
section_1,section_2, … - Fields:
field_1,field_2, …
- Sections:
This makes templates safe to store, diff, and round-trip in a stable way.
🚀 Quickstart
1. Define a template
import type { ReportTemplateSchema } from "@frt-platform/report-core";
const template: ReportTemplateSchema = {
version: 1,
sections: [
{
id: "general",
title: "General Info",
fields: [
{
id: "title",
type: "shortText",
label: "Incident title",
required: true,
},
{
id: "severity",
type: "singleSelect",
label: "Severity",
required: true,
options: ["Low", "Medium", "High"],
},
{
id: "details",
type: "longText",
label: "Details",
minLength: 10,
},
],
},
],
};2. Validate a response (throwing API)
import { validateReportResponse } from "@frt-platform/report-core";
const parsed = validateReportResponse(template, {
title: "Broken fire alarm",
severity: "High",
details: "Triggered after smoke test",
});If invalid → throws a ZodError.
3. Validate without throwing (UI-friendly)
import { validateReportResponseDetailed } from "@frt-platform/report-core";
const result = validateReportResponseDetailed(template, {
title: "",
severity: "High",
});
if (!result.success) {
console.log(result.errors);
}Produces:
[
{
fieldId: "title",
sectionId: "general",
sectionTitle: "General Info",
label: "Incident title",
code: "field.too_small",
message:
'Section "General Info" → Field "Incident title": String must contain at least 1 character(s).'
}
]🔀 Conditional Logic Example
{
id: "follow_up_notes",
type: "longText",
label: "Follow-up notes",
visibleIf: { equals: { follow_up_required: true } },
requiredIf: { equals: { follow_up_required: true } },
}Behavior:
- If
follow_up_required = false→ field is hidden and ignored - If
true→ field becomes required
🔁 Repeat Group Example
{
id: "injured",
type: "repeatGroup",
label: "Injured people",
min: 1,
max: 5,
fields: [
{ id: "name", type: "shortText", label: "Name", required: true },
{ id: "injury", type: "longText", label: "Injury description" }
]
}Response shape:
injured: Array<{ name: string; injury?: string }>;📝 repeatGroup behavior & limitations
Base constraints
min/maxare always enforced on the row array.- Each row is an object keyed by nested field IDs.
Conditional
minIf/maxIf- If
minIf/maxIfare present, they are evaluated against the current response. - When the condition is
true, the conditional value overrides the staticmin/maxfor that validation pass. - When the condition is
false, the engine falls back to the staticmin/max(if any).
- If
Conditional logic inside rows
- Nested fields in a repeatGroup support the same
visibleIf/requiredIfsemantics as top-level fields. - Hidden nested fields are treated as optional and are stripped from the parsed response, just like hidden top-level fields.
- For now, row-level conditions see the full response object, not just the row. This matches top-level behavior and keeps the logic model simple.
- Nested fields in a repeatGroup support the same
JSON Schema export
- repeatGroup constraints and conditions are exported via
x-frt-*vendor extensions (e.g.x-frt-minIf,x-frt-maxIf,x-frt-visibleIf,x-frt-requiredIf), so you can mirror this behavior in other runtimes.
- repeatGroup constraints and conditions are exported via
🧩 Field Registry (Custom Types)
import { FieldRegistry } from "@frt-platform/report-core";
import { z } from "zod";
FieldRegistry.register("richText", {
defaults: { label: "Details" },
buildResponseSchema(field) {
let schema = z.string();
if (field.minLength) schema = schema.min(field.minLength);
return field.required ? schema : schema.optional();
},
});Now templates may include fields like:
{ id: "body", type: "richText", label: "Report body" }If a template uses a field type that is not registered and not one of the built-in core types, the engine safely falls back to z.any() so unknown types never crash validation.
🧾 JSON Schema Export
import { exportJSONSchema } from "@frt-platform/report-core";
const jsonSchema = exportJSONSchema(template);Produces JSON Schema with:
- field types
- enums
- min/max constraints
- default values
- conditional logic preserved as custom
x-frt-*properties
🔍 Diff Templates
import { diffTemplates } from "@frt-platform/report-core";
const diff = diffTemplates(oldTemplate, newTemplate);Detects:
- added/removed/reordered sections
- added/removed/reordered fields
- modified fields
- nested diffs for repeat groups
Perfect for:
- Version history
- Audit logs
- Template editing UI
🧬 Type Inference
Given a template:
export const myTemplate = {
version: 1,
sections: [
{
id: "s",
fields: [
{ id: "title", type: "shortText", required: true },
{ id: "tags", type: "multiSelect", options: ["A", "B"] },
]
}
]
} as const;Infer response type:
type MyResponse = InferResponse<typeof myTemplate>;Produces:
type MyResponse = {
title: string;
tags?: ("A" | "B")[];
};🧾 Serialization
import { serializeReportTemplateSchema } from "@frt-platform/report-core";
const json = serializeReportTemplateSchema(template, {
pretty: true,
sortSectionsById: true,
sortFieldsById: true,
});Useful for deterministic output in Git and stable diffs across environments.
🧱 Roadmap
Phase 1 — Core Maturation (✔️ COMPLETE)
- Validation
- Conditional logic
- Diffing
- Field Registry
- Error helpers
- Serialization features
- Parsing & normalization helpers
Phase 2 — Advanced Field System (IN PROGRESS)
- Richer repeatGroup UX
- Computed fields (design)
- RichText / FileUpload via registry
Phase 3 — Reactions & Analytics (Planned)
- Scoring rules
- Auto-tagging
- Suggested outcomes
Phase 4 — React UI Package (Planned)
- Form renderer
- Template builder
- Field palette
- Full ShadCN integration
📄 License
MIT — feel free to use, extend, or contribute.
