Upgrading to v0.5
v0.5 is in progress; this page lists its changes so far.
Services are built with New
The services an app builds from its settings in setup are now made by
their package’s New, the word Go uses for it; ForApp is gone from the
docs and stays only as a deprecated name until v0.6. db.Connect and
redis.Connect keep their names: they connect to a server.
| v0.4 | v0.5 |
|---|---|
cache.ForApp(app), queue.ForApp(app), session.ForApp(app)… | cache.New(app), queue.New(app), session.New(app)… |
openapi.ForApp(app, srv, cfg) | openapi.Register(app, srv, cfg) |
With Go 1.26 or later, go fix ./... rewrites these calls for you:
go fix ./...It leaves auth.ForApp and social.ForApp alone (Go’s inliner doesn’t
yet rewrite calls to generic functions): change those two to auth.New
and social.New by hand.
New was already the name of each package’s constructor from explicit
parts, without the app, which apps mostly use in tests. Those are
renamed, and go fix can’t rewrite them: the compiler points at each
call.
| v0.4 | v0.5 |
|---|---|
cache.New(store, prefix) | cache.NewWithStore(store, prefix) |
queue.New(store, cfg, …) | queue.NewWithStore(store, cfg, …) |
pubsub.New(broker, …) | pubsub.NewWithBroker(broker, …) |
mailer.New(transport, …) | mailer.NewWithTransport(transport, …) |
storage.New(disk, …) | storage.NewWithDisks(disk, …) |
ai.New(provider, …) | ai.NewWithProvider(provider, …) |
events.New(…) | events.NewBus(…) |
schedule.New(…) | schedule.NewScheduler(…) |
i18n.New(cfg, …) | i18n.NewTranslator(cfg, …) |
rbac.New(perms, roles…) | rbac.NewRegistry(perms, roles…) |
encryption.New(key, previous…) | encryption.NewEncrypter(key, previous…) |
auth.New(cfg, users, enc, …) | auth.NewWithConfig(cfg, users, enc, …) |
social.New(a, resolve, url, creds, providers, …) | social.NewWithConfig(…) |
session.New() (an empty session, in tests) | session.NewSession() |
go fix needs code that compiles, tests included: rename the calls
go vet ./... reports first, then run go fix ./.... A project made by anetos new
calls only ForApp, so go fix and the two auth/social edits do
it.
A second New (or db.Connect) for the same app is now an error
(cache: New called twice for one app) where some packages used to open
a second connection or replace the first service. Code that needs a
service built elsewhere asks the app for it:
anetos.Resolve[*cache.Cache](app). encryption.New(app) returns the
same encrypter every time.
Settings with one name per idea
Every setting now starts with its area, and the one that picks a
backend is <AREA>_DRIVER. The former names are still read, with a
warning at startup and in go run . doctor, until v0.6:
| Before v0.5 | Since |
|---|---|
DB_CONNECTION | DB_DRIVER |
DB_DATABASE | DB_NAME |
DB_USERNAME | DB_USER |
MIGRATE_ON_RUN | DB_MIGRATE_ON_START |
MIGRATE_READINESS | DB_MIGRATE_READINESS |
SEARCH_LANGUAGE, SEARCH_RANKING | DB_SEARCH_LANGUAGE, DB_SEARCH_RANKING |
CACHE_STORE | CACHE_DRIVER |
SESSION_LIFETIME, SESSION_MAX_LIFETIME | SESSION_TTL, SESSION_MAX_TTL |
AUTH_REMEMBER_LIFETIME | AUTH_REMEMBER_TTL |
QUEUE_POLL | QUEUE_POLL_INTERVAL |
LOCALE_URL | APP_LOCALE_STRATEGY |
Rename them in your .env files, the server’s environment and the
deploy files (deploy/production.env.example, the systemd unit, the
Dockerfile, a Render or Fly file). In the project’s folder:
for f in .env .env.example .env.testing Dockerfile deploy/*; do
[ -f "$f" ] && sed -i.bak \
-e 's/\bDB_CONNECTION\b/DB_DRIVER/g' -e 's/\bDB_DATABASE\b/DB_NAME/g' \
-e 's/\bDB_USERNAME\b/DB_USER/g' -e 's/\bCACHE_STORE\b/CACHE_DRIVER/g' \
-e 's/\bMIGRATE_ON_RUN\b/DB_MIGRATE_ON_START/g' \
-e 's/\bMIGRATE_READINESS\b/DB_MIGRATE_READINESS/g' \
-e 's/\bSEARCH_LANGUAGE\b/DB_SEARCH_LANGUAGE/g' \
-e 's/\bSEARCH_RANKING\b/DB_SEARCH_RANKING/g' \
-e 's/\bSESSION_MAX_LIFETIME\b/SESSION_MAX_TTL/g' \
-e 's/\bSESSION_LIFETIME\b/SESSION_TTL/g' \
-e 's/\bAUTH_REMEMBER_LIFETIME\b/AUTH_REMEMBER_TTL/g' \
-e 's/\bQUEUE_POLL\b/QUEUE_POLL_INTERVAL/g' \
-e 's/\bLOCALE_URL\b/APP_LOCALE_STRATEGY/g' "$f"
done(\b needs GNU sed; on macOS, brew install gnu-sed and use gsed,
or rename them in your editor. _ counts as a letter for \b, so
QUEUE_POLL_INTERVAL and DB_SEARCH_LANGUAGE aren’t renamed twice.) The
image’s HTTP_ADDR=:8080 line can go too: the port is the default, and
a platform’s PORT is used when HTTP_ADDR isn’t set. The
fields of the config structs follow: db.Config’s Connection,
Database and Username are Driver, Name and User;
cache.Config.Store is Driver; session.Config’s Lifetime and
MaxLifetime are TTL and MaxTTL; auth.Config.RememberLifetime is
RememberTTL; queue.Config.Poll is PollInterval;
migrate.Config.OnRun is OnStart; i18n.Config.URL is Strategy, and
i18n.URLNone, URLPrefix and URLSubdomain are StrategyNone,
StrategyPrefix and StrategySubdomain (go fix rewrites those). Apps
rarely set these fields: the compiler points at any that do.
go run . doctor also points at a key in your .env files that is
another framework’s name for a setting (QUEUE_CONNECTION,
MAIL_MAILER), a former name left beside its new one, or a typo of a
setting.
app.Source() now returns the app’s own source, which records the keys
read: code that type-asserted it (to a config.Map, in a test) should
pass the map to anetos.WithSource and keep it instead.
Logging in
The framework says “log in” and “log out” everywhere now, in its API,
the pages make:auth writes and the messages, where it used to say
“sign in” in some places. The old names stay, deprecated, until v0.6.
| v0.4 | v0.5 |
|---|---|
a.SignIn(ctx, u, remember) | a.Login(ctx, u, remember) (see below) |
a.SignOutOthers(ctx, u), a.SignOutEverywhere(ctx, u) | a.LogoutOthers(ctx, u), a.LogoutEverywhere(ctx, u) |
a.CanTwoFactor(), a.CanRemember(), a.CanSignOutEverywhere() | a.SupportsTwoFactor(), a.SupportsRemember(), a.SupportsLogoutEverywhere() |
a.ActAs(ctx, id), auth.ActAs(ctx, id), auth.ActOption | a.WithUser(ctx, id), auth.WithUser(ctx, id), auth.UserOption |
auth.ErrNoPendingSignIn, auth.ErrNoUser | auth.ErrNoPendingLogin, auth.ErrUserNotFound |
*social.ErrNoAccount | *social.NoAccountError |
app.SocialSignIn(redirect, account) (anetostest) | app.SocialLogin(redirect, account) |
go fix ./... rewrites auth.ActAs, auth.ActOption,
social.ErrNoAccount and app.SocialSignIn. It doesn’t rewrite the
methods of *auth.Auth[U] (a generic type, which Go’s inliner leaves
alone so far) or the error variables; the linters flag them. In the
project’s folder:
grep -rlE --include='*.go' 'SignIn\(|SignOut(Others|Everywhere)|Can(TwoFactor|Remember|SignOutEverywhere)\(|\.ActAs\(|ErrNoPendingSignIn|ErrNoUser\b' . |
xargs -r sed -i.bak -E \
-e 's/\.SignIn\(/.Login(/g' \
-e 's/\.CanSignOutEverywhere\(/.SupportsLogoutEverywhere(/g' \
-e 's/\.SignOut(Others|Everywhere)\(/.Logout\1(/g' \
-e 's/\.Can(TwoFactor|Remember)\(/.Supports\1(/g' -e 's/\.ActAs\(/.WithUser(/g' \
-e 's/ErrNoPendingSignIn/ErrNoPendingLogin/g' -e 's/ErrNoUser\b/ErrUserNotFound/g'
find . -name '*.go.bak' -delete # once the diff looks right(GNU sed and xargs; on macOS, gsed and gxargs.) Its \.SignIn\( also
matches a method of your own called SignIn: check the diff.
Login now asks for the two-factor code. For a user with two-factor
authentication on, a.Login returns auth.ErrTwoFactorRequired and the
login waits for a code at AUTH_CHALLENGE_URL, as SignIn did; it used
to log them in without one. The pages make:auth wrote call Login
only after registration, when no one has the code on, so they behave as
before. If your code calls Login for a user who may have it on, handle
the error as the login page does, or send them to the challenge page. A
user without a password who logs in through Login counts as having
confirmed their password lately, as with SignIn.
The admin says “Impersonate” where it said “Act as user”, and
“Log out everywhere” where it said “Sign out everywhere”; that action’s
URL is …/actions/logout (was …/actions/sign-out), and the audit log
records it as user.logged_out_everywhere (was
user.signed_out_everywhere): search for both in older entries.
Messages. Errors that tests compare by text say “log in” and
“impersonate” too: auth: not logged in (ErrUnauthenticated, was auth: not signed in), auth: user not found (was auth: no such user),
auth: no login waits for a code, auth: two-factor authentication is on already and isn't on (ErrTwoFactorOn, ErrTwoFactorOff), auth: not impersonating another user (ErrNotImpersonating). The framework’s messages (i18n/locales/en.yaml, the
social_* errors) say “log in”. In a project, make:auth’s pages and
locales/*/auth.yaml are yours: they keep working as they are. To match
v0.5’s pages and the translations of
anetos.dev/locales, whose keys
changed, rename three keys, in the YAML files and the code that uses
them:
grep -rlE 'sign_in_(with|again|first)' app views locales |
xargs -r sed -i.bak -E 's/sign_in_(with|again|first)/login_\1/g'
find app views locales -name '*.bak' -delete # once the diff looks right(locales/ is embedded whole, .bak files included, so delete them.)
and replace “Sign in”, “sign in” and “two-factor sign-in” in your pages
and locales/en/auth.yaml as you like. An API project’s
handlers.SignInResponse is LoginResponse in new projects; yours can
keep its name.
Commands and the project
Command names. The app’s and the anetos tool’s commands use one
form, <group>:<action> with a singular group, and an existing tool’s
name where one has the command (route:list, queue:work). The old names still run, with a warning, until
v0.6: change them in scripts, CI and deploy files. (gen runs without
one: names may now be shortened while they stay unique, and gen is
generate shortened.)
| Before v0.5 | Since |
|---|---|
./app routes:list | ./app route:list |
./app plugins:list, ./app plugins:env | ./app plugin:list, ./app plugin:env |
./app lang:check | ./app locale:check |
./app schedule:run <task> | ./app schedule:test <task> |
go tool anetos gen [-check] | go tool anetos generate [--check] |
go tool anetos lang:add, add lang | go tool anetos locale:add |
go tool anetos make:admin:resource | go tool anetos make:admin-resource |
schedule:run without a task is new: it runs the tasks due this minute,
once, for a crontab entry or a Kubernetes CronJob in place of a
long-running scheduler. queue:work and schedule:work are new names
for run --only=worker and run --only=scheduler. An app whose own
command has one of the new names (queue:work, schedule:test,
schedule:work, or a former name such as routes:list) now fails to
register it: rename yours.
anetos generate writes Code generated by anetos generate at the top
of models_gen.go: run it once after upgrading the tool, or
generate --check (in CI) reports the files as out of date. Change
//go:generate go tool anetos gen in main.go to generate (the old
line still works).
key:generate writes .env. It sets APP_KEY there when it is
missing or empty, and prints what it did on stderr, so the old
go tool anetos key:generate >> .env still works. A key that is set is
kept unless --force, which moves it to APP_PREVIOUS_KEYS. To print a
key for a server’s environment, use --show:
APP_KEY=$(go tool anetos key:generate --show | cut -d= -f2-).
Route names. make:crud names a resource’s routes after its
handlers: posts.create and posts.delete, where it wrote
posts.store and posts.destroy, and make:auth names the token
routes tokens.create and tokens.delete. Routes you generated keep
their names: they are your code. Rename them only with every
web.URL, web.MustURL and web.RouteIs call that uses them; in an
API project, the route name is the OpenAPI operation ID, which clients
generated from openapi.json may use. The admin’s own routes changed
the same way: its form is admin.<resource>.new (it was .create),
saving is .create (was .store), deleting .delete (was .destroy),
and its password confirmation’s form posts to admin.confirm.check
(was admin.confirm.store). A link of yours to admin.<resource>.create
still resolves, to the list’s URL: point it at .new.
make:auth’s group of logged-in routes is loggedIn in new
projects, in both stacks (it was members in a web project and me in
an API one). Yours keeps its name; the guides say loggedIn.
public/ is the web root. New web projects serve the files of
public/ at / (public/robots.txt at /robots.txt,
.well-known/ too), after the routes; public/static/ stays at
/assets/. In a project made before v0.5, embed the folder and add the
route:
// illustrative
// public/public.go: replace "//go:embed static" and "var files embed.FS" with
//go:embed *
var Files embed.FS
// and use Files where it used files (fs.Sub(Files, "static")).
// routes/web.go, after the /assets route:
r.Static("/", public.Files)//go:embed * embeds every file of public/, the top-level dotfiles
too (not served), and fails to build when a folder of it holds only
hidden files (a uploads/.gitkeep): give it a file, or keep it out of
public/.
Commands that describe the app. Every app has about and
security:check, and migrate.New adds db:schema: an app’s own
command by one of these names is now refused at startup (“taken”); rename it. route:list,
migrate:status, schedule:list, plugin:list and help take
--json. web.RouteInfo.Middleware now has an entry for every
middleware of the route, each with its Name, not only those that
called web.Documented: code that counted its entries, or read the
first one as the first documented middleware, filters on Security or
Responses instead. web.RouteInfo also has File, Line and
Public: a test that compares Routes() with reflect.DeepEqual
clears File and Line first. anetos.Finding has a Rule, so
unkeyed literals (anetos.Finding{anetos.Warning, "…"}, which go vet
warns about) need the field names.
Names across the API
The rest of the API now uses the words Go and web developers already
know. The old names stay, deprecated, until v0.6, and go fix ./... (Go 1.26 or
later) rewrites the calls to them:
| Before v0.5 | Since |
|---|---|
auth.DefaultHomeURL, mailer.BaseURL, mailer.DefaultFrom, i18n.WarnMissing | auth.WithDefaultHomeURL, mailer.WithAppURL, mailer.WithDefaultFrom, i18n.WithWarnMissing |
storage.BaseURL, storage.Public, storage.SignWith | storage.WithBaseURL, storage.WithPublic, storage.WithSigner |
admin.Title, admin.UserName | admin.WithDefaultTitle, admin.WithUserName |
web.TrustedOrigins, gcs.ClientOptions, s3.Transport | web.WithTrustedOrigins, gcs.WithClientOptions, s3.WithTransport |
postmark.BaseURL, HTTPClient, Stream | postmark.WithBaseURL, WithHTTPClient, WithStream |
events.BusOption, audit.Option | events.Option, audit.TrackOption |
session.NewContext, web.RouteFromContext, web.ClientIPFrom | session.WithSession, web.CurrentRoute, web.ClientIP |
anetos.Unit, UnitFunc, app.StartUnit, AroundUnits, HasAroundUnits | anetos.Operation, OperationFunc, app.StartOperation, AroundOperations, HasAroundOperations |
anetos.Roles, app.Supervisor().Roles() | anetos.ProcessTypes, ProcessTypes() |
db.Untracked, db.TxWith, db.PruneTrashed, page.HasMore() | db.AllowRepeatedQueries, db.TxWithOptions, db.Prunable, page.HasNext() |
t.UniqueLive(…), .UniqueLive() (migrations) | UniqueWithoutTrashed |
pubsub.MaxAttempts, cache.Forget | pubsub.Tries, cache.Delete |
c.Events(), web.View(…), s.String(key) (a session) | c.EventStream(), web.Render(…), s.GetString(key) |
r.HandleStd(method, pattern, h) | r.Get(pattern, web.WrapHandler(h)) (go fix writes r.Handle(method, pattern, web.WrapHandler(h))) |
ai.Func(name, description, fn) | ai.NewTool(name, description, fn) |
admin.Field[T](title, column) | admin.TextColumn[T](column, label): the arguments the other way round: the column first, then its label |
go fix leaves calls whose type arguments Go infers, ai.Func(…) and
admin.UserName(fn), and page.HasMore() (a method of a generic
type): rename those by hand. The validation rule unique_live is
unique_without_trashed: change it in your validate tags (the old
name works, with a warning, until v0.6), and rename a translation of
its message (validation.unique_live) with it.
Interfaces you implement. A user model’s
CommunicationLocale() string (i18n.CommunicationPreference) is
PreferredMailLocale() string (i18n.MailLocalePreference), as
PreferredLocale and PreferredTimeZone; a storage backend’s
SignedURL is TemporaryURL (storage.TemporaryURLBackend), as
Disk.TemporaryURL, and storagetest.Features.SignedURLs is
TemporaryURLs. The old methods are still used, with a warning, until
v0.6: rename them.
Process types. What run --only selects is a process type, the
word of Heroku’s Procfile; “role” is left to roles and permissions.
Three changed names:
| Before v0.5 | Since |
|---|---|
run --only=http | run --only=web (serve is unchanged) |
run --only=workers | run --only=worker |
run --only=listeners | run --only=listener |
scheduler stays. The old names still work, with a warning in the log,
until v0.6: change them in your Dockerfile, fly.toml, Procfile,
Compose and Kubernetes files, and your own components'
anetos.ProcessTypes("workers"). The supervisor’s log line says
process_types= (it said roles=).
Changes the compiler points at. These have no deprecated copy:
web.ClientIP(r)takes the request’s context:web.ClientIP(r.Context()), orweb.ClientIP(c)in a handler. Outside aweb.Router(a handler of your own wrapping it), it returns "" withoutRealIP: user.RemoteAddrthere.storage.From(ctx)returns the default disk; a named one isstorage.DiskFrom(ctx, "avatars")(wasstorage.From(ctx, "avatars")).- An admin resource’s fields take the usual admin-panel names:
PluralLabel(wasTitle),Label(wasSingular) andRecordTitle(was the functionLabel); a column’s, action’s, bulk action’s, filter’s and dashboard link’sTitleisLabel. db.Chunk.ModelisEmbeddingModel,db.RepeatedQuery.UnitisOperation,schedule.Config.TimezoneisTimeZone, andsupervisor.Spec.RolesandComponentStatus.RolesareProcessTypes.gcppubsub.Driver(opts...)takes its own options:gcppubsub.Driver(gcppubsub.WithClientOptions(opts...)). Ans3.Optionno longer changes the MinIO client’s options:s3.WithTransportis the one there is.- If you register a listener’s options in a variable typed
events.Option, it is nowevents.ListenOption.
The repeated-query warning’s attribute is operation= (it was
unit=): update log queries that look for it.
Shapes that changed
These changes have no deprecated copy: the compiler points at each.
| Before v0.5 | Since |
|---|---|
ai.Embed(ctx, 0, texts...), ai.Embed(ctx, 768, texts...) | ai.Embed(ctx, texts), ai.Embed(ctx, texts, ai.Dimensions(768)) |
ai.EmbedQuery(ctx, dims, text) | ai.EmbedQuery(ctx, text) (with ai.Dimensions(dims) if it wasn’t 0) |
ai.ConstraintKeywords | ai.ConstraintKeywords() |
d.Watch(table, w, n) | d.Watch(table, w, db.WatchBulkValues(n)) |
ai.Request.Options (any, in a provider of your own) | a []any: range over it and take the values of your types |
ai.Embed and EmbedQuery now apply ai.Using, ai.ForUser and
ai.Timeout, which they ignored, so AI_TIMEOUT bounds each request
for embeddings too. In tests, app.AI().Embeddings() is
EmbedRequests() (go fix rewrites it). ai.ProviderOptions adds to the
call’s options rather than replacing them, so the options of several
drivers can travel together. In the gemini driver, Options.Config is
Params (the old field works until v0.6).
Behaviour. r.As("api.") returns a router of its own, as Group
does, and leaves r’s route names alone: code that called As on a
router and then registered on that same router (not on As’s result)
must use the result. HTTPError.Wrap returns a copy instead of
changing the error (and the copy still matches it with errors.Is).
A plugin’s Listen(bus) is Listeners(bus) (Listen is still called,
with a warning, until v0.6, but a var _ ext.HasListeners = (*Plugin)(nil) check stops compiling until you rename it). A
web.CSRFOption of your own, written against
*http.CrossOriginProtection, no longer compiles: use
web.WithTrustedOrigins.
Storage backends of your own with temporary URLs
(storage.TemporaryURLBackend) must have their store serve HTML, SVG
and JavaScript as attachments (Content-Disposition: attachment, set
when Put stores such a file), as the s3 and gcs drivers do;
storagetest with TemporaryURLs now checks it.
Removed (nothing an app was meant to use): db.WithTestTx,
db.SQLiteTimeFormat, db.SearchIndexesTable, db.Plural,
migrate.Runner.Command and migrate.Commands (run ./app migrate
and the other commands through app.Execute), the database drivers'
DSN, storage.DiskSource, storage.AppConfig, ai.SearchResult,
ext.Info and postmark.Event. db.Dialect can no longer be
implemented outside package db.
Generated pages call views/ui
Since v0.5, anetos new writes views/ui, a package of components
(ui.Card, ui.Field, ui.Button…) that carries the markup and the
CSS framework’s classes, and the layout and the pages that make:crud and
make:auth write call it instead of writing class names (Style your
app).
A project made before v0.5 has no views/ui, and nothing changes
until a generator runs. The first make:crud or make:auth writes
views/ui before its pages, and says so: the starter theme’s
components when public/static/app.css has the theme’s .card rule,
else plain HTML without classes. Your stylesheet, layout and other
pages are left as they are, and the generators still find the header’s
nav (</nav>) to add their links.
To build the layout from the components too (optional), compare
views/layout.templ with a new project’s (anetos new in another
folder):
- In
<head>, replace the stylesheet’s<link>with@ui.Head(). - Replace
<header class="topbar">…</header>with@ui.Header("Name", web.MustURL(ctx, "home")) { … }, its nav with@ui.Nav(label) { … }, and keep@AccountMenu()after the nav’s block. - Make
navLinkcall@ui.NavLink(web.MustURL(ctx, route), label, web.RouteIs(…)). - Replace
<main class="container">with@ui.Main() { … }, the flash’s<p class="flash">with@ui.Flash(ui.Success) { … }, and the footer with@ui.Footer() { … }.
The theme’s stylesheet from a new project styles .flash neutral and
adds .flash.success and .info: copy it too, or add those rules, if
your layout or pages use ui.Flash.
The generator also writes views/ui/css.json, which records the
components it wrote. Once the layout calls the components,
anetos css:use <framework> can switch the project to another CSS framework (Style
your app); it asks for --force
the first time, since your app.css isn’t the one anetos wrote. It adds the menu
button’s name, nav.menu, to locales/en/app.yaml.
–css=none writes plain HTML
anetos new --css=none used to keep the starter theme’s class names in
the markup, with an empty stylesheet. Its views/ui now writes plain
HTML without classes: add your own classes in views/ui’s components.
Projects made before are unchanged.