Each module README now says where the module sits in the dependency graph, why it is its own module, and lists use cases for application developers.
16 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.HasLangOverridesandpact.HasMailTemplates. - HTTP contracts: the
pact.Routergroup builder (implemented by surf), thepact.Middlewaretype, and named, parameterized (name:param) and house-envelope middleware throughpact.HasMiddleware,pact.HasMiddlewareFactoriesandpact.HasHouseMiddleware. - Backend registration data:
pact.Permission,pact.NavigationItemandpact.SettingsItem, exposed throughpact.HasPermissions,pact.HasNavigationandpact.HasSettings. A settings entry is either a singleton form or a link that opens an admin controller from the Settings page. - Admin controller contracts:
pact.AdminController,pact.HasAdminControllers,pact.AdminAssets(embedded Winter-shaped admin YAML),pact.AdminPermissionedandpact.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 embeddedassets/tree, Winter'saddJs/addCss),pact.HasAdminActionswithpact.AdminAction,pact.AdminActionInputandpact.AdminActionResult(named toolbar and widget actions whose routes, CSRF check, permissions and record scoping the framework owns),pact.HasAdminBulkActionswithpact.AdminBulkAction,pact.AdminBulkActionInputandpact.AdminBulkActionResult(named actions on the rows selected in a list, which receive records the framework loaded through the list scope, never ids),pact.HasAdminRecordActionswithpact.AdminRecordAction,pact.AdminRecordActionInputandpact.AdminRecordActionResult(named actions on one record, each with anAppliesrule for the record's state), andpact.AdminPartialData(the curated view model a partial template renders). - Plugin field types:
pact.HasFormFieldTypesreturnspact.FormFieldTypeentries, admin form field types any controller'sfields.yamluses by a short alias (type: notes). A type names its custom element, its scripts and styles, its own option keys and its Go funcs:Decodeturns a submittedpact.FormFieldValueinto apact.FormFieldDecoded(the value to store plus optionalpact.FormFieldNoticemessages for the administrator, such as what a sanitizer removed) or refuses it with apact.FormFieldError;Loadserves the stored value to the element,ListValueformats a list column, andUploadstores a file from apact.FormFieldUploadand answers apact.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.ListRowStateswith the fixedpact.RowStatesetpact.RowStateDeleted,pact.RowStateNegativeandpact.RowStateDisabled), create, update and delete hooks (pact.FormBeforeCreate,pact.FormAfterUpdate,pact.FormBeforeDeleteand 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.HasSchedulereturnspact.ScheduledCommandentries (a registered command name, its arguments and apact.Cadencebuilt withpact.Daily,pact.DailyAtorpact.Every), the Go form of WinterCMSregisterSchedule. 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.HasModelsandpact.OptionalMessageare 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: 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). |
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.