From 0775bd44d12c69eadba90f0aef61ccd0a3bbb121 Mon Sep 17 00:00:00 2001 From: Talgat Date: Thu, 23 Jan 2025 19:03:48 +0500 Subject: [PATCH 1/3] add exercise9 --- exercise9/README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ exercise9/go.mod | 3 +++ 2 files changed, 45 insertions(+) create mode 100644 exercise9/README.md create mode 100644 exercise9/go.mod diff --git a/exercise9/README.md b/exercise9/README.md new file mode 100644 index 00000000..5d54880e --- /dev/null +++ b/exercise9/README.md @@ -0,0 +1,42 @@ +# Exercise 9 + +Project + +## Teams + +Team 1 + +1. Имангали Аскар (controller) +2. Зернов Владислав (controller) +3. Курмашев Сабит (api) +4. Кабулов Нуртас (api) +5. Омаров Темирлан (db) +6. Сагиндиков Меирбек (db) + +Team 2 + +1. Тұрарова Айзада (controller) +2. Толеу Аян (controller) +3. Мырзаханов Алинур (api) +4. Еркибаев Зураб (api) +5. Бақатай Ақжол (db) +6. Бимаканова Мадина (db) + +Team 3 + +1. Кабдылкак Арнур (controller) +2. Калкин Ернар (controller) +3. Манкенов Арай (api) +4. Усербай Асылбек (api) +5. Камбаров Руслан (db) +6. Қайратұлы Шыңғысхан (db) + +Team 4 + +1. Жақуда Жарқынай (controller) +2. Жантасов Адлет (controller) +3. Туралин Аргын (api) +4. Алтынбек Жандос (api) +5. Жакупов Жандаулет (db) +6. Мұхаметқали Арайлым (db) +7. Кемалатдин Ғалымжан (your choice) diff --git a/exercise9/go.mod b/exercise9/go.mod new file mode 100644 index 00000000..72f28b6f --- /dev/null +++ b/exercise9/go.mod @@ -0,0 +1,3 @@ +module github.com/talgat-ruby/exercises-go/exercise9 + +go 1.23.5 From 753fb05717e34fa7f5ab8fe21338794728a2c75c Mon Sep 17 00:00:00 2001 From: Talgat Date: Mon, 27 Jan 2025 18:18:10 +0500 Subject: [PATCH 2/3] update exercise9 --- exercise9/README.md | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/exercise9/README.md b/exercise9/README.md index 5d54880e..ef1d468e 100644 --- a/exercise9/README.md +++ b/exercise9/README.md @@ -6,37 +6,9 @@ Project Team 1 -1. Имангали Аскар (controller) -2. Зернов Владислав (controller) -3. Курмашев Сабит (api) -4. Кабулов Нуртас (api) -5. Омаров Темирлан (db) -6. Сагиндиков Меирбек (db) - -Team 2 - -1. Тұрарова Айзада (controller) -2. Толеу Аян (controller) -3. Мырзаханов Алинур (api) -4. Еркибаев Зураб (api) -5. Бақатай Ақжол (db) -6. Бимаканова Мадина (db) - -Team 3 - -1. Кабдылкак Арнур (controller) -2. Калкин Ернар (controller) -3. Манкенов Арай (api) -4. Усербай Асылбек (api) -5. Камбаров Руслан (db) -6. Қайратұлы Шыңғысхан (db) - -Team 4 - -1. Жақуда Жарқынай (controller) -2. Жантасов Адлет (controller) -3. Туралин Аргын (api) -4. Алтынбек Жандос (api) -5. Жакупов Жандаулет (db) -6. Мұхаметқали Арайлым (db) -7. Кемалатдин Ғалымжан (your choice) +1. Тұрарова Айзада (api) +2. Манкенов Арай (api) +3. Усербай Асылбек (controller) +4. Кемалатдин Ғалымжан (controller) +5. Имангали Аскар (db) +6. Кабдылкак Арнур (db) From 570b807d8a557d632993f4891fef9487a232964f Mon Sep 17 00:00:00 2001 From: zhandaulet19 Date: Fri, 28 Feb 2025 22:56:49 +0500 Subject: [PATCH 3/3] project --- exercise7/blogging-platform/Dockerfile | 15 ++ exercise7/blogging-platform/cmd/main.go | 38 +++++ exercise7/blogging-platform/config/config.go | 25 ++++ .../blogging-platform/docker-compose.yml | 18 +++ exercise7/blogging-platform/go.mod | 7 +- exercise7/blogging-platform/go.sum | 8 +- .../internal/handler/post_handler.go | 118 +++++++++++++++ .../internal/repository/post_repository.go | 137 ++++++++++++++++++ .../internal/service/post_service.go | 68 +++++++++ exercise7/blogging-platform/models/post.go | 14 ++ 10 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 exercise7/blogging-platform/Dockerfile create mode 100644 exercise7/blogging-platform/cmd/main.go create mode 100644 exercise7/blogging-platform/config/config.go create mode 100644 exercise7/blogging-platform/docker-compose.yml create mode 100644 exercise7/blogging-platform/internal/handler/post_handler.go create mode 100644 exercise7/blogging-platform/internal/repository/post_repository.go create mode 100644 exercise7/blogging-platform/internal/service/post_service.go create mode 100644 exercise7/blogging-platform/models/post.go diff --git a/exercise7/blogging-platform/Dockerfile b/exercise7/blogging-platform/Dockerfile new file mode 100644 index 00000000..c28803ca --- /dev/null +++ b/exercise7/blogging-platform/Dockerfile @@ -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"] diff --git a/exercise7/blogging-platform/cmd/main.go b/exercise7/blogging-platform/cmd/main.go new file mode 100644 index 00000000..8f1d6e06 --- /dev/null +++ b/exercise7/blogging-platform/cmd/main.go @@ -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)) +} diff --git a/exercise7/blogging-platform/config/config.go b/exercise7/blogging-platform/config/config.go new file mode 100644 index 00000000..069bad88 --- /dev/null +++ b/exercise7/blogging-platform/config/config.go @@ -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 +} diff --git a/exercise7/blogging-platform/docker-compose.yml b/exercise7/blogging-platform/docker-compose.yml new file mode 100644 index 00000000..0f417700 --- /dev/null +++ b/exercise7/blogging-platform/docker-compose.yml @@ -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: diff --git a/exercise7/blogging-platform/go.mod b/exercise7/blogging-platform/go.mod index ca16e703..0ae19bff 100644 --- a/exercise7/blogging-platform/go.mod +++ b/exercise7/blogging-platform/go.mod @@ -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 diff --git a/exercise7/blogging-platform/go.sum b/exercise7/blogging-platform/go.sum index aeddeae3..e5d89554 100644 --- a/exercise7/blogging-platform/go.sum +++ b/exercise7/blogging-platform/go.sum @@ -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= diff --git a/exercise7/blogging-platform/internal/handler/post_handler.go b/exercise7/blogging-platform/internal/handler/post_handler.go new file mode 100644 index 00000000..887d90a5 --- /dev/null +++ b/exercise7/blogging-platform/internal/handler/post_handler.go @@ -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")) +} \ No newline at end of file diff --git a/exercise7/blogging-platform/internal/repository/post_repository.go b/exercise7/blogging-platform/internal/repository/post_repository.go new file mode 100644 index 00000000..eb85515f --- /dev/null +++ b/exercise7/blogging-platform/internal/repository/post_repository.go @@ -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 +} \ No newline at end of file diff --git a/exercise7/blogging-platform/internal/service/post_service.go b/exercise7/blogging-platform/internal/service/post_service.go new file mode 100644 index 00000000..af28028e --- /dev/null +++ b/exercise7/blogging-platform/internal/service/post_service.go @@ -0,0 +1,68 @@ +package service + +import ( + "errors" + + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/internal/repository" + "github.com/talgat-ruby/exercises-go/exercise7/blogging-platform/models" +) + +type PostService struct { + repo *repository.PostRepository +} + +func NewPostService(repo *repository.PostRepository) *PostService { + return &PostService{ + repo: repo, + } +} + +// CreatePost validates and creates a new post. +func (ps *PostService) CreatePost(post *models.Post) error { + if post.Title == "" || post.Content == "" || post.Category == "" { + return errors.New("all fields (title, content, category) are required") + } + return ps.repo.Create(post) +} + +// GetAllPosts retrieves every post. +func (ps *PostService) GetAllPosts() ([]models.Post, error) { + return ps.repo.GetAll() +} + +// GetPostByID fetches a post using its unique ID. +func (ps *PostService) GetPostByID(id int64) (*models.Post, error) { + p, err := ps.repo.GetPostByID(int(id)) + if err != nil { + return nil, errors.New("post not found") + } + return p, nil +} + +// UpdatePost validates and updates an existing post. +func (ps *PostService) UpdatePost(post *models.Post) error { + if post.Title == "" || post.Content == "" || post.Category == "" { + return errors.New("all fields (title, content, category) are required") + } + + existingPost, err := ps.repo.GetPostByID(int(post.ID)) + if err != nil { + return errors.New("post not found") + } + + existingPost.Title = post.Title + existingPost.Content = post.Content + existingPost.Category = post.Category + existingPost.Tags = post.Tags + + return ps.repo.Update(existingPost) +} + +// DeletePost removes a post given its ID. +func (ps *PostService) DeletePost(id int64) error { + _, err := ps.repo.GetPostByID(int(id)) + if err != nil { + return errors.New("post not found") + } + return ps.repo.Delete(int(id)) +} \ No newline at end of file diff --git a/exercise7/blogging-platform/models/post.go b/exercise7/blogging-platform/models/post.go new file mode 100644 index 00000000..60e559cf --- /dev/null +++ b/exercise7/blogging-platform/models/post.go @@ -0,0 +1,14 @@ +package models + +import "time" + +type Post struct { + ID int64 `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Category string `json:"category"` + Tags []string `json:"tags"` +} +