feat(12.2-01): add lagoon.Date and lagoon.TimeOfDay with Fill and required support

- Date (DATE) and TimeOfDay (TIME) with Scanner, Valuer, JSON and text forms
- Fill falls back to encoding.TextUnmarshaler for string sources after
  every existing conversion, so time.Time and the new types fill from JSON
- required treats a zero time.Time, Date or TimeOfDay as empty
- lagoon README, models and casts-and-validation docs
This commit is contained in:
Jakub Zych
2026-10-02 17:40:48 +02:00
parent 19f4cf8232
commit f48a886f94
7 changed files with 532 additions and 5 deletions

View File

@@ -72,6 +72,20 @@ Keep the key in the environment (`SUMMER_APP__KEY`), not in a committed file.
The ciphertext format is not Laravel's. To import rows that the PHP application encrypted, decrypt them once with `lagoon.DecryptLaravelPayload` and the old `APP_KEY`, then save them through `lagoon.Encrypted`. It is meant for a one-off import, never for reading live data.
## Date and time columns
Three Go types cover WinterCMS's date, datetime and time columns, and all of them fill from the JSON strings an admin form posts:
| Column | Go type | JSON form | Nullable variant |
|--------|---------|-----------|------------------|
| `DATE` | `lagoon.Date` | `"2026-10-02"` | `*lagoon.Date` |
| `TIME` | `lagoon.TimeOfDay` | `"14:30:00"` | `*lagoon.TimeOfDay` |
| `TIMESTAMPTZ` | `time.Time` | `"2026-10-02T12:30:00Z"` | `*time.Time` |
`lagoon.Date` has no time of day and no time zone. Build one with `lagoon.NewDate`, `lagoon.DateOf` (the calendar date of a `time.Time` in its own location) or `lagoon.ParseDate`, which accepts exactly `YYYY-MM-DD`. Reading a `DATE` column takes the calendar date the driver returns, with no time-zone conversion, so a date written as 2026-10-02 reads back as 2026-10-02 in every process time zone. `lagoon.TimeOfDay` holds hours, minutes and seconds; `lagoon.ParseTimeOfDay` accepts `HH:MM` and `HH:MM:SS` and drops fractional seconds, and `lagoon.NewTimeOfDay` builds one from numbers. A `time.Time` keeps the instant of a timestamp with an offset, and GORM writes `timestamptz` in UTC.
The zero value of `lagoon.Date` and `lagoon.TimeOfDay` means "no value": it is stored as NULL and marshals to JSON `null`, and an empty string fills it. Midnight, `00:00:00`, is a set time. For an optional column, prefer the pointer variants, which keep NULL distinct from a set value through every layer.
## Validation
`lagoon.Validate` checks a map of input values against Laravel-style rule strings and returns the errors in Laravel's shape, a map from field to messages. It returns `nil` when the input is valid. The second return value is for failures that are not the user's fault, such as an unknown rule or a database error:
@@ -94,7 +108,7 @@ fmt.Println(string(out))
// {"title":["The title field is required."],"views":["The views may not be greater than 1000."]}
```
The supported rules are `required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different` and `mimes`. Any other rule is an error, so a rule that SummerCMS does not implement cannot be skipped by accident. On a field that is `integer` or `numeric`, `min`, `max` and `between` compare the number; on other fields they compare the length. A number below the lower bound gets the `min` message, one above the upper bound the `max` message, and a bound that came from `between` gets the numeric `between` message (`The views must be between 0 and 10.`).
A zero `time.Time`, `lagoon.Date` or `lagoon.TimeOfDay`, or a pointer to one, is empty for `required`, so a required date field with no value fails instead of saving year 1. The supported rules are `required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different` and `mimes`. Any other rule is an error, so a rule that SummerCMS does not implement cannot be skipped by accident. On a field that is `integer` or `numeric`, `min`, `max` and `between` compare the number; on other fields they compare the length. A number below the lower bound gets the `min` message, one above the upper bound the `max` message, and a bound that came from `between` gets the numeric `between` message (`The views must be between 0 and 10.`).
`unique:<table>` runs a query to check that no other row of the table has the value in the field's column, so it needs a database handle; pass the transaction you are writing in. Soft-deleted rows do not count, and when the model you pass has an ID, its own row does not count either, so the same rules work for create and update. Pass a `phrasebook.Translator` as the last argument to get the messages in the request locale from the `lagoon::validate` catalog; with `nil` they are in English. See [Localization](../services/localization.md).