diff --git a/cmd/rpdata-restore/main.go b/cmd/rpdata-restore/main.go index ddb42d6..4e95b27 100644 --- a/cmd/rpdata-restore/main.go +++ b/cmd/rpdata-restore/main.go @@ -83,7 +83,7 @@ func main() { hideList := make(map[string]bool) - if parts[1] != "story" { + if parts[1] != "story" && parts[1] != "chapter" { continue } diff --git a/database/postgres/chapters.go b/database/postgres/chapters.go new file mode 100644 index 0000000..c8db618 --- /dev/null +++ b/database/postgres/chapters.go @@ -0,0 +1,127 @@ +package postgres + +import ( + "context" + "database/sql" + "git.aiterp.net/rpdata/api/database/postgres/psqlcore" + "git.aiterp.net/rpdata/api/internal/generate" + "git.aiterp.net/rpdata/api/models" +) + +type chapterRepository struct { + insertWithIDs bool + db *sql.DB +} + +func (r *chapterRepository) Find(ctx context.Context, id string) (*models.Chapter, error) { + chapter, err := psqlcore.New(r.db).SelectChapter(ctx, id) + if err != nil { + return nil, err + } + + return r.chapter(chapter), nil +} + +func (r *chapterRepository) List(ctx context.Context, filter models.ChapterFilter) ([]*models.Chapter, error) { + params := psqlcore.SelectChaptersParams{ + StoryID: "", + LimitSize: 10000, + } + if filter.StoryID != nil { + params.StoryID = *filter.StoryID + } + if filter.Limit > 0 { + params.LimitSize = int32(filter.Limit) + } + + chapters, err := psqlcore.New(r.db).SelectChapters(ctx, params) + if err != nil { + return nil, err + } + + return r.chapters(chapters), nil +} + +func (r *chapterRepository) Insert(ctx context.Context, chapter models.Chapter) (*models.Chapter, error) { + if !r.insertWithIDs || len(chapter.ID) < 8 { + chapter.ID = generate.ChapterID() + } + + err := psqlcore.New(r.db).InsertChapter(ctx, psqlcore.InsertChapterParams{ + ID: chapter.ID, + StoryID: chapter.StoryID, + Title: chapter.Title, + Author: chapter.Author, + Source: chapter.Source, + CreatedDate: chapter.CreatedDate.UTC(), + FictionalDate: chapter.FictionalDate.UTC(), + EditedDate: chapter.EditedDate.UTC(), + CommentMode: string(chapter.CommentMode), + CommentsLocked: chapter.CommentsLocked, + }) + if err != nil { + return nil, err + } + + return &chapter, nil +} + +func (r *chapterRepository) Update(ctx context.Context, chapter models.Chapter, update models.ChapterUpdate) (*models.Chapter, error) { + chapter.ApplyUpdate(update) + + err := psqlcore.New(r.db).UpdateChapter(ctx, psqlcore.UpdateChapterParams{ + Title: chapter.Title, + Source: chapter.Source, + FictionalDate: chapter.FictionalDate.UTC(), + CommentMode: string(chapter.CommentMode), + CommentsLocked: chapter.CommentsLocked, + ID: chapter.ID, + }) + if err != nil { + return nil, err + } + + return &chapter, nil +} + +func (r *chapterRepository) Move(ctx context.Context, chapter models.Chapter, from, to models.Story) (*models.Chapter, error) { + err := psqlcore.New(r.db).UpdateChapterStoryID(ctx, psqlcore.UpdateChapterStoryIDParams{ + StoryID: to.ID, + ID: chapter.ID, + }) + if err != nil { + return nil, err + } + + chapter.StoryID = to.ID + + return &chapter, nil +} + +func (r *chapterRepository) Delete(ctx context.Context, chapter models.Chapter) error { + return psqlcore.New(r.db).DeleteChapter(ctx, chapter.ID) +} + +func (r *chapterRepository) chapter(chapter psqlcore.StoryChapter) *models.Chapter { + return &models.Chapter{ + ID: chapter.ID, + StoryID: chapter.StoryID, + Title: chapter.Title, + Author: chapter.Author, + Source: chapter.Source, + CreatedDate: chapter.CreatedDate, + FictionalDate: chapter.FictionalDate, + EditedDate: chapter.EditedDate, + CommentMode: models.ChapterCommentMode(chapter.CommentMode), + CommentsLocked: chapter.CommentsLocked, + } +} + +func (r *chapterRepository) chapters(chapters []psqlcore.StoryChapter) []*models.Chapter { + results := make([]*models.Chapter, 0, len(chapters)) + for _, chapter := range chapters { + results = append(results, r.chapter(chapter)) + } + + return results +} diff --git a/database/postgres/db.go b/database/postgres/db.go index a230f16..0b9df7f 100644 --- a/database/postgres/db.go +++ b/database/postgres/db.go @@ -81,7 +81,7 @@ func (d *DB) Stories() repositories.StoryRepository { } func (d *DB) Chapters() repositories.ChapterRepository { - panic("implement me") + return &chapterRepository{insertWithIDs: d.insertWithIDs, db: d.db} } func (d *DB) Comments() repositories.CommentRepository { diff --git a/database/postgres/migrations/20210326165205_create_table_chapter.sql b/database/postgres/migrations/20210326165205_create_table_chapter.sql new file mode 100644 index 0000000..0dc79eb --- /dev/null +++ b/database/postgres/migrations/20210326165205_create_table_chapter.sql @@ -0,0 +1,20 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE story_chapter ( + id TEXT NOT NULL PRIMARY KEY, + story_id TEXT NOT NULL, + title TEXT NOT NULL, + author TEXT NOT NULL, + source TEXT NOT NULL, + created_date TIMESTAMP NOT NULL, + fictional_date TIMESTAMP NOT NULL, + edited_date TIMESTAMP NOT NULL, + comment_mode TEXT NOT NULL, + comments_locked BOOLEAN NOT NULL +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE story_chapter; +-- +goose StatementEnd diff --git a/database/postgres/migrations/20210326165603_create_index_chapter_story_id.sql b/database/postgres/migrations/20210326165603_create_index_chapter_story_id.sql new file mode 100644 index 0000000..480e8b1 --- /dev/null +++ b/database/postgres/migrations/20210326165603_create_index_chapter_story_id.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX story_chapter_index_story_id ON story_chapter (story_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS story_chapter_index_story_id; +-- +goose StatementEnd diff --git a/database/postgres/psqlcore/changes.sql.go b/database/postgres/psqlcore/changes.sql.go index f660043..f1e172f 100644 --- a/database/postgres/psqlcore/changes.sql.go +++ b/database/postgres/psqlcore/changes.sql.go @@ -80,6 +80,7 @@ WHERE ($1::bool = false OR keys && ($2::text[])) AND ($3::bool = false OR date >= $4::timestamp) AND ($5::bool = false OR date <= $6::timestamp) AND ($7::bool = false OR author = $8::text) +ORDER BY date DESC LIMIT $9::int ` diff --git a/database/postgres/psqlcore/channels.sql.go b/database/postgres/psqlcore/channels.sql.go index b9eebb8..28ff666 100644 --- a/database/postgres/psqlcore/channels.sql.go +++ b/database/postgres/psqlcore/channels.sql.go @@ -69,6 +69,7 @@ WHERE ($1::bool = false OR name = ANY($2::text[])) AND ($3::bool = false OR logged = $4) AND ($5::bool = false OR event_name = $6) AND ($7::bool = false OR location_name = $8) +ORDER BY name LIMIT $9::int ` diff --git a/database/postgres/psqlcore/chapters.sql.go b/database/postgres/psqlcore/chapters.sql.go new file mode 100644 index 0000000..ca35325 --- /dev/null +++ b/database/postgres/psqlcore/chapters.sql.go @@ -0,0 +1,177 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: chapters.sql + +package psqlcore + +import ( + "context" + "time" +) + +const deleteChapter = `-- name: DeleteChapter :exec +DELETE FROM story_chapter WHERE id=$1 +` + +func (q *Queries) DeleteChapter(ctx context.Context, id string) error { + _, err := q.db.ExecContext(ctx, deleteChapter, id) + return err +} + +const deleteChaptersByStoryID = `-- name: DeleteChaptersByStoryID :exec +DELETE FROM story_chapter WHERE story_id=$1 +` + +func (q *Queries) DeleteChaptersByStoryID(ctx context.Context, storyID string) error { + _, err := q.db.ExecContext(ctx, deleteChaptersByStoryID, storyID) + return err +} + +const insertChapter = `-- name: InsertChapter :exec +INSERT INTO story_chapter (id, story_id, title, author, source, created_date, fictional_date, edited_date, comment_mode, comments_locked) +VALUES ( + $1::TEXT, $2::TEXT, $3::TEXT, $4::TEXT, $5::TEXT, + $6::TIMESTAMP, $7::TIMESTAMP, $8::TIMESTAMP, + $9::TEXT, $10::BOOLEAN +) +` + +type InsertChapterParams struct { + ID string `json:"id"` + StoryID string `json:"story_id"` + Title string `json:"title"` + Author string `json:"author"` + Source string `json:"source"` + CreatedDate time.Time `json:"created_date"` + FictionalDate time.Time `json:"fictional_date"` + EditedDate time.Time `json:"edited_date"` + CommentMode string `json:"comment_mode"` + CommentsLocked bool `json:"comments_locked"` +} + +func (q *Queries) InsertChapter(ctx context.Context, arg InsertChapterParams) error { + _, err := q.db.ExecContext(ctx, insertChapter, + arg.ID, + arg.StoryID, + arg.Title, + arg.Author, + arg.Source, + arg.CreatedDate, + arg.FictionalDate, + arg.EditedDate, + arg.CommentMode, + arg.CommentsLocked, + ) + return err +} + +const selectChapter = `-- name: SelectChapter :one +SELECT id, story_id, title, author, source, created_date, fictional_date, edited_date, comment_mode, comments_locked FROM story_chapter WHERE id=$1::TEXT LIMIT 1 +` + +func (q *Queries) SelectChapter(ctx context.Context, dollar_1 string) (StoryChapter, error) { + row := q.db.QueryRowContext(ctx, selectChapter, dollar_1) + var i StoryChapter + err := row.Scan( + &i.ID, + &i.StoryID, + &i.Title, + &i.Author, + &i.Source, + &i.CreatedDate, + &i.FictionalDate, + &i.EditedDate, + &i.CommentMode, + &i.CommentsLocked, + ) + return i, err +} + +const selectChapters = `-- name: SelectChapters :many +SELECT id, story_id, title, author, source, created_date, fictional_date, edited_date, comment_mode, comments_locked FROM story_chapter WHERE (sqlx.arg(story_id)::TEXT == '' OR story_id=$1::TEXT) ORDER BY created_date LIMIT $2::INT +` + +type SelectChaptersParams struct { + StoryID string `json:"story_id"` + LimitSize int32 `json:"limit_size"` +} + +func (q *Queries) SelectChapters(ctx context.Context, arg SelectChaptersParams) ([]StoryChapter, error) { + rows, err := q.db.QueryContext(ctx, selectChapters, arg.StoryID, arg.LimitSize) + if err != nil { + return nil, err + } + defer rows.Close() + items := []StoryChapter{} + for rows.Next() { + var i StoryChapter + if err := rows.Scan( + &i.ID, + &i.StoryID, + &i.Title, + &i.Author, + &i.Source, + &i.CreatedDate, + &i.FictionalDate, + &i.EditedDate, + &i.CommentMode, + &i.CommentsLocked, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateChapter = `-- name: UpdateChapter :exec +UPDATE story_chapter +SET title=$1, + source=$2, + fictional_date=$3, + comment_mode=$4, + comments_locked=$5 +WHERE id=$6 +` + +type UpdateChapterParams struct { + Title string `json:"title"` + Source string `json:"source"` + FictionalDate time.Time `json:"fictional_date"` + CommentMode string `json:"comment_mode"` + CommentsLocked bool `json:"comments_locked"` + ID string `json:"id"` +} + +func (q *Queries) UpdateChapter(ctx context.Context, arg UpdateChapterParams) error { + _, err := q.db.ExecContext(ctx, updateChapter, + arg.Title, + arg.Source, + arg.FictionalDate, + arg.CommentMode, + arg.CommentsLocked, + arg.ID, + ) + return err +} + +const updateChapterStoryID = `-- name: UpdateChapterStoryID :exec +UPDATE story_chapter +SET story_id=$1::TEXT +WHERE id=$2 +` + +type UpdateChapterStoryIDParams struct { + StoryID string `json:"story_id"` + ID string `json:"id"` +} + +func (q *Queries) UpdateChapterStoryID(ctx context.Context, arg UpdateChapterStoryIDParams) error { + _, err := q.db.ExecContext(ctx, updateChapterStoryID, arg.StoryID, arg.ID) + return err +} diff --git a/database/postgres/psqlcore/models.go b/database/postgres/psqlcore/models.go index dfa104b..b7af1f2 100644 --- a/database/postgres/psqlcore/models.go +++ b/database/postgres/psqlcore/models.go @@ -60,3 +60,16 @@ type Story struct { FictionalDate time.Time `json:"fictional_date"` UpdatedDate time.Time `json:"updated_date"` } + +type StoryChapter struct { + ID string `json:"id"` + StoryID string `json:"story_id"` + Title string `json:"title"` + Author string `json:"author"` + Source string `json:"source"` + CreatedDate time.Time `json:"created_date"` + FictionalDate time.Time `json:"fictional_date"` + EditedDate time.Time `json:"edited_date"` + CommentMode string `json:"comment_mode"` + CommentsLocked bool `json:"comments_locked"` +} diff --git a/database/postgres/psqlcore/stories.sql.go b/database/postgres/psqlcore/stories.sql.go index 3b4bfce..c104a66 100644 --- a/database/postgres/psqlcore/stories.sql.go +++ b/database/postgres/psqlcore/stories.sql.go @@ -66,6 +66,7 @@ WHERE ($1::bool = false OR id = ANY($2::text[])) AND ($9::bool = false OR category = $10::text) AND ($11::bool = false OR open = $12::bool) AND ($13::bool = false OR unlisted = $14::bool) +ORDER BY updated_date LIMIT $15::int ` diff --git a/database/postgres/queries/chapters.sql b/database/postgres/queries/chapters.sql new file mode 100644 index 0000000..3f215aa --- /dev/null +++ b/database/postgres/queries/chapters.sql @@ -0,0 +1,33 @@ +-- name: SelectChapter :one +SELECT * FROM story_chapter WHERE id=$1::TEXT LIMIT 1; + +-- name: SelectChapters :many +SELECT * FROM story_chapter WHERE (sqlx.arg(story_id)::TEXT == '' OR story_id=sqlc.arg(story_id)::TEXT) ORDER BY created_date LIMIT sqlc.arg(limit_size)::INT; + +-- name: InsertChapter :exec +INSERT INTO story_chapter (id, story_id, title, author, source, created_date, fictional_date, edited_date, comment_mode, comments_locked) +VALUES ( + sqlc.arg(id)::TEXT, sqlc.arg(story_id)::TEXT, sqlc.arg(title)::TEXT, sqlc.arg(author)::TEXT, sqlc.arg(source)::TEXT, + sqlc.arg(created_date)::TIMESTAMP, sqlc.arg(fictional_date)::TIMESTAMP, sqlc.arg(edited_date)::TIMESTAMP, + sqlc.arg(comment_mode)::TEXT, sqlc.arg(comments_locked)::BOOLEAN +); + +-- name: UpdateChapterStoryID :exec +UPDATE story_chapter +SET story_id=sqlc.arg(story_id)::TEXT +WHERE id=sqlc.arg(id); + +-- name: UpdateChapter :exec +UPDATE story_chapter +SET title=sqlc.arg(title), + source=sqlc.arg(source), + fictional_date=sqlc.arg(fictional_date), + comment_mode=sqlc.arg(comment_mode), + comments_locked=sqlc.arg(comments_locked) +WHERE id=sqlc.arg(id); + +-- name: DeleteChapter :exec +DELETE FROM story_chapter WHERE id=$1; + +-- name: DeleteChaptersByStoryID :exec +DELETE FROM story_chapter WHERE story_id=$1; diff --git a/database/postgres/stories.go b/database/postgres/stories.go index 2d00dd7..2dc3556 100644 --- a/database/postgres/stories.go +++ b/database/postgres/stories.go @@ -178,9 +178,14 @@ func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag } func (r *storyRepository) Delete(ctx context.Context, story models.Story) error { - q := psqlcore.New(r.db) + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + q := psqlcore.New(tx) - err := q.ClearTagsByTarget(ctx, psqlcore.ClearTagsByTargetParams{ + err = q.ClearTagsByTarget(ctx, psqlcore.ClearTagsByTargetParams{ TargetKind: "Story", TargetID: story.ID, }) @@ -188,7 +193,17 @@ func (r *storyRepository) Delete(ctx context.Context, story models.Story) error return err } - return q.DeleteStory(ctx, story.ID) + err = q.DeleteChaptersByStoryID(ctx, story.ID) + if err != nil { + return err + } + + err = q.DeleteStory(ctx, story.ID) + if err != nil { + return err + } + + return tx.Commit() } func (r *storyRepository) story(story psqlcore.Story, tags []psqlcore.CommonTag) *models.Story { diff --git a/models/chapter.go b/models/chapter.go index 317318f..2543cc6 100644 --- a/models/chapter.go +++ b/models/chapter.go @@ -16,6 +16,24 @@ type Chapter struct { CommentsLocked bool `bson:"commentsLocked"` } +func (chapter *Chapter) ApplyUpdate(update ChapterUpdate) { + if update.Title != nil { + chapter.Title = *update.Title + } + if update.Source != nil { + chapter.Source = *update.Source + } + if update.FictionalDate != nil { + chapter.FictionalDate = update.FictionalDate.UTC() + } + if update.CommentMode != nil { + chapter.CommentMode = *update.CommentMode + } + if update.CommentsLocked != nil { + chapter.CommentsLocked = *update.CommentsLocked + } +} + // CanComment returns true if the chapter can be commented to. func (chapter *Chapter) CanComment() bool { return !chapter.CommentsLocked && chapter.CommentMode.IsEnabled()