diff --git a/backend/application/usecase/articles.go b/backend/application/usecase/articles.go new file mode 100644 index 00000000..9b762e16 --- /dev/null +++ b/backend/application/usecase/articles.go @@ -0,0 +1,27 @@ +package usecase + +import ( + "github.com/yoshihiro-shu/draft-backend/backend/domain/model" + "github.com/yoshihiro-shu/draft-backend/backend/domain/repository" +) + +type ArticlesUseCase interface { + GetArticlesByCategory(articles *[]model.Article, slug string) error + GetArticlesByTag(articles *[]model.Article, slug string) error +} + +type articlesUseCase struct { + articleRepo repository.ArticleRepository +} + +func NewArticlesUseCase(articleRepo repository.ArticleRepository) ArticlesUseCase { + return &articlesUseCase{articleRepo: articleRepo} +} + +func (au *articlesUseCase) GetArticlesByCategory(articles *[]model.Article, slug string) error { + return au.articleRepo.GetArticlesByCategory(articles, slug) +} + +func (au *articlesUseCase) GetArticlesByTag(articles *[]model.Article, slug string) error { + return au.articleRepo.GetArticlesByTag(articles, slug) +} diff --git a/backend/domain/model/article.go b/backend/domain/model/article.go index 11fa9726..b67906f6 100644 --- a/backend/domain/model/article.go +++ b/backend/domain/model/article.go @@ -5,18 +5,18 @@ import ( ) type Article struct { - Id int `gorm:"primaryKey;" json:"id"` - UserId int `json:"userId"` - ThumbnailUrl string `json:"thumbnailUrl"` - Title string `json:"title"` - Content string `json:"content"` - Status int `json:"status"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - CategoryId int `json:"categoryId"` - User *User `gorm:"foreignKey:UserId;" json:"user"` - Category *Category `gorm:"foreignKey:CategoryId;" json:"category"` - Tags []Tag `gorm:"many2many:article_tags;" json:"tags"` + Id int `gorm:"primaryKey;" json:"id,omitempty"` + UserId int `json:"userId,omitempty"` + ThumbnailUrl string `json:"thumbnailUrl,omitempty"` + Title string `json:"title,omitempty"` + Content string `json:"content,omitempty"` + Status int `json:"status,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` + UpdatedAt time.Time `json:"updatedAt,omitempty"` + CategoryId int `json:"categoryId,omitempty"` + User *User `gorm:"foreignKey:UserId;" json:"user,omitempty"` + Category *Category `gorm:"foreignKey:CategoryId;" json:"category,omitempty"` + Tags []Tag `gorm:"many2many:article_tags;" json:"tags,omitempty"` } func NewArticle(Id int) *Article { diff --git a/backend/domain/model/category.go b/backend/domain/model/category.go index 5b420256..e778a604 100644 --- a/backend/domain/model/category.go +++ b/backend/domain/model/category.go @@ -3,10 +3,9 @@ package model import "time" type Category struct { - Id int `gorm:"primaryKey;" json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - ParentId int `json:"parentId"` - CreatedAt time.Time `json:"createdAt"` + Id int `gorm:"primaryKey;" json:"id,omitempty"` + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` } diff --git a/backend/domain/model/refresh_token.go b/backend/domain/model/refresh_token.go index 457df0b6..0026a0e0 100644 --- a/backend/domain/model/refresh_token.go +++ b/backend/domain/model/refresh_token.go @@ -5,11 +5,11 @@ import ( ) type RefreshToken struct { - Id int `json:"id"` - UserId int `json:"user_id"` - JwtId string `json:"jwt_id"` - ExpiredAt time.Time `json:"expired_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - User *User `gorm:"foreignKey:user_id;" json:"user"` + Id int `json:"id,omitempty"` + UserId int `json:"user_id,omitempty"` + JwtId string `json:"jwt_id,omitempty"` + ExpiredAt time.Time `json:"expired_at,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + User *User `gorm:"foreignKey:user_id;" json:"user,omitempty"` } diff --git a/backend/domain/model/tag.go b/backend/domain/model/tag.go index c132bf45..280b66ea 100644 --- a/backend/domain/model/tag.go +++ b/backend/domain/model/tag.go @@ -3,11 +3,11 @@ package model import "time" type Tag struct { - Id int `gorm:"primaryKey;" json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - CreatedAt time.Time `json:"createdAt"` + Id int `gorm:"primaryKey;" json:"id,omitempty"` + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` } type ArticleTags struct { diff --git a/backend/domain/model/user.go b/backend/domain/model/user.go index 1ed10347..de986dfe 100644 --- a/backend/domain/model/user.go +++ b/backend/domain/model/user.go @@ -3,11 +3,11 @@ package model import "time" type User struct { - Id int `gorm:"primaryKey;" json:"id"` - Name string `json:"name"` - Password string `json:"password"` - Email string `json:"email"` - CreatedAt time.Time `json:"createdAt"` + Id int `gorm:"primaryKey;" json:"id,omitempty"` + Name string `json:"name,omitempty"` + Password string `json:"password,omitempty"` + Email string `json:"email,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` } func NewUser(name, password, email string) *User { diff --git a/backend/domain/repository/article.go b/backend/domain/repository/article.go index b665c82b..31b7fca8 100644 --- a/backend/domain/repository/article.go +++ b/backend/domain/repository/article.go @@ -6,6 +6,8 @@ type ArticleRepository interface { Create(article *model.Article) (*model.Article, error) FindByID(article *model.Article) error GetArticles(articles *[]model.Article, limit, offset int) error + GetArticlesByCategory(articles *[]model.Article, slug string) error + GetArticlesByTag(articles *[]model.Article, slug string) error GetPager(article *model.Article) (int, error) Update(article *model.Article) (*model.Article, error) Delete(article *model.Article) error diff --git a/backend/infrastructure/persistence/article.go b/backend/infrastructure/persistence/article.go index f6f41f15..662292c5 100644 --- a/backend/infrastructure/persistence/article.go +++ b/backend/infrastructure/persistence/article.go @@ -52,6 +52,32 @@ func (ap *articlePersistence) GetPager(article *model.Article) (int, error) { return int(count), nil } +func (ap *articlePersistence) GetArticlesByCategory(articles *[]model.Article, slug string) error { + return ap.Reprica(). + // Preload("User"). + Preload("Category"). + Preload("Tags"). + Joins("LEFT JOIN categories AS category ON articles.category_id = category.id"). + Where("category.slug = ?", slug). + Find(&articles).Error +} + +// GetArticlesByTag retrieves articles based on a given tag slug. +// +// articles: a pointer to a slice of model.Article to store the retrieved articles. +// slug: the slug of the tag to filter the articles by. +// error: an error indicating if there was any issue retrieving the articles. +func (ap *articlePersistence) GetArticlesByTag(articles *[]model.Article, slug string) error { + return ap.Reprica(). + // Preload("User"). + Preload("Category"). + Preload("Tags"). + Joins("JOIN article_tags ON articles.id = article_tags.article_id"). + Joins("JOIN tags ON tags.id = article_tags.tag_id"). + Where("tags.slug = ?", slug). + Find(&articles).Error +} + func (ap *articlePersistence) Update(article *model.Article) (*model.Article, error) { return &model.Article{}, nil } diff --git a/backend/interfaces/api/handler/article.go b/backend/interfaces/api/handler/article.go index cb328b78..47fd1e63 100644 --- a/backend/interfaces/api/handler/article.go +++ b/backend/interfaces/api/handler/article.go @@ -6,31 +6,21 @@ import ( "github.com/gorilla/mux" "github.com/yoshihiro-shu/draft-backend/backend/application/usecase" + "github.com/yoshihiro-shu/draft-backend/backend/domain/model" "github.com/yoshihiro-shu/draft-backend/backend/interfaces/api/request" "gorm.io/gorm" ) type ArticleHandler interface { - Post(w http.ResponseWriter, r *http.Request) error Get(w http.ResponseWriter, r *http.Request) error - Put(w http.ResponseWriter, r *http.Request) error - Delete(w http.ResponseWriter, r *http.Request) error + GetArticlesByCategory(w http.ResponseWriter, r *http.Request) error + GetArticlesByTag(w http.ResponseWriter, r *http.Request) error } type articleHandler struct { - articleUseCase usecase.ArticleUseCase - C *request.Context -} - -func NewArticleHandler(articleUseCase usecase.ArticleUseCase, c *request.Context) ArticleHandler { - return &articleHandler{ - articleUseCase: articleUseCase, - C: c, - } -} - -func (ah *articleHandler) Post(w http.ResponseWriter, r *http.Request) error { - return nil + articleUseCase usecase.ArticleUseCase + articlesUseCase usecase.ArticlesUseCase + C *request.Context } func (ah *articleHandler) Get(w http.ResponseWriter, r *http.Request) error { @@ -52,10 +42,50 @@ func (ah *articleHandler) Get(w http.ResponseWriter, r *http.Request) error { return ah.C.JSON(w, http.StatusOK, article) } -func (ah *articleHandler) Put(w http.ResponseWriter, r *http.Request) error { - return nil +type responseGetArticlesByCategory struct { + Articles []model.Article `json:"articles"` +} + +func (ah *articleHandler) GetArticlesByCategory(w http.ResponseWriter, r *http.Request) error { + var res responseGetArticlesByCategory + vars := mux.Vars(r) + slug := vars["slug"] + + err := ah.articlesUseCase.GetArticlesByCategory(&res.Articles, slug) + if err != nil { + if err == gorm.ErrRecordNotFound { + ah.C.Logger.Warn("err no articles at latest Articles Handler") + return ah.C.JSON(w, http.StatusNotFound, err) + } + } + + return ah.C.JSON(w, http.StatusOK, res) +} + +type responseGetArticlesByTag struct { + Articles []model.Article `json:"articles"` +} + +func (ah *articleHandler) GetArticlesByTag(w http.ResponseWriter, r *http.Request) error { + var res responseGetArticlesByTag + vars := mux.Vars(r) + slug := vars["slug"] + + err := ah.articlesUseCase.GetArticlesByTag(&res.Articles, slug) + if err != nil { + if err == gorm.ErrRecordNotFound { + ah.C.Logger.Warn("err no articles at latest Articles Handler") + return ah.C.JSON(w, http.StatusNotFound, err) + } + } + + return ah.C.JSON(w, http.StatusOK, res) } -func (ah *articleHandler) Delete(w http.ResponseWriter, r *http.Request) error { - return nil +func NewArticleHandler(articleUseCase usecase.ArticleUseCase, articlesUseCase usecase.ArticlesUseCase, c *request.Context) ArticleHandler { + return &articleHandler{ + articleUseCase: articleUseCase, + articlesUseCase: articlesUseCase, + C: c, + } } diff --git a/backend/interfaces/api/handler/index.go b/backend/interfaces/api/handler/index.go index a15aa700..df8f6213 100644 --- a/backend/interfaces/api/handler/index.go +++ b/backend/interfaces/api/handler/index.go @@ -18,11 +18,6 @@ type TestRedis struct { func (h indexHandler) Index(w http.ResponseWriter, r *http.Request) error { return h.JSON(w, http.StatusOK, "HELLO WORLD") } -func (h indexHandler) AuthIndex(w http.ResponseWriter, r *http.Request) error { - id := h.GetAuthUserID(r.Context()) - return h.JSON(w, http.StatusOK, id) -} - func NewIndexHandler(c *request.Context) *indexHandler { return &indexHandler{c} } diff --git a/backend/interfaces/api/handler/lastest_articles.go b/backend/interfaces/api/handler/lastest_articles.go index 1d0dd1c3..687bd63b 100644 --- a/backend/interfaces/api/handler/lastest_articles.go +++ b/backend/interfaces/api/handler/lastest_articles.go @@ -61,7 +61,7 @@ func (h latestArticlesHandler) Get(w http.ResponseWriter, r *http.Request) error h.logger.Warn("err no articles at latest Articles Handler") return h.JSON(w, http.StatusNotFound, err) } - h.logger.Warn("failed at get articles at latest articles.", zap.Error(err)) + h.logger.Error("failed at get articles at latest articles.", zap.Error(err)) return h.Error(w, http.StatusInternalServerError, err) } diff --git a/backend/interfaces/api/user_api/api.go b/backend/interfaces/api/user_api/api.go index 50d41d7c..f67f06db 100644 --- a/backend/interfaces/api/user_api/api.go +++ b/backend/interfaces/api/user_api/api.go @@ -65,6 +65,8 @@ func Apply(r router.Router, conf config.Configs, logger logger.Logger, db model. ) article := r.Group("/articles") article.GET("/{id:[0-9]+}", articleHandler.Get) + article.GET("/category/{slug}", articleHandler.GetArticlesByCategory) + article.GET("/tag/{slug}", articleHandler.GetArticlesByTag) } // { // a := r.Group("/auth") diff --git a/backend/registory/article_registory.go b/backend/registory/article_registory.go index 6be90c49..f10b8b7a 100644 --- a/backend/registory/article_registory.go +++ b/backend/registory/article_registory.go @@ -11,5 +11,6 @@ import ( func NewArticleRegistory(ctx *request.Context, master, reprica func() *gorm.DB) handler.ArticleHandler { articleRepository := persistence.NewArticlePersistence(master, reprica) articleUseCase := usecase.NewArticleUseCase(articleRepository) - return handler.NewArticleHandler(articleUseCase, ctx) + articlesUseCase := usecase.NewArticlesUseCase(articleRepository) + return handler.NewArticleHandler(articleUseCase, articlesUseCase, ctx) } diff --git a/batch/qiita/src/main.rs b/batch/qiita/src/main.rs index 3c76bce3..c314bbee 100644 --- a/batch/qiita/src/main.rs +++ b/batch/qiita/src/main.rs @@ -4,6 +4,8 @@ use tokio; // tokioは非同期ランタイムです use tokio_postgres::{NoTls}; mod qiita_response; +mod tag_map; +mod tag_category_map; #[derive(Debug)] struct Tag { @@ -81,7 +83,33 @@ async fn main() -> Result<(), Box> { }); }; + // Insert New Tags + let tag_map = tag_map::create_map(); + for r in &res { + for t in &r.tags { + // insert tag if not exists + let check = db_client.query("SELECT * FROM tags WHERE name = $1", &[&t.name]).await?; + if check.len() == 0 { + let slug = tag_map.get(&t.name.as_str()); + let mut tag_id: i32 = 0; + if slug.is_none() { + let inserted_tag = db_client.query("INSERT INTO tags (name, slug) VALUES ($1, $2) RETURNING id", &[&t.name, &t.name]).await?; + tag_id = inserted_tag[0].get("id"); + } else { + let inserted_tag = db_client.query("INSERT INTO tags (name, slug) VALUES ($1, $2) RETURNING id", &[&t.name, &slug]).await?; + tag_id = inserted_tag[0].get("id"); + } + tags.push(Tag{ + id: tag_id, + name: t.name.clone(), + }); + println!("inserted tag: {}", t.name); + } + } + } + // Insert articles from Qiita + let tag_category_map = tag_category_map::create_map(); for r in res { let check = db_client.query("SELECT * FROM articles WHERE title = $1", &[&r.title]).await?; if check.len() != 0 { @@ -89,7 +117,17 @@ async fn main() -> Result<(), Box> { continue; } - let inserted_data = db_client.query("INSERT INTO articles (user_id, thumbnail_url, title, content, status) VALUES ($1, $2, $3, $4, $5) RETURNING id", &[&1, &"",&r.title, &r.body, &2]).await?; + let mut category_id : i32 = Default::default(); + for t in &r.tags { + if !tag_category_map.get(&t.name.as_str()).is_none() { + println!("{}: {}", t.name, tag_category_map.get(&t.name.as_str()).unwrap()); + let categpory_name = tag_category_map.get(&t.name.as_str()).unwrap().to_string(); + let query_get_cagterogry = db_client.query("SELECT id FROM categories WHERE name = $1", &[&categpory_name]).await?; + category_id = query_get_cagterogry[0].get("id"); + } + } + + let inserted_data = db_client.query("INSERT INTO articles (user_id, thumbnail_url, title, content, status, category_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id", &[&1, &"",&r.title, &r.body, &2, &category_id]).await?; let inserted_id: i32 = inserted_data[0].get("id"); for t in r.tags { // insert tag if not exists @@ -102,7 +140,6 @@ async fn main() -> Result<(), Box> { name: t.name.clone(), }) } - // insert article_tags for tt in &tags { if t.name == tt.name { @@ -110,6 +147,7 @@ async fn main() -> Result<(), Box> { } } } + println!("inserted article: {}", r.title); } Ok(()) diff --git a/batch/qiita/src/tag_category_map.rs b/batch/qiita/src/tag_category_map.rs new file mode 100644 index 00000000..596dcfdc --- /dev/null +++ b/batch/qiita/src/tag_category_map.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +pub fn create_map() -> HashMap<&'static str, &'static str> { + let mut tag_category: HashMap<&str, &str> = HashMap::new(); + + tag_category.insert("インフラ", "Infrastructure"); + tag_category.insert("アジャイル", "Agile"); + tag_category.insert("ビジネス", "Bussiness"); + tag_category.insert("マーケティング", "Marketing"); + tag_category.insert("kubernetes", "Infrastructure"); + tag_category.insert("Docker", "Infrastructure"); + tag_category.insert("要件定義", "System Design"); + tag_category.insert("ワイヤーフレーム", "System Design"); + tag_category.insert("googlecloud", "Infrastructure"); + tag_category.insert("Nuxt", "Frontend"); + + tag_category +} diff --git a/batch/qiita/src/tag_map.rs b/batch/qiita/src/tag_map.rs new file mode 100644 index 00000000..852a1eca --- /dev/null +++ b/batch/qiita/src/tag_map.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +pub fn create_map() -> HashMap<&'static str, &'static str> { + let mut tag_map: HashMap<&str, &str> = HashMap::new(); + + tag_map.insert("Docker", "docker"); + tag_map.insert("Kubernetes", "kubernetes"); + tag_map.insert("Golang", "golang"); + tag_map.insert("Agile", "agile"); + tag_map.insert("Requirement definition", "requirement-definition"); + tag_map.insert("Nuxt", "nuxt"); + tag_map.insert("Network", "network"); + tag_map.insert("dns", "dns"); + // 日本語の部分を英語に変換 + tag_map.insert("インフラ", "infrastructure"); + tag_map.insert("アジャイル", "agile-methodology"); + tag_map.insert("プロジェクト管理", "project-management"); + tag_map.insert("チームビルディング", "team-building"); + tag_map.insert("ふりかえり", "reflection"); + tag_map.insert("プロジェクトマネジメント", "project-management-advanced"); + tag_map.insert("AI", "ai"); + tag_map.insert("ビジネス", "business"); + tag_map.insert("生産性向上", "productivity-improvement"); + tag_map.insert("Google", "google"); + tag_map.insert("マーケティング", "marketing"); + tag_map.insert("SEO対策", "seo-strategies"); + tag_map.insert("解決", "problem-solving"); + tag_map.insert("論理的思考", "logical-thinking"); + tag_map.insert("リーダー", "leader"); + tag_map.insert("kubernetes", "kubernetes"); + tag_map.insert("kubectl", "kubectl"); + tag_map.insert("ckad", "ckad"); + tag_map.insert("CKA", "cka"); + tag_map.insert("プレゼンテーション", "presentation"); + tag_map.insert("ロジカルシンキング", "logical-thinking-advanced"); + tag_map.insert("Go", "go"); + tag_map.insert("dockerfile", "dockerfile"); + tag_map.insert("DockerHub", "docker-hub"); + tag_map.insert("沼", "quagmire"); + tag_map.insert("個人開発", "personal-development"); + tag_map.insert("GitHubActions", "github-actions"); + tag_map.insert("要件定義", "requirement-definition-advanced"); + tag_map.insert("ユースケース", "use-case"); + tag_map.insert("ワイヤーフレーム", "wireframe"); + tag_map.insert("デザイン設計", "design-planning"); + tag_map.insert("Cloud", "cloud"); + tag_map.insert("docker-compose", "docker-compose"); + tag_map.insert("googlecloud", "googlecloud"); + tag_map.insert("cookie", "cookie"); + tag_map.insert("Vue.js", "vue-js"); + tag_map.insert("Vuex", "vuex"); + tag_map.insert("ssr", "ssr"); + tag_map.insert("開発環境", "development-environment"); + tag_map.insert("TypeScript", "typescript"); + tag_map.insert("Nuxt3", "nuxt3"); + + tag_map +} diff --git a/migrations/db/20221013200749_create_categories.sql b/migrations/db/20221013200749_create_categories.sql index 67ae2482..a1da7463 100644 --- a/migrations/db/20221013200749_create_categories.sql +++ b/migrations/db/20221013200749_create_categories.sql @@ -8,7 +8,6 @@ CREATE TABLE categories ( name varchar(255) NOT NULL, slug varchar(255) NOT NULL, description varchar(255), - parent_id INTEGER, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (parent_id) REFERENCES categories(id) ); diff --git a/migrations/db/20221013202627_add_test_category_data.sql b/migrations/db/20221013202627_add_test_category_data.sql index 8d1a65ba..a2e373c7 100644 --- a/migrations/db/20221013202627_add_test_category_data.sql +++ b/migrations/db/20221013202627_add_test_category_data.sql @@ -3,12 +3,13 @@ SELECT 'up SQL query'; -- +goose StatementEnd -INSERT INTO categories (name, slug) VALUES ('Docker', 'docker'); -INSERT INTO categories (name, slug) VALUES ('Kubernetes', 'kubernetes'); -INSERT INTO categories (name, slug) VALUES ('Golang', 'golang'); INSERT INTO categories (name, slug) VALUES ('Agile', 'agile'); -INSERT INTO categories (name, slug) VALUES ('Requirement definition', 'requirement-definition'); -INSERT INTO categories (name, slug) VALUES ('Nuxt', 'nuxt'); +INSERT INTO categories (name, slug) VALUES ('Bussiness', 'bussiness'); +INSERT INTO categories (name, slug) VALUES ('Marketing', 'marketing'); +INSERT INTO categories (name, slug) VALUES ('Frontend', 'frontend'); +INSERT INTO categories (name, slug) VALUES ('Backend', 'backend'); +INSERT INTO categories (name, slug) VALUES ('Infrastructure', 'infrastructure'); +INSERT INTO categories (name, slug) VALUES ('System Design', 'system-design'); UPDATE articles SET category_id = 1 WHERE id = 1; UPDATE articles SET category_id = 2 WHERE id = 2; @@ -18,7 +19,11 @@ UPDATE articles SET category_id = 3 WHERE id = 3; -- +goose StatementBegin SELECT 'down SQL query'; -DELETE FROM categories WHERE 'name' = 'category-1'; -DELETE FROM categories WHERE 'name' = 'category-2'; -DELETE FROM categories WHERE 'name' = 'category-3'; +DELETE FROM categories WHERE 'name' = 'Agile'; +DELETE FROM categories WHERE 'name' = 'Bussiness'; +DELETE FROM categories WHERE 'name' = 'Backend'; +DELETE FROM categories WHERE 'name' = 'Frontend'; +DELETE FROM categories WHERE 'name' = 'Infrastructure'; +DELETE FROM categories WHERE 'name' = 'Marketing'; +DELETE FROM categories WHERE 'name' = 'System Design'; -- +goose StatementEnd