Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 22:41
· 150 commits to main since this release

Added

  • JSON fields. tango:"json" on a string field declares it holds JSON text (model.FieldMeta.JSON). Store.Create and Store.Update refuse text that is not valid JSON with a *db.InvalidJSONError that matches db.ErrInvalidJSON and never contains the submitted text; the empty string is unset, null is valid, and the stored text is exactly what was given. It is ordinary TEXT on SQLite and PostgreSQL, so adding the tag to an existing field generates no migration and does not scan existing rows; reads are not checked. json cannot be combined with varchar=n, text, pk, fk, unique or index. See ADR 0059.

  • JSON fields in the admin. A tango:"json" field gets admin.JSON() by default (Edit and Formatted tabs) with no Options.Widgets entry; an explicit entry still wins, and read-only and changelist rendering are unchanged. A save the store refuses for invalid JSON, including through a non-JSON widget such as admin.Textarea(), re-renders with a 422 and a translatable field error (admin.error.invalid_json, "Not valid JSON.") beside the field, keeping the text as typed. See admin registration.

  • Admin refinement. admin.JSON() edits a string field as JSON (syntax checked on save, the submitted text stored exactly, a browser-side Formatted tab). admin.Markdown(...) edits Markdown in a monospace textarea with a character count and, when you pass admin.WithMarkdownPreview, a sandboxed Preview tab from your own renderer: tanGO ships no parser or sanitizer, and you decide how HTML is sanitized (ADR 0058). Options.Filters adds boolean and foreign-key filters to the changelist (they combine with search and ordering; an over-large foreign-key filter still applies a primary key given in the URL). Options.Display with admin.DisplayFunc and admin.Display renders computed changelist cells, such as a file's size and an authorized download link, without admin knowing about storage; admin.ByteSize and admin.ShortHash help. admin.FieldError lets a widget report an error beside its own field. Forms now render HelpText and mark invalid fields with aria-invalid and aria-describedby; the changelist gets a labelled search box and filters, a caption and column scopes. Options.Labels now also names a field's changelist column when set. See admin registration and ADRs 0057 and 0058.

  • Bounded strings. tango:"varchar=n" declares a string field of at most n characters (1 to 10,485,760), and tango:"text" states an unbounded one explicitly; a bare string stays unbounded text and generates no migration. model.FieldMeta.MaxLength carries the limit and a malformed tag fails Register with model.ErrInvalidFieldTag. Store.Create and Store.Update refuse a longer value before any SQL with a *db.ValueTooLongError (db.ErrValueTooLong), counting runes the way PostgreSQL counts characters; SQLite does not enforce the length and raw SQL is not checked. tango makemigrations writes a VARCHAR(n) column (Column.Length, additive, with no CHECK), generates the widening changes (varchar(n) to text or a larger varchar) and the narrowing ones (text to varchar(n) or a smaller varchar), and a narrowing checks the existing data in Go first, failing with the number of rows and one offending key instead of truncating anything. A migration of narrowings is reversible; a widening makes its migration irreversible. The admin renders maxlength and shows a translatable per-field message (admin.error.too_long) for a refused save, and admin.FieldContext gains MaxLength. Built-in models are not retagged. See models and tags and changing a field's type.

  • Accounts for a single-page app. accounts.WithJSON mounts six bearer-only JSON endpoints under /accounts/api/ (register, login, password reset request and confirm, verify, resend verification) for a separate client, sharing the HTML flow's rules, limiters and outbox. The host supplies the token side through accounts.JSONAuth (Issue, Authenticate) and AuthGrant, so accounts imports no token package; JSON mode never reads or sets a cookie and has no logout. JSONConfig also takes the emailed link templates VerifyURL and ResetURL ({token} once; prefer a fragment), an OnRegister hook that runs inside the account's transaction with a Registration (a FieldError or *db.ValueTooLongError answers 422), and ResolveIdentifier so login can take a username. Requests are strict (415, 413, 400 invalid_body), errors are {"error", "code", "fields"?}, and every response is no-store. New db.Store.InTx runs a callback in one transaction (commit, rollback on error, rollback and re-panic on panic; nested calls join). See the accounts guide, the new examples/spa-accounts and ADRs 0050 and 0051.

  • Accounts answers invalid JSON as a field error. In the JSON accounts API, a *db.InvalidJSONError from a registration hook's tx.Create (alone or joined with errors.Join) answers 422 invalid_field, keyed by the Go field name with the message must be valid JSON, instead of a generic 500; the document is never echoed. See the accounts guide.

  • File uploads and storage. The new storage package is an explicit object store for uploaded files, used from ordinary Views: Put streams a file in, always sniffs its content type, enforces a required size cap and an optional allowed-types list, and returns a generated opaque key with the size and SHA-256; Stat, Open (a byte range) and Delete complete the Store. There is no FileField, no file model, no listing and no signed URLs: the host keeps the key and metadata in its own model and authorizes every access. storage.Upload reads one multipart file with bounded memory, and storage.Serve delivers an authorized object with ranges, conditional requests, attachment by default and nosniff. Backends: storage/local (atomic, os.Root-contained, for development and one host), storage/s3 (Amazon S3, Cloudflare R2 and other S3-compatible providers, in its own Go module so no cloud SDK reaches your go.mod), and storagetest with an in-memory Store and the conformance suite every Store passes. See the uploads guide, the new examples/uploads and ADRs 0052 to 0054. Native Google Cloud Storage is a later release.

  • Caching. The new cache package is a small, explicit cache for recomputable reads: a bytes-only cache.Store (Get, Set with a TTL of up to 30 days, Delete) where a miss is not an error, one portable key rule, cache.Prefix for namespaces and bulk invalidation by version, typed JSON helpers (GetJSON, SetJSON, ErrCorrupt), and the fail-open FetchJSON read-through that reports cache failures to an OnError hook and never fails the request because the cache is down. cache/local is a bounded in-process store (entry limit, optional byte budget, least-recently-used eviction); cache/redis (Redis and Valkey) and cache/memcache are their own Go modules, so no client library reaches your go.mod. cachetest.Run is the conformance suite every backend passes. There is no response, fragment or ORM caching, no automatic invalidation, and nothing logs. See the caching guide, the new examples/cache and ADRs 0055 and 0056.

Changed

  • An admin changelist ?page= beyond the last page now shows the last page, so the rows and the pager always describe the same page (before, it showed an empty list under a pager that named the last page).

  • The admin foreign key select now starts with a "— None —" choice (translation key admin.fk.none), and an unset foreign key shows an em dash in the changelist instead of 0. Saving "None" stores the same unset value (NULL) as before.

  • POST /admin/logout/ now requires the session's CSRF token when the request carries a session cookie, so a cross-site form cannot sign an admin out. A missing or wrong token gets 403 and leaves the session in place. A request without a session cookie still redirects to the login page.

  • The admin sidebar footer no longer says "Signed in over Basic Auth" (admin has used session logins since 0.3). It now shows "Signed in as " with a Log out button that posts to /admin/logout/ with the session's CSRF token. The translation key admin.layout.signed_in is replaced by admin.layout.signed_in_as and admin.layout.logout.

  • /accounts/login/ now always spends one password comparison, even for an unknown or inactive account, so its timing no longer says whether an account exists. Nothing else about the HTML flow changes.