diff --git a/db/migrations/1657379568_create_courses_table.up.sql b/db/migrations/1657379568_create_courses_table.up.sql index 4ea480a..db913cd 100644 --- a/db/migrations/1657379568_create_courses_table.up.sql +++ b/db/migrations/1657379568_create_courses_table.up.sql @@ -2,9 +2,9 @@ BEGIN; CREATE TABLE courses ( - id bigserial CONSTRAINT courses_pk PRIMARY KEY, + id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY, uuid uuid DEFAULT uuid_generate_v4() NOT NULL, - code varchar NOT NULL UNIQUE, + code varchar NOT NULL, name varchar NOT NULL, underline varchar NOT NULL, image varchar NULL, @@ -16,9 +16,11 @@ CREATE TABLE courses deleted_at timestamp ); -CREATE UNIQUE INDEX courses_id_uindex - ON courses (id); +COMMENT ON COLUMN courses.deleted_at IS 'Timestamp indicating when a course was softly deleted, allowing for data recovery. A NULL value means the course is active.'; + CREATE UNIQUE INDEX courses_uuid_uindex ON courses (uuid); +CREATE UNIQUE INDEX courses_code_uindex + ON courses (code, deleted_at) NULLS NOT DISTINCT; COMMIT; diff --git a/internal/course/database/course_queries.go b/internal/course/database/course_queries.go index 967831c..fbdfd68 100644 --- a/internal/course/database/course_queries.go +++ b/internal/course/database/course_queries.go @@ -1,27 +1,39 @@ package database +import ( + "fmt" +) + const ( - createCourse = "create course" - deleteCourse = "delete course by uuid" - getCourse = "get course by uuid" - listCourse = "list course" - updateCourseByID = "update course by uuid" - updateCourseByCode = "update course by code" + returningColumns = "uuid, code, name, underline, image, image_cover, excerpt, description, created_at, updated_at" + // CREATE. + createCourse = "create course" + // READ. + listCourse = "list course" + getCourse = "get course by uuid" + // UPDATE. + updateCourse = "update course by uuid" + // DELETE. + deleteCourse = "delete course by uuid" ) func queriesCourse() map[string]string { return map[string]string{ - createCourse: `INSERT INTO - courses (code, name, underline, image, image_cover, excerpt, description) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, + // CREATE. + createCourse: fmt.Sprintf(`INSERT INTO courses ( + code, name, underline, image, image_cover, excerpt, description + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING %s`, returningColumns), + // READ. + listCourse: fmt.Sprintf("SELECT %s FROM courses WHERE deleted_at IS NULL", returningColumns), + getCourse: fmt.Sprintf("SELECT %s FROM courses WHERE uuid = $1 AND deleted_at IS NULL", returningColumns), + // UPDATE. + updateCourse: fmt.Sprintf(`UPDATE courses SET + code = $1, name = $2, underline = $3, image = $4, image_cover = $5, excerpt = $6, description = $7 + WHERE uuid = $8 + AND deleted_at IS NULL + RETURNING %s`, returningColumns), + // DELETE. deleteCourse: "UPDATE courses SET deleted_at = NOW() WHERE uuid = $1 AND deleted_at IS NULL", - getCourse: "SELECT * FROM courses WHERE uuid = $1 AND deleted_at IS NULL", - listCourse: "SELECT * FROM courses WHERE deleted_at IS NULL", - updateCourseByID: `UPDATE courses - SET code = $1, name = $2, underline = $3, image = $4, image_cover = $5, excerpt = $6, description = $7 - WHERE uuid = $8 AND deleted_at IS NULL RETURNING *`, - updateCourseByCode: `UPDATE courses - SET name = $1, underline = $2, image = $3, image_cover = $4, excerpt = $5, description = $6 - WHERE code = $7 AND deleted_at IS NULL RETURNING *`, } } diff --git a/internal/course/database/course_repository.go b/internal/course/database/course_repository.go index 4cf7242..b4558cd 100644 --- a/internal/course/database/course_repository.go +++ b/internal/course/database/course_repository.go @@ -36,15 +36,15 @@ func (r CourseRepository) statement(s string) (*sqlx.Stmt, error) { return stmt, nil } -// Course get the Course by given id. -func (r CourseRepository) Course(id uuid.UUID) (domain.Course, error) { +// Course get the Course by given uuid. +func (r CourseRepository) Course(courseUUID uuid.UUID) (domain.Course, error) { stmt, err := r.statement(getCourse) if err != nil { return domain.Course{}, err } var c domain.Course - if err := stmt.Get(&c, id); err != nil { + if err := stmt.Get(&c, courseUUID); err != nil { return domain.Course{}, errors.WrapErrorf(err, errors.ErrCodeUnknown, "error getting course") } return c, nil @@ -86,9 +86,9 @@ func (r CourseRepository) CreateCourse(c *domain.Course) error { return nil } -// UpdateCourseByID update the given course by ID. -func (r CourseRepository) UpdateCourseByID(c *domain.Course) error { - stmt, err := r.statement(updateCourseByID) +// UpdateCourse update the given course by ID. +func (r CourseRepository) UpdateCourse(c *domain.Course) error { + stmt, err := r.statement(updateCourse) if err != nil { return err } @@ -111,38 +111,14 @@ func (r CourseRepository) UpdateCourseByID(c *domain.Course) error { return nil } -func (r CourseRepository) UpdateCourseByCode(c *domain.Course) error { - stmt, err := r.statement(updateCourseByCode) - if err != nil { - return err - } - - args := []interface{}{ - // set - c.Name, - c.Underline, - c.Image, - c.ImageCover, - c.Excerpt, - c.Description, - // where - c.Code, - } - - if err := stmt.Get(c, args...); err != nil { - return errors.WrapErrorf(err, errors.ErrCodeUnknown, "error updating course") - } - return nil -} - -// DeleteCourse soft delete the course by given id. -func (r CourseRepository) DeleteCourse(id uuid.UUID) error { +// DeleteCourse soft delete the course by given uuid. +func (r CourseRepository) DeleteCourse(courseUUID uuid.UUID) error { stmt, err := r.statement(deleteCourse) if err != nil { return err } - if _, err := stmt.Exec(id); err != nil { + if _, err := stmt.Exec(courseUUID); err != nil { return errors.WrapErrorf(err, errors.ErrCodeUnknown, "error deleting course") } return nil diff --git a/internal/course/database/course_repository_test.go b/internal/course/database/course_repository_test.go index cde1fd3..6f95ff2 100644 --- a/internal/course/database/course_repository_test.go +++ b/internal/course/database/course_repository_test.go @@ -14,7 +14,6 @@ import ( var ( course = domain.Course{ - ID: 1, UUID: utils.CourseUUID, Code: "SUME123", Name: "Course Name", @@ -25,7 +24,6 @@ var ( Description: "Course Description", CreatedAt: utils.Now, UpdatedAt: utils.Now, - DeletedAt: nil, } ) @@ -34,13 +32,13 @@ func newCourseTestDB() (*sqlx.DB, sqlmock.Sqlmock, map[string]*sqlmock.ExpectedP } func TestRepository_Course(t *testing.T) { - validRows := sqlmock.NewRows([]string{"id", "uuid", "code", "name", "underline", "image", "image_cover", "excerpt", - "description", "created_at", "updated_at", "deleted_at"}). - AddRow(course.ID, course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, - course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt, course.DeletedAt) + validRows := sqlmock.NewRows([]string{"uuid", "code", "name", "underline", "image", "image_cover", "excerpt", + "description", "created_at", "updated_at"}). + AddRow(course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, + course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt) type args struct { - id uuid.UUID + courseUUID uuid.UUID } tests := []struct { @@ -52,14 +50,14 @@ func TestRepository_Course(t *testing.T) { }{ { name: "get course", - args: args{id: course.UUID}, + args: args{courseUUID: course.UUID}, rows: validRows, want: course, wantErr: false, }, { name: "course not found error", - args: args{id: uuid.MustParse("6cd7a01c-ff18-4cfb-9b35-16e710115c5f")}, + args: args{courseUUID: uuid.MustParse("6cd7a01c-ff18-4cfb-9b35-16e710115c5f")}, rows: utils.EmptyRows, want: domain.Course{}, wantErr: true, @@ -83,7 +81,7 @@ func TestRepository_Course(t *testing.T) { prep.ExpectQuery().WithArgs(utils.CourseUUID).WillReturnRows(validRows) - got, err := r.Course(tt.args.id) + got, err := r.Course(tt.args.courseUUID) if (err != nil) != tt.wantErr { t.Errorf("Course() error = %v, wantErr %v", err, tt.wantErr) return @@ -96,13 +94,13 @@ func TestRepository_Course(t *testing.T) { } func TestRepository_Courses(t *testing.T) { - validRows := sqlmock.NewRows([]string{"id", "uuid", "code", "name", "underline", "image", "image_cover", "excerpt", - "description", "created_at", "updated_at", "deleted_at"}). - AddRow(course.ID, course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, - course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt, course.DeletedAt). - AddRow(2, uuid.MustParse("7aec21ad-2fa8-4ddd-b5af-073144031ecc"), course.Code, course.Name, + validRows := sqlmock.NewRows([]string{"uuid", "code", "name", "underline", "image", "image_cover", "excerpt", + "description", "created_at", "updated_at"}). + AddRow(course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, + course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt). + AddRow(uuid.MustParse("7aec21ad-2fa8-4ddd-b5af-073144031ecc"), course.Code, course.Name, course.Underline, course.Image, course.ImageCover, course.Excerpt, course.Description, course.CreatedAt, - course.UpdatedAt, course.DeletedAt) + course.UpdatedAt) tests := []struct { name string @@ -154,10 +152,10 @@ func TestRepository_Courses(t *testing.T) { } func TestRepository_CreateCourse(t *testing.T) { - validRows := sqlmock.NewRows([]string{"id", "uuid", "code", "name", "underline", "image", "image_cover", "excerpt", - "description", "created_at", "updated_at", "deleted_at"}). - AddRow(course.ID, course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, - course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt, course.DeletedAt) + validRows := sqlmock.NewRows([]string{"uuid", "code", "name", "underline", "image", "image_cover", "excerpt", + "description", "created_at", "updated_at"}). + AddRow(course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, + course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt) type args struct { c *domain.Course @@ -207,11 +205,11 @@ func TestRepository_CreateCourse(t *testing.T) { } } -func TestRepository_UpdateCourseByID(t *testing.T) { - validRows := sqlmock.NewRows([]string{"id", "uuid", "code", "name", "underline", "image", "image_cover", "excerpt", - "description", "created_at", "updated_at", "deleted_at"}). - AddRow(course.ID, course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, - course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt, course.DeletedAt) +func TestRepository_UpdateCourse(t *testing.T) { + validRows := sqlmock.NewRows([]string{"uuid", "code", "name", "underline", "image", "image_cover", "excerpt", + "description", "created_at", "updated_at"}). + AddRow(course.UUID, course.Code, course.Name, course.Underline, course.Image, course.ImageCover, + course.Excerpt, course.Description, course.CreatedAt, course.UpdatedAt) type args struct { c *domain.Course @@ -247,18 +245,16 @@ func TestRepository_UpdateCourseByID(t *testing.T) { if err != nil { t.Fatalf("an error '%s' was not expected when creating the CourseRepository", err) } - prep, ok := stmts[updateCourseByID] + prep, ok := stmts[updateCourse] if !ok { - t.Fatalf("prepared statement %s not found", updateCourseByID) + t.Fatalf("prepared statement %s not found", updateCourse) } prep.ExpectQuery().WillReturnRows(tt.rows) - if err := r.UpdateCourseByID(tt.args.c); (err != nil) != tt.wantErr { + if err := r.UpdateCourse(tt.args.c); (err != nil) != tt.wantErr { t.Errorf("UpdateCourse() error = %v, wantErr %v", err, tt.wantErr) } }) } } - -// TODO Test_UpdateCourseByCode diff --git a/internal/course/domain/course.go b/internal/course/domain/course.go index 04a82b9..680a148 100644 --- a/internal/course/domain/course.go +++ b/internal/course/domain/course.go @@ -6,18 +6,15 @@ import ( "github.com/google/uuid" ) -// Course struct. type Course struct { - ID uint `json:"id"` - UUID uuid.UUID `json:"uuid"` - Code string `json:"code"` - Name string `json:"name"` - Underline string `json:"underline"` - Image string `json:"image"` - ImageCover string `db:"image_cover" json:"image_cover"` - Excerpt string `json:"excerpt"` - Description string `json:"description"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"` + UUID uuid.UUID `json:"uuid"` + Code string `json:"code"` + Name string `json:"name"` + Underline string `json:"underline"` + Image string `json:"image,omitempty"` + ImageCover string `db:"image_cover" json:"image_cover,omitempty"` + Excerpt string `json:"excerpt"` + Description string `json:"description,omitempty"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } diff --git a/internal/course/domain/course_repository.go b/internal/course/domain/course_repository.go index bc42aa6..1e0f25d 100644 --- a/internal/course/domain/course_repository.go +++ b/internal/course/domain/course_repository.go @@ -3,10 +3,9 @@ package domain import "github.com/google/uuid" type CourseRepository interface { - Course(id uuid.UUID) (Course, error) + Course(courseUUID uuid.UUID) (Course, error) Courses() ([]Course, error) CreateCourse(c *Course) error - UpdateCourseByID(c *Course) error - UpdateCourseByCode(c *Course) error - DeleteCourse(id uuid.UUID) error + UpdateCourse(c *Course) error + DeleteCourse(courseUUID uuid.UUID) error } diff --git a/internal/course/domain/course_service.go b/internal/course/domain/course_service.go index 09a2128..b59b435 100644 --- a/internal/course/domain/course_service.go +++ b/internal/course/domain/course_service.go @@ -7,8 +7,8 @@ import ( "github.com/google/uuid" ) -func (s *Service) Course(_ context.Context, id uuid.UUID) (Course, error) { - c, err := s.courses.Course(id) +func (s *Service) Course(_ context.Context, courseUUID uuid.UUID) (Course, error) { + c, err := s.courses.Course(courseUUID) if err != nil { return Course{}, fmt.Errorf("service can't find course: %w", err) } @@ -34,25 +34,14 @@ func (s *Service) CreateCourse(_ context.Context, c *Course) error { } func (s *Service) UpdateCourse(_ context.Context, c *Course) error { - exists, err := s.courses.Course(c.UUID) - if err != nil { - return err - } - - if exists.Code == c.Code { - err = s.courses.UpdateCourseByCode(c) - } else { - err = s.courses.UpdateCourseByID(c) - } - - if err != nil { + if err := s.courses.UpdateCourse(c); err != nil { return fmt.Errorf("service can't update course: %w", err) } return nil } -func (s *Service) DeleteCourse(_ context.Context, id uuid.UUID) error { - if err := s.courses.DeleteCourse(id); err != nil { +func (s *Service) DeleteCourse(_ context.Context, courseUUID uuid.UUID) error { + if err := s.courses.DeleteCourse(courseUUID); err != nil { return fmt.Errorf("service can't delete course: %w", err) }