refac: migration crash, real-database tests, site deleted during create, ruels out of the repo, lists
This commit is contained in:
1 parent
680b9a1308
commit
9d284e65a9
62 files changed
+1858
-987
No files matched your search
@@ -44,42 +44,76 @@ func lacksReadableVersionIn(languageExpression string, filters domain.StoryFilte
|
||||
return condition
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) versionRestriction(filters domain.StoryFilters) ([]uint32, bool, error) {
|
||||
restricted := filters.VersionIDs != nil
|
||||
var allowed []uint32
|
||||
if restricted {
|
||||
allowed = *filters.VersionIDs
|
||||
}
|
||||
query := strings.TrimSpace(filters.Q)
|
||||
if query == "" {
|
||||
return allowed, restricted, nil
|
||||
}
|
||||
func (a *ArticleRepository) FindTitleMatches(q string, filters domain.StoryFilters) ([]uint32, error) {
|
||||
readable := readableCondition("q", filters)
|
||||
if filters.SiteID != nil {
|
||||
readable.add("q.site_id = ?", *filters.SiteID)
|
||||
}
|
||||
var candidates []entity.Article
|
||||
if err := a.db.Raw("SELECT q.id AS id, q.title AS title FROM articles q WHERE "+readable.where(), readable.args...).Scan(&candidates).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
needle := foldForSearch(query)
|
||||
permitted := map[uint32]bool{}
|
||||
for _, id := range allowed {
|
||||
permitted[id] = true
|
||||
return nil, err
|
||||
}
|
||||
needle := foldForSearch(strings.TrimSpace(q))
|
||||
matching := make([]uint32, 0)
|
||||
for _, candidate := range candidates {
|
||||
if restricted && !permitted[candidate.ID] {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(foldForSearch(candidate.Title), needle) {
|
||||
matching = append(matching, candidate.ID)
|
||||
}
|
||||
}
|
||||
return matching, true, nil
|
||||
return matching, nil
|
||||
}
|
||||
|
||||
func qualifyingVersions(filters domain.StoryFilters, allowed []uint32, restricted bool) storySQL {
|
||||
type versionSet map[uint32]bool
|
||||
|
||||
func restrictionOf(filters domain.StoryFilters) versionSet {
|
||||
if filters.VersionIDs == nil {
|
||||
return nil
|
||||
}
|
||||
set := make(versionSet, len(*filters.VersionIDs))
|
||||
for _, id := range *filters.VersionIDs {
|
||||
set[id] = true
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (set versionSet) keep(rows []storyRefRow) []storyRefRow {
|
||||
kept := make([]storyRefRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if set[row.ArticleID] {
|
||||
kept = append(kept, row)
|
||||
}
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
||||
func storiesOf(rows []storyRefRow) map[uint32]bool {
|
||||
stories := make(map[uint32]bool, len(rows))
|
||||
for _, row := range rows {
|
||||
stories[row.StoryID] = true
|
||||
}
|
||||
return stories
|
||||
}
|
||||
|
||||
func unitsIn(filters domain.StoryFilters, rows []storyRefRow) int {
|
||||
if filters.PerLanguage() {
|
||||
return len(rows)
|
||||
}
|
||||
return len(storiesOf(rows))
|
||||
}
|
||||
|
||||
func pageOf(rows []storyRefRow, offset int, size int) []storyRefRow {
|
||||
start := min(offset, len(rows))
|
||||
return rows[start:min(start+size, len(rows))]
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) qualifyingRefs(filters domain.StoryFilters, restriction versionSet) ([]storyRefRow, error) {
|
||||
qualifying := qualifyingVersions(filters)
|
||||
var rows []storyRefRow
|
||||
err := a.db.Raw("SELECT q.story_id AS story_id, q.id AS article_id FROM articles q WHERE "+qualifying.where(), qualifying.args...).Scan(&rows).Error
|
||||
return restriction.keep(rows), err
|
||||
}
|
||||
|
||||
func qualifyingVersions(filters domain.StoryFilters) storySQL {
|
||||
condition := readableCondition("q", filters)
|
||||
if filters.SiteID != nil {
|
||||
condition.add("q.site_id = ?", *filters.SiteID)
|
||||
@@ -96,13 +130,6 @@ func qualifyingVersions(filters domain.StoryFilters, allowed []uint32, restricte
|
||||
if filters.AuthorID != nil {
|
||||
condition.add("q.id IN (SELECT aa.article_id FROM article_authors aa WHERE aa.author_id = ?)", *filters.AuthorID)
|
||||
}
|
||||
if restricted {
|
||||
if len(allowed) == 0 {
|
||||
condition.add("1 = 0")
|
||||
} else {
|
||||
condition.add("q.id IN ?", allowed)
|
||||
}
|
||||
}
|
||||
if filters.Missing {
|
||||
condition.merge(lacksReadableVersionIn("?", filters, ""))
|
||||
condition.merge(missingCondition(filters))
|
||||
@@ -129,8 +156,12 @@ func unitExpression(filters domain.StoryFilters) string {
|
||||
return "COUNT(DISTINCT q.story_id)"
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) countUnits(filters domain.StoryFilters, allowed []uint32, restricted bool) (int, error) {
|
||||
qualifying := qualifyingVersions(filters, allowed, restricted)
|
||||
func (a *ArticleRepository) countUnits(filters domain.StoryFilters, restriction versionSet) (int, error) {
|
||||
if restriction != nil {
|
||||
rows, err := a.qualifyingRefs(filters, restriction)
|
||||
return unitsIn(filters, rows), err
|
||||
}
|
||||
qualifying := qualifyingVersions(filters)
|
||||
var total int64
|
||||
err := a.db.Raw("SELECT "+unitExpression(filters)+" FROM articles q WHERE "+qualifying.where(), qualifying.args...).Scan(&total).Error
|
||||
return int(total), err
|
||||
@@ -175,11 +206,8 @@ func toStoryRefs(rows []storyRefRow) []domain.StoryRef {
|
||||
func (a *ArticleRepository) FindStoryPage(filters domain.StoryFilters) (domain.StoryPage, error) {
|
||||
filters.Page = max(filters.Page, 1)
|
||||
filters.Size = max(filters.Size, 0)
|
||||
allowed, restricted, err := a.versionRestriction(filters)
|
||||
if err != nil {
|
||||
return domain.StoryPage{}, err
|
||||
}
|
||||
total, err := a.countUnits(filters, allowed, restricted)
|
||||
restriction := restrictionOf(filters)
|
||||
total, err := a.countUnits(filters, restriction)
|
||||
if err != nil {
|
||||
return domain.StoryPage{}, err
|
||||
}
|
||||
@@ -187,16 +215,13 @@ func (a *ArticleRepository) FindStoryPage(filters domain.StoryFilters) (domain.S
|
||||
if total == 0 {
|
||||
return page, nil
|
||||
}
|
||||
qualifying := qualifyingVersions(filters, allowed, restricted)
|
||||
offset := (filters.Page - 1) * filters.Size
|
||||
var rows []storyRefRow
|
||||
if filters.PerLanguage() {
|
||||
query := "SELECT q.story_id AS story_id, q.id AS article_id FROM articles q WHERE " + qualifying.where() +
|
||||
" ORDER BY " + storyOrderClause(versionRowOrder, filters.Sort, "q.story_id") + " LIMIT ? OFFSET ?"
|
||||
err = a.db.Raw(query, append(qualifying.args, filters.Size, offset)...).Scan(&rows).Error
|
||||
if restriction != nil {
|
||||
rows, err = a.restrictedStoryRows(filters, restriction)
|
||||
rows = pageOf(rows, offset, filters.Size)
|
||||
} else {
|
||||
query, args := storyRowsQuery(filters, qualifying)
|
||||
err = a.db.Raw(query, append(args, filters.Size, offset)...).Scan(&rows).Error
|
||||
rows, err = a.storyRows(filters, " LIMIT ? OFFSET ?", filters.Size, offset)
|
||||
}
|
||||
if err != nil {
|
||||
return domain.StoryPage{}, err
|
||||
@@ -205,6 +230,44 @@ func (a *ArticleRepository) FindStoryPage(filters domain.StoryFilters) (domain.S
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) storyRows(filters domain.StoryFilters, suffix string, extra ...any) ([]storyRefRow, error) {
|
||||
qualifying := qualifyingVersions(filters)
|
||||
var query string
|
||||
var args []any
|
||||
if filters.PerLanguage() {
|
||||
query = "SELECT q.story_id AS story_id, q.id AS article_id FROM articles q WHERE " + qualifying.where() +
|
||||
" ORDER BY " + storyOrderClause(versionRowOrder, filters.Sort, "q.story_id")
|
||||
args = qualifying.args
|
||||
} else {
|
||||
query, args = storyRowsQuery(filters, qualifying)
|
||||
}
|
||||
var rows []storyRefRow
|
||||
err := a.db.Raw(query+suffix, append(args, extra...)...).Scan(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) restrictedStoryRows(filters domain.StoryFilters, restriction versionSet) ([]storyRefRow, error) {
|
||||
ordered, err := a.storyRows(filters, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filters.PerLanguage() {
|
||||
return restriction.keep(ordered), nil
|
||||
}
|
||||
qualifying, err := a.qualifyingRefs(filters, restriction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stories := storiesOf(qualifying)
|
||||
kept := make([]storyRefRow, 0, len(stories))
|
||||
for _, row := range ordered {
|
||||
if stories[row.StoryID] {
|
||||
kept = append(kept, row)
|
||||
}
|
||||
}
|
||||
return kept, nil
|
||||
}
|
||||
|
||||
func storyRowsQuery(filters domain.StoryFilters, qualifying storySQL) (string, []any) {
|
||||
readable := readableCondition("a", filters)
|
||||
args := []any{true}
|
||||
@@ -218,17 +281,15 @@ func storyRowsQuery(filters domain.StoryFilters, qualifying storySQL) (string, [
|
||||
"ROW_NUMBER() OVER (PARTITION BY a.story_id ORDER BY CASE WHEN d.lang IS NULL THEN 1 ELSE 0 END, a.lang, a.id) AS row_rank " +
|
||||
"FROM articles a LEFT JOIN site_languages d ON d.site_id = a.site_id AND d.lang = a.lang AND d.is_default = ? " +
|
||||
"WHERE " + readable.where() + " AND a.story_id IN (SELECT q.story_id FROM articles q WHERE " + qualifying.where() + ")" +
|
||||
") r WHERE r.row_rank = 1) s ORDER BY " + storyOrderClause(storyRowOrder, filters.Sort, "s.story_id") + " LIMIT ? OFFSET ?"
|
||||
") r WHERE r.row_rank = 1) s ORDER BY " + storyOrderClause(storyRowOrder, filters.Sort, "s.story_id")
|
||||
return query, args
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) CountStories(filters domain.StoryFilters) (domain.StoryCounts, error) {
|
||||
filters.Status = ""
|
||||
allowed, restricted, err := a.versionRestriction(filters)
|
||||
if err != nil {
|
||||
return domain.StoryCounts{}, err
|
||||
}
|
||||
restriction := restrictionOf(filters)
|
||||
counts := domain.StoryCounts{}
|
||||
var err error
|
||||
for _, target := range []struct {
|
||||
status string
|
||||
into *int
|
||||
@@ -236,7 +297,7 @@ func (a *ArticleRepository) CountStories(filters domain.StoryFilters) (domain.St
|
||||
scoped := filters
|
||||
scoped.Missing = false
|
||||
scoped.Status = target.status
|
||||
if *target.into, err = a.countUnits(scoped, allowed, restricted); err != nil {
|
||||
if *target.into, err = a.countUnits(scoped, restriction); err != nil {
|
||||
return domain.StoryCounts{}, err
|
||||
}
|
||||
}
|
||||
@@ -245,15 +306,22 @@ func (a *ArticleRepository) CountStories(filters domain.StoryFilters) (domain.St
|
||||
}
|
||||
missing := filters
|
||||
missing.Missing = true
|
||||
counts.Missing, err = a.countUnits(missing, allowed, restricted)
|
||||
counts.Missing, err = a.countUnits(missing, restriction)
|
||||
return counts, err
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) ListByStories(storyIDs []uint32) ([]domain.Article, error) {
|
||||
func (a *ArticleRepository) ListByStories(storyIDs []uint32, filters domain.StoryFilters) ([]domain.Article, error) {
|
||||
var found []entity.Article
|
||||
for _, chunk := range chunkIDs(storyIDs) {
|
||||
var loaded []entity.Article
|
||||
if err := a.db.Model(&entity.Article{}).Where("story_id IN ?", chunk).Order("id ASC").Find(&loaded).Error; err != nil {
|
||||
query := a.db.Model(&entity.Article{}).Omit("body").Where("story_id IN ?", chunk)
|
||||
if filters.OnlyOnline {
|
||||
query = query.Where("is_online = ?", true)
|
||||
}
|
||||
if filters.VisibleTo != nil {
|
||||
query = query.Where("(is_online = ? OR created_by = ?)", true, *filters.VisibleTo)
|
||||
}
|
||||
if err := query.Order("id ASC").Find(&loaded).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
found = append(found, loaded...)
|
||||
|
||||
Reference in new issue
Block a user