package api import ( "context" "errors" "net/http" "git.golem15.com/golem15/sm-seo-plugin/classes" ) // ApplyBatchInput is the body of POST /_seo/api/v1/batch. type ApplyBatchInput struct { DryRun bool `json:"dry_run,omitempty" jsonschema:"answer the diff and the conflicts without writing anything"` Changes []classes.BatchChange `json:"changes" jsonschema:"the changes, applied in order: op set_seo (url, locale, values, force_rule), create_redirect and update_redirect (id, redirect), delete_redirect (id), create_rule and update_rule (id, rule), delete_rule (id)"` } // ApplyBatch serves POST /_seo/api/v1/batch (D-12, P-26): the changes run // in one transaction. With dry_run the answer is every change's diff and // the conflicts (redirect loops and chains, noindex on a published // record's address, a canonical pointing to a redirected or noindex // address) and nothing is written. Without it the batch is applied as a // whole under one batch id, which history and undo_batch use. A refused // change refuses the whole batch with 422: redirect_loop with the change // and the loop's path, or batch_refused with the change's errors. func ApplyBatch(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { var in ApplyBatchInput if err := decode(r, 4*maxBody, &in); err != nil { return 0, nil, err } res, err := svc.RunBatch(ctx, in.Changes, classes.BatchOptions{DryRun: in.DryRun}) var refused *classes.BatchRefusedError if errors.As(err, &refused) { return 0, nil, &apiError{Status: http.StatusUnprocessableEntity, Body: BatchRefusal(refused.Result)} } if err != nil { return 0, nil, err } return http.StatusOK, res, nil }) } // BatchRefusal is the 422 answer of a refused batch: the first refused // change, its error, path or field errors, and every change's outcome. func BatchRefusal(res classes.BatchResult) map[string]any { item := res.Refused() out := map[string]any{"error": "batch_refused", "changes": res.Changes} if item == nil { return out } out["change"] = item.Index out["message"] = item.Message if item.Error == classes.ConflictLoop { out["error"] = "redirect_loop" out["path"] = item.Path } if len(item.Errors) > 0 { out["errors"] = item.Errors } return out }