qsl designer
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package qslcard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Record is one stored template row. JSON holds the template document;
|
||||
// callers Parse/Validate it as needed (the list view only needs metadata).
|
||||
type Record struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProfileID *int64 `json:"profile_id,omitempty"`
|
||||
JSON string `json:"json"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Repo accesses the qsl_templates table.
|
||||
type Repo struct{ db *sql.DB }
|
||||
|
||||
// NewRepo builds a template repo on the given connection.
|
||||
func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }
|
||||
|
||||
const repoCols = `id, name, profile_id, json, is_default, created_at, updated_at`
|
||||
|
||||
// List returns all templates, defaults first then newest first.
|
||||
func (r *Repo) List(ctx context.Context) ([]Record, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT `+repoCols+`
|
||||
FROM qsl_templates ORDER BY is_default DESC, id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Record
|
||||
for rows.Next() {
|
||||
rec, err := scanRecord(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, rec)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Get returns one template by ID, or sql.ErrNoRows if missing.
|
||||
func (r *Repo) Get(ctx context.Context, id int64) (Record, error) {
|
||||
row := r.db.QueryRowContext(ctx, `SELECT `+repoCols+`
|
||||
FROM qsl_templates WHERE id = ?`, id)
|
||||
return scanRecord(row)
|
||||
}
|
||||
|
||||
// Save upserts a template. rec.ID == 0 means "create"; the generated ID is
|
||||
// written back so the caller can place the template's asset folder.
|
||||
func (r *Repo) Save(ctx context.Context, rec *Record) error {
|
||||
if rec.Name == "" {
|
||||
return fmt.Errorf("template name required")
|
||||
}
|
||||
if rec.JSON == "" {
|
||||
return fmt.Errorf("template json required")
|
||||
}
|
||||
now := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
|
||||
if rec.ID == 0 {
|
||||
res, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO qsl_templates (name, profile_id, json, is_default, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
rec.Name, nullableID(rec.ProfileID), rec.JSON, boolInt(rec.IsDefault), now, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert template: %w", err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert template id: %w", err)
|
||||
}
|
||||
rec.ID = id
|
||||
return nil
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE qsl_templates SET name = ?, profile_id = ?, json = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
rec.Name, nullableID(rec.ProfileID), rec.JSON, now, rec.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a template row. The caller removes the asset folder.
|
||||
func (r *Repo) Delete(ctx context.Context, id int64) error {
|
||||
_, err := r.db.ExecContext(ctx, `DELETE FROM qsl_templates WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDefault marks one template as the default for its profile scope,
|
||||
// clearing the flag on every other template of the same scope.
|
||||
func (r *Repo) SetDefault(ctx context.Context, id int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
var profileID sql.NullInt64
|
||||
if err := tx.QueryRowContext(ctx,
|
||||
`SELECT profile_id FROM qsl_templates WHERE id = ?`, id).Scan(&profileID); err != nil {
|
||||
return err
|
||||
}
|
||||
if profileID.Valid {
|
||||
_, err = tx.ExecContext(ctx, `UPDATE qsl_templates SET is_default = 0 WHERE profile_id = ?`, profileID.Int64)
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `UPDATE qsl_templates SET is_default = 0 WHERE profile_id IS NULL`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE qsl_templates SET is_default = 1 WHERE id = ?`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// DefaultFor returns the best template for a profile: its own default, then
|
||||
// any profile-less default, then the newest template. sql.ErrNoRows if none.
|
||||
func (r *Repo) DefaultFor(ctx context.Context, profileID int64) (Record, error) {
|
||||
row := r.db.QueryRowContext(ctx, `SELECT `+repoCols+` FROM qsl_templates
|
||||
ORDER BY (is_default = 1 AND profile_id = ?) DESC,
|
||||
(is_default = 1 AND profile_id IS NULL) DESC,
|
||||
id DESC LIMIT 1`, profileID)
|
||||
return scanRecord(row)
|
||||
}
|
||||
|
||||
// ----- helpers -----
|
||||
|
||||
type scannable interface{ Scan(dest ...any) error }
|
||||
|
||||
func scanRecord(row scannable) (Record, error) {
|
||||
var rec Record
|
||||
var profileID sql.NullInt64
|
||||
var isDefault int
|
||||
var createdAt, updatedAt string
|
||||
if err := row.Scan(&rec.ID, &rec.Name, &profileID, &rec.JSON, &isDefault, &createdAt, &updatedAt); err != nil {
|
||||
return rec, err
|
||||
}
|
||||
if profileID.Valid {
|
||||
v := profileID.Int64
|
||||
rec.ProfileID = &v
|
||||
}
|
||||
rec.IsDefault = isDefault == 1
|
||||
rec.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAt)
|
||||
rec.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAt)
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
func nullableID(p *int64) any {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func boolInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user