package classes import ( "context" "errors" "strconv" "strings" "time" "git.golem15.com/golem15/sm-seo-plugin/models" "gorm.io/gorm" ) // ErrNoRecord is returned for an id that names no row. var ErrNoRecord = errors.New("golem15.seo: record not found") // LoadRule returns rule id; ErrNoRecord when there is none. func LoadRule(db *gorm.DB, id uint) (models.Rule, error) { var rows []models.Rule if err := db.Where("id = ?", id).Limit(1).Find(&rows).Error; err != nil { return models.Rule{}, err } if len(rows) == 0 { return models.Rule{}, ErrNoRecord } return rows[0], nil } // SaveRule validates and stores r inside tx and records it in history: the // address is trimmed and the match normalised, robots values are brought // to the C5 grammar, and a canonical and the social image must be a path // or an http(s) URL. Field problems are refused with a *ProblemsError. func (s *Service) SaveRule(ctx context.Context, tx *gorm.DB, r *models.Rule) error { if r == nil { return errors.New("golem15.seo: no rule") } r.URL = strings.TrimSpace(r.URL) r.Match = models.NormalizeMatch(r.Match) problems := r.Problems(tx) if v := strings.TrimSpace(r.Robots); v != "" { normal, err := NormalizeRobots(v) if err != nil { problems["robots"] = append(problems["robots"], models.Problem{Key: "golem15.seo::lang.robots.invalid"}) } r.Robots = normal } if v := strings.TrimSpace(r.Canonical); v != "" && !ValidURLRef(v) { problems["canonical"] = append(problems["canonical"], models.Problem{Key: "golem15.seo::lang.api.canonical_invalid"}) } r.OGImage = strings.TrimSpace(r.OGImage) if !ValidImageURL(r.OGImage) { problems["og_image"] = append(problems["og_image"], models.Problem{Key: errImageURL}) } if len(problems) > 0 { return &ProblemsError{Problems: problems} } now := time.Now().UTC() var before map[string]any if r.ID == 0 { r.CreatedAt, r.UpdatedAt = &now, &now if err := tx.Create(r).Error; err != nil { return err } } else { old, err := LoadRule(tx, r.ID) if err != nil { return err } before = RuleValues(old) r.CreatedAt, r.UpdatedAt = old.CreatedAt, &now if err := tx.Save(r).Error; err != nil { return err } } return s.Record(ctx, tx, Change{ Kind: models.SubjectRule, SubjectID: strconv.FormatUint(uint64(r.ID), 10), What: s.Phrase("golem15.seo::lang.history.what_rule", map[string]string{"url": r.URL}), Before: before, After: RuleValues(*r), }) } // DeleteRule deletes r inside tx and records it in history. func (s *Service) DeleteRule(ctx context.Context, tx *gorm.DB, r models.Rule) error { if err := tx.Delete(&models.Rule{}, r.ID).Error; err != nil { return err } return s.Record(ctx, tx, Change{ Kind: models.SubjectRule, SubjectID: strconv.FormatUint(uint64(r.ID), 10), What: s.Phrase("golem15.seo::lang.history.what_rule", map[string]string{"url": r.URL}), Before: RuleValues(r), }) } // SetRuleValue writes one value column of a rule; an unknown column is // ignored. func SetRuleValue(r *models.Rule, column, value string) { switch column { case "url": r.URL = value case "match": r.Match = value case "note": r.Note = value case "title": r.Title = value case "description": r.Description = value case "canonical": r.Canonical = value case "robots": r.Robots = value case "og_title": r.OGTitle = value case "og_description": r.OGDescription = value case "og_image": r.OGImage = value } }