contracts

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package contracts is everything another module, an app or a test may know about a tenant's public site: the settings, the event, the permission and the Service interface. The implementation is in ../internal.

This module is the data a public site is made of and none of the rendering. A theme reads a title, a navigation and a colour and decides what to do with them; E4's admin shell and whatever public theme follows it are the ones that turn this into HTML. Keeping the two apart is what lets a deployment replace the theme without touching what a tenant configured, and it is why there is no template anywhere in this module.

Index

Constants

View Source
const (
	ThemeLight  = "light"
	ThemeDark   = "dark"
	ThemeSystem = "system"
)

The three themes. system is the absence of a choice, which is why an empty string means it: a tenant that has said nothing has not chosen dark.

View Source
const (
	MaxNav      = 12
	MaxLabel    = 40
	MaxPath     = 200
	MaxTitle    = 120
	MaxTagline  = 200
	MaxHomeSlug = 200
)

The bounds. A navigation with more than a dozen links is not a navigation, and a label longer than a few words is a sentence in a menu.

View Source
const DefaultPrimaryColor = "#2563eb"

DefaultPrimaryColor is the colour a site that has chosen none is rendered in. It is here rather than in a theme because a theme may be replaced and this is what a tenant's settings say when they say nothing.

View Source
const EventSettingsUpdated = "site.settings_updated"

The one event this module emits. There is no created and no deleted: the settings of a tenant are not created — every tenant has some from the moment it exists — and they are not deleted either, because a site without settings is a site that cannot render.

View Source
const PermissionSiteManage = "site:manage"

The one permission a site has. There is no site:read: reading the settings is reading how the site is configured, which is the same act as configuring it, and the half a visitor needs is the public route — which asks for nothing because publishing a site is deciding that anybody may see it.

The list the manifest declares is in ../module.go, which keeps kit/module out of this package's build graph.

Variables

Events is every event this module emits, for the manifest.

Functions

This section is empty.

Types

type Nav []NavItem

Nav is the navigation, one jsonb column. It is a named type so the codec is written once, the way modules/user spells Roles and modules/billing spells Features; this one is JSON rather than a Postgres array because an item has two fields.

func (n *Nav) Scan(src any) error
func (n Nav) Value() (driver.Value, error)
type NavItem struct {
	Label string `json:"label" doc:"What the link says" example:"About"`
	// Path is a path within the site and not a URL: a navigation that could
	// carry an absolute one is a navigation somebody can use to send a tenant's
	// visitors somewhere else.
	Path string `json:"path" doc:"Path within this site" example:"/about-us"`
}

NavItem is one link in a public site's navigation.

type Public

type Public struct {
	Title string `json:"title"`
	Nav   Nav    `json:"nav"`
	Theme string `json:"theme"`
}

Public is what an anonymous visitor may read: the name, the navigation and the colour scheme. The rest — the home slug, the logo, the timestamps — is either an internal reference or nobody's business, and a public response that carried the whole row would be an admin screen anybody could read.

type Service

type Service interface {
	// Settings is what this tenant has configured, and the defaults when it has
	// configured nothing. It never reports "not found": every tenant has a
	// site, whether or not anybody has saved anything about it.
	Settings(ctx context.Context, tx db.Tx[db.Tenant]) (*SiteSettings, error)

	// Save writes the settings and publishes site.settings_updated. Saving what
	// is already stored changes nothing and says nothing, so a screen that
	// submits its form twice does not invalidate a cache twice.
	Save(ctx context.Context, tx db.Tx[db.Tenant], in *SiteSettings) (*SiteSettings, error)
}

Service is the tenant's site settings: one read and one write, because a singleton has no list, no create and no delete.

Both take the caller's transaction rather than opening one, so the change and its event commit together. The error a caller can act on is crud.ErrInvalid, from the entity's own Validate.

type SettingsUpdated

type SettingsUpdated struct {
	SettingsID uuid.UUID `json:"settingsId"`
	Title      string    `json:"title,omitempty"`
	HomeSlug   string    `json:"homeSlug,omitempty"`
	Theme      string    `json:"theme"`
	At         time.Time `json:"at"`
}

SettingsUpdated is the payload: what the site is now. It carries the values a cache would key on rather than only an id, because the subscriber this exists for is whatever renders the public site, and it should not have to read the row back to know the title changed.

type SiteSettings

type SiteSettings struct {
	crud.Base

	// Title is the site's name, and Tagline the line under it.
	Title   string `json:"title,omitempty" gorm:"type:varchar(120);not null;default:''" maxLength:"120" doc:"The site's name" example:"Acme"`
	Tagline string `` /* 140-byte string literal not displayed */

	// HomeSlug is the content served at the site's root. It is a slug and not
	// an id because a page can be rewritten and replaced and still be the home
	// page; it is empty until somebody chooses one.
	HomeSlug string `` /* 142-byte string literal not displayed */

	// Theme and PrimaryColor are the whole of what a tenant may say about how
	// their site looks. A theme that needed more would be a theme with a
	// stylesheet editor in it.
	Theme        string `` /* 195-byte string literal not displayed */
	PrimaryColor string `` /* 175-byte string literal not displayed */

	// LogoFileID is a file id with no foreign key behind it, which is what
	// "cross-module dependencies are Go interfaces" costs at the database: this
	// module never names modules/file, and a logo that has been deleted is a
	// site that renders without one.
	LogoFileID *uuid.UUID `json:"logoFileId,omitempty" gorm:"type:uuid" format:"uuid" doc:"File id of the logo"`

	// Nav is the navigation, in the order it is shown. There is no Order field
	// on an item for the same reason module.NavEntry has none: the order is the
	// order somebody wrote them in.
	Nav Nav `json:"nav,omitempty" gorm:"type:jsonb;not null;default:'[]'" required:"false" doc:"The site's navigation, in order"`
}

SiteSettings is one tenant's public site, and there is one row or none: migrations/000018 says so with a unique index on the tenant. A tenant that has never saved any still has settings — the zero value below, which is what Service.Settings answers with.

func (SiteSettings) TableName

func (SiteSettings) TableName() string

TableName pins the table, so the entity and migrations/000018 agree.

func (*SiteSettings) Validate

func (s *SiteSettings) Validate(context.Context) error

Validate is the entity's own check, run by kit/crud on every write whichever door it came through. It normalises as well as refuses.

Directories

Path Synopsis
Package sitetest is the conformance suite for contracts.Service, and a fake that passes it.
Package sitetest is the conformance suite for contracts.Service, and a fake that passes it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL