Documentation
¶
Overview ¶
Package schema is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.
Index ¶
- Constants
- Variables
- func DecimalColumn(f Field) (precision, scale int, bounded bool)
- func DecimalExactOnSQLite(colType, s string) bool
- func DecimalExponentFits(s string) bool
- func FieldTypeLabel(t FieldType, wire bool) string
- func FormatTimestamp(t time.Time) string
- func JSONSchema(fields []Field) map[string]any
- func LengthBound(v float64, round func(float64) float64) (n int, ok bool)
- func Validate(field Field, value any) error
- func VarcharWidth(max *float64) (int, bool)
- type AutoGenerate
- type Field
- type FieldType
- type Schema
- type ValidationResult
- type Violation
Constants ¶
const DecimalColumnType = "DECIMAL(19,4)"
DecimalColumnType is the column a Decimal field gets when it sets no RawType: 19 significant digits, 4 of them after the point, so 15 before it. The migrators emit it and DecimalColumn reads it, so the write check and the DDL cannot drift.
const MaxDecimalExponent = 1000
MaxDecimalExponent is the largest exponent magnitude Postgres numeric_in reads (NUMERIC_MAX_PRECISION). Past it the literal is 22P02 even for an unconstrained NUMERIC, while SQLite stores it and strconv.ParseFloat reads "1e-1001" as 0 with no error. The write check and the filter coercion (framework/filter) share it.
const MaxVarcharWidth = 10485760
MaxVarcharWidth is the widest VARCHAR(n) every supported dialect accepts: Postgres refuses n past 10485760 at CREATE TABLE.
const SQLiteDecimalDigits = 15
SQLiteDecimalDigits is how many significant digits a SQLite column keeps of a decimal with a fraction. SQLite has no exact decimal type: a DECIMAL or NUMERIC column stores such a value as a 64-bit float, which holds any 15 significant digits exactly and not every 16.
const TimestampLayout = "2006-01-02T15:04:05.000000Z07:00"
TimestampLayout is the one spelling a Timestamp column holds: UTC, microseconds, trailing zeros kept. crud writes every Timestamp in it, a keyset cursor carries it, and every filter and typed query condition binds a Timestamp value in it. SQLite compares the column as text, so a value bound as the "...00Z" a caller sent matched no row stored as "...00.000000Z" for the same instant, and gt/lt compared spellings.
Variables ¶
var ( ReasonRequired = i18n.Define("validation.required", "is required") ReasonInvalid = i18n.Define("validation.invalid", "is invalid: {{detail}}") ReasonUnsupportedType = i18n.Define("validation.unsupported_type", "has unsupported field type {{type}}") ReasonNotString = i18n.Define("validation.not_string", "must be a string") ReasonInvalidText = i18n.Define("validation.invalid_text", "must be valid UTF-8 without NUL bytes") ReasonMinLength = i18n.Define("validation.min_length", "must be at least {{min}} characters") ReasonMaxLength = i18n.Define("validation.max_length", "must be at most {{max}} characters") ReasonPatternInvalid = i18n.Define("validation.pattern_invalid", "has an invalid pattern: {{detail}}") ReasonPattern = i18n.Define("validation.pattern", "must match pattern {{pattern}}") ReasonNotInteger = i18n.Define("validation.not_integer", "must be an integer") ReasonMin = i18n.Define("validation.min", "must be at least {{min}}") ReasonMax = i18n.Define("validation.max", "must be at most {{max}}") ReasonNotNumber = i18n.Define("validation.not_number", "must be a number") ReasonNotFinite = i18n.Define("validation.not_finite", "must be a finite number") ReasonNotDecimalString = i18n.Define("validation.not_decimal_string", "must be a decimal string") ReasonNotDecimal = i18n.Define("validation.not_decimal", "must be a valid decimal number") ReasonNotFiniteDecimal = i18n.Define("validation.not_finite_decimal", "must be a finite decimal number") // ReasonDecimalExponent, ReasonDecimalIntegerDigits and // ReasonDecimalFractionDigits refuse a decimal its column cannot // store as sent (see [DecimalColumn]). ReasonDecimalExponent = i18n.Define("validation.decimal_exponent", "must have an exponent between -{{max}} and {{max}}") ReasonDecimalIntegerDigits = i18n.Define("validation.decimal_integer_digits", "must have at most {{max}} digits before the decimal point") ReasonDecimalFractionDigits = i18n.Define("validation.decimal_fraction_digits", "must have at most {{max}} digits after the decimal point") // ReasonDecimalSignificantDigits refuses, on SQLite, a decimal with a // fraction the column would keep only as a 64-bit float (see // [DecimalExactOnSQLite]). ReasonDecimalSignificantDigits = i18n.Define("validation.decimal_significant_digits", "must have at most {{max}} significant digits") ReasonNotTrueFalse = i18n.Define("validation.not_true_false", "must be true or false") ReasonNotBoolean = i18n.Define("validation.not_boolean", "must be a boolean") ReasonNotOneOf = i18n.Define("validation.not_one_of", "must be one of {{values}}") ReasonNotUUID = i18n.Define("validation.not_uuid", "must be a valid UUID") ReasonNotTimestamp = i18n.Define("validation.not_timestamp", "must be a valid RFC 3339 timestamp") ReasonNotDate = i18n.Define("validation.not_date", "must be a valid date (YYYY-MM-DD)") ReasonNotJSON = i18n.Define("validation.not_json", "must be valid JSON") ReasonNotReference = i18n.Define("validation.not_reference", "must be a string reference") ReasonEmptyReference = i18n.Define("validation.empty_reference", "must be a non-empty reference") ReasonNotPath = i18n.Define("validation.not_path", "must be a string path or URL") ReasonEmptyPath = i18n.Define("validation.empty_path", "must be a non-empty path or URL") ReasonUniqueTaken = i18n.Define("validation.unique_taken", "is already in use") )
The built-in validation phrases, under "validation.<code>".
Functions ¶
func DecimalColumn ¶ added in v0.96.0
DecimalColumn reports the precision and scale of the column a Decimal field is stored in: DecimalColumnType when RawType is empty, or the field's own RawType when it is DECIMAL(p[,s]) or NUMERIC(p[,s]) (a missing scale is 0, as in SQL). bounded is false for any other RawType (a bare NUMERIC, TEXT, a domain) or a precision/scale pair outside 1 <= p <= 1000, 0 <= s <= p: the write check then bounds the exponent alone.
func DecimalExactOnSQLite ¶ added in v0.96.0
DecimalExactOnSQLite reports whether a SQLite column of type colType (a Decimal field's RawType, or DecimalColumnType when it has none) stores the decimal literal s exactly. A column with TEXT or BLOB affinity stores the text as sent. Any other affinity stores a whole number that fits 64 bits as an integer, and anything else as a 64-bit float, which keeps SQLiteDecimalDigits significant digits. s is a literal the Decimal validator accepted.
func DecimalExponentFits ¶ added in v0.96.0
DecimalExponentFits reports whether a decimal literal's exponent, if it has one, is within ±[MaxDecimalExponent]. It reads the exponent text alone, so it is safe ahead of big.Rat, whose SetString builds 10^|exp| in full.
func FieldTypeLabel ¶ added in v0.87.0
FieldTypeLabel returns the human-readable label for a field type. It is the one table behind both renderings that name field types in prose: the llm.md entity reference (framework/crud) and the SDK reference screens (framework/sdkdocs), which used to carry two copies that could drift. wire selects the audience: true appends the wire-format notes an SDK author needs (decimal arrives as a string, image and file fields arrive as URLs); false returns the bare name the llm.md table uses. An unknown FieldType labels as "string", matching what both tables previously answered.
func FormatTimestamp ¶ added in v0.96.0
FormatTimestamp is t in TimestampLayout.
func JSONSchema ¶
JSONSchema generates a JSON Schema draft-07 object from a slice of Field definitions. The returned map can be marshaled directly to JSON.
func LengthBound ¶ added in v0.96.0
LengthBound converts a String/Text field's Min or Max to the integer a JSON Schema minLength/maxLength (or a column width) takes: rounded with round, then clamped to [0, math.MaxInt32]. ok is false for NaN, and the caller leaves the keyword out.
A bare int(*f.Max) is implementation-defined for a finite value past the int range (MinInt64 on amd64, MaxInt64 on arm64), so a huge Max published a negative or nonsense maxLength.
func Validate ¶
Validate checks a single value against a Field definition. Returns nil on success or a descriptive error on failure.
func VarcharWidth ¶ added in v0.96.0
VarcharWidth reports the VARCHAR width for a String field's Max, and false when the column should be TEXT instead: no Max, a Max under 1, or one wider than MaxVarcharWidth (validation still enforces the Max).
Types ¶
type AutoGenerate ¶
type AutoGenerate int
AutoGenerate controls how a field value is automatically produced server-side.
const ( AutoNone AutoGenerate = iota // not auto-generated AutoUUID // UUID v4 AutoTimestamp // current RFC 3339 timestamp AutoIncrement // auto-incrementing integer )
type Field ¶
type Field struct {
Name string // field name (must be unique within a Schema)
Type FieldType // field type
Required bool // value must be present and non-zero
Unique bool // value must be unique across entities
Default any // default value when omitted
AutoGenerate AutoGenerate // server-side auto-generation strategy
ReadOnly bool // excluded from create/update request bodies
Hidden bool // excluded from API responses
NoQuery bool // returned in responses, but never filterable/sortable/searchable
Max *float64 // upper bound (numeric max / string max-length)
Min *float64 // lower bound (numeric min / string min-length)
Pattern string // regex pattern for string validation
Values []string // allowed values for Enum
To string // target entity name for Relation
Many bool // has-many relation flag
RawType string // explicit SQL column type, overrides Type→SQL mapping (e.g. "NUMERIC(10,2)", "INET")
// WireName overrides the JSON key this field is exposed under. When
// empty, the wire key is derived from Name via the entity's JSONCase
// (camelCase → "authorId", snake_case → "author_id"). When set, it is
// used verbatim as the JSON output key, the accepted input key, and the
// filter parameter name. The DB column (Name) is never changed. This is
// how an API version renames or aliases a field without altering the
// underlying table: v2 exposes column "author_id" as "authorId" (the
// JSONCase default) or as a custom "writer" (WireName), while v1 keeps
// the bare column shape. Both read and write the same column.
WireName string
}
Field defines a single field in an entity schema.
Hidden and NoQuery both restrict a field, at different layers. Hidden removes it from responses AND from every query surface, so its value and its existence stay server-side. NoQuery keeps the field in responses but bars it from filters, sorting, and search.
NoQuery exists for values the caller may see only in transformed form, a card masked to last-4 by an AfterGet/AfterList hook, say. Without it the stored value is still a live column in WHERE and ORDER BY, so a caller recovers it a character at a time (?card_like=4111) while every response reads "****1111". Mark such a field NoQuery and the query surface refuses it. See framework/crud/redaction_oracle_security_test.go.
Hidden implies NoQuery; setting both is redundant but harmless.
type FieldType ¶
type FieldType int
FieldType enumerates all supported field types in a GoFastr entity schema.
const ( String FieldType = iota // short string Text // long text / textarea Int // integer Float // floating point Decimal // fixed-precision decimal (stored as string) Bool // boolean Enum // one of a fixed set of strings UUID // UUID v4 identifier Timestamp // RFC 3339 timestamp Date // calendar date (2006-01-02) JSON // arbitrary JSON blob Relation // reference to another entity Image // image URL or path File // file URL or path )
type Schema ¶
type Schema struct {
Fields []Field
}
Schema is an ordered collection of Fields with convenience helpers.
func (Schema) AutoGeneratedFields ¶
AutoGeneratedFields returns fields that are auto-generated server-side.
func (Schema) FieldByName ¶
FieldByName returns the field with the given name and true, or the zero Field and false if not found.
func (Schema) RequiredFields ¶
RequiredFields returns only the required fields.
func (Schema) VisibleFields ¶
VisibleFields returns fields that are included in API responses (not hidden).
func (Schema) WritableFields ¶
WritableFields returns fields that can be set by the client (not auto-generated, not read-only).
type ValidationResult ¶
type ValidationResult struct {
Valid bool
Errors map[string][]Violation // field name → its violations
}
ValidationResult holds the outcome of validating a full map of values.
func ValidateAll ¶
func ValidateAll(s Schema, values map[string]any) ValidationResult
ValidateAll validates a map of values against every Field in the Schema. Unknown keys are ignored; missing required fields produce errors.
func ValidatePartial ¶
func ValidatePartial(s Schema, values map[string]any) ValidationResult
ValidatePartial validates only the fields that are present in `values`. Missing fields, required or not, are not reported. Use this for partial-update flows (PUT/PATCH with only the fields the caller wants to change) so a sparse update doesn't fail because some other required field happens not to be in the body.
type Violation ¶ added in v0.94.0
Violation is one field-level validation failure: the phrase that names it and the values its message interpolates. It is an error whose Error() is the English message; Violation.Text renders it in the caller's locale, and Violation.Code is the stable code an API response carries next to the message.
An app's own validators define their phrases the same way the built-in ones below are defined, so their messages translate too:
var vatInvalid = i18n.Define("validation.vat_invalid", "is not a valid VAT number")
... schema.Violation{Reason: vatInvalid}
func AsViolation ¶ added in v0.94.0
AsViolation returns err as a Violation. An error that is not one (a validator that returned a plain error) becomes ReasonInvalid with the error's text as its detail.