package api import ( "context" "net/http" "strings" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/models" "gorm.io/gorm" ) // ListRulesInput is the query of GET /_seo/api/v1/rules. type ListRulesInput struct { Page int `json:"page,omitempty" query:"page" jsonschema:"the page number, from 1"` PerPage int `json:"per_page,omitempty" query:"per_page" jsonschema:"rows per page, at most 200 (50 by default)"` Search string `json:"search,omitempty" query:"search" jsonschema:"text the address, title or description contains"` Match string `json:"match,omitempty" query:"match" jsonschema:"exact or pattern"` } // RuleIDInput names one URL rule. type RuleIDInput struct { ID uint `json:"id" path:"id" jsonschema:"the URL rule id"` } // CreateRuleInput is the body of POST /_seo/api/v1/rules. type CreateRuleInput struct { URL string `json:"url" jsonschema:"the address: a path with an optional query; with match pattern * matches any text"` Match *string `json:"match,omitempty" jsonschema:"exact (the default) or pattern"` Title *string `json:"title,omitempty" jsonschema:"the SEO title"` Description *string `json:"description,omitempty" jsonschema:"the meta description"` Canonical *string `json:"canonical,omitempty" jsonschema:"the canonical URL"` Robots *string `json:"robots,omitempty" jsonschema:"robots directives, for example noindex,follow"` OGTitle *string `json:"og_title,omitempty" jsonschema:"the social title"` OGDescription *string `json:"og_description,omitempty" jsonschema:"the social description"` OGImage *string `json:"og_image,omitempty" jsonschema:"the social image"` Note *string `json:"note,omitempty" jsonschema:"an internal note"` } // UpdateRuleInput is PUT /_seo/api/v1/rules/{id}: only the fields sent are // changed. type UpdateRuleInput struct { ID uint `json:"id" path:"id" jsonschema:"the URL rule id"` URL *string `json:"url,omitempty" jsonschema:"the address"` Match *string `json:"match,omitempty" jsonschema:"exact or pattern"` Title *string `json:"title,omitempty" jsonschema:"the SEO title"` Description *string `json:"description,omitempty" jsonschema:"the meta description"` Canonical *string `json:"canonical,omitempty" jsonschema:"the canonical URL"` Robots *string `json:"robots,omitempty" jsonschema:"robots directives"` OGTitle *string `json:"og_title,omitempty" jsonschema:"the social title"` OGDescription *string `json:"og_description,omitempty" jsonschema:"the social description"` OGImage *string `json:"og_image,omitempty" jsonschema:"the social image"` 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) out["id"] = r.ID out["created_at"] = r.CreatedAt out["updated_at"] = r.UpdatedAt return out } // ListRules serves GET /_seo/api/v1/rules: URL rules by address, filtered // by search text and match. func ListRules(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { db, err := read(ctx, svc) if err != nil { return 0, nil, err } in := listParams(r) q := db.Model(&models.Rule{}).Order("url ASC, id ASC") if in.Search != "" { p := like(in.Search) q = q.Where("lower(url) LIKE ? OR lower(title) LIKE ? OR lower(description) LIKE ?", p, p, p) } if m := r.URL.Query().Get("match"); m != "" { q = q.Where("match = ?", m) } var rows []models.Rule total, err := paginate(q, in, &rows) if err != nil { return 0, nil, err } out := make([]map[string]any, 0, len(rows)) for _, row := range rows { out = append(out, RuleJSON(row)) } return http.StatusOK, listBody(out, in, total), nil }) } // ShowRule serves GET /_seo/api/v1/rules/{id}. func ShowRule(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { id, err := pathID(r, "id") if err != nil { return 0, nil, err } db, err := read(ctx, svc) if err != nil { return 0, nil, err } rule, err := classes.LoadRule(db, id) if err != nil { return 0, nil, err } return http.StatusOK, map[string]any{"data": RuleJSON(rule)}, nil }) } // CreateRule serves POST /_seo/api/v1/rules: 201 with the rule, 422 for // 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 if err := decode(r, maxBody, &body); err != nil { return 0, nil, err } rule := models.Rule{Match: models.MatchExact} body.apply(&rule) err := write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error { return svc.SaveRule(ctx, tx, &rule) }) if err != nil { return 0, nil, err } return http.StatusCreated, map[string]any{"data": RuleJSON(rule)}, nil }) } // UpdateRule serves PUT /_seo/api/v1/rules/{id}: the fields sent change. func UpdateRule(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { id, err := pathID(r, "id") if err != nil { return 0, nil, err } var body ruleBody if err := decode(r, maxBody, &body); err != nil { return 0, nil, err } var rule models.Rule err = write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error { var err error if rule, err = classes.LoadRule(tx, id); err != nil { return err } body.apply(&rule) return svc.SaveRule(ctx, tx, &rule) }) if err != nil { return 0, nil, err } return http.StatusOK, map[string]any{"data": RuleJSON(rule)}, nil }) } // DeleteRule serves DELETE /_seo/api/v1/rules/{id}. func DeleteRule(service func() *classes.Service) http.HandlerFunc { return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { id, err := pathID(r, "id") if err != nil { return 0, nil, err } err = write(ctx, svc, func(ctx context.Context, tx *gorm.DB) error { rule, err := classes.LoadRule(tx, id) if err != nil { return err } return svc.DeleteRule(ctx, tx, rule) }) if err != nil { return 0, nil, err } return http.StatusOK, map[string]any{"deleted": id}, nil }) }