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"` 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) VALUES ($1, $2, $3, $4) RETURNING id, created, version` args := []interface{}{ issue.Title, issue.Description, issue.StartTime, issue.EndTime, } 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 } // Define the SQL query for retrieving the issue data. query :=` SELECT id, title, description, start_time, end_time, created, version FROM issues WHERE id = $1` // Declare a Issue struct to hold the data returned by the query. var issue Issue // Execute the query using the QueryRow() method, passing in the provided id value // as a placeholder parameter, and scan the response data into the fields of the // Issue struct. Importantly, notice that we need to convert the scan target for the // genres column using the pq.Array() adapter function again. err := m.DB.QueryRow(query, id).Scan( &issue.ID, &issue.Title, &issue.Description, &issue.StartTime, &issue.EndTime, &issue.Created, &issue.Version, ) // Handle any errors. If there was no matching issue found, Scan() will return // a sql.ErrNoRows error. We check for this and return our custom ErrRecordNotFound // error instead. if err != nil { switch { case errors.Is(err, sql.ErrNoRows): return nil, ErrRecordNotFound default: return nil, err } } // Otherwise, return a pointer to the Issue struct. return &issue, nil } func (m IssueModel) Update(issue *Issue) error { query := ` UPDATE issues SET title = $1, description = $2, start_time = $3, end_time = $4, version = version + 1 WHERE id = $5 AND version = $6 RETURNING version` // Create an args slice containing the values for the placeholder parameters. args := []interface{}{ issue.Title, issue.Description, issue.StartTime, issue.EndTime, issue.ID, issue.Version, } // Use the QueryRow() method to execute the query, passing in the args slice as a // variadic parameter and scanning the new version value into the issue struct. 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, 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.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 }