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 }