// SPDX-License-Identifier: MPL-2.0 package cmssqlite import ( "context" "database/sql" "errors" "gamertan.com/web/cms" ) func notFound(err error) error { if errors.Is(err, sql.ErrNoRows) { return cms.ErrNotFound } return err } func (r *Reader) Taxonomy(ctx context.Context, id string) (cms.Taxonomy, error) { var v cms.Taxonomy err := r.db.QueryRowContext(ctx, `SELECT id,slug,name,description,revision,active FROM gwf_cms_taxonomies WHERE scope=? AND id=?`, r.scope, id).Scan(&v.ID, &v.Slug, &v.Name, &v.Description, &v.Revision, &v.Active) return v, notFound(err) } func (r *Reader) Taxonomies(ctx context.Context) ([]cms.Taxonomy, error) { rows, err := r.db.QueryContext(ctx, `SELECT id,slug,name,description,revision,active FROM gwf_cms_taxonomies WHERE scope=? ORDER BY slug LIMIT 101`, r.scope) if err != nil { return nil, err } defer rows.Close() values := []cms.Taxonomy{} for rows.Next() { var v cms.Taxonomy if err := rows.Scan(&v.ID, &v.Slug, &v.Name, &v.Description, &v.Revision, &v.Active); err != nil { return nil, err } values = append(values, v) } if len(values) > 100 { return nil, cms.ErrInvalid } return values, rows.Err() } func PutTaxonomy(ctx context.Context, tx *sql.Tx, scope string, v cms.Taxonomy, expected int64) error { if !cms.ValidID(scope) || v.Validate() != nil || expected < 0 || v.Revision != expected+1 { return cms.ErrInvalid } var result sql.Result var err error if expected == 0 { var count int if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM gwf_cms_taxonomies WHERE scope=?`, scope).Scan(&count); err != nil { return err } if count >= 100 { return cms.ErrInvalid } result, err = tx.ExecContext(ctx, `INSERT INTO gwf_cms_taxonomies(scope,id,slug,name,description,revision,active) VALUES(?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, scope, v.ID, v.Slug, v.Name, v.Description, v.Revision, v.Active) } else { result, err = tx.ExecContext(ctx, `UPDATE gwf_cms_taxonomies SET name=?,description=?,revision=?,active=? WHERE scope=? AND id=? AND revision=? AND slug=?`, v.Name, v.Description, v.Revision, v.Active, scope, v.ID, expected, v.Slug) } return oneRow(result, err) } func oneRow(result sql.Result, err error) error { if err != nil { return err } n, err := result.RowsAffected() if err != nil { return err } if n != 1 { return cms.ErrConflict } return nil } func (r *Reader) Term(ctx context.Context, id string) (cms.Term, error) { var v cms.Term err := r.db.QueryRowContext(ctx, `SELECT id,taxonomy_id,slug,name,description,revision,active FROM gwf_cms_terms WHERE scope=? AND id=?`, r.scope, id).Scan(&v.ID, &v.TaxonomyID, &v.Slug, &v.Name, &v.Description, &v.Revision, &v.Active) return v, notFound(err) } // Terms pages by immutable ID, including retired values for editors. The caller // filters public availability using both taxonomy and term Active fields. func (r *Reader) Terms(ctx context.Context, taxonomyID, after string, limit int) ([]cms.Term, error) { if !cms.ValidID(taxonomyID) || (after != "" && !cms.ValidID(after)) || limit < 1 || limit > 200 { return nil, cms.ErrInvalid } rows, err := r.db.QueryContext(ctx, `SELECT id,taxonomy_id,slug,name,description,revision,active FROM gwf_cms_terms WHERE scope=? AND taxonomy_id=? AND id>? ORDER BY id LIMIT ?`, r.scope, taxonomyID, after, limit) if err != nil { return nil, err } defer rows.Close() values := []cms.Term{} for rows.Next() { var v cms.Term if err := rows.Scan(&v.ID, &v.TaxonomyID, &v.Slug, &v.Name, &v.Description, &v.Revision, &v.Active); err != nil { return nil, err } values = append(values, v) } return values, rows.Err() } // TermBySlug resolves old term slugs to the current record. The caller redirects // to v.Slug after checking visibility; no private/retired term is published here. func (r *Reader) TermBySlug(ctx context.Context, taxonomyID, slug string) (cms.Term, error) { var id string err := r.db.QueryRowContext(ctx, `SELECT id FROM gwf_cms_terms WHERE scope=? AND taxonomy_id=? AND slug=? UNION SELECT term_id FROM gwf_cms_term_slugs WHERE scope=? AND taxonomy_id=? AND slug=? LIMIT 1`, r.scope, taxonomyID, slug, r.scope, taxonomyID, slug).Scan(&id) if err != nil { return cms.Term{}, notFound(err) } return r.Term(ctx, id) } func PutTerm(ctx context.Context, tx *sql.Tx, scope string, v cms.Term, expected int64) error { if !cms.ValidID(scope) || v.Validate() != nil || expected < 0 || v.Revision != expected+1 { return cms.ErrInvalid } r, _ := New(tx, scope) tax, err := r.Taxonomy(ctx, v.TaxonomyID) if err != nil { return err } if !tax.Active && v.Active { return cms.ErrInvalid } var old cms.Term if expected > 0 { old, err = r.Term(ctx, v.ID) if err != nil { return err } if old.Revision != expected || old.TaxonomyID != v.TaxonomyID { return cms.ErrConflict } } occupied, err := r.TermBySlug(ctx, v.TaxonomyID, v.Slug) if err == nil && occupied.ID != v.ID { return cms.ErrConflict } if err != nil && !errors.Is(err, cms.ErrNotFound) { return err } var result sql.Result if expected == 0 { result, err = tx.ExecContext(ctx, `INSERT INTO gwf_cms_terms(scope,id,taxonomy_id,slug,name,description,revision,active) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, scope, v.ID, v.TaxonomyID, v.Slug, v.Name, v.Description, v.Revision, v.Active) } else { result, err = tx.ExecContext(ctx, `UPDATE gwf_cms_terms SET slug=?,name=?,description=?,revision=?,active=? WHERE scope=? AND id=? AND revision=?`, v.Slug, v.Name, v.Description, v.Revision, v.Active, scope, v.ID, expected) } if err = oneRow(result, err); err != nil { return err } if expected > 0 && old.Slug != v.Slug { if _, err = tx.ExecContext(ctx, `DELETE FROM gwf_cms_term_slugs WHERE scope=? AND taxonomy_id=? AND slug=? AND term_id=?`, scope, v.TaxonomyID, v.Slug, v.ID); err != nil { return err } _, err = tx.ExecContext(ctx, `INSERT INTO gwf_cms_term_slugs(scope,taxonomy_id,slug,term_id) VALUES(?,?,?,?)`, scope, v.TaxonomyID, old.Slug, v.ID) } return err }