feat(12.1-01): declared record actions with an applicability rule
- pact.HasAdminRecordActions with AdminRecordAction (Applies, Run)
- config_form.yaml recordActions, compiled fail-loud
- show response meta.actions lists the permitted actions that apply
- POST .../{controller}/{id}/actions/{action}: record loaded and locked
through the form scope; 404 out of scope, 409 when it does not apply
- RecordActions.vue with confirm and request flow (mounted by plan 02)
- roster fixture, smoke tests, OpenAPI, TS types, READMEs, docs
This commit is contained in:
@@ -46,8 +46,23 @@ func (rosterPerson) Rules() map[string]string { return map[string]string{"name":
|
||||
|
||||
// rosterSpy records what each registered action's Run receives.
|
||||
type rosterSpy struct {
|
||||
mu sync.Mutex
|
||||
bulk []pact.AdminBulkActionInput
|
||||
mu sync.Mutex
|
||||
bulk []pact.AdminBulkActionInput
|
||||
record []pact.AdminRecordActionInput
|
||||
}
|
||||
|
||||
func (s *rosterSpy) recordOne(in pact.AdminRecordActionInput) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.record = append(s.record, in)
|
||||
}
|
||||
|
||||
func (s *rosterSpy) takeRecord() []pact.AdminRecordActionInput {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := s.record
|
||||
s.record = nil
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *rosterSpy) recordBulk(in pact.AdminBulkActionInput) {
|
||||
@@ -64,7 +79,12 @@ func (s *rosterSpy) takeBulk() []pact.AdminBulkActionInput {
|
||||
return out
|
||||
}
|
||||
|
||||
type rosterPlugin struct{ spy *rosterSpy }
|
||||
// rosterPlugin is the acme.roster fixture plugin. fsys, when set, replaces
|
||||
// the fixture tree (boot-error tests).
|
||||
type rosterPlugin struct {
|
||||
spy *rosterSpy
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
func (rosterPlugin) ID() string { return "acme.roster" }
|
||||
func (rosterPlugin) Requires() []string { return nil }
|
||||
@@ -76,7 +96,12 @@ func (p rosterPlugin) AdminControllers() []pact.AdminController {
|
||||
func (rosterPlugin) Permissions() []pact.Permission {
|
||||
return []pact.Permission{{Code: "acme.roster.access", Roles: []string{"developer"}}, {Code: "acme.roster.manage", Roles: []string{"developer"}}}
|
||||
}
|
||||
func (rosterPlugin) AdminFS() fs.FS { return os.DirFS(rosterDir) }
|
||||
func (p rosterPlugin) AdminFS() fs.FS {
|
||||
if p.fsys != nil {
|
||||
return p.fsys
|
||||
}
|
||||
return os.DirFS(rosterDir)
|
||||
}
|
||||
|
||||
// LangFS serves only the fixture's lang/ tree.
|
||||
func (rosterPlugin) LangFS() fs.FS {
|
||||
@@ -153,6 +178,46 @@ func (c rosterController) AdminBulkActions() []pact.AdminBulkAction {
|
||||
}}
|
||||
}
|
||||
|
||||
// AdminRecordActions: activate needs acme.roster.manage and applies to a
|
||||
// person who is not active; reinstate applies to a banned person and lifts
|
||||
// the ban.
|
||||
func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
return []pact.AdminRecordAction{{
|
||||
Name: "activate", Label: "acme.roster::lang.people.activate",
|
||||
Permissions: []string{"acme.roster.manage"},
|
||||
Applies: func(_ context.Context, record any) (bool, error) {
|
||||
return !record.(*rosterPerson).Active, nil
|
||||
},
|
||||
Run: func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
|
||||
c.spy.recordOne(in)
|
||||
tx, ok := cabana.TxFromContext(ctx)
|
||||
if !ok {
|
||||
return pact.AdminRecordActionResult{}, fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
if err := tx.Model(in.Record).Update("active", true).Error; err != nil {
|
||||
return pact.AdminRecordActionResult{}, err
|
||||
}
|
||||
return pact.AdminRecordActionResult{Message: "acme.roster::lang.people.activated"}, nil
|
||||
},
|
||||
}, {
|
||||
Name: "reinstate", Label: "acme.roster::lang.people.reinstate", Confirm: "acme.roster::lang.people.reinstate_confirm",
|
||||
Applies: func(_ context.Context, record any) (bool, error) {
|
||||
return record.(*rosterPerson).Banned, nil
|
||||
},
|
||||
Run: func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
|
||||
c.spy.recordOne(in)
|
||||
tx, ok := cabana.TxFromContext(ctx)
|
||||
if !ok {
|
||||
return pact.AdminRecordActionResult{}, fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
if err := tx.Model(in.Record).Update("banned", false).Error; err != nil {
|
||||
return pact.AdminRecordActionResult{}, err
|
||||
}
|
||||
return pact.AdminRecordActionResult{}, nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// rosterEnv is the assembled admin API over the roster fixture. The embedded
|
||||
// actEnv supplies call and expect with the four auth modes: bearer (developer
|
||||
// token), limited (acme.roster.access only), cookie and cookie-only.
|
||||
|
||||
Reference in New Issue
Block a user