Commit e7083eec authored by k1350's avatar k1350
Browse files

[update] 記事作成、ブログ削除、記事削除追加

parent 2d12b87c
......@@ -2,7 +2,8 @@ create table user (
id int auto_increment primary key comment "ユーザーID",
last_logined datetime not null comment "最終ログイン日時",
created_at datetime not null comment "作成日時",
updated_at datetime not null comment "更新日時"
updated_at datetime not null comment "更新日時",
deleted_at datetime default null comment "削除日時"
) comment = "ユーザー";
create table provider_user (
......@@ -11,6 +12,7 @@ create table provider_user (
user_id int not null comment "ユーザーID",
created_at datetime not null comment "作成日時",
updated_at datetime not null comment "更新日時",
deleted_at datetime default null comment "削除日時",
primary key(uid, provider),
index user_id_index (user_id)
) comment = "連携ユーザー";
......@@ -27,6 +29,7 @@ create table blog (
password varchar(255) not null default "" comment "パスワード",
created_at datetime not null comment "作成日時",
updated_at datetime not null comment "更新日時",
deleted_at datetime default null comment "削除日時",
index blog_key_index (blog_key),
index user_id_index (user_id)
) comment = "ブログ";
......@@ -38,6 +41,7 @@ create table blog_link (
url varchar(3000) not null default "" comment "リンクURL",
created_at datetime not null comment "作成日時",
updated_at datetime not null comment "更新日時",
deleted_at datetime default null comment "削除日時",
index blog_id_index (blog_id)
) comment = "ブログリンク";
......@@ -47,5 +51,6 @@ create table article (
content text not null comment "内容",
created_at datetime not null comment "作成日時",
updated_at datetime not null comment "更新日時",
deleted_at datetime default null comment "削除日時",
index blog_id_index (blog_id)
) comment = "記事";
\ No newline at end of file
......@@ -35,7 +35,7 @@ resolver:
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
# - "github.com/k1350/gql_sololog/graph/model"
- "github.com/k1350/gql_sololog/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
......
This diff is collapsed.
......@@ -57,6 +57,11 @@ type BlogLinkInput struct {
URL string `json:"url"`
}
type CreateArticleInput struct {
BlogID string `json:"blogId"`
Content string `json:"content"`
}
type CreateBlogInput struct {
Author string `json:"author"`
Name string `json:"name"`
......
......@@ -8,6 +8,9 @@ type Query {
type Mutation {
createBlog(input: CreateBlogInput!): Blog
createArticle(input: CreateArticleInput!): Article
deleteBlog(id: ID! @stringValue(isInt: true)): Boolean!
deleteArticle(id: ID! @stringValue(isInt: true)): Boolean!
}
type PageInfo {
......@@ -79,17 +82,22 @@ input ArticlePaginationInput {
}
input CreateBlogInput {
author: String! @stringValue(max: 50)
name: String! @stringValue(max: 255)
description: String @stringValue(max: 500)
author: String! @stringValue(min: 1, max: 50)
name: String! @stringValue(min: 1, max: 255)
description: String @stringValue(min: 1, max: 500)
publishOption: PublishOption! @publishOptionValue(requireIf: "password,PASSWORD")
password: String @passwordValue(isValidPassword: true)
links: [BlogLinkInput!]
}
input BlogLinkInput {
name: String @stringValue(max: 25)
url: String! @stringValue(max: 3000)
name: String @stringValue(min: 1, max: 255)
url: String! @stringValue(min: 1, max: 3000)
}
input CreateArticleInput {
blogId: ID! @stringValue(isInt: true)
content: String! @stringValue(min: 1, max: 40000)
}
enum PublishOption {
......@@ -105,6 +113,7 @@ directive @numberValue(
directive @stringValue(
isInt: Boolean
min: Int
max: Int
) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
......
......@@ -71,6 +71,48 @@ func (r *mutationResolver) CreateBlog(ctx context.Context, input model.CreateBlo
return result, nil
}
func (r *mutationResolver) CreateArticle(ctx context.Context, input model.CreateArticleInput) (*model.Article, error) {
userId := authMIddleware.CtxAuthContext(ctx).UserId
if userId == nil {
return nil, errors.New("Not Authenticated")
}
result, err := r.ArticleRepository.CreateArticle(input, *userId)
if err != nil {
log.Println(err)
return nil, err
}
return result, nil
}
func (r *mutationResolver) DeleteBlog(ctx context.Context, id string) (bool, error) {
userId := authMIddleware.CtxAuthContext(ctx).UserId
if userId == nil {
return false, errors.New("Not Authenticated")
}
intId, _ := strconv.ParseInt(id, 10, 64)
result, err := r.BlogRepository.DeleteById(intId, *userId)
if err != nil {
log.Println(err)
}
return result, err
}
func (r *mutationResolver) DeleteArticle(ctx context.Context, id string) (bool, error) {
userId := authMIddleware.CtxAuthContext(ctx).UserId
if userId == nil {
return false, errors.New("Not Authenticated")
}
intId, _ := strconv.ParseInt(id, 10, 64)
result, err := r.ArticleRepository.DeleteById(intId, *userId)
if err != nil {
log.Println(err)
}
return result, err
}
func (r *queryResolver) Blogs(ctx context.Context) ([]*model.Blog, error) {
userId := authMIddleware.CtxAuthContext(ctx).UserId
if userId == nil {
......
......@@ -3,11 +3,11 @@ package directive
import (
"context"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"unicode/utf8"
"reflect"
"github.com/99designs/gqlgen/graphql"
"github.com/k1350/gql_sololog/graph/model"
......@@ -53,7 +53,7 @@ func NumberValue(ctx context.Context, obj interface{}, next graphql.Resolver, mi
return v, nil
}
func StringValue(ctx context.Context, obj interface{}, next graphql.Resolver, isInt *bool, max *int) (res interface{}, err error) {
func StringValue(ctx context.Context, obj interface{}, next graphql.Resolver, isInt *bool, min *int, max *int) (res interface{}, err error) {
v, err := next(ctx)
if err != nil {
return nil, err
......@@ -70,7 +70,12 @@ func StringValue(ctx context.Context, obj interface{}, next graphql.Resolver, is
}
if max != nil {
if *max < utf8.RuneCountInString(*value) {
return nil, fmt.Errorf(*value + ": length is larger than max length of " + strconv.Itoa(*max))
return nil, fmt.Errorf("length is larger than max length of " + strconv.Itoa(*max))
}
}
if min != nil {
if utf8.RuneCountInString(*value) < *min {
return nil, fmt.Errorf("length is smaller than min length of " + strconv.Itoa(*min))
}
}
case string:
......@@ -82,7 +87,12 @@ func StringValue(ctx context.Context, obj interface{}, next graphql.Resolver, is
}
if max != nil {
if *max < utf8.RuneCountInString(v.(string)) {
return nil, fmt.Errorf(v.(string) + ": length is larger than max length of " + strconv.Itoa(*max))
return nil, fmt.Errorf("length is larger than max length of " + strconv.Itoa(*max))
}
}
if min != nil {
if utf8.RuneCountInString(v.(string)) < *min {
return nil, fmt.Errorf("length is smaller than min length of " + strconv.Itoa(*min))
}
}
default:
......
......@@ -2,8 +2,10 @@ package article
import (
"database/sql"
"errors"
"strconv"
"strings"
"time"
"github.com/k1350/gql_sololog/graph/model"
)
......@@ -12,6 +14,8 @@ type IArticleRepository interface {
FindByBlogId(blogId int64, input model.ArticlePaginationInput) ([]*model.Article, error)
HasPreviousPage(blogId int64, input model.ArticlePaginationInput) (bool, error)
HasNextPage(blogId int64, input model.ArticlePaginationInput) (bool, error)
CreateArticle(input model.CreateArticleInput, userId int) (*model.Article, error)
DeleteById(id int64, userId int) (bool, error)
}
type ArticleRepository struct {
......@@ -25,7 +29,7 @@ func NewArticleRepository(db *sql.DB) IArticleRepository {
func (r *ArticleRepository) FindByBlogId(blogId int64, input model.ArticlePaginationInput) ([]*model.Article, error) {
var sb strings.Builder
var args []interface{}
s := "SELECT article.id, article.content, article.created_at, article.updated_at FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ?"
s := "SELECT article.id, article.content, article.created_at, article.updated_at FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ? AND blog.deleted_at IS NULL AND article.deleted_at IS NULL"
args = append(args, blogId)
sb.WriteString(s)
......@@ -94,7 +98,7 @@ func (r *ArticleRepository) HasPreviousPage(blogId int64, input model.ArticlePag
if input.After != nil {
var sb strings.Builder
var args []interface{}
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ?"
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ? AND blog.deleted_at IS NULL AND article.deleted_at IS NULL"
sb.WriteString(s)
args = append(args, blogId)
......@@ -129,7 +133,7 @@ func (r *ArticleRepository) HasNextPage(blogId int64, input model.ArticlePaginat
if input.Before != nil {
var sb strings.Builder
var args []interface{}
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ?"
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ? AND blog.deleted_at IS NULL AND article.deleted_at IS NULL"
sb.WriteString(s)
args = append(args, blogId)
......@@ -150,10 +154,68 @@ func (r *ArticleRepository) HasNextPage(blogId int64, input model.ArticlePaginat
return false, nil
}
func (r *ArticleRepository) CreateArticle(input model.CreateArticleInput, userId int) (*model.Article, error) {
// 本体の存在チェック&権限チェックを行う
blogId, _ := strconv.ParseInt(input.BlogID, 10, 64)
s := "SELECT count(id) FROM blog WHERE id = ? AND user_id = ? AND deleted_at IS NULL"
var i string
if err := r.QueryRow(s, blogId, userId).Scan(&i); err != nil {
return nil, err
}
count, err := strconv.Atoi(i)
if err != nil {
return nil, err
}
if count == 0 {
return nil, errors.New("Not Authenticated")
}
now := time.Now().UTC()
cas := "INSERT INTO article (blog_id, content, created_at, updated_at) VALUES (?, ?, ?, ?)"
res, err := r.Exec(cas, blogId, input.Content, now, now)
if err != nil {
return nil, err
}
insertId, err := res.LastInsertId()
if err != nil {
return nil, err
}
sas := "SELECT id, content, created_at, updated_at FROM article WHERE id = ? AND deleted_at IS NULL"
a := &model.Article{}
if err = r.QueryRow(sas, insertId).Scan(&a.ID, &a.Content, &a.CreatedAt, &a.UpdatedAt); err != nil {
return nil, err
}
return a, nil
}
func (r *ArticleRepository) DeleteById(id int64, userId int) (bool, error) {
// 対象の存在チェック&権限チェックを行う
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE article.id = ? AND blog.user_id = ? AND article.deleted_at IS NULL"
var i string
if err := r.QueryRow(s, id, userId).Scan(&i); err != nil {
return false, err
}
count, err := strconv.Atoi(i)
if err != nil {
return false, err
}
if count == 0 {
return false, errors.New("Not Authenticated")
}
// 削除
now := time.Now().UTC()
as := "UPDATE article SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL"
_, err = r.Exec(as, now, id)
if err != nil {
return false, err
}
return true, nil
}
func (r *ArticleRepository) countApplyCursorsToEdges(blogId int64, input model.ArticlePaginationInput) (int, error) {
var sb strings.Builder
var args []interface{}
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ?"
s := "SELECT count(article.id) FROM article INNER JOIN blog ON blog.id = article.blog_id WHERE blog.id = ? AND blog.deleted_at IS NULL AND article.deleted_at IS NULL"
sb.WriteString(s)
args = append(args, blogId)
......
......@@ -3,6 +3,7 @@ package blog
import (
"database/sql"
"golang.org/x/crypto/bcrypt"
"strconv"
"strings"
"time"
......@@ -16,6 +17,7 @@ type IBlogRepository interface {
FindByBlogKey(blogKey string, opts *BlogByKeyOpts) (*model.Blog, error)
FindPublishOption(blogKey string) (*model.PublishOption, error)
CreateBlog(input model.CreateBlogInput, userId int) (int64, error)
DeleteById(id int64, userId int) (bool, error)
}
type BlogRepository struct {
......@@ -35,7 +37,7 @@ func NewBlogRepository(db *sql.DB) IBlogRepository {
}
func (r *BlogRepository) FindByUserId(userId int) ([]*model.Blog, error) {
s := "SELECT " + fields + " FROM blog WHERE user_id = ? ORDER BY id DESC"
s := "SELECT " + fields + " FROM blog WHERE user_id = ? AND deleted_at IS NULL ORDER BY id DESC"
rows, err := r.Query(s, userId)
......@@ -64,7 +66,7 @@ func (r *BlogRepository) FindByUserId(userId int) ([]*model.Blog, error) {
}
func (r *BlogRepository) FindById(id int64, userId int) (*model.Blog, error) {
s := "SELECT " + fields + " FROM blog WHERE id = ? AND user_id = ? LIMIT 1"
s := "SELECT " + fields + " FROM blog WHERE id = ? AND user_id = ? AND deleted_at IS NULL LIMIT 1"
b := &model.Blog{}
err := r.QueryRow(s, id, userId).Scan(&b.ID, &b.BlogKey, &b.UserId, &b.Author, &b.Name, &b.Description, &b.PublishOption, &b.CreatedAt, &b.UpdatedAt)
......@@ -80,7 +82,7 @@ func (r *BlogRepository) FindById(id int64, userId int) (*model.Blog, error) {
func (r *BlogRepository) FindByBlogKey(blogKey string, opts *BlogByKeyOpts) (*model.Blog, error) {
var sb strings.Builder
s := "SELECT " + fields + ", password FROM blog WHERE blog_key = ? AND (publish_option = 1"
s := "SELECT " + fields + ", password FROM blog WHERE blog_key = ? AND deleted_at IS NULL AND (publish_option = 1"
sb.WriteString(s)
......@@ -139,7 +141,7 @@ func (r *BlogRepository) FindByBlogKey(blogKey string, opts *BlogByKeyOpts) (*mo
}
func (r *BlogRepository) FindPublishOption(blogKey string) (*model.PublishOption, error) {
s := "SELECT " + publishOptionField + " FROM blog WHERE blog_key = ? LIMIT 1"
s := "SELECT " + publishOptionField + " FROM blog WHERE blog_key = ? AND deleted_at IS NULL LIMIT 1"
var po *model.PublishOption
err := r.QueryRow(s, blogKey).Scan(&po)
switch {
......@@ -231,6 +233,57 @@ func (r *BlogRepository) CreateBlog(input model.CreateBlogInput, userId int) (in
return insertId, nil
}
func (r *BlogRepository) DeleteById(id int64, userId int) (bool, error) {
// 最初に削除対象の存在チェック&権限チェックする
s := "SELECT count(id) FROM blog WHERE id = ? AND user_id = ? AND deleted_at IS NULL"
var i string
if err := r.QueryRow(s, id, userId).Scan(&i); err != nil {
return false, err
}
count, err := strconv.Atoi(i)
if err != nil {
return false, err
}
if count == 0 {
return false, nil
}
now := time.Now().UTC()
sb := "UPDATE blog SET deleted_at = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL"
sbl := "UPDATE blog_link SET deleted_at = ? WHERE blog_id = ? AND deleted_at IS NULL"
sa := "UPDATE article SET deleted_at = ? WHERE blog_id = ? AND deleted_at IS NULL"
// トランザクション処理
tx, err := r.Begin()
defer func() {
// panicが起きたらロールバック
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}()
_, err = tx.Exec(sb, now, id, userId)
if err != nil {
tx.Rollback()
return false, err
}
_, err = tx.Exec(sbl, now, id)
if err != nil {
tx.Rollback()
return false, err
}
_, err = tx.Exec(sa, now, id)
if err != nil {
tx.Rollback()
return false, err
}
tx.Commit()
return true, nil
}
func convertPublishOptionToInt(opt model.PublishOption) int {
switch opt {
case model.PublishOptionPublic:
......
......@@ -20,7 +20,7 @@ func NewBlogLinkRepository(db *sql.DB) IBlogLinkRepository {
}
func (r *BlogLinkRepository) FindByBlogIds(blogIds []int) (map[int][]*model.BlogLink, error) {
sql := "SELECT id, name, url, blog_id FROM blog_link WHERE blog_id IN (?" + strings.Repeat(",?", len(blogIds)-1) + ") ORDER BY id"
sql := "SELECT id, name, url, blog_id FROM blog_link WHERE blog_id IN (?" + strings.Repeat(",?", len(blogIds)-1) + ") AND deleted_at IS NULL ORDER BY id"
args := make([]interface{}, len(blogIds))
for i, blogId := range blogIds {
......
......@@ -19,7 +19,7 @@ func TestFindByBlogIds(t *testing.T) {
AddRow(1, "テスト", "http://example.com", -1).
AddRow(3, "テスト2", "http://2.example.com", -1)
s := "SELECT id, name, url, blog_id FROM blog_link WHERE blog_id IN (?,?) ORDER BY id"
s := "SELECT id, name, url, blog_id FROM blog_link WHERE blog_id IN (?,?) AND deleted_at IS NULL ORDER BY id"
args := []int{-1, -2}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment