- deferred:purge [--days] in lagoon.RuntimeCommands (purge_days, default 5)
- lagoon.FrameworkSchedule entry at purge_at (default 03:00, empty disables)
- conga prepends framework entries as summercms.lagoon[i]:<command>
- pact.Relation{Before,After}{Create,Update,Delete} optional hooks
- lagoon, conga and pact READMEs, scheduling and setup docs
496 lines
17 KiB
Go
496 lines
17 KiB
Go
package pact
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// HasCommands is implemented by plugins that register console commands.
|
|
type HasCommands interface {
|
|
Commands() []bonfire.Command
|
|
}
|
|
|
|
// HasConfig is implemented by plugins that ship default YAML configuration.
|
|
// ConfigFS should contain files such as config/config.yaml, merged later at
|
|
// the plugin ID path.
|
|
type HasConfig interface {
|
|
ConfigFS() fs.FS
|
|
}
|
|
|
|
// HasMigrations is implemented by plugins that ship an ordered gormigrate
|
|
// set. The kernel runs each set in party.Activate order with a separate
|
|
// history table per plugin ID.
|
|
type HasMigrations interface {
|
|
Migrations() []*gormigrate.Migration
|
|
}
|
|
|
|
// Middleware is a named HTTP wrapper registered by a plugin.
|
|
type Middleware func(http.Handler) http.Handler
|
|
|
|
// HasMiddleware is implemented by plugins that register named middleware.
|
|
type HasMiddleware interface {
|
|
Middlewares() map[string]Middleware
|
|
}
|
|
|
|
// HasMiddlewareFactories is implemented by plugins that register
|
|
// parameterized middleware ("name:param"), resolved at wrap time.
|
|
type HasMiddlewareFactories interface {
|
|
MiddlewareFactories() map[string]func(param string) Middleware
|
|
}
|
|
|
|
// HasHouseMiddleware is implemented by plugins that register middleware
|
|
// tagged as house-envelope/error handling -- refused inside a raw group
|
|
// (D-16). This is the ONLY way a plugin declares a house-tagged name:
|
|
// plugins never call a Router.Register* method directly (there is no such
|
|
// call site anywhere in this codebase -- RegisterMiddleware/
|
|
// RegisterMiddlewareFactory/RegisterHouseMiddleware are all called
|
|
// exclusively from surf.Assemble/BuildRouter's plugin loop, the same way
|
|
// HasMiddleware's Middlewares() map is today). A name present in both
|
|
// Middlewares() and HouseMiddlewares() (from the same or a different
|
|
// plugin) fails boot with the existing duplicate-name error, since both are
|
|
// registered into the same underlying name table.
|
|
type HasHouseMiddleware interface {
|
|
HouseMiddlewares() map[string]Middleware
|
|
}
|
|
|
|
// Router is the Laravel-like group builder implemented by surf.
|
|
type Router interface {
|
|
Group(prefix string, middleware []string, fn func(Router))
|
|
GroupRaw(prefix string, middleware []string, fn func(Router))
|
|
Get(path string, handler http.HandlerFunc, middleware ...string)
|
|
Post(path string, handler http.HandlerFunc, middleware ...string)
|
|
Put(path string, handler http.HandlerFunc, middleware ...string)
|
|
Patch(path string, handler http.HandlerFunc, middleware ...string)
|
|
Delete(path string, handler http.HandlerFunc, middleware ...string)
|
|
Where(param, pattern string)
|
|
WhereIn(param string, values ...string)
|
|
}
|
|
|
|
// HasRoutes is implemented by plugins that declare HTTP routes.
|
|
type HasRoutes interface {
|
|
Routes(Router) error
|
|
}
|
|
|
|
// HasModels is implemented by plugins that expose GORM models.
|
|
type HasModels interface {
|
|
Models() []any
|
|
}
|
|
|
|
// JobArgs is the typed payload a job worker receives. Kind identifies the
|
|
// job so a worker can reject unexpected argument types.
|
|
type JobArgs interface {
|
|
Kind() string
|
|
}
|
|
|
|
// Job is a unit of background work. Phase 11 adapts this onto River; the
|
|
// interface itself must not import River.
|
|
type Job interface {
|
|
Work(ctx context.Context, args JobArgs) error
|
|
}
|
|
|
|
// HasJobs is implemented by plugins that register background jobs.
|
|
type HasJobs interface {
|
|
Jobs() []Job
|
|
}
|
|
|
|
type cadenceKind uint8
|
|
|
|
const (
|
|
cadenceNone cadenceKind = iota
|
|
cadenceDaily
|
|
cadenceEvery
|
|
)
|
|
|
|
// Cadence is how often a scheduled command runs. Build one with Daily,
|
|
// DailyAt or Every; the zero value is no cadence and is rejected by the
|
|
// scheduler.
|
|
type Cadence struct {
|
|
kind cadenceKind
|
|
hour int
|
|
minute int
|
|
every time.Duration
|
|
}
|
|
|
|
// Daily runs once a day at 00:00 in the app timezone (Laravel ->daily()).
|
|
func Daily() Cadence { return DailyAt(0, 0) }
|
|
|
|
// DailyAt runs once a day at hour:minute in the app timezone.
|
|
func DailyAt(hour, minute int) Cadence {
|
|
return Cadence{kind: cadenceDaily, hour: hour, minute: minute}
|
|
}
|
|
|
|
// Every runs at every multiple of d since local midnight in the app
|
|
// timezone. The scheduler requires d to be at least one second and to divide
|
|
// 24h evenly.
|
|
func Every(d time.Duration) Cadence {
|
|
return Cadence{kind: cadenceEvery, every: d}
|
|
}
|
|
|
|
// IsZero reports whether c is the zero Cadence.
|
|
func (c Cadence) IsZero() bool { return c.kind == cadenceNone }
|
|
|
|
// Interval is the period of c: 24h for a daily cadence, d for Every(d) and
|
|
// zero for the zero Cadence.
|
|
func (c Cadence) Interval() time.Duration {
|
|
switch c.kind {
|
|
case cadenceDaily:
|
|
return 24 * time.Hour
|
|
case cadenceEvery:
|
|
return c.every
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// At returns the wall-clock time of a daily cadence; ok is false for Every
|
|
// and for the zero Cadence.
|
|
func (c Cadence) At() (hour, minute int, ok bool) {
|
|
if c.kind != cadenceDaily {
|
|
return 0, 0, false
|
|
}
|
|
return c.hour, c.minute, true
|
|
}
|
|
|
|
// ScheduledCommand is one recurring run of a registered console command.
|
|
// Command is the command name (namespace:verb) and Args its arguments.
|
|
type ScheduledCommand struct {
|
|
Command string
|
|
Args []string
|
|
Cadence Cadence
|
|
}
|
|
|
|
// HasSchedule is implemented by plugins that run console commands on a
|
|
// schedule. Only these compiled entries are ever executed by the scheduler.
|
|
type HasSchedule interface {
|
|
Schedule() []ScheduledCommand
|
|
}
|
|
|
|
// AdminController is the compile-time admin controller contract. Phase 9
|
|
// grows the schema pipeline; ID, model name and YAML config directory are
|
|
// enough for generated stubs to compile.
|
|
type AdminController interface {
|
|
ID() string
|
|
ModelName() string
|
|
ConfigDir() string
|
|
}
|
|
|
|
// HasAdminControllers is implemented by plugins that register admin controllers.
|
|
type HasAdminControllers interface {
|
|
AdminControllers() []AdminController
|
|
}
|
|
|
|
// AdminAssets is the plugin-owned embedded tree of Winter admin YAML.
|
|
// Paths are relative to the plugin root (controllers/..., models/...).
|
|
type AdminAssets interface {
|
|
AdminFS() fs.FS
|
|
}
|
|
|
|
// AdminPermissioned is the D-03 permission list enforced before schema or SQL.
|
|
type AdminPermissioned interface {
|
|
RequiredPermissions() []string
|
|
}
|
|
|
|
// AdminRecordSource supplies the GORM model the generic admin handlers query.
|
|
// NewRecord returns a pointer to the model struct.
|
|
type AdminRecordSource interface {
|
|
NewRecord() any
|
|
}
|
|
|
|
// AdminClientAssets is Winter's addJs/addCss for one admin controller. The
|
|
// paths are relative to the owning plugin's AdminFS and must live under
|
|
// assets/ (for example assets/js/lookup.js). The admin SPA loads them when the
|
|
// controller's list or form opens; files are always served from the embedded
|
|
// tree, never from disk. It is separate from AdminAssets, which is the YAML
|
|
// tree itself.
|
|
type AdminClientAssets interface {
|
|
AdminJS() []string
|
|
AdminCSS() []string
|
|
}
|
|
|
|
// AdminAction is one controller action that a list toolbar button or a form
|
|
// widget runs. The admin framework owns the HTTP route, the CSRF check,
|
|
// authentication and record scoping; Run only carries the business logic.
|
|
// Name is an identifier unique within the controller; create and delete are
|
|
// reserved for the built-in toolbar actions. Label is a phrase key or literal
|
|
// text used as the button caption. Permissions are checked in addition to the
|
|
// controller's RequiredPermissions.
|
|
type AdminAction struct {
|
|
Name string
|
|
Label string
|
|
Permissions []string
|
|
Run func(ctx context.Context, in AdminActionInput) (AdminActionResult, error) `json:"-"`
|
|
}
|
|
|
|
// AdminActionInput is what the framework hands an AdminAction. Field is the
|
|
// widget field name and is empty for a toolbar action. RecordID is nil on the
|
|
// create form and always nil for a toolbar action: toolbar actions carry no
|
|
// record ids, so an id list can never become an unscoped lookup. Record is the
|
|
// record the framework loaded through the controller's FormExtendQuery scope,
|
|
// nil when RecordID is nil. Values is the widget's snapshot of its fill
|
|
// fields, already reduced to the field's declared fill keys and to scalars.
|
|
type AdminActionInput struct {
|
|
Field string
|
|
RecordID *uint64
|
|
Record any
|
|
Values map[string]any
|
|
}
|
|
|
|
// AdminActionResult is an action's answer. Message is a phrase key or text,
|
|
// localized by the framework and shown as a toast. Fill is the widget
|
|
// write-back; keys outside the field's declared fill keys and non-scalar
|
|
// values are dropped before the response is written.
|
|
type AdminActionResult struct {
|
|
Message string
|
|
Fill map[string]any
|
|
}
|
|
|
|
// HasAdminActions is implemented by an admin controller that registers named
|
|
// actions for its toolbar.buttons list and its `type: widget` form fields.
|
|
type HasAdminActions interface {
|
|
AdminActions() []AdminAction
|
|
}
|
|
|
|
// AdminPartialData supplies the view model a controller partial template
|
|
// renders (config_list.yaml headerPartial, fields.yaml `type: partial`). name
|
|
// is the partial name; record is the scoped record for a form partial on an
|
|
// existing record, else nil. The result must be a curated view model built
|
|
// for the template, never a GORM model: the framework refuses a view model
|
|
// that carries the controller's model or any other GORM model, even nested
|
|
// in a field, a collection or an interface value.
|
|
type AdminPartialData interface {
|
|
PartialData(ctx context.Context, name string, record any) (any, error)
|
|
}
|
|
|
|
// Permission is one registerPermissions() entry.
|
|
type Permission struct {
|
|
Code string
|
|
Tab string
|
|
Label string
|
|
Roles []string
|
|
}
|
|
|
|
// HasPermissions is implemented by plugins that declare backend permissions.
|
|
type HasPermissions interface {
|
|
Permissions() []Permission
|
|
}
|
|
|
|
// NavigationItem is one registerNavigation() entry. Controller is the admin
|
|
// controller ID; the SPA derives its route from that ID.
|
|
type NavigationItem struct {
|
|
Code string
|
|
Label string
|
|
Icon string
|
|
Permissions []string
|
|
Order int
|
|
Controller string
|
|
SideMenu []NavigationItem
|
|
}
|
|
|
|
// HasNavigation is implemented by plugins that declare backend navigation.
|
|
type HasNavigation interface {
|
|
Navigation() []NavigationItem
|
|
}
|
|
|
|
// SettingsItem is one registerSettings() entry.
|
|
type SettingsItem struct {
|
|
Code string
|
|
Label string
|
|
Description string
|
|
Category string
|
|
Icon string
|
|
Model string
|
|
Order int
|
|
Keywords []string
|
|
Permissions []string
|
|
Form string `json:"-"`
|
|
NewModel func() any `json:"-"`
|
|
}
|
|
|
|
// HasSettings is implemented by plugins that declare settings screens.
|
|
type HasSettings interface {
|
|
Settings() []SettingsItem
|
|
}
|
|
|
|
// Option is one dropdown choice. Label may be a phrase key until request time.
|
|
type Option struct {
|
|
Value string `json:"value"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
// DropdownOptionsProvider serves method-backed dropdown options.
|
|
type DropdownOptionsProvider interface {
|
|
DropdownOptions(field string) []Option
|
|
}
|
|
|
|
// ListExtendQuery optionally narrows the admin list query.
|
|
type ListExtendQuery interface {
|
|
ListExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB
|
|
}
|
|
|
|
// ListRelationColumnMapper maps a source-schema relation column onto the
|
|
// physical column exposed by the related Go model. It supports legacy admin
|
|
// schemas whose public field names no longer match the database schema.
|
|
type ListRelationColumnMapper interface {
|
|
ListRelationColumn(relation, column string) (string, bool)
|
|
}
|
|
|
|
// FormExtendQuery optionally narrows admin form record lookup.
|
|
type FormExtendQuery interface {
|
|
FormExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB
|
|
}
|
|
|
|
// FormBeforeCreate optionally rejects or stamps a record before insert.
|
|
type FormBeforeCreate interface {
|
|
FormBeforeCreate(ctx context.Context, model any) error
|
|
}
|
|
|
|
// FormBeforeUpdate optionally rejects or stamps a record before update.
|
|
type FormBeforeUpdate interface {
|
|
FormBeforeUpdate(ctx context.Context, model any) error
|
|
}
|
|
|
|
// FormAfterCreate optionally runs after insert, inside the same transaction.
|
|
type FormAfterCreate interface {
|
|
FormAfterCreate(ctx context.Context, model any) error
|
|
}
|
|
|
|
// FormAfterUpdate optionally runs after update, inside the same transaction.
|
|
type FormAfterUpdate interface {
|
|
FormAfterUpdate(ctx context.Context, model any) error
|
|
}
|
|
|
|
// FormBeforeDelete optionally rejects a record before delete.
|
|
type FormBeforeDelete interface {
|
|
FormBeforeDelete(ctx context.Context, model any) error
|
|
}
|
|
|
|
// FormAfterDelete optionally runs after delete, inside the same transaction.
|
|
type FormAfterDelete interface {
|
|
FormAfterDelete(ctx context.Context, model any) error
|
|
}
|
|
|
|
// RelationExtendManageQuery optionally narrows relation-manager candidates.
|
|
type RelationExtendManageQuery interface {
|
|
RelationExtendManageQuery(ctx context.Context, relation string, db *gorm.DB) *gorm.DB
|
|
}
|
|
|
|
// RelationExtendOptionsQuery optionally narrows the rows a form relation
|
|
// field offers (D-17). The same scoped query revalidates submitted ids on
|
|
// save, so a row it does not return cannot be attached (D-18).
|
|
type RelationExtendOptionsQuery interface {
|
|
RelationExtendOptionsQuery(ctx context.Context, field string, db *gorm.DB) *gorm.DB
|
|
}
|
|
|
|
// RelationBeforeLink optionally stamps pivot columns before a link insert.
|
|
type RelationBeforeLink interface {
|
|
RelationBeforeLink(ctx context.Context, relation string, parent, related any, pivot map[string]any) error
|
|
}
|
|
|
|
// The six relation child hooks below are optional admin-controller
|
|
// capabilities for a relation manager that creates, updates or deletes
|
|
// related records. Before hooks run inside the child write's transaction
|
|
// before the row is written; After hooks run after the write and before the
|
|
// commit. parent is the loaded parent record, or a fresh record with a zero
|
|
// key while the parent is not saved yet (deferral). child is the related
|
|
// record being written. An error rolls the write back and the admin API
|
|
// answers its opaque lifecycle error.
|
|
|
|
// RelationBeforeCreate optionally runs before a related record is created
|
|
// through a relation manager.
|
|
type RelationBeforeCreate interface {
|
|
RelationBeforeCreate(ctx context.Context, relation string, parent, child any) error
|
|
}
|
|
|
|
// RelationAfterCreate optionally runs after a related record is created
|
|
// through a relation manager, before the commit.
|
|
type RelationAfterCreate interface {
|
|
RelationAfterCreate(ctx context.Context, relation string, parent, child any) error
|
|
}
|
|
|
|
// RelationBeforeUpdate optionally runs before a related record is updated
|
|
// through a relation manager.
|
|
type RelationBeforeUpdate interface {
|
|
RelationBeforeUpdate(ctx context.Context, relation string, parent, child any) error
|
|
}
|
|
|
|
// RelationAfterUpdate optionally runs after a related record is updated
|
|
// through a relation manager, before the commit.
|
|
type RelationAfterUpdate interface {
|
|
RelationAfterUpdate(ctx context.Context, relation string, parent, child any) error
|
|
}
|
|
|
|
// RelationBeforeDelete optionally runs before a related record is deleted
|
|
// through a relation manager.
|
|
type RelationBeforeDelete interface {
|
|
RelationBeforeDelete(ctx context.Context, relation string, parent, child any) error
|
|
}
|
|
|
|
// RelationAfterDelete optionally runs after a related record is deleted
|
|
// through a relation manager, before the commit.
|
|
type RelationAfterDelete interface {
|
|
RelationAfterDelete(ctx context.Context, relation string, parent, child any) error
|
|
}
|
|
|
|
// FilterScope is the model capability a config_filter scope name may call.
|
|
// FilterScopes is the exact, case-sensitive set of names; request text never
|
|
// selects a method outside that set.
|
|
type FilterScope interface {
|
|
FilterScopes() []string
|
|
FilterScope(name string, db *gorm.DB, value any) *gorm.DB
|
|
}
|
|
|
|
// FilterOptions serves the choices of a model-backed config_filter scope
|
|
// (D-27). It is implemented by the same model as FilterScope and receives the
|
|
// filter's scope method name; labels may be phrase keys.
|
|
type FilterOptions interface {
|
|
FilterOptions(scope string) []Option
|
|
}
|
|
|
|
// HasLang is implemented by plugins that ship embedded translation YAML
|
|
// under lang/<locale>/<group>.yaml.
|
|
type HasLang interface {
|
|
LangFS() fs.FS
|
|
}
|
|
|
|
// HasLangOverrides is implemented by plugins (usually the app plugin) that
|
|
// replace or add translations of any loaded namespace, including the
|
|
// framework's backend::lang admin strings, and may add locales (D-20).
|
|
// Layout: lang/<locale>/<namespace>/<group>.yaml, e.g.
|
|
// lang/pl/backend/lang.yaml.
|
|
type HasLangOverrides interface {
|
|
LangOverridesFS() fs.FS
|
|
}
|
|
|
|
// HasMailTemplates is implemented by plugins that ship Winter-shaped mail
|
|
// templates and layout aliases. MailTemplatesFS contains views/mail assets;
|
|
// MailTemplates lists dotted template names; MailLayouts maps a short layout
|
|
// name to the full dotted layout name.
|
|
type HasMailTemplates interface {
|
|
MailTemplatesFS() fs.FS
|
|
MailTemplates() []string
|
|
MailLayouts() map[string]string
|
|
}
|
|
|
|
// OptionalMessage is a service an optional plugin may publish so other
|
|
// plugins can integrate without importing that plugin's package.
|
|
type OptionalMessage interface {
|
|
Message() string
|
|
}
|
|
|
|
// Future capability families are type-asserted when their first consumer
|
|
// packages exist:
|
|
//
|
|
// HasListeners
|
|
//
|
|
// The kernel type-asserts HasConfig (party, before Register), HasCommands
|
|
// (generated app main, after Boot), HasMigrations (lagoon migrate),
|
|
// HasJobs and HasSchedule (conga workers), and
|
|
// HasMiddleware/HasMiddlewareFactories/HasHouseMiddleware/HasRoutes
|
|
// (surf assemble). surf.BucketProvider is type-asserted in Assemble/
|
|
// BuildRouter (not a pact interface: pact cannot import surf without a cycle).
|