feat(08-01): add custom_dorks table and CRUD for user-authored dorks
- schema.sql: CREATE TABLE IF NOT EXISTS custom_dorks with unique dork_id, source/category indexes, and tags stored as JSON TEXT - custom_dorks.go: Save/List/Get/GetByDorkID/Delete with JSON tag round-trip - Tests: round-trip, newest-first ordering, not-found, unique constraint, delete no-op, schema migration idempotency
This commit is contained in:
145
pkg/storage/custom_dorks.go
Normal file
145
pkg/storage/custom_dorks.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CustomDork is a user-authored dork persisted to the custom_dorks table.
|
||||
// It parallels pkg/dorks.Dork but owns its own id (auto-increment) and
|
||||
// timestamp. DorkID is the stable string identifier exposed to users, and
|
||||
// is constrained UNIQUE at the SQL layer.
|
||||
type CustomDork struct {
|
||||
ID int64
|
||||
DorkID string
|
||||
Name string
|
||||
Source string
|
||||
Category string
|
||||
Query string
|
||||
Description string
|
||||
Tags []string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// SaveCustomDork inserts a custom dork row and returns its auto-generated
|
||||
// primary key. Tags are serialised to JSON for storage in the TEXT column.
|
||||
func (db *DB) SaveCustomDork(d CustomDork) (int64, error) {
|
||||
tagsJSON, err := marshalTags(d.Tags)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("encoding tags: %w", err)
|
||||
}
|
||||
res, err := db.sql.Exec(`
|
||||
INSERT INTO custom_dorks (dork_id, name, source, category, query, description, tags)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, d.DorkID, d.Name, d.Source, d.Category, d.Query, d.Description, tagsJSON)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("inserting custom dork: %w", err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("fetching insert id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ListCustomDorks returns every custom dork ordered newest-first.
|
||||
func (db *DB) ListCustomDorks() ([]CustomDork, error) {
|
||||
rows, err := db.sql.Query(`
|
||||
SELECT id, dork_id, name, source, category, query, description, tags, created_at
|
||||
FROM custom_dorks
|
||||
ORDER BY created_at DESC, id DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying custom dorks: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []CustomDork
|
||||
for rows.Next() {
|
||||
d, err := scanCustomDork(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, d)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetCustomDork returns the custom dork with the given auto-increment id.
|
||||
// It returns sql.ErrNoRows when no such row exists.
|
||||
func (db *DB) GetCustomDork(id int64) (CustomDork, error) {
|
||||
row := db.sql.QueryRow(`
|
||||
SELECT id, dork_id, name, source, category, query, description, tags, created_at
|
||||
FROM custom_dorks WHERE id = ?
|
||||
`, id)
|
||||
return scanCustomDork(row)
|
||||
}
|
||||
|
||||
// GetCustomDorkByDorkID looks up a custom dork by its user-facing dork_id.
|
||||
// It returns sql.ErrNoRows when no match exists.
|
||||
func (db *DB) GetCustomDorkByDorkID(dorkID string) (CustomDork, error) {
|
||||
row := db.sql.QueryRow(`
|
||||
SELECT id, dork_id, name, source, category, query, description, tags, created_at
|
||||
FROM custom_dorks WHERE dork_id = ?
|
||||
`, dorkID)
|
||||
return scanCustomDork(row)
|
||||
}
|
||||
|
||||
// DeleteCustomDork removes the row with the given id and returns the number
|
||||
// of rows affected (0 or 1).
|
||||
func (db *DB) DeleteCustomDork(id int64) (int64, error) {
|
||||
res, err := db.sql.Exec(`DELETE FROM custom_dorks WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("deleting custom dork: %w", err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("rows affected: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// rowScanner abstracts *sql.Row and *sql.Rows so scanCustomDork can serve
|
||||
// both single-row Get and multi-row List paths.
|
||||
type rowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanCustomDork(r rowScanner) (CustomDork, error) {
|
||||
var (
|
||||
d CustomDork
|
||||
description sql.NullString
|
||||
tagsJSON sql.NullString
|
||||
)
|
||||
if err := r.Scan(
|
||||
&d.ID, &d.DorkID, &d.Name, &d.Source, &d.Category, &d.Query,
|
||||
&description, &tagsJSON, &d.CreatedAt,
|
||||
); err != nil {
|
||||
return CustomDork{}, err
|
||||
}
|
||||
if description.Valid {
|
||||
d.Description = description.String
|
||||
}
|
||||
if tagsJSON.Valid && tagsJSON.String != "" {
|
||||
if err := json.Unmarshal([]byte(tagsJSON.String), &d.Tags); err != nil {
|
||||
return CustomDork{}, fmt.Errorf("decoding tags: %w", err)
|
||||
}
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func marshalTags(tags []string) (string, error) {
|
||||
if len(tags) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
b, err := json.Marshal(tags)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
Reference in New Issue
Block a user