feat: batches with dry run, undo, and CSV import and export
- POST /_seo/api/v1/batch runs changes in one transaction: dry_run answers the diff and the conflicts (loops, chains, noindex on a published record, canonicals to redirected or noindex addresses) and rolls back; a refused change refuses the batch (D-12) - undo of a change or a whole batch from History and through MCP, with the undo_stale, undo_loop and undo_gone refusals (D-13) - CSV export and import of redirects, rules and records by URL: BOM, comma or semicolon, formula cells escaped, a dry-run preview, all or nothing with via import; the screen's apply token is HMAC-SHA256 over the file and a fingerprint of the data (transfer.stale) - Import and export singleton with the seo_transfer widget action, History record actions undo and undo_batch, navigation entry - MCP tools undo_change, undo_batch, apply_batch, export_csv, import_csv - TestBatch, TestUndo, TestCSVTransfer, TestLoopRefused mcp and import
This commit is contained in:
@@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-seo-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-seo-plugin/models"
|
||||
@@ -53,33 +52,6 @@ type UpdateRuleInput struct {
|
||||
Note *string `json:"note,omitempty" jsonschema:"an internal note"`
|
||||
}
|
||||
|
||||
// ruleBody is the decoded body of a rule write.
|
||||
type ruleBody struct {
|
||||
URL *string `json:"url"`
|
||||
Match *string `json:"match"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Canonical *string `json:"canonical"`
|
||||
Robots *string `json:"robots"`
|
||||
OGTitle *string `json:"og_title"`
|
||||
OGDescription *string `json:"og_description"`
|
||||
OGImage *string `json:"og_image"`
|
||||
Note *string `json:"note"`
|
||||
}
|
||||
|
||||
// apply writes the fields sent onto r.
|
||||
func (b ruleBody) apply(r *models.Rule) {
|
||||
for column, v := range map[string]*string{
|
||||
"url": b.URL, "match": b.Match, "title": b.Title, "description": b.Description,
|
||||
"canonical": b.Canonical, "robots": b.Robots, "og_title": b.OGTitle,
|
||||
"og_description": b.OGDescription, "og_image": b.OGImage, "note": b.Note,
|
||||
} {
|
||||
if v != nil {
|
||||
classes.SetRuleValue(r, column, strings.TrimSpace(*v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RuleJSON is a rule in an answer.
|
||||
func RuleJSON(r models.Rule) map[string]any {
|
||||
out := classes.RuleValues(r)
|
||||
@@ -142,12 +114,12 @@ func ShowRule(service func() *classes.Service) http.HandlerFunc {
|
||||
// an invalid or taken address or invalid values.
|
||||
func CreateRule(service func() *classes.Service) http.HandlerFunc {
|
||||
return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) {
|
||||
var body ruleBody
|
||||
var body classes.RuleFields
|
||||
if err := decode(r, maxBody, &body); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
rule := models.Rule{Match: models.MatchExact}
|
||||
body.apply(&rule)
|
||||
body.Apply(&rule)
|
||||
err := write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return svc.SaveRule(ctx, tx, &rule)
|
||||
})
|
||||
@@ -165,7 +137,7 @@ func UpdateRule(service func() *classes.Service) http.HandlerFunc {
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
var body ruleBody
|
||||
var body classes.RuleFields
|
||||
if err := decode(r, maxBody, &body); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -175,7 +147,7 @@ func UpdateRule(service func() *classes.Service) http.HandlerFunc {
|
||||
if rule, err = classes.LoadRule(tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
body.apply(&rule)
|
||||
body.Apply(&rule)
|
||||
return svc.SaveRule(ctx, tx, &rule)
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user