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:
@@ -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).
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ if errors.As(err, &typeErr) {
|
||||
// invalid value for views
|
||||
```
|
||||
|
||||
`lagoon.Fill` matches keys by the GORM column name, not the Go field name. A number decoded with `json.Decoder.UseNumber` fills integer and float fields; a fraction or an overflow for an integer field is a `lagoon.FillTypeError`.
|
||||
`lagoon.Fill` matches keys by the GORM column name, not the Go field name. A number decoded with `json.Decoder.UseNumber` fills integer and float fields; a fraction or an overflow for an integer field is a `lagoon.FillTypeError`. A string fills a date or time column: an RFC 3339 timestamp such as `2026-10-02T12:30:00Z` fills a `time.Time` or `*time.Time` field, `2026-10-02` a `lagoon.Date` and `14:30:00` a `lagoon.TimeOfDay`, pointer variants included, so a model needs no date type of its own. A string that does not parse is a `lagoon.FillTypeError`. See [Date and time columns](casts-and-validation.md#date-and-time-columns).
|
||||
|
||||
## Creating a record
|
||||
|
||||
|
||||
Reference in New Issue
Block a user