Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions exercise7/blogging-platform/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM golang:1.23

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY . .

RUN go build -o blog-crud ./cmd

EXPOSE 8080

CMD ["./blog-crud"]
38 changes: 38 additions & 0 deletions exercise7/blogging-platform/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"fmt"
"log"
"net/http"

"github.com/gorilla/mux"
"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/config"
"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/handler"
"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/repository"
"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/service"
)

func main() {
db := config.ConnectDB()
defer db.Close()

postRepo := repository.NewPostRepository(db)
postService := service.NewPostService(postRepo)
postHandler := handler.NewPostHandler(postService)

r := mux.NewRouter()

r.HandleFunc("/posts", postHandler.CreatePost).Methods("POST")
r.HandleFunc("/posts", postHandler.GetAllPosts).Methods("GET")
r.HandleFunc("/posts/{id:[0-9]+}", postHandler.GetPostByID).Methods("GET")
r.HandleFunc("/posts/{id:[0-9]+}", postHandler.UpdatePost).Methods("PUT")
r.HandleFunc("/posts/{id:[0-9]+}", postHandler.DeletePost).Methods("DELETE")

r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to the blogging platform!"))
})

fmt.Println("Сервер запущен на порту 8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
25 changes: 25 additions & 0 deletions exercise7/blogging-platform/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package config

import (
"database/sql"
"fmt"
"log"

_ "github.com/go-sql-driver/mysql"
)

func ConnectDB() *sql.DB {
dsn := "user:root@tcp(127.0.0.1:3306)/blog_platform"

db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error connecting to the database: %v", err)
}

if err = db.Ping(); err != nil {
log.Fatalf("DB is not responding: %v", err)
}

fmt.Println("Successful connection to the database")
return db
}
18 changes: 18 additions & 0 deletions exercise7/blogging-platform/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3.8'

services:
mysql:
image: mysql:8.0
container_name: blog_mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: blogdb
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql

volumes:
mysql_data:
7 changes: 6 additions & 1 deletion exercise7/blogging-platform/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ module github.com/talgat-ruby/exercises-go/exercise7/blogging-platform

go 1.23.3

require github.com/lib/pq v1.10.9
require (
github.com/go-sql-driver/mysql v1.9.0
github.com/gorilla/mux v1.8.1
)

require filippo.io/edwards25519 v1.1.0 // indirect
8 changes: 6 additions & 2 deletions exercise7/blogging-platform/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
118 changes: 118 additions & 0 deletions exercise7/blogging-platform/internal/handler/post_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package handler

import (
"encoding/json"
"net/http"
"strconv"

"github.com/gorilla/mux"
"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/service"
"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/models"
)

type PostHandler struct {
postService *service.PostService
}

func NewPostHandler(svc *service.PostService) *PostHandler {
return &PostHandler{
postService: svc,
}
}

// CreatePost decodes the incoming request and creates a new post.
func (ph *PostHandler) CreatePost(w http.ResponseWriter, r *http.Request) {
var blogPost models.Post
if err := json.NewDecoder(r.Body).Decode(&blogPost); err != nil {
http.Error(w, "Invalid request format", http.StatusBadRequest)
return
}

if err := ph.postService.CreatePost(&blogPost); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(blogPost); err != nil {
http.Error(w, "Error sending response", http.StatusInternalServerError)
}
}

