package data import ( "time" "database/sql" "errors" "context" "fmt" "party.at/party/internal/validator" ) type Issue struct { ID int64 `json:"id"` Title string `json:"title"` Description string `json:"description"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` N []byte `json:"-"` E int `json:"-"` PrivatePem []byte `json:"-"` Created time.Time `json:"created"` Version int32 `json:"version"` } func ValidateIssue(v *validator.Validator, issue *Issue) { v.Check(issue.Title != "", "title", "must be provided") v.Check(len(issue.Title) <= 500, "title", "must not be more than 500 bytes long") v.Check(issue.Description != "", "description", "must be provided") v.Check(len(issue.Description) <= 500, "description", "must not be greater than 500 bytes long") // v.Check(issue.StartTime != "", "start_time", "must be provided") // v.Check(len(issue.StartTime) <= 500, "start_time", "must not be more than 500 bytes long") // v.Check(issue.EndTime != "", "end_time", "must be provided") // v.Check(len(issue.EndTime) <= 500, "end_time", "must not be more than 500 bytes long") } type IssueModel struct { DB *sql.DB } func (m IssueModel) Insert(issue *Issue) error { query := `INSERT INTO issues (title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created, version` args := []interface{}{ issue.Title, issue.Description, issue.StartTime, issue.EndTime, issue.N, issue.E, issue.PrivatePem, } return m.DB.QueryRow(query, args...).Scan( &issue.ID, &issue.Created, &issue.Version, ) } func (m IssueModel) InsertWithOptions(issue *Issue, options []*Option) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() tx, err := m.DB.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() query := `INSERT INTO issues (title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created, version` args := []any{ issue.Title, issue.Description, issue.StartTime, issue.EndTime, issue.N, issue.E, issue.PrivatePem, } err = tx.QueryRowContext(ctx, query, args...).Scan(&issue.ID, &issue.Created, &issue.Version) if err != nil { return err } for _, option := range options { option.IssueID = issue.ID err = tx.QueryRowContext(ctx, `INSERT INTO options (issue_id, label) VALUES ($1, $2) RETURNING id, created, version`, option.IssueID, option.Label, ).Scan(&option.ID, &option.Created, &option.Version) if err != nil { return err } } err = tx.Commit() if err != nil { return err } return nil } func (m IssueModel) Get(id int64) (*Issue, error) { if id < 1 { return nil, ErrRecordNotFound } query := `SELECT id, title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem, created, version FROM issues WHERE id = $1` var issue Issue err := m.DB.QueryRow(query, id).Scan( &issue.ID, &issue.Title, &issue.Description, &issue.StartTime, &issue.EndTime, &issue.N, &issue.E, &issue.PrivatePem, &issue.Created, &issue.Version, ) if err != nil { switch { case errors.Is(err, sql.ErrNoRows): return nil, ErrRecordNotFound default: return nil, err } } return &issue, nil } func (m IssueModel) Update(issue *Issue) error { query := ` UPDATE issues SET title = $1, description = $2, start_time = $3, end_time = $4, rsa_n = $5, rsa_e = $6, rsa_private_pem = $7, version = version + 1 WHERE id = $8 AND version = $9 RETURNING version` args := []interface{}{ issue.Title, issue.Description, issue.StartTime, issue.EndTime, issue.N, issue.E, issue.PrivatePem, issue.ID, issue.Version, } err := m.DB.QueryRow(query, args...).Scan(&issue.Version) if err != nil { switch { case errors.Is(err, sql.ErrNoRows): return ErrEditConflict default: return err } } return nil } func (m IssueModel) Delete(id int64) error { if id < 1 { return ErrRecordNotFound } query := ` DELETE FROM issues WHERE id = $1` result, err := m.DB.Exec(query, id) if err != nil { return err } rowsAffected, err := result.RowsAffected() if err != nil { return err } if rowsAffected == 0 { return ErrRecordNotFound } return nil } func (m IssueModel) GetAll(title string, filters Filters) ([]*Issue, Metadata, error) { query := fmt.Sprintf(` SELECT COUNT(*) OVER(), id, title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem, created, version FROM issues WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '') ORDER BY %s %s, id ASC LIMIT $2 OFFSET $3`, filters.sortColumn(), filters.sortDirection(), ) ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second) defer cancel() args := []interface{}{title, filters.limit(), filters.offset()} rows, err := m.DB.QueryContext(ctx, query, args...) if err != nil { return nil, Metadata{}, err } defer rows.Close() totalRecords := 0 issues := []*Issue{} for rows.Next() { var issue Issue err := rows.Scan( &totalRecords, &issue.ID, &issue.Title, &issue.Description, &issue.StartTime, &issue.EndTime, &issue.N, &issue.E, &issue.PrivatePem, &issue.Created, &issue.Version, ) if err != nil { return nil, Metadata{}, err } issues = append(issues, &issue) } if err = rows.Err(); err != nil { return nil, Metadata{}, err } metadata := calculateMetadata(totalRecords, filters.Page, filters.PageSize) return issues, metadata, nil }