package data import ( "time" "party.at/party/internal/validator" "database/sql" "errors" "context" "fmt" ) 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, ) } // Add a placeholder method for fetching a specific record from the issues table. 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 } // Add a placeholder method for deleting a specific record from the issues table. func (m IssueModel) Delete(id int64) error { if id < 1 { return ErrRecordNotFound } // Construct the SQL query to delete the record. query := ` DELETE FROM issues WHERE id = $1` // Execute the SQL query using the Exec() method, passing in the id variable as // the value for the placeholder parameter. The Exec() method returns a sql.Result // object. result, err := m.DB.Exec(query, id) if err != nil { return err } // Call the RowsAffected() method on the sql.Result object to get the number of rows // affected by the query. rowsAffected, err := result.RowsAffected() if err != nil { return err } // If no rows were affected, we know that the issues table didn't contain a record // with the provided ID at the moment we tried to delete it. In that case we // return an ErrRecordNotFound error. if rowsAffected == 0 { return ErrRecordNotFound } return nil } func (m IssueModel) GetAll(title string, filters Filters) ([]*Issue, Metadata, error) { // Construct the SQL query to retrieve all issue records. 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(), ) // Create a context with a 3-second timeout. 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 } // Importantly, defer a call to rows.Close() to ensure that the resultset is closed // before GetAll() returns. defer rows.Close() totalRecords := 0 issues := []*Issue{} // Use rows.Next to iterate through the rows in the resultset. for rows.Next() { // Initialize an empty Issue struct to hold the data for an individual issue. var issue Issue // Scan the values from the row into the Issue struct. Again, note that we're // using the pq.Array() adapter on the genres field here. 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 } // Add the Issue struct to the slice. issues = append(issues, &issue) } // When the rows.Next() loop has finished, call rows.Err() to retrieve any error // that was encountered during the iteration. if err = rows.Err(); err != nil { return nil, Metadata{}, err } metadata := calculateMetadata(totalRecords, filters.Page, filters.PageSize) // If everything went OK, then return the slice of issues. return issues, metadata, nil }