feed_query_builder.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // Copyright 2021 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package storage // import "miniflux.app/storage"
  5. import (
  6. "database/sql"
  7. "fmt"
  8. "strings"
  9. "miniflux.app/model"
  10. "miniflux.app/timezone"
  11. )
  12. // FeedQueryBuilder builds a SQL query to fetch feeds.
  13. type FeedQueryBuilder struct {
  14. store *Storage
  15. args []interface{}
  16. conditions []string
  17. order string
  18. direction string
  19. limit int
  20. offset int
  21. withCounters bool
  22. counterJoinFeeds bool
  23. counterArgs []interface{}
  24. counterConditions []string
  25. }
  26. // NewFeedQueryBuilder returns a new FeedQueryBuilder.
  27. func NewFeedQueryBuilder(store *Storage, userID int64) *FeedQueryBuilder {
  28. return &FeedQueryBuilder{
  29. store: store,
  30. args: []interface{}{userID},
  31. conditions: []string{"f.user_id = $1"},
  32. counterArgs: []interface{}{userID, model.EntryStatusRead, model.EntryStatusUnread},
  33. counterConditions: []string{"e.user_id = $1", "e.status IN ($2, $3)"},
  34. }
  35. }
  36. // WithCategoryID filter by category ID.
  37. func (f *FeedQueryBuilder) WithCategoryID(categoryID int64) *FeedQueryBuilder {
  38. if categoryID > 0 {
  39. f.conditions = append(f.conditions, fmt.Sprintf("f.category_id = $%d", len(f.args)+1))
  40. f.args = append(f.args, categoryID)
  41. f.counterConditions = append(f.counterConditions, fmt.Sprintf("f.category_id = $%d", len(f.counterArgs)+1))
  42. f.counterArgs = append(f.counterArgs, categoryID)
  43. f.counterJoinFeeds = true
  44. }
  45. return f
  46. }
  47. // WithFeedID filter by feed ID.
  48. func (f *FeedQueryBuilder) WithFeedID(feedID int64) *FeedQueryBuilder {
  49. if feedID > 0 {
  50. f.conditions = append(f.conditions, fmt.Sprintf("f.id = $%d", len(f.args)+1))
  51. f.args = append(f.args, feedID)
  52. }
  53. return f
  54. }
  55. // WithCounters let the builder return feeds with counters of statuses of entries.
  56. func (f *FeedQueryBuilder) WithCounters() *FeedQueryBuilder {
  57. f.withCounters = true
  58. return f
  59. }
  60. // WithOrder set the sorting order.
  61. func (f *FeedQueryBuilder) WithOrder(order string) *FeedQueryBuilder {
  62. f.order = order
  63. return f
  64. }
  65. // WithDirection set the sorting direction.
  66. func (f *FeedQueryBuilder) WithDirection(direction string) *FeedQueryBuilder {
  67. f.direction = direction
  68. return f
  69. }
  70. // WithLimit set the limit.
  71. func (f *FeedQueryBuilder) WithLimit(limit int) *FeedQueryBuilder {
  72. f.limit = limit
  73. return f
  74. }
  75. // WithOffset set the offset.
  76. func (f *FeedQueryBuilder) WithOffset(offset int) *FeedQueryBuilder {
  77. f.offset = offset
  78. return f
  79. }
  80. func (f *FeedQueryBuilder) buildCondition() string {
  81. return strings.Join(f.conditions, " AND ")
  82. }
  83. func (f *FeedQueryBuilder) buildCounterCondition() string {
  84. return strings.Join(f.counterConditions, " AND ")
  85. }
  86. func (f *FeedQueryBuilder) buildSorting() string {
  87. var parts []string
  88. if f.order != "" {
  89. parts = append(parts, fmt.Sprintf(`ORDER BY %s`, f.order))
  90. }
  91. if f.direction != "" {
  92. parts = append(parts, f.direction)
  93. }
  94. if len(parts) > 0 {
  95. parts = append(parts, ", lower(f.title) ASC")
  96. }
  97. if f.limit > 0 {
  98. parts = append(parts, fmt.Sprintf(`LIMIT %d`, f.limit))
  99. }
  100. if f.offset > 0 {
  101. parts = append(parts, fmt.Sprintf(`OFFSET %d`, f.offset))
  102. }
  103. return strings.Join(parts, " ")
  104. }
  105. // GetFeed returns a single feed that match the condition.
  106. func (f *FeedQueryBuilder) GetFeed() (*model.Feed, error) {
  107. f.limit = 1
  108. feeds, err := f.GetFeeds()
  109. if err != nil {
  110. return nil, err
  111. }
  112. if len(feeds) != 1 {
  113. return nil, nil
  114. }
  115. return feeds[0], nil
  116. }
  117. // GetFeeds returns a list of feeds that match the condition.
  118. func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
  119. var query = `
  120. SELECT
  121. f.id,
  122. f.feed_url,
  123. f.site_url,
  124. f.title,
  125. f.etag_header,
  126. f.last_modified_header,
  127. f.user_id,
  128. f.checked_at at time zone u.timezone,
  129. f.parsing_error_count,
  130. f.parsing_error_msg,
  131. f.scraper_rules,
  132. f.rewrite_rules,
  133. f.blocklist_rules,
  134. f.keeplist_rules,
  135. f.url_rewrite_rules,
  136. f.crawler,
  137. f.user_agent,
  138. f.cookie,
  139. f.username,
  140. f.password,
  141. f.ignore_http_cache,
  142. f.allow_self_signed_certificates,
  143. f.fetch_via_proxy,
  144. f.disabled,
  145. f.hide_globally,
  146. f.category_id,
  147. c.title as category_title,
  148. c.hide_globally as category_hidden,
  149. fi.icon_id,
  150. u.timezone
  151. FROM
  152. feeds f
  153. LEFT JOIN
  154. categories c ON c.id=f.category_id
  155. LEFT JOIN
  156. feed_icons fi ON fi.feed_id=f.id
  157. LEFT JOIN
  158. users u ON u.id=f.user_id
  159. WHERE %s
  160. %s
  161. `
  162. query = fmt.Sprintf(query, f.buildCondition(), f.buildSorting())
  163. rows, err := f.store.db.Query(query, f.args...)
  164. if err != nil {
  165. return nil, fmt.Errorf(`store: unable to fetch feeds: %w`, err)
  166. }
  167. defer rows.Close()
  168. readCounters, unreadCounters, err := f.fetchFeedCounter()
  169. if err != nil {
  170. return nil, err
  171. }
  172. feeds := make(model.Feeds, 0)
  173. for rows.Next() {
  174. var feed model.Feed
  175. var iconID sql.NullInt64
  176. var tz string
  177. feed.Category = &model.Category{}
  178. err := rows.Scan(
  179. &feed.ID,
  180. &feed.FeedURL,
  181. &feed.SiteURL,
  182. &feed.Title,
  183. &feed.EtagHeader,
  184. &feed.LastModifiedHeader,
  185. &feed.UserID,
  186. &feed.CheckedAt,
  187. &feed.ParsingErrorCount,
  188. &feed.ParsingErrorMsg,
  189. &feed.ScraperRules,
  190. &feed.RewriteRules,
  191. &feed.BlocklistRules,
  192. &feed.KeeplistRules,
  193. &feed.UrlRewriteRules,
  194. &feed.Crawler,
  195. &feed.UserAgent,
  196. &feed.Cookie,
  197. &feed.Username,
  198. &feed.Password,
  199. &feed.IgnoreHTTPCache,
  200. &feed.AllowSelfSignedCertificates,
  201. &feed.FetchViaProxy,
  202. &feed.Disabled,
  203. &feed.HideGlobally,
  204. &feed.Category.ID,
  205. &feed.Category.Title,
  206. &feed.Category.HideGlobally,
  207. &iconID,
  208. &tz,
  209. )
  210. if err != nil {
  211. return nil, fmt.Errorf(`store: unable to fetch feeds row: %w`, err)
  212. }
  213. if iconID.Valid {
  214. feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.Int64}
  215. } else {
  216. feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: 0}
  217. }
  218. if readCounters != nil {
  219. if count, found := readCounters[feed.ID]; found {
  220. feed.ReadCount = count
  221. }
  222. }
  223. if unreadCounters != nil {
  224. if count, found := unreadCounters[feed.ID]; found {
  225. feed.UnreadCount = count
  226. }
  227. }
  228. feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
  229. feed.Category.UserID = feed.UserID
  230. feeds = append(feeds, &feed)
  231. }
  232. return feeds, nil
  233. }
  234. func (f *FeedQueryBuilder) fetchFeedCounter() (unreadCounters map[int64]int, readCounters map[int64]int, err error) {
  235. if !f.withCounters {
  236. return nil, nil, nil
  237. }
  238. query := `
  239. SELECT
  240. e.feed_id,
  241. e.status,
  242. count(*)
  243. FROM
  244. entries e
  245. %s
  246. WHERE
  247. %s
  248. GROUP BY
  249. e.feed_id, e.status
  250. `
  251. join := ""
  252. if f.counterJoinFeeds {
  253. join = "LEFT JOIN feeds f ON f.id=e.feed_id"
  254. }
  255. query = fmt.Sprintf(query, join, f.buildCounterCondition())
  256. rows, err := f.store.db.Query(query, f.counterArgs...)
  257. if err != nil {
  258. return nil, nil, fmt.Errorf(`store: unable to fetch feed counts: %w`, err)
  259. }
  260. defer rows.Close()
  261. readCounters = make(map[int64]int)
  262. unreadCounters = make(map[int64]int)
  263. for rows.Next() {
  264. var feedID int64
  265. var status string
  266. var count int
  267. if err := rows.Scan(&feedID, &status, &count); err != nil {
  268. return nil, nil, fmt.Errorf(`store: unable to fetch feed counter row: %w`, err)
  269. }
  270. if status == model.EntryStatusRead {
  271. readCounters[feedID] = count
  272. } else if status == model.EntryStatusUnread {
  273. unreadCounters[feedID] = count
  274. }
  275. }
  276. return readCounters, unreadCounters, nil
  277. }