Files
summercms/modules/pact/README.md
Jakub Zych 23f33c3911 feat(17-01): tree reorder page and singleton admin controllers
- cabana: config_reorder.yaml (title, nameFrom, modelClass, parentColumn,
  sortColumn, maxDepth, partition) with fail-loud boot checks and the
  built-in reorder toolbar button; list schema carries reorder
- GET/POST {controller}/reorder: scoped set (ListExtendQuery plus the
  partition) locked FOR UPDATE, 409 reorder_stale on any set difference,
  422 for duplicates, foreign parents, cycles and maxDepth, both columns
  written without model hooks
- pact.AdminSingleton: GET {controller}/singleton (first or create, form
  scope checked), form schema singleton, list/create/bulk/delete 404,
  no config_list.yaml needed
- swag, inventories, token classes, CSRF walk, conformance cases, backend
  lang reorder.stale and load_failed, reorder and singleton smoke tests, docs
2026-10-12 15:35:43 +02:00

19 KiB

pact

Capability interfaces that compiled plugins implement to contribute routes, config, migrations, middleware, commands, admin screens, translations, mail templates, jobs and scheduled commands.

import "git.golem15.com/golem15/summercms/modules/pact"

Name: An agreement both sides keep; it holds the interfaces a plugin agrees to implement.

Overview

pact is the contract layer between plugins and the framework. It holds interfaces and plain data types, with no behaviour of its own. A plugin opts into a capability by implementing one of the Has* interfaces, and the framework package that owns the capability discovers it with a type assertion (party for config, surf for routes and middleware, lagoon for migrations, cabana for admin controllers, conga for jobs and schedules). It replaces the register*() methods of a WinterCMS PluginBase (registerPermissions, registerNavigation, registerSettings and so on) with small, separately implementable interfaces.

Role in the framework

The contract layer between the framework and plugins; it depends only on bonfire. party, lagoon, conga, cabana, phrasebook, postcard, lighthouse and surf all read it. It holds interfaces only, so a plugin can depend on the contracts without pulling in the modules that implement them.

Use cases:

  • Make a plugin contribute routes, migrations, commands and admin screens by implementing the matching interfaces.
  • Offer optional features: a plugin implements only the capabilities it needs.
  • Write plugins that are compiled in without a runtime loader, checked by the compiler.

