You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
158 lines
4.5 KiB
Go
158 lines
4.5 KiB
Go
package model
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
type Snippet struct {
|
|
ID int
|
|
// Title string
|
|
// Content string
|
|
Title sql.NullString
|
|
Content sql.NullString
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
func (s *Snippet) GetTitle() {
|
|
return
|
|
}
|
|
|
|
type SnippetServiceInterface interface {
|
|
Insert(title, content string, expiresAt int) (int, error)
|
|
Get(id int) (Snippet, error)
|
|
Lastest() ([]Snippet, error)
|
|
}
|
|
|
|
type SnippetService struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
// Insert inserts a new SnippetModel into the database
|
|
func (s *SnippetService) Insert(title, content string, expiresAt int) (int, error) {
|
|
slog.Debug(fmt.Sprintf("Inserting new snippet. Title: %s", title))
|
|
// Really don't prepare statements. There are a lot of gotcha's where they exist on the connection objects they were created. They can potentially
|
|
// recreate connections. It's an optimization you probably don't need at the moment.
|
|
stmt, err := s.DB.Prepare("INSERT INTO snippets (title, content, expires_at) VALUES ($1, $2, DATETIME(CURRENT_TIMESTAMP, '+' || $3 || ' DAY'))")
|
|
if err != nil {
|
|
slog.Debug("The prepared statement has an error")
|
|
return 0, err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
// stmt.Exec returns a sql.Result. That also has access to the statement metadata
|
|
// use _ if you don't care about the result and only want to check the err.
|
|
|
|
// Exec will NOT reserve a connection. unlike db.Query which returns a sql.Rows that
|
|
// will hold on to a connection until .Close() is called.
|
|
res, err := stmt.Exec(title, content, expiresAt)
|
|
if err != nil {
|
|
slog.Debug("SQL DML statement returned an error.")
|
|
return 0, err
|
|
}
|
|
|
|
// Use the LastInsertId() method on the result to get the ID of our
|
|
// newly inserted record in the snippets table.
|
|
lastId, err := res.LastInsertId()
|
|
if err != nil {
|
|
slog.Debug("An error occured when retrieving insert result id.")
|
|
return 0, err
|
|
}
|
|
|
|
// The ID returned has the type int64, so we convert it to an int type
|
|
// before returning.
|
|
slog.Debug(fmt.Sprintf("Inserted new snippet. Snippet pk: %d", int(lastId)))
|
|
return int(lastId), nil
|
|
}
|
|
|
|
// Get retrieves a specific Snippet by ID ignoring the record if expired.
|
|
func (s *SnippetService) Get(id int) (Snippet, error) {
|
|
|
|
stmt := `SELECT id, title, content, created_at, updated_at, expires_at FROM snippets
|
|
WHERE expires_at > CURRENT_TIMESTAMP AND id = $1`
|
|
|
|
// errors from DB.QueryRow() are deferred until Scan() is called.
|
|
// meaning you could also have used DB.QueryRow(...).Scan(...)
|
|
row := s.DB.QueryRow(stmt, id)
|
|
|
|
var snip Snippet
|
|
|
|
err := row.Scan(&snip.ID,
|
|
&snip.Title,
|
|
&snip.Content,
|
|
&snip.CreatedAt,
|
|
&snip.UpdatedAt,
|
|
&snip.ExpiresAt,
|
|
)
|
|
|
|
if err != nil {
|
|
slog.Debug("SQL DML statement returned an error.")
|
|
// Loop up the difference between errors.Is and errors.As
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return Snippet{}, ErrNoRecord
|
|
} else {
|
|
return Snippet{}, err
|
|
}
|
|
}
|
|
|
|
return snip, nil
|
|
}
|
|
|
|
// Latest retrieves up to latest 10 Snippets from the database.
|
|
func (s *SnippetService) Lastest() ([]Snippet, error) {
|
|
|
|
stmt := `SELECT id, title, content, created_at, updated_at, expires_at FROM snippets
|
|
WHERE expires_at > CURRENT_TIMESTAMP ORDER BY id DESC LIMIT 10`
|
|
|
|
rows, err := s.DB.Query(stmt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We defer rows.Close() to ensure the sql.Rows resultset is
|
|
// always properly closed before the Latest() method returns. This defer
|
|
// statement should come *after* you check for an error from the Query()
|
|
// method. Otherwise, if Query() returns an error, you'll get a panic
|
|
// trying to close a nil resultset.
|
|
defer rows.Close()
|
|
|
|
var snippets []Snippet
|
|
|
|
// Use rows.Next to iterate through the rows in the resultset. This
|
|
// prepares the first (and then each subsequent) row to be acted on by the
|
|
// rows.Scan() method. If iteration over all the rows completes then the
|
|
// resultset automatically closes itself and frees-up the underlying
|
|
// database connection.
|
|
for rows.Next() {
|
|
var snip Snippet
|
|
|
|
err := rows.Scan(&snip.ID,
|
|
&snip.Title,
|
|
&snip.Content,
|
|
&snip.CreatedAt,
|
|
&snip.UpdatedAt,
|
|
&snip.ExpiresAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
snippets = append(snippets, snip)
|
|
}
|
|
|
|
// When the rows.Next() loop has finished we call rows.Err() to retrieve any
|
|
// error that was encountered during the iteration. It's important to
|
|
// call this - don't assume that a successful iteration was completed
|
|
// over the whole resultset.
|
|
if err = rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return snippets, nil
|
|
}
|