Database
Casts and validation
Store JSON and encrypted columns with lagoon.Jsonable and lagoon.Encrypted, and validate input with Laravel-style rule strings through lagoon.Validate.
On this page
Eloquent casts a column through $jsonable, $casts and the encrypted cast, and WinterCMS models validate with a $rules array. lagoon keeps both: column types that implement sql.Scanner and driver.Valuer, and a validator that reads the same rule strings.
JSON columns#
lagoon.Jsonable is the Go form of $jsonable. It stores any Go value as JSON text in a TEXT column (not jsonb, so data copied from a WinterCMS database fits as it is). The value lives in Data; Valid tells SQL NULL apart from an empty value, because a nil slice and an empty one are different rows:
tags := lagoon.Jsonable[[]string]{Data: []string{"go", "cms"}, Valid: true}
v, _ := tags.Value()
fmt.Println(v)
var none lagoon.Jsonable[[]string] // Valid false stores SQL NULL
v, _ = none.Value()
fmt.Println(v)
var read lagoon.Jsonable[[]string]
_ = read.Scan(`["winter"]`)
fmt.Println(read.Get(), read.Valid)
// Output:
// ["go","cms"]
// <nil>
// [winter] true
Set NullOnEmpty on a slice or map column that should store NULL rather than [] or {} when it is empty. Pick the behaviour the PHP table already has.
Encrypted columns#
lagoon.Encrypted is the encrypted cast. It stores AES-256-GCM ciphertext under a key derived from app.key, and decrypts values written under any key in app.previous_keys, so you can rotate the key without rewriting every row at once. The plaintext has one accessor, lagoon.Encrypted.Reveal; printing the value or marshalling it to JSON always gives [redacted]:
// The application publishes the keys from app.key at boot; a test can
// install a key directly.
key := []byte("0123456789abcdef0123456789abcdef")
if err := lagoon.PublishEncryptionKeys(nil, key, nil); err != nil {
fmt.Println(err)
return
}
token := lagoon.NewEncrypted("s3cret")
stored, _ := token.Value() // what the column holds
fmt.Println(strings.Contains(fmt.Sprint(stored), "s3cret"))
var read lagoon.Encrypted
if err := read.Scan(stored); err != nil {
fmt.Println(err)
return
}
out, _ := json.Marshal(map[string]any{"api_token": read})
fmt.Println(read, string(out))
fmt.Println(read.Reveal())
// Output:
// false
// [redacted] {"api_token":"[redacted]"}
// s3cret
The application loads the keys once at boot (lagoon.OpenFromApp calls lagoon.LoadAppKey and lagoon.PublishEncryptionKeys). A missing or short app.key stops the application with an error; there is no default key. Generate one with key:generate, which prints a key and writes nothing:
./bin/acme key:generate
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.
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:
rules := map[string]string{
"title": "required|max:10",
"views": "nullable|integer|max:1000",
}
input := map[string]any{"title": "", "views": 5000}
// A nil translator gives the built-in English messages; unique: rules
// need a database handle instead of nil.
errs, err := lagoon.Validate(context.Background(), nil, &Post{}, rules, input, nil)
if err != nil {
fmt.Println(err)
}
out, _ := json.Marshal(errs)
fmt.Println(string(out))
// Output:
// {"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.
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.
A handler validates before it fills and saves the model, as the create example on Models shows. Answer a non-nil error map with status 422 and the body shape the endpoint's existing clients expect.