diff --git a/bookback/internal/models/page.go b/bookback/internal/models/page.go index d40b1ef..eb80a56 100644 --- a/bookback/internal/models/page.go +++ b/bookback/internal/models/page.go @@ -5,14 +5,15 @@ import ( ) type Page struct { - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt NullTime `json:"deleted_at"` - Text string `json:"text"` - ChapterID string `json:"chapter_id"` - IsPublic bool `json:"is_public"` - MapParamsID string `json:"map_params"` // параметры карты (координаты и тп.) + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt NullTime `json:"deleted_at"` + Title string `json:"title"` + Text string `json:"text"` + ChapterID string `json:"chapter_id"` + IsPublic bool `json:"is_public"` + MapParamsID NullString `json:"map_params"` // параметры карты (координаты и тп.) //Paragraphs []Paragraph //Variables []Variable `json:"variables"` // переменные мира key: key, value: value } diff --git a/bookback/internal/servers/http/controllers/page/controller.go b/bookback/internal/servers/http/controllers/page/controller.go index fec65d8..452ce32 100644 --- a/bookback/internal/servers/http/controllers/page/controller.go +++ b/bookback/internal/servers/http/controllers/page/controller.go @@ -2,6 +2,7 @@ package page import ( "context" + "fmt" "github.com/SShlykov/zeitment/bookback/internal/config" "github.com/SShlykov/zeitment/bookback/internal/models" service "github.com/SShlykov/zeitment/bookback/internal/services/page" @@ -38,6 +39,7 @@ func (p *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { func (p *Controller) ListPages(c echo.Context, ctx context.Context) error { pages, err := p.Service.ListPages(ctx) if err != nil { + fmt.Println(err) return echo.NewHTTPError(http.StatusBadGateway, config.ErrorForbidden) } return c.JSON(http.StatusOK, pages) @@ -102,6 +104,7 @@ func (p *Controller) UpdatePage(c echo.Context, ctx context.Context) error { } updatedPage, err := p.Service.UpdatePage(ctx, id, &page) if err != nil { + fmt.Println(err) return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotUpdated) } return c.JSON(http.StatusOK, updatedPage) @@ -120,6 +123,7 @@ func (p *Controller) DeletePage(c echo.Context, ctx context.Context) error { id := c.Param("id") deletedPage, err := p.Service.DeletePage(ctx, id) if err != nil { + fmt.Println(err) return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotDeleted) } return c.JSON(http.StatusOK, deletedPage) diff --git a/bookback/internal/services/book/repository.go b/bookback/internal/services/book/repository.go index edba958..26be6ae 100644 --- a/bookback/internal/services/book/repository.go +++ b/bookback/internal/services/book/repository.go @@ -83,8 +83,7 @@ func (r *repository) Create(ctx context.Context, book *models.Book) (string, err } func (r *repository) FindByID(ctx context.Context, id string) (*models.Book, error) { - query := `SELECT ` + allItems() + ` FROM ` + tableName + Where + columnID + ` = $1 AND ` + - columnDeletedAt + ` IS NULL LIMIT 1` + query := services.SelectWhere(allItems, tableName, columnID) + " AND " + columnDeletedAt + ` IS NULL` + " LIMIT 1" q := db.Query{Name: "BookRepository.FindById", Raw: query} @@ -110,7 +109,7 @@ func (r *repository) Update(ctx context.Context, id string, updBook *models.Book } func (r *repository) Delete(ctx context.Context, id string) (*models.Book, error) { - query := `DELETE FROM` + " " + tableName + Where + columnID + ` = $1 RETURNING ` + allItems() + query := services.DeleteQuery(tableName, columnID) + ` RETURNING ` + allItems() q := db.Query{Name: "BookRepository.Delete", Raw: query} diff --git a/bookback/internal/services/book/utils.go b/bookback/internal/services/book/utils.go index 37fa9f9..85be18c 100644 --- a/bookback/internal/services/book/utils.go +++ b/bookback/internal/services/book/utils.go @@ -5,7 +5,10 @@ import ( "github.com/jackc/pgx/v5" ) +// readList reads a list of books from the database. +// It returns a slice of books and an error if any. func readList(rows pgx.Rows) ([]models.Book, error) { + defer rows.Close() books := make([]models.Book, 0) for rows.Next() { item, err := readItem(rows) @@ -18,7 +21,7 @@ func readList(rows pgx.Rows) ([]models.Book, error) { return nil, err } - return books, nil + return books, rows.Err() } func readItem(row pgx.Row) (*models.Book, error) { diff --git a/bookback/internal/services/bookevents/utils.go b/bookback/internal/services/bookevents/utils.go index 941b85d..781f29d 100644 --- a/bookback/internal/services/bookevents/utils.go +++ b/bookback/internal/services/bookevents/utils.go @@ -6,6 +6,7 @@ import ( ) func readList(rows pgx.Rows) ([]models.BookEvent, error) { + defer rows.Close() events := make([]models.BookEvent, 0) for rows.Next() { item, err := readItem(rows) @@ -18,7 +19,7 @@ func readList(rows pgx.Rows) ([]models.BookEvent, error) { return nil, err } - return events, nil + return events, rows.Err() } func readItem(row pgx.Row) (*models.BookEvent, error) { diff --git a/bookback/internal/services/chapter/repository.go b/bookback/internal/services/chapter/repository.go index 9dbd5cb..d7894e4 100644 --- a/bookback/internal/services/chapter/repository.go +++ b/bookback/internal/services/chapter/repository.go @@ -106,7 +106,7 @@ func (r *repository) Update(ctx context.Context, id string, updChapter *models.C } func (r *repository) Delete(ctx context.Context, id string) (*models.Chapter, error) { - query := `DELETE ` + FromTable + WHERE + columnID + ` = $1` + Returning + allItems() + query := services.DeleteQuery(tableName, columnID) + ` RETURNING ` + allItems() q := db.Query{Name: "BookRepository.Delete", Raw: query} row := r.db.DB().QueryRowContext(ctx, q, id) diff --git a/bookback/internal/services/chapter/utils.go b/bookback/internal/services/chapter/utils.go index 6aa49e0..77d2631 100644 --- a/bookback/internal/services/chapter/utils.go +++ b/bookback/internal/services/chapter/utils.go @@ -6,6 +6,7 @@ import ( ) func readList(rows pgx.Rows) ([]models.Chapter, error) { + defer rows.Close() chapters := make([]models.Chapter, 0) for rows.Next() { item, err := readItem(rows) @@ -18,7 +19,7 @@ func readList(rows pgx.Rows) ([]models.Chapter, error) { return nil, err } - return chapters, nil + return chapters, rows.Err() } func readItem(row pgx.Row) (*models.Chapter, error) { diff --git a/bookback/internal/services/helpers.go b/bookback/internal/services/helpers.go index 24880f4..b369edd 100644 --- a/bookback/internal/services/helpers.go +++ b/bookback/internal/services/helpers.go @@ -2,6 +2,16 @@ package services import "strconv" +// DeleteQuery returns a SQL query to delete a row from a table by its ID. +// e.g. DeleteQuery(table_name, id_name) -> DELETE FROM table_name WHERE id_name = $1 +func DeleteQuery(tableName, idName string) string { + sql := `DELETE FROM` + " " + tableName + ` WHERE ` + idName + ` = $1` + + return sql +} + +// SelectWhere returns a SQL query to select all items from a table with a WHERE clause. +// e.g. SelectWhere(allItems, table_name, column1, column2) -> SELECT allItems... FROM table_name WHERE column1 = $1 AND column2 = $2 func SelectWhere(allItems func() string, tableName string, args ...string) string { sql := `SELECT ` + allItems() + ` FROM ` + tableName if len(args) > 0 { @@ -10,6 +20,8 @@ func SelectWhere(allItems func() string, tableName string, args ...string) strin return sql } +// ParamsToQuery returns a string of SQL query parameters. +// e.g. ParamsToQuery(", ", "column1", "column2") -> column1 = $1, column2 = $2 func ParamsToQuery(joiner string, args ...string) (sql string) { for i, arg := range args { sql += arg + ` = $` + strconv.Itoa(i+1) diff --git a/bookback/internal/services/helpers_test.go b/bookback/internal/services/helpers_test.go index e46b5f0..217f7a7 100644 --- a/bookback/internal/services/helpers_test.go +++ b/bookback/internal/services/helpers_test.go @@ -10,6 +10,29 @@ func mockAllItems() string { return "*" } +func TestDeleteQuery(t *testing.T) { + tests := []struct { + name string + tableName string + idName string + want string + }{ + { + name: "Single WHERE condition", + tableName: "books", + idName: "id", + want: "DELETE FROM books WHERE id = $1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DeleteQuery(tt.tableName, tt.idName) + assert.Equal(t, tt.want, got) + }) + } +} + func TestSelectWhere(t *testing.T) { tests := []struct { name string diff --git a/bookback/internal/services/mapvariables/utils.go b/bookback/internal/services/mapvariables/utils.go index 2859090..34ab2f6 100644 --- a/bookback/internal/services/mapvariables/utils.go +++ b/bookback/internal/services/mapvariables/utils.go @@ -6,6 +6,7 @@ import ( ) func readList(rows pgx.Rows) ([]models.MapVariable, error) { + defer rows.Close() variables := make([]models.MapVariable, 0) for rows.Next() { item, err := readItem(rows) @@ -18,7 +19,7 @@ func readList(rows pgx.Rows) ([]models.MapVariable, error) { return nil, err } - return variables, nil + return variables, rows.Err() } func readItem(row pgx.Row) (*models.MapVariable, error) { diff --git a/bookback/internal/services/page/repository.go b/bookback/internal/services/page/repository.go index 801a490..6d43983 100644 --- a/bookback/internal/services/page/repository.go +++ b/bookback/internal/services/page/repository.go @@ -3,22 +3,23 @@ package page import ( "context" "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/services" "github.com/SShlykov/zeitment/bookback/pkg/db" - "github.com/SShlykov/zeitment/bookback/pkg/db/sq" - "github.com/google/uuid" + "strings" ) const ( - // model fields and table name for books table - tableName = "pages" - columnID = "id" - columnCreatedAt = "created_at" - columnUpdatedAt = "updated_at" - columnDeletedAt = "deleted_at" - columnText = "text" - columnChapterID = "chapter_id" - columnIsPublic = "is_public" - Returning = "RETURNING " + tableName = "pages" + columnID = "id" + columnCreatedAt = "created_at" + columnUpdatedAt = "updated_at" + columnDeletedAt = "deleted_at" + columnTitle = "title" + columnText = "text" + columnChapterID = "chapter_id" + columnIsPublic = "is_public" + columnMapParamsID = "map_params_id" + Returning = " RETURNING " ) // Repository определяет интерфейс для взаимодействия с хранилищем книг. @@ -41,23 +42,30 @@ func NewRepository(database db.Client) Repository { return &repository{database} } +func allItems() string { + cols := []string{columnID, columnCreatedAt, columnUpdatedAt, columnDeletedAt, columnTitle, columnText, columnChapterID, + columnIsPublic, columnMapParamsID} + + return strings.Join(cols, ", ") +} + +func insertItems() string { + cols := []string{columnTitle, columnText, columnChapterID, columnIsPublic, columnMapParamsID} + + return strings.Join(cols, ", ") +} + // Create inserts a new page into the database and returns its ID. func (r *repository) Create(ctx context.Context, page *models.Page) (string, error) { - page.ID = uuid.New().String() - query, args, err := sq.Insert(tableName). - Columns(columnID, columnText, columnChapterID, columnIsPublic). - Values(page.ID, page.Text, page.ChapterID, page.IsPublic). - Suffix("RETURNING id"). - ToSql() - if err != nil { - return "", err - } + query := `INSERT INTO` + " " + tableName + ` (` + insertItems() + `) VALUES ($1, $2, $3, $4, $5) ` + + Returning + columnID + args := []interface{}{page.Title, page.Text, page.ChapterID, page.IsPublic, page.MapParamsID} q := db.Query{Name: "PageRepository.Create", Raw: query} row := r.db.DB().QueryRowContext(ctx, q, args...) var id string - if err = row.Scan(&id); err != nil { + if err := row.Scan(&id); err != nil { return "", err } @@ -66,38 +74,21 @@ func (r *repository) Create(ctx context.Context, page *models.Page) (string, err // FindByID retrieves a page by its ID. func (r *repository) FindByID(ctx context.Context, id string) (*models.Page, error) { - query, args, err := sq.Select(columnID, columnCreatedAt, columnUpdatedAt, columnDeletedAt, columnText, - columnChapterID, columnIsPublic). - From("pages"). - Where(sq.Eq{"id": id, "deleted_at": nil}). - Limit(1). - ToSql() - if err != nil { - return nil, err - } - + query := services.SelectWhere(allItems, tableName, columnID) q := db.Query{Name: "PageRepository.FindByID", Raw: query} - var page models.Page - if err = r.db.DB().QueryRowContext(ctx, q, args...).Scan(&page.ID, &page.CreatedAt, &page.UpdatedAt, - &page.DeletedAt, &page.Text, &page.ChapterID, &page.IsPublic); err != nil { - return nil, err - } + row := r.db.DB().QueryRowContext(ctx, q, id) - return &page, nil + return readItem(row) } // Update modifies an existing page's data. func (r *repository) Update(ctx context.Context, id string, updPage *models.Page) (*models.Page, error) { - query, args, err := sq.Update(tableName). - Set(columnText, updPage.Text). - Set(columnIsPublic, updPage.IsPublic). - Where(sq.Eq{"id": id}). - Suffix("RETURNING id, created_at, updated_at, deleted_at, text, chapter_id, is_public"). - ToSql() - if err != nil { - return nil, err - } + query := "Update " + tableName + " SET " + + services.ParamsToQuery(", ", columnTitle, columnText, columnChapterID, columnIsPublic, columnMapParamsID) + + " WHERE " + columnID + " = $6" + Returning + allItems() + + args := []interface{}{updPage.Title, updPage.Text, updPage.ChapterID, updPage.IsPublic, updPage.MapParamsID, id} q := db.Query{Name: "PageRepository.Update", Raw: query} @@ -108,48 +99,30 @@ func (r *repository) Update(ctx context.Context, id string, updPage *models.Page // Delete removes a page from the database. func (r *repository) Delete(ctx context.Context, id string) (*models.Page, error) { - query, args, err := sq.SQ.Delete(tableName). - Where(sq.Eq{"id": id}). - Suffix("RETURNING id"). - ToSql() - if err != nil { - return nil, err - } + query := services.DeleteQuery(tableName, columnID) + Returning + allItems() q := db.Query{Name: "PageRepository.Delete", Raw: query} + row := r.db.DB().QueryRowContext(ctx, q, id) - var deletedID string - if err = r.db.DB().QueryRowContext(ctx, q, args...).Scan(&deletedID); err != nil { - return nil, err - } - - return &models.Page{ID: deletedID}, nil + return readItem(row) } // List retrieves all pages for a given chapter ID. func (r *repository) List(ctx context.Context) ([]models.Page, error) { - query := - `SELECT id, created_at, updated_at, deleted_at, text, chapter_id, is_public - FROM pages - WHERE deleted_at IS NULL` + query := `Select ` + allItems() + ` FROM ` + tableName + ` WHERE ` + columnDeletedAt + ` IS NULL` q := db.Query{Name: "PageRepository.List", Raw: query} - rows, err := r.db.DB().QueryContext(ctx, q) if err != nil { return nil, err } - defer rows.Close() return readList(rows) } // GetPagesByChapterID retrieves all pages for a given chapter ID. func (r *repository) GetPagesByChapterID(ctx context.Context, chapterID string) ([]models.Page, error) { - query := - `SELECT id, created_at, updated_at, deleted_at, text, chapter_id, is_public - FROM pages - WHERE chapter_id = $1 AND deleted_at IS NULL` + query := services.SelectWhere(allItems, tableName, columnChapterID) + ` AND ` + columnDeletedAt + ` IS NULL` q := db.Query{Name: "PageRepository.GetPagesByChapterID", Raw: query} @@ -157,7 +130,6 @@ func (r *repository) GetPagesByChapterID(ctx context.Context, chapterID string) if err != nil { return nil, err } - defer rows.Close() return readList(rows) } diff --git a/bookback/internal/services/page/repository_test.go b/bookback/internal/services/page/repository_test.go new file mode 100644 index 0000000..3c0059c --- /dev/null +++ b/bookback/internal/services/page/repository_test.go @@ -0,0 +1,99 @@ +package page + +import ( + "github.com/SShlykov/zeitment/bookback/internal/mocks" + "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +func newTestPage() *models.Page { + return &models.Page{ + ID: "1", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Title: faker.Word(), + Text: "Test text", + ChapterID: faker.UUIDHyphenated(), + IsPublic: true, + MapParamsID: models.NewNullString(faker.UUIDHyphenated(), true), + } +} + +func rowFromPage(page *models.Page) *mocks.ScanResult { + return mocks.NewScanResult([]interface{}{page.ID, page.CreatedAt, page.UpdatedAt, page.DeletedAt, + page.Title, page.Text, page.ChapterID, page.IsPublic, page.MapParamsID}) +} + +func initPages(ctrl *gomock.Controller) (Repository, *models.Page) { + client := mocks.NewMockClient(ctrl) + db := mocks.NewMockDB(ctrl) + + testPage := newTestPage() + row := rowFromPage(testPage) + + client.EXPECT().DB().Return(db) + db.EXPECT().QueryRowContext(gomock.Any(), gomock.Any(), gomock.Any()).Return(row) + + repo := NewRepository(client) + + return repo, testPage +} + +func TestRepository_FindByID(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + repo, testPage := initPages(ctrl) + + page, err := repo.FindByID(nil, testPage.ID) + + assert.Empty(t, err) + assert.Equal(t, testPage, page) +} + +func TestRepository_Create(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocks.NewMockClient(ctrl) + db := mocks.NewMockDB(ctrl) + + testPage := &models.Page{} + + row := mocks.NewScanResult([]interface{}{faker.UUIDHyphenated()}) + + client.EXPECT().DB().Return(db) + db.EXPECT().QueryRowContext(gomock.Any(), gomock.Any(), gomock.Any()).Return(row) + + repo := NewRepository(client) + id, err := repo.Create(nil, testPage) + assert.Empty(t, err) + assert.NotEmpty(t, id) +} + +func TestRepository_Update(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + repo, testPage := initPages(ctrl) + + page, err := repo.Update(nil, testPage.ID, testPage) + assert.Empty(t, err) + assert.Equal(t, testPage, page) +} + +func TestRepository_Delete(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + repo, testPage := initPages(ctrl) + + page, err := repo.Delete(nil, testPage.ID) + assert.Empty(t, err) + assert.Equal(t, testPage, page) +} diff --git a/bookback/internal/services/page/utils.go b/bookback/internal/services/page/utils.go index 77fb1d0..696e60c 100644 --- a/bookback/internal/services/page/utils.go +++ b/bookback/internal/services/page/utils.go @@ -6,6 +6,7 @@ import ( ) func readList(rows pgx.Rows) ([]models.Page, error) { + defer rows.Close() pages := make([]models.Page, 0) for rows.Next() { item, err := readItem(rows) @@ -14,17 +15,14 @@ func readList(rows pgx.Rows) ([]models.Page, error) { } pages = append(pages, *item) } - if err := rows.Err(); err != nil { - return nil, err - } - return pages, nil + return pages, rows.Err() } func readItem(row pgx.Row) (*models.Page, error) { var page models.Page - if err := row.Scan(&page.ID, &page.CreatedAt, &page.UpdatedAt, &page.DeletedAt, &page.Text, &page.ChapterID, - &page.IsPublic); err != nil { + if err := row.Scan(&page.ID, &page.CreatedAt, &page.UpdatedAt, &page.DeletedAt, &page.Title, &page.Text, &page.ChapterID, + &page.IsPublic, &page.MapParamsID); err != nil { return nil, err } diff --git a/bookback/internal/services/paragraph/utils.go b/bookback/internal/services/paragraph/utils.go index 98b1dc6..d00befe 100644 --- a/bookback/internal/services/paragraph/utils.go +++ b/bookback/internal/services/paragraph/utils.go @@ -6,6 +6,7 @@ import ( ) func readList(rows pgx.Rows) ([]models.Paragraph, error) { + defer rows.Close() paragraphs := make([]models.Paragraph, 0) for rows.Next() { item, err := readItem(rows) @@ -14,11 +15,8 @@ func readList(rows pgx.Rows) ([]models.Paragraph, error) { } paragraphs = append(paragraphs, *item) } - if err := rows.Err(); err != nil { - return nil, err - } - return paragraphs, nil + return paragraphs, rows.Err() } func readItem(row pgx.Row) (*models.Paragraph, error) { diff --git a/bookback/migrations/20240302202835_update_page_table_added_title.sql b/bookback/migrations/20240302202835_update_page_table_added_title.sql new file mode 100644 index 0000000..82556a2 --- /dev/null +++ b/bookback/migrations/20240302202835_update_page_table_added_title.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER table pages ADD COLUMN title text; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE pages DROP COLUMN title; +-- +goose StatementEnd