Files
summercms/modules/pact
Jakub Zych 2e94cbf9f8 test(12.1-05): unit tests for bulk and record actions, row state, forbidden, preview and the form seams
- bulk action: empty, duplicate, unordered, absent, partial, out-of-scope, rollback, concurrent runs, permissions, CSRF, body cap
- record action: scope, Applies, strict body, offered order, rollback, Applies error
- ForbiddenError from every Form hook, the bulk delete and the relation link and child hooks
- permission editor modes, locked codes and provider errors; relation locks on create, update and belongsTo
- TestPhase121BootErrors: every boot error of plans 01 and 02 with plugin, controller and file
- pact: the action, row state and filter contracts on a sample controller
2026-10-05 14:48:34 +02:00
..

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"

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.

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.
  • 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).
  • 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).
  • 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.NavigationItem One backend navigation entry, with an optional side menu.
pact.SettingsItem One settings screen 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, and the fill snapshot.
pact.AdminActionResult What an action returns: a message for the toast and the fill write-back values.
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.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).

Dependencies

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

Testing

go test ./modules/pact/...

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