Adding Insert(), GetByEmail(), Update() UserModel's methods.
Some checks failed
Deploy Greenlight API / deploy (push) Failing after 51s
Some checks failed
Deploy Greenlight API / deploy (push) Failing after 51s
This commit is contained in:
6
.idea/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$PROJECT_DIR$/internal/data/users.go" value="ca630426-342e-416b-ad62-190643f9cf1e" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -15,11 +15,13 @@ var (
|
||||
// Models : Wraps the MovieModel. We'll add other models to this, like a UserModel and PermissionModel, as our build progresses.
|
||||
type Models struct {
|
||||
Movies MovieModel
|
||||
Users UserModel
|
||||
}
|
||||
|
||||
// NewModels : For ease of use, this method returns a Models struct containing the initialized MovieModel.
|
||||
func NewModels(db *sql.DB) Models {
|
||||
return Models{
|
||||
Movies: MovieModel{DB: db},
|
||||
Users: UserModel{DB: db},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
@@ -8,6 +10,10 @@ import (
|
||||
"greenlight.craftr.fr/internal/validator"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDuplicateEmail = errors.New("duplicate email")
|
||||
)
|
||||
|
||||
// User struct represent an individual user. Importantly, notice how we are using the json:"-" struct tag to prevent the Password and Version fields appearing in any output when we encode it to JSON. Also notice that the Password field uses the custom password type defined below.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
@@ -81,3 +87,103 @@ func ValidateUser(v *validator.Validator, user *User) {
|
||||
panic("missing password hash for user")
|
||||
}
|
||||
}
|
||||
|
||||
// UserModel struct which wraps the connection pool
|
||||
type UserModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// Insert a new record in the database for the user. Note that the id, created_at and version fields are all automatically generated by our database, so we use the RETURNING clause to read them into the User struct after the insert, in the same way that we did when creating a movie
|
||||
func (m UserModel) Insert(user *User) error {
|
||||
query := `
|
||||
INSERT INTO users (name, email, password_hash, activated)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, created_at, version`
|
||||
|
||||
args := []any{user.Name, user.Email, user.Password.hash, user.Activated}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// If the table already contains a record with this email address, then when we try to perform the insert, there will be a violation of the UNIQUE "users_email_key" constraint we set up in the previous chapter. We check for this error specifically, and return custom ErrDuplicateEmail error instead
|
||||
err := m.DB.QueryRowContext(ctx, query, args...).Scan(&user.ID, &user.CreatedAt, &user.Version)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
|
||||
return ErrDuplicateEmail
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByEmail : Retrieve the User details from the database based on the user's email address. Because we have a UNIQUE constraint on the email column, this SQL query will only return one record (or none at all, in which case we return a ErrRecordNotFound error)
|
||||
func (m UserModel) GetByEmail(email string) (*User, error) {
|
||||
query := `
|
||||
SELECT id, created_at, name, email, password_hash, activated, version
|
||||
FROM users
|
||||
WHERE email=$1`
|
||||
|
||||
var user User
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.DB.QueryRowContext(ctx, query, email).Scan(
|
||||
&user.ID,
|
||||
&user.CreatedAt,
|
||||
&user.Name,
|
||||
&user.Email,
|
||||
&user.Password.hash,
|
||||
&user.Activated,
|
||||
&user.Version,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, ErrRecordNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update : the details for a specific user. Notice that we check against the version field to help prevent any race conditions during the request cycle, just like we did when updating a movie. And we also check for a violation of the "users_email_key" constraint when performing the update, just like we did when inserting the user record originally.
|
||||
func (m UserModel) Update(user *User) error {
|
||||
query := `
|
||||
UPDATE users
|
||||
SET name = $1, email = $2, password_hash = $3, activated = $4, version = version + 1
|
||||
WHERE id = $5 AND version = $6
|
||||
RETURNING version`
|
||||
|
||||
args := []any{
|
||||
user.Name,
|
||||
user.Email,
|
||||
user.Password.hash,
|
||||
user.Activated,
|
||||
user.ID,
|
||||
user.Version,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := m.DB.QueryRowContext(ctx, query, args...).Scan(&user.Version)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
|
||||
return ErrDuplicateEmail
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return ErrEditConflict
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user