feat(13-01): add the prohibited rule and dated-download and notification masks
- lagoon.ValidateRequest supports Laravel 9 prohibited (!required, not
implicit); with no catalog line its message is validation.prohibited
- tide compares Content-Disposition with real calendar dates masked on both
sides; a different name, an invalid date or a one-sided date still diffs
- tide.NormalizePublications masks a Carbon +00:00 $.data.payload.created_at
and an uncaptured positive integer $.data.payload.id as {{id}}
- the album-date test's outside-album sibling moves off payload.created_at,
which now has its own mask
- READMEs and docs describe the rule and both masks
This commit is contained in:
@@ -18,7 +18,7 @@ Postgres data layer: the shared GORM connection, per-plugin migrations, model he
|
||||
- Per-plugin migrations: `lagoon.Migrate` runs the framework's `system_files` set (`attach.Migrations`), backend admin identity set (`lagoon.BackendAdminMigrations`), `deferred_bindings` set (`lagoon.DeferredBindingMigrations`, under the `lagoon.DeferredHistoryID` history) and job-queue set (`lagoon.QueueMigrations`: River's schema pinned at `lagoon.RiverSchemaVersion`, then the `lagoon.JobsTable` record table, under the `lagoon.QueueHistoryID` history), then every `pact.HasMigrations` set in plugin activation order, each in its own `summer_migrations_<plugin_id>` history table (`lagoon.HistoryTableName`). `lagoon.RollbackLast` and `lagoon.Status` cover rollback and history.
|
||||
- Mass assignment: `lagoon.Fill` copies only allow-listed keys onto a model by GORM column name and silently drops the rest, logging each dropped key once outside production. A `json.Number` (from a decoder using `UseNumber`) fills integer, unsigned and float fields. A value that does not fit its column (a fraction, an exponent or an overflow for an integer field, or a value of the wrong type) is a `lagoon.FillTypeError` naming the key, so a caller can answer it as a validation failure on that field. `lagoon.HasFillable` and `lagoon.HasHidden` are the Go forms of `$fillable` and `$hidden`.
|
||||
- Validation: `lagoon.Validate` (where `required` fails on a zero date or time) accepts Laravel-style rule strings (`required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different`, `mimes`) and returns a field-to-messages map, translated through phrasebook when a translator is given. Unknown rule tokens are an error. A failed numeric range reports the bound that failed: the `min` message below the lower bound, the `max` message above the upper one, and the numeric `between` message when the bound came from `between`.
|
||||
- Request validation: `lagoon.ValidateRequest` reproduces Laravel 9 request validation for ported API endpoints, so a 422 body matches the PHP one message for message. It takes the decoded input and an ordered `lagoon.RequestRule` table (attribute names may hold `*` wildcards, expanded against the input to `posts.0.title`), runs the rules of each attribute in order and stops an attribute after a failed implicit rule (`required`, `present`, `filled`, `accepted`) or, under `bail`, after any failure. A non-implicit rule is skipped for an absent attribute, a blank string, a null value under `nullable` and an absent key under `sometimes`. Supported rules: `required`, `present`, `filled`, `accepted`, `nullable`, `sometimes`, `bail`, `array`, `string`, `integer`, `numeric`, `boolean`, `email` (PHP `FILTER_VALIDATE_EMAIL`, WinterCMS's default), `url`, `date`, `after`, `after_or_equal`, `before`, `before_or_equal` (a date, a relative word such as `tomorrow`, or another field), `exists:table,column`, `regex`, `not_regex`, `in`, `not_in`, `file`, `image`, `mimes`, `min`, `max`, `size` and `between`, plus closure rules built with `lagoon.CustomRule`. The size rules compare the number under `numeric` or `integer` (exactly, as decimals), the element count of an array, kilobytes of a `lagoon.UploadedFile`, and otherwise the length in characters, and pick the matching message. Messages come from the `lagoon::validation` catalog in the request locale; `lagoon.ErrorKeys` gives the attribute order of PHP's message bag.
|
||||
- Request validation: `lagoon.ValidateRequest` reproduces Laravel 9 request validation for ported API endpoints, so a 422 body matches the PHP one message for message. It takes the decoded input and an ordered `lagoon.RequestRule` table (attribute names may hold `*` wildcards, expanded against the input to `posts.0.title`), runs the rules of each attribute in order and stops an attribute after a failed implicit rule (`required`, `present`, `filled`, `accepted`) or, under `bail`, after any failure. A non-implicit rule is skipped for an absent attribute, a blank string, a null value under `nullable` and an absent key under `sometimes`. Supported rules: `required`, `present`, `filled`, `accepted`, `nullable`, `sometimes`, `bail`, `array`, `string`, `integer`, `numeric`, `boolean`, `email` (PHP `FILTER_VALIDATE_EMAIL`, WinterCMS's default), `url`, `date`, `after`, `after_or_equal`, `before`, `before_or_equal` (a date, a relative word such as `tomorrow`, or another field), `exists:table,column`, `regex`, `not_regex`, `in`, `not_in`, `file`, `image`, `mimes`, `min`, `max`, `size`, `between` and `prohibited` (fails for any value `required` would accept, `0` and `false` included; not implicit, so an absent key, `null`, a blank string and an empty array pass, and with no catalog line its message is the key `validation.prohibited`), plus closure rules built with `lagoon.CustomRule`. The size rules compare the number under `numeric` or `integer` (exactly, as decimals), the element count of an array, kilobytes of a `lagoon.UploadedFile`, and otherwise the length in characters, and pick the matching message. Messages come from the `lagoon::validation` catalog in the request locale; `lagoon.ErrorKeys` gives the attribute order of PHP's message bag.
|
||||
- Safe ordering: `lagoon.OrderBy` appends an ORDER BY only for an allow-listed column and an `asc` or `desc` direction, and `lagoon.Collate` adds a validated `COLLATE` clause for language-specific text order (for example the ICU collation `pl-x-icu`); lagoon puts no requirement on the database's default locale.
|
||||
- Pagination: `lagoon.Paginate` builds a `lagoon.Page` with `data` and `meta` (`current_page`, `last_page`, `per_page`, `total`).
|
||||
- Date and time columns: `lagoon.Date` (a `DATE` column, JSON `"2026-10-02"`) and `lagoon.TimeOfDay` (a `TIME` column, JSON `"14:30:00"`) implement `sql.Scanner`, `driver.Valuer`, JSON and text marshalling, and store NULL for their zero value; `*lagoon.Date` and `*lagoon.TimeOfDay` are the nullable variants, next to `time.Time` and `*time.Time` for `timestamptz`. Build them with `lagoon.NewDate`, `lagoon.DateOf`, `lagoon.ParseDate`, `lagoon.NewTimeOfDay` and `lagoon.ParseTimeOfDay`. `lagoon.Fill` fills all six from JSON strings (RFC 3339 for `time.Time`) through their text unmarshalling, after every conversion it already made. Behaviour change: `required` now treats a zero `time.Time`, `lagoon.Date` or `lagoon.TimeOfDay` (or a pointer to one) as empty, so declare optional dates as pointer fields.
|
||||
|
||||
@@ -664,3 +664,52 @@ func TestValidateRequestExistsRule(t *testing.T) {
|
||||
t.Error("a missing table must be an error for arrays too")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateRequestProhibited ports Laravel 9's prohibited rule: it is
|
||||
// !validateRequired and not implicit, so an absent key and a blank string
|
||||
// are never validated, null and an empty array pass, and any value required
|
||||
// would accept fails, 0 and false included. Neither catalog has a
|
||||
// validation.prohibited line, so the message is the key itself.
|
||||
func TestValidateRequestProhibited(t *testing.T) {
|
||||
rules := []RequestRule{{Field: "condition", Rules: ParseRules("prohibited")}}
|
||||
pass := map[string]map[string]any{
|
||||
"absent": {},
|
||||
"null": {"condition": nil},
|
||||
"empty string": {"condition": ""},
|
||||
"blank string": {"condition": " "},
|
||||
"empty array": {"condition": []any{}},
|
||||
"empty map": {"condition": map[string]any{}},
|
||||
}
|
||||
for name, input := range pass {
|
||||
if got := mustValidate(t, inLocale("pl"), input, rules); len(got) != 0 {
|
||||
t.Errorf("%s: errors = %v, want none", name, got)
|
||||
}
|
||||
}
|
||||
fail := map[string]any{
|
||||
"string": "VG",
|
||||
"zero": json.Number("0"),
|
||||
"float zero": float64(0),
|
||||
"false": false,
|
||||
"true": true,
|
||||
"non-empty array": []any{"x"},
|
||||
"non-empty map": map[string]any{"a": 1},
|
||||
}
|
||||
for name, value := range fail {
|
||||
for _, loc := range []string{"pl", "en"} {
|
||||
got := mustValidate(t, inLocale(loc), map[string]any{"condition": value}, rules)
|
||||
want := map[string][]string{"condition": {"validation.prohibited"}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s (%s): errors = %#v, want %#v", name, loc, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Next to other rules, prohibited fails like any non-implicit rule.
|
||||
mixed := []RequestRule{
|
||||
{Field: "name", Rules: ParseRules("required|string")},
|
||||
{Field: "shelf", Rules: ParseRules("prohibited")},
|
||||
}
|
||||
got := mustValidate(t, inLocale("en"), map[string]any{"name": "Kind of Blue", "shelf": "A1"}, mixed)
|
||||
if want := map[string][]string{"shelf": {"validation.prohibited"}}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("mixed = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ var imageExtensions = []string{"jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"
|
||||
// parameter count: -1 any number (at least one), 0 none.
|
||||
var requestRuleArity = map[string]int{
|
||||
"required": 0, "present": 0, "filled": 0, "accepted": 0,
|
||||
"nullable": 0, "sometimes": 0, "bail": 0,
|
||||
"nullable": 0, "sometimes": 0, "bail": 0, "prohibited": 0,
|
||||
"array": -2, "string": 0, "integer": 0, "numeric": 0, "boolean": 0,
|
||||
"email": 0, "url": 0, "date": 0,
|
||||
"after": 1, "after_or_equal": 1, "before": 1, "before_or_equal": 1,
|
||||
@@ -278,6 +278,10 @@ func (v *requestValidator) passes(rule Rule, attr string, value any, present boo
|
||||
return !present || validateRequired(value), nil
|
||||
case "accepted":
|
||||
return validateRequired(value) && isAccepted(value), nil
|
||||
case "prohibited":
|
||||
// Laravel 9 validateProhibited: !validateRequired. Not implicit, so it
|
||||
// never runs for an absent key or a blank string.
|
||||
return !validateRequired(value), nil
|
||||
case "nullable", "sometimes", "bail":
|
||||
return true, nil
|
||||
case "array":
|
||||
|
||||
Reference in New Issue
Block a user