Files
summercms/modules/pact/capabilities.go
Jakub Zych 7333f450ad fix(10.1): WR-03 walk the whole partial view model before rendering
refusedViewModel compared only the top-level type with the controller's
model. It now walks the type through pointers, slices, arrays, maps,
struct fields and the results of exported methods, and the values held
in interface-typed members, refusing the controller's model, any other
GORM model (TableName, a gorm tag, gorm.Model, gorm.DeletedAt) and
html/template's trusted content types anywhere in that structure.
2026-09-29 09:54:35 +02:00

379 lines
13 KiB
Go

package pact
import (
"context"
"io/fs"
"net/http"
"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
}
// 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
}
// 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
// HasSchedule
//
// The kernel type-asserts HasConfig (party, before Register), HasCommands
// (generated app main, after Boot), HasMigrations (lagoon migrate), 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).