// GetAllPosts fetches all posts and returns them in JSON.
func (ph *PostHandler) GetAllPosts(w http.ResponseWriter, r *http.Request) {
posts, err := ph.postService.GetAllPosts()
if err != nil {
http.Error(w, "Error retrieving posts", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(posts); err != nil {
http.Error(w, "Error sending response", http.StatusInternalServerError)
}
}

// GetPostByID retrieves a specific post using its ID.
func (ph *PostHandler) GetPostByID(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
if err != nil {
http.Error(w, "Invalid ID format", http.StatusBadRequest)
return
}

post, err := ph.postService.GetPostByID(int64(id))
if err != nil {
http.Error(w, "Post not found", http.StatusNotFound)
return
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(post); err != nil {
http.Error(w, "Error sending response", http.StatusInternalServerError)
}
}

// UpdatePost decodes the update request and applies changes to an existing post.
func (ph *PostHandler) UpdatePost(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
if err != nil {
http.Error(w, "Invalid request format", http.StatusBadRequest)
return
}

var updatedPost models.Post
if err := json.NewDecoder(r.Body).Decode(&updatedPost); err != nil {
http.Error(w, "Invalid request format", http.StatusBadRequest)
return
}
updatedPost.ID = int64(id)

if err := ph.postService.UpdatePost(&updatedPost); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte("Post updated successfully"))
}

// DeletePost removes a post based on its ID.
func (ph *PostHandler) DeletePost(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
if err != nil {
http.Error(w, "Invalid ID format", http.StatusBadRequest)
return
}

if err := ph.postService.DeletePost(int64(id)); err != nil {
http.Error(w, "Error deleting post", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte("Post deleted successfully"))
}
137 changes: 137 additions & 0 deletions exercise7/blogging-platform/internal/repository/post_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package repository

import (
"database/sql"
"log"
"strings"
"time"

"github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/models"
)

type PostRepository struct {
db *sql.DB
}

func NewPostRepository(database *sql.DB) *PostRepository {
return &PostRepository{db: database}
}

// Create inserts a new post into the database.
func (pr *PostRepository) Create(post *models.Post) error {
combinedTags := strings.Join(post.Tags, ",")
query := `INSERT INTO posts (title, content, category, tags) VALUES (?, ?, ?, ?)`
_, err := pr.db.Exec(query, post.Title, post.Content, post.Category, combinedTags)
if err != nil {
log.Printf("Error creating post: %v", err)
}
return err
}

// GetPostByID retrieves a post by its ID from the database.
func (pr *PostRepository) GetPostByID(id int) (*models.Post, error) {
query := `SELECT id, title, content, created_at, category, tags, updated_at FROM posts WHERE id = ?`
row := pr.db.QueryRow(query, id)

post := &models.Post{}
var tagsField sql.NullString
var createdTime, updatedTime string

err := row.Scan(&post.ID, &post.Title, &post.Content, &createdTime, &post.Category, &tagsField, &updatedTime)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("No post found with ID %d", id)
return nil, nil
}
log.Printf("Error retrieving post by ID: %v", err)
return nil, err
}

post.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdTime)
if err != nil {
log.Printf("Error parsing created_at: %v", err)
return nil, err
}

post.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedTime)
if err != nil {
log.Printf("Error parsing updated_at: %v", err)
return nil, err
}

if tagsField.Valid {
post.Tags = strings.Split(tagsField.String, ",")
}

return post, nil
}

// GetAll retrieves all posts from the database.
func (pr *PostRepository) GetAll() ([]models.Post, error) {
query := `SELECT id, title, content, created_at, category, tags, updated_at FROM posts`
rows, err := pr.db.Query(query)
if err != nil {
log.Printf("Error retrieving posts: %v", err)
return nil, err
}
defer rows.Close()

var posts []models.Post
for rows.Next() {
var p models.Post
var tagsField sql.NullString
var createdTime, updatedTime string

err := rows.Scan(&p.ID, &p.Title, &p.Content, &createdTime, &p.Category, &tagsField, &updatedTime)
if err != nil {
log.Printf("Error scanning post: %v", err)
return nil, err
}

p.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdTime)
if err != nil {
log.Printf("Error parsing created_at: %v", err)
return nil, err
}

p.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedTime)
if err != nil {
log.Printf("Error parsing updated_at: %v", err)
return nil, err
}

if tagsField.Valid {
p.Tags = strings.Split(tagsField.String, ",")
}

posts = append(posts, p)
}

if err := rows.Err(); err != nil {
log.Printf("Error processing rows: %v", err)
return nil, err
}

return posts, nil
}

// Update modifies an existing post in the database.
func (pr *PostRepository) Update(post *models.Post) error {
combinedTags := strings.Join(post.Tags, ",")
query := `UPDATE posts SET title = ?, content = ?, category = ?, tags = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
_, err := pr.db.Exec(query, post.Title, post.Content, post.Category, combinedTags, post.ID)
if err != nil {
log.Printf("Error updating post with ID %d: %v", post.ID, err)
}
return err
}

// Delete removes a post from the database by its ID.
func (pr *PostRepository) Delete(id int) error {
query := `DELETE FROM posts WHERE id = ?`
_, err := pr.db.Exec(query, id)
if err != nil {
log.Printf("Error deleting post with ID %d: %v", id, err)
}
return err
}
Loading