Core Concepts
/
Validation & Data Import
Validation & Data Import
Constraints compile into a state machine — the same gates validate a form keystroke and an imported spreadsheet row, and rastack import loads CSV, Excel and PDF data through them.
View as Markdown
llms.txt
The generator's constraints — requiredness, datatype, maxLength, enum membership, unique, foreign keys — are not a bag of ad-hoc checks. They compile into a deterministic finite state machine (rastack/validate): each constraint becomes one gate, and a value must pass every gate, in a fixed order, to reach the accepting state.
the constraint state machine
text · copy
1
pristine → present → typed → bounded → member → unique → resolved → valid
2
└─────────┴───── any gate fails ────┴─────────┘
3
4
invalid
Compiling builds the constraints: a gate is only emitted when its constraint exists, so an unconstrained optional string's machine is just pristine → present → typed → valid. Running walks the gates with a raw value, coercing as it goes ("12"12, "yes"true), and halts at the first failing guard — reporting the state reached and the exact constraint that rejected the value.
The gates
Gate
Constraint
Emitted when
present
required / optional
always (first gate)
typed
datatype + string format
always — also coerces the value
bounded
maxLength
the field has a maxLength
member
enum membership
the schema carries an enum
unique
no duplicate in the dataset
the field is unique
resolved
the FK target exists
the field is a foreign key
Good to know
unique and resolved are dataset-level gates: they only engage when the run is given a dataset context (a seen-set, a relation resolver). A form validating a single value passes through them untouched — mirroring the server, where uniqueness and FK existence are checked against the table, not the payload.
The transition gate
One gate is record-level rather than per-field: when a resource declares state transitions, a change to the state field must follow a declared edge. The gate runs everywhere the per-field machine runs — the form hooks (useForm), rastack import, and the engine itself — and rejects an illegal move with the edge that was missing (status: no transition from "scheduled" to "departed"). The per-field pristine → … → valid gates are unchanged; the transition gate sits alongside them, on the record.
In the form hooks
Every form field carries its machine. getFormField(name) compiles it from the same write JSON Schema the form already validates against, and runs it against the current value on every render:
app/airports.tsx
tsx · copy
1
const
form
=
useForm
(
Airport
,
{
formValues
:
{
code
:
""
}
});
2
const
field
=
form
.
getFormField
(
"code"
);
3
4
field
.
constraints
;
// ["required", "string", "maxLength ≤ 3", "unique"]
5
field
.
validation
?.
state
;
// "valid" | "invalid" for the current value
6
field
.
validation
?.
path
;
// ["pristine", "present", "typed", "invalid"]
7
field
.
validation
?.
failed
;
// { gate: "bounded", constraint: "maxLength ≤ 3", … }
So a form can render exactly which gate the value stops at — with the same semantics the API enforces on write, because both compile from the same constraints. The existing validation path is unchanged; the machine is purely additive.
Import external data
rastack import loads CSV, Excel (XLSX) and PDF files into the shape of a compiled resource and runs every row through the resource's machine — with the full dataset context, so in-file duplicates and dangling foreign keys are rejected with row-located errors.
terminal
bash · copy
1
# validate a CSV against a resource's constraints
2
rastack
import
airports.csv
--resource
airports.airport
3
4
# resolve foreign keys against a related file's ids
5
rastack
import
terminals.xlsx
--resource
airports.terminal
\
6
--related
airports.airport=airports.csv
7
8
# write the coerced valid records for seeding or POSTing
9
rastack
import
bookings.pdf
--resource
airports.booking
--out
clean.json
Source columns map onto fields by normalised name — "Airport Code", airport_code and airportCode all match — and every failure names its row, field and gate:
rastack import output
text · copy
1
code → code
2
name → name
3
4
✗ row 2 code: "LHRX" is longer than 3 characters [failed at "bounded" — maxLength ≤ 3]
5
✗ row 3 code: duplicate value "LHR" [failed at "unique" — unique]
6
✗ row 4 code: code is required [failed at "present" — required]
7
8
✗ 1/4 row(s) valid, 3 rejected
The exit status is non-zero when any row fails (so CI can gate on it) unless --allow-invalid is passed; --report report.json writes the full validation report.
Source formats
Format
How it's read
.csv
the standard CSV parser (quoted fields supported)
.xlsx
a dependency-free reader: shared + inline strings, numbers, cached formula results from the first worksheet
.pdf
a best-effort table extractor: decodes the content streams and clusters positioned text into rows
! Warning
PDFs carry no table structure — only positioned text — so the PDF reader targets machine-generated tabular reports. Merged cells, wrapped cell text and scanned pages won't survive; export those sources to CSV or XLSX instead. Every extracted value still runs through the machine, so a misread cell is rejected with a located error, never silently imported.
Tip
The machine compiles from the manifest, so run rastack compile first (or point --schema at a schema.rastack.json). Command details on the CLI Reference page.
Build with AI
Every page is available as clean Markdown for LLMs and coding agents. Point one at the files below to build on top of React API Stack.
React API Stack · alpha · built to scale to millions