Features

  • Plugin capability interfaces: pact.HasRoutes, pact.HasConfig, pact.HasMigrations, pact.HasCommands, pact.HasModels, pact.HasJobs, pact.HasSchedule, pact.HasLang, pact.HasLangOverrides and pact.HasMailTemplates.
  • HTTP contracts: the pact.Router group builder (implemented by surf), the pact.Middleware type, and named, parameterized (name:param) and house-envelope middleware through pact.HasMiddleware, pact.HasMiddlewareFactories and pact.HasHouseMiddleware.
  • Backend registration data: pact.Permission, pact.NavigationItem and pact.SettingsItem, exposed through pact.HasPermissions, pact.HasNavigation and pact.HasSettings. A settings entry is either a singleton form or a link that opens an admin controller from the Settings page.
  • Personal API token scopes: pact.TokenScope and pact.HasTokenScopes, so a plugin declares named scopes (permissions and HTTP methods) that administrators can assign when creating a personal API token.
  • Admin controller contracts: pact.AdminController, pact.HasAdminControllers, pact.AdminAssets (embedded Winter-shaped admin YAML), pact.AdminPermissioned and pact.AdminRecordSource.
  • Admin extension contracts, so a plugin extends the compiled admin SPA without a Node build: pact.AdminClientAssets (per-controller JS and CSS from the plugin's embedded assets/ tree, Winter's addJs/addCss), pact.HasAdminActions with pact.AdminAction, pact.AdminActionInput and pact.AdminActionResult (named toolbar and widget actions whose routes, CSRF check, permissions and record scoping the framework owns), pact.HasAdminBulkActions with pact.AdminBulkAction, pact.AdminBulkActionInput and pact.AdminBulkActionResult (named actions on the rows selected in a list, which receive records the framework loaded through the list scope, never ids), pact.HasAdminRecordActions with pact.AdminRecordAction, pact.AdminRecordActionInput and pact.AdminRecordActionResult (named actions on one record, each with an Applies rule for the record's state), and pact.AdminPartialData (the curated view model a partial template renders).
  • Plugin field types: pact.HasFormFieldTypes returns pact.FormFieldType entries, admin form field types any controller's fields.yaml uses by a short alias (type: notes). A type names its custom element, its scripts and styles, its own option keys and its Go funcs: Decode turns a submitted pact.FormFieldValue into a pact.FormFieldDecoded (the value to store plus optional pact.FormFieldNotice messages for the administrator, such as what a sanitizer removed) or refuses it with a pact.FormFieldError; Load serves the stored value to the element, ListValue formats a list column, and Upload stores a file from a pact.FormFieldUpload and answers a pact.FormFieldUploadResult. The admin framework owns the routes, permissions, record scope, locale storage of a translatable type and the localization of notices.
  • Optional admin hooks a controller or model can implement: list and form query scoping (pact.ListExtendQuery, pact.FormExtendQuery), list row states (pact.ListRowStates with the fixed pact.RowState set pact.RowStateDeleted, pact.RowStateNegative and pact.RowStateDisabled), create, update and delete hooks (pact.FormBeforeCreate, pact.FormAfterUpdate, pact.FormBeforeDelete and their siblings), form-only fields that reach those hooks without being model columns (pact.FormVirtualFields), validation rules per operation for admin saves (pact.FormRules), relation hooks (pact.RelationExtendManageQuery, pact.RelationExtendOptionsQuery, pact.RelationBeforeLink), relation child hooks around creating, updating and deleting a related record (pact.RelationBeforeCreate, pact.RelationAfterCreate, pact.RelationBeforeUpdate, pact.RelationAfterUpdate, pact.RelationBeforeDelete, pact.RelationAfterDelete), filter scopes (pact.FilterScope, pact.FilterOptions) and dropdown options (pact.DropdownOptionsProvider).
  • Dependent dropdown options: pact.DependentOptionsProvider answers a pact.DependentOptionSet (whether the sibling's value takes a choice at all, and the choices) for a dropdown with dependsOn, from the submitted values of its siblings and with a context, so the choices can come from the database; the admin framework calls it for the options route and again on every save. pact.Option.Depth indents a choice taken from a tree.
  • Menu item types: pact.HasMenuItemTypes returns pact.MenuItemType values (key, label, default URL pattern, PatternFrom for a list type, the References an item can point at as a pact.MenuItemReference tree, and Resolve), the typed successor of WinterCMS's pages.menuitem.* events. Resolve receives a pact.MenuItemResolve with the pattern already layered and answers a pact.ResolvedMenuItem with its alternates as an ordered []pact.MenuItemAlternate. signpost collects them at boot.
  • Singleton admin controllers: pact.AdminSingleton (SingletonRecord, first or create) makes a controller edit exactly one record, opened directly instead of from a list.
  • A background job contract (pact.Job, pact.JobArgs) that does not depend on any queue library.
  • A schedule contract: pact.HasSchedule returns pact.ScheduledCommand entries (a registered command name, its arguments and a pact.Cadence built with pact.Daily, pact.DailyAt or pact.Every), the Go form of WinterCMS registerSchedule. It does not depend on any queue library either.
  • pact.OptionalMessage, a service an optional plugin can publish so others integrate with it without importing its package.
  • pact.HasModels and pact.OptionalMessage are declared for plugins to implement, but no framework package consumes them yet.

Usage

A plugin declares its capabilities by implementing the interfaces and asserting them at compile time:

package blog

import (
	"net/http"

	"git.golem15.com/golem15/summercms/modules/pact"
)

var (
	_ pact.HasRoutes      = (*Plugin)(nil)
	_ pact.HasPermissions = (*Plugin)(nil)
)

type Plugin struct{}

func (p *Plugin) Routes(r pact.Router) error {
	r.Group("/api/blog", []string{"auth"}, func(r pact.Router) {
		r.Get("/posts", listPosts)
		r.Get("/posts/{id}", showPost)
		r.Where("id", "[0-9]+")
	})
	return nil
}

func (p *Plugin) Permissions() []pact.Permission {
	return []pact.Permission{
		{Code: "acme.blog.access_posts", Tab: "Blog", Label: "Manage posts"},
	}
}

func listPosts(w http.ResponseWriter, r *http.Request) {}
func showPost(w http.ResponseWriter, r *http.Request)  {}

A plugin schedules one of its registered commands by implementing pact.HasSchedule:

var _ pact.HasSchedule = (*Plugin)(nil)

func (p *Plugin) Schedule() []pact.ScheduledCommand {
	return []pact.ScheduledCommand{
		{Command: "blog:prune-drafts", Cadence: pact.Daily()},
		{Command: "blog:ping", Args: []string{"--quiet"}, Cadence: pact.Every(15 * time.Minute)},
	}
}

API reference

Identifier Description
pact.Router Laravel-style route group builder (pact.Router.Group, pact.Router.GroupRaw, one method per HTTP verb, pact.Router.Where, pact.Router.WhereIn); implemented by surf.
pact.HasRoutes Declares HTTP routes on a pact.Router.
pact.Middleware A named func(http.Handler) http.Handler wrapper.
pact.HasMiddleware Registers named middleware.
pact.HasMiddlewareFactories Registers parameterized middleware resolved from name:param at wrap time.
pact.HasHouseMiddleware Registers middleware tagged as envelope and error handling, which raw groups refuse.
pact.HasConfig Ships default YAML config, merged under the plugin ID.
pact.HasMigrations Ships an ordered gormigrate set, run with a per-plugin history table.
pact.HasCommands Contributes bonfire console commands to the application binary.
pact.HasModels Exposes GORM models.
pact.Job Background unit of work that receives pact.JobArgs.
pact.HasJobs Registers background jobs.
pact.HasSchedule Declares recurring console commands (Schedule() []pact.ScheduledCommand); conga workers run them.
pact.ScheduledCommand One schedule entry: Command, Args and Cadence.
pact.Cadence Opaque run frequency with IsZero, Interval (24h for daily cadences) and At (hour and minute of a daily cadence).
pact.Daily Cadence at 00:00 every day in the app timezone (Laravel ->daily()).
pact.DailyAt Cadence at a given hour and minute every day in the app timezone.
pact.Every Cadence at every multiple of an interval since local midnight; the interval must be at least 1s and divide 24h.
pact.HasLang Ships translation YAML under lang/<locale>/<group>.yaml.
pact.HasLangOverrides Replaces or adds translations of any loaded namespace, including the framework's own.
pact.HasMailTemplates Ships mail templates and layout aliases.
pact.Permission One backend permission entry (code, tab, label, roles).
pact.HasPermissions Declares backend permissions.
pact.TokenScope One personal API token scope: name, label, description, permission patterns and HTTP methods.
pact.HasTokenScopes Declares personal API token scopes compiled at admin boot.
pact.NavigationItem One backend navigation entry, with an optional side menu.
pact.SettingsItem One settings screen entry: a singleton settings form (Model, Form, NewModel), or a link to an admin controller when Controller names its ID, as a WinterCMS 'url' => Backend::url(...) entry.
pact.AdminController Admin controller identity: ID, model name and YAML config directory.
pact.AdminAssets Embedded tree of the plugin's admin YAML.
pact.AdminRecordSource Supplies a new model record for the generic admin handlers.
pact.AdminClientAssets Declares a controller's admin JS (AdminJS) and CSS (AdminCSS) files, paths under the plugin's assets/ directory.
pact.AdminAction One named controller action: name, label, extra permissions and the Go Run function.
pact.AdminActionInput What an action receives: widget field, optional record id and scoped record, the fill snapshot, and the widget's raw JSON payload (Payload, at most 64 KiB, nil when absent, never set for a toolbar action, not inspected by the framework).
pact.AdminActionResult What an action returns: a message for the toast, the fill write-back values, and an optional Data value passed through to the widget as data (any JSON up to 256 KiB encoded, not filtered like fill, omitted when nil).
pact.HasAdminActions Registers a controller's actions for toolbar.buttons and type: widget fields.
pact.AdminBulkAction One named bulk action: name, label, optional confirm text, extra permissions and the Go Run function.
pact.AdminBulkActionInput What a bulk action receives: Records, the selected records loaded and row-locked through the list scope.
pact.AdminBulkActionResult What a bulk action returns: an optional message for the toast and Affected, the number of records it changed.
pact.HasAdminBulkActions Registers a controller's bulk actions for the bulkActions list of config_list.yaml.
pact.AdminRecordAction One named record action: name, label, optional confirm text, extra permissions, the optional Applies rule and the Go Run function.
pact.AdminRecordActionInput What a record action receives: RecordID and Record, the record loaded and row-locked through the form scope.
pact.AdminRecordActionResult What a record action returns: an optional message for the toast.
pact.HasAdminRecordActions Registers a controller's record actions for the recordActions list of config_form.yaml.
pact.AdminPartialData Supplies the view model a controller partial template renders; never the GORM model.
pact.HasFormFieldTypes Implemented by a plugin (not a controller) that registers admin form field types.
pact.FormFieldType One field type: the alias Name, the custom-element tag Element, Translatable, the Scripts and Styles under assets/, the Options keys, and the funcs CheckOptions (optional), Decode (required), Load, ListValue (func(ctx context.Context, value any) string) and Upload (optional).
pact.FormFieldValue What Decode and Load receive: the transaction, the record, the field, its options, the content locale (translatable types), whether the value lives in the host column, the value (always a string for a translatable type) and an exact reader of another attribute in the same locale.
pact.FormFieldDecoded Decode's answer: the Value to store (a string for a translatable type) and optional Notices.
pact.FormFieldNotice A message about a saved value: a phrase key or text with params; a positive Count selects the plural form.
pact.FormFieldUpload What Upload receives: the transaction, the scoped record (nil on the create form), the field, its options, the file name and the capped body.
pact.FormFieldUploadResult Upload's answer: the URL of the stored file.
pact.FormFieldError A value or upload a field type refuses: messages shown on the field (validation failed).
pact.ListRowStates Optional controller hook called once per list page; returns the states of the page's records, index-aligned.
pact.RowState One state of a list row: pact.RowStateDeleted, pact.RowStateNegative or pact.RowStateDisabled.
pact.FormVirtualFields Optional controller list of form fields that are not model columns for the form (a password and its confirmation, for example): never bound, filled or returned; their submitted values reach the Form hooks through the admin framework's context accessor.
pact.FormRules Optional controller hook returning the validation rules of an admin save for create or update; the set replaces the model's Rules() for those saves and may name virtual fields.
pact.FilterScope Model scopes a list filter may call, limited to an exact allow list.
pact.FilterOptions Serves the choices of a scope filter. The admin controller may implement it and is asked first, so choices can be read from the database; otherwise the model is asked.
pact.RelationBeforeLink Optional controller hook that checks or fills pivot columns before a relation link is written.
pact.RelationBeforeCreate Optional controller hook run in the write transaction before a relation manager creates a related record.
pact.RelationAfterCreate Optional controller hook run after a relation manager creates a related record, before the commit.
pact.RelationBeforeUpdate Optional controller hook run in the write transaction before a relation manager updates a related record.
pact.RelationAfterUpdate Optional controller hook run after a relation manager updates a related record, before the commit.
pact.RelationBeforeDelete Optional controller hook run in the write transaction before a relation manager deletes a related record.
pact.RelationAfterDelete Optional controller hook run after a relation manager deletes a related record, before the commit.
pact.Option One dropdown choice (value and label), with Depth, the nesting level of a choice taken from a tree (omitted from JSON when 0).
pact.AdminSingleton Optional controller capability: SingletonRecord returns the one record the controller edits, creating it when absent; the admin opens it directly and the list, create, bulk and delete routes answer 404.
pact.DependentOptionsProvider Optional controller hook serving the choices of a dropdown with dependsOn from the submitted values of its siblings; asked by the options route and on every save.
pact.DependentOptionSet DependentOptions' answer: Applies (false when the sibling's value takes no choice; the field then stores null) and Options.
pact.HasMenuItemTypes Implemented by plugins that register menu item types; collected once at boot by signpost.
pact.MenuItemType One menu item type: Key, Label, DefaultPattern (required, starts with /), PatternFrom (a list type's record type), Placeholders, Nesting, DynamicItems, the optional References func and the required Resolve func.
pact.MenuItemReference One record a menu item can point at: value, label and nested records.
pact.MenuItemResolve What Resolve receives: type, reference, the layered URL pattern, whether to expand nested items and the unprefixed locale.
pact.ResolvedMenuItem A resolved item: title, URL (empty for a pure list type), modification time, ordered alternates and nested items.
pact.MenuItemAlternate The URL and title of a resolved item in one locale.

Dependencies

  • SummerCMS modules: bonfire (the command type in pact.HasCommands).
  • Third-party: github.com/go-gormigrate/gormigrate/v2, gorm.io/gorm.
  • Standard library: context, encoding/json, io, io/fs, net/http, strings, time.

Testing

go test ./modules/pact/...

The tests are compile-time interface checks and need no external services.