pact.SettingsItem gains an additive Controller field, the equivalent of a WinterCMS registerSettings 'url' => Backend::url(...) entry. A link entry declares no Model, Form or NewModel and needs no AdminFS; start-up fails when it combines Controller with a singleton form or a model, or names an unregistered controller. Settings codes stay one namespace. GET /settings lists a link entry with its controller only when the principal passes the item's permissions and may open the controller. Registry.Setting never returns a link entry, so the singleton settings endpoints answer 404 for its code. SettingsEntry carries controller, empty for singletons; the OpenAPI document, generated SPA types and settings fixture follow, and the pact and cabana READMEs and the settings docs describe the link.
149 lines
13 KiB
Markdown
149 lines
13 KiB
Markdown
# 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](../party/README.md) for config, [surf](../surf/README.md) for routes and middleware, [lagoon](../lagoon/README.md) for migrations, [cabana](../cabana/README.md) for admin controllers, [conga](../conga/README.md) 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`. 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.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:
|
|
|
|
```go
|
|
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`:
|
|
|
|
```go
|
|
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.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](../bonfire/README.md) (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
|
|
|
|
```sh
|
|
go test ./modules/pact/...
|
|
```
|
|
|
|
The tests are compile-time interface checks and need no external services.
|