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 storyRepository struct { insertWithIDs bool db *sql.DB } func (r *storyRepository) Find(ctx context.Context, id string) (*models.Story, error) { q := psqlcore.New(r.db) story, err := q.SelectStory(ctx, id) if err != nil { return nil, err } tags, err := q.SelectTagsByTarget(ctx, psqlcore.SelectTagsByTargetParams{ TargetKind: "Story", TargetID: story.ID, }) if err != nil && err != sql.ErrNoRows { return nil, err } return r.story(story, tags), nil } func (r *storyRepository) List(ctx context.Context, filter models.StoryFilter) ([]*models.Story, error) { q := psqlcore.New(r.db) params := psqlcore.SelectStoriesParams{LimitSize: 100} if len(filter.Tags) > 0 { targets, err := q.SelectTargetsByTags(ctx, psqlcore.SelectTargetsByTagsParams{ TagNames: models.EncodeTagArray(filter.Tags), TargetKind: "Story", }) if err != nil && err != sql.ErrNoRows { return nil, err } if len(params.Ids) == 0 { return []*models.Story{}, nil } params.FilterID = true params.Ids = targets } if filter.Author != nil { params.FilterAuthor = true params.Author = *filter.Author } if filter.Category != nil { params.FilterCategory = true params.Category = string(*filter.Category) } if !filter.EarliestFictionalDate.IsZero() { params.FilterEarlistFictionalDate = true params.EarliestFictionalDate = filter.EarliestFictionalDate.UTC() } if !filter.LatestFictionalDate.IsZero() { params.FilterLastestFictionalDate = true params.LatestFictionalDate = filter.LatestFictionalDate.UTC() } if filter.Open != nil { params.FilterOpen = true params.Open = *filter.Open } if filter.Unlisted != nil { params.FilterUnlisted = true params.Unlisted = *filter.Unlisted } if filter.Limit <= 0 { params.LimitSize = 1000 } stories, err := q.SelectStories(ctx, params) if err != nil { return nil, err } targetIDs := make([]string, len(stories)) for i, story := range stories { targetIDs[i] = story.ID } tags, err := q.SelectTagsByTargets(ctx, psqlcore.SelectTagsByTargetsParams{ TargetKind: "Story", TargetIds: targetIDs, }) return r.stories(stories, tags), nil } func (r *storyRepository) Insert(ctx context.Context, story models.Story) (*models.Story, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { _ = tx.Rollback() }() q := psqlcore.New(tx) if !r.insertWithIDs || len(story.ID) < 8 { story.ID = generate.StoryID() } if story.UpdatedDate.Before(story.CreatedDate) { story.UpdatedDate = story.CreatedDate } err = q.InsertStory(ctx, psqlcore.InsertStoryParams{ ID: story.ID, Author: story.Author, Name: story.Name, Category: string(story.Category), Open: story.Open, Listed: story.Listed, SortByFictionalDate: story.SortByFictionalDate, CreatedDate: story.CreatedDate.UTC(), FictionalDate: story.FictionalDate.UTC(), UpdatedDate: story.UpdatedDate.UTC(), }) if err != nil { return nil, err } // This is inefficient, but tags are very rarely added before the story is submitted. err = q.SetTags(ctx, psqlcore.SetTagsParams{ Tags: models.EncodeTagArray(story.Tags), TargetKind: "Story", TargetID: story.ID, }) if err != nil { return nil, err } return &story, tx.Commit() } func (r *storyRepository) Update(ctx context.Context, story models.Story, update models.StoryUpdate) (*models.Story, error) { story.ApplyUpdate(update) err := psqlcore.New(r.db).UpdateStory(ctx, psqlcore.UpdateStoryParams{ Name: story.Name, Category: string(story.Category), Author: story.Author, Open: story.Open, Listed: story.Listed, FictionalDate: story.FictionalDate.UTC(), UpdatedDate: story.UpdatedDate.UTC(), SortByFictionalDate: story.SortByFictionalDate, ID: story.ID, }) if err != nil { return nil, err } return &story, nil } func (r *storyRepository) AddTag(ctx context.Context, story models.Story, tag models.Tag) error { return psqlcore.New(r.db).SetTag(ctx, psqlcore.SetTagParams{ TargetKind: "Story", TargetID: story.ID, Tag: tag.String(), }) } func (r *storyRepository) RemoveTag(ctx context.Context, story models.Story, tag models.Tag) error { return psqlcore.New(r.db).ClearTag(ctx, psqlcore.ClearTagParams{ TargetKind: "Story", TargetID: story.ID, Tag: tag.String(), }) } func (r *storyRepository) Delete(ctx context.Context, story models.Story) error { 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{ TargetKind: "Story", TargetID: story.ID, }) if err != nil { return err } 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 { storyTags := make([]models.Tag, 0, 8) for _, tag := range tags { if tag.TargetKind == "Story" && tag.TargetID == story.ID { newTag := models.Tag{} err := newTag.Decode(tag.Tag) if err != nil { continue } storyTags = append(storyTags, newTag) } } return &models.Story{ ID: story.ID, Author: story.Author, Name: story.Name, Category: models.StoryCategory(story.Category), Open: story.Open, Listed: story.Listed, Tags: storyTags, CreatedDate: story.CreatedDate, FictionalDate: story.FictionalDate, UpdatedDate: story.UpdatedDate, SortByFictionalDate: story.SortByFictionalDate, } } func (r *storyRepository) stories(stories []psqlcore.Story, tags []psqlcore.CommonTag) []*models.Story { results := make([]*models.Story, 0, len(stories)) for _, story := range stories { results = append(results, r.story(story, tags)) } return